| 1 | const std = @import("std"); |
| 2 | const builtin = @import("builtin"); |
| 3 | const tracer = @import("./mod.zig"); |
| 4 | const root = @import("root"); |
| 5 | const nfs = @import("nfs"); |
| 6 | const nio = @import("nio"); |
| 7 | const time = @import("time"); |
| 8 | const extras = @import("extras"); |
| 9 | |
| 10 | const 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 | |
| 23 | var etc_os_release: []const u8 = ""; |
| 24 | var @"os.version": ?[]const u8 = null; |
| 25 | var @"os.name": ?[]const u8 = null; |
| 26 | |
| 27 | pub 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 | |
| 51 | pub fn deinit() void { |
| 52 | nfs.munmap(etc_os_release); |
| 53 | } |
| 54 | |
| 55 | threadlocal var allocator: std.mem.Allocator = undefined; |
| 56 | threadlocal var traces_endpoint: ?std.Uri = null; |
| 57 | pub threadlocal var trace_id: [16]u8 = undefined; |
| 58 | threadlocal var prev_span_id: ?[8]u8 = undefined; |
| 59 | threadlocal var spans: std.ArrayListUnmanaged([]const u8) = .empty; |
| 60 | |
| 61 | pub var @"service.version": ?[]const u8 = null; |
| 62 | pub var @"server.address": ?[]const u8 = null; |
| 63 | pub var @"server.port": ?u16 = null; |
| 64 | pub threadlocal var @"http.request.method": ?[]const u8 = null; |
| 65 | pub threadlocal var @"http.response.status_code": ?u16 = null; |
| 66 | pub threadlocal var @"http.route": ?[]const u8 = null; |
| 67 | |
| 68 | pub threadlocal var @"url.path": ?[]const u8 = null; |
| 69 | pub threadlocal var @"url.query": ?[]const u8 = null; |
| 70 | |
| 71 | pub 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 | |
| 77 | pub fn deinit_thread() void { |
| 78 | deinit_thread_inner() catch {}; |
| 79 | spans.clearAndFree(allocator); |
| 80 | } |
| 81 | fn 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 | |
| 161 | pub 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 | |
| 178 | pub fn trace_end(ctx: tracer.Ctx) void { |
| 179 | trace_end_inner(ctx) catch {}; |
| 180 | prev_span_id = ctx.data.parent_id; |
| 181 | } |
| 182 | fn 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 | |
| 200 | pub const Data = struct { |
| 201 | span_id: [8]u8, |
| 202 | parent_id: ?[8]u8, |
| 203 | name: []const u8, |
| 204 | time_start_ns: u64, |
| 205 | }; |
| 206 | |
| 207 | const WireType = enum { |
| 208 | varint, |
| 209 | i64, |
| 210 | len, |
| 211 | sgroup, |
| 212 | egroup, |
| 213 | i32, |
| 214 | }; |
| 215 | |
| 216 | fn 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 | |
| 223 | fn 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 | |
| 243 | fn 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 | |
| 249 | fn 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 | |
| 255 | fn writef_varint(w: anytype, nr: u64, i: u64) !void { |
| 256 | try write_tag(w, nr, .varint); |
| 257 | try write_varint(w, i); |
| 258 | } |
| 259 | |
| 260 | fn 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/ |
| 266 | fn 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 | } |
| 291 | fn 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 | |
| 314 | fn 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 | } |