logfire client for zig
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix trace ID, metric temporality, and memory leak

1. trace ID: generate new ID for each root span (when no active spans)
- add active_span_count tracking
- add newTrace() for explicit trace boundaries

2. metrics: change from cumulative to delta temporality
- each counter() call is an increment, not aggregated total

3. memory: track allocated NumberDataPoints and free on flush
- allocated_data_points list tracks allocations
- freed in flush() and deinit()

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

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

zzstoatzz 3e05dab1 560715ef

+65 -14
+64 -14
src/root.zig
··· 62 62 pending_metrics: std.ArrayList(MetricData), 63 63 pending_mutex: std.Thread.Mutex, 64 64 65 + /// allocated data points (freed on flush) 66 + allocated_data_points: std.ArrayList(*NumberDataPoint), 67 + 65 68 /// trace context 66 69 current_trace_id: ?[16]u8, 70 + active_span_count: std.atomic.Value(u32), 67 71 span_id_counter: std.atomic.Value(u64), 68 72 69 73 /// background flush thread ··· 82 86 .pending_logs = .{}, 83 87 .pending_metrics = .{}, 84 88 .pending_mutex = .{}, 89 + .allocated_data_points = .{}, 85 90 .current_trace_id = null, 91 + .active_span_count = std.atomic.Value(u32).init(0), 86 92 .span_id_counter = std.atomic.Value(u64).init(1), 87 93 .flush_thread = null, 88 94 .running = std.atomic.Value(bool).init(true), 89 95 }; 90 - 91 - // generate initial trace id 92 - self.current_trace_id = generateTraceId(); 93 96 94 97 // start background flush thread 95 98 if (resolved.batch_timeout_ms > 0) { ··· 111 114 } 112 115 113 116 pub fn deinit(self: *Logfire) void { 117 + // free any remaining allocated data points 118 + for (self.allocated_data_points.items) |dp| { 119 + self.allocator.destroy(dp); 120 + } 121 + self.allocated_data_points.deinit(self.allocator); 114 122 self.pending_spans.deinit(self.allocator); 115 123 self.pending_logs.deinit(self.allocator); 116 124 self.pending_metrics.deinit(self.allocator); ··· 152 160 self.pending_spans.clearRetainingCapacity(); 153 161 self.pending_logs.clearRetainingCapacity(); 154 162 self.pending_metrics.clearRetainingCapacity(); 163 + 164 + // free allocated data points 165 + for (self.allocated_data_points.items) |dp| { 166 + self.allocator.destroy(dp); 167 + } 168 + self.allocated_data_points.clearRetainingCapacity(); 155 169 } 156 170 } 157 171 158 172 pub fn createSpan(self: *Logfire, name: []const u8, attributes: anytype) Span { 173 + // generate new trace ID if no active spans (root span) 174 + if (self.active_span_count.fetchAdd(1, .acquire) == 0) { 175 + self.current_trace_id = generateTraceId(); 176 + } 159 177 const span_id = self.span_id_counter.fetchAdd(1, .monotonic); 160 178 return Span.init(self, name, span_id, attributes); 179 + } 180 + 181 + /// called when a span ends to track active span count 182 + pub fn spanEnded(self: *Logfire) void { 183 + _ = self.active_span_count.fetchSub(1, .release); 184 + } 185 + 186 + /// start a new trace (generates new trace ID) 187 + pub fn newTrace(self: *Logfire) void { 188 + self.current_trace_id = generateTraceId(); 161 189 } 162 190 163 191 pub fn recordLog(self: *Logfire, level: Level, message: []const u8, attributes: anytype) void { ··· 191 219 }; 192 220 } 193 221 194 - /// record a counter value (monotonic sum) 222 + /// record a counter value (monotonic sum, delta temporality) 195 223 pub fn recordCounter(self: *Logfire, name: []const u8, value: i64, opts: InstrumentOptions) void { 196 224 const now = std.time.nanoTimestamp(); 197 225 const dp = self.allocator.create(NumberDataPoint) catch return; ··· 203 231 204 232 self.pending_mutex.lock(); 205 233 defer self.pending_mutex.unlock(); 234 + 235 + // track allocation for cleanup 236 + self.allocated_data_points.append(self.allocator, dp) catch { 237 + self.allocator.destroy(dp); 238 + return; 239 + }; 240 + 206 241 self.pending_metrics.append(self.allocator, .{ 207 242 .name = name, 208 243 .description = opts.description, ··· 210 245 .data = .{ 211 246 .sum = .{ 212 247 .data_points = @as(*const [1]NumberDataPoint, dp), 213 - .temporality = .cumulative, 248 + .temporality = .delta, // delta since we send individual increments 214 249 .is_monotonic = true, 215 250 }, 216 251 }, 217 - }) catch { 218 - self.allocator.destroy(dp); 219 - }; 252 + }) catch {}; 220 253 } 221 254 222 255 /// record a gauge value (instantaneous) ··· 231 264 232 265 self.pending_mutex.lock(); 233 266 defer self.pending_mutex.unlock(); 267 + 268 + // track allocation for cleanup 269 + self.allocated_data_points.append(self.allocator, dp) catch { 270 + self.allocator.destroy(dp); 271 + return; 272 + }; 273 + 234 274 self.pending_metrics.append(self.allocator, .{ 235 275 .name = name, 236 276 .description = opts.description, ··· 240 280 .data_points = @as(*const [1]NumberDataPoint, dp), 241 281 }, 242 282 }, 243 - }) catch { 244 - self.allocator.destroy(dp); 245 - }; 283 + }) catch {}; 246 284 } 247 285 248 286 /// record a gauge value (instantaneous, f64) ··· 257 295 258 296 self.pending_mutex.lock(); 259 297 defer self.pending_mutex.unlock(); 298 + 299 + // track allocation for cleanup 300 + self.allocated_data_points.append(self.allocator, dp) catch { 301 + self.allocator.destroy(dp); 302 + return; 303 + }; 304 + 260 305 self.pending_metrics.append(self.allocator, .{ 261 306 .name = name, 262 307 .description = opts.description, ··· 266 311 .data_points = @as(*const [1]NumberDataPoint, dp), 267 312 }, 268 313 }, 269 - }) catch { 270 - self.allocator.destroy(dp); 271 - }; 314 + }) catch {}; 272 315 } 273 316 274 317 fn generateTraceId() [16]u8 { ··· 304 347 return lf.createSpan(name, attributes); 305 348 } 306 349 return Span.noop(); 350 + } 351 + 352 + /// start a new trace (generates new trace ID for subsequent spans) 353 + pub fn newTrace() void { 354 + if (getInstance()) |lf| { 355 + lf.newTrace(); 356 + } 307 357 } 308 358 309 359 pub fn trace(comptime fmt: []const u8, args: anytype) void {
+1
src/span.zig
··· 77 77 var data = self.data; 78 78 data.end_time_ns = std.time.nanoTimestamp(); 79 79 lf.recordSpanEnd(data); 80 + lf.spanEnded(); 80 81 } 81 82 } 82 83 };