logfire client for zig
1//! OTLP attribute types
2//!
3//! converts zig values to OTLP-compatible attribute format.
4//! string values are copied into internal storage for memory safety.
5
6const std = @import("std");
7
8pub const Attribute = struct {
9 key: []const u8,
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;
16
17 pub const Value = union(enum) {
18 string: []const u8,
19 int: i64,
20 float: f64,
21 bool_val: bool,
22 };
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
32 /// convert a comptime struct to attributes array
33 /// returns number of attributes written
34 /// string values are copied for memory safety
35 pub fn fromStruct(attrs: anytype, out: []Attribute) usize {
36 const T = @TypeOf(attrs);
37 const info = @typeInfo(T);
38
39 if (info != .@"struct") return 0;
40
41 const fields = info.@"struct".fields;
42 var count: usize = 0;
43
44 inline for (fields) |field| {
45 if (count >= out.len) break;
46
47 const field_value = @field(attrs, field.name);
48 if (toValueWithCopy(&out[count], field.name, field_value)) {
49 count += 1;
50 }
51 }
52
53 return count;
54 }
55
56 /// convert value and copy strings into attribute's internal storage
57 fn toValueWithCopy(attr: *Attribute, key: []const u8, value: anytype) bool {
58 const T = @TypeOf(value);
59 const info = @typeInfo(T);
60
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 },
74 .pointer => |ptr| {
75 if (ptr.size == .slice and ptr.child == u8) {
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;
81 }
82 if (ptr.size == .one) {
83 const child_info = @typeInfo(ptr.child);
84 if (child_info == .array and child_info.array.child == u8) {
85 attr.* = .{ .key = key, .value = undefined };
86 const copied = attr.copyString(value);
87 attr.value = .{ .string = copied };
88 return true;
89 }
90 }
91 return false;
92 },
93 .array => |arr| {
94 if (arr.child == u8) {
95 attr.* = .{ .key = key, .value = undefined };
96 const copied = attr.copyString(&value);
97 attr.value = .{ .string = copied };
98 return true;
99 }
100 return false;
101 },
102 .optional => {
103 if (value) |v| {
104 return toValueWithCopy(attr, key, v);
105 }
106 return false;
107 },
108 else => return false,
109 }
110 }
111
112 /// write attribute as OTLP JSON
113 pub fn writeJson(self: Attribute, w: anytype) !void {
114 try w.writeAll("{\"key\":");
115 try writeJsonString(w, self.key);
116 try w.writeAll(",\"value\":{");
117
118 switch (self.value) {
119 .string => |s| {
120 try w.writeAll("\"stringValue\":");
121 try writeJsonString(w, s);
122 },
123 .int => |i| {
124 try w.print("\"intValue\":\"{d}\"", .{i});
125 },
126 .float => |f| {
127 try w.print("\"doubleValue\":{d}", .{f});
128 },
129 .bool_val => |b| {
130 try w.print("\"boolValue\":{}", .{b});
131 },
132 }
133
134 try w.writeAll("}}");
135 }
136};
137
138fn writeJsonString(w: anytype, s: []const u8) !void {
139 try w.writeByte('"');
140 for (s) |c| {
141 switch (c) {
142 '"' => try w.writeAll("\\\""),
143 '\\' => try w.writeAll("\\\\"),
144 '\n' => try w.writeAll("\\n"),
145 '\r' => try w.writeAll("\\r"),
146 '\t' => try w.writeAll("\\t"),
147 0x00...0x08, 0x0b, 0x0c, 0x0e...0x1f => try w.print("\\u00{x:0>2}", .{c}),
148 else => try w.writeByte(c),
149 }
150 }
151 try w.writeByte('"');
152}
153
154// tests
155
156test "fromStruct basic" {
157 var attrs: [8]Attribute = undefined;
158 const count = Attribute.fromStruct(.{
159 .name = "test",
160 .count = @as(i64, 42),
161 .enabled = true,
162 }, &attrs);
163
164 try std.testing.expectEqual(@as(usize, 3), count);
165 try std.testing.expectEqualStrings("name", attrs[0].key);
166 try std.testing.expectEqualStrings("test", attrs[0].value.string);
167 try std.testing.expectEqual(@as(i64, 42), attrs[1].value.int);
168 try std.testing.expectEqual(true, attrs[2].value.bool_val);
169}
170
171test "writeJson string" {
172 var buf: [256]u8 = undefined;
173 var fbs = std.io.fixedBufferStream(&buf);
174 const attr = Attribute{ .key = "foo", .value = .{ .string = "bar" } };
175 try attr.writeJson(fbs.writer());
176 try std.testing.expectEqualStrings("{\"key\":\"foo\",\"value\":{\"stringValue\":\"bar\"}}", fbs.getWritten());
177}