{"id":15691160,"url":"https://github.com/lukaszkorecki/mokujin","last_synced_at":"2025-10-06T13:08:03.939Z","repository":{"id":206885732,"uuid":"712016056","full_name":"lukaszkorecki/mokujin","owner":"lukaszkorecki","description":"Structured logging for Clojure. Thin layer on top of clojure.tools.logging with MDC support","archived":false,"fork":false,"pushed_at":"2025-05-06T14:29:39.000Z","size":146,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-08T00:52:43.840Z","etag":null,"topics":["clojure","clojure-library","log4j2","logback","logging","logging-library"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lukaszkorecki.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-10-30T16:15:57.000Z","updated_at":"2025-05-06T23:01:29.000Z","dependencies_parsed_at":"2023-11-27T23:31:01.229Z","dependency_job_id":"ce1e0dd5-394a-43b1-920f-3e861359826f","html_url":"https://github.com/lukaszkorecki/mokujin","commit_stats":null,"previous_names":["lukaszkorecki/mokujin"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukaszkorecki%2Fmokujin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukaszkorecki%2Fmokujin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukaszkorecki%2Fmokujin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukaszkorecki%2Fmokujin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lukaszkorecki","download_url":"https://codeload.github.com/lukaszkorecki/mokujin/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252978724,"owners_count":21834914,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojure-library","log4j2","logback","logging","logging-library"],"created_at":"2024-10-03T18:20:31.050Z","updated_at":"2025-10-06T13:08:03.830Z","avatar_url":"https://github.com/lukaszkorecki.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mokujin\n\n\u003cimg src=\"https://static.wikia.nocookie.net/topstrongest/images/1/15/Mokujin_TTT2.png/revision/latest/scale-to-width-down/1000?cb=20200503180655\" align=\"right\" height=\"250\" /\u003e\n\n\u003e Mokujin (木人 Wooden person?) is a character in the Tekken series of fighting games. It is a spiritually sensitive animated training dummy,\n\u003e and serves as a guardian of good against supernatural evil threats.\n\u003e Mokujin is made of logs, hence the name.\n\n[![Clojars Project](https://img.shields.io/clojars/v/org.clojars.lukaszkorecki/mokujin.svg)](https://clojars.org/org.clojars.lukaszkorecki/mokujin)\n[![Clojars Project](https://img.shields.io/clojars/v/org.clojars.lukaszkorecki/mokujin-logback.svg)](https://clojars.org/org.clojars.lukaszkorecki/mokujin-logback)\n\n\n\u003e [!WARNING]\n\u003e While Mokujin has been used in live, production applications - it's still under *active* development,\n\u003e consider the API stable-ish, with possible small changes here and there. See [the roadmap](#todo) section.\n\n## Just enough (structured) logging\n\n### Quick example\n\n\n```clojure\n;; assuming that log4j2 or logback dependencies \u0026 configs are on the classpath, or you're using mokujin-logback\n(require '[mokujin.log :as log])\n\n\n;; just a message\n(log/info \"hi!\")\n\n;; logging exceptions\n(try\n   ....\n   (catch Exception e\n     (log/error e \"something went wrong\")))\n\n\n;; pass a context map to add structured data to the message\n(log/info \"hi!\" {:foo \"bar\" :baz \"qux\"})\n\n;; context can be set \n(log/with-context {:action \"vibe-check\"}\n  (log/debug \"checking the vibes\")\n  (let [are-you-ok? (do-the-vibe-check)]\n    (if are-you-ok?\n      (log/info \"hi!\")\n      (log/warn \"something is up!\"))))\n```\n\n\n### Rationalle\n\n`clojure.tools.logging` is a Good Enough :tm: solution for majority of logging needs. It's fast, works with any\nlogging backend supported on the JVM and is simple to use.\n\nThe only area where it falls short is producing structured logs.\n\nSidebar: In the past I'd use [`logfmt`-like](https://brandur.org/logfmt) formatting style when using `infof` (and friends) to produce logs.\nThen I'd configure the log ingester (like Fluentd) to parse log lines using this format. That's fine for simple cases, but as soon as\nexceptions and  stack traces got thrown into the mix, I fell into the deep rabbit hole of multi-line log parsers and my sanity never quite made it back.\n\nWhat we want instead is descriptive log messages, with the ability to attach structured data to them.\n\nThis is where [Mapped Diagnostic Context (MDC)](https://logback.qos.ch/manual/mdc.html) comes in. Combined with different logging backends and appenders, we can produce easy to read logs during dev time, and emit logs as data in production.\n\nSecond part of ensuring that right things are logged and we keep a good performance profile is restricting how logging is done, and what information is included in log events:\n\n- prohibit var-arg dispatch\n- only flat maps are allowed in the log context\n- discourage use of `printf` style logging, unless strictly necessary\n\n### How does it work?\n\nMokujin wraps `clojure.tools.logging` and injects SLF4J's MDC into logging events, if provided. \nKeep in mind that `infof` (and friends) variants are present, but do not support passing the MDC (more on that later).\n\n\n#### Performance\n\nWhile effort was made to keep things as efficient as possible, there is some impact\nintroduced by manipulating the MDC object. Typical direct call to `clojure.tools.logging/info` is measured in nano-seconds.\n\nA call to `mokujin.log/info` will have nearly the same performance characteristics. Only when introducing several levels of MDC\nprocessing you can expect a small slow down but still maintain sub-microsecond performance. This is absolutely fine for\nyour typical usage - most of applications running out there do a lot of I/O, where processing times are measured in milliseconds\nor seconds even, so any overhead introduced by logging is negligble.\n\nYou can run the benchmark via `clj -M:benchmark`. Latest results (as of 30/03/2025) run on my M4 MBP Pro are:\n\n```\n#'mokujin.log-bench/mokujin-log : 76.876114 ns\n#'mokujin.log-bench/mokujin-log+context : 346.391248 ns\n#'mokujin.log-bench/tools-logging-log : 78.078607 ns\n#'mokujin.log-bench/tools-logging-log+context : 221.394879 ns\n```\n\n\u003e [!NOTE]\n\u003e Benchmarks are tricky, and always should be taken with a grain of salt.\n\u003e My sole focus with these is to ensure that Mokujin keeps up with `tools.logging` as far as performance is concerned.\n\u003e There's many more variables that need to be taken into account when measuring performance impact of logs in your application.\n\n## API \u0026 Usage\n\n### Full API\n\n```clojure\n(log/info [msg] [msg ctx])\n(log/warn [msg] [msg ctx])\n(log/debug [msg] [msg ctx])\n(log/trace [msg] [msg ctx])\n(log/error [msg] [exc msg] [exc msg ctx])\n(log/infof [fmt \u0026 fmt-args])\n(log/warnf [fmt \u0026 fmt-args])\n(log/debugf [fmt \u0026 fmt-args])\n(log/tracef [fmt \u0026 fmt-args])\n(log/errorf [fmt \u0026 fmt-args])\n(log/with-context [ctx \u0026 body])\n```\n\nJust like `clojure.tools.logging`, Mokujin preserves caller context, and ensures that the right\ninformation (namespace, line numbers) is attached to the log event.\n\nThe API is close enough that Mokujin is *almost* a drop-in replacement for `c.t.logging`, **however** to promote good practices, and\nmaintain good performance logging functions that support format strings e.g. `log/infof` or `log/errorf` **do not support the context map** as input.\n\nThat's because in 99% of the cases where I'd use `log/infof` what I wanted to do was `(log/info \"message\" context)` instead.\nIn cases where you really, really, really want to use formatted strings and the context, this Works Just Fine :tm: :\n\n```clojure\n(log/with-context {:some :ctx}\n  (log/infof \"thing %s happened to %s\" thing-a thing-b))\n```\n\nSecond difference is that only 1- and 2-arity (or in case of `log/error` 3-arity) log functions are suported, so:\n\n```clojure\n;; work's in clojure.tools.logging, but will throw an exception in Mokujin\n\n(log/info \"hello\" \"there\" \"world\")\n```\n\nTo help with migration and good log hygine Mokujin ships with custom hooks for `clj-kondo` and \nreport warnings in case of suspicious or incompatible call styles are detected.\n\n\n### The context\n\n\nThe context is a `Map[String]String` internally. To make it easier to work with standard log ingestion infrastructure,\nall keywords present in the keys and values are converted to `\"snake_case\"` strings. Other value types are stringified.\nThis frees you up from figuring out what can and cannot be serialized by the appender that your logging backend is using.\nIf you need to change the layout of the produced log line, you can and should defer it to the appender configuration instead.\n\n\u003e [!WARNING]\n\u003e Nested maps or other complex data types are not allowed - this is something that might change in the future.\n\n\n#### `with-context`\n\nYou can use `with-context` macro, to set context for a form, request handler body etc.\nContexts can be nested, but see the next section for caveats.\n\nAll map keys and values will be stringified using `name` + `str` combo.\n\n```clojure\n(log/with-context ctx body)\n```\n\nExample:\n\n``` clojure\n(defn handle-notification [user notification]\n  (log/with-context {:user-id (:id user) :notification-type (:type notification)}\n    (let [{:keys [success? response-code] result} (do-something-with-notification user notification)]\n      (log/with-context {:response-code response-code}\n        (if success?\n          (log/info \"success\")\n          (log/error \"problem with notification processing\"))))))\n```\n\n#### Context and threads\n\nMDC is bound to the current thread and won't be propagated to other threads e.g. when dispatching a future or a thread pool task from a Ring handler.\n\n``` clojure\n(log/with-context {:foo \"bar\"}\n  (future\n    (log/info \"hi!\"))) ;; won't have the context\n```\n\n\n\u003e [!INFO]\n\u003e This **might change** in the future, but for now, you're better off passing the context explicitly to the function that needs it.\n\n\nTo work around this, you can use `log/with-context` to wrap the form that needs the context:\n\n\n``` clojure\n(log/with-context {:foo \"bar\"}\n  (let [current-context (log/get-context)])\n  (future\n    (log/with-context current-context\n      (log/info \"hi!\")))) ;; will have the context\n```\n\n\n## Setup\n\nFirst of all you need to include Mokujin as your dependency. Second step is to use a logging backend that supports MDC.\nMost popular choices are Logback and Log4j2. See `examples` directory for both.\n\nOnce you have your logging backend set up, you can start using Mokujin by using `mokujin.log` namespace.\n\n### Migrating from `clojure.tools.logging`\n\nPretty simple, just replace `clojure.tools.logging` with `mokujin.log` in your `ns` declaration and you're mostly there. Run `clj-kondo --lint .` and look for\nwarnings or errors in `:mokujin.log` keyword namespace\n\n### Logback\n\n`mokujin-logback` is a sister library which provides a set of helpers to configure Logback at run time using EDN.\n You can of course keep using your hand-crafted, artisanal XML files if you have them already. Both legacy and new XML formats are supported.\n\nTo make it easy, Mokujin offers a couple of configuration presets for quick setup, which are suitable for most use cases.\nProvided 'presets':\n\n- `:mokujin.logback/text` - plain text appender, will log all standard fields plus MDC\n- `:mokujin.logback/json` - powered by Logstash appender, emits log events as JSON with MDC fields merged in\n- `:mokujin.logback/json-async` - same as above, but uses async appender which buffers events before rednering them in a background thread, useful for high log volumes\n\n```clojure\n;; assuming both mokujin and mokujin-logback are in your classpath\n(require '[mokujin.log :as log]\n         '[mokujin.logback :as logback])\n\n\n;; in your REPL init code\n(mokujin.logback/configure! {:config :mokujin.logback/text})\n\n\n;; in `core` namespace of your application:\n(mokujin.logback/configure! {:config :mokujin.logback/json\n                             ;; you can specify package names or namespaces here to control log levels outside of global setting\n                             :logger-filters {\"org.eclipse.jetty\" \"ERROR\"}})\n\n;; completely custom configuration as EDN:\n(def log-config\n  [[:configuration\n    [:appender\n     {:name \"STDOUT\"\n      :class \"ch.qos.logback.core.ConsoleAppender\"\n      :encoder\n      [:pattern\n       {:pattern \"%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n\"}]}]\n    [:root\n     {:level \"debug\"\n      :appender-ref\n      {:ref \"STDOUT\"}}]]])\n\n(mokujin.logback/configure! {:config log-config})\n\n\n;; XML files (both pre 1.3 and 1.3+ formats are supported) - as long as logback.xml and/or logback-test.xml is in your classpath\n;; they will be loaded automatically, but you can also specify a custom one at run time:\n(mokujin.logback/configure! {:config (io/resource \"logback.xml\")})\n```\n\n\nLogback's configuration system is very powerful, and provides several features, including MDC processing, log rotation, sanitization and more.\nThis way we can delegate things like redacting MDC or async appenders to Logback, and keep Mokujin focused on providing streamlined API.\n\nCheck `:mokujin.logback/json-async` configuration preset for a good example of how to set up async appenders with MDC support.\n\n## TODO\n\n- [x] Maven release\n- [x] Improve performance of adding/removing keys from the MDC - see Cambium's or Aviso-logging source for a good approach\n- [x] Finalize `log/error` API - it works, but there are cases where its usage can be confusing\n- [x] Split library into core and logback-specific components\n- [ ] timing support - some form of `with-timing` macro or an arg on `with-context`\n- [ ] Provide a way to customize how context data is transformed?\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukaszkorecki%2Fmokujin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukaszkorecki%2Fmokujin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukaszkorecki%2Fmokujin/lists"}