Skip to content

Commit 2b30457

Browse files
refactor: api design
1 parent 865ab47 commit 2b30457

27 files changed

Lines changed: 4125 additions & 2275 deletions

README.md

Lines changed: 122 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,22 @@
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
3546
2. Configure in `build.zig`:
3647

@@ -46,9 +57,13 @@ const std = @import("std");
4657
const zlog = @import("zlog");
4758
4859
pub 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");
8599
const zlog = @import("zlog");
86100
87101
pub 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
174197
var 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
179202
logger.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);
189213
defer redaction_config.deinit();
190214
191215
try 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

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

263296
zlog provides full W3C Trace Context specification compliance:
@@ -273,7 +306,8 @@ const child_ctx = trace_ctx.createChild(true);
273306
const 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");
286320
const zlog = @import("zlog");
287321
288322
pub 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);
340380
defer 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

394436
Output 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
408468
const 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

Comments
 (0)