Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions examples/logs/basic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("Emitting log records...\n\n", .{});

// Emit some logs
logger.emit(9, "INFO", "Application started", null);
logger.emit(9, "Application started", .{});

logger.emit(5, "DEBUG", "Debug message with details", null);
logger.emit(5, "Debug message with details", .{});

// Emit with attributes
const attrs = [_]sdk.attributes.Attribute{
.{ .key = "user.id", .value = .{ .int = 12345 } },
.{ .key = "request.path", .value = .{ .string = "/api/users" } },
};
logger.emit(9, "INFO", "Processing request", &attrs);
logger.emit(9, "Processing request", .{ .attributes = &attrs });

logger.emit(17, "ERROR", "Something went wrong!", null);
logger.emit(17, "Something went wrong!", .{});

std.debug.print("\n\nShutting down...\n", .{});

Expand Down
2 changes: 1 addition & 1 deletion examples/logs/batching.zig
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub fn main(init: std.process.Init) !void {
// Emit 10 logs quickly - should trigger 2 batches
var i: usize = 0;
while (i < 10) : (i += 1) {
logger.emit(9, "INFO", "Batched log message", null);
logger.emit(9, "Batched log message", .{});
clock.sleep(50 * std.time.ns_per_ms); // Small delay
}

Expand Down
18 changes: 10 additions & 8 deletions examples/logs/otlp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ pub fn main(init: std.process.Init) !void {

std.debug.print("Emitting log records to OTLP collector...\n\n", .{});

// Emit logs with different severity levels
logger.emit(1, "TRACE", "Trace level message - very detailed", null);
logger.emit(5, "DEBUG", "Debug level message", null);
logger.emit(9, "INFO", "Application started successfully", null);
logger.emit(13, "WARN", "This is a warning message", null);
logger.emit(17, "ERROR", "An error occurred", null);
logger.emit(21, "FATAL", "Fatal error - application cannot continue", null);
// Emit logs with different severity levels.
// severity_text is optional — useful when bridging from an existing logging system
// that has its own level names (e.g. "CRITICAL" instead of "FATAL").
logger.emit(1, "Trace level message - very detailed", .{ .severity_text = "TRACE" });
logger.emit(5, "Debug level message", .{ .severity_text = "DEBUG" });
logger.emit(9, "Application started successfully", .{ .severity_text = "INFO" });
logger.emit(13, "This is a warning message", .{ .severity_text = "WARN" });
logger.emit(17, "An error occurred", .{ .severity_text = "ERROR" });
logger.emit(21, "Fatal error - application cannot continue", .{ .severity_text = "CRITICAL" });

// Emit with attributes
const attrs = [_]sdk.attributes.Attribute{
Expand All @@ -72,7 +74,7 @@ pub fn main(init: std.process.Init) !void {
.{ .key = "http.status_code", .value = .{ .int = 200 } },
.{ .key = "http.response_time_ms", .value = .{ .double = 45.67 } },
};
logger.emit(9, "INFO", "HTTP request processed", &attrs);
logger.emit(9, "HTTP request processed", .{ .attributes = &attrs });

// Emit log with trace correlation (demonstrates distributed tracing integration)
// In a real application, these would come from the current span context
Expand Down
18 changes: 9 additions & 9 deletions integration_tests/logs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ fn testLogs(
const logger = try provider.getLogger(scope);

const num_logs = 5;
logger.emit(1, "TRACE", "Test trace log", null);
logger.emit(5, "DEBUG", "Test debug log", null);
logger.emit(9, "INFO", "Test info log", null);
logger.emit(13, "WARN", "Test warning log", null);
logger.emit(17, "ERROR", "Test error log", null);
logger.emit(1, "Test trace log", .{});
logger.emit(5, "Test debug log", .{});
logger.emit(9, "Test info log", .{});
logger.emit(13, "Test warning log", .{});
logger.emit(17, "Test error log", .{ .severity_text = "ERROR" });

const attrs = [_]sdk.attributes.Attribute{
.{ .key = "test.iteration", .value = .{ .int = 1 } },
.{ .key = "test.name", .value = .{ .string = "integration-test" } },
};
logger.emit(9, "INFO", "Test log with attributes", &attrs);
logger.emit(9, "Test log with attributes", .{ .attributes = &attrs });

try provider.shutdown();

Expand Down Expand Up @@ -136,9 +136,9 @@ fn testLogsWithCompression(
.{ .key = "test.type", .value = .{ .string = "compression" } },
};

logger.emit(9, "INFO", "Compressed log 1", &attrs);
logger.emit(9, "INFO", "Compressed log 2", &attrs);
logger.emit(9, "INFO", "Compressed log 3", &attrs);
logger.emit(9, "Compressed log 1", .{ .attributes = &attrs });
logger.emit(9, "Compressed log 2", .{ .attributes = &attrs });
logger.emit(9, "Compressed log 3", .{ .attributes = &attrs });

try provider.shutdown();

Expand Down
39 changes: 26 additions & 13 deletions src/api/logs/logger_provider.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const Attributes = @import("../../attributes.zig").Attributes;
const InstrumentationScope = @import("../../scope.zig").InstrumentationScope;
const Context = @import("../context/context.zig").Context;
const EnabledParameters = @import("enabled_parameters.zig").EnabledParameters;
const trace = @import("../trace.zig");

// Import configuration module
const Configuration = @import("../../sdk/config.zig").Configuration;
Expand Down Expand Up @@ -290,37 +291,49 @@ pub const Logger = struct {
self.allocator.destroy(self);
}

/// Emit a log record
pub const EmitOptions = struct {
Comment thread
agagniere marked this conversation as resolved.
Outdated
/// Human-readable severity label (e.g. "WARN", "CRITICAL").
/// Optional: backends can derive a standard label from `severity_number`.
/// Useful when bridging from a logging system that has its own level names.
severity_text: ?[]const u8 = null,

/// Key-value pairs attached to this log record.
attributes: ?[]const Attribute = null,

/// Span context to correlate this log record with an active trace.
/// Pass `span.span_context` to enable log-trace correlation in the backend.
span_context: ?trace.SpanContext = null,
};

@inge4pres inge4pres May 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add all optional parameters dicated by the API spec of emit?
https://opentelemetry.io/docs/specs/otel/logs/api/#emit-a-logrecord

I like the dedicated struct for options, it's also wide-spread practice in Zig codebases, so we can have all fields in the struct.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to add for now Timestamp and Observed Timestamp which are actually optional and already supported by ReadWriteLogRecord. If I recall correctly we support implicit Context and we can later on support an explicit definition.


/// Emit a log record.
pub fn emit(
self: *Self,
severity_number: ?u8,
severity_text: ?[]const u8,
body: ?[]const u8,
attributes: ?[]const Attribute,
severity_number: u8,
body: []const u8,
options: EmitOptions,
Comment thread
inge4pres marked this conversation as resolved.
Outdated
) void {
if (self.provider.sdk_disabled or self.provider.is_shutdown.load(.acquire)) {
return;
}

// Create ReadWriteLogRecord
var log_record = ReadWriteLogRecord.init(self.scope);
defer log_record.deinit(self.allocator);

log_record.severity_number = severity_number;
log_record.severity_text = severity_text;
log_record.severity_text = options.severity_text;
log_record.body = body;
log_record.resource = self.provider.resource;
log_record.trace_id = if (options.span_context) |sc| sc.trace_id.toBinary() else null;
log_record.span_id = if (options.span_context) |sc| sc.span_id.toBinary() else null;

// Add attributes if provided
if (attributes) |attrs| {
if (options.attributes) |attrs| {
for (attrs) |attr| {
log_record.setAttribute(self.allocator, attr) catch |err| {
std.log.err("Failed to add attribute to log record: {}", .{err});
};
}
}

// Call processors in order
const ctx = Context.init();
self.provider.mutex.lockUncancelable(self.provider.io);
defer self.provider.mutex.unlock(self.provider.io);
Expand All @@ -337,7 +350,7 @@ pub const Logger = struct {
/// ```zig
/// if (logger.enabled(.{ .context = ctx, .severity = 9 })) {
/// const expensive_data = computeExpensiveDebugInfo();
/// logger.emit(9, "INFO", expensive_data, null);
/// logger.emit(9, expensive_data, .{});
/// }
/// ```
///
Expand Down Expand Up @@ -443,7 +456,7 @@ test "LoggerProvider with processor" {
const logger = try provider.getLogger(scope);

// Emit a log
logger.emit(9, "INFO", "test message", null);
logger.emit(9, "test message", .{ .severity_text = "INFO" });

// Verify export was called
try std.testing.expectEqual(@as(usize, 1), mock_exporter.export_count);
Expand Down Expand Up @@ -522,7 +535,7 @@ test "Logger log records inherit resource from provider" {
const logger = try provider.getLogger(scope);

// Emit a log
logger.emit(9, "INFO", "test message", null);
logger.emit(9, "test message", .{ .severity_text = "INFO" });

// Verify resource was passed to the log record
try std.testing.expect(mock_exporter.captured_resource != null);
Expand Down
11 changes: 6 additions & 5 deletions src/c/logs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,13 @@ pub fn loggerEmit(
const attrs = convertAttributes(allocator, attributes, attr_count) catch return .error_out_of_memory;
defer if (attrs) |a| allocator.free(a);

// Emit the log record
zigLogger.emit(
if (severity_number > 0) @intCast(severity_number) else null,
if (severity_text) |st| std.mem.span(st) else null,
if (body) |b| std.mem.span(b) else null,
attrs,
if (severity_number > 0) @intCast(severity_number) else 0,
if (body) |b| std.mem.span(b) else "",
.{
.severity_text = if (severity_text) |st| std.mem.span(st) else null,
.attributes = attrs,
},
);

return .ok;
Expand Down
2 changes: 1 addition & 1 deletion src/sdk/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ test "Configuration LoggerProvider with SDK disabled" {
try std.testing.expect(!logger.enabled(.{ .context = ctx }));

// Emit should do nothing (no crash, no processing)
logger.emit(9, "INFO", "test message", null);
logger.emit(9, "test message", .{ .severity_text = "INFO" });
}

const IDGenerator = @import("trace/id_generator.zig").IDGenerator;
Expand Down
4 changes: 2 additions & 2 deletions src/sdk/logs/concurrency_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ fn emitLogWorker(logger: *Logger, count: usize) void {
for (0..count) |i| {
const body = std.fmt.allocPrint(std.heap.page_allocator, "Log message {}", .{i}) catch return;
defer std.heap.page_allocator.free(body);
logger.emit(null, null, body, &.{});
logger.emit(0, body, .{});
}
}

Expand Down Expand Up @@ -376,7 +376,7 @@ test "concurrent forceFlush" {

// Emit some logs first
for (0..50) |_| {
logger.emit(null, null, "test log", &.{});
logger.emit(0, "test log", .{});
}

const num_flush_threads = 10;
Expand Down
36 changes: 19 additions & 17 deletions src/sdk/logs/exporters/otlp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,19 @@ pub const OTLPExporter = struct {
try attributes.append(self.allocator, key_value);
}

// Convert trace_id to hex string (16 bytes -> 32 char hex).
// Duplicated onto the exporter allocator because bytesToHex returns a
// fixed-size array; borrowing its address would escape the stack frame.
const trace_id_str: []const u8 = if (log_record.trace_id) |tid| blk: {
const hex = std.fmt.bytesToHex(&tid, .lower);
break :blk try self.allocator.dupe(u8, &hex);
} else "";

// Convert span_id to hex string (8 bytes -> 16 char hex)
const span_id_str: []const u8 = if (log_record.span_id) |sid| blk: {
const hex = std.fmt.bytesToHex(&sid, .lower);
break :blk try self.allocator.dupe(u8, &hex);
} else "";
// trace_id and span_id are protobuf `bytes` fields.
// Binary (gRPC/http_protobuf): serialized as raw bytes, so 16 and 8 bytes respectively.
// JSON (http_json): zig-protobuf base64-encodes bytes fields per proto3 JSON spec;
// the collector's protojson unmarshaler base64-decodes back to the correct bytes.
Comment thread
inge4pres marked this conversation as resolved.
const trace_id_str: []const u8 = if (log_record.trace_id) |tid|
try self.allocator.dupe(u8, &tid)
else
"";

const span_id_str: []const u8 = if (log_record.span_id) |sid|
try self.allocator.dupe(u8, &sid)
else
"";

// Convert body to AnyValue
const body: ?pbcommon.AnyValue = if (log_record.body) |b|
Expand Down Expand Up @@ -431,6 +431,9 @@ test "Log record to OTLP conversion with all fields" {
try std.testing.expectEqualStrings("ERROR", otlp_log.severity_text);
try std.testing.expectEqualStrings("Test log message", otlp_log.body.?.value.?.string_value);
try std.testing.expectEqual(@as(usize, 2), otlp_log.attributes.items.len);
// trace_id and span_id are stored as raw bytes (not hex strings).
try std.testing.expectEqualSlices(u8, &trace_id, otlp_log.trace_id);
try std.testing.expectEqualSlices(u8, &span_id, otlp_log.span_id);
}

test "Log records grouped by instrumentation scope" {
Expand Down Expand Up @@ -576,7 +579,7 @@ test "Resource attributes in OTLP export" {
try std.testing.expectEqualStrings("my-service", resource.attributes.items[0].value.?.value.?.string_value);
}

test "Trace context hex conversion" {
test "Trace context binary encoding" {
const allocator = std.testing.allocator;
const io = std.testing.io;

Expand Down Expand Up @@ -613,9 +616,8 @@ test "Trace context hex conversion" {
if (otlp_log.span_id.len > 0) allocator.free(otlp_log.span_id);
}

// Verify hex conversion (lowercase hex without 0x prefix)
try std.testing.expectEqualStrings("0123456789abcdef0123456789abcdef", otlp_log.trace_id);
try std.testing.expectEqualStrings("0123456789abcdef", otlp_log.span_id);
try std.testing.expectEqualSlices(u8, &trace_id, otlp_log.trace_id);
try std.testing.expectEqualSlices(u8, &span_id, otlp_log.span_id);
Comment thread
inge4pres marked this conversation as resolved.
}

test "Memory cleanup verification" {
Expand Down
15 changes: 2 additions & 13 deletions src/sdk/logs/std_log_bridge.zig
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,7 @@ pub fn logFn(
const body = std.fmt.bufPrint(&buf, format, args) catch |err| {
// If formatting fails, log the error and the raw format string
std.log.err("std_log_bridge: failed to format log message: {}", .{err});
unwrapped_logger.emit(
mapSeverity(level),
mapSeverityText(level),
format,
null,
);
unwrapped_logger.emit(mapSeverity(level), format, .{ .severity_text = mapSeverityText(level) });
if (config.also_log_to_stderr) {
std.log.defaultLog(level, scope, format, args);
}
Expand Down Expand Up @@ -282,13 +277,7 @@ pub fn logFn(

const attrs = if (attrs_count > 0) attrs_buffer[0..attrs_count] else null;

// Emit to OpenTelemetry
unwrapped_logger.emit(
mapSeverity(level),
mapSeverityText(level),
body,
attrs,
);
unwrapped_logger.emit(mapSeverity(level), body, .{ .severity_text = mapSeverityText(level), .attributes = attrs });

// Also log to stderr if dual mode is enabled
if (config.also_log_to_stderr) {
Expand Down
Loading