search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

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

docs: replace logfire design notes with adoption guide

- remove zig-logfire-client.md (pre-implementation design notes)
- add logfire-zig-adoption.md (concrete integration guide)

the client exists now, design exploration is obsolete.

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

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

zzstoatzz 365fff8f c42aeb68

+272 -259
+272
docs/scratch/logfire-zig-adoption.md
··· 1 + # logfire-zig adoption guide for leaflet-search 2 + 3 + guide for integrating logfire-zig into the leaflet-search backend. 4 + 5 + ## 1. add dependency 6 + 7 + in `backend/build.zig.zon`: 8 + 9 + ```zig 10 + .dependencies = .{ 11 + // ... existing deps ... 12 + .logfire = .{ 13 + .url = "https://tangled.sh/zzstoatzz.io/logfire-zig/archive/main", 14 + .hash = "...", // run zig build to get hash 15 + }, 16 + }, 17 + ``` 18 + 19 + in `backend/build.zig`, add the import: 20 + 21 + ```zig 22 + const logfire = b.dependency("logfire", .{ 23 + .target = target, 24 + .optimize = optimize, 25 + }); 26 + exe.root_module.addImport("logfire", logfire.module("logfire")); 27 + ``` 28 + 29 + ## 2. configure in main.zig 30 + 31 + ```zig 32 + const std = @import("std"); 33 + const logfire = @import("logfire"); 34 + // ... other imports ... 35 + 36 + pub fn main() !void { 37 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 38 + defer _ = gpa.deinit(); 39 + const allocator = gpa.allocator(); 40 + 41 + // configure logfire early 42 + // reads LOGFIRE_WRITE_TOKEN from env automatically 43 + const lf = try logfire.configure(.{ 44 + .service_name = "leaflet-search", 45 + .service_version = "0.0.1", 46 + .environment = std.posix.getenv("FLY_APP_NAME") orelse "development", 47 + }); 48 + defer lf.shutdown(); 49 + 50 + logfire.info("starting leaflet-search on port {d}", .{port}); 51 + 52 + // ... rest of main ... 53 + } 54 + ``` 55 + 56 + ## 3. replace timing.zig with spans 57 + 58 + current pattern in server.zig: 59 + 60 + ```zig 61 + fn handleSearch(request: *http.Server.Request, target: []const u8) !void { 62 + const start_time = std.time.microTimestamp(); 63 + defer timing.record(.search, start_time); 64 + // ... 65 + } 66 + ``` 67 + 68 + with logfire: 69 + 70 + ```zig 71 + fn handleSearch(request: *http.Server.Request, target: []const u8) !void { 72 + const span = logfire.span("search.handle", .{}); 73 + defer span.end(); 74 + 75 + // parse params 76 + const query = parseQueryParam(alloc, target, "q") catch ""; 77 + 78 + // add attributes after parsing 79 + span.setAttribute("query", query); 80 + span.setAttribute("tag", tag_filter orelse ""); 81 + 82 + // ... 83 + } 84 + ``` 85 + 86 + for nested operations: 87 + 88 + ```zig 89 + fn search(alloc: Allocator, query: []const u8, ...) ![]Result { 90 + const span = logfire.span("search.execute", .{ 91 + .query_length = @intCast(query.len), 92 + }); 93 + defer span.end(); 94 + 95 + // FTS query 96 + { 97 + const fts_span = logfire.span("search.fts", .{}); 98 + defer fts_span.end(); 99 + // ... FTS logic ... 100 + } 101 + 102 + // vector search fallback 103 + if (results.len < limit) { 104 + const vec_span = logfire.span("search.vector", .{}); 105 + defer vec_span.end(); 106 + // ... vector search ... 107 + } 108 + 109 + return results; 110 + } 111 + ``` 112 + 113 + ## 4. add structured logging 114 + 115 + replace `std.debug.print` with logfire: 116 + 117 + ```zig 118 + // before 119 + std.debug.print("accept error: {}\n", .{err}); 120 + 121 + // after 122 + logfire.err("accept error: {}", .{err}); 123 + ``` 124 + 125 + ```zig 126 + // before 127 + std.debug.print("{s} listening on http://0.0.0.0:{d}\n", .{app_name, port}); 128 + 129 + // after 130 + logfire.info("{s} listening on port {d}", .{app_name, port}); 131 + ``` 132 + 133 + for sync operations in tap.zig: 134 + 135 + ```zig 136 + logfire.info("sync complete", .{}); 137 + logfire.debug("processed {d} events", .{event_count}); 138 + ``` 139 + 140 + for errors: 141 + 142 + ```zig 143 + logfire.err("turso query failed: {}", .{@errorName(err)}); 144 + ``` 145 + 146 + ## 5. add metrics 147 + 148 + replace stats.zig counters with logfire metrics: 149 + 150 + ```zig 151 + // before (in stats.zig) 152 + pub fn recordSearch(query: []const u8) void { 153 + total_searches.fetchAdd(1, .monotonic); 154 + // ... 155 + } 156 + 157 + // with logfire (in server.zig or stats.zig) 158 + pub fn recordSearch(query: []const u8) void { 159 + logfire.counter("search.total", 1); 160 + // existing logic... 161 + } 162 + ``` 163 + 164 + for gauges (e.g., active connections, document counts): 165 + 166 + ```zig 167 + logfire.gaugeInt("documents.indexed", doc_count); 168 + logfire.gaugeInt("connections.active", active_count); 169 + ``` 170 + 171 + for latency histograms (more detail than counter): 172 + 173 + ```zig 174 + // after search completes 175 + logfire.metric(.{ 176 + .name = "search.latency_ms", 177 + .unit = "ms", 178 + .data = .{ 179 + .histogram = .{ 180 + .data_points = &[_]logfire.HistogramDataPoint{.{ 181 + .start_time_ns = start_ns, 182 + .time_ns = std.time.nanoTimestamp(), 183 + .count = 1, 184 + .sum = latency_ms, 185 + .bucket_counts = ..., 186 + .explicit_bounds = ..., 187 + .min = latency_ms, 188 + .max = latency_ms, 189 + }}, 190 + }, 191 + }, 192 + }); 193 + ``` 194 + 195 + ## 6. deployment 196 + 197 + add to fly.toml secrets: 198 + 199 + ```bash 200 + fly secrets set LOGFIRE_WRITE_TOKEN=pylf_v1_us_xxxxx --app leaflet-search-backend 201 + ``` 202 + 203 + logfire-zig reads from `LOGFIRE_WRITE_TOKEN` or `LOGFIRE_TOKEN` automatically. 204 + 205 + ## 7. what to keep from existing code 206 + 207 + **keep timing.zig** - it provides local latency histograms for the dashboard API. logfire spans complement this with distributed tracing. 208 + 209 + **keep stats.zig** - local counters are still useful for the `/stats` endpoint. logfire metrics add remote observability. 210 + 211 + **keep activity.zig** - tracks recent activity for the dashboard. orthogonal to logfire. 212 + 213 + the pattern is: local state for dashboard UI, logfire for observability. 214 + 215 + ## 8. migration order 216 + 217 + 1. add dependency, configure in main.zig 218 + 2. add spans to request handlers (search, similar, tags, popular) 219 + 3. add structured logging for errors and important events 220 + 4. add metrics for key counters 221 + 5. gradually replace `std.debug.print` with logfire logging 222 + 6. consider removing timing.zig if logfire histograms are sufficient 223 + 224 + ## 9. example: full search handler 225 + 226 + ```zig 227 + fn handleSearch(request: *http.Server.Request, target: []const u8) !void { 228 + const span = logfire.span("http.search", .{}); 229 + defer span.end(); 230 + 231 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 232 + defer arena.deinit(); 233 + const alloc = arena.allocator(); 234 + 235 + const query = parseQueryParam(alloc, target, "q") catch ""; 236 + const tag_filter = parseQueryParam(alloc, target, "tag") catch null; 237 + 238 + if (query.len == 0 and tag_filter == null) { 239 + logfire.debug("empty search request", .{}); 240 + try sendJson(request, "{\"error\":\"enter a search term\"}"); 241 + return; 242 + } 243 + 244 + const results = search.search(alloc, query, tag_filter, null, null) catch |err| { 245 + logfire.err("search failed: {}", .{@errorName(err)}); 246 + stats.recordError(); 247 + return err; 248 + }; 249 + 250 + logfire.counter("search.requests", 1); 251 + logfire.info("search completed", .{}); 252 + 253 + // ... send response ... 254 + } 255 + ``` 256 + 257 + ## 10. verifying it works 258 + 259 + run locally: 260 + 261 + ```bash 262 + LOGFIRE_WRITE_TOKEN=pylf_v1_us_xxx zig build run 263 + ``` 264 + 265 + check logfire dashboard for traces from `leaflet-search` service. 266 + 267 + without token (console fallback): 268 + 269 + ```bash 270 + zig build run 271 + # prints [span], [info], [metric] to stderr 272 + ```
-259
docs/scratch/zig-logfire-client.md
··· 1 - # zig logfire client - design notes 2 - 3 - a potential official logfire client for zig. this document captures what would be useful from a practitioner's perspective, referencing existing implementations and protocol details. 4 - 5 - ## motivation 6 - 7 - logfire has official clients for python, rust, and typescript. a zig client would enable observability for zig applications (like this one - leaflet-search) that want to participate in the pydantic/logfire ecosystem. 8 - 9 - zig's growing use in performance-critical systems (databases, network services, game engines) makes it a natural fit. these are exactly the applications where observability matters most. 10 - 11 - ## what logfire actually is 12 - 13 - logfire is an opinionated wrapper around opentelemetry. it's not a replacement - it's a distribution that: 14 - 15 - 1. simplifies configuration (single `configure()` call vs. wiring up providers/exporters manually) 16 - 2. provides ergonomic macros for structured logging and spans 17 - 3. sends data to logfire's platform (or any OTLP-compatible backend) 18 - 4. follows opentelemetry standards (OTLP protocol, semantic conventions, W3C trace context) 19 - 20 - the key insight: you don't need to implement opentelemetry from scratch. you need to implement a thin wrapper that configures OTLP export and provides nice ergonomics. 21 - 22 - ## protocol details 23 - 24 - from the [logfire alternative clients documentation](https://logfire.pydantic.dev/docs/how-to-guides/alternative-clients/): 25 - 26 - **endpoints:** 27 - - US: `https://logfire-us.pydantic.dev` 28 - - EU: `https://logfire-eu.pydantic.dev` 29 - - signals: `/v1/traces`, `/v1/metrics`, `/v1/logs` 30 - 31 - **authentication:** 32 - - header: `Authorization: {write-token}` 33 - - token obtained from logfire project settings 34 - 35 - **protocol:** 36 - - HTTP with protobuf encoding (`http/protobuf`) - preferred 37 - - HTTP with JSON encoding (`http/json`) - also supported 38 - - no gRPC requirement 39 - 40 - ## reference: rust client architecture 41 - 42 - the [logfire-rust](https://github.com/pydantic/logfire-rust) client (used in find-bufo) demonstrates the pattern: 43 - 44 - ``` 45 - src/ 46 - ├── lib.rs # public API, re-exports 47 - ├── config.rs # LogfireConfigBuilder 48 - ├── exporters.rs # OTLP exporter setup 49 - ├── logfire.rs # core Logfire struct 50 - ├── macros/ # span!(), info!(), debug!(), etc. 51 - ├── metrics.rs # counter, gauge, histogram 52 - ├── bridges/ # integration with tracing/log crates 53 - └── internal/ # implementation details 54 - ``` 55 - 56 - key design decisions from rust client: 57 - 58 - 1. **builder pattern for configuration** 59 - ```rust 60 - let logfire = logfire::configure() 61 - .with_default_level_filter(LevelFilter::INFO) 62 - .finish()?; 63 - ``` 64 - 65 - 2. **shutdown guard for clean exit** 66 - ```rust 67 - let _guard = logfire.shutdown_guard(); 68 - // spans/logs flushed when guard drops 69 - ``` 70 - 71 - 3. **structured logging macros** 72 - ```rust 73 - logfire::info!("search completed", 74 - query = &query_text, 75 - results_count = count as i64 76 - ); 77 - ``` 78 - 79 - 4. **span creation with attributes** 80 - ```rust 81 - let _span = logfire::span!( 82 - "turbopuffer.vector_search", 83 - query = &query, 84 - top_k = k as i64 85 - ).entered(); 86 - ``` 87 - 88 - 5. **wraps opentelemetry, doesn't replace it** 89 - - uses `opentelemetry-otlp` crate for export 90 - - uses `tracing` crate for span/event capture 91 - - provides bridge to integrate with existing tracing code 92 - 93 - ## what would be useful for zig 94 - 95 - ### configuration 96 - 97 - ```zig 98 - const logfire = @import("logfire"); 99 - 100 - pub fn main() !void { 101 - const lf = try logfire.configure(.{ 102 - .service_name = "leaflet-search", 103 - .default_level = .info, 104 - }); 105 - defer lf.shutdown(); 106 - 107 - // ... 108 - } 109 - ``` 110 - 111 - environment variables should work automatically: 112 - - `LOGFIRE_TOKEN` - required for sending to logfire 113 - - `LOGFIRE_SERVICE_NAME` - optional override 114 - - `OTEL_EXPORTER_OTLP_ENDPOINT` - for custom backends 115 - 116 - ### spans 117 - 118 - ```zig 119 - const span = logfire.span("sync.full_sync", .{ 120 - .doc_count = doc_count, 121 - .pub_count = pub_count, 122 - }); 123 - defer span.end(); 124 - 125 - // work happens here 126 - ``` 127 - 128 - or with a callback pattern: 129 - 130 - ```zig 131 - try logfire.withSpan("db.query", .{ .sql = sql }, struct { 132 - fn execute(ctx: *Context) !void { 133 - // work 134 - } 135 - }.execute, &ctx); 136 - ``` 137 - 138 - ### structured logging 139 - 140 - ```zig 141 - logfire.info("search completed", .{ 142 - .query = query, 143 - .results_count = @intCast(results.len), 144 - .latency_ms = timer.read() / std.time.ns_per_ms, 145 - }); 146 - 147 - logfire.err("sync failed", .{ 148 - .error = @errorName(err), 149 - .offset = offset, 150 - }); 151 - ``` 152 - 153 - the key is compile-time type safety on the structured fields while producing OTLP-compatible attribute encoding. 154 - 155 - ### metrics 156 - 157 - ```zig 158 - const search_latency = logfire.histogram("search.latency_ms", .{ 159 - .unit = "ms", 160 - .description = "search request latency", 161 - }); 162 - 163 - // later 164 - search_latency.record(elapsed_ms, .{ .endpoint = "/search" }); 165 - ``` 166 - 167 - ### what i'd actually use day-to-day 168 - 169 - from working on leaflet-search, the most common patterns are: 170 - 171 - 1. **timing operations** 172 - ```zig 173 - const span = logfire.span("turso.query", .{ .sql = sql[0..@min(sql.len, 100)] }); 174 - defer span.end(); 175 - ``` 176 - 177 - 2. **logging with context** 178 - ```zig 179 - logfire.info("sync complete", .{ 180 - .docs = doc_count, 181 - .pubs = pub_count, 182 - .duration_ms = duration, 183 - }); 184 - ``` 185 - 186 - 3. **error tracking** 187 - ```zig 188 - logfire.err("query failed", .{ 189 - .error = @errorName(err), 190 - .query = truncated_query, 191 - }); 192 - ``` 193 - 194 - 4. **request tracing** (if building HTTP services) 195 - - trace ID propagation via headers 196 - - automatic span creation for requests 197 - 198 - ## implementation considerations 199 - 200 - ### OTLP encoding 201 - 202 - protobuf is preferred but requires either: 203 - - generated zig code from `.proto` files 204 - - hand-rolled protobuf encoder (OTLP schema is well-documented) 205 - 206 - JSON encoding is simpler to implement and logfire supports it. could start with JSON and add protobuf later. 207 - 208 - ### batching and export 209 - 210 - spans/logs should be batched and exported asynchronously to avoid blocking application code. this is where zig's async/threading model matters: 211 - 212 - - batch buffer with configurable size/timeout 213 - - background thread or async task for export 214 - - graceful shutdown (flush on exit) 215 - 216 - ### allocator design 217 - 218 - zig clients need to be explicit about allocation. options: 219 - 220 - 1. require allocator passed to `configure()` 221 - 2. use arena per batch, free on export 222 - 3. fixed-size buffers with overflow handling 223 - 224 - ### error handling 225 - 226 - zig's explicit error handling is a strength here. export failures shouldn't crash the application: 227 - 228 - ```zig 229 - lf.flush() catch |err| { 230 - std.log.warn("logfire export failed: {}", .{err}); 231 - }; 232 - ``` 233 - 234 - ## prior art 235 - 236 - - [logfire-rust](https://github.com/pydantic/logfire-rust) - official rust client 237 - - [opentelemetry-zig](https://github.com/open-telemetry/opentelemetry-zig) - community otel implementation (incomplete) 238 - - [zig-opentelemetry](https://github.com/baaalad/zig-opentelemetry) - another community attempt 239 - 240 - the opentelemetry-zig implementations are incomplete, which is actually an opportunity - a logfire-zig client could become the de facto OTLP implementation for zig. 241 - 242 - ## references 243 - 244 - - [logfire docs](https://logfire.pydantic.dev/docs/) 245 - - [alternative clients guide](https://logfire.pydantic.dev/docs/how-to-guides/alternative-clients/) 246 - - [OTLP specification](https://opentelemetry.io/docs/specs/otlp/) 247 - - [find-bufo](https://github.com/zzstoatzz/find-bufo) - rust app using logfire (src/main.rs, src/search.rs) 248 - - [plyr.fm logfire docs](../scratch/../../../plyr.fm/docs/tools/logfire.md) - querying patterns 249 - 250 - ## open questions 251 - 252 - 1. should this be `logfire-zig` (pydantic-branded) or `zig-logfire` (community)? 253 - 2. JSON-first or protobuf-first? 254 - 3. how to handle trace context propagation for HTTP frameworks that don't exist yet in zig? 255 - 4. should it integrate with zig's `std.log` or provide its own logging? 256 - 257 - --- 258 - 259 - *these are practitioner notes, not a specification. the goal is to inform implementation decisions while leaving room for the implementer to make the right choices.*