logbrew-sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8171c73805b0809b5be561b9e315e64db5e49dc7bcc67df243a35e675bda9d36
4
- data.tar.gz: e6884f90cbcc65af3873aed8aa7f2c5fa1ba3a53031d8c4ac4c3632fd2fc5a3d
3
+ metadata.gz: e9f3c896d1d43ab4b324623e3a121cbc675fa9780fcb157f01d2aa52c54d07b4
4
+ data.tar.gz: ef86c767dbcf0b73903f37aa5492f3a54fda28318b838725155229ed95cee368
5
5
  SHA512:
6
- metadata.gz: 2418916010dcadc2b7df1f584111f1e8cc9aa293b74a1eb905c12b74848a95fa092e681ee6e6d30efc237992192fc21b0463abd7301ab23df12382f10b048f21
7
- data.tar.gz: 2e4a4fba1bd44796895e88394bc6e12da80370a1946b36e36b1c97eb87a5ebd1db39a30d9ef55414ffecca282b9f4f6eb1ac7338b5bb236e358004aca11ce6e3
6
+ metadata.gz: 90883b0323ec61e83561a09599cbcd3e1219fe25ad8ecbbd683ea10bb49f45e44d41aa66cfce8ea1818dc6858218c79dce654a5c68578a7f7ef82b80b209951f
7
+ data.tar.gz: c6af849cc5d6e96cfa8e8bfa2dedc536dd82d64ba9007a9faa9bbb13b5f7bed4b63f9e32661c879199ebf0c3b94f00ecbd6e74d3a21919fcee40e9b0f6f8942f
data/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # LogBrew Ruby SDK
2
2
 
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/LogBrewCo/sdk/main/assets/brand/logbrew-logo-transparent-512.png" alt="LogBrew logo" width="96" height="96">
5
+ </p>
6
+
3
7
  Public Ruby SDK for building, validating, previewing, and flushing LogBrew event batches, with standard-library `Net::HTTP` delivery, opt-in standard-library `Logger` support, Rack-compatible middleware, and a Rails error subscriber for Rails apps.
4
8
 
5
- The package uses only Ruby standard-library features at runtime. The repository checks build the gem, inspect the artifact, install it into a fresh gem home, run shipped examples, and exercise HTTP delivery plus failure/lifecycle paths like a real user.
9
+ The package uses only Ruby standard-library features at runtime.
6
10
 
7
11
  ## Install
8
12
 
@@ -10,13 +14,6 @@ The package uses only Ruby standard-library features at runtime. The repository
10
14
  gem install logbrew-sdk
11
15
  ```
12
16
 
13
- For local testing from this repository:
14
-
15
- ```bash
16
- bash scripts/check_ruby_package.sh
17
- bash scripts/real_user_ruby_smoke.sh
18
- ```
19
-
20
17
  ## Usage
21
18
 
22
19
  ```ruby
@@ -46,6 +43,294 @@ response = client.shutdown(LogBrew::RecordingTransport.always_accept)
46
43
  warn response.status_code
47
44
  ```
48
45
 
46
+ ## Serialized Worker Lifecycle
47
+
48
+ Use `LogBrew::WorkerLifecycle` when a prefork or long-running worker processes
49
+ one work item at a time and needs an explicit telemetry boundary:
50
+
51
+ ```ruby
52
+ client = LogBrew::Client.create(
53
+ api_key: "LOGBREW_API_KEY",
54
+ sdk_name: "checkout-worker",
55
+ sdk_version: "1.0.0"
56
+ )
57
+ transport = LogBrew::HttpTransport.new
58
+ lifecycle = LogBrew::WorkerLifecycle.create(
59
+ client: client,
60
+ transport: transport,
61
+ on_delivery_failure: ->(failure) {
62
+ warn "LogBrew delivery #{failure.code}; #{failure.pending_events} events retained"
63
+ }
64
+ )
65
+
66
+ result = lifecycle.run do
67
+ client.log(
68
+ "evt_job_started",
69
+ Time.now.utc.iso8601,
70
+ message: "job started",
71
+ level: "info",
72
+ logger: "checkout-worker"
73
+ )
74
+ perform_one_job
75
+ end
76
+
77
+ lifecycle.shutdown
78
+ ```
79
+
80
+ Create the client, transport, and lifecycle inside each child process after
81
+ forking. An inherited lifecycle rejects both work and shutdown before touching
82
+ its copied queue or transport, and ownership is checked again after application
83
+ work so a process change cannot flush copied parent state. `run` attempts one
84
+ bounded flush whether the application returns or raises, but always preserves
85
+ the exact application result or original exception. Delivery diagnostics expose
86
+ only a stable stage/code and aggregate queued/dropped counts; they never include
87
+ event content, request bodies, authorization values, exception messages,
88
+ process IDs, paths, or transport state.
89
+
90
+ This helper is intentionally explicit and installs no background thread,
91
+ timer, signal hook, global fork patch, destructor, or `at_exit` flush. It is for
92
+ serialized worker loops, not concurrent Sidekiq-style job execution. Keep using
93
+ direct `client.flush`/`client.shutdown`, or a framework-specific integration,
94
+ when that lifecycle fits the application better.
95
+
96
+ ## First Useful Service Telemetry
97
+
98
+ For a service request, combine release, environment, log, product action, network milestone, metric, and span events around one shared W3C trace:
99
+
100
+ ```ruby
101
+ incoming = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
102
+ trace = LogBrew::Traceparent.parse(incoming)
103
+ child_span_id = "b7ad6b7169203331"
104
+ route_template = "/checkout/:cart_id"
105
+ session_id = "sess_checkout_123"
106
+
107
+ client.log(
108
+ "evt_log_checkout_started",
109
+ "2026-06-02T10:00:02Z",
110
+ message: "checkout request started",
111
+ level: "info",
112
+ logger: "checkout",
113
+ metadata: { traceId: trace.trace_id, sessionId: session_id, routeTemplate: route_template }
114
+ )
115
+ client.action(
116
+ "evt_action_checkout_submit",
117
+ "2026-06-02T10:00:03Z",
118
+ LogBrew::ProductTimeline.product_action(
119
+ name: "checkout.submit",
120
+ route_template: "/checkout/:cart_id",
121
+ session_id: session_id,
122
+ trace_id: trace.trace_id,
123
+ screen: "Checkout",
124
+ funnel: "checkout",
125
+ step: "submit"
126
+ )
127
+ )
128
+ client.metric(
129
+ "evt_metric_http_server_duration",
130
+ "2026-06-02T10:00:05Z",
131
+ name: "http.server.duration",
132
+ kind: "histogram",
133
+ value: 183.4,
134
+ unit: "ms",
135
+ temporality: "delta",
136
+ metadata: { method: "POST", routeTemplate: route_template, statusCode: 202, traceId: trace.trace_id }
137
+ )
138
+ client.span(
139
+ "evt_span_checkout_request",
140
+ "2026-06-02T10:00:06Z",
141
+ LogBrew::Traceparent.span_attributes_from_traceparent(
142
+ trace,
143
+ LogBrew::TraceparentSpanInput.new(
144
+ name: "POST /checkout/:cart_id",
145
+ span_id: child_span_id,
146
+ duration_ms: 183.4,
147
+ metadata: { sampled: trace.sampled, routeTemplate: route_template, sessionId: session_id }
148
+ )
149
+ )
150
+ )
151
+
152
+ outgoing_headers = LogBrew::Traceparent.create_headers(
153
+ trace_id: trace.trace_id,
154
+ span_id: child_span_id,
155
+ trace_flags: trace.trace_flags
156
+ )
157
+ ```
158
+
159
+ The packaged `examples/first_useful_telemetry.rb` file shows the full flow, including release, environment, and network milestone events. Route templates stay query-free, metadata is primitive-only, and the SDK does not capture request bodies or arbitrary transport metadata.
160
+
161
+ ## W3C Trace Context
162
+
163
+ Use `LogBrew::Traceparent` when your app already has an incoming or outgoing W3C `traceparent` value:
164
+
165
+ ```ruby
166
+ trace = LogBrew::Traceparent.parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
167
+ headers = LogBrew::Traceparent.create_headers(
168
+ trace_id: trace.trace_id,
169
+ span_id: "b7ad6b7169203331",
170
+ trace_flags: trace.trace_flags
171
+ )
172
+ ```
173
+
174
+ The helper accepts W3C-shaped values, rejects forbidden or all-zero IDs, normalizes uppercase hex to lowercase, exposes the sampled flag, and creates LogBrew child span attributes with a new caller-provided span ID. It does not patch Ruby HTTP clients globally.
175
+
176
+ ## HTTP Request Trace Correlation
177
+
178
+ Use `LogBrew::RackMiddleware` and `LogBrew::Trace.current` when request logs, handled errors, product actions, metrics, and the request span should share one W3C trace:
179
+
180
+ ```ruby
181
+ app = lambda do |_env|
182
+ trace = LogBrew::Trace.current
183
+ logger.info("checkout started")
184
+ client.metric(
185
+ "evt_checkout_duration",
186
+ "2026-06-02T10:00:05Z",
187
+ name: "http.server.duration",
188
+ kind: "histogram",
189
+ value: 183.4,
190
+ unit: "ms",
191
+ temporality: "delta",
192
+ metadata: { routeTemplate: "/checkout/:cart_id", statusCode: 202 }
193
+ )
194
+ outgoing_headers = LogBrew::Trace.create_headers(trace)
195
+ [202, {}, ["ok"]]
196
+ end
197
+
198
+ rack = LogBrew::RackMiddleware.new(app, client: client)
199
+ ```
200
+
201
+ The middleware reads only W3C `traceparent`, creates a request-local span ID, exposes `LogBrew::Trace.current` while your app runs, and uses that same span ID on the emitted request span. `LogBrew::Logger`, direct `client.log`, `client.issue`, `client.action`, `client.metric`, and `LogBrew::RailsErrorSubscriber` add active `traceId`, `spanId`, `parentSpanId`, `traceFlags`, and `traceSampled` metadata when a request trace is active. Malformed propagation falls back to a local root trace without raising into the app. Raw propagation values, request bodies, arbitrary headers, cookies, and query strings are not captured.
202
+
203
+ ## Dependency Operation Spans
204
+
205
+ Use `LogBrew::OperationTracing` when your app owns the database, cache, or queue call and wants one correlated dependency span without monkeypatching ActiveRecord, Redis, Sidekiq, or other Ruby libraries:
206
+
207
+ ```ruby
208
+ result = LogBrew::OperationTracing.database_operation(
209
+ client,
210
+ "users.lookup",
211
+ system: "postgresql",
212
+ operation: "select",
213
+ target: "users",
214
+ metadata: { service: "api", rowCount: 1 }
215
+ ) do
216
+ User.find_by(email: email)
217
+ end
218
+ ```
219
+
220
+ `database_operation`, `cache_operation`, and `queue_operation` run your block under a child `LogBrew::Trace` context, preserve the block result or original exception, and emit exactly one span with primitive metadata. Capture failures can be observed with `on_error:` without replacing app behavior. The helpers intentionally drop SQL statements, query params, connection strings, cache keys/values, message bodies, job IDs, headers, cookies, URLs, auth-like fields, and other sensitive-looking metadata. Failed dependency spans include only the exception type in metadata plus one bounded `exception` span event with `exceptionType` and `exceptionEscaped: true`; exception messages and stacks stay out by default.
221
+
222
+ ## Outbound HTTP Tracing
223
+
224
+ Wrap an app-owned `Net::HTTP` connection explicitly when outbound work should become a child of the active LogBrew trace:
225
+
226
+ ```ruby
227
+ uri = URI("https://service.example/health")
228
+ http = LogBrew::HttpClientTracing.wrap_net_http(
229
+ Net::HTTP.new(uri.host, uri.port),
230
+ client: client,
231
+ on_capture_error: ->(error) { warn(error.class.name) }
232
+ )
233
+
234
+ response = http.request(Net::HTTP::Get.new(uri.request_uri))
235
+ ```
236
+
237
+ Faraday remains optional. Apps that already use Faraday can load the integration and place its middleware inside retry middleware so every actual retry receives a distinct child span:
238
+
239
+ ```ruby
240
+ require "faraday"
241
+ require "logbrew/faraday_tracing"
242
+
243
+ connection = Faraday.new("https://service.example") do |builder|
244
+ builder.use LogBrew::FaradayTracingMiddleware, client: client
245
+ builder.adapter :net_http
246
+ end
247
+ ```
248
+
249
+ Both adapters are literal pass-throughs when `LogBrew::Trace.current` is absent. With an active parent, they propagate one W3C `traceparent`, return the caller-visible header and trace scope to their prior values, and capture one completion span per actual execution. Duplicate wrappers, nested LogBrew HTTP middleware, and SDK delivery are suppressed without process-wide hooks. Net::HTTP start blocks, response streaming, Faraday middleware ordering, responses, and exceptions retain their normal behavior; telemetry capture failures are advisory.
250
+
251
+ Outbound HTTP spans allow only method, normalized host, status code, duration, adapter source, sampled state, and exception type. They never record scheme, port, path, query, fragment, full URL, request or response headers, bodies or sizes, exception messages or stacks, authentication material, cookies, baggage, tracestate, resolved addresses, or arbitrary request options.
252
+
253
+ ## Metrics
254
+
255
+ Use `metric` for explicit, application-owned measurements. LogBrew validates the metric name, kind, value, unit, temporality, and optional metadata before queueing the event:
256
+
257
+ ```ruby
258
+ client.metric(
259
+ "evt_metric_queue_depth",
260
+ "2026-06-02T10:00:06Z",
261
+ name: "queue.depth",
262
+ kind: "gauge",
263
+ value: 42,
264
+ unit: "{items}",
265
+ temporality: "instant",
266
+ metadata: { service: "worker", queue: "checkout" }
267
+ )
268
+ ```
269
+
270
+ Supported metric kinds are `counter`, `gauge`, and `histogram`. Counters and histograms require `delta` or `cumulative` temporality and non-negative values; gauges require `instant` temporality and may be negative. Keep metadata low-cardinality and primitive. This SDK does not automatically collect Ruby runtime, Rack, Rails, or database metrics yet.
271
+
272
+ ## Product And Network Timelines
273
+
274
+ Use `LogBrew::ProductTimeline` when your app already knows the product step or API milestone that matters and you want an agent-readable timeline without recording a visual session replay:
275
+
276
+ ```ruby
277
+ client.action(
278
+ "evt_checkout_submit",
279
+ "2026-06-02T10:00:07Z",
280
+ LogBrew::ProductTimeline.product_action(
281
+ name: "checkout.submit",
282
+ route_template: "/checkout/:cart_id",
283
+ session_id: "session_123",
284
+ trace_id: "trace_123",
285
+ screen: "Checkout",
286
+ funnel: "purchase",
287
+ step: "submit",
288
+ metadata: { plan: "pro" }
289
+ )
290
+ )
291
+
292
+ client.action(
293
+ "evt_checkout_api",
294
+ "2026-06-02T10:00:08Z",
295
+ LogBrew::ProductTimeline.network_milestone(
296
+ route_template: "/api/checkout/:cart_id",
297
+ method: "POST",
298
+ status_code: 503,
299
+ duration_ms: 42.5,
300
+ session_id: "session_123",
301
+ trace_id: "trace_123",
302
+ metadata: { region: "iad" }
303
+ )
304
+ )
305
+ ```
306
+
307
+ The helpers return normal `action` attributes, so they work with the existing queue, preview, flush, and retry behavior. They accept only primitive metadata, copy it defensively, strip query strings and hashes from route templates, reduce full HTTP URLs to paths, normalize HTTP methods, and infer failed network milestones from `4xx`/`5xx` status codes. They do not patch `Net::HTTP`, capture request or response payloads, capture arbitrary headers, auto-capture clicks, or claim visual replay.
308
+
309
+ ## Support Ticket Draft Diagnostics
310
+
311
+ Use `LogBrew::SupportTicketDraft.create` when a developer or support agent explicitly asks for a local JSON payload for the planned LogBrew support-ticket routes. The helper validates the public source/category contract, normalizes W3C trace IDs, redacts diagnostics, and returns a plain Ruby hash:
312
+
313
+ ```ruby
314
+ draft = LogBrew::SupportTicketDraft.create(
315
+ source: "sdk",
316
+ category: "ingest_failure",
317
+ title: "Telemetry flush failed",
318
+ description: "Flush returned usage_limit_exceeded",
319
+ sdk_package: "logbrew-sdk",
320
+ sdk_version: "0.1.0",
321
+ trace_id: "4BF92F3577B34DA6A3CE929D0E0E4736",
322
+ diagnostics: {
323
+ endpoint: "https://api.example/ingest?debug=true",
324
+ apiKey: "lbw_ingest_redacted",
325
+ error: RuntimeError.new("hidden token")
326
+ }
327
+ )
328
+
329
+ puts JSON.generate(draft)
330
+ ```
331
+
332
+ This helper is local-only. It does not send data, open a ticket, call backend support-ticket routes, use account/session API credentials, or infer backend ownership. Diagnostics are bounded to JSON-like values; token-like keys and strings are redacted, HTTP URLs keep only the path, local filesystem paths are replaced, exceptions keep only the class name, and unsupported Ruby objects are omitted.
333
+
49
334
  ## HTTP Delivery
50
335
 
51
336
  Use `LogBrew::HttpTransport` when you want the SDK to POST queued batches to LogBrew:
@@ -72,20 +357,132 @@ warn response.status_code
72
357
 
73
358
  `HttpTransport` sends JSON with the SDK key in the `authorization` header, supports a custom endpoint, headers, timeout, and app-owned HTTP client object, maps HTTP statuses through the client's retry rules, and converts request/time-out failures into retryable transport errors.
74
359
 
75
- ## Examples
360
+ ## Bounded Delivery
76
361
 
77
- From `ruby/logbrew-ruby`:
362
+ The client bounds queued telemetry and each transport request independently. Queue defaults are 1,000 events and 4 MiB of compact event JSON. Request defaults are 100 events and 256 KiB. When a queue limit is reached, LogBrew rejects the new event so earlier release, environment, and trace context stays available for the next flush. An event that cannot fit one request is rejected before it enters the queue.
78
363
 
79
- ```bash
80
- cd examples && make
81
- cd examples && make run-readme-example
82
- cd examples && make run
83
- cd examples && make run-real-user-smoke
84
- ruby examples/readme_example.rb
85
- ruby examples/real_user_smoke.rb
364
+ ```ruby
365
+ dropped = 0
366
+ client = LogBrew::Client.create(
367
+ api_key: "LOGBREW_API_KEY",
368
+ sdk_name: "my-ruby-app",
369
+ sdk_version: "1.0.0",
370
+ max_queue_size: 1_000,
371
+ max_queue_bytes: 4 * 1024 * 1024,
372
+ max_batch_size: 100,
373
+ max_batch_bytes: 256 * 1024,
374
+ on_event_dropped: lambda do |notice|
375
+ dropped = notice.dropped_events
376
+ warn "LogBrew queue pressure: #{notice.reason} (#{dropped} dropped)"
377
+ end
378
+ )
379
+ ```
380
+
381
+ `pending_events`, `pending_event_bytes`, and `dropped_events` expose local pressure without a network call. Events are serialized once at capture, so later mutation of caller-owned strings or metadata cannot change queued content, byte accounting, or retry bodies. `LogBrew::DroppedEvent` contains only the rejected event ID/type, the stable reason `queue_overflow`, `event_too_large`, or opt-in `persistence_failure`, cumulative loss, and retained count/bytes; it never includes event attributes or payload content. Callback errors are isolated from application capture.
382
+
383
+ Transport bodies use compact JSON and stay under both request limits. `response.attempts` aggregates every request attempt and `response.batches` reports accepted request batches. Each successful request removes only its accepted queue prefix. If a later batch fails, its events and every later event remain queued in order. The failed body is frozen across later `flush` or `shutdown` calls, so events captured after failure cannot change retry bytes. A flush drains only the events present when it started; events captured during transport I/O remain queued.
384
+
385
+ Existing custom transports keep the same `send(api_key, body)` interface, but they must allow one `flush` to call `send` more than once. Treat each call as an independent compact request and use `response.batches` when application code needs the accepted request count; do not assume one transport call per flush.
386
+
387
+ `shutdown` rejects new capture while its final flush is running, closes only after every start-snapshot batch is accepted, and reopens capture if delivery fails. Clients created with `Client.create` own no background worker or timer; applications keep explicit control over when network delivery happens.
388
+
389
+ ## Automatic Delivery
390
+
391
+ Applications that own their transport can opt into one lazy delivery worker. Manual clients remain the default.
392
+
393
+ ```ruby
394
+ transport = LogBrew::HttpTransport.new(timeout: 10)
395
+ client = LogBrew::Client.create_automatic(
396
+ api_key: ENV.fetch("LOGBREW_API_KEY"),
397
+ sdk_name: "checkout-worker",
398
+ sdk_version: "1.0.0",
399
+ transport: transport,
400
+ flush_interval: 5,
401
+ flush_threshold: 100,
402
+ retry_base_delay: 0.25,
403
+ retry_max_delay: 30,
404
+ persistent_queue_path: ENV["LOGBREW_PERSISTENT_QUEUE_PATH"]
405
+ )
406
+
407
+ client.log("evt_job_started", Time.now.utc.iso8601, message: "job started", level: "info")
408
+ warn JSON.generate(client.delivery_health.to_h)
409
+ client.shutdown
86
410
  ```
87
411
 
88
- `make run` is the shorter alias for the stronger real-user smoke example.
412
+ The worker starts only after accepted queue work exists, then wakes when the queue reaches `flush_threshold` or `flush_interval` expires. Restart-hydrated persistent work wakes it immediately. Automatic and manual sends share the existing serialized flush, immutable failed prefix, accepted-prefix acknowledgement, batch bounds, and persistence format; no second queue or transport path is created.
413
+
414
+ Retryable network, `408`, and `5xx` failures retain the exact failed body and use capped equal-jitter backoff. Authentication (`401`/`403`), quota (`429`), validation (`400`/`422`), and other non-retryable responses pause automatic sends without dropping queued work. `recover_automatic_delivery` performs one explicit synchronous flush through the owned transport and resumes scheduling only after success. Calling `flush(transport)` directly provides the same explicit recovery boundary. `stop_automatic_delivery` joins the worker without draining or discarding work; a later manual flush remains available.
415
+
416
+ `delivery_health` returns an immutable, JSON-serializable `LogBrew::DeliveryHealth` snapshot. Its fixed fields are lifecycle state, queued event/byte counts, dropped count, in-flight state, bounded outcome/failure/flush counters, pause reason, and current retry delay. It never contains event data, event IDs, API keys, endpoints, headers, response bodies, filesystem paths, process or thread IDs, exception messages, or server text.
417
+
418
+ Automatic ownership is process-local. An inherited automatic client rejects capture, flush, purge, stop, and shutdown after `fork`; each child must create a fresh client, transport, and persistent queue owner. No signal handler, global fork hook, `at_exit`, or finalizer is installed. `shutdown` stops and joins the worker before its final drain, and a failed drain reopens the client with retryable failures scheduled or terminal failures paused. Application transport timeouts still bound how promptly an in-flight send can stop.
419
+
420
+ ## Sidekiq Tracing
421
+
422
+ Sidekiq integration is explicit and optional. Sidekiq is not a dependency of the base gem. Create the LogBrew client and instrumentation in the process that owns the middleware, then register the client and server sides you use:
423
+
424
+ ```ruby
425
+ require "logbrew"
426
+ require "logbrew/sidekiq"
427
+ require "sidekiq"
428
+
429
+ transport = LogBrew::HttpTransport.new(timeout: 10)
430
+ client = LogBrew::Client.create_automatic(
431
+ api_key: ENV.fetch("LOGBREW_API_KEY"),
432
+ sdk_name: "checkout-worker",
433
+ sdk_version: "1.0.0",
434
+ transport: transport
435
+ )
436
+ sidekiq_tracing = LogBrew::Sidekiq::Instrumentation.create(
437
+ client: client,
438
+ max_retries: 25
439
+ )
440
+
441
+ Sidekiq.configure_client { |config| sidekiq_tracing.register_client(config) }
442
+ Sidekiq.configure_server do |config|
443
+ sidekiq_tracing.register_client(config)
444
+ sidekiq_tracing.register_server(config)
445
+ config.on(:quiet) { sidekiq_tracing.quiet }
446
+ config.on(:shutdown) { sidekiq_tracing.shutdown }
447
+ end
448
+ ```
449
+
450
+ Registration is app-owned, idempotent, and reversible with `unregister_client` and `unregister_server`; the first instrumentation registered for each middleware class owns that entry. `disable` and `enable` provide reversible capture control. `quiet` stops new Sidekiq instrumentation while already queued LogBrew events remain under the existing delivery owner. `shutdown` is idempotent and delegates draining to the existing client; automatic clients use their owned transport, while manual clients must pass `transport:` when the instrumentation is created.
451
+
452
+ The client middleware adds one bounded `logbrew` carrier containing only a version, W3C `traceparent`, and enqueue time. Valid retries keep that carrier without creating another enqueue span. The server middleware continues a valid carrier or starts a fresh trace when it is absent or malformed, returns the caller trace state after every result, and records bounded queue-wait and execution timing. Set `max_retries` to the same default retry limit used by your Sidekiq configuration; per-job integer or disabled retry settings are honored. Retryable failures keep only error spans, while the terminal escaped failure adds one deduplicated fixed-title issue and preserves the original exception.
453
+
454
+ Sidekiq spans contain only fixed source, sampled state, bounded retry count, bounded queue-wait duration, execution duration, status, and real cancellation. The integration does not capture job arguments, payload fields, job identifiers, worker names, queue values, connection data, exception messages or stacks, baggage, or tracestate. Capture failures are advisory and never replace job execution or retry behavior. Create fresh clients and instrumentation after `fork`; inherited instances fail closed without changing jobs.
455
+
456
+ ## Persistent Worker Delivery
457
+
458
+ Server workers that need restart recovery can opt into an app-owned persistent queue. Create the client after forking and give every worker its own normalized absolute directory:
459
+
460
+ ```ruby
461
+ queue_path = ENV.fetch("LOGBREW_PERSISTENT_QUEUE_PATH")
462
+
463
+ client = LogBrew::Client.create(
464
+ api_key: ENV.fetch("LOGBREW_API_KEY"),
465
+ sdk_name: "checkout-worker",
466
+ sdk_version: "1.0.0",
467
+ persistent_queue_path: queue_path,
468
+ on_event_dropped: lambda do |notice|
469
+ warn "LogBrew delivery pressure: #{notice.reason} (#{notice.dropped_events} dropped)"
470
+ end
471
+ )
472
+
473
+ client.log("evt_job_started", Time.now.utc.iso8601, message: "job started", level: "info")
474
+ client.shutdown(LogBrew::HttpTransport.new)
475
+ ```
476
+
477
+ Persistence is disabled by default and adds no background thread, timer, or `at_exit` hook. Admission writes and syncs each validated event with an atomic same-directory rename. If that rename completes but directory sync cannot be confirmed, capture raises the content-free `persistence_commit_error`; the event remains pending and cannot be sent or purged until a later sync succeeds, and it is never reported as dropped. Restart reads the oldest records first, preserves the normal 1,000-event/4 MiB bounds, and keeps the same 100-event/256 KiB transport splitting. A server-accepted prefix is recorded before local compaction, so interrupted compaction does not replay it. A crash before that marker may replay a stable event ID; delivery is intentionally at-least-once, not exactly-once.
478
+
479
+ The queue directory must be dedicated, owner-only, and used by one process. Symlinks, unexpected files, corrupt records, concurrent owners, and inherited pre-fork clients fail closed. Build each child client after fork with a unique path. Successful `shutdown` releases ownership; failed delivery remains restartable. Use `client.purge_pending_events` only when the application explicitly chooses to discard pending telemetry.
480
+
481
+ Event files contain the same validated event JSON your application submitted, including message and metadata values. Protect the directory and do not put API keys or other sensitive values in telemetry. The SDK never adds the API key, endpoint, request headers, process ID, SDK request envelope, or queue path to stored records. Persistence failures before an event rename reject the new event with the content-free `persistence_failure` drop reason; they do not fall back to an in-memory-only event.
482
+
483
+ ## Example Source
484
+
485
+ The `examples` directory contains copyable snippets for creating a client, sending through `HttpTransport`, using the standard logger wrapper, attaching Rack middleware, and subscribing to Rails errors in your own Ruby app.
89
486
 
90
487
  ## Standard Logger
91
488
 
@@ -113,7 +510,7 @@ logger.error(RuntimeError.new("payment failed"))
113
510
  client.flush(LogBrew::RecordingTransport.always_accept)
114
511
  ```
115
512
 
116
- The adapter respects Ruby logger levels and lazy block messages, maps `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`, and `UNKNOWN` to LogBrew log levels, captures `progname`, primitive base metadata, and exception type/message, and omits exception backtrace text unless `include_exception_backtrace: true` is set. Logs queue by default; pass `transport:` plus `flush_on_log: true` or call `flush_logbrew` for immediate delivery.
513
+ The adapter respects Ruby logger levels and lazy block messages, maps `DEBUG`/`INFO` to LogBrew `info`, `WARN` to `warning`, `ERROR` to `error`, and `FATAL` to `critical`, captures `progname`, primitive base metadata, and exception type/message, and omits exception backtrace text unless `include_exception_backtrace: true` is set. Logs queue by default; pass `transport:` plus `flush_on_log: true` or call `flush_logbrew` for immediate delivery.
117
514
 
118
515
  ## Rack And Rails Middleware
119
516
 
@@ -178,10 +575,17 @@ The subscriber implements `report(error, handled:, severity:, context:, source:,
178
575
  ## Behavior
179
576
 
180
577
  - `preview_json` returns the queued batch as pretty JSON.
181
- - `flush(transport)` sends queued events, retries retryable failures, and clears the queue only after a 2xx response.
578
+ - `persistent_queue_path:` enables explicit owner-only, single-process restart recovery; `purge_pending_events` explicitly discards its pending prefix.
579
+ - `Client.create_automatic(...)` opts an owned transport into one lazy interval/threshold worker; `delivery_health`, `recover_automatic_delivery`, and `stop_automatic_delivery` expose fixed process-local lifecycle control.
580
+ - `LogBrew::Sidekiq::Instrumentation` explicitly installs optional client/server middleware with bounded W3C propagation, fixed telemetry, and app-owned quiet/shutdown hooks.
581
+ - `flush(transport)` splits its queue snapshot into compact 100-event/256 KiB requests, freezes failed retry bytes, acknowledges only accepted prefixes, and leaves transport-time capture queued.
582
+ - Queues default to 1,000 events and 4 MiB of compact serialized event data; `pending_event_bytes`, `dropped_events`, and `on_event_dropped` expose pressure locally. `TransportResponse#attempts` and `#batches` expose request work.
583
+ - `metric(...)` queues explicit, application-owned metric events with name, kind, value, unit, temporality, and low-cardinality metadata validation.
584
+ - `LogBrew::ProductTimeline` builds explicit, application-owned product action and network milestone timeline events with primitive metadata and query/hash-free routes.
585
+ - `LogBrew::SupportTicketDraft.create` builds explicit, local-only support-ticket create payload drafts with redacted diagnostics and no backend route calls.
182
586
  - `LogBrew::HttpTransport` sends queued batches through Ruby's standard `Net::HTTP` with configurable endpoint, headers, timeout, and app-owned HTTP client support.
183
587
  - `LogBrew::RackMiddleware` captures Rack request spans and unhandled app exceptions without requiring Rails or Rack at runtime.
184
588
  - `LogBrew::RailsErrorSubscriber` captures handled/manual Rails error reports without requiring Rails at runtime.
185
589
  - `shutdown(transport)` flushes queued events and rejects later writes.
186
- - `LogBrew::RecordingTransport.always_accept` is useful for local examples and tests.
590
+ - `LogBrew::RecordingTransport.always_accept` is useful when you want to inspect queued JSON before network delivery.
187
591
  - `LogBrew::SdkError` exposes stable `code` and `message` values for user-facing failure handling.
data/examples/Makefile CHANGED
@@ -1,9 +1,14 @@
1
- .PHONY: help run run-readme-example run-real-user-smoke
1
+ .PHONY: help run run-readme-example run-real-user-smoke run-first-useful-telemetry run-http-trace-correlation run-persistent-worker-delivery run-automatic-delivery run-sidekiq-tracing
2
2
 
3
3
  help:
4
4
  @printf '%s\n' 'run-readme-example -> make run-readme-example'
5
5
  @printf '%s\n' 'run (real-user-smoke) -> make run'
6
6
  @printf '%s\n' 'run-real-user-smoke -> make run-real-user-smoke'
7
+ @printf '%s\n' 'run-first-useful-telemetry -> make run-first-useful-telemetry'
8
+ @printf '%s\n' 'run-http-trace-correlation -> make run-http-trace-correlation'
9
+ @printf '%s\n' 'run-persistent-worker-delivery -> make run-persistent-worker-delivery'
10
+ @printf '%s\n' 'run-automatic-delivery -> make run-automatic-delivery'
11
+ @printf '%s\n' 'run-sidekiq-tracing -> make run-sidekiq-tracing'
7
12
 
8
13
  run: run-real-user-smoke
9
14
 
@@ -12,3 +17,18 @@ run-readme-example:
12
17
 
13
18
  run-real-user-smoke:
14
19
  @ruby real_user_smoke.rb
20
+
21
+ run-first-useful-telemetry:
22
+ @ruby first_useful_telemetry.rb
23
+
24
+ run-http-trace-correlation:
25
+ @ruby http_trace_correlation.rb
26
+
27
+ run-persistent-worker-delivery:
28
+ @ruby persistent_worker_delivery.rb
29
+
30
+ run-automatic-delivery:
31
+ @ruby automatic_delivery.rb
32
+
33
+ run-sidekiq-tracing:
34
+ @ruby sidekiq_tracing.rb
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logbrew"
5
+
6
+ transport = LogBrew::RecordingTransport.always_accept
7
+ client = LogBrew::Client.create_automatic(
8
+ api_key: ENV.fetch("LOGBREW_API_KEY", "local-example-key"),
9
+ sdk_name: "automatic-delivery-example",
10
+ sdk_version: "1.0.0",
11
+ transport: transport,
12
+ flush_interval: 1,
13
+ flush_threshold: 1
14
+ )
15
+
16
+ client.log(
17
+ "evt_automatic_example",
18
+ Time.now.utc.iso8601,
19
+ message: "automatic delivery example",
20
+ level: "info"
21
+ )
22
+
23
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
24
+ sleep(0.01) until client.pending_events.zero? || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
25
+ health = client.delivery_health.to_h
26
+ response = client.shutdown
27
+
28
+ puts JSON.generate(
29
+ "ok" => response.status_code == 204 && health.fetch("last_outcome") == "accepted",
30
+ "state" => client.delivery_health.state,
31
+ "sentBodies" => transport.sent_bodies.length
32
+ )