- Zig 100%
- Shell <1%
- Docker <1%
logfire-zig #
unofficial Zig SDK for Pydantic Logfire, built on otel-zig for OTLP/protobuf export. Follows the shape of logfire-rust; the parity table at the bottom says exactly how far that goes.
adopting it #
Three lines in your application root, then std.log and spans just work.
const std = @import("std");
const logfire = @import("logfire");
// every std.log.* call — yours, your dependencies', the stdlib's — becomes an
// OTEL log record carrying the current span, and still reaches stderr
pub const std_options: std.Options = .{ .log_level = .info, .logFn = logfire.logFn };
// a panic's message is shipped before the process dies
pub const panic = std.debug.FullPanic(logfire.panicFn);
pub fn main() !void {
const lf = try logfire.configure(.{
.service_name = "my-service",
.service_version = "1.4.0",
.environment = "production",
});
defer lf.shutdown(); // flushes, then tears down
const span = logfire.span("process.files", .{ .count = @as(i64, 3) });
defer span.end();
std.log.info("starting", .{}); // renders under process.files in logfire
}
build.zig.zon:
.logfire = .{
.url = "https://tangled.org/zzstoatzz.io/logfire-zig/archive/v0.4.0.tar.gz",
.hash = "...", // zig build tells you
},
build.zig:
const logfire = b.dependency("logfire", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("logfire", logfire.module("logfire"));
Run with LOGFIRE_TOKEN=pylf_v1_us_…. Without a token nothing is exported and
std.log behaves exactly as it did before.
the rules that keep telemetry honest #
- flush before you exit, and the flush is bounded. Export is batched on
a timer (500 ms). A watchdog that logs "no progress, exiting" and calls
std.process.exitships nothing unless it callslogfire.flush()first.lf.shutdown()flushes too. Both wait at mostexit_flush_timeout_ms(3 s) per provider — never unbounded, because an exit path that waits on a wedged or dead export thread does not exit (0.3.1 did this; a panic on the export thread left a process alive-but-dead for 22 hours). - a panic prints its trace before it ships.
panicFnwrites the message and stack trace to stderr first, then logs and flushes withinpanic_flush_timeout_ms(1.5 s), then aborts. A panic raised inside the telemetry path falls through to the default handler instead of re-entering. - spans across tasks need an explicit parent. The current span lives in
thread-local state, so a span opened inside
io.concurrentor a spawned thread starts as a trace root. Capturelogfire.currentContext()on the spawning side and open the task's span withlogfire.spanWithParent(parent, …). - prefer
std.logoverlogfire.info(...). The direct functions exist for parity, but the hook is what makes stdlib and dependency logs show up too. Both funnel into the same pipeline.
what a span carries #
logfire.span(name, attrs) exports an OTEL span with your attributes.
Opt into pending_spans = true to also export a copy at start
(logfire.span_type = pending_span) for in-flight visibility. This doubles
span ingestion and is disabled by default. span.setStatus(.@"error", …)
and span.recordError(err) also stamp logfire.level_num = 17 so the span
renders as errored.
Every log record carries code.module.name (the std.log scope), the level
as severity_text, and the current span's trace_id/span_id.
Every signal carries the resource: service.name, service.version,
deployment.environment.name, process.pid, process.executable.*, and
telemetry.sdk.* with the version read from this package.
API #
const lf = try logfire.configure(.{ .service_name = "svc" }); // *Logfire
lf.flush(); // or logfire.flush(); bounded by exit_flush_timeout_ms
lf.flushWithin(500);
lf.shutdown(); // or logfire.shutdown()
const span = logfire.span("op", .{ .user_id = @as(i64, 7) });
span.setAttribute("rows", @as(i64, 42));
span.setStatus(.ok, null);
span.recordError(error.Timeout);
span.end();
const parent = logfire.currentContext(); // ?SpanContext
const child = logfire.spanWithParent(parent, "leg", .{});
logfire.httpSpan("GET", "/api/users", .{ .status = @as(i64, 200) }); // "HTTP GET /api/users"
logfire.sqlSpan("SELECT * FROM users WHERE id = ?", "sqlite");
logfire.info("direct log {d}", .{1}); // also trace/debug/warn/err
Attribute values: integers, floats, bools, strings, and optionals of those (null is omitted).
configuration #
| field | env | default |
|---|---|---|
token |
LOGFIRE_WRITE_TOKEN, LOGFIRE_TOKEN |
— |
service_name |
LOGFIRE_SERVICE_NAME, OTEL_SERVICE_NAME |
logfire-zig |
service_version |
LOGFIRE_SERVICE_VERSION, OTEL_SERVICE_VERSION |
— |
environment |
LOGFIRE_ENVIRONMENT |
— |
base_url |
OTEL_EXPORTER_OTLP_ENDPOINT |
from the token's region |
send_to_logfire |
— | .if_token_present |
console |
— | enabled: std.log keeps writing to stderr |
pending_spans |
— | false |
batch_timeout_ms |
— | 500 |
parity with logfire-rust #
| capability | rust | zig |
|---|---|---|
| spans with typed attributes, nesting, explicit parent | ✓ | ✓ |
| pending spans on start | ✓ | ✓ |
error status → logfire.level_num |
✓ | ✓ |
| logs through the language's standard facade | log/tracing |
std.log via logFn |
| logs carry span context, module name, severity text | ✓ | ✓ |
logs carry code.file.path / code.line.number |
✓ | ✗ (std.log has no source location) |
| resource: service name/version, environment, process, sdk | ✓ | ✓ |
| shutdown guard / flush | ✓ | ✓ (shutdown, flush) |
| panic hook | ✓ | ✓ (panicFn) |
| console exporter for spans | ✓ | ✗ (logs only) |
| metrics | ✓ | stubs |
RUST_LOG-style runtime level filter |
✓ | compile-time std.options.log_level |
| W3C trace context propagation over HTTP | ✓ | ✗ |
development #
zig build test # unit tests (pending spans, attribute handling, bridge shape)
zig build example # LOGFIRE_TOKEN=… zig build example to see it in a project
license #
MIT