mu/log: a Clojure library that logs events instead of words
μ/log is a micro-logging library that logs events and data, not words!
At a glance
- What is it?
- mu/log is a Clojure micro-logging library built on the idea that a log entry should be structured data rather than a formatted string, so that events can be indexed, filtered and aggregated without parsing text. Events are published asynchronously, nothing appears until you start a publisher, and the repository ships adapters for Kafka, CloudWatch, Prometheus, OpenTelemetry and more.
- Who is it for?
- Adopt mu/log if you are writing Clojure services whose logs are collected centrally and whose data has to be queried rather than grepped, and you want to drop the three pillars of observability for a single event stream. Do not adopt it if you need human-readable log lines, a mature logging framework's configuration surface, or per-message formatting control, because the library is explicitly not a logging system in the Log4j sense.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 118 days ago.
- What is it written in?
- Mainly Clojure, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Events as the unit, not messages
mu/log describes itself as a micro-logging library that logs events and data, not words. That single sentence is the whole design, and the README spends most of its motivation section on why.
The argument runs like this. In the projects the author worked on, logging text messages produced a large amount of strings that were hard to make sense of and therefore mostly ignored. The library's idea is to replace the three pillars of observability with a more fundamental concept, the event. Event-based data is easy to index, search, augment, aggregate and visualise, and that is the argument for treating it as a replacement for traditional logs, metrics and traces.
The historical reason is given too. Existing logging libraries follow a design from the eighties and early nineties, when systems ran on standalone servers and the main use of logging was printing to console or file for debugging. Modern systems are distributed in virtualized machines in the cloud, where a machine can disappear at any time, so local file logs are easily lost, which is why log collectors and centralised processors became the norm. The ELK stack has been predominant in that space, alongside many commercial and open source products, and all of them have to deal with unstructured data represented as formatted strings. Extracting information from those strings is tedious, error prone and, as the README puts it, not fun.
The library is aimed at a Clojure developer whose data lands in a centralised aggregator and who wants to query it rather than read it.
The name is the Greek letter mu, which is a prefix for micro, and the library is pronounced accordingly.
Adding the dependency and the namespace alias
The library is a normal Clojure dependency, and the README gives the coordinates for both build tools, Leiningen and deps.edn:
[com.brunobonacci/mulog "0.10.1"]
;; deps.edn format
{:deps { com.brunobonacci/mulog {:mvn/version "0.10.1"}}}Then you require the namespace, and you have a choice about how to alias it. The Greek letter is the default, and an ASCII alternative is offered for anyone who finds that inconvenient:
(ns your-ns
(:require [com.brunobonacci.mulog :as μ]))
;; or for the more ASCII traditionalists
(ns your-ns
(:require [com.brunobonacci.mulog :as u]))Note that the namespace is com.brunobonacci.mulog while the artifact is com.brunobonacci/mulog, so the alias and the artifact name are not the same string, which is one small thing to get right when you are copying from a blog post.
The package is published on Clojars and documented on cljdoc, and the README links the online documentation as the place to look for the full API. The releases are 0.10.0 on 2025-12-11 and 0.10.1 on 2026-02-06, and the last push to the repository was on 2026-06-27.
Logging an event and seeing it in the REPL
The call form is a function with an event name followed by as many key and value pairs as you want:
(μ/log event-name, key1 value1, key2 value2, ... keyN valueN)In practice, and the README recommends it, the event name should be a namespaced keyword:
;; good to use namespaced keywords for the event-name
(μ/log ::hello :to "New World!")The examples in the documentation show the range of shapes you can express. A system start event carries a version and an init time, a user login event carries a user id, a remote IP and an auth method, an HTTP request event carries a path, a method, a remote IP, a status and a request time, and an invalid request event carries the exception object itself alongside a user id and a count.
The key detail is what happens next. The README is emphatic that you will not see any events until you add a publisher, because a publisher is what takes the events and sends them to a distributed logger or to your local console while you are developing:
(μ/start-publisher! {:type :console})Once that is running, the event you logged shows up in the REPL as a map. The documented output carries a trace id, a timestamp, the event name, the namespace it came from and your own key:
{:mulog/trace-id #mulog/flake "4VTBeu2scrIEMle9us8StnmvRrj9ThWP", :mulog/timestamp 1587500402972, :mulog/event-name :your-ns/hello, :mulog/namespace "your-ns", :to "New World!"}That map is the point. The trace id and timestamp are added by the library, and everything else is the data you supplied, so what reaches your aggregator is queryable rather than a line of text with a timestamp in front of it.
Asynchronous publishing, bounded memory, and dropping events
The feature list gives the performance and safety characteristics, and the interesting one is a design decision stated as a policy.
Logging is fast, under 300 nanoseconds per log entry according to the README, and memory bound with no unbounded use of memory. All the processing and rendering happens asynchronously, which is what keeps the log call itself cheap. Adding publishers does not affect logging performance, so the cost of the log call does not grow with the number of destinations you ship to.
Contextual logging is supported, so you can attach ambient information to the events of a request without repeating it at every call site.
The policy line is the one worth dwelling on. Event logs are useful, but not as important as process flow, therefore it is preferable to drop events rather than crash the process. That is a deliberate choice about what a logging library is allowed to do to the program it observes. Most logging frameworks will happily propagate an exception from a failed appender, and the classical advice is to wrap your logging calls defensively. mu/log takes the opposite position: the log path must never be the reason the process dies.
The README connects this to a habit, that because it is cheap to log events you can freely log plenty. The other side of that is stated directly, that events are just data so you can process, enrich, filter, aggregate and visualise them with your own tools. The library does not try to be the query engine or the dashboard.
One more design goal listed is that it is extremely easy to create stateful publishers for new systems, which is what makes the wide publisher list extensible rather than closed.
A repository that is really a set of publishers
The top-level repository listing is unusual for a Clojure library, and it explains the shape of the project. Alongside mulog-core there are mulog-adv-console, mulog-adv-file, mulog-cloudwatch, mulog-elasticsearch, mulog-filesystem-metrics, mulog-json, mulog-jvm-metrics, mulog-kafka, mulog-kinesis, mulog-mbean-sampler, mulog-opentelemetry, mulog-prometheus, mulog-slack and mulog-zipkin, plus a ver/ directory, a doc/ directory, an examples/ directory and a scripts/ directory.
So the core is the event model and the publisher interface, and everything else is a destination. Each of those modules is a separate artifact with its own name, which means you depend on the core plus the one or two publishers you actually use rather than on a single fat jar.
The spread of destinations is itself the design statement. Kafka and Kinesis for streaming into a log pipeline, Elasticsearch for search, CloudWatch for AWS environments, Prometheus and filesystem-metrics for metric-style output, OpenTelemetry and Zipkin for tracing, Slack for chat notifications, the two adv modules for console and file output during development, and a JSON module for plain structured output. There is also a jvm-metrics module and an mbean-sampler, which is how the library covers the metrics side of what it claims to replace.
The examples directory contains a project called roads-disruptions, which is a worked example rather than a toy snippet, and the ver/ directory is for version-specific work. The CHANGELOG.md at the root is where the release history lives.
The trade-off of giving up log messages
The README is clear that mu/log is not intended to be a logging system in the sense of Log4j and its relatives, and that boundary is where the costs of the approach show up.
The first cost is human readability. A structured event map is excellent for a query engine and poor for a developer tailing a log file at 2am. There is no documented mechanism in the README for formatting an event into a message, no pattern layout, no level-based filtering with a severity threshold in the conventional sense. If part of your team reads logs as prose, mu/log serves them poorly, and a separate console publisher with its own rendering is the only comfort available.
The second cost is the publisher list is fixed at what exists. The README calls it a wide range of publishers available and points at a list in the documentation, and while creating a new publisher is described as easy, someone still has to write it. If your destination is not in that list, you are writing adapter code and you own its failure modes.
The third cost is that a centrally collected event stream is only as good as the fields you log. Because there is no message to fall back on, an event that omits the key you wanted to group by is not recoverable later. The discipline falls on the developer at every call site rather than on a logging configuration file.
There is also the trace id to think about, since the library attaches one to every event. The README shows the trace id as a flake type, which suggests correlation ids are part of the model rather than an add-on, but the relationship between that id and a trace system is not documented in the README.
Editorial conclusion
Adopt mu/log if you are writing Clojure services whose logs are collected centrally and whose data has to be queried rather than grepped, and you want to drop the three pillars of observability for a single event stream. Do not adopt it if you need human-readable log lines, a mature logging framework's configuration surface, or per-message formatting control, because the library is explicitly not a logging system in the Log4j sense. Verify first that the publisher for your backend exists among the modules in the repository, that the event names you use are namespaced keywords, and that the latency claim in the feature list, under 300 nanoseconds per log entry, still holds for the payload sizes you intend to log.
Frequently asked questions
What is mu/log and how is it different from a normal logging library?
mu/log is a Clojure micro-logging library that logs events and data rather than words, so an entry is a map with an event name, key and value pairs, a trace id and a timestamp. The README states it is not intended to be a logging system in the sense of Log4j, and its design is aimed at dynamic distributed systems using centralised log aggregators.
How do I add mu/log to a Clojure project?
Add the dependency to your project.clj as [com.brunobonacci/mulog "0.10.1"] for Leiningen, or add com.brunobonacci/mulog with mvn/version "0.10.1" to the deps map in deps.edn. Then require the namespace com.brunobonacci.mulog, aliased as μ or as u for ASCII.
Why do I not see any events after calling mu/log?
Because no publisher is running. The README states you will not see any events until you add a publisher, and the console publisher is started with (μ/start-publisher! {:type :console}), which sends the events to your local terminal while you develop.
Which backends can mu/log publish to?
The repository contains separate modules for the console and file advanced publishers, CloudWatch, Elasticsearch, JSON output, filesystem metrics, JVM metrics, Kafka, Kinesis, an MBean sampler, OpenTelemetry, Prometheus, Slack and Zipkin, each of which is a distinct artifact alongside the mulog-core library.
What happens if a mu/log publisher fails?
The README states the policy directly, that event logs are useful but not as important as process flow, therefore it is preferable to drop events rather than crash the process. All processing and rendering happens asynchronously, and the library is described as memory bound with no unbounded use of memory.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/brunobonacci-mulog)Was this useful?