logfire client for zig

handle optional attribute values

- toOtelValue now returns ?AttributeValue
- attrsToOtel skips null values from optionals
- enables leaflet-search patterns like .{ .uri = optional_uri }

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+19 -7
+19 -7
src/otel_wrapper.zig
··· 302 302 return "https://logfire-us.pydantic.dev:443"; 303 303 } 304 304 305 - fn toOtelValue(value: anytype) otel_api.common.AttributeValue { 305 + fn toOtelValue(value: anytype) ?otel_api.common.AttributeValue { 306 306 const T = @TypeOf(value); 307 307 return switch (@typeInfo(T)) { 308 308 .int, .comptime_int => .{ .int = @intCast(value) }, 309 309 .float, .comptime_float => .{ .double = @floatCast(value) }, 310 310 .bool => .{ .bool = value }, 311 + .optional => { 312 + // handle optional types - return null if the value is null 313 + if (value) |v| { 314 + return toOtelValue(v); 315 + } 316 + return null; 317 + }, 311 318 .pointer => |ptr| { 312 319 if (ptr.size == .slice and ptr.child == u8) { 313 320 return .{ .string = value }; ··· 339 346 // note: not thread-safe, but attributes are copied by otel-zig immediately 340 347 const Storage = struct { 341 348 var result: [fields.len]otel_api.common.AttributeKeyValue = undefined; 349 + var count: usize = 0; 342 350 }; 343 351 344 - inline for (fields, 0..) |field, i| { 345 - Storage.result[i] = .{ 346 - .key = field.name, 347 - .value = toOtelValue(@field(attrs, field.name)), 348 - }; 352 + Storage.count = 0; 353 + inline for (fields) |field| { 354 + if (toOtelValue(@field(attrs, field.name))) |val| { 355 + Storage.result[Storage.count] = .{ 356 + .key = field.name, 357 + .value = val, 358 + }; 359 + Storage.count += 1; 360 + } 349 361 } 350 - return &Storage.result; 362 + return Storage.result[0..Storage.count]; 351 363 }