logbrew-sdk 0.1.1 → 0.1.3

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: dd015a45db45c66bd30d0d2a273a9aab8c8c4ec8b4d486545bf778bb0eba8fbf
4
- data.tar.gz: edc117c673cb97a6321cc0a58029673cd0ecdba0b5e2c1d48d41e71e8e967a30
3
+ metadata.gz: 233087ef98eb35d7df84720956a89dfbf66368ac55269900a38491192c7a67a3
4
+ data.tar.gz: 1720b83bf8dcc562771936091f0196996415005e6a5ea254ca4e0a803e3ea1ac
5
5
  SHA512:
6
- metadata.gz: 2b610b4733c474a40fbb55725281966fc708332f5d3215f807205ff8b31c38dafdf6ad24764b816a30d303584948a15925d48910b4b6b04e5d7cd5a90f5c516c
7
- data.tar.gz: 24dbb604cebc482ccc8d9450d58e523a678d284387908bf907eb1d8f70daa1649fb8898f9a41ef27ebdfa649be67329ba3cbea1aba44745ecbda637eac7a3e82
6
+ metadata.gz: 1e67486d07bb9334f6cf553111e8e198f1b448105cc491cad566d03e4356a190b00891c054552f9022c9dc982019bce506dc7ab09f7340d3321785935b34fef7
7
+ data.tar.gz: 66c92e704c995f6a0ab73586eefa8a65cf7aef4af621a3dbbc68c925b6063851ebc422c8d23d5f30de40ea7b111989bd0154ed740fc334d109ce72cfabe8971b
data/README.md CHANGED
@@ -4,9 +4,10 @@
4
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
5
  </p>
6
6
 
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.
7
+ Public Ruby SDK for building, validating, previewing, and flushing LogBrew event batches, with automatic Rails request/error capture, standard-library `Net::HTTP` delivery, opt-in standard-library `Logger` support, and manual Rack helpers.
8
8
 
9
- The package uses only Ruby standard-library features at runtime.
9
+ The core package has no runtime gem dependencies. Its automatic integration
10
+ activates only inside an application that has already loaded Rails.
10
11
 
11
12
  ## Install
12
13
 
@@ -14,13 +15,106 @@ The package uses only Ruby standard-library features at runtime.
14
15
  gem install logbrew-sdk
15
16
  ```
16
17
 
18
+ ## Rails Quick Start
19
+
20
+ Add the gem normally. Its package-name require shim loads the Rails integration
21
+ when Rails is present, so `require: "logbrew"` and a custom initializer are not
22
+ needed:
23
+
24
+ ```ruby
25
+ # Gemfile
26
+ gem "logbrew-sdk", "~> 0.1.3"
27
+ ```
28
+
29
+ ```bash
30
+ bundle install
31
+ export LOGBREW_SERVER_API_KEY="your project-scoped server ingest key"
32
+ bin/rails server
33
+ ```
34
+
35
+ That is the complete Rails application change. The Railtie installs one
36
+ request middleware, subscribes to handled Rails errors, creates a fresh client
37
+ inside each server process, delivers in the background, and performs one
38
+ bounded shutdown drain. Without `LOGBREW_SERVER_API_KEY`, the integration stays
39
+ disabled and the application behaves normally. Set `LOGBREW_ENABLED=false` to
40
+ disable it explicitly. If the Gemfile uses `require: false`, load
41
+ `logbrew/rails` yourself after Rails.
42
+
43
+ The automatic integration records route-template request spans and type-only
44
+ issues. It does not record concrete request paths, query strings, request or
45
+ response bodies, arbitrary headers, authorization values, cookies, user IDs,
46
+ exception messages, or exception backtraces. Exception messages and backtraces
47
+ are separate opt-ins:
48
+
49
+ | Environment variable | Default | Purpose |
50
+ | --- | --- | --- |
51
+ | `LOGBREW_ENABLED` | inferred | Optional explicit `true` or `false` override |
52
+ | `LOGBREW_SERVER_API_KEY` | unset | Project-scoped server ingest key; enables the integration |
53
+ | `LOGBREW_SERVICE_NAME` | Rails application name | Bounded service metadata |
54
+ | `LOGBREW_ENVIRONMENT` | `Rails.env` | Bounded environment metadata |
55
+ | `LOGBREW_RELEASE` | unset | Optional release identifier |
56
+ | `LOGBREW_ENDPOINT` | `https://api.logbrew.co/v1/events` | HTTPS intake URL; loopback HTTP is accepted for local development |
57
+ | `LOGBREW_REQUEST_TIMEOUT_MS` | `10000` | Per-request delivery timeout from 1 to 600000 ms |
58
+ | `LOGBREW_FLUSH_INTERVAL_MS` | `5000` | Automatic delivery interval from 10 to 3600000 ms |
59
+ | `LOGBREW_FLUSH_THRESHOLD` | `100` | Queue size from 1 to 1000 that requests an earlier flush |
60
+ | `LOGBREW_CAPTURE_EXCEPTION_MESSAGES` | `false` | Opt in to exception message capture |
61
+ | `LOGBREW_INCLUDE_EXCEPTION_BACKTRACE` | `false` | Opt in to exception backtrace capture |
62
+
63
+ `LOGBREW_API_KEY` and `LOGBREW_INGEST_KEY` are not Rails aliases. If either is
64
+ set without the canonical server key, startup reports the exact
65
+ `LOGBREW_SERVER_API_KEY` correction without printing any key value.
66
+
67
+ ### Create a Project and Confirm Hosted Rails Delivery
68
+
69
+ LogBrew CLI 0.1.32 or newer can create a project and one-time key without a
70
+ dashboard handoff. The destination key file must not already exist:
71
+
72
+ ```bash
73
+ logbrew status --json
74
+ install -d -m 700 "$HOME/.logbrew"
75
+
76
+ project_result="$(
77
+ logbrew projects create rails-service \
78
+ --runtime ruby \
79
+ --environment development \
80
+ --ingest-key-file "$HOME/.logbrew/rails-service.ingest" \
81
+ --json
82
+ )"
83
+ export LOGBREW_PROJECT_ID="$(jq -er '.project.id' <<<"$project_result")"
84
+ unset project_result
85
+ export LOGBREW_SERVER_API_KEY="$(< "$HOME/.logbrew/rails-service.ingest")"
86
+ export LOGBREW_SERVICE_NAME="rails-service"
87
+ ```
88
+
89
+ Start Rails and request one application route. Then inspect the same project
90
+ through the approved CLI session:
91
+
92
+ ```bash
93
+ logbrew doctor --project "$LOGBREW_PROJECT_ID" --json
94
+ logbrew traces --project "$LOGBREW_PROJECT_ID" \
95
+ --service rails-service \
96
+ --since 1h \
97
+ --json
98
+ ```
99
+
100
+ When the temporary project is no longer needed, archive it and remove the
101
+ revoked one-time key:
102
+
103
+ ```bash
104
+ unset LOGBREW_SERVER_API_KEY
105
+ unset LOGBREW_SERVICE_NAME
106
+ logbrew projects archive "$LOGBREW_PROJECT_ID" --yes --json
107
+ rm -f "$HOME/.logbrew/rails-service.ingest"
108
+ unset LOGBREW_PROJECT_ID
109
+ ```
110
+
17
111
  ## Usage
18
112
 
19
113
  ```ruby
20
114
  require "logbrew"
21
115
 
22
116
  client = LogBrew::Client.create(
23
- api_key: "LOGBREW_API_KEY",
117
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
24
118
  sdk_name: "my-ruby-app",
25
119
  sdk_version: "1.0.0"
26
120
  )
@@ -43,6 +137,56 @@ response = client.shutdown(LogBrew::RecordingTransport.always_accept)
43
137
  warn response.status_code
44
138
  ```
45
139
 
140
+ ## Serialized Worker Lifecycle
141
+
142
+ Use `LogBrew::WorkerLifecycle` when a prefork or long-running worker processes
143
+ one work item at a time and needs an explicit telemetry boundary:
144
+
145
+ ```ruby
146
+ client = LogBrew::Client.create(
147
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
148
+ sdk_name: "checkout-worker",
149
+ sdk_version: "1.0.0"
150
+ )
151
+ transport = LogBrew::HttpTransport.new
152
+ lifecycle = LogBrew::WorkerLifecycle.create(
153
+ client: client,
154
+ transport: transport,
155
+ on_delivery_failure: ->(failure) {
156
+ warn "LogBrew delivery #{failure.code}; #{failure.pending_events} events retained"
157
+ }
158
+ )
159
+
160
+ result = lifecycle.run do
161
+ client.log(
162
+ "evt_job_started",
163
+ Time.now.utc.iso8601,
164
+ message: "job started",
165
+ level: "info",
166
+ logger: "checkout-worker"
167
+ )
168
+ perform_one_job
169
+ end
170
+
171
+ lifecycle.shutdown
172
+ ```
173
+
174
+ Create the client, transport, and lifecycle inside each child process after
175
+ forking. An inherited lifecycle rejects both work and shutdown before touching
176
+ its copied queue or transport, and ownership is checked again after application
177
+ work so a process change cannot flush copied parent state. `run` attempts one
178
+ bounded flush whether the application returns or raises, but always preserves
179
+ the exact application result or original exception. Delivery diagnostics expose
180
+ only a stable stage/code and aggregate queued/dropped counts; they never include
181
+ event content, request bodies, authorization values, exception messages,
182
+ process IDs, paths, or transport state.
183
+
184
+ This helper is intentionally explicit and installs no background thread,
185
+ timer, signal hook, global fork patch, destructor, or `at_exit` flush. It is for
186
+ serialized worker loops, not concurrent Sidekiq-style job execution. Keep using
187
+ direct `client.flush`/`client.shutdown`, or a framework-specific integration,
188
+ when that lifecycle fits the application better.
189
+
46
190
  ## First Useful Service Telemetry
47
191
 
48
192
  For a service request, combine release, environment, log, product action, network milestone, metric, and span events around one shared W3C trace:
@@ -121,7 +265,7 @@ headers = LogBrew::Traceparent.create_headers(
121
265
  )
122
266
  ```
123
267
 
124
- 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.
268
+ 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.
125
269
 
126
270
  ## HTTP Request Trace Correlation
127
271
 
@@ -167,7 +311,38 @@ result = LogBrew::OperationTracing.database_operation(
167
311
  end
168
312
  ```
169
313
 
170
- `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; exception spans include only the exception type by default.
314
+ `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.
315
+
316
+ ## Outbound HTTP Tracing
317
+
318
+ Wrap an app-owned `Net::HTTP` connection explicitly when outbound work should become a child of the active LogBrew trace:
319
+
320
+ ```ruby
321
+ uri = URI("https://service.example/health")
322
+ http = LogBrew::HttpClientTracing.wrap_net_http(
323
+ Net::HTTP.new(uri.host, uri.port),
324
+ client: client,
325
+ on_capture_error: ->(error) { warn(error.class.name) }
326
+ )
327
+
328
+ response = http.request(Net::HTTP::Get.new(uri.request_uri))
329
+ ```
330
+
331
+ 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:
332
+
333
+ ```ruby
334
+ require "faraday"
335
+ require "logbrew/faraday_tracing"
336
+
337
+ connection = Faraday.new("https://service.example") do |builder|
338
+ builder.use LogBrew::FaradayTracingMiddleware, client: client
339
+ builder.adapter :net_http
340
+ end
341
+ ```
342
+
343
+ 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.
344
+
345
+ 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.
171
346
 
172
347
  ## Metrics
173
348
 
@@ -258,7 +433,7 @@ Use `LogBrew::HttpTransport` when you want the SDK to POST queued batches to Log
258
433
  require "logbrew"
259
434
 
260
435
  client = LogBrew::Client.create(
261
- api_key: "LOGBREW_API_KEY",
436
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
262
437
  sdk_name: "my-ruby-app",
263
438
  sdk_version: "1.0.0"
264
439
  )
@@ -276,6 +451,129 @@ warn response.status_code
276
451
 
277
452
  `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.
278
453
 
454
+ ## Bounded Delivery
455
+
456
+ 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.
457
+
458
+ ```ruby
459
+ dropped = 0
460
+ client = LogBrew::Client.create(
461
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
462
+ sdk_name: "my-ruby-app",
463
+ sdk_version: "1.0.0",
464
+ max_queue_size: 1_000,
465
+ max_queue_bytes: 4 * 1024 * 1024,
466
+ max_batch_size: 100,
467
+ max_batch_bytes: 256 * 1024,
468
+ on_event_dropped: lambda do |notice|
469
+ dropped = notice.dropped_events
470
+ warn "LogBrew queue pressure: #{notice.reason} (#{dropped} dropped)"
471
+ end
472
+ )
473
+ ```
474
+
475
+ `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.
476
+
477
+ 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.
478
+
479
+ 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.
480
+
481
+ `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.
482
+
483
+ ## Automatic Delivery
484
+
485
+ Applications that own their transport can opt into one lazy delivery worker. Manual clients remain the default.
486
+
487
+ ```ruby
488
+ transport = LogBrew::HttpTransport.new(timeout: 10)
489
+ client = LogBrew::Client.create_automatic(
490
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
491
+ sdk_name: "checkout-worker",
492
+ sdk_version: "1.0.0",
493
+ transport: transport,
494
+ flush_interval: 5,
495
+ flush_threshold: 100,
496
+ retry_base_delay: 0.25,
497
+ retry_max_delay: 30,
498
+ persistent_queue_path: ENV["LOGBREW_PERSISTENT_QUEUE_PATH"]
499
+ )
500
+
501
+ client.log("evt_job_started", Time.now.utc.iso8601, message: "job started", level: "info")
502
+ warn JSON.generate(client.delivery_health.to_h)
503
+ client.shutdown
504
+ ```
505
+
506
+ 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.
507
+
508
+ 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.
509
+
510
+ `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.
511
+
512
+ 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.
513
+
514
+ ## Sidekiq Tracing
515
+
516
+ 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:
517
+
518
+ ```ruby
519
+ require "logbrew"
520
+ require "logbrew/sidekiq"
521
+ require "sidekiq"
522
+
523
+ transport = LogBrew::HttpTransport.new(timeout: 10)
524
+ client = LogBrew::Client.create_automatic(
525
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
526
+ sdk_name: "checkout-worker",
527
+ sdk_version: "1.0.0",
528
+ transport: transport
529
+ )
530
+ sidekiq_tracing = LogBrew::Sidekiq::Instrumentation.create(
531
+ client: client,
532
+ max_retries: 25
533
+ )
534
+
535
+ Sidekiq.configure_client { |config| sidekiq_tracing.register_client(config) }
536
+ Sidekiq.configure_server do |config|
537
+ sidekiq_tracing.register_client(config)
538
+ sidekiq_tracing.register_server(config)
539
+ config.on(:quiet) { sidekiq_tracing.quiet }
540
+ config.on(:shutdown) { sidekiq_tracing.shutdown }
541
+ end
542
+ ```
543
+
544
+ 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.
545
+
546
+ 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.
547
+
548
+ 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.
549
+
550
+ ## Persistent Worker Delivery
551
+
552
+ 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:
553
+
554
+ ```ruby
555
+ queue_path = ENV.fetch("LOGBREW_PERSISTENT_QUEUE_PATH")
556
+
557
+ client = LogBrew::Client.create(
558
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
559
+ sdk_name: "checkout-worker",
560
+ sdk_version: "1.0.0",
561
+ persistent_queue_path: queue_path,
562
+ on_event_dropped: lambda do |notice|
563
+ warn "LogBrew delivery pressure: #{notice.reason} (#{notice.dropped_events} dropped)"
564
+ end
565
+ )
566
+
567
+ client.log("evt_job_started", Time.now.utc.iso8601, message: "job started", level: "info")
568
+ client.shutdown(LogBrew::HttpTransport.new)
569
+ ```
570
+
571
+ 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.
572
+
573
+ 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.
574
+
575
+ 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.
576
+
279
577
  ## Example Source
280
578
 
281
579
  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.
@@ -288,7 +586,7 @@ The `examples` directory contains copyable snippets for creating a client, sendi
288
586
  require "logbrew"
289
587
 
290
588
  client = LogBrew::Client.create(
291
- api_key: "LOGBREW_API_KEY",
589
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
292
590
  sdk_name: "my-ruby-app",
293
591
  sdk_version: "1.0.0"
294
592
  )
@@ -310,13 +608,15 @@ The adapter respects Ruby logger levels and lazy block messages, maps `DEBUG`/`I
310
608
 
311
609
  ## Rack And Rails Middleware
312
610
 
313
- Use `LogBrew::RackMiddleware` when a Rails, Sinatra, or Rack app should capture request spans and unhandled app exceptions without adding a framework dependency to the SDK.
611
+ Rails applications should use the automatic Rails quick start above. Use
612
+ `LogBrew::RackMiddleware` directly only for Sinatra, plain Rack, or a Rails app
613
+ that intentionally owns custom middleware wiring.
314
614
 
315
615
  ```ruby
316
616
  require "logbrew"
317
617
 
318
618
  client = LogBrew::Client.create(
319
- api_key: "LOGBREW_API_KEY",
619
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
320
620
  sdk_name: "my-rails-app",
321
621
  sdk_version: "1.0.0"
322
622
  )
@@ -340,17 +640,26 @@ app = LogBrew::RackMiddleware.new(
340
640
  )
341
641
  ```
342
642
 
343
- The middleware records successful responses as span events, records unhandled app exceptions as issue plus error-span events, and re-raises app exceptions so Rails or Rack keeps normal response handling. It captures method, path without query text, status code, request id when present, primitive base metadata, exception type/message, and duration. Exception backtrace text is omitted unless `include_exception_backtrace: true` is set. Events queue by default; pass `transport:` plus `flush_on_response: true` when each response should flush.
643
+ The manual middleware records successful responses as span events, records
644
+ unhandled app exceptions as issue plus error-span events, and re-raises app
645
+ exceptions so Rack keeps normal response handling. Its compatibility defaults
646
+ retain path, request-ID, and exception-message capture. Set
647
+ `include_exception_message: false` for type-only issues. Exception backtrace
648
+ text is omitted unless `include_exception_backtrace: true` is set. Events queue
649
+ by default; pass `transport:` plus `flush_on_response: true` when each response
650
+ should flush.
344
651
 
345
652
  ## Rails Error Subscriber
346
653
 
347
- Use `LogBrew::RailsErrorSubscriber` when handled or manually reported Rails errors should queue LogBrew issue events through Rails' own error reporter.
654
+ The automatic Rails integration already subscribes to handled Rails errors.
655
+ Use `LogBrew::RailsErrorSubscriber` directly only when an application owns a
656
+ custom Rails error-reporting lifecycle.
348
657
 
349
658
  ```ruby
350
659
  require "logbrew"
351
660
 
352
661
  client = LogBrew::Client.create(
353
- api_key: "LOGBREW_API_KEY",
662
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
354
663
  sdk_name: "my-rails-app",
355
664
  sdk_version: "1.0.0"
356
665
  )
@@ -366,18 +675,29 @@ Rails.error.subscribe(
366
675
  )
367
676
  ```
368
677
 
369
- The subscriber implements `report(error, handled:, severity:, context:, source:, **options)`, captures handled state, severity, Rails source, primitive context values, primitive base metadata, and exception type/message, and omits exception backtrace text unless `include_exception_backtrace: true` is set. It queues by default; pass `transport:` plus `flush_on_report: true` when each report should flush. If you also use `LogBrew::RackMiddleware`, keep the subscriber focused on handled/manual reports so unhandled request exceptions are not captured twice.
678
+ The manual subscriber implements
679
+ `report(error, handled:, severity:, context:, source:, **options)`. Its
680
+ compatibility default includes primitive context values and exception messages;
681
+ set `include_exception_message: false` for type-only issues. Backtrace text is
682
+ omitted unless `include_exception_backtrace: true` is set. If you also use the
683
+ manual Rack middleware, keep this subscriber focused on handled reports so
684
+ unhandled request exceptions are not captured twice.
370
685
 
371
686
  ## Behavior
372
687
 
373
688
  - `preview_json` returns the queued batch as pretty JSON.
374
- - `flush(transport)` sends queued events, retries retryable failures, and clears the queue only after a 2xx response.
689
+ - `persistent_queue_path:` enables explicit owner-only, single-process restart recovery; `purge_pending_events` explicitly discards its pending prefix.
690
+ - `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.
691
+ - `LogBrew::Sidekiq::Instrumentation` explicitly installs optional client/server middleware with bounded W3C propagation, fixed telemetry, and app-owned quiet/shutdown hooks.
692
+ - `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.
693
+ - 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.
375
694
  - `metric(...)` queues explicit, application-owned metric events with name, kind, value, unit, temporality, and low-cardinality metadata validation.
376
695
  - `LogBrew::ProductTimeline` builds explicit, application-owned product action and network milestone timeline events with primitive metadata and query/hash-free routes.
377
696
  - `LogBrew::SupportTicketDraft.create` builds explicit, local-only support-ticket create payload drafts with redacted diagnostics and no backend route calls.
378
697
  - `LogBrew::HttpTransport` sends queued batches through Ruby's standard `Net::HTTP` with configurable endpoint, headers, timeout, and app-owned HTTP client support.
379
698
  - `LogBrew::RackMiddleware` captures Rack request spans and unhandled app exceptions without requiring Rails or Rack at runtime.
380
699
  - `LogBrew::RailsErrorSubscriber` captures handled/manual Rails error reports without requiring Rails at runtime.
700
+ - `LogBrew::Rails` automatically installs privacy-bounded Rails request spans, handled-error issues, per-process delivery, health access, and idempotent shutdown when the canonical server key is configured.
381
701
  - `shutdown(transport)` flushes queued events and rejects later writes.
382
702
  - `LogBrew::RecordingTransport.always_accept` is useful when you want to inspect queued JSON before network delivery.
383
703
  - `LogBrew::SdkError` exposes stable `code` and `message` values for user-facing failure handling.
data/examples/Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: help run run-readme-example run-real-user-smoke run-first-useful-telemetry run-http-trace-correlation
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'
@@ -6,6 +6,9 @@ help:
6
6
  @printf '%s\n' 'run-real-user-smoke -> make run-real-user-smoke'
7
7
  @printf '%s\n' 'run-first-useful-telemetry -> make run-first-useful-telemetry'
8
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'
9
12
 
10
13
  run: run-real-user-smoke
11
14
 
@@ -20,3 +23,12 @@ run-first-useful-telemetry:
20
23
 
21
24
  run-http-trace-correlation:
22
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_SERVER_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
+ )
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logbrew"
5
+
6
+ queue_path = ENV.fetch("LOGBREW_PERSISTENT_QUEUE_PATH")
7
+ dropped = 0
8
+ last_drop_reason = nil
9
+ loaded_spec = Gem.loaded_specs["logbrew-sdk"]
10
+ client = LogBrew::Client.create(
11
+ api_key: "LOGBREW_API_KEY",
12
+ sdk_name: "logbrew-ruby-persistent-worker",
13
+ sdk_version: loaded_spec ? loaded_spec.version.to_s : "0.1.0",
14
+ persistent_queue_path: queue_path,
15
+ on_event_dropped: lambda do |notice|
16
+ dropped = notice.dropped_events
17
+ last_drop_reason = notice.reason
18
+ end
19
+ )
20
+
21
+ client.release("evt_worker_release", "2026-07-13T18:00:00Z", version: "2.0.0")
22
+ client.environment("evt_worker_environment", "2026-07-13T18:00:01Z", name: "production")
23
+ client.log("evt_worker_started", "2026-07-13T18:00:02Z", message: "worker started", level: "info")
24
+
25
+ response = client.shutdown(LogBrew::RecordingTransport.always_accept)
26
+ puts JSON.generate(
27
+ ok: response.status_code == 202,
28
+ status: response.status_code,
29
+ attempts: response.attempts,
30
+ batches: response.batches,
31
+ dropped: dropped,
32
+ lastDropReason: last_drop_reason
33
+ )
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logbrew"
4
+ require "logbrew/sidekiq"
5
+ require "sidekiq"
6
+
7
+ transport = LogBrew::HttpTransport.new(timeout: 10)
8
+ client = LogBrew::Client.create_automatic(
9
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
10
+ sdk_name: "checkout-worker",
11
+ sdk_version: "1.0.0",
12
+ transport: transport
13
+ )
14
+ instrumentation = LogBrew::Sidekiq::Instrumentation.create(client: client, max_retries: 25)
15
+
16
+ Sidekiq.configure_client { |config| instrumentation.register_client(config) }
17
+ Sidekiq.configure_server do |config|
18
+ instrumentation.register_client(config)
19
+ instrumentation.register_server(config)
20
+ config.on(:quiet) { instrumentation.quiet }
21
+ config.on(:shutdown) { instrumentation.shutdown }
22
+ end