Skip to content

Commit dd389f6

Browse files
feat: replace libxev with native async io
1 parent 89987f2 commit dd389f6

23 files changed

Lines changed: 2242 additions & 4772 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.zig-cache
2+
zig-pkg
23
zig-out
34

45
.DS_Store

README.md

Lines changed: 57 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,7 @@
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
4635
2. Configure in `build.zig`:
4736

@@ -56,13 +45,8 @@ exe.root_module.addImport("zlog", zlog_module);
5645
const std = @import("std");
5746
const 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:
9883
const std = @import("std");
9984
const 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);
157127
defer 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);
163131
defer 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
197164
var 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
202169
logger.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;
212179
var redaction_config = zlog.RedactionConfig.init(&redaction_storage);
213180
defer 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

282249
Output:
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

296254
zlog provides full W3C Trace Context specification compliance:
@@ -306,8 +264,7 @@ const child_ctx = trace_ctx.createChild(true);
306264
const 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:
319276
const std = @import("std");
320277
const 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);
368321
defer 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+
});
380335
defer 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
430382
logger.spanEnd(span, .{
@@ -435,51 +387,37 @@ logger.spanEnd(span, .{
435387

436388
Output 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
468401
const 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+
);
472412
defer 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

Comments
 (0)