logfire client for zig

fix: copy string attributes for memory safety

attributes now copy string data into internal storage instead of
storing pointers. this fixes segfaults when arena-allocated strings
are passed as span attributes - the data outlives the arena.

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

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

+50 -21
+50 -21
src/attribute.zig
··· 1 1 //! OTLP attribute types 2 2 //! 3 3 //! converts zig values to OTLP-compatible attribute format. 4 + //! string values are copied into internal storage for memory safety. 4 5 5 6 const std = @import("std"); 6 7 7 8 pub const Attribute = struct { 8 9 key: []const u8, 9 10 value: Value, 11 + /// internal storage for copied strings 12 + _string_storage: [max_string_len]u8 = undefined, 13 + _string_len: usize = 0, 14 + 15 + pub const max_string_len = 512; 10 16 11 17 pub const Value = union(enum) { 12 18 string: []const u8, ··· 15 21 bool_val: bool, 16 22 }; 17 23 24 + /// copy a string into internal storage and return slice pointing to it 25 + fn copyString(self: *Attribute, str: []const u8) []const u8 { 26 + const len = @min(str.len, max_string_len); 27 + @memcpy(self._string_storage[0..len], str[0..len]); 28 + self._string_len = len; 29 + return self._string_storage[0..len]; 30 + } 31 + 18 32 /// convert a comptime struct to attributes array 19 33 /// returns number of attributes written 34 + /// string values are copied for memory safety 20 35 pub fn fromStruct(attrs: anytype, out: []Attribute) usize { 21 36 const T = @TypeOf(attrs); 22 37 const info = @typeInfo(T); ··· 30 45 if (count >= out.len) break; 31 46 32 47 const field_value = @field(attrs, field.name); 33 - if (toValue(field_value)) |value| { 34 - out[count] = .{ 35 - .key = field.name, 36 - .value = value, 37 - }; 48 + if (toValueWithCopy(&out[count], field.name, field_value)) { 38 49 count += 1; 39 50 } 40 51 } ··· 42 53 return count; 43 54 } 44 55 45 - /// convert a zig value to an attribute value 46 - fn toValue(value: anytype) ?Value { 56 + /// convert value and copy strings into attribute's internal storage 57 + fn toValueWithCopy(attr: *Attribute, key: []const u8, value: anytype) bool { 47 58 const T = @TypeOf(value); 48 59 const info = @typeInfo(T); 49 60 50 - return switch (info) { 51 - .int, .comptime_int => .{ .int = @intCast(value) }, 52 - .float, .comptime_float => .{ .float = @floatCast(value) }, 53 - .bool => .{ .bool_val = value }, 61 + switch (info) { 62 + .int, .comptime_int => { 63 + attr.* = .{ .key = key, .value = .{ .int = @intCast(value) } }; 64 + return true; 65 + }, 66 + .float, .comptime_float => { 67 + attr.* = .{ .key = key, .value = .{ .float = @floatCast(value) } }; 68 + return true; 69 + }, 70 + .bool => { 71 + attr.* = .{ .key = key, .value = .{ .bool_val = value } }; 72 + return true; 73 + }, 54 74 .pointer => |ptr| { 55 75 if (ptr.size == .slice and ptr.child == u8) { 56 - return .{ .string = value }; 76 + // copy the string into internal storage 77 + attr.* = .{ .key = key, .value = undefined }; 78 + const copied = attr.copyString(value); 79 + attr.value = .{ .string = copied }; 80 + return true; 57 81 } 58 82 if (ptr.size == .one) { 59 - // pointer to array (e.g., *const [N]u8) 60 83 const child_info = @typeInfo(ptr.child); 61 84 if (child_info == .array and child_info.array.child == u8) { 62 - return .{ .string = value }; 85 + attr.* = .{ .key = key, .value = undefined }; 86 + const copied = attr.copyString(value); 87 + attr.value = .{ .string = copied }; 88 + return true; 63 89 } 64 90 } 65 - return null; 91 + return false; 66 92 }, 67 93 .array => |arr| { 68 94 if (arr.child == u8) { 69 - return .{ .string = &value }; 95 + attr.* = .{ .key = key, .value = undefined }; 96 + const copied = attr.copyString(&value); 97 + attr.value = .{ .string = copied }; 98 + return true; 70 99 } 71 - return null; 100 + return false; 72 101 }, 73 102 .optional => { 74 103 if (value) |v| { 75 - return toValue(v); 104 + return toValueWithCopy(attr, key, v); 76 105 } 77 - return null; 106 + return false; 78 107 }, 79 - else => null, 80 - }; 108 + else => return false, 109 + } 81 110 } 82 111 83 112 /// write attribute as OTLP JSON