logbrew-sdk 0.1.2 → 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: e9f3c896d1d43ab4b324623e3a121cbc675fa9780fcb157f01d2aa52c54d07b4
4
- data.tar.gz: ef86c767dbcf0b73903f37aa5492f3a54fda28318b838725155229ed95cee368
3
+ metadata.gz: 233087ef98eb35d7df84720956a89dfbf66368ac55269900a38491192c7a67a3
4
+ data.tar.gz: 1720b83bf8dcc562771936091f0196996415005e6a5ea254ca4e0a803e3ea1ac
5
5
  SHA512:
6
- metadata.gz: 90883b0323ec61e83561a09599cbcd3e1219fe25ad8ecbbd683ea10bb49f45e44d41aa66cfce8ea1818dc6858218c79dce654a5c68578a7f7ef82b80b209951f
7
- data.tar.gz: c6af849cc5d6e96cfa8e8bfa2dedc536dd82d64ba9007a9faa9bbb13b5f7bed4b63f9e32661c879199ebf0c3b94f00ecbd6e74d3a21919fcee40e9b0f6f8942f
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
  )
@@ -50,7 +144,7 @@ one work item at a time and needs an explicit telemetry boundary:
50
144
 
51
145
  ```ruby
52
146
  client = LogBrew::Client.create(
53
- api_key: "LOGBREW_API_KEY",
147
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
54
148
  sdk_name: "checkout-worker",
55
149
  sdk_version: "1.0.0"
56
150
  )
@@ -339,7 +433,7 @@ Use `LogBrew::HttpTransport` when you want the SDK to POST queued batches to Log
339
433
  require "logbrew"
340
434
 
341
435
  client = LogBrew::Client.create(
342
- api_key: "LOGBREW_API_KEY",
436
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
343
437
  sdk_name: "my-ruby-app",
344
438
  sdk_version: "1.0.0"
345
439
  )
@@ -364,7 +458,7 @@ The client bounds queued telemetry and each transport request independently. Que
364
458
  ```ruby
365
459
  dropped = 0
366
460
  client = LogBrew::Client.create(
367
- api_key: "LOGBREW_API_KEY",
461
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
368
462
  sdk_name: "my-ruby-app",
369
463
  sdk_version: "1.0.0",
370
464
  max_queue_size: 1_000,
@@ -393,7 +487,7 @@ Applications that own their transport can opt into one lazy delivery worker. Man
393
487
  ```ruby
394
488
  transport = LogBrew::HttpTransport.new(timeout: 10)
395
489
  client = LogBrew::Client.create_automatic(
396
- api_key: ENV.fetch("LOGBREW_API_KEY"),
490
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
397
491
  sdk_name: "checkout-worker",
398
492
  sdk_version: "1.0.0",
399
493
  transport: transport,
@@ -428,7 +522,7 @@ require "sidekiq"
428
522
 
429
523
  transport = LogBrew::HttpTransport.new(timeout: 10)
430
524
  client = LogBrew::Client.create_automatic(
431
- api_key: ENV.fetch("LOGBREW_API_KEY"),
525
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
432
526
  sdk_name: "checkout-worker",
433
527
  sdk_version: "1.0.0",
434
528
  transport: transport
@@ -461,7 +555,7 @@ Server workers that need restart recovery can opt into an app-owned persistent q
461
555
  queue_path = ENV.fetch("LOGBREW_PERSISTENT_QUEUE_PATH")
462
556
 
463
557
  client = LogBrew::Client.create(
464
- api_key: ENV.fetch("LOGBREW_API_KEY"),
558
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
465
559
  sdk_name: "checkout-worker",
466
560
  sdk_version: "1.0.0",
467
561
  persistent_queue_path: queue_path,
@@ -492,7 +586,7 @@ The `examples` directory contains copyable snippets for creating a client, sendi
492
586
  require "logbrew"
493
587
 
494
588
  client = LogBrew::Client.create(
495
- api_key: "LOGBREW_API_KEY",
589
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
496
590
  sdk_name: "my-ruby-app",
497
591
  sdk_version: "1.0.0"
498
592
  )
@@ -514,13 +608,15 @@ The adapter respects Ruby logger levels and lazy block messages, maps `DEBUG`/`I
514
608
 
515
609
  ## Rack And Rails Middleware
516
610
 
517
- 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.
518
614
 
519
615
  ```ruby
520
616
  require "logbrew"
521
617
 
522
618
  client = LogBrew::Client.create(
523
- api_key: "LOGBREW_API_KEY",
619
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
524
620
  sdk_name: "my-rails-app",
525
621
  sdk_version: "1.0.0"
526
622
  )
@@ -544,17 +640,26 @@ app = LogBrew::RackMiddleware.new(
544
640
  )
545
641
  ```
546
642
 
547
- 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.
548
651
 
549
652
  ## Rails Error Subscriber
550
653
 
551
- 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.
552
657
 
553
658
  ```ruby
554
659
  require "logbrew"
555
660
 
556
661
  client = LogBrew::Client.create(
557
- api_key: "LOGBREW_API_KEY",
662
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
558
663
  sdk_name: "my-rails-app",
559
664
  sdk_version: "1.0.0"
560
665
  )
@@ -570,7 +675,13 @@ Rails.error.subscribe(
570
675
  )
571
676
  ```
572
677
 
573
- 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.
574
685
 
575
686
  ## Behavior
576
687
 
@@ -586,6 +697,7 @@ The subscriber implements `report(error, handled:, severity:, context:, source:,
586
697
  - `LogBrew::HttpTransport` sends queued batches through Ruby's standard `Net::HTTP` with configurable endpoint, headers, timeout, and app-owned HTTP client support.
587
698
  - `LogBrew::RackMiddleware` captures Rack request spans and unhandled app exceptions without requiring Rails or Rack at runtime.
588
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.
589
701
  - `shutdown(transport)` flushes queued events and rejects later writes.
590
702
  - `LogBrew::RecordingTransport.always_accept` is useful when you want to inspect queued JSON before network delivery.
591
703
  - `LogBrew::SdkError` exposes stable `code` and `message` values for user-facing failure handling.
@@ -5,7 +5,7 @@ require "logbrew"
5
5
 
6
6
  transport = LogBrew::RecordingTransport.always_accept
7
7
  client = LogBrew::Client.create_automatic(
8
- api_key: ENV.fetch("LOGBREW_API_KEY", "local-example-key"),
8
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY", "local-example-key"),
9
9
  sdk_name: "automatic-delivery-example",
10
10
  sdk_version: "1.0.0",
11
11
  transport: transport,
@@ -6,7 +6,7 @@ require "sidekiq"
6
6
 
7
7
  transport = LogBrew::HttpTransport.new(timeout: 10)
8
8
  client = LogBrew::Client.create_automatic(
9
- api_key: ENV.fetch("LOGBREW_API_KEY"),
9
+ api_key: ENV.fetch("LOGBREW_SERVER_API_KEY"),
10
10
  sdk_name: "checkout-worker",
11
11
  sdk_version: "1.0.0",
12
12
  transport: transport
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logbrew"
4
+ require "rails/railtie"
5
+ require_relative "rails_integration"
6
+
7
+ module LogBrew
8
+ # Process-safe Rails integration installed by RailsRailtie.
9
+ module Rails
10
+ class << self
11
+ attr_reader :runtime
12
+
13
+ def install(application:, environment: ENV)
14
+ @installation_mutex ||= Mutex.new
15
+ @installation_mutex.synchronize do
16
+ return @runtime unless @runtime.nil?
17
+
18
+ configuration = Configuration.from_environment(
19
+ environment,
20
+ application_name: application_name(application),
21
+ rails_environment: ::Rails.env.to_s,
22
+ rails_version: ::Rails.version.to_s
23
+ )
24
+ @runtime = Runtime.new(
25
+ configuration,
26
+ on_error: ->(stage, error) { log_capture_failure(application, stage, error) }
27
+ )
28
+ register_shutdown
29
+ @runtime
30
+ end
31
+ end
32
+
33
+ def client
34
+ @runtime&.client
35
+ end
36
+
37
+ def delivery_health
38
+ @runtime&.delivery_health
39
+ end
40
+
41
+ def shutdown
42
+ @runtime&.shutdown
43
+ end
44
+
45
+ private
46
+
47
+ def application_name(application)
48
+ application_class = application.class
49
+ if application_class.respond_to?(:module_parent_name)
50
+ name = application_class.module_parent_name.to_s
51
+ return name unless name.empty?
52
+ end
53
+
54
+ name = application_class.name.to_s.sub(/::Application\z/, "")
55
+ name.empty? ? "rails" : name
56
+ end
57
+
58
+ def register_shutdown
59
+ return if @shutdown_registered
60
+
61
+ at_exit { shutdown }
62
+ @shutdown_registered = true
63
+ end
64
+
65
+ def log_capture_failure(application, stage, error)
66
+ logger = application.respond_to?(:logger) ? application.logger : nil
67
+ logger ||= ::Rails.logger if ::Rails.respond_to?(:logger)
68
+ return unless logger.respond_to?(:warn)
69
+
70
+ logger.warn("LogBrew Rails #{stage} failed (#{error.class.name}); application behavior was preserved")
71
+ rescue StandardError
72
+ nil
73
+ end
74
+ end
75
+ end
76
+
77
+ class RailsRailtie < ::Rails::Railtie
78
+ initializer "logbrew.install", after: :load_config_initializers do |application|
79
+ runtime = LogBrew::Rails.install(application: application)
80
+ middleware = application.config.middleware
81
+ if defined?(::ActionDispatch::ShowExceptions)
82
+ middleware.insert_after(
83
+ ::ActionDispatch::ShowExceptions,
84
+ LogBrew::Rails::RequestMiddleware,
85
+ runtime: runtime
86
+ )
87
+ else
88
+ middleware.use(LogBrew::Rails::RequestMiddleware, runtime: runtime)
89
+ end
90
+ end
91
+
92
+ config.after_initialize do |application|
93
+ runtime = LogBrew::Rails.runtime
94
+ next if runtime.nil? || !runtime.configuration.enabled?
95
+
96
+ reporter = LogBrew::Rails::ErrorReporter.new(runtime)
97
+ if ::Rails.respond_to?(:error) && ::Rails.error.respond_to?(:subscribe)
98
+ ::Rails.error.subscribe(reporter)
99
+ elsif application.respond_to?(:executor) && application.executor.respond_to?(:error_reporter)
100
+ error_reporter = application.executor.error_reporter
101
+ error_reporter.subscribe(reporter) if error_reporter.respond_to?(:subscribe)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,622 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logbrew" unless defined?(LogBrew::Client)
4
+ require "uri"
5
+
6
+ module LogBrew
7
+ module Rails
8
+ # Immutable, environment-derived settings for the automatic Rails adapter.
9
+ class Configuration
10
+ DEFAULT_ENDPOINT = LogBrew::HttpTransport::DEFAULT_ENDPOINT
11
+ DEFAULT_REQUEST_TIMEOUT_MS = 10_000
12
+ DEFAULT_FLUSH_INTERVAL_MS = 5_000
13
+ DEFAULT_FLUSH_THRESHOLD = 100
14
+ MAX_LABEL_BYTES = 255
15
+ MAX_ENDPOINT_BYTES = 2_048
16
+ LOOPBACK_HOSTS = %w[localhost 127.0.0.1 ::1 [::1]].freeze
17
+
18
+ attr_reader(
19
+ :api_key,
20
+ :service_name,
21
+ :app_environment,
22
+ :rails_version,
23
+ :release,
24
+ :endpoint,
25
+ :request_timeout,
26
+ :flush_interval,
27
+ :flush_threshold
28
+ )
29
+
30
+ def self.from_environment(environment, application_name:, rails_environment:, rails_version:)
31
+ unless environment.respond_to?(:[]) && environment.respond_to?(:key?)
32
+ raise SdkError.new("configuration_error", "Rails environment must provide key lookup")
33
+ end
34
+
35
+ enabled_setting = boolean_value(environment, "LOGBREW_ENABLED", nil)
36
+ return disabled(application_name, rails_environment, rails_version) if enabled_setting == false
37
+
38
+ canonical_key = server_key_value(environment["LOGBREW_SERVER_API_KEY"])
39
+ if canonical_key.nil?
40
+ legacy_present = %w[LOGBREW_API_KEY LOGBREW_INGEST_KEY].any? do |name|
41
+ !optional_text(environment[name]).nil?
42
+ end
43
+ if enabled_setting == true || legacy_present || environment.key?("LOGBREW_SERVER_API_KEY")
44
+ raise SdkError.new(
45
+ "configuration_error",
46
+ "set LOGBREW_SERVER_API_KEY to a non-empty server API key, or set LOGBREW_ENABLED=false"
47
+ )
48
+ end
49
+ return disabled(application_name, rails_environment, rails_version)
50
+ end
51
+
52
+ new(
53
+ enabled: true,
54
+ api_key: canonical_key,
55
+ service_name: bounded_label(
56
+ optional_text(environment["LOGBREW_SERVICE_NAME"]) || application_name,
57
+ "LOGBREW_SERVICE_NAME"
58
+ ),
59
+ app_environment: bounded_label(
60
+ optional_text(environment["LOGBREW_ENVIRONMENT"]) || rails_environment,
61
+ "LOGBREW_ENVIRONMENT"
62
+ ),
63
+ rails_version: bounded_label(rails_version, "Rails version"),
64
+ release: optional_bounded_label(environment["LOGBREW_RELEASE"], "LOGBREW_RELEASE"),
65
+ endpoint: endpoint_value(environment["LOGBREW_ENDPOINT"]),
66
+ request_timeout: integer_value(
67
+ environment,
68
+ "LOGBREW_REQUEST_TIMEOUT_MS",
69
+ DEFAULT_REQUEST_TIMEOUT_MS,
70
+ 1,
71
+ 600_000
72
+ ) / 1_000.0,
73
+ flush_interval: integer_value(
74
+ environment,
75
+ "LOGBREW_FLUSH_INTERVAL_MS",
76
+ DEFAULT_FLUSH_INTERVAL_MS,
77
+ 10,
78
+ 3_600_000
79
+ ) / 1_000.0,
80
+ flush_threshold: integer_value(
81
+ environment,
82
+ "LOGBREW_FLUSH_THRESHOLD",
83
+ DEFAULT_FLUSH_THRESHOLD,
84
+ 1,
85
+ 1_000
86
+ ),
87
+ capture_exception_messages: boolean_value(
88
+ environment,
89
+ "LOGBREW_CAPTURE_EXCEPTION_MESSAGES",
90
+ false
91
+ ),
92
+ include_exception_backtrace: boolean_value(
93
+ environment,
94
+ "LOGBREW_INCLUDE_EXCEPTION_BACKTRACE",
95
+ false
96
+ )
97
+ )
98
+ end
99
+
100
+ def self.disabled(application_name, rails_environment, rails_version)
101
+ new(
102
+ enabled: false,
103
+ api_key: nil,
104
+ service_name: bounded_label(application_name, "Rails application name"),
105
+ app_environment: bounded_label(rails_environment, "Rails environment"),
106
+ rails_version: bounded_label(rails_version, "Rails version"),
107
+ release: nil,
108
+ endpoint: DEFAULT_ENDPOINT,
109
+ request_timeout: DEFAULT_REQUEST_TIMEOUT_MS / 1_000.0,
110
+ flush_interval: DEFAULT_FLUSH_INTERVAL_MS / 1_000.0,
111
+ flush_threshold: DEFAULT_FLUSH_THRESHOLD,
112
+ capture_exception_messages: false,
113
+ include_exception_backtrace: false
114
+ )
115
+ end
116
+
117
+ def self.optional_text(value)
118
+ return nil if value.nil?
119
+
120
+ text = value.to_s.strip
121
+ text.empty? ? nil : text
122
+ end
123
+ private_class_method :optional_text
124
+
125
+ def self.server_key_value(value)
126
+ text = optional_text(value)
127
+ return nil if text.nil?
128
+ if text.bytesize > 4_096 || !text.valid_encoding?
129
+ raise SdkError.new("configuration_error", "LOGBREW_SERVER_API_KEY is invalid")
130
+ end
131
+
132
+ text
133
+ end
134
+ private_class_method :server_key_value
135
+
136
+ def self.bounded_label(value, label)
137
+ text = optional_text(value)
138
+ raise SdkError.new("configuration_error", "#{label} must be non-empty") if text.nil?
139
+ if text.bytesize > MAX_LABEL_BYTES || !text.valid_encoding? || text.match?(/[[:cntrl:]]/)
140
+ raise SdkError.new("configuration_error", "#{label} must be at most #{MAX_LABEL_BYTES} bytes")
141
+ end
142
+
143
+ text.freeze
144
+ end
145
+ private_class_method :bounded_label
146
+
147
+ def self.optional_bounded_label(value, label)
148
+ text = optional_text(value)
149
+ return nil if text.nil?
150
+
151
+ bounded_label(text, label)
152
+ end
153
+ private_class_method :optional_bounded_label
154
+
155
+ def self.boolean_value(environment, name, default)
156
+ value = optional_text(environment[name])
157
+ return default if value.nil? && !environment.key?(name)
158
+
159
+ case value&.downcase
160
+ when "true", "1", "yes", "on" then true
161
+ when "false", "0", "no", "off" then false
162
+ else
163
+ raise SdkError.new("configuration_error", "#{name} must be true or false")
164
+ end
165
+ end
166
+ private_class_method :boolean_value
167
+
168
+ def self.integer_value(environment, name, default, minimum, maximum)
169
+ text = optional_text(environment[name])
170
+ return default if text.nil? && !environment.key?(name)
171
+
172
+ value = Integer(text, 10)
173
+ return value if value >= minimum && value <= maximum
174
+
175
+ raise ArgumentError
176
+ rescue ArgumentError, TypeError
177
+ raise SdkError.new(
178
+ "configuration_error",
179
+ "#{name} must be an integer between #{minimum} and #{maximum}"
180
+ )
181
+ end
182
+ private_class_method :integer_value
183
+
184
+ def self.endpoint_value(value)
185
+ text = optional_text(value) || DEFAULT_ENDPOINT
186
+ if text.bytesize > MAX_ENDPOINT_BYTES
187
+ raise SdkError.new("configuration_error", "LOGBREW_ENDPOINT is too long")
188
+ end
189
+
190
+ uri = URI.parse(text)
191
+ valid_http = uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
192
+ safe_scheme = uri.scheme == "https" || (
193
+ uri.scheme == "http" && LOOPBACK_HOSTS.include?(uri.host.to_s.downcase)
194
+ )
195
+ if !valid_http || !safe_scheme || !uri.userinfo.nil? || !uri.fragment.nil?
196
+ raise SdkError.new(
197
+ "configuration_error",
198
+ "LOGBREW_ENDPOINT must use https, or http on localhost, without embedded user info or a fragment"
199
+ )
200
+ end
201
+
202
+ uri.to_s.freeze
203
+ rescue URI::InvalidURIError
204
+ raise SdkError.new("configuration_error", "LOGBREW_ENDPOINT must be a valid HTTP URL")
205
+ end
206
+ private_class_method :endpoint_value
207
+
208
+ def initialize(
209
+ enabled:,
210
+ api_key:,
211
+ service_name:,
212
+ app_environment:,
213
+ rails_version:,
214
+ release:,
215
+ endpoint:,
216
+ request_timeout:,
217
+ flush_interval:,
218
+ flush_threshold:,
219
+ capture_exception_messages:,
220
+ include_exception_backtrace:
221
+ )
222
+ @enabled = enabled
223
+ @api_key = api_key&.dup&.freeze
224
+ @service_name = service_name
225
+ @app_environment = app_environment
226
+ @rails_version = rails_version
227
+ @release = release
228
+ @endpoint = endpoint
229
+ @request_timeout = request_timeout
230
+ @flush_interval = flush_interval
231
+ @flush_threshold = flush_threshold
232
+ @capture_exception_messages = capture_exception_messages
233
+ @include_exception_backtrace = include_exception_backtrace
234
+ freeze
235
+ end
236
+
237
+ def enabled?
238
+ @enabled
239
+ end
240
+
241
+ def capture_exception_messages?
242
+ @capture_exception_messages
243
+ end
244
+
245
+ def include_exception_backtrace?
246
+ @include_exception_backtrace
247
+ end
248
+ end
249
+
250
+ # Owns one lazy automatic-delivery client per operating-system process.
251
+ class Runtime
252
+ attr_reader :configuration
253
+
254
+ def initialize(
255
+ configuration,
256
+ transport_factory: nil,
257
+ client_factory: nil,
258
+ timestamp_provider: nil,
259
+ process_id_provider: nil,
260
+ on_error: nil
261
+ )
262
+ unless configuration.is_a?(Configuration)
263
+ raise SdkError.new("configuration_error", "Rails runtime requires a Rails configuration")
264
+ end
265
+
266
+ @configuration = configuration
267
+ @transport_factory = transport_factory || method(:build_transport)
268
+ @client_factory = client_factory || method(:build_client)
269
+ @timestamp_provider = timestamp_provider || -> { Time.now.utc }
270
+ @process_id_provider = process_id_provider || -> { Process.pid }
271
+ @on_error = on_error
272
+ @mutex = Mutex.new
273
+ @state_process_id = @process_id_provider.call
274
+ @client = nil
275
+ @shutdown_response = nil
276
+ end
277
+
278
+ def client
279
+ return nil unless @configuration.enabled?
280
+
281
+ prepare_process_state
282
+ @mutex.synchronize do
283
+ return nil unless @shutdown_response.nil?
284
+
285
+ @client ||= create_client
286
+ end
287
+ rescue StandardError => error
288
+ report_error("client_initialization", error)
289
+ nil
290
+ end
291
+
292
+ def delivery_health
293
+ active_client = client
294
+ active_client&.delivery_health
295
+ rescue StandardError => error
296
+ report_error("delivery_health", error)
297
+ nil
298
+ end
299
+
300
+ def shutdown
301
+ return nil unless @configuration.enabled?
302
+
303
+ prepare_process_state
304
+ @mutex.synchronize do
305
+ return @shutdown_response unless @shutdown_response.nil?
306
+ return nil if @client.nil?
307
+
308
+ @shutdown_response = @client.shutdown
309
+ end
310
+ rescue StandardError => error
311
+ report_error("shutdown", error)
312
+ nil
313
+ end
314
+
315
+ def report_error(stage, error)
316
+ return unless @on_error.respond_to?(:call)
317
+
318
+ @on_error.call(stage.to_s, error)
319
+ rescue StandardError
320
+ nil
321
+ end
322
+
323
+ private
324
+
325
+ def prepare_process_state
326
+ process_id = @process_id_provider.call
327
+ return if @state_process_id == process_id
328
+
329
+ @mutex = Mutex.new
330
+ @client = nil
331
+ @shutdown_response = nil
332
+ @state_process_id = process_id
333
+ end
334
+
335
+ def create_client
336
+ transport = @transport_factory.call(@configuration)
337
+ created = @client_factory.call(@configuration, transport)
338
+ record_process_context(created)
339
+ created
340
+ end
341
+
342
+ def build_transport(configuration)
343
+ LogBrew::HttpTransport.new(
344
+ endpoint: configuration.endpoint,
345
+ timeout: configuration.request_timeout
346
+ )
347
+ end
348
+
349
+ def build_client(configuration, transport)
350
+ LogBrew::Client.create_automatic(
351
+ api_key: configuration.api_key,
352
+ sdk_name: "logbrew-ruby-rails",
353
+ sdk_version: LogBrew::VERSION,
354
+ transport: transport,
355
+ flush_interval: configuration.flush_interval,
356
+ flush_threshold: configuration.flush_threshold
357
+ )
358
+ end
359
+
360
+ def record_process_context(created)
361
+ timestamp = logbrew_timestamp
362
+ metadata = base_metadata
363
+ created.environment(
364
+ "ruby_rails_environment_#{SecureRandom.hex(8)}",
365
+ timestamp,
366
+ name: @configuration.app_environment,
367
+ metadata: metadata
368
+ )
369
+ return if @configuration.release.nil?
370
+
371
+ created.release(
372
+ "ruby_rails_release_#{SecureRandom.hex(8)}",
373
+ timestamp,
374
+ version: @configuration.release,
375
+ metadata: metadata
376
+ )
377
+ end
378
+
379
+ def base_metadata
380
+ {
381
+ "service" => @configuration.service_name,
382
+ "environment" => @configuration.app_environment,
383
+ "framework" => "rails",
384
+ "framework.version" => @configuration.rails_version
385
+ }
386
+ end
387
+
388
+ def logbrew_timestamp
389
+ timestamp = @timestamp_provider.call
390
+ return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
391
+
392
+ timestamp.to_s
393
+ end
394
+ end
395
+
396
+ # Internal Rack adapter that replaces concrete request paths with Rails
397
+ # route templates while reusing the core request/error lifecycle.
398
+ class RailsRackMiddleware < LogBrew::RackMiddleware
399
+ private
400
+
401
+ def request_name(env)
402
+ "#{request_method(env)} #{route_template(env)}"
403
+ end
404
+
405
+ def request_method(env)
406
+ value = env_value(env, "REQUEST_METHOD").to_s.upcase
407
+ value.match?(/\A[A-Z]{1,16}\z/) ? value : "GET"
408
+ end
409
+
410
+ def request_path(env)
411
+ route_template(env)
412
+ end
413
+
414
+ def request_metadata(env, status_code)
415
+ metadata = super
416
+ metadata.delete("http.path")
417
+ metadata.delete("action_dispatch.request_id")
418
+ metadata.delete("HTTP_X_REQUEST_ID")
419
+ metadata["source"] = "rails"
420
+ metadata["http.method"] = request_method(env)
421
+ metadata["http.route"] = route_template(env)
422
+ metadata["http.status_code"] = status_code
423
+ metadata["http.status_class"] = "#{status_code.to_i / 100}xx"
424
+ controller, action = controller_and_action(env)
425
+ metadata["rails.controller"] = controller unless controller.nil?
426
+ metadata["rails.action"] = action unless action.nil?
427
+ metadata
428
+ end
429
+
430
+ def route_template(env)
431
+ route = bounded_route(env_value(env, "action_dispatch.route_uri_pattern"))
432
+ return route unless route.nil?
433
+
434
+ route = bounded_route(matched_route_pattern(env))
435
+ return route unless route.nil?
436
+
437
+ controller, action = controller_and_action(env)
438
+ return "/#{controller}##{action}" unless controller.nil? || action.nil?
439
+
440
+ "<unmatched>"
441
+ end
442
+
443
+ def matched_route_pattern(env)
444
+ return nil unless env.respond_to?(:[])
445
+
446
+ route = env["action_dispatch.route"]
447
+ return nil unless route.respond_to?(:path)
448
+
449
+ path = route.path
450
+ return nil unless path.respond_to?(:spec)
451
+
452
+ path.spec.to_s
453
+ rescue StandardError
454
+ nil
455
+ end
456
+
457
+ def controller_and_action(env)
458
+ return [nil, nil] unless env.respond_to?(:[])
459
+
460
+ parameters = env["action_dispatch.request.path_parameters"]
461
+ return [nil, nil] unless parameters.is_a?(Hash)
462
+
463
+ [bounded_identifier(parameters[:controller] || parameters["controller"]),
464
+ bounded_identifier(parameters[:action] || parameters["action"])]
465
+ end
466
+
467
+ def bounded_route(value)
468
+ return nil if value.nil?
469
+
470
+ route = value.to_s.split(/[?#]/, 2).first.to_s.strip
471
+ return nil if route.empty? || route.bytesize > 255 || !route.valid_encoding?
472
+
473
+ route.start_with?("/") ? route : "/#{route}"
474
+ end
475
+
476
+ def bounded_identifier(value)
477
+ return nil if value.nil?
478
+
479
+ identifier = value.to_s
480
+ return nil unless identifier.match?(/\A[a-zA-Z0-9_\/.-]{1,128}\z/)
481
+
482
+ identifier
483
+ end
484
+ end
485
+ private_constant :RailsRackMiddleware
486
+
487
+ # Rails middleware entry point. Capture failures never call the app twice.
488
+ class RequestMiddleware
489
+ def initialize(app, runtime: LogBrew::Rails.runtime)
490
+ raise SdkError.new("validation_error", "Rails app must respond to call") unless app.respond_to?(:call)
491
+ raise SdkError.new("configuration_error", "LogBrew Rails runtime is not installed") if runtime.nil?
492
+
493
+ @app = app
494
+ @runtime = runtime
495
+ @mutex = Mutex.new
496
+ @adapter_client = nil
497
+ @adapter = nil
498
+ end
499
+
500
+ def call(environment)
501
+ active_client = @runtime.client
502
+ return @app.call(environment) if active_client.nil?
503
+
504
+ adapter = adapter_for(active_client)
505
+ return @app.call(environment) if adapter.nil?
506
+
507
+ adapter.call(environment)
508
+ end
509
+
510
+ private
511
+
512
+ def adapter_for(active_client)
513
+ @mutex.synchronize do
514
+ return @adapter if @adapter_client.equal?(active_client) && !@adapter.nil?
515
+
516
+ @adapter = RailsRackMiddleware.new(
517
+ @app,
518
+ client: active_client,
519
+ flush_on_response: false,
520
+ metadata: base_metadata,
521
+ include_exception_message: @runtime.configuration.capture_exception_messages?,
522
+ include_exception_backtrace: @runtime.configuration.include_exception_backtrace?,
523
+ on_error: ->(error) { @runtime.report_error("request_capture", error) }
524
+ )
525
+ @adapter_client = active_client
526
+ @adapter
527
+ end
528
+ rescue StandardError => error
529
+ @runtime.report_error("request_adapter", error)
530
+ nil
531
+ end
532
+
533
+ def base_metadata
534
+ configuration = @runtime.configuration
535
+ {
536
+ "service" => configuration.service_name,
537
+ "environment" => configuration.app_environment,
538
+ "framework" => "rails",
539
+ "framework.version" => configuration.rails_version
540
+ }.tap do |metadata|
541
+ metadata["release"] = configuration.release unless configuration.release.nil?
542
+ end
543
+ end
544
+ end
545
+
546
+ # Rails.error subscriber for handled reports. Unhandled errors stay owned
547
+ # by the request middleware so one exception cannot create two issues.
548
+ class ErrorReporter
549
+ CONTEXT_KEYS = %w[controller action].freeze
550
+
551
+ def initialize(runtime)
552
+ @runtime = runtime
553
+ @mutex = Mutex.new
554
+ @subscriber_client = nil
555
+ @subscriber = nil
556
+ end
557
+
558
+ def report(error, handled: true, severity: :error, context: nil, source: nil, **options)
559
+ return nil unless handled
560
+
561
+ active_client = @runtime.client
562
+ return nil if active_client.nil?
563
+
564
+ subscriber_for(active_client).report(
565
+ error,
566
+ handled: true,
567
+ severity: severity,
568
+ context: safe_context(context),
569
+ source: bounded_source(source),
570
+ **options
571
+ )
572
+ rescue StandardError => capture_error
573
+ @runtime.report_error("handled_error_capture", capture_error)
574
+ nil
575
+ end
576
+
577
+ private
578
+
579
+ def subscriber_for(active_client)
580
+ @mutex.synchronize do
581
+ return @subscriber if @subscriber_client.equal?(active_client) && !@subscriber.nil?
582
+
583
+ configuration = @runtime.configuration
584
+ @subscriber = LogBrew::RailsErrorSubscriber.new(
585
+ client: active_client,
586
+ flush_on_report: false,
587
+ metadata: {
588
+ "service" => configuration.service_name,
589
+ "environment" => configuration.app_environment,
590
+ "framework" => "rails",
591
+ "framework.version" => configuration.rails_version
592
+ },
593
+ include_exception_message: configuration.capture_exception_messages?,
594
+ include_exception_backtrace: configuration.include_exception_backtrace?,
595
+ on_error: ->(error) { @runtime.report_error("handled_error_capture", error) }
596
+ )
597
+ @subscriber_client = active_client
598
+ @subscriber
599
+ end
600
+ end
601
+
602
+ def safe_context(context)
603
+ return nil unless context.is_a?(Hash)
604
+
605
+ CONTEXT_KEYS.each_with_object({}) do |key, safe|
606
+ value = context[key] || context[key.to_sym]
607
+ next if value.nil?
608
+
609
+ text = value.to_s
610
+ safe[key] = text if text.match?(/\A[a-zA-Z0-9_\/.-]{1,128}\z/)
611
+ end
612
+ end
613
+
614
+ def bounded_source(source)
615
+ return nil if source.nil?
616
+
617
+ value = source.to_s
618
+ value.match?(/\A[a-zA-Z0-9_.:-]{1,64}\z/) ? value : "rails"
619
+ end
620
+ end
621
+ end
622
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogBrew
4
+ VERSION = "0.1.3"
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logbrew"
4
+ require "logbrew/rails" if defined?(::Rails::Railtie)
data/lib/logbrew.rb CHANGED
@@ -8,6 +8,8 @@ require "time"
8
8
  require "timeout"
9
9
  require "uri"
10
10
 
11
+ require_relative "logbrew/version"
12
+
11
13
  module LogBrew
12
14
  SEVERITY_VALUES = %w[trace debug info warn warning error fatal critical].freeze
13
15
  SEVERITY_ALIASES = {
@@ -369,6 +371,7 @@ module LogBrew
369
371
  event_id_prefix: DEFAULT_EVENT_ID_PREFIX,
370
372
  metadata: nil,
371
373
  timestamp_provider: nil,
374
+ include_exception_message: true,
372
375
  include_exception_backtrace: false,
373
376
  on_error: nil,
374
377
  raise_errors: false
@@ -384,10 +387,12 @@ module LogBrew
384
387
  @event_id_prefix = event_id_prefix
385
388
  @metadata = metadata || {}
386
389
  @timestamp_provider = timestamp_provider
390
+ @include_exception_message = include_exception_message
387
391
  @include_exception_backtrace = include_exception_backtrace
388
392
  @on_error = on_error
389
393
  @raise_errors = raise_errors
390
394
  @next_event_number = 0
395
+ @event_id_mutex = Mutex.new
391
396
  end
392
397
 
393
398
  def call(env)
@@ -428,19 +433,20 @@ module LogBrew
428
433
  end
429
434
 
430
435
  def capture_exception_issue(env, error)
431
- @client.issue(
432
- next_event_id("issue"),
433
- logbrew_timestamp,
436
+ attributes = {
434
437
  title: error.class.name,
435
438
  level: "error",
436
- message: error.message,
437
439
  metadata: exception_metadata(env, error)
438
- )
440
+ }
441
+ attributes[:message] = error.message if @include_exception_message
442
+ @client.issue(next_event_id("issue"), logbrew_timestamp, attributes)
439
443
  end
440
444
 
441
445
  def next_event_id(kind)
442
- @next_event_number += 1
443
- "#{@event_id_prefix}_#{kind}_#{@next_event_number}"
446
+ @event_id_mutex.synchronize do
447
+ @next_event_number += 1
448
+ "#{@event_id_prefix}_#{kind}_#{@next_event_number}"
449
+ end
444
450
  end
445
451
 
446
452
  def logbrew_timestamp
@@ -492,7 +498,7 @@ module LogBrew
492
498
  def exception_metadata(env, error)
493
499
  request_metadata(env, 500).tap do |metadata|
494
500
  metadata["exceptionType"] = error.class.name
495
- metadata["exceptionMessage"] = error.message
501
+ metadata["exceptionMessage"] = error.message if @include_exception_message
496
502
  metadata["exceptionBacktrace"] = error.backtrace.join("\n") if @include_exception_backtrace && error.backtrace
497
503
  end
498
504
  end
@@ -573,6 +579,7 @@ module LogBrew
573
579
  event_id_prefix: DEFAULT_EVENT_ID_PREFIX,
574
580
  metadata: nil,
575
581
  timestamp_provider: nil,
582
+ include_exception_message: true,
576
583
  include_exception_backtrace: false,
577
584
  on_error: nil,
578
585
  raise_errors: false
@@ -586,23 +593,23 @@ module LogBrew
586
593
  @event_id_prefix = event_id_prefix
587
594
  @metadata = metadata || {}
588
595
  @timestamp_provider = timestamp_provider
596
+ @include_exception_message = include_exception_message
589
597
  @include_exception_backtrace = include_exception_backtrace
590
598
  @on_error = on_error
591
599
  @raise_errors = raise_errors
592
600
  @next_event_number = 0
601
+ @event_id_mutex = Mutex.new
593
602
  end
594
603
 
595
604
  def report(error, handled: true, severity: :error, context: nil, source: nil, **_options)
596
605
  capture_safely do
597
- @next_event_number += 1
598
- @client.issue(
599
- "#{@event_id_prefix}_#{@next_event_number}",
600
- logbrew_timestamp,
606
+ attributes = {
601
607
  title: error_title(error),
602
608
  level: issue_level(severity),
603
- message: error_message(error),
604
609
  metadata: rails_metadata(error, handled, severity, context, source)
605
- )
610
+ }
611
+ attributes[:message] = error_message(error) if @include_exception_message
612
+ @client.issue(next_event_id, logbrew_timestamp, attributes)
606
613
  flush_if_configured
607
614
  end
608
615
  end
@@ -629,6 +636,13 @@ module LogBrew
629
636
  error.inspect
630
637
  end
631
638
 
639
+ def next_event_id
640
+ @event_id_mutex.synchronize do
641
+ @next_event_number += 1
642
+ "#{@event_id_prefix}_#{@next_event_number}"
643
+ end
644
+ end
645
+
632
646
  def issue_level(severity)
633
647
  SEVERITY_TO_ISSUE_LEVEL.fetch(severity.to_s, "error")
634
648
  end
@@ -655,7 +669,7 @@ module LogBrew
655
669
 
656
670
  def add_exception_metadata(metadata, exception)
657
671
  metadata["exceptionType"] = exception.class.name
658
- metadata["exceptionMessage"] = exception.message
672
+ metadata["exceptionMessage"] = exception.message if @include_exception_message
659
673
  metadata["exceptionBacktrace"] = exception.backtrace.join("\n") if @include_exception_backtrace && exception.backtrace
660
674
  end
661
675
 
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logbrew-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogBrew
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-23 00:00:00.000000000 Z
11
+ date: 2026-08-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Public LogBrew Ruby SDK for building, validating, and flushing event
14
- batches.
13
+ description: Public LogBrew Ruby SDK with automatic Rails request/error capture and
14
+ standard-library delivery.
15
15
  email:
16
16
  executables: []
17
17
  extensions: []
@@ -26,6 +26,7 @@ files:
26
26
  - examples/readme_example.rb
27
27
  - examples/real_user_smoke.rb
28
28
  - examples/sidekiq_tracing.rb
29
+ - lib/logbrew-sdk.rb
29
30
  - lib/logbrew.rb
30
31
  - lib/logbrew/automatic_delivery.rb
31
32
  - lib/logbrew/bounded_event_queue.rb
@@ -35,11 +36,14 @@ files:
35
36
  - lib/logbrew/operation_tracing.rb
36
37
  - lib/logbrew/persistent_event_store.rb
37
38
  - lib/logbrew/product_timeline.rb
39
+ - lib/logbrew/rails.rb
40
+ - lib/logbrew/rails_integration.rb
38
41
  - lib/logbrew/sidekiq.rb
39
42
  - lib/logbrew/span_events.rb
40
43
  - lib/logbrew/support_ticket.rb
41
44
  - lib/logbrew/trace.rb
42
45
  - lib/logbrew/traceparent.rb
46
+ - lib/logbrew/version.rb
43
47
  - lib/logbrew/worker_lifecycle.rb
44
48
  homepage: https://github.com/LogBrewCo/sdk
45
49
  licenses: