Skip to content

Commit 15bee3e

Browse files
kmosinge4pres
authored andcommitted
feat(logs): add std.log bridge for seamless integration
Implement std.log bridge to route standard Zig logging calls to OpenTelemetry LoggerProvider without requiring code changes. This addresses issue #116. Features: - Thread-safe configuration with optimized mutex locking - Two scope strategies: single_scope (default) and per_zig_scope - Migration-friendly dual-mode (emit to both OTel and stderr) - Proper severity mapping following OpenTelemetry spec - Three comprehensive examples demonstrating different use cases The bridge allows existing Zig codebases using std.log to adopt OpenTelemetry observability without refactoring every log call site. Closes #116
1 parent 1a23bdf commit 15bee3e

File tree

5 files changed

+617
-0
lines changed

5 files changed

+617
-0
lines changed

examples/logs/std_log_basic.zig

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const std = @import("std");
2+
const sdk = @import("opentelemetry-sdk");
3+
4+
// Override std.log to use OpenTelemetry bridge
5+
pub const std_options: std.Options = .{
6+
.logFn = sdk.logs.std_log_bridge.logFn,
7+
};
8+
9+
pub fn main() !void {
10+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
11+
defer _ = gpa.deinit();
12+
const allocator = gpa.allocator();
13+
14+
std.debug.print("OpenTelemetry std.log Bridge - Basic Example\n", .{});
15+
std.debug.print("=============================================\n\n", .{});
16+
17+
// Create a stdout exporter
18+
const stdout_file = std.fs.File.stdout();
19+
var stdout_exporter = sdk.logs.StdoutExporter.init(stdout_file.deprecatedWriter());
20+
const exporter = stdout_exporter.asLogRecordExporter();
21+
22+
// Create a simple processor
23+
var simple_processor = sdk.logs.SimpleLogRecordProcessor.init(allocator, exporter);
24+
const processor = simple_processor.asLogRecordProcessor();
25+
26+
// Create a logger provider
27+
var provider = try sdk.logs.LoggerProvider.init(allocator, null);
28+
defer provider.deinit();
29+
30+
// Add the processor
31+
try provider.addLogRecordProcessor(processor);
32+
33+
// Configure the std.log bridge
34+
try sdk.logs.std_log_bridge.configure(.{
35+
.provider = provider,
36+
.also_log_to_stderr = false, // Only log to OpenTelemetry
37+
.include_scope_attribute = true,
38+
.include_source_location = true,
39+
});
40+
defer sdk.logs.std_log_bridge.shutdown();
41+
42+
std.debug.print("Using std.log (routed to OpenTelemetry)...\n\n", .{});
43+
44+
// Now use standard std.log functions - they'll go to OpenTelemetry!
45+
std.log.info("Application started successfully", .{});
46+
std.log.debug("Debug information: processing {d} items", .{42});
47+
std.log.warn("Low memory warning: {d}% available", .{15});
48+
49+
// Simulate some application work
50+
processRequest("user-123") catch |err| {
51+
std.log.err("Failed to process request: {}", .{err});
52+
};
53+
54+
std.debug.print("\n\nShutting down...\n", .{});
55+
56+
// Shutdown (flushes all pending logs)
57+
try provider.shutdown();
58+
59+
std.debug.print("Done!\n", .{});
60+
}
61+
62+
fn processRequest(user_id: []const u8) !void {
63+
std.log.info("Processing request for user: {s}", .{user_id});
64+
65+
// Simulate some work
66+
std.log.debug("Validating user credentials", .{});
67+
std.log.debug("Loading user profile", .{});
68+
69+
// Simulate an error
70+
std.log.err("Database connection timeout", .{});
71+
return error.DatabaseTimeout;
72+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const std = @import("std");
2+
const sdk = @import("opentelemetry-sdk");
3+
4+
// Override std.log to use OpenTelemetry bridge
5+
pub const std_options: std.Options = .{
6+
.logFn = sdk.logs.std_log_bridge.logFn,
7+
};
8+
9+
pub fn main() !void {
10+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
11+
defer _ = gpa.deinit();
12+
const allocator = gpa.allocator();
13+
14+
std.debug.print("OpenTelemetry std.log Bridge - Migration Example\n", .{});
15+
std.debug.print("=================================================\n\n", .{});
16+
std.debug.print("This example demonstrates dual-mode logging:\n", .{});
17+
std.debug.print("- Logs are sent to OpenTelemetry exporters\n", .{});
18+
std.debug.print("- Logs ALSO appear on stderr (for compatibility)\n\n", .{});
19+
20+
// Create a stdout exporter
21+
const stdout_file = std.fs.File.stdout();
22+
var stdout_exporter = sdk.logs.StdoutExporter.init(stdout_file.deprecatedWriter());
23+
const exporter = stdout_exporter.asLogRecordExporter();
24+
25+
// Create a batching processor (more realistic for production)
26+
var batch_processor = try sdk.logs.BatchingLogRecordProcessor.init(allocator, exporter, .{
27+
.max_queue_size = 100,
28+
.scheduled_delay_millis = 1000,
29+
.max_export_batch_size = 10,
30+
});
31+
defer {
32+
const processor = batch_processor.asLogRecordProcessor();
33+
processor.shutdown() catch {};
34+
batch_processor.deinit();
35+
}
36+
const processor = batch_processor.asLogRecordProcessor();
37+
38+
// Create a logger provider with resource attributes
39+
const service_name: []const u8 = "migration-example";
40+
const service_version: []const u8 = "1.0.0";
41+
const resource = try sdk.attributes.Attributes.from(allocator, .{
42+
"service.name", service_name,
43+
"service.version", service_version,
44+
});
45+
defer if (resource) |r| allocator.free(r);
46+
47+
var provider = try sdk.logs.LoggerProvider.init(allocator, resource);
48+
defer provider.deinit();
49+
50+
// Add the processor
51+
try provider.addLogRecordProcessor(processor);
52+
53+
// Configure the std.log bridge in DUAL MODE
54+
try sdk.logs.std_log_bridge.configure(.{
55+
.provider = provider,
56+
.also_log_to_stderr = true, // DUAL MODE: Send to both OTel AND stderr
57+
.include_scope_attribute = true,
58+
.include_source_location = true,
59+
});
60+
defer sdk.logs.std_log_bridge.shutdown();
61+
62+
std.debug.print("Starting application with dual-mode logging...\n", .{});
63+
std.debug.print("(You should see logs both in OTel format AND on stderr)\n\n", .{});
64+
65+
// Use standard std.log - logs go to BOTH destinations
66+
std.log.info("Application starting", .{});
67+
std.log.info("Configuration loaded from environment", .{});
68+
69+
// Simulate application lifecycle
70+
try runApplication();
71+
72+
std.debug.print("\n\nShutting down...\n", .{});
73+
74+
// Force flush before shutdown to ensure all batched logs are exported
75+
try provider.forceFlush();
76+
try provider.shutdown();
77+
78+
std.debug.print("Done! All logs were sent to both OpenTelemetry and stderr.\n", .{});
79+
}
80+
81+
fn runApplication() !void {
82+
std.log.info("Initializing database connection", .{});
83+
84+
// Simulate database connection
85+
std.log.debug("Connecting to database at localhost:5432", .{});
86+
std.log.info("Database connection established", .{});
87+
88+
// Simulate processing
89+
for (0..3) |i| {
90+
std.log.info("Processing batch {d}/3", .{i + 1});
91+
std.log.debug("Fetching records from database", .{});
92+
93+
if (i == 1) {
94+
std.log.warn("Slow query detected, took 2.5s", .{});
95+
}
96+
}
97+
98+
std.log.info("All batches processed successfully", .{});
99+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const std = @import("std");
2+
const sdk = @import("opentelemetry-sdk");
3+
4+
// Override std.log to use OpenTelemetry bridge
5+
pub const std_options: std.Options = .{
6+
.logFn = sdk.logs.std_log_bridge.logFn,
7+
};
8+
9+
// Define custom log scopes for different parts of the application
10+
const log = struct {
11+
pub const http = std.log.scoped(.http);
12+
pub const database = std.log.scoped(.database);
13+
pub const auth = std.log.scoped(.auth);
14+
pub const cache = std.log.scoped(.cache);
15+
};
16+
17+
pub fn main() !void {
18+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
19+
defer _ = gpa.deinit();
20+
const allocator = gpa.allocator();
21+
22+
std.debug.print("OpenTelemetry std.log Bridge - Per-Scope Example\n", .{});
23+
std.debug.print("=================================================\n\n", .{});
24+
std.debug.print("This example demonstrates per-scope logging:\n", .{});
25+
std.debug.print("- Each Zig log scope gets its own OpenTelemetry Logger\n", .{});
26+
std.debug.print("- Allows fine-grained control and filtering per component\n\n", .{});
27+
28+
// Create a stdout exporter
29+
const stdout_file = std.fs.File.stdout();
30+
var stdout_exporter = sdk.logs.StdoutExporter.init(stdout_file.deprecatedWriter());
31+
const exporter = stdout_exporter.asLogRecordExporter();
32+
33+
// Create a simple processor
34+
var simple_processor = sdk.logs.SimpleLogRecordProcessor.init(allocator, exporter);
35+
const processor = simple_processor.asLogRecordProcessor();
36+
37+
// Create a logger provider
38+
var provider = try sdk.logs.LoggerProvider.init(allocator, null);
39+
defer provider.deinit();
40+
41+
// Add the processor
42+
try provider.addLogRecordProcessor(processor);
43+
44+
// Configure the std.log bridge with PER-SCOPE strategy
45+
try sdk.logs.std_log_bridge.configure(.{
46+
.provider = provider,
47+
.scope_strategy = .per_zig_scope, // Each Zig scope gets its own Logger
48+
.also_log_to_stderr = false,
49+
.include_scope_attribute = true,
50+
.include_source_location = true,
51+
});
52+
defer sdk.logs.std_log_bridge.shutdown();
53+
54+
std.debug.print("Starting application with per-scope logging...\n\n", .{});
55+
56+
// Use different scopes - each will create a separate Logger
57+
try simulateHttpServer();
58+
try simulateDatabaseOperations();
59+
try simulateAuthentication();
60+
61+
std.debug.print("\n\nShutting down...\n", .{});
62+
63+
// Shutdown (flushes all pending logs)
64+
try provider.shutdown();
65+
66+
std.debug.print("Done!\n", .{});
67+
}
68+
69+
fn simulateHttpServer() !void {
70+
// These logs will use the "http" scope -> Logger with scope "http"
71+
log.http.info("HTTP server starting on port 8080", .{});
72+
log.http.debug("Registering routes", .{});
73+
74+
// Simulate some requests
75+
log.http.info("GET /api/users - 200 OK - 45ms", .{});
76+
log.http.info("POST /api/users - 201 Created - 120ms", .{});
77+
log.http.warn("GET /api/slow - 200 OK - 2500ms (slow request)", .{});
78+
log.http.err("POST /api/error - 500 Internal Server Error", .{});
79+
}
80+
81+
fn simulateDatabaseOperations() !void {
82+
// These logs will use the "database" scope -> Logger with scope "database"
83+
log.database.info("Connecting to PostgreSQL database", .{});
84+
log.database.debug("Connection pool initialized with 10 connections", .{});
85+
86+
// Simulate queries
87+
log.database.debug("Executing query: SELECT * FROM users WHERE id = $1", .{});
88+
log.database.info("Query executed successfully in 12ms", .{});
89+
90+
log.database.debug("Starting transaction", .{});
91+
log.database.debug("INSERT INTO orders VALUES ($1, $2, $3)", .{});
92+
log.database.debug("UPDATE inventory SET quantity = quantity - 1", .{});
93+
log.database.info("Transaction committed successfully", .{});
94+
95+
// Simulate a slow query
96+
log.database.warn("Slow query detected: took 3500ms", .{});
97+
}
98+
99+
fn simulateAuthentication() !void {
100+
// These logs will use the "auth" scope -> Logger with scope "auth"
101+
log.auth.info("Authentication service initialized", .{});
102+
log.auth.debug("Loading JWT signing keys", .{});
103+
104+
// Simulate auth attempts
105+
log.auth.info("User login attempt: username=alice", .{});
106+
log.auth.debug("Validating password hash", .{});
107+
log.auth.info("User authenticated successfully: user_id=12345", .{});
108+
109+
// Failed attempt
110+
log.auth.warn("Failed login attempt: username=bob (invalid password)", .{});
111+
log.auth.warn("Multiple failed attempts detected for username=bob", .{});
112+
113+
// Token operations
114+
log.auth.debug("Generating JWT token for user_id=12345", .{});
115+
log.auth.info("Access token generated, expires in 3600s", .{});
116+
}

src/sdk/logs.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ pub const StdoutExporter = @import("logs/exporters/generic.zig").StdoutExporter;
1717
pub const InMemoryExporter = @import("logs/exporters/generic.zig").InMemoryExporter;
1818
pub const OTLPExporter = @import("logs/exporters/otlp.zig").OTLPExporter;
1919

20+
// std.log bridge
21+
pub const std_log_bridge = @import("logs/std_log_bridge.zig");
22+
2023
test {
2124
_ = @import("logs/log_record_processor.zig");
2225
_ = @import("logs/log_record_exporter.zig");
2326
_ = @import("logs/exporters/generic.zig");
2427
_ = @import("logs/exporters/otlp.zig");
28+
_ = @import("logs/std_log_bridge.zig");
2529
}

0 commit comments

Comments
 (0)