Skip to content

Commit d801061

Browse files
committed
fix: encode OTLP trace identifiers as hex
1 parent 8b99dba commit d801061

1 file changed

Lines changed: 111 additions & 1 deletion

File tree

opentelemetry-sdk/src/otlp.zig

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
161227
test "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
9071017
test {
9081018
_ = @import("otlp_test.zig");

0 commit comments

Comments
 (0)