@@ -131,7 +131,7 @@ pub const Signal = enum {
131131 .http_json = > {
132132 switch (self ) {
133133 inline else = > | data | {
134- return try protobuf . json . encode (data , .{}, .{ . emit_oneof_field_name = false } , allocator );
134+ return try encodeOtlpJson (data , allocator );
135135 },
136136 }
137137 },
@@ -158,6 +158,72 @@ pub const Signal = enum {
158158 };
159159};
160160
161+ const OtlpIdValue = struct {
162+ field_start : usize ,
163+ value_start : usize ,
164+ value_end : usize ,
165+ };
166+
167+ fn nextOtlpIdValue (json : []const u8 , start : usize ) ? OtlpIdValue {
168+ const fields = [_ ][]const u8 {
169+ "\" traceId\" :\" " ,
170+ "\" spanId\" :\" " ,
171+ "\" parentSpanId\" :\" " ,
172+ };
173+ var next : ? OtlpIdValue = null ;
174+
175+ for (fields ) | field | {
176+ const field_start = std .mem .indexOfPos (u8 , json , start , field ) orelse continue ;
177+ if (next != null and next .? .field_start <= field_start ) continue ;
178+
179+ const value_start = field_start + field .len ;
180+ const value_len = std .mem .indexOfScalar (u8 , json [value_start .. ], '"' ) orelse continue ;
181+ next = .{
182+ .field_start = field_start ,
183+ .value_start = value_start ,
184+ .value_end = value_start + value_len ,
185+ };
186+ }
187+
188+ return next ;
189+ }
190+
191+ fn encodeOtlpIdsAsHex (json : []const u8 , allocator : std.mem.Allocator ) ! []u8 {
192+ const digits = "0123456789abcdef" ;
193+ var output = std .Io .Writer .Allocating .init (allocator );
194+ errdefer output .deinit ();
195+
196+ var cursor : usize = 0 ;
197+ var search_from : usize = 0 ;
198+ while (nextOtlpIdValue (json , search_from )) | match | {
199+ try output .writer .writeAll (json [cursor .. match .value_start ]);
200+
201+ const encoded = json [match .value_start .. match .value_end ];
202+ const decoded_len = try std .base64 .standard .Decoder .calcSizeForSlice (encoded );
203+ const decoded = try allocator .alloc (u8 , decoded_len );
204+ defer allocator .free (decoded );
205+ try std .base64 .standard .Decoder .decode (decoded , encoded );
206+
207+ for (decoded ) | byte | {
208+ try output .writer .writeByte (digits [byte >> 4 ]);
209+ try output .writer .writeByte (digits [byte & 0x0f ]);
210+ }
211+
212+ cursor = match .value_end ;
213+ search_from = match .value_end ;
214+ }
215+
216+ try output .writer .writeAll (json [cursor .. ]);
217+ return output .toOwnedSlice ();
218+ }
219+
220+ fn encodeOtlpJson (data : anytype , allocator : std.mem.Allocator ) ! []const u8 {
221+ const json = try protobuf .json .encode (data , .{}, .{ .emit_oneof_field_name = false }, allocator );
222+ if (nextOtlpIdValue (json , 0 ) == null ) return json ;
223+ defer allocator .free (json );
224+ return encodeOtlpIdsAsHex (json , allocator );
225+ }
226+
161227test "otlp Signal.Data get payload bytes" {
162228 const allocator = std .testing .allocator ;
163229 var data = Signal.Data {
@@ -903,6 +969,50 @@ pub fn ExportFile(
903969 try file .sync (io );
904970}
905971
972+ test "OTLP JSON encodes trace identifiers as lowercase hex" {
973+ const allocator = std .testing .allocator ;
974+ const trace_id = [16 ]u8 { 0x01 , 0x23 , 0x45 , 0x67 , 0x89 , 0xab , 0xcd , 0xef , 0x10 , 0x32 , 0x54 , 0x76 , 0x98 , 0xba , 0xdc , 0xfe };
975+ const span_id = [8 ]u8 { 0x01 , 0x23 , 0x45 , 0x67 , 0x89 , 0xab , 0xcd , 0xef };
976+ const parent_span_id = [8 ]u8 { 0xfe , 0xdc , 0xba , 0x98 , 0x76 , 0x54 , 0x32 , 0x10 };
977+
978+ var attributes : std .ArrayList (pbcommon .KeyValue ) = .empty ;
979+ defer attributes .deinit (allocator );
980+ try attributes .append (allocator , .{
981+ .key = "bytes" ,
982+ .value = .{ .value = .{ .bytes_value = & span_id } },
983+ });
984+
985+ var links : std .ArrayList (pbtrace .Span .Link ) = .empty ;
986+ defer links .deinit (allocator );
987+ try links .append (allocator , .{ .trace_id = & trace_id , .span_id = & span_id });
988+
989+ var spans : std .ArrayList (pbtrace .Span ) = .empty ;
990+ defer spans .deinit (allocator );
991+ try spans .append (allocator , .{
992+ .trace_id = & trace_id ,
993+ .span_id = & span_id ,
994+ .parent_span_id = & parent_span_id ,
995+ .attributes = attributes ,
996+ .links = links ,
997+ });
998+
999+ var scope_spans : std .ArrayList (pbtrace .ScopeSpans ) = .empty ;
1000+ defer scope_spans .deinit (allocator );
1001+ try scope_spans .append (allocator , .{ .spans = spans });
1002+
1003+ var resource_spans : std .ArrayList (pbtrace .ResourceSpans ) = .empty ;
1004+ defer resource_spans .deinit (allocator );
1005+ try resource_spans .append (allocator , .{ .scope_spans = scope_spans });
1006+
1007+ const payload = try (Signal.Data { .traces = .{ .resource_spans = resource_spans } }).toOwnedSlice (allocator , .http_json );
1008+ defer allocator .free (payload );
1009+
1010+ try std .testing .expectEqual (@as (usize , 2 ), std .mem .count (u8 , payload , "\" traceId\" :\" 0123456789abcdef1032547698badcfe\" " ));
1011+ try std .testing .expectEqual (@as (usize , 2 ), std .mem .count (u8 , payload , "\" spanId\" :\" 0123456789abcdef\" " ));
1012+ try std .testing .expect (std .mem .indexOf (u8 , payload , "\" parentSpanId\" :\" fedcba9876543210\" " ) != null );
1013+ try std .testing .expect (std .mem .indexOf (u8 , payload , "\" bytesValue\" :\" ASNFZ4mrze8=\" " ) != null );
1014+ }
1015+
9061016// Integration tests
9071017test {
9081018 _ = @import ("otlp_test.zig" );
0 commit comments