1const std = @import("std");
2const builtin = @import("builtin");
3const tracer = @import("./mod.zig");
4const root = @import("root");
5const nfs = @import("nfs");
6const nio = @import("nio");
7const time = @import("time");
8const extras = @import("extras");
9
10const sys = switch (builtin.target.os.tag) {
11 .linux => @import("sys-linux"),
12 else => unreachable,
13};
14
15// export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
16// export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
17// export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
18
19// https://opentelemetry.io/docs/specs/otlp/
20// https://protobuf.dev/programming-guides/encoding/
21// https://github.com/open-telemetry/opentelemetry-proto/tree/v1.10.0/opentelemetry/proto
22
23var etc_os_release: []const u8 = "";
24var @"os.version": ?[]const u8 = null;
25var @"os.name": ?[]const u8 = null;
26
27pub fn init(args: struct {}) !void {
28 _ = args;
29 const file = try nfs.cwd().openFile("/etc/os-release", .{});
30 defer file.close();
31 etc_os_release = try file.mmap();
32 var iter = std.mem.splitScalar(u8, etc_os_release, '\n');
33 iter.index = 0;
34 while (iter.next()) |line| {
35 if (extras.trimPrefixEnsure(line, "VERSION_ID=")) |val| {
36 @"os.version" = std.mem.trim(u8, val, &.{'"'});
37 break;
38 }
39 }
40 iter.index = 0;
41 while (iter.next()) |line| {
42 if (extras.trimPrefixEnsure(line, "NAME=")) |val| {
43 @"os.name" = std.mem.trim(u8, val, &.{'"'});
44 break;
45 }
46 }
47 if (@"os.version" == null) std.log.warn("/etc/os-release did not contain a VERSION_ID field", .{});
48 if (@"os.name" == null) std.log.warn("/etc/os-release did not contain a NAME field", .{});
49}
50
51pub fn deinit() void {
52 nfs.munmap(etc_os_release);
53}
54
55threadlocal var allocator: std.mem.Allocator = undefined;
56threadlocal var traces_endpoint: ?std.Uri = null;
57pub threadlocal var trace_id: [16]u8 = undefined;
58threadlocal var prev_span_id: ?[8]u8 = undefined;
59threadlocal var spans: std.ArrayListUnmanaged([]const u8) = .empty;
60
61pub var @"service.version": ?[]const u8 = null;
62pub var @"server.address": ?[]const u8 = null;
63pub var @"server.port": ?u16 = null;
64pub threadlocal var @"http.request.method": ?[]const u8 = null;
65pub threadlocal var @"http.response.status_code": ?u16 = null;
66pub threadlocal var @"http.route": ?[]const u8 = null;
67
68pub threadlocal var @"url.path": ?[]const u8 = null;
69pub threadlocal var @"url.query": ?[]const u8 = null;
70
71pub fn init_thread(args: struct { std.mem.Allocator, ?std.Uri }) !void {
72 allocator, traces_endpoint = args;
73 trace_id = nio.randomBytes(16);
74 prev_span_id = null;
75}
76
77pub fn deinit_thread() void {
78 deinit_thread_inner() catch {};
79 spans.clearAndFree(allocator);
80}
81fn deinit_thread_inner() !void {
82 if (traces_endpoint == null) return;
83 var instrumentation_scope: nio.AllocatingWriter = .init(allocator);
84 defer instrumentation_scope.deinit();
85 {
86 const w = &instrumentation_scope;
87 try writef_string(w, 1, "github.com/nektro/zig-tracer");
88 try writef_string(w, 2, "(hash)");
89 try writef_len(w, 3, 0);
90 try writef_varint(w, 4, 0);
91 }
92 var scope_spans: nio.AllocatingWriter = .init(allocator);
93 defer scope_spans.deinit();
94 {
95 const w = &scope_spans;
96 try writef_len(w, 1, instrumentation_scope.items.len);
97 try w.writeAll(instrumentation_scope.items);
98 instrumentation_scope.clearAndFree();
99 for (spans.items) |sp| {
100 try writef_len(w, 2, sp.len);
101 try w.writeAll(sp);
102 }
103 }
104 var resource: nio.AllocatingWriter = .init(allocator);
105 defer resource.deinit();
106 {
107 const w = &resource;
108 try writef_kv(w, 1, .{
109 .@"telemetry.sdk.name" = "github.com/nektro/zig-tracer",
110 .@"telemetry.sdk.version" = "(hash)",
111 .@"telemetry.sdk.language" = "zig",
112 .@"service.name" = root.otel_service_name,
113 .@"service.version" = @"service.version",
114 .@"os.type" = "linux",
115 .@"os.version" = @"os.version",
116 .@"os.name" = @"os.name",
117 .@"server.address" = @"server.address",
118 .@"server.port" = @"server.port",
119 .@"http.request.method" = @"http.request.method",
120 .@"http.response.status_code" = @"http.response.status_code",
121 .@"http.route" = @"http.route",
122 });
123 try writef_varint(w, 2, 0);
124 }
125 var resource_spans: nio.AllocatingWriter = .init(allocator);
126 defer resource_spans.deinit();
127 {
128 const w = &resource_spans;
129 try writef_len(w, 1, resource.items.len);
130 try w.writeAll(resource.items);
131 resource.clearAndFree();
132 try writef_len(w, 2, scope_spans.items.len);
133 try w.writeAll(scope_spans.items);
134 scope_spans.clearAndFree();
135 }
136 var export_request: nio.AllocatingWriter = .init(allocator);
137 defer export_request.deinit();
138 {
139 const w = &export_request;
140 try writef_len(w, 1, resource_spans.items.len);
141 try w.writeAll(resource_spans.items);
142 resource_spans.clearAndFree();
143 }
144 {
145 const io = root.io;
146 var client: std.http.Client = .{ .allocator = allocator, .io = io };
147 defer client.deinit();
148 var req = try client.request(.POST, traces_endpoint.?, .{
149 .headers = .{
150 .content_type = .{ .override = "application/x-protobuf" },
151 },
152 .redirect_behavior = .not_allowed,
153 });
154 defer req.deinit();
155 try req.sendBodyComplete(export_request.items);
156 const resp = try req.receiveHead(&.{});
157 if (resp.head.status != .ok) std.log.scoped(.tracer).warn("otel: {s} {d}", .{ &extras.to_hex(trace_id), resp.head.status });
158 }
159}
160
161pub fn trace_begin(src: std.builtin.SourceLocation, comptime ifmt: []const u8, iargs: anytype) Data {
162 const span_id = nio.randomBytes(8);
163 const parent_id = prev_span_id;
164 prev_span_id = span_id;
165 const fmt = "{s}:{d}:{d} ({s})" ++ ifmt;
166 const args = .{ src.file, src.line, src.column, src.fn_name } ++ iargs;
167 const name = if (traces_endpoint == null) "" else nio.fmt.allocPrint(allocator, fmt, args) catch "";
168 const time_start_ns: u64 = @intCast(time.nanoTimestamp()); // this will overflow 2554 July 21 23:34:33.709 Z
169
170 return .{
171 .span_id = span_id,
172 .parent_id = parent_id,
173 .name = name,
174 .time_start_ns = time_start_ns,
175 };
176}
177
178pub fn trace_end(ctx: tracer.Ctx) void {
179 trace_end_inner(ctx) catch {};
180 prev_span_id = ctx.data.parent_id;
181}
182fn trace_end_inner(ctx: tracer.Ctx) !void {
183 if (traces_endpoint == null) return;
184 var temp: nio.AllocatingWriter = .init(allocator);
185 defer temp.deinit();
186 const w = &temp;
187 defer if (ctx.data.name.len > 0) allocator.free(ctx.data.name);
188 try writef_bytes(w, 1, &trace_id);
189 try writef_bytes(w, 2, &ctx.data.span_id);
190 try writef_string(w, 3, "");
191 if (ctx.data.parent_id) |*pi| try writef_bytes(w, 4, pi);
192 try writef_string(w, 5, ctx.data.name);
193 try writef_varint(w, 6, 2);
194 try writef_i64(w, 7, (ctx.data.time_start_ns));
195 try writef_i64(w, 8, @intCast(time.nanoTimestamp()));
196 if (ctx.data.parent_id == null) try writef_kv(w, 9, .{ .@"url.path" = @"url.path", .@"url.query" = @"url.query" });
197 try spans.append(allocator, try temp.toOwnedSlice());
198}
199
200pub const Data = struct {
201 span_id: [8]u8,
202 parent_id: ?[8]u8,
203 name: []const u8,
204 time_start_ns: u64,
205};
206
207const WireType = enum {
208 varint,
209 i64,
210 len,
211 sgroup,
212 egroup,
213 i32,
214};
215
216fn write_tag(w: anytype, nr: u64, ty: WireType) !void {
217 var _nr = nr;
218 _nr <<= 3;
219 _nr |= @intFromEnum(ty);
220 return write_varint(w, _nr);
221}
222
223fn write_varint(w: anytype, x: u64) !void {
224 var _x = x;
225 inline for (0..10) |i| {
226 if (x < std.math.powi(u64, 2, 7 * (i + 1)) catch unreachable) {
227 var b: [i + 1]u8 = @splat(0);
228 for (b[0..i]) |*n| {
229 n.* |= 128;
230 }
231 for (0..b.len) |j| {
232 b[j] |= @intCast(_x & 127);
233 _x >>= 7;
234 }
235 std.debug.assert(_x == 0);
236 try w.writeAll(&b);
237 return;
238 }
239 }
240 unreachable;
241}
242
243fn writef_bytes(w: anytype, nr: u64, bs: []const u8) !void {
244 try write_tag(w, nr, .len);
245 try write_varint(w, bs.len);
246 try w.writeAll(bs);
247}
248
249fn writef_string(w: anytype, nr: u64, bs: []const u8) !void {
250 try write_tag(w, nr, .len);
251 try write_varint(w, bs.len);
252 try w.writeAll(bs);
253}
254
255fn writef_varint(w: anytype, nr: u64, i: u64) !void {
256 try write_tag(w, nr, .varint);
257 try write_varint(w, i);
258}
259
260fn writef_len(w: anytype, nr: u64, l: u64) !void {
261 try write_tag(w, nr, .len);
262 try write_varint(w, l);
263}
264
265// https://opentelemetry.io/docs/specs/semconv/registry/attributes/
266fn writef_kv(w: anytype, nr: u64, kvs: anytype) !void {
267 var temp: nio.AllocatingWriter = .init(allocator);
268 defer temp.deinit();
269 const x = &temp;
270
271 var temp2: nio.AllocatingWriter = .init(allocator);
272 defer temp2.deinit();
273 const y = &temp2;
274
275 inline for (comptime std.meta.fields(@TypeOf(kvs))) |field| blk: {
276 const value = @field(kvs, field.name);
277 if (@typeInfo(@TypeOf(value)) == .optional and value == null) break :blk;
278
279 try writef_string(x, 1, field.name);
280
281 try writef_kv_v(y, value);
282 try writef_len(x, 2, temp2.items.len);
283 try x.writeAll(temp2.items);
284 temp2.items.len = 0;
285
286 try writef_len(w, nr, temp.items.len);
287 try w.writeAll(temp.items);
288 temp.items.len = 0;
289 }
290}
291fn writef_kv_v(w: anytype, v: anytype) !void {
292 const V = @TypeOf(v);
293 const info = @typeInfo(V);
294 if (info == .optional) {
295 if (v == null) return;
296 return writef_kv_v(w, v.?);
297 }
298 if (comptime extras.isZigString(V)) {
299 return writef_string(w, 1, v);
300 }
301 if (info == .bool) {
302 return write_varint(w, 2, @intFromBool(v));
303 }
304 if (info == .int) {
305 return writef_varint(w, 3, v);
306 }
307 if (info == .float) {
308 return writef_i64(w, 4, @bitCast(v));
309 }
310 @compileLog(v);
311 comptime unreachable;
312}
313
314fn writef_i64(w: anytype, nr: u64, i: u64) !void {
315 try write_tag(w, nr, .i64);
316 try w.writeAll(&std.mem.toBytes((i)));
317}