- Zig 84.8%
- Nix 15.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Every job builds with Nix, so each gets an OIDC token and ends by pushing to the niks3 cache from main, as the template does. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CTNofVkpYkzrGvXFroKDxf |
||
| .forgejo/workflows | ||
| examples | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
| typos.toml | ||
journalz
A Zig library for writing structured log messages directly to the systemd
journal over its native datagram socket at /run/systemd/journal/socket.
Talking to the socket directly means a message carries its own fields rather
than being flattened into a line of text. Every message is built up in a
memfd, sealed against further writes, and handed to the journal as an
SCM_RIGHTS file descriptor over sendmsg, so there is no size limit to work
around and the journal reads the payload the sender can no longer change.
Sending is done through a std.Io, which the caller supplies, so the library
names no I/O implementation of its own.
The API documentation is generated from the doc comments, which carry most of
the explanation, and is published at
https://jeff.jcollie.page/journalz/. zig build docs-serve reads the same
thing locally -- it has to be served rather than opened, since the viewer
fetches its sources at runtime.
const journalz = @import("journalz");
const logger: journalz.Logger = try .init(io, "myprogram", .DAEMON);
defer logger.deinit();
// A formatted MESSAGE field, with failures swallowed.
logger.info("listening on port {d}", .{port});
// Or build the fields yourself.
const msg = try logger.message(.INFO);
errdefer msg.cancel();
var buf: [1024]u8 = undefined;
var w = msg.writer(&buf);
try w.writeAll("MESSAGE", "connection accepted");
try w.print("REMOTE_ADDR", "{f}", .{addr});
try w.writeSrc(@src());
try w.flush();
try msg.send();
Routing std.log to the journal
examples/std_log.zig replaces std.Options.logFn, so that everything the
program and its libraries already log through std.log lands in the journal.
The level becomes a real syslog priority and the scope becomes a field, which
means journald indexes both:
journalctl --identifier=journalz-example --priority=warning
journalctl --identifier=journalz-example LOG_SCOPE=database
rather than leaving a reader to grep for warning: and (database) in a line
of text. The example is built by zig build and run against a real journald
by the NixOS check, so it cannot quietly stop working.
Where this lives
The canonical repository is on Forgejo, with mirrors on Tangled and Radicle:
git clone https://git.jcollie.dev/jeff/journalz.git
- Forgejo — https://git.jcollie.dev/jeff/journalz
- Tangled — https://tangled.org/jcollie.dev/journalz
Radicle
The repository is also on Radicle, where its Repository ID is the only way to find it:
rad:z4LAwD5xmrZ9WB8HmbV65Q2PEmsDX
rad clone rad:z4LAwD5xmrZ9WB8HmbV65Q2PEmsDX
Building
The build needs nothing beyond Zig 0.17.0; the flake's devshell provides it.
nix develop -c zig build test
nix develop -c zig build check
For Zig 0.16.0, use the zig-0.16 branch, which holds the last of journalz to
build with it:
git clone -b zig-0.16 https://git.jcollie.dev/jeff/journalz.git
or, as a dependency:
zig fetch --save https://git.jcollie.dev/jeff/journalz/archive/zig-0.16.tar.gz
The v0.1.0 tag also predates the move to 0.17 and builds with 0.16.0.
Testing
The unit tests talk to a fake journald of their own -- a Unix datagram socket that receives the message, pulls the sealed memfd out of the ancillary data and decodes the wire format back into fields. So they assert on what actually crossed the socket, and they run anywhere, including in a Nix build sandbox with no systemd present.
What they cannot show is that systemd-journald agrees, since the decoder they
check against was written from the same understanding as the encoder. That is
what the NixOS virtual machine check is for: it boots a machine with a real
journald, runs tests/e2e.zig against it, and reads the entries back out of
journalctl -- the priorities, the caller's own indexed fields, a value with
newlines in it, and a 256 KiB field that no datagram could have carried.
nix flake check # everything, virtual machine included
nix build .#checks.x86_64-linux.journald # just the journald machine
License
MIT. See LICENSE.