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 +4 -4
- data/README.md +128 -16
- data/examples/automatic_delivery.rb +1 -1
- data/examples/sidekiq_tracing.rb +1 -1
- data/lib/logbrew/rails.rb +105 -0
- data/lib/logbrew/rails_integration.rb +622 -0
- data/lib/logbrew/version.rb +5 -0
- data/lib/logbrew-sdk.rb +4 -0
- data/lib/logbrew.rb +29 -15
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 233087ef98eb35d7df84720956a89dfbf66368ac55269900a38491192c7a67a3
|
|
4
|
+
data.tar.gz: 1720b83bf8dcc562771936091f0196996415005e6a5ea254ca4e0a803e3ea1ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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("
|
|
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("
|
|
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("
|
|
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: "
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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("
|
|
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,
|
data/examples/sidekiq_tracing.rb
CHANGED
|
@@ -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("
|
|
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
|
data/lib/logbrew-sdk.rb
ADDED
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
|
-
|
|
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
|
-
@
|
|
443
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-08-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: Public LogBrew Ruby SDK
|
|
14
|
-
|
|
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:
|