77[ ![ PRs Welcome] ( https://img.shields.io/badge/PRs-welcome-brightgreen.svg )] ( https://github.com/hendriknielaender/zlog/blob/HEAD/CONTRIBUTING.md )
88<img src =" logo.png " alt =" zlog logo " align =" right " width =" 20% " />
99
10- zlog is a high-performance structured logging library for Zig with full OpenTelemetry support.
11- The synchronous formatting path avoids heap allocation, and async batching, redaction, and OTLP
12- export use caller-owned bounded state. Designed for system-level applications requiring maximum
13- performance and observability, zlog provides clean anonymous struct logging with comprehensive
14- tracing capabilities.
15-
16- [ badge_license ] : https://img.shields.io/badge/license-MIT-blue.svg
17- [ badge_code_size ] : https://img.shields.io/github/languages/code-size/hendriknielaender/zlog
18- [ badge_prs ] : https://img.shields.io/badge/PRs-welcome-brightgreen.svg
19- [ license_link ] : https://github.com/hendriknielaender/zlog/blob/HEAD/LICENSE
20- [ contributing_link ] : https://github.com/hendriknielaender/zlog/blob/HEAD/CONTRIBUTING.md
10+ zlog is a high-performance, zero-allocation structured logging library for Zig with full OpenTelemetry support. Designed for system-level applications requiring maximum performance and observability, zlog provides clean anonymous struct logging with comprehensive tracing capabilities.
2111
2212---
2313
@@ -40,8 +30,7 @@ tracing capabilities.
4030}
4131```
4232
43- > ** Note** : zlog has no runtime dependency beyond Zig itself. Async logging uses caller-owned
44- > bounded state and explicit ` drain() ` / ` flush() ` calls.
33+ > ** Note** : zlog now uses Zig 0.16's native ` std.Io ` runtime. No external event-loop dependency is required.
4534
46352 . Configure in ` build.zig ` :
4736
@@ -56,13 +45,8 @@ exe.root_module.addImport("zlog", zlog_module);
5645const std = @import("std");
5746const zlog = @import("zlog");
5847
59- pub fn main() !void {
60- // Prefer std.fs.File.Writer so stdout/stderr stays buffered.
61- var stderr_buffer: [4096]u8 = undefined;
62- var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
63- defer stderr_writer.interface.flush() catch {};
64-
65- var logger = zlog.Logger(.{}).init(&stderr_writer);
48+ pub fn main(init: std.process.Init) !void {
49+ var logger = try zlog.Logger(.{ .async_mode = true }).initAsyncOwnedStderr(init.gpa, init.io);
6650 defer logger.deinit();
6751
6852 // Clean, ergonomic logging with anonymous structs
@@ -86,7 +70,8 @@ pub fn main() !void {
8670 .status_code = 200,
8771 });
8872
89- try logger.flush();
73+ // Flush queued async work before shutdown
74+ try logger.runEventLoopUntilDone();
9075}
9176```
9277
@@ -98,39 +83,29 @@ For maximum throughput in high-load scenarios:
9883const std = @import("std");
9984const zlog = @import("zlog");
10085
101- pub fn main() !void {
102- // Create async logger with caller-owned bounded state.
86+ pub fn main(init: std.process.Init) !void {
10387 const config = zlog.Config{
10488 .async_mode = true,
10589 .async_queue_size = 1024,
10690 .batch_size = 32,
10791 .enable_simd = true,
10892 };
10993
110- var stdout_buffer: [4096]u8 = undefined;
111- var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
112- defer stdout_writer.interface.flush() catch {};
113-
114- var async_state = zlog.Logger(config).AsyncState{};
115- var logger = zlog.Logger(config).initAsync(&stdout_writer, &async_state);
94+ var logger = try zlog.Logger(config).initAsyncOwnedStderr(init.gpa, init.io);
11695 defer logger.deinit();
11796
11897 const trace_ctx = zlog.TraceContext.init(true);
11998
12099 // Log messages per second
121100 for (0..1_000_000) |i| {
122- logger.infoWithTrace("High throughput message", trace_ctx, & .{
123- zlog.field.uint(" iteration", i ),
124- zlog.field.string(" service", "api") ,
101+ logger.infoWithTrace("High throughput message", trace_ctx, .{
102+ . iteration = @as(u64, @intCast(i) ),
103+ . service = "api",
125104 });
126-
127- // Drain queued writes periodically.
128- if (i % 1000 == 0) {
129- logger.drain();
130- }
131105 }
132106
133- try logger.flush();
107+ // Final flush
108+ try logger.runEventLoopUntilDone();
134109}
135110```
136111
@@ -147,21 +122,13 @@ const Config = zlog.Config{
147122 .enable_simd = true, // Enable SIMD optimizations
148123};
149124
150- var output_buffer: [4096]u8 = undefined;
151- var output_writer = std.fs.File.stdout().writer(&output_buffer);
152- defer output_writer.interface.flush() catch {};
153-
154- // Async logger with caller-owned bounded state.
155- var async_state = zlog.Logger(Config).AsyncState{};
156- var logger = zlog.Logger(Config).initAsync(&output_writer, &async_state);
125+ // Async logger using Zig 0.16's native std.Io runtime
126+ var logger = try zlog.Logger(Config).initAsyncOwnedStderr(allocator, io);
157127defer logger.deinit();
158- logger.drain();
159- try logger.flush();
160128
161- // Or sync logger.
162- var sync_logger = zlog.Logger(.{}).init(&output_writer );
129+ // Or sync logger
130+ var sync_logger = try zlog.Logger(.{}).initOwnedStderr(allocator, io );
163131defer sync_logger.deinit();
164- try sync_logger.flush();
165132```
166133
167134## Anonymous Struct API
@@ -196,7 +163,7 @@ zlog provides a hybrid compile-time and runtime redaction system for sensitive d
196163// Define sensitive fields at compile-time
197164var logger = zlog.loggerWithRedaction(.{
198165 .redacted_fields = &.{ "password", "api_key", "ssn" },
199- }, writer );
166+ });
200167
201168// These fields will be automatically redacted with zero runtime cost
202169logger.info("User login", &.{
@@ -208,7 +175,7 @@ logger.info("User login", &.{
208175### Runtime Redaction (Dynamic)
209176
210177``` zig
211- var redaction_storage: [16 ][]const u8 = undefined;
178+ var redaction_storage: [8 ][]const u8 = undefined;
212179var redaction_config = zlog.RedactionConfig.init(&redaction_storage);
213180defer redaction_config.deinit();
214181
@@ -242,7 +209,7 @@ logger.fatal("Fatal error", &.{}); // Highest priority
242209
243210## Performance Benchmarks
244211
245- zlog is designed for zero-allocation synchronous logging and bounded-allocation async/export paths :
212+ zlog is designed for zero-allocation logging with exceptional performance :
246213
247214``` bash
248215# Run all benchmarks
@@ -260,7 +227,7 @@ zig build test
260227
261228### Key Performance Features
262229
263- - ** Zero Heap In Sync Hot Path ** : Synchronous formatting avoids heap allocations
230+ - ** Zero Allocations ** : No heap allocations during logging operations
264231- ** SIMD Optimizations** : Vectorized string operations where available
265232- ** Async Batching** : Intelligent batching with backpressure handling
266233- ** Pre-formatted Traces** : Hex strings generated once, reused efficiently
@@ -281,16 +248,7 @@ logger.infoWithTrace("Request processed", trace_ctx, .{
281248
282249Output:
283250``` json
284- {
285- "level" :" INFO" ,
286- "msg" :" Request processed" ,
287- "trace" :" a1b2c3d4e5f67890a1b2c3d4e5f67890" ,
288- "span" :" 1234567890abcdef" ,
289- "ts" :1640995200000 ,
290- "tid" :12345 ,
291- "service" :" api" ,
292- "status_code" :200
293- }
251+ {"level" :" INFO" ,"msg" :" Request processed" ,"trace" :" a1b2c3d4e5f67890a1b2c3d4e5f67890" ,"span" :" 1234567890abcdef" ,"ts" :1640995200000 ,"tid" :12345 ,"service" :" api" ,"status_code" :200 }
294252```
295253
296254zlog provides full W3C Trace Context specification compliance:
@@ -306,8 +264,7 @@ const child_ctx = trace_ctx.createChild(true);
306264const short_id = zlog.extract_short_from_trace_id(trace_ctx.trace_id);
307265```
308266
309- Pre-formatted hex strings eliminate per-log conversion overhead, which matters in ultra-high
310- throughput scenarios.
267+ Pre-formatted hex strings eliminate per-log conversion overhead, crucial for ultra-high throughput scenarios.
311268
312269## OpenTelemetry Support
313270
@@ -319,16 +276,10 @@ zlog provides full OpenTelemetry compliance with dedicated OTel loggers:
319276const std = @import("std");
320277const zlog = @import("zlog");
321278
322- pub fn main() !void {
323- // Create OpenTelemetry-compliant logger with caller-owned async state.
324- var stdout_buffer: [4096]u8 = undefined;
325- var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
326- defer stdout_writer.interface.flush() catch {};
327-
328- var async_state = zlog.OTelLogger(.{
279+ pub fn main(init: std.process.Init) !void {
280+ var otel_logger = try zlog.OTelLogger(.{
329281 .base_config = .{ .async_mode = true },
330- }).AsyncState{};
331- var otel_logger = zlog.otelLogger(&stdout_writer, &async_state);
282+ }).initAsyncOwnedStderr(init.gpa, init.io);
332283 defer otel_logger.deinit();
333284
334285 // Log with OTel semantic conventions
@@ -339,7 +290,7 @@ pub fn main() !void {
339290 .@"http.user_agent" = "curl/7.68.0",
340291 });
341292
342- try otel_logger.flush ();
293+ try otel_logger.runEventLoopUntilDone ();
343294}
344295```
345296
@@ -363,26 +314,28 @@ const otel_config = zlog.OTelConfig{
363314 },
364315};
365316
366- var async_state = zlog.OTelLogger(otel_config).AsyncState{};
367- var otel_logger = zlog.otelLoggerWithConfig(otel_config, writer, &async_state);
317+ var shared_runtime = zlog.EventLoop.init(allocator);
318+ defer shared_runtime.deinit();
319+
320+ var otel_logger = try zlog.otelLoggerWithConfig(otel_config, &shared_runtime, allocator);
368321defer otel_logger.deinit();
369- otel_logger.drain();
370- try otel_logger.flush();
371322```
372323
373324### OTLP Export
374325
375- Serialize OTLP payloads with caller-owned header storage and transport :
326+ Export logs directly to OpenTelemetry collectors :
376327
377328``` zig
378- var header_storage: [4]zlog.OTLPExporter.Header = undefined;
379- var exporter = zlog.OTLPExporter.init("http://localhost:4318/v1/logs", &header_storage);
329+ const exporter = zlog.OTLPExporter.init(allocator, .{
330+ .endpoint = "http://localhost:4318/v1/logs",
331+ .headers = &.{
332+ .{ .key = "Authorization", .value = "Bearer token123" },
333+ },
334+ });
380335defer exporter.deinit();
381336
382- try exporter.setHeader("Authorization", "Bearer token123");
383-
384- // Serialize the OTLP JSON payload to your chosen transport or buffer.
385- try exporter.exportLogs(writer, log_records);
337+ // Export log records
338+ try exporter.export(&log_records);
386339```
387340
388341### Semantic Conventions
@@ -424,7 +377,6 @@ const span = logger.spanStart("user_authentication", .{
424377});
425378
426379// Your business logic here...
427- std.time.sleep(100 * std.time.ns_per_ms);
428380
429381// End the span with results
430382logger.spanEnd(span, .{
@@ -435,51 +387,37 @@ logger.spanEnd(span, .{
435387
436388Output includes automatic span correlation:
437389``` json
438- {
439- "level" :" INFO" ,
440- "msg" :" user_authentication" ,
441- "span_mark" :" start" ,
442- "span_id" :123 ,
443- "task_id" :456 ,
444- "thread_id" :789 ,
445- "user_id" :" 12345" ,
446- "method" :" oauth"
447- }
448- {
449- "level" :" INFO" ,
450- "msg" :" user_authentication" ,
451- "span_mark" :" end" ,
452- "span_id" :123 ,
453- "task_id" :456 ,
454- "thread_id" :789 ,
455- "duration_ns" :100000000 ,
456- "success" :true ,
457- "token_type" :" bearer"
458- }
390+ {"level" :" INFO" ,"msg" :" user_authentication" ,"span_mark" :" start" ,"span_id" :123 ,"task_id" :456 ,"thread_id" :789 ,"user_id" :" 12345" ,"method" :" oauth" }
391+ {"level" :" INFO" ,"msg" :" user_authentication" ,"span_mark" :" end" ,"span_id" :123 ,"task_id" :456 ,"thread_id" :789 ,"duration_ns" :100000000 ,"success" :true ,"token_type" :" bearer" }
459392```
460393
461394## Advanced Usage
462395
463- ### Explicit Queue Draining
396+ ### Shared Runtime Management
464397
465- Async logging is a bounded queue with explicit draining :
398+ For advanced users who want to share a ` std.Io ` runtime across multiple components :
466399
467400``` zig
468401const zlog = @import("zlog");
469402
470- var async_state = zlog.Logger(.{ .async_mode = true }).AsyncState{};
471- var logger = zlog.Logger(.{ .async_mode = true }).initAsync(writer, &async_state);
403+ var runtime = zlog.EventLoop.init(allocator);
404+ defer runtime.deinit();
405+
406+ // `writer` is a `*std.Io.Writer`
407+ var logger = try zlog.Logger(.{ .async_mode = true }).initAsyncWithEventLoop(
408+ writer,
409+ &runtime,
410+ allocator
411+ );
472412defer logger.deinit();
473413
474- // You control when the queue is drained.
475- logger.drain();
476- try logger.flush();
414+ try logger.runEventLoopUntilDone();
477415```
478416
479- ### Drain vs Flush
417+ ### Managed vs Shared Runtime
480418
481- - ` drain() ` : move queued entries to the writer without flushing the writer itself .
482- - ` flush ()` : drain the queue and flush the underlying writer .
419+ - ** Managed (Recommended) ** : Use ` initAsync() ` or ` initAsyncOwnedStderr() ` and let zlog create its own runtime .
420+ - ** Shared Runtime (Advanced) ** : Use ` initAsyncWithIo ()` or ` initAsyncWithEventLoop() ` when you want zlog to reuse an existing ` std.Io ` runtime .
483421
484422## License
485423
0 commit comments