logbrew-sdk 0.1.10 → 0.1.12
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 +12 -2
- data/lib/logbrew/http_client_tracing.rb +2 -1
- data/lib/logbrew/rails.rb +1 -0
- data/lib/logbrew/rails_integration.rb +157 -37
- data/lib/logbrew/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0af4b97ef8e2b7c4e89c4c7541e1a512d85f4d8aa6e3d87639551145f99875c
|
|
4
|
+
data.tar.gz: eaa3d1ff11b605c3547964918cfb37714e8ea737c72748f02d08ce2b27fed221
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc95d14c11974aa1c83adefa37beb10864029e9d6c5e518c9dccd99ad1901974aa6fa3885a186ad05a00ae23e12642e61f578bec7f1a78d7629dcbf0e4d022dc
|
|
7
|
+
data.tar.gz: 2154ac935379e496a6d8888058ee22964ad7cda7449ed0af86120f5099656a52f7be54042117f650e66259c848e435df8150f70f3f38ff07e856e6f1cafd3fbc
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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 automatic Rails request, database, cache, view, error, ActiveJob, and outbound `Net::HTTP` capture; standard-library delivery; opt-in standard-library `Logger` support; and manual Rack helpers.
|
|
7
|
+
Public Ruby SDK for building, validating, previewing, and flushing LogBrew event batches, with automatic Rails request, database, cache, view, error, ActiveJob, and outbound `Net::HTTP` capture; trace-correlated Rails application logs by explicit opt-in; standard-library delivery; opt-in standard-library `Logger` support; and manual Rack helpers.
|
|
8
8
|
|
|
9
9
|
The core package has no runtime gem dependencies. Its automatic integration
|
|
10
10
|
activates only inside an application that has already loaded Rails.
|
|
@@ -23,7 +23,7 @@ needed:
|
|
|
23
23
|
|
|
24
24
|
```ruby
|
|
25
25
|
# Gemfile
|
|
26
|
-
gem "logbrew-sdk", "~> 0.1.
|
|
26
|
+
gem "logbrew-sdk", "~> 0.1.12"
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
```bash
|
|
@@ -70,6 +70,15 @@ need no initializer. They propagate one W3C child and record only method,
|
|
|
70
70
|
normalized host, status, duration, adapter source, sampled state, and exception
|
|
71
71
|
type. Calls outside an active trace remain exact pass-throughs.
|
|
72
72
|
|
|
73
|
+
Set `LOGBREW_CAPTURE_RAILS_LOGS=true` to add application-owned `Rails.logger`
|
|
74
|
+
messages to the active request or ActiveJob trace. The adapter keeps the app's
|
|
75
|
+
configured logger, output, level, return value, and lazy block behavior intact.
|
|
76
|
+
It captures only calls originating from files below `Rails.root`; framework,
|
|
77
|
+
gem, startup, and out-of-trace logs remain local. Messages are limited to 2,048
|
|
78
|
+
characters and include `messageState: captured` or `messageState: truncated`.
|
|
79
|
+
Log messages are application-provided telemetry and can contain sensitive
|
|
80
|
+
values, so enable this only after the application applies its normal filtering.
|
|
81
|
+
|
|
73
82
|
The explicit Rails capture settings are:
|
|
74
83
|
|
|
75
84
|
| Environment variable | Default | Purpose |
|
|
@@ -84,6 +93,7 @@ The explicit Rails capture settings are:
|
|
|
84
93
|
| `LOGBREW_FLUSH_INTERVAL_MS` | `5000` | Automatic delivery interval from 10 to 3600000 ms |
|
|
85
94
|
| `LOGBREW_FLUSH_THRESHOLD` | `100` | Queue size from 1 to 1000 that requests an earlier flush |
|
|
86
95
|
| `LOGBREW_CAPTURE_EXCEPTION_MESSAGES` | `false` | Opt in to exception message capture |
|
|
96
|
+
| `LOGBREW_CAPTURE_RAILS_LOGS` | `false` | Opt in to bounded application-origin logs on active request and job traces |
|
|
87
97
|
| `LOGBREW_INCLUDE_EXCEPTION_BACKTRACE` | `false` | Opt in to raw exception backtrace text; sanitized structured frames are always captured |
|
|
88
98
|
|
|
89
99
|
`LOGBREW_API_KEY` and `LOGBREW_INGEST_KEY` are not Rails aliases. If either is
|
|
@@ -19,6 +19,7 @@ module LogBrew
|
|
|
19
19
|
@method = HttpClientTracing.normalize_method(method)
|
|
20
20
|
@host = HttpClientTracing.normalize_host(host)
|
|
21
21
|
@on_capture_error = on_capture_error
|
|
22
|
+
@timestamp = Time.now.utc.iso8601(6)
|
|
22
23
|
@started_at = HttpClientTracing.monotonic_time
|
|
23
24
|
@mutex = Mutex.new
|
|
24
25
|
@finished = false
|
|
@@ -60,7 +61,7 @@ module LogBrew
|
|
|
60
61
|
|
|
61
62
|
@client.span(
|
|
62
63
|
"ruby_http_span_#{@context.span_id}",
|
|
63
|
-
|
|
64
|
+
@timestamp,
|
|
64
65
|
{
|
|
65
66
|
name: "http.client:#{@method}",
|
|
66
67
|
traceId: @context.trace_id,
|
data/lib/logbrew/rails.rb
CHANGED
|
@@ -98,6 +98,7 @@ module LogBrew
|
|
|
98
98
|
runtime = LogBrew::Rails.runtime
|
|
99
99
|
next if runtime.nil? || !runtime.configuration.enabled?
|
|
100
100
|
|
|
101
|
+
LogBrew::Rails.const_get(:ApplicationLogCapture).install(::Rails.logger, runtime, application_root: ::Rails.root)
|
|
101
102
|
reporter = LogBrew::Rails::ErrorReporter.new(runtime)
|
|
102
103
|
if ::Rails.respond_to?(:error) && ::Rails.error.respond_to?(:subscribe)
|
|
103
104
|
::Rails.error.subscribe(reporter)
|
|
@@ -18,17 +18,13 @@ module LogBrew
|
|
|
18
18
|
MAX_ENDPOINT_BYTES = 2_048
|
|
19
19
|
LOOPBACK_HOSTS = %w[localhost 127.0.0.1 ::1 [::1]].freeze
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
:request_timeout,
|
|
29
|
-
:flush_interval,
|
|
30
|
-
:flush_threshold
|
|
31
|
-
)
|
|
21
|
+
ATTRIBUTES = %i[
|
|
22
|
+
enabled api_key service_name app_environment rails_version release endpoint
|
|
23
|
+
request_timeout flush_interval flush_threshold capture_exception_messages
|
|
24
|
+
capture_rails_logs include_exception_backtrace
|
|
25
|
+
].freeze
|
|
26
|
+
private_constant :ATTRIBUTES
|
|
27
|
+
attr_reader(*ATTRIBUTES.drop(1).reject { |name| name.to_s.start_with?("capture_", "include_") })
|
|
32
28
|
|
|
33
29
|
def self.from_environment(environment, application_name:, rails_environment:, rails_version:)
|
|
34
30
|
unless environment.respond_to?(:[]) && environment.respond_to?(:key?)
|
|
@@ -92,6 +88,7 @@ module LogBrew
|
|
|
92
88
|
"LOGBREW_CAPTURE_EXCEPTION_MESSAGES",
|
|
93
89
|
false
|
|
94
90
|
),
|
|
91
|
+
capture_rails_logs: boolean_value(environment, "LOGBREW_CAPTURE_RAILS_LOGS", false),
|
|
95
92
|
include_exception_backtrace: boolean_value(
|
|
96
93
|
environment,
|
|
97
94
|
"LOGBREW_INCLUDE_EXCEPTION_BACKTRACE",
|
|
@@ -113,6 +110,7 @@ module LogBrew
|
|
|
113
110
|
flush_interval: DEFAULT_FLUSH_INTERVAL_MS / 1_000.0,
|
|
114
111
|
flush_threshold: DEFAULT_FLUSH_THRESHOLD,
|
|
115
112
|
capture_exception_messages: false,
|
|
113
|
+
capture_rails_logs: false,
|
|
116
114
|
include_exception_backtrace: false
|
|
117
115
|
)
|
|
118
116
|
end
|
|
@@ -203,32 +201,11 @@ module LogBrew
|
|
|
203
201
|
private_class_method :optional_text, :server_key_value, :bounded_label,
|
|
204
202
|
:optional_bounded_label, :boolean_value, :integer_value, :endpoint_value
|
|
205
203
|
|
|
206
|
-
def initialize(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
rails_version:,
|
|
212
|
-
release:,
|
|
213
|
-
endpoint:,
|
|
214
|
-
request_timeout:,
|
|
215
|
-
flush_interval:,
|
|
216
|
-
flush_threshold:,
|
|
217
|
-
capture_exception_messages:,
|
|
218
|
-
include_exception_backtrace:
|
|
219
|
-
)
|
|
220
|
-
@enabled = enabled
|
|
221
|
-
@api_key = api_key&.dup&.freeze
|
|
222
|
-
@service_name = service_name
|
|
223
|
-
@app_environment = app_environment
|
|
224
|
-
@rails_version = rails_version
|
|
225
|
-
@release = release
|
|
226
|
-
@endpoint = endpoint
|
|
227
|
-
@request_timeout = request_timeout
|
|
228
|
-
@flush_interval = flush_interval
|
|
229
|
-
@flush_threshold = flush_threshold
|
|
230
|
-
@capture_exception_messages = capture_exception_messages
|
|
231
|
-
@include_exception_backtrace = include_exception_backtrace
|
|
204
|
+
def initialize(**attributes)
|
|
205
|
+
raise ArgumentError, "Rails configuration attributes do not match" unless attributes.keys.sort == ATTRIBUTES.sort
|
|
206
|
+
|
|
207
|
+
ATTRIBUTES.each { |name| instance_variable_set("@#{name}", attributes.fetch(name)) }
|
|
208
|
+
@api_key = @api_key&.dup&.freeze
|
|
232
209
|
freeze
|
|
233
210
|
end
|
|
234
211
|
|
|
@@ -240,6 +217,10 @@ module LogBrew
|
|
|
240
217
|
@capture_exception_messages
|
|
241
218
|
end
|
|
242
219
|
|
|
220
|
+
def capture_rails_logs?
|
|
221
|
+
@capture_rails_logs
|
|
222
|
+
end
|
|
223
|
+
|
|
243
224
|
def include_exception_backtrace?
|
|
244
225
|
@include_exception_backtrace
|
|
245
226
|
end
|
|
@@ -398,6 +379,145 @@ module LogBrew
|
|
|
398
379
|
end
|
|
399
380
|
end
|
|
400
381
|
|
|
382
|
+
# Adds one bounded, trace-aware LogBrew sink without replacing the app logger.
|
|
383
|
+
module ApplicationLogCapture
|
|
384
|
+
MAX_MESSAGE_CHARACTERS = 2_048
|
|
385
|
+
SINK_IVAR = :@logbrew_rails_log_sink
|
|
386
|
+
ROOT_IVAR = :@logbrew_rails_application_root
|
|
387
|
+
GUARD_KEY = :logbrew_rails_log_capture
|
|
388
|
+
ORIGIN_KEY = :logbrew_rails_log_origin
|
|
389
|
+
LIBRARY_ROOT = File.expand_path("..", __dir__)
|
|
390
|
+
LOGGER_PATH = ::Logger.instance_method(:add).source_location&.first
|
|
391
|
+
LOGGER_WRAPPER_PATH = %r{/active_support/(?:broadcast_logger|tagged_logging|logger(?:_silence|_thread_safe_level)?)\.rb\z}
|
|
392
|
+
GEM_ROOTS = (defined?(::Gem) ? ::Gem.path : []).map { |root| "#{File.expand_path(root)}#{File::SEPARATOR}" }.freeze
|
|
393
|
+
LEVELS = LogBrew::Logger::SEVERITY_TO_LOGBREW_LEVEL
|
|
394
|
+
private_constant :MAX_MESSAGE_CHARACTERS, :SINK_IVAR, :ROOT_IVAR, :GUARD_KEY, :ORIGIN_KEY,
|
|
395
|
+
:LIBRARY_ROOT, :LOGGER_PATH, :LOGGER_WRAPPER_PATH, :GEM_ROOTS, :LEVELS
|
|
396
|
+
|
|
397
|
+
class Sink < ::Logger
|
|
398
|
+
def initialize(runtime, application_root, level)
|
|
399
|
+
@runtime = runtime
|
|
400
|
+
@application_root = File.expand_path(application_root.to_s)
|
|
401
|
+
super(File::NULL)
|
|
402
|
+
self.level = level
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def add(severity, message = nil, progname = nil)
|
|
406
|
+
severity ||= ::Logger::UNKNOWN
|
|
407
|
+
return true if severity < level
|
|
408
|
+
|
|
409
|
+
resolved = message.nil? ? (block_given? ? yield : progname) : message
|
|
410
|
+
ApplicationLogCapture.capture(@runtime, @application_root, severity, resolved)
|
|
411
|
+
true
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
private_constant :Sink
|
|
415
|
+
|
|
416
|
+
module LoggerTap
|
|
417
|
+
def add(severity, message = nil, progname = nil, &block)
|
|
418
|
+
severity ||= ::Logger::UNKNOWN
|
|
419
|
+
configured_progname = respond_to?(:progname) ? self.progname : nil
|
|
420
|
+
captured = message.nil? && block.nil? ? (progname || configured_progname) : message
|
|
421
|
+
wrapped = block && proc { captured = block.call }
|
|
422
|
+
result = super(severity, message, progname, &wrapped)
|
|
423
|
+
sink = instance_variable_get(SINK_IVAR)
|
|
424
|
+
if sink && severity >= level
|
|
425
|
+
sink.level = level
|
|
426
|
+
sink.add(severity, captured)
|
|
427
|
+
end
|
|
428
|
+
result
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
private_constant :LoggerTap
|
|
432
|
+
|
|
433
|
+
module BroadcastOrigin
|
|
434
|
+
def dispatch(...)
|
|
435
|
+
previous = Thread.current[ORIGIN_KEY]
|
|
436
|
+
origin = ApplicationLogCapture.application_caller?(instance_variable_get(ROOT_IVAR))
|
|
437
|
+
Thread.current[ORIGIN_KEY] = origin
|
|
438
|
+
super
|
|
439
|
+
ensure
|
|
440
|
+
Thread.current[ORIGIN_KEY] = previous
|
|
441
|
+
end
|
|
442
|
+
private :dispatch
|
|
443
|
+
end
|
|
444
|
+
private_constant :BroadcastOrigin
|
|
445
|
+
|
|
446
|
+
module_function
|
|
447
|
+
|
|
448
|
+
def install(logger, runtime, application_root: nil)
|
|
449
|
+
return logger unless runtime.configuration.capture_rails_logs?
|
|
450
|
+
return logger if application_root.nil?
|
|
451
|
+
return logger unless logger.respond_to?(:add) || logger.respond_to?(:broadcast_to)
|
|
452
|
+
return logger unless logger.instance_variable_get(SINK_IVAR).nil?
|
|
453
|
+
|
|
454
|
+
level = logger.respond_to?(:level) ? logger.level : ::Logger::DEBUG
|
|
455
|
+
root = File.expand_path(application_root.to_s)
|
|
456
|
+
sink = Sink.new(runtime, root, level)
|
|
457
|
+
if logger.respond_to?(:broadcast_to)
|
|
458
|
+
logger.instance_variable_set(ROOT_IVAR, root)
|
|
459
|
+
logger.singleton_class.prepend(BroadcastOrigin)
|
|
460
|
+
logger.broadcast_to(sink)
|
|
461
|
+
else
|
|
462
|
+
logger.singleton_class.prepend(LoggerTap)
|
|
463
|
+
end
|
|
464
|
+
logger.instance_variable_set(SINK_IVAR, sink)
|
|
465
|
+
logger
|
|
466
|
+
rescue StandardError => error
|
|
467
|
+
runtime.report_error("application_log_installation", error)
|
|
468
|
+
logger
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def capture(runtime, application_root, severity, message)
|
|
472
|
+
return if LogBrew::Trace.current.nil? || Thread.current[GUARD_KEY]
|
|
473
|
+
origin = Thread.current[ORIGIN_KEY]
|
|
474
|
+
origin = application_caller?(application_root) if origin.nil?
|
|
475
|
+
return unless origin
|
|
476
|
+
|
|
477
|
+
Thread.current[GUARD_KEY] = true
|
|
478
|
+
begin
|
|
479
|
+
client = runtime.client
|
|
480
|
+
return if client.nil?
|
|
481
|
+
|
|
482
|
+
text = message.is_a?(String) ? message : message.inspect
|
|
483
|
+
truncated = text.length > MAX_MESSAGE_CHARACTERS
|
|
484
|
+
metadata = runtime.metadata.merge(
|
|
485
|
+
"source" => "rails.logger",
|
|
486
|
+
"rubySeverity" => ::Logger::SEV_LABEL[severity.to_i] || "ANY",
|
|
487
|
+
"messageState" => truncated ? "truncated" : "captured"
|
|
488
|
+
)
|
|
489
|
+
client.log(
|
|
490
|
+
"ruby_rails_log_#{SecureRandom.hex(8)}",
|
|
491
|
+
Time.now.utc.iso8601(6),
|
|
492
|
+
message: text[0, MAX_MESSAGE_CHARACTERS],
|
|
493
|
+
level: LEVELS.fetch(severity.to_i, severity.to_i >= ::Logger::FATAL ? "critical" : "info"),
|
|
494
|
+
logger: "rails",
|
|
495
|
+
metadata: metadata
|
|
496
|
+
)
|
|
497
|
+
rescue StandardError => error
|
|
498
|
+
runtime.report_error("application_log_capture", error)
|
|
499
|
+
ensure
|
|
500
|
+
Thread.current[GUARD_KEY] = false
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def application_caller?(application_root)
|
|
505
|
+
location = caller_locations(2, 24).find do |caller|
|
|
506
|
+
path = caller.absolute_path || caller.path
|
|
507
|
+
path && !path.start_with?(LIBRARY_ROOT) && path != LOGGER_PATH && !path.match?(LOGGER_WRAPPER_PATH)
|
|
508
|
+
end
|
|
509
|
+
return false if location.nil?
|
|
510
|
+
|
|
511
|
+
path = File.expand_path(location.absolute_path || location.path)
|
|
512
|
+
return false if GEM_ROOTS.any? { |root| path.start_with?(root) }
|
|
513
|
+
|
|
514
|
+
path == application_root || path.start_with?("#{application_root}#{File::SEPARATOR}")
|
|
515
|
+
rescue StandardError
|
|
516
|
+
false
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
private_constant :ApplicationLogCapture
|
|
520
|
+
|
|
401
521
|
# Adds bounded child spans to Net::HTTP calls made inside an active Rails trace.
|
|
402
522
|
module OutboundHttp
|
|
403
523
|
module Request
|
data/lib/logbrew/version.rb
CHANGED