Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/zig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ jobs:
- uses: actions/checkout@v6
- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
version: 0.16.0
- run: zig build test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
version: 0.16.0
- run: zig fmt --check src/*.zig
- uses: DonIsaac/zlint-action@v0
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.zig-cache
zig-pkg
zig-out

.DS_Store
176 changes: 57 additions & 119 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hendriknielaender/zlog/blob/HEAD/CONTRIBUTING.md)
<img src="logo.png" alt="zlog logo" align="right" width="20%"/>

zlog is a high-performance structured logging library for Zig with full OpenTelemetry support.
The synchronous formatting path avoids heap allocation, and async batching, redaction, and OTLP
export use caller-owned bounded state. Designed for system-level applications requiring maximum
performance and observability, zlog provides clean anonymous struct logging with comprehensive
tracing capabilities.

[badge_license]: https://img.shields.io/badge/license-MIT-blue.svg
[badge_code_size]: https://img.shields.io/github/languages/code-size/hendriknielaender/zlog
[badge_prs]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
[license_link]: https://github.com/hendriknielaender/zlog/blob/HEAD/LICENSE
[contributing_link]: https://github.com/hendriknielaender/zlog/blob/HEAD/CONTRIBUTING.md
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.

---

Expand All @@ -40,8 +30,7 @@ tracing capabilities.
}
```

> **Note**: zlog has no runtime dependency beyond Zig itself. Async logging uses caller-owned
> bounded state and explicit `drain()` / `flush()` calls.
> **Note**: zlog now uses Zig 0.16's native `std.Io` runtime. No external event-loop dependency is required.

2. Configure in `build.zig`:

Expand All @@ -56,13 +45,8 @@ exe.root_module.addImport("zlog", zlog_module);
const std = @import("std");
const zlog = @import("zlog");

pub fn main() !void {
// Prefer std.fs.File.Writer so stdout/stderr stays buffered.
var stderr_buffer: [4096]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
defer stderr_writer.interface.flush() catch {};

var logger = zlog.Logger(.{}).init(&stderr_writer);
pub fn main(init: std.process.Init) !void {
var logger = try zlog.Logger(.{ .async_mode = true }).initAsyncOwnedStderr(init.gpa, init.io);
defer logger.deinit();

// Clean, ergonomic logging with anonymous structs
Expand All @@ -86,7 +70,8 @@ pub fn main() !void {
.status_code = 200,
});

try logger.flush();
// Flush queued async work before shutdown
try logger.runEventLoopUntilDone();
}
```

Expand All @@ -98,39 +83,29 @@ For maximum throughput in high-load scenarios:
const std = @import("std");
const zlog = @import("zlog");

pub fn main() !void {
// Create async logger with caller-owned bounded state.
pub fn main(init: std.process.Init) !void {
const config = zlog.Config{
.async_mode = true,
.async_queue_size = 1024,
.batch_size = 32,
.enable_simd = true,
};

var stdout_buffer: [4096]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
defer stdout_writer.interface.flush() catch {};

var async_state = zlog.Logger(config).AsyncState{};
var logger = zlog.Logger(config).initAsync(&stdout_writer, &async_state);
var logger = try zlog.Logger(config).initAsyncOwnedStderr(init.gpa, init.io);
defer logger.deinit();

const trace_ctx = zlog.TraceContext.init(true);

// Log messages per second
for (0..1_000_000) |i| {
logger.infoWithTrace("High throughput message", trace_ctx, &.{
zlog.field.uint("iteration", i),
zlog.field.string("service", "api"),
logger.infoWithTrace("High throughput message", trace_ctx, .{
.iteration = @as(u64, @intCast(i)),
.service = "api",
});

// Drain queued writes periodically.
if (i % 1000 == 0) {
logger.drain();
}
}

try logger.flush();
// Final flush
try logger.runEventLoopUntilDone();
}
```

Expand All @@ -147,21 +122,13 @@ const Config = zlog.Config{
.enable_simd = true, // Enable SIMD optimizations
};

var output_buffer: [4096]u8 = undefined;
var output_writer = std.fs.File.stdout().writer(&output_buffer);
defer output_writer.interface.flush() catch {};

// Async logger with caller-owned bounded state.
var async_state = zlog.Logger(Config).AsyncState{};
var logger = zlog.Logger(Config).initAsync(&output_writer, &async_state);
// Async logger using Zig 0.16's native std.Io runtime
var logger = try zlog.Logger(Config).initAsyncOwnedStderr(allocator, io);
defer logger.deinit();
logger.drain();
try logger.flush();

// Or sync logger.
var sync_logger = zlog.Logger(.{}).init(&output_writer);
// Or sync logger
var sync_logger = try zlog.Logger(.{}).initOwnedStderr(allocator, io);
defer sync_logger.deinit();
try sync_logger.flush();
```

## Anonymous Struct API
Expand Down Expand Up @@ -196,7 +163,7 @@ zlog provides a hybrid compile-time and runtime redaction system for sensitive d
// Define sensitive fields at compile-time
var logger = zlog.loggerWithRedaction(.{
.redacted_fields = &.{ "password", "api_key", "ssn" },
}, writer);
});

// These fields will be automatically redacted with zero runtime cost
logger.info("User login", &.{
Expand All @@ -208,7 +175,7 @@ logger.info("User login", &.{
### Runtime Redaction (Dynamic)

```zig
var redaction_storage: [16][]const u8 = undefined;
var redaction_storage: [8][]const u8 = undefined;
var redaction_config = zlog.RedactionConfig.init(&redaction_storage);
defer redaction_config.deinit();

Expand Down Expand Up @@ -242,7 +209,7 @@ logger.fatal("Fatal error", &.{}); // Highest priority

## Performance Benchmarks

zlog is designed for zero-allocation synchronous logging and bounded-allocation async/export paths:
zlog is designed for zero-allocation logging with exceptional performance:

```bash
# Run all benchmarks
Expand All @@ -260,7 +227,7 @@ zig build test

### Key Performance Features

- **Zero Heap In Sync Hot Path**: Synchronous formatting avoids heap allocations
- **Zero Allocations**: No heap allocations during logging operations
- **SIMD Optimizations**: Vectorized string operations where available
- **Async Batching**: Intelligent batching with backpressure handling
- **Pre-formatted Traces**: Hex strings generated once, reused efficiently
Expand All @@ -281,16 +248,7 @@ logger.infoWithTrace("Request processed", trace_ctx, .{

Output:
```json
{
"level":"INFO",
"msg":"Request processed",
"trace":"a1b2c3d4e5f67890a1b2c3d4e5f67890",
"span":"1234567890abcdef",
"ts":1640995200000,
"tid":12345,
"service":"api",
"status_code":200
}
{"level":"INFO","msg":"Request processed","trace":"a1b2c3d4e5f67890a1b2c3d4e5f67890","span":"1234567890abcdef","ts":1640995200000,"tid":12345,"service":"api","status_code":200}
```

zlog provides full W3C Trace Context specification compliance:
Expand All @@ -306,8 +264,7 @@ const child_ctx = trace_ctx.createChild(true);
const short_id = zlog.extract_short_from_trace_id(trace_ctx.trace_id);
```

Pre-formatted hex strings eliminate per-log conversion overhead, which matters in ultra-high
throughput scenarios.
Pre-formatted hex strings eliminate per-log conversion overhead, crucial for ultra-high throughput scenarios.

## OpenTelemetry Support

Expand All @@ -319,16 +276,10 @@ zlog provides full OpenTelemetry compliance with dedicated OTel loggers:
const std = @import("std");
const zlog = @import("zlog");

pub fn main() !void {
// Create OpenTelemetry-compliant logger with caller-owned async state.
var stdout_buffer: [4096]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
defer stdout_writer.interface.flush() catch {};

var async_state = zlog.OTelLogger(.{
pub fn main(init: std.process.Init) !void {
var otel_logger = try zlog.OTelLogger(.{
.base_config = .{ .async_mode = true },
}).AsyncState{};
var otel_logger = zlog.otelLogger(&stdout_writer, &async_state);
}).initAsyncOwnedStderr(init.gpa, init.io);
defer otel_logger.deinit();

// Log with OTel semantic conventions
Expand All @@ -339,7 +290,7 @@ pub fn main() !void {
.@"http.user_agent" = "curl/7.68.0",
});

try otel_logger.flush();
try otel_logger.runEventLoopUntilDone();
}
```

Expand All @@ -363,26 +314,28 @@ const otel_config = zlog.OTelConfig{
},
};

var async_state = zlog.OTelLogger(otel_config).AsyncState{};
var otel_logger = zlog.otelLoggerWithConfig(otel_config, writer, &async_state);
var shared_runtime = zlog.EventLoop.init(allocator);
defer shared_runtime.deinit();

var otel_logger = try zlog.otelLoggerWithConfig(otel_config, &shared_runtime, allocator);
defer otel_logger.deinit();
otel_logger.drain();
try otel_logger.flush();
```

### OTLP Export

Serialize OTLP payloads with caller-owned header storage and transport:
Export logs directly to OpenTelemetry collectors:

```zig
var header_storage: [4]zlog.OTLPExporter.Header = undefined;
var exporter = zlog.OTLPExporter.init("http://localhost:4318/v1/logs", &header_storage);
const exporter = zlog.OTLPExporter.init(allocator, .{
.endpoint = "http://localhost:4318/v1/logs",
.headers = &.{
.{ .key = "Authorization", .value = "Bearer token123" },
},
});
defer exporter.deinit();

try exporter.setHeader("Authorization", "Bearer token123");

// Serialize the OTLP JSON payload to your chosen transport or buffer.
try exporter.exportLogs(writer, log_records);
// Export log records
try exporter.export(&log_records);
```

### Semantic Conventions
Expand Down Expand Up @@ -424,7 +377,6 @@ const span = logger.spanStart("user_authentication", .{
});

// Your business logic here...
std.time.sleep(100 * std.time.ns_per_ms);

// End the span with results
logger.spanEnd(span, .{
Expand All @@ -435,51 +387,37 @@ logger.spanEnd(span, .{

Output includes automatic span correlation:
```json
{
"level":"INFO",
"msg":"user_authentication",
"span_mark":"start",
"span_id":123,
"task_id":456,
"thread_id":789,
"user_id":"12345",
"method":"oauth"
}
{
"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"
}
{"level":"INFO","msg":"user_authentication","span_mark":"start","span_id":123,"task_id":456,"thread_id":789,"user_id":"12345","method":"oauth"}
{"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"}
```

## Advanced Usage

### Explicit Queue Draining
### Shared Runtime Management

Async logging is a bounded queue with explicit draining:
For advanced users who want to share a `std.Io` runtime across multiple components:

```zig
const zlog = @import("zlog");

var async_state = zlog.Logger(.{ .async_mode = true }).AsyncState{};
var logger = zlog.Logger(.{ .async_mode = true }).initAsync(writer, &async_state);
var runtime = zlog.EventLoop.init(allocator);
defer runtime.deinit();

// `writer` is a `*std.Io.Writer`
var logger = try zlog.Logger(.{ .async_mode = true }).initAsyncWithEventLoop(
writer,
&runtime,
allocator
);
defer logger.deinit();

// You control when the queue is drained.
logger.drain();
try logger.flush();
try logger.runEventLoopUntilDone();
```

### Drain vs Flush
### Managed vs Shared Runtime

- `drain()`: move queued entries to the writer without flushing the writer itself.
- `flush()`: drain the queue and flush the underlying writer.
- **Managed (Recommended)**: Use `initAsync()` or `initAsyncOwnedStderr()` and let zlog create its own runtime.
- **Shared Runtime (Advanced)**: Use `initAsyncWithIo()` or `initAsyncWithEventLoop()` when you want zlog to reuse an existing `std.Io` runtime.

## License

Expand Down
Loading
Loading