logz: printf debugging across processes
2 November 2026
Some bugs only show up when several processes talk to each other: a Go service, a .NET client, a TypeScript UI, maybe a container or two. Each one logs somewhere different, in its own format, with its own clock. By the time I have lined up three terminal windows and a log file, I have forgotten what I was looking for.
What I want in that moment is printf debugging that works across processes: one call that puts a line in one place, from any language, with no setup. logz is that.
Print debugging
Being an old Perl programmer, I learned to be pragmatic. Sometimes it is easier to just watch what a program does, without any infrastructure, using something that is easy to understand and easy to build. There are sophisticated debuggers today, Visual Studio’s among them, but for this kind of problem the simple approach wins. Brian Kernighan said it in UNIX for Beginners, which shipped with Seventh Edition Unix in 1979:
The most effective debugging tool is still careful thought, coupled with judiciously placed print statements.
Why not OpenTelemetry
For production I use proper observability, and OpenTelemetry is the right tool for traces and metrics. It is not what I reach for when the question is “does this line even run, and in what order relative to that other process”. Adding an SDK, an exporter and a collector to three projects in three languages to answer that is more work than the bug.
logz is deliberately the opposite: no authentication, no TLS, no delivery guarantee, and no packages. It is for a development machine and nothing else.
The protocol
A message is a header line, the message lines, and a terminator, over TCP:
<timestamp> <hostname> <source>
<message line 1>
<message line 2>
__END__
That is the whole thing. A line that is exactly __END__ is sent
doubled. The server reads messages, prints them with a colour per
source, and appends them to logz.log, so tail -f and grep work
straight away.
Because the protocol is that small, a client is one file. There is one each for .NET, Go, Node and Deno, and the idea is that you copy the file into your project instead of adding a dependency:
Logz.Log("API", $"order {id} accepted");
logz.Log("WORKER", fmt.Sprintf("picked up %s", id))
Logging is fire and forget. If the server is not running the messages are dropped, and nothing in the application blocks or throws because of logz.
A few extras
The .NET client grew a few helpers, each because I needed it once:
BeginScopetags every line in an async flow, so lines from one request can be told apart from another’s.WatchAsynclogs when an operation starts, warns while it is slow, and logs when it finishes. It is the quickest way I know to find out which await is hanging.InstallCrashHandlerlogs an unhandled exception before the process disappears.
Then there is traffic that is not my code’s logging at all. Two extras
cover that: a mitmproxy addon that records
HTTP traffic as a JSONL index plus one file per body, and a small Node
script that drives a browser over the Chrome DevTools
Protocol and
records network, console and page events. Both write to files that are easy to grep and jq.
The limits
It is a debugging aid, not a logging system. The server holds a connection for a short time and expects clients to reconnect, messages can be lost, and anything on the network can write to it. The mitmproxy recordings contain whatever went over the wire, cookies and tokens included, so they stay out of version control.
The code is on GitHub under the MIT license. The clients are meant to be copied and changed, so treat them as a starting point rather than a library.