logfire client for zig

feat: add otel-zig as foundation for OTel compliance

adds otel-zig dependency and creates wrapper module that:
- configures OTLP exporter with Logfire endpoint/auth
- provides OtelLogfire struct with span() API
- handles token-based region detection (us/eu)

new files:
- src/otel_wrapper.zig: logfire-otel module wrapping otel-zig
- examples/otel_basic.zig: demonstrates new otel-backed API
- tests/validate_otel_export.zig: validates OTLP export to Logfire
- docs/otel-adoption-plan.md: migration plan and findings

build targets:
- zig build otel-example: run otel-backed example
- zig build validate-otel: test OTLP export (requires LOGFIRE_TOKEN)

legacy API unchanged - both APIs available during transition.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+684 -1
+60 -1
build.zig
··· 4 4 const target = b.standardTargetOptions(.{}); 5 5 const optimize = b.standardOptimizeOption(.{}); 6 6 7 - // library module 7 + // otel-zig dependency 8 + const otel_dep = b.dependency("opentelemetry", .{ 9 + .target = target, 10 + .optimize = optimize, 11 + }); 12 + 13 + // library module (legacy API) 8 14 const mod = b.addModule("logfire", .{ 9 15 .root_source_file = b.path("src/root.zig"), 10 16 .target = target, 11 17 .optimize = optimize, 18 + .imports = &.{ 19 + .{ .name = "otel-api", .module = otel_dep.module("otel-api") }, 20 + .{ .name = "otel-sdk", .module = otel_dep.module("otel-sdk") }, 21 + .{ .name = "otel-exporters", .module = otel_dep.module("otel-exporters") }, 22 + .{ .name = "otel-semconv", .module = otel_dep.module("otel-semconv") }, 23 + }, 24 + }); 25 + 26 + // otel-based wrapper module (new API) 27 + const otel_mod = b.addModule("logfire-otel", .{ 28 + .root_source_file = b.path("src/otel_wrapper.zig"), 29 + .target = target, 30 + .optimize = optimize, 31 + .imports = &.{ 32 + .{ .name = "otel", .module = otel_dep.module("otel") }, 33 + .{ .name = "config", .module = b.createModule(.{ 34 + .root_source_file = b.path("src/config.zig"), 35 + .target = target, 36 + .optimize = optimize, 37 + }) }, 38 + }, 12 39 }); 13 40 14 41 // tests ··· 32 59 const run_example = b.addRunArtifact(example); 33 60 const example_step = b.step("example", "run the basic example"); 34 61 example_step.dependOn(&run_example.step); 62 + 63 + // otel-based example 64 + const otel_example = b.addExecutable(.{ 65 + .name = "otel-example", 66 + .root_module = b.createModule(.{ 67 + .root_source_file = b.path("examples/otel_basic.zig"), 68 + .target = target, 69 + .optimize = optimize, 70 + .imports = &.{.{ .name = "logfire-otel", .module = otel_mod }}, 71 + }), 72 + }); 73 + 74 + const run_otel_example = b.addRunArtifact(otel_example); 75 + const otel_example_step = b.step("otel-example", "run the otel-based example"); 76 + otel_example_step.dependOn(&run_otel_example.step); 77 + 78 + // validation script for otel-zig OTLP export (not in CI, requires LOGFIRE_TOKEN) 79 + const validate_otel = b.addExecutable(.{ 80 + .name = "validate-otel-export", 81 + .root_module = b.createModule(.{ 82 + .root_source_file = b.path("tests/validate_otel_export.zig"), 83 + .target = target, 84 + .optimize = optimize, 85 + .imports = &.{ 86 + .{ .name = "otel", .module = otel_dep.module("otel") }, 87 + }, 88 + }), 89 + }); 90 + 91 + const run_validate_otel = b.addRunArtifact(validate_otel); 92 + const validate_step = b.step("validate-otel", "validate otel-zig OTLP export to logfire (requires LOGFIRE_TOKEN)"); 93 + validate_step.dependOn(&run_validate_otel.step); 35 94 }
+6
build.zig.zon
··· 8 8 "build.zig.zon", 9 9 "src", 10 10 }, 11 + .dependencies = .{ 12 + .opentelemetry = .{ 13 + .url = "https://github.com/ibd1279/otel-zig/archive/refs/heads/trunk.tar.gz", 14 + .hash = "otel-0.0.1-9q9ZWhF9EwACdQz1-ETtOK8585KTaWSFqmGgbfxW4l7c", 15 + }, 16 + }, 11 17 }
+249
docs/otel-adoption-plan.md
··· 1 + # otel-zig adoption plan 2 + 3 + ## status: in progress (2025-01-25) 4 + 5 + this document explores how logfire-zig should adopt [otel-zig](https://github.com/ibd1279/otel-zig) to become a proper OpenTelemetry-based observability library. 6 + 7 + ## background 8 + 9 + ### current state of logfire-zig 10 + 11 + logfire-zig is currently a standalone OTLP exporter with: 12 + - simple span/log API 13 + - OTLP HTTP/JSON export to Logfire 14 + - token-based configuration (Logfire-specific) 15 + - background flush thread 16 + 17 + it does NOT have: 18 + - full OTel API/SDK compliance 19 + - semantic conventions 20 + - context propagation (W3C traceparent) 21 + - metrics support (partial - types exist but incomplete) 22 + - pipeline architecture 23 + 24 + ### python logfire architecture 25 + 26 + python logfire wraps opentelemetry-python: 27 + 28 + ``` 29 + opentelemetry-python (API/SDK) 30 + +-- opentelemetry-instrumentation-* (ecosystem plugins) 31 + +-- logfire (wrapper + Logfire-specific config) 32 + ``` 33 + 34 + this means: 35 + - instrumentation plugins work for ALL OTel users 36 + - logfire users get ecosystem plugins for free 37 + - logfire adds value through config, dashboards, hosted backend 38 + 39 + ### otel-zig 40 + 41 + [otel-zig](https://github.com/ibd1279/otel-zig) by ibd1279 is a comprehensive OTel implementation. 42 + 43 + **zig version:** 0.15.1 (compatible with logfire-zig's 0.15.0 requirement) 44 + 45 + ## findings 46 + 47 + ### logfire-zig implementation analysis 48 + 49 + **architecture:** 6 modules (root, config, span, log, attribute, exporter, metrics) with a central `Logfire` singleton. 50 + 51 + **core types:** 52 + - `Logfire` - singleton managing all observability, background flush thread 53 + - `Span.Data` - immutable span with trace_id, span_id, parent, timestamps, 32 max attributes 54 + - `LogRecord` - severity, message, trace correlation, attributes 55 + - `Attribute` - tagged union with internal string storage (512 bytes) for copy safety 56 + - `MetricData` - union of sum/gauge/histogram/exponential_histogram 57 + 58 + **threading model:** 59 + - thread-local trace context (`threadlocal var tl_trace_id`, `tl_current_span_id`) 60 + - background flush thread (configurable interval) 61 + - mutex-protected pending lists 62 + - atomic span ID counter 63 + 64 + **OTLP export:** 65 + - HTTP/JSON only (no protobuf, no gRPC) 66 + - per-endpoint batching (/v1/traces, /v1/logs, /v1/metrics) 67 + - uses `std.json.Stringify` for streaming JSON generation 68 + - special handling for hex trace IDs, nanosecond timestamps 69 + 70 + **limitations vs full OTel:** 71 + 1. no trace links or span events 72 + 2. no span status (always UNSET) 73 + 3. no resource detection 74 + 4. no W3C traceparent propagation 75 + 5. metrics attributes ignored 76 + 6. no sampling 77 + 7. no pipeline architecture (processor -> exporter) 78 + 8. 32 attribute limit, 512 byte string limit 79 + 9. no semantic conventions 80 + 81 + **strengths:** 82 + - simple, focused API 83 + - memory-safe attribute handling (internal string storage) 84 + - working background export 85 + - Logfire-specific token handling 86 + 87 + ### otel-zig implementation analysis 88 + 89 + **architecture:** proper OTel structure with api/, sdk/, exporters/, semconv/ separation. 90 + 91 + **core types:** 92 + - `Span`, `Tracer`, `TracerProvider` - union-tagged for noop/bridge polymorphism 93 + - `LogRecord`, `Logger`, `LoggerProvider` - same pattern 94 + - `Meter`, `MeterProvider`, `Counter`, `Gauge`, `Histogram` - full metrics 95 + - `SpanContext` - trace_id, span_id, trace_flags, trace_state, is_remote 96 + 97 + **pipeline architecture:** 98 + ``` 99 + Provider -> Processor -> Exporter 100 + ``` 101 + - traces: TracerProvider -> SpanProcessor -> SpanExporter 102 + - logs: LoggerProvider -> LogRecordProcessor -> LogRecordExporter 103 + - metrics: MeterProvider -> Reader -> MetricExporter 104 + - fluent pipeline builder pattern at compile time 105 + 106 + **OTLP export:** 107 + - protobuf-based (using zig-protobuf dependency) 108 + - HTTP transport (gRPC/JSON configurable) 109 + - proper conversion from Zig types to OTLP messages 110 + 111 + **semantic conventions:** 112 + - comprehensive coverage (v1.24.0) 113 + - http, database, rpc, messaging, exception, network, resource 114 + - standard constant values 115 + 116 + **W3C context propagation:** 117 + - traceparent header injection/extraction 118 + - TextMapCarrier abstraction 119 + - marks remote contexts properly 120 + 121 + **maturity: ~70-75% production-ready** 122 + 123 + known gaps (per TODO.md): 124 + - trace state parsing incomplete (potential leak) 125 + - thread-local context storage not implemented 126 + - histogram configurations incomplete 127 + - parent sampling decorator incomplete 128 + 129 + **strengths:** 130 + - proper API/SDK separation 131 + - all three signals (traces, logs, metrics) 132 + - thread-safe implementations 133 + - no-op implementations for safety 134 + - std.log bridge 135 + - comprehensive tests 136 + 137 + ## comparison 138 + 139 + | aspect | logfire-zig | otel-zig | 140 + |--------|-------------|----------| 141 + | zig version | 0.15.0 | 0.15.1 | 142 + | spec compliance | minimal | ~80% | 143 + | API/SDK separation | no | yes | 144 + | semantic conventions | none | comprehensive | 145 + | context propagation | threadlocal only | W3C traceparent | 146 + | OTLP format | HTTP/JSON | HTTP/protobuf | 147 + | pipeline architecture | no | yes | 148 + | metrics support | partial | full | 149 + | std.log bridge | no | yes | 150 + | spans | working | working | 151 + | logs | working | working | 152 + | maturity | simple/focused | comprehensive | 153 + | Logfire-specific | yes (token, endpoint) | no | 154 + 155 + ## recommendation 156 + 157 + **adopt otel-zig as a dependency** rather than reimplementing. 158 + 159 + ### rationale 160 + 161 + 1. **otel-zig is already 70-75% production-ready** - reimplementing would take months 162 + 2. **proper architecture** - API/SDK separation enables future extensibility 163 + 3. **semantic conventions** - we get http, database, messaging, etc. for free 164 + 4. **W3C propagation** - distributed tracing across services 165 + 5. **ecosystem potential** - instrumentation plugins can target otel-zig API 166 + 6. **same zig version** - no compatibility concerns 167 + 168 + ### what logfire-zig becomes 169 + 170 + 1. **thin wrapper around otel-zig** 171 + 2. **Logfire-specific configuration** (token parsing, endpoint selection) 172 + 3. **Logfire OTLP exporter** (configured otel-zig exporter pointing to Logfire) 173 + 4. **convenience APIs** (optional simpler interface for common cases) 174 + 175 + ### migration path 176 + 177 + **phase 1: add otel-zig dependency** 178 + - add to build.zig.zon 179 + - verify builds and tests pass 180 + - verify OTLP export works with Logfire endpoint 181 + 182 + **phase 2: refactor internals** 183 + - replace `Span.Data` with otel-zig spans 184 + - replace `LogRecord` with otel-zig logs 185 + - replace `MetricData` with otel-zig metrics 186 + - keep `Config` for Logfire-specific settings 187 + - use otel-zig pipeline architecture 188 + 189 + **phase 3: update public API** 190 + - option A: keep simple API, wrap otel internally 191 + ```zig 192 + // current API preserved 193 + const span = logfire.span("http.request", .{ .method = "GET" }); 194 + defer span.end(); 195 + ``` 196 + - option B: expose otel API with convenience wrapper 197 + ```zig 198 + // full otel access when needed 199 + var tracer = logfire.getTracer(); 200 + var span = tracer.startSpan("http.request", .{...}, ctx); 201 + ``` 202 + 203 + **phase 4: deprecate redundant code** 204 + - remove custom OTLP JSON serialization (use otel-zig protobuf) 205 + - remove custom span/log types 206 + - keep only Logfire-specific additions 207 + 208 + ### risks and mitigations 209 + 210 + | risk | mitigation | 211 + |------|------------| 212 + | otel-zig protobuf may not work with Logfire | Logfire accepts OTLP protobuf; test early | 213 + | otel-zig API may change | pin to specific commit/version | 214 + | maintenance burden | contribute fixes upstream | 215 + | otel-zig abandoned | fork if necessary (MIT licensed) | 216 + 217 + ### open questions 218 + 219 + 1. **should we contribute instrumentation plugins to otel-zig?** 220 + - http server instrumentation 221 + - database instrumentation 222 + - or separate zig-otel-contrib repo? 223 + 224 + 2. **how much of current logfire-zig API to preserve?** 225 + - simpler API is nice for quick usage 226 + - but full otel API enables advanced use cases 227 + 228 + 3. **should we help complete otel-zig's gaps?** 229 + - thread-local context storage 230 + - trace state handling 231 + - histogram configurations 232 + 233 + ## next steps 234 + 235 + 1. [x] add otel-zig as dependency in build.zig.zon 236 + 2. [x] test OTLP export to Logfire endpoint (validated with `zig build validate-otel`) 237 + 3. [x] prototype wrapper API (`src/otel_wrapper.zig`, exposed as `logfire-otel` module) 238 + 4. [ ] benchmark performance vs current implementation 239 + 5. [ ] complete wrapper API (logs, metrics) 240 + 6. [ ] create migration PR 241 + 242 + ## appendix: key otel-zig files to understand 243 + 244 + - `src/api/trace/span.zig` - span interface 245 + - `src/sdk/trace/root.zig` - tracer provider setup 246 + - `src/exporters/otlp/root.zig` - OTLP exporter config 247 + - `src/semconv/http.zig` - HTTP semantic conventions 248 + - `src/api/trace/w3c_propagator.zig` - context propagation 249 + - `examples/simple_trace_sdk.zig` - usage example
+59
examples/otel_basic.zig
··· 1 + //! otel-based logfire example 2 + //! 3 + //! demonstrates the new otel-zig backed API. 4 + //! 5 + //! run with: 6 + //! LOGFIRE_TOKEN=your_token zig build otel-example 7 + //! 8 + //! or without token for no-op behavior: 9 + //! zig build otel-example 10 + 11 + const std = @import("std"); 12 + const logfire = @import("logfire-otel"); 13 + 14 + pub fn main() !void { 15 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 16 + defer _ = gpa.deinit(); 17 + const allocator = gpa.allocator(); 18 + 19 + // configure with otel-zig backend 20 + const lf = try logfire.OtelLogfire.init(allocator, .{ 21 + .service_name = "logfire-zig-otel-example", 22 + .service_version = "0.1.0", 23 + .environment = "development", 24 + }); 25 + defer lf.shutdown(); 26 + 27 + std.debug.print("logfire initialized (otel backend)\n", .{}); 28 + 29 + // create a span 30 + { 31 + var s = lf.span("example.work"); 32 + defer s.end(); 33 + 34 + // add attributes 35 + s.setAttribute("iteration", @as(i64, 1)); 36 + s.setAttribute("query", "prefect python"); 37 + 38 + // simulate work 39 + std.posix.nanosleep(0, 50 * std.time.ns_per_ms); 40 + } 41 + 42 + // nested spans 43 + { 44 + var outer = lf.span("example.outer"); 45 + defer outer.end(); 46 + 47 + { 48 + var inner = lf.span("example.inner"); 49 + defer inner.end(); 50 + 51 + std.posix.nanosleep(0, 25 * std.time.ns_per_ms); 52 + } 53 + } 54 + 55 + // flush to ensure export 56 + try lf.flush(); 57 + 58 + std.debug.print("example complete - check logfire dashboard\n", .{}); 59 + }
+207
src/otel_wrapper.zig
··· 1 + //! otel-zig wrapper for logfire 2 + //! 3 + //! provides a simple, logfire-specific wrapper around otel-zig. 4 + //! this is phase 3 of the otel-zig adoption plan. 5 + 6 + const std = @import("std"); 7 + const otel = @import("otel"); 8 + const otel_api = otel.api; 9 + const otel_sdk = otel.sdk; 10 + const otel_exporters = otel.exporters; 11 + 12 + const Config = @import("config.zig").Config; 13 + 14 + /// logfire wrapper around otel-zig providers 15 + pub const OtelLogfire = struct { 16 + allocator: std.mem.Allocator, 17 + config: Config, 18 + 19 + // providers (owned) 20 + trace_provider: ?*otel_sdk.trace.TracerProvider = null, 21 + log_provider: ?*otel_sdk.logs.LoggerProvider = null, 22 + // metric_provider: ?*otel_sdk.metrics.MeterProvider = null, 23 + 24 + // cached instruments 25 + tracer: ?otel_api.trace.Tracer = null, 26 + logger: ?otel_api.logs.Logger = null, 27 + 28 + // heap-allocated auth header (must outlive exporter) 29 + auth_header_value: ?[]u8 = null, 30 + headers: ?[]std.http.Header = null, 31 + 32 + const Self = @This(); 33 + 34 + pub fn init(allocator: std.mem.Allocator, config: Config) !*Self { 35 + const resolved = config.resolve(); 36 + 37 + if (!resolved.shouldSend()) { 38 + // return no-op instance 39 + const self = try allocator.create(Self); 40 + self.* = .{ 41 + .allocator = allocator, 42 + .config = resolved, 43 + }; 44 + return self; 45 + } 46 + 47 + const self = try allocator.create(Self); 48 + errdefer allocator.destroy(self); 49 + 50 + self.* = .{ 51 + .allocator = allocator, 52 + .config = resolved, 53 + }; 54 + 55 + // set up trace provider with OTLP exporter 56 + if (resolved.token) |token| { 57 + const endpoint = getEndpointFromToken(token); 58 + 59 + // heap-allocate auth header value (must outlive exporter) 60 + self.auth_header_value = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token}); 61 + errdefer if (self.auth_header_value) |v| allocator.free(v); 62 + 63 + // heap-allocate headers array 64 + self.headers = try allocator.alloc(std.http.Header, 1); 65 + errdefer if (self.headers) |h| allocator.free(h); 66 + 67 + self.headers.?[0] = .{ 68 + .name = "Authorization", 69 + .value = self.auth_header_value.?, 70 + }; 71 + 72 + const otlp_config = otel_exporters.otlp.OtlpExporterConfig{ 73 + .endpoint = endpoint, 74 + .transport = .http_protobuf, 75 + .headers = self.headers.?, 76 + }; 77 + 78 + // set up trace provider 79 + self.trace_provider = try otel_sdk.trace.setupGlobalProvider( 80 + allocator, 81 + .{otel_sdk.trace.BasicSpanProcessor.PipelineStep.init({}) 82 + .flowTo(otel_exporters.otlp.OtlpTraceExporter.PipelineStep.init(otlp_config))}, 83 + ); 84 + 85 + // get tracer 86 + const scope = otel_api.InstrumentationScope{ 87 + .name = resolved.service_name orelse "logfire-zig", 88 + .version = resolved.service_version orelse "0.1.0", 89 + }; 90 + self.tracer = try otel_api.getGlobalTracerProvider().getTracerWithScope(scope); 91 + } 92 + 93 + return self; 94 + } 95 + 96 + pub fn deinit(self: *Self) void { 97 + if (self.trace_provider) |p| { 98 + p.deinit(); 99 + p.destroy(); 100 + } 101 + if (self.log_provider) |p| { 102 + p.deinit(); 103 + p.destroy(); 104 + } 105 + otel_api.provider_registry.unsetAllProviders(); 106 + 107 + // free heap-allocated header data 108 + if (self.headers) |h| self.allocator.free(h); 109 + if (self.auth_header_value) |v| self.allocator.free(v); 110 + 111 + self.allocator.destroy(self); 112 + } 113 + 114 + /// create a span with the given name and attributes 115 + pub fn span(self: *Self, name: []const u8) Span { 116 + if (self.tracer) |*tracer| { 117 + const ctx = &[_]otel_api.ContextKeyValue{}; 118 + const span_result = tracer.startSpan(name, .{ 119 + .kind = .internal, 120 + }, ctx) catch { 121 + return .{ .inner = null }; 122 + }; 123 + return .{ .inner = span_result }; 124 + } 125 + return .{ .inner = null }; 126 + } 127 + 128 + /// flush any pending data 129 + pub fn flush(self: *Self) !void { 130 + if (self.trace_provider) |p| { 131 + _ = p.forceFlush(null); 132 + } 133 + } 134 + 135 + /// shutdown the provider 136 + pub fn shutdown(self: *Self) void { 137 + if (self.trace_provider) |p| { 138 + _ = p.shutdown(null); 139 + } 140 + self.deinit(); 141 + } 142 + }; 143 + 144 + /// span wrapper for ergonomic usage 145 + pub const Span = struct { 146 + inner: ?otel_api.trace.Span, 147 + 148 + pub fn end(self: *Span) void { 149 + if (self.inner) |*s| { 150 + s.end(null); 151 + s.deinit(); 152 + self.inner = null; 153 + } 154 + } 155 + 156 + pub fn setAttribute(self: *Span, key: []const u8, value: anytype) void { 157 + if (self.inner) |*s| { 158 + const attr_value = toAttributeValue(value); 159 + s.setAttribute(.{ .key = key, .value = attr_value }); 160 + } 161 + } 162 + }; 163 + 164 + fn toAttributeValue(value: anytype) otel_api.common.AttributeValue { 165 + const T = @TypeOf(value); 166 + return switch (@typeInfo(T)) { 167 + .int, .comptime_int => .{ .int = @intCast(value) }, 168 + .float, .comptime_float => .{ .double = @floatCast(value) }, 169 + .bool => .{ .bool = value }, 170 + .pointer => |ptr| { 171 + // handle []const u8 (slices) 172 + if (ptr.size == .slice and ptr.child == u8) { 173 + return .{ .string = value }; 174 + } 175 + // handle *const [N]u8 and *const [N:0]u8 (string literals) 176 + if (ptr.size == .one) { 177 + const child_info = @typeInfo(ptr.child); 178 + if (child_info == .array and child_info.array.child == u8) { 179 + return .{ .string = value }; 180 + } 181 + } 182 + @compileError("unsupported pointer type for attribute"); 183 + }, 184 + else => @compileError("unsupported type for attribute: " ++ @typeName(T)), 185 + }; 186 + } 187 + 188 + /// extract region from logfire token and return base OTLP endpoint 189 + fn getEndpointFromToken(token: []const u8) []const u8 { 190 + if (std.mem.startsWith(u8, token, "pylf_v")) { 191 + var it = std.mem.splitScalar(u8, token, '_'); 192 + _ = it.next(); // pylf 193 + _ = it.next(); // v1 194 + if (it.next()) |region| { 195 + if (std.mem.eql(u8, region, "eu")) { 196 + return "https://logfire-eu.pydantic.dev:443"; 197 + } 198 + } 199 + } 200 + return "https://logfire-us.pydantic.dev:443"; 201 + } 202 + 203 + // convenience function to create and configure 204 + pub fn configure(config: Config) !*OtelLogfire { 205 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 206 + return OtelLogfire.init(gpa.allocator(), config); 207 + }
+103
tests/validate_otel_export.zig
··· 1 + //! otel-zig OTLP export test 2 + //! 3 + //! tests that otel-zig's OTLP exporter works with logfire endpoint. 4 + //! 5 + //! run with: 6 + //! LOGFIRE_TOKEN=your_token zig build otel-test 7 + //! 8 + //! this validates phase 1 of the otel-zig adoption plan. 9 + 10 + const std = @import("std"); 11 + const otel = @import("otel"); 12 + const otel_api = otel.api; 13 + const otel_sdk = otel.sdk; 14 + const otel_exporters = otel.exporters; 15 + 16 + pub fn main() !void { 17 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 18 + defer _ = gpa.deinit(); 19 + const allocator = gpa.allocator(); 20 + 21 + // get logfire token and endpoint from env 22 + const token = std.posix.getenv("LOGFIRE_WRITE_TOKEN") orelse 23 + std.posix.getenv("LOGFIRE_TOKEN") orelse { 24 + std.debug.print("LOGFIRE_TOKEN not set, skipping OTLP test\n", .{}); 25 + return; 26 + }; 27 + 28 + // determine endpoint from token region 29 + const endpoint = getEndpointFromToken(token); 30 + std.debug.print("using endpoint: {s}\n", .{endpoint}); 31 + 32 + // build authorization header 33 + var auth_header_buf: [256]u8 = undefined; 34 + const auth_value = std.fmt.bufPrint(&auth_header_buf, "Bearer {s}", .{token}) catch { 35 + std.debug.print("token too long\n", .{}); 36 + return; 37 + }; 38 + 39 + // configure OTLP exporter for logfire 40 + const otlp_config = otel_exporters.otlp.OtlpExporterConfig{ 41 + .endpoint = endpoint, 42 + .transport = .http_protobuf, 43 + .headers = &[_]std.http.Header{ 44 + .{ .name = "Authorization", .value = auth_value }, 45 + }, 46 + }; 47 + 48 + // set up trace provider with OTLP exporter 49 + const provider = try otel_sdk.trace.setupGlobalProvider( 50 + allocator, 51 + .{otel_sdk.trace.BasicSpanProcessor.PipelineStep.init({}) 52 + .flowTo(otel_exporters.otlp.OtlpTraceExporter.PipelineStep.init(otlp_config))}, 53 + ); 54 + defer { 55 + provider.deinit(); 56 + provider.destroy(); 57 + } 58 + defer otel_api.provider_registry.unsetAllProviders(); 59 + 60 + // get tracer 61 + const scope = otel_api.InstrumentationScope{ 62 + .name = "logfire-zig-otel-test", 63 + .version = "0.1.0", 64 + }; 65 + var tracer = try otel_api.getGlobalTracerProvider().getTracerWithScope(scope); 66 + 67 + // create a test span 68 + const ctx = &[_]otel_api.ContextKeyValue{}; 69 + var span_result = try tracer.startSpan("otel-zig-test-span", .{ 70 + .kind = .internal, 71 + .attributes = &[_]otel_api.common.AttributeKeyValue{ 72 + .{ .key = "test.source", .value = .{ .string = "logfire-zig" } }, 73 + .{ .key = "test.type", .value = .{ .string = "otel-adoption-validation" } }, 74 + }, 75 + }, ctx); 76 + 77 + // simulate some work 78 + std.posix.nanosleep(0, 50 * std.time.ns_per_ms); 79 + 80 + // end span (triggers export) 81 + span_result.end(null); 82 + span_result.deinit(); 83 + 84 + std.debug.print("span exported successfully!\n", .{}); 85 + std.debug.print("check logfire dashboard for 'otel-zig-test-span'\n", .{}); 86 + } 87 + 88 + /// extract region from logfire token and return base OTLP endpoint 89 + /// note: otel-zig appends /v1/traces path automatically 90 + fn getEndpointFromToken(token: []const u8) []const u8 { 91 + // token format: pylf_v{version}_{region}_{token} 92 + if (std.mem.startsWith(u8, token, "pylf_v")) { 93 + var it = std.mem.splitScalar(u8, token, '_'); 94 + _ = it.next(); // pylf 95 + _ = it.next(); // v1 96 + if (it.next()) |region| { 97 + if (std.mem.eql(u8, region, "eu")) { 98 + return "https://logfire-eu.pydantic.dev:443"; 99 + } 100 + } 101 + } 102 + return "https://logfire-us.pydantic.dev:443"; 103 + }