22> Still work in progress.
33
44# zlog - structured logging for zig
5- [ ![ MIT license] ( https://img.shields.io/badge/license-MIT-blue.svg )] ( https://github.com/hendriknielaender/zlog/blob/HEAD/LICENSE )
6- ![ GitHub code size in bytes] ( https://img.shields.io/github/languages/code-size/hendriknielaender/zlog )
7- [ ![ PRs Welcome] ( https://img.shields.io/badge/PRs-welcome-brightgreen.svg )] ( https://github.com/hendriknielaender/zlog/blob/HEAD/CONTRIBUTING.md )
5+ [ ![ MIT license] [ badge_license ]] [ license_link ]
6+ ![ GitHub code size in bytes] [ badge_code_size ]
7+ [ ![ PRs Welcome] [ badge_prs ]] [ contributing_link ]
88<img src =" logo.png " alt =" zlog logo " align =" right " width =" 20% " />
99
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.
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
1121
1222---
1323
@@ -30,7 +40,8 @@ zlog is a high-performance, zero-allocation structured logging library for Zig w
3040}
3141```
3242
33- > ** Note** : libxev is automatically included as a dependency of zlog and managed internally. No need to import or manage event loops manually.
43+ > ** Note** : zlog has no runtime dependency beyond Zig itself. Async logging uses caller-owned
44+ > bounded state and explicit ` drain() ` / ` flush() ` calls.
3445
35462 . Configure in ` build.zig ` :
3647
@@ -46,9 +57,13 @@ const std = @import("std");
4657const zlog = @import("zlog");
4758
4859pub fn main() !void {
49- // High-performance async logger with managed event loop
50- var logger = try zlog.default(std.heap.page_allocator);
51- defer logger.deinitWithAllocator(std.heap.page_allocator);
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);
66+ defer logger.deinit();
5267
5368 // Clean, ergonomic logging with anonymous structs
5469 logger.info("Service started", .{
@@ -71,8 +86,7 @@ pub fn main() !void {
7186 .status_code = 200,
7287 });
7388
74- // Process async events (when using async logger)
75- try logger.runEventLoop();
89+ try logger.flush();
7690}
7791```
7892
@@ -85,20 +99,21 @@ const std = @import("std");
8599const zlog = @import("zlog");
86100
87101pub fn main() !void {
88- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
89- defer _ = gpa.deinit();
90-
91- // Create async logger with managed event loop
102+ // Create async logger with caller-owned bounded state.
92103 const config = zlog.Config{
93104 .async_mode = true,
94105 .async_queue_size = 1024,
95106 .batch_size = 32,
96107 .enable_simd = true,
97108 };
98109
99- const stdout = std.io.getStdOut().writer();
100- var logger = try zlog.Logger(config).initAsync(stdout.any(), gpa.allocator());
101- defer logger.deinitWithAllocator(gpa.allocator());
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);
116+ defer logger.deinit();
102117
103118 const trace_ctx = zlog.TraceContext.init(true);
104119
@@ -109,14 +124,13 @@ pub fn main() !void {
109124 zlog.field.string("service", "api"),
110125 });
111126
112- // Process event loop periodically
127+ // Drain queued writes periodically.
113128 if (i % 1000 == 0) {
114- try logger.runEventLoop ();
129+ logger.drain ();
115130 }
116131 }
117132
118- // Final flush
119- try logger.runEventLoop();
133+ try logger.flush();
120134}
121135```
122136
@@ -133,12 +147,21 @@ const Config = zlog.Config{
133147 .enable_simd = true, // Enable SIMD optimizations
134148};
135149
136- // Async logger with managed event loop
137- var logger = try zlog.Logger(Config).initAsync(writer, allocator);
138- defer logger.deinitWithAllocator(allocator);
139-
140- // Or sync logger (no event loop needed)
141- var sync_logger = zlog.Logger(.{}).init(writer);
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);
157+ defer logger.deinit();
158+ logger.drain();
159+ try logger.flush();
160+
161+ // Or sync logger.
162+ var sync_logger = zlog.Logger(.{}).init(&output_writer);
163+ defer sync_logger.deinit();
164+ try sync_logger.flush();
142165```
143166
144167## Anonymous Struct API
@@ -173,7 +196,7 @@ zlog provides a hybrid compile-time and runtime redaction system for sensitive d
173196// Define sensitive fields at compile-time
174197var logger = zlog.loggerWithRedaction(.{
175198 .redacted_fields = &.{ "password", "api_key", "ssn" },
176- });
199+ }, writer );
177200
178201// These fields will be automatically redacted with zero runtime cost
179202logger.info("User login", &.{
@@ -185,7 +208,8 @@ logger.info("User login", &.{
185208### Runtime Redaction (Dynamic)
186209
187210``` zig
188- var redaction_config = zlog.RedactionConfig.init(allocator);
211+ var redaction_storage: [16][]const u8 = undefined;
212+ var redaction_config = zlog.RedactionConfig.init(&redaction_storage);
189213defer redaction_config.deinit();
190214
191215try redaction_config.addKey("credit_card");
@@ -218,7 +242,7 @@ logger.fatal("Fatal error", &.{}); // Highest priority
218242
219243## Performance Benchmarks
220244
221- zlog is designed for zero-allocation logging with exceptional performance :
245+ zlog is designed for zero-allocation synchronous logging and bounded-allocation async/export paths :
222246
223247``` bash
224248# Run all benchmarks
@@ -236,7 +260,7 @@ zig build test
236260
237261### Key Performance Features
238262
239- - ** Zero Allocations ** : No heap allocations during logging operations
263+ - ** Zero Heap In Sync Hot Path ** : Synchronous formatting avoids heap allocations
240264- ** SIMD Optimizations** : Vectorized string operations where available
241265- ** Async Batching** : Intelligent batching with backpressure handling
242266- ** Pre-formatted Traces** : Hex strings generated once, reused efficiently
@@ -257,7 +281,16 @@ logger.infoWithTrace("Request processed", trace_ctx, .{
257281
258282Output:
259283``` json
260- {"level" :" INFO" ,"msg" :" Request processed" ,"trace" :" a1b2c3d4e5f67890a1b2c3d4e5f67890" ,"span" :" 1234567890abcdef" ,"ts" :1640995200000 ,"tid" :12345 ,"service" :" api" ,"status_code" :200 }
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+ }
261294```
262295
263296zlog provides full W3C Trace Context specification compliance:
@@ -273,7 +306,8 @@ const child_ctx = trace_ctx.createChild(true);
273306const short_id = zlog.extract_short_from_trace_id(trace_ctx.trace_id);
274307```
275308
276- Pre-formatted hex strings eliminate per-log conversion overhead, crucial for ultra-high throughput scenarios.
309+ Pre-formatted hex strings eliminate per-log conversion overhead, which matters in ultra-high
310+ throughput scenarios.
277311
278312## OpenTelemetry Support
279313
@@ -286,12 +320,16 @@ const std = @import("std");
286320const zlog = @import("zlog");
287321
288322pub fn main() !void {
289- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
290- defer _ = gpa.deinit();
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 {};
291327
292- // Create OpenTelemetry-compliant logger with managed event loop
293- var otel_logger = try zlog.otelLogger(gpa.allocator());
294- defer otel_logger.deinitWithAllocator(gpa.allocator());
328+ var async_state = zlog.OTelLogger(.{
329+ .base_config = .{ .async_mode = true },
330+ }).AsyncState{};
331+ var otel_logger = zlog.otelLogger(&stdout_writer, &async_state);
332+ defer otel_logger.deinit();
295333
296334 // Log with OTel semantic conventions
297335 otel_logger.info("HTTP request received", .{
@@ -300,6 +338,8 @@ pub fn main() !void {
300338 .@"http.status_code" = 200,
301339 .@"http.user_agent" = "curl/7.68.0",
302340 });
341+
342+ try otel_logger.flush();
303343}
304344```
305345
@@ -323,24 +363,26 @@ const otel_config = zlog.OTelConfig{
323363 },
324364};
325365
326- var otel_logger = try zlog.otelLoggerWithConfig(otel_config, allocator);
366+ var async_state = zlog.OTelLogger(otel_config).AsyncState{};
367+ var otel_logger = zlog.otelLoggerWithConfig(otel_config, writer, &async_state);
368+ defer otel_logger.deinit();
369+ otel_logger.drain();
370+ try otel_logger.flush();
327371```
328372
329373### OTLP Export
330374
331- Export logs directly to OpenTelemetry collectors :
375+ Serialize OTLP payloads with caller-owned header storage and transport :
332376
333377``` zig
334- const exporter = zlog.OTLPExporter.init(allocator, .{
335- .endpoint = "http://localhost:4318/v1/logs",
336- .headers = &.{
337- .{ .key = "Authorization", .value = "Bearer token123" },
338- },
339- });
378+ var header_storage: [4]zlog.OTLPExporter.Header = undefined;
379+ var exporter = zlog.OTLPExporter.init("http://localhost:4318/v1/logs", &header_storage);
340380defer exporter.deinit();
341381
342- // Export log records
343- try exporter.export(&log_records);
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);
344386```
345387
346388### Semantic Conventions
@@ -393,40 +435,51 @@ logger.spanEnd(span, .{
393435
394436Output includes automatic span correlation:
395437``` json
396- {"level" :" INFO" ,"msg" :" user_authentication" ,"span_mark" :" start" ,"span_id" :123 ,"task_id" :456 ,"thread_id" :789 ,"user_id" :" 12345" ,"method" :" oauth" }
397- {"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" }
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+ }
398459```
399460
400461## Advanced Usage
401462
402- ### Custom Event Loop Management
463+ ### Explicit Queue Draining
403464
404- For advanced users who need to integrate with existing event loops :
465+ Async logging is a bounded queue with explicit draining :
405466
406467``` zig
407- const xev = @import("xev"); // Only needed for advanced usage
408468const zlog = @import("zlog");
409469
410- // Create your own event loop
411- var loop = try xev.Loop.init(.{});
412- defer loop.deinit();
413-
414- // Use the advanced API with custom event loop
415- var logger = try zlog.Logger(.{ .async_mode = true }).initAsyncWithEventLoop(
416- writer,
417- &loop,
418- allocator
419- );
420- defer logger.deinit(); // No allocator needed since you manage the loop
470+ var async_state = zlog.Logger(.{ .async_mode = true }).AsyncState{};
471+ var logger = zlog.Logger(.{ .async_mode = true }).initAsync(writer, &async_state);
472+ defer logger.deinit();
421473
422- // You control the event loop
423- try loop.run(.no_wait);
474+ // You control when the queue is drained.
475+ logger.drain();
476+ try logger.flush();
424477```
425478
426- ### Managed vs Custom Event Loop
479+ ### Drain vs Flush
427480
428- - ** Managed (Recommended) ** : Use ` initAsync() ` - zlog handles everything
429- - ** Custom (Advanced) ** : Use ` initAsyncWithEventLoop() ` - you control the loop
481+ - ` drain() ` : move queued entries to the writer without flushing the writer itself.
482+ - ` flush() ` : drain the queue and flush the underlying writer.
430483
431484## License
432485
0 commit comments