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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ed4c09b597bf3b6c6d2a9426c378c1ef86c3ad32b141cdbf8782d0477ba41d6
4
- data.tar.gz: aae266ff513452aa9adc3e48d53ff117aa9ea80bbd6d5dd3ed35da05db2cbb9c
3
+ metadata.gz: a0af4b97ef8e2b7c4e89c4c7541e1a512d85f4d8aa6e3d87639551145f99875c
4
+ data.tar.gz: eaa3d1ff11b605c3547964918cfb37714e8ea737c72748f02d08ce2b27fed221
5
5
  SHA512:
6
- metadata.gz: 8ba3ea392a5a0f56cd7ab4f2c7a23271424149b9fcf8eaa0527b7002403069f9368f08729531b70f4e1c98390a261f544f61f540f95b1f65bfcb0e165ad5df1f
7
- data.tar.gz: e0d0b0c6f19fd4c14b1ade31e6e23f31f80928e2faeeb34875fc65ad15e1784a6fe5830e59b958f8650b09cc64965f00235e5ea83f35407a828b00f7a93d3798
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.10"
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
- Time.now.utc.iso8601,
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
- attr_reader(
22
- :api_key,
23
- :service_name,
24
- :app_environment,
25
- :rails_version,
26
- :release,
27
- :endpoint,
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
- enabled:,
208
- api_key:,
209
- service_name:,
210
- app_environment:,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBrew
4
- VERSION = "0.1.10"
4
+ VERSION = "0.1.12"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logbrew-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogBrew