logfire client for zig
1//! logfire configuration
2//!
3//! mirrors the rust client's builder pattern.
4//! supports both explicit config and environment variables.
5
6const std = @import("std");
7
8pub const Config = struct {
9 /// service name (required for logfire)
10 /// env: LOGFIRE_SERVICE_NAME, OTEL_SERVICE_NAME
11 service_name: ?[]const u8 = null,
12
13 /// service version
14 /// env: LOGFIRE_SERVICE_VERSION, OTEL_SERVICE_VERSION
15 service_version: ?[]const u8 = null,
16
17 /// deployment environment (e.g., "production", "staging")
18 /// env: LOGFIRE_ENVIRONMENT
19 environment: ?[]const u8 = null,
20
21 /// logfire/OTLP write token
22 /// env: LOGFIRE_TOKEN
23 token: ?[]const u8 = null,
24
25 /// base URL for logfire API
26 /// env: OTEL_EXPORTER_OTLP_ENDPOINT
27 /// defaults based on token region (us/eu)
28 base_url: ?[]const u8 = null,
29
30 /// whether to send to logfire
31 /// if false, just prints to console
32 send_to_logfire: SendToLogfire = .if_token_present,
33
34 /// minimum log level to capture
35 default_level: Level = .info,
36
37 /// console output options
38 console: ?ConsoleOptions = .{},
39
40 /// batch export settings
41 batch_size: usize = 512,
42 batch_timeout_ms: u64 = 500,
43
44 pub const SendToLogfire = enum {
45 yes,
46 no,
47 if_token_present,
48 };
49
50 pub const Level = enum {
51 trace,
52 debug,
53 info,
54 warn,
55 err,
56
57 pub fn severity(self: Level) u8 {
58 return switch (self) {
59 .trace => 1,
60 .debug => 5,
61 .info => 9,
62 .warn => 13,
63 .err => 17,
64 };
65 }
66
67 pub fn name(self: Level) []const u8 {
68 return @tagName(self);
69 }
70 };
71
72 pub const ConsoleOptions = struct {
73 enabled: bool = true,
74 colors: bool = true,
75 min_level: Level = .info,
76 };
77
78 /// resolve config from explicit values + environment
79 pub fn resolve(self: Config) Config {
80 var resolved = self;
81
82 // token
83 if (resolved.token == null) {
84 resolved.token = std.posix.getenv("LOGFIRE_WRITE_TOKEN") orelse
85 std.posix.getenv("LOGFIRE_TOKEN");
86 }
87
88 // service name
89 if (resolved.service_name == null) {
90 resolved.service_name = std.posix.getenv("LOGFIRE_SERVICE_NAME") orelse
91 std.posix.getenv("OTEL_SERVICE_NAME");
92 }
93
94 // service version
95 if (resolved.service_version == null) {
96 resolved.service_version = std.posix.getenv("LOGFIRE_SERVICE_VERSION") orelse
97 std.posix.getenv("OTEL_SERVICE_VERSION");
98 }
99
100 // environment
101 if (resolved.environment == null) {
102 resolved.environment = std.posix.getenv("LOGFIRE_ENVIRONMENT");
103 }
104
105 // base URL - derive from token region if not explicit
106 if (resolved.base_url == null) {
107 if (std.posix.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) |endpoint| {
108 resolved.base_url = endpoint;
109 } else if (resolved.token) |token| {
110 resolved.base_url = getBaseUrlFromToken(token);
111 }
112 }
113
114 return resolved;
115 }
116
117 /// determine if we should actually send data
118 pub fn shouldSend(self: Config) bool {
119 return switch (self.send_to_logfire) {
120 .yes => true,
121 .no => false,
122 .if_token_present => self.token != null,
123 };
124 }
125};
126
127/// extract region from logfire token and return appropriate endpoint
128/// token format: pylf_v{version}_{region}_{token}
129fn getBaseUrlFromToken(token: []const u8) []const u8 {
130 // pylf_v1_eu_xxxxx -> eu
131 // pylf_v1_us_xxxxx -> us
132 if (std.mem.startsWith(u8, token, "pylf_v")) {
133 var it = std.mem.splitScalar(u8, token, '_');
134 _ = it.next(); // pylf
135 _ = it.next(); // v1
136 if (it.next()) |region| {
137 if (std.mem.eql(u8, region, "eu")) {
138 return "https://logfire-eu.pydantic.dev";
139 }
140 }
141 }
142 // default to US
143 return "https://logfire-us.pydantic.dev";
144}
145
146// tests
147
148test "resolve from environment" {
149 // can't easily test env vars, but test the default resolution
150 const config = Config{
151 .service_name = "test",
152 };
153 const resolved = config.resolve();
154 try std.testing.expectEqualStrings("test", resolved.service_name.?);
155}
156
157test "getBaseUrlFromToken us" {
158 const url = getBaseUrlFromToken("pylf_v1_us_abc123");
159 try std.testing.expectEqualStrings("https://logfire-us.pydantic.dev", url);
160}
161
162test "getBaseUrlFromToken eu" {
163 const url = getBaseUrlFromToken("pylf_v1_eu_abc123");
164 try std.testing.expectEqualStrings("https://logfire-eu.pydantic.dev", url);
165}
166
167test "getBaseUrlFromToken unknown defaults to us" {
168 const url = getBaseUrlFromToken("some_other_token");
169 try std.testing.expectEqualStrings("https://logfire-us.pydantic.dev", url);
170}