logbrew-sdk 0.1.11 → 0.1.13

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: e4d8cebbc9fcb274134dda84b6de6b086a4dc6cdc119b80f02c3458624ca1587
4
- data.tar.gz: c35a55684af97f05ef24f3626412f471c12af7cef0bcc7468430393dfbfccfdd
3
+ metadata.gz: 9f4065e8e726690a78d3b682ba49d4c35d931e4e81619ea2ff11a4170f4572cc
4
+ data.tar.gz: 29252ab423d8a184ed96724a76d57aafca1fd4008c383df86f44721eb6b511cf
5
5
  SHA512:
6
- metadata.gz: e9f385c563b7de3bd38294d83e5aa9b3e72c57efb21cf6b22c59de8c5c0328d3a117dbbbee2bf42a91a36d98d97ece63afb2997661968a902f525c932351e5de
7
- data.tar.gz: ae840cec0095218353192f4af76c00b4ec80707e4bc208c03ba6ae654e2c98158c2af8c198d68e1d279a84df944382e654312a599d7311cd8ad4a6eb519e8d8a
6
+ metadata.gz: 972aaa22ee930144da4508ce6a80e3e149a6b1cdad744178e3c5056349c43974b7681769d1b2338ce318547de5fefc6122d6c9066481858698e3d54a954603a4
7
+ data.tar.gz: 0c7acd8d564f9bda21ce4b44d541e6b4d3417eb3afd46adb57487ed6802a72953ecd19449a0c8f8546f01bc7315c63731e949e87376091737e89beb58d2b082a
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.11"
26
+ gem "logbrew-sdk", "~> 0.1.13"
27
27
  ```
28
28
 
29
29
  ```bash
@@ -57,19 +57,29 @@ paths. Exception messages and raw backtrace text are separate opt-ins.
57
57
 
58
58
  ActiveJob needs no extra initializer. Each enqueue and execution emits a
59
59
  producer or worker span, including bounded adapter identity, retry count, and
60
- queue wait. A versioned W3C carrier keeps retries and supported queue adapters
61
- on one trace. A retryable failure remains span evidence; only an unexpected
62
- terminal `StandardError` creates an unhandled `rails.active_job` issue with
63
- sanitized structured frames. The adapter does not capture job IDs, queue names,
64
- arguments, serialized payloads, exception messages, or raw backtraces by
65
- default. When ActiveJob uses LogBrew's Sidekiq middleware, the inner carrier
66
- suppresses duplicate Sidekiq spans and issues.
60
+ queue wait. Enqueue spans use `queue.publish`; worker spans use `queue.process`.
61
+ A versioned W3C carrier keeps retries and supported queue adapters on one trace.
62
+ A retryable failure remains span evidence; only an unexpected terminal
63
+ `StandardError` creates an unhandled `rails.active_job` issue with sanitized
64
+ structured frames. The adapter does not capture job IDs, queue names, arguments,
65
+ serialized payloads, exception messages, or raw backtraces by default. When
66
+ ActiveJob uses LogBrew's Sidekiq middleware, the inner carrier suppresses
67
+ duplicate Sidekiq spans and issues.
67
68
 
68
69
  Outbound `Net::HTTP` calls made inside an active Rails request or job trace also
69
70
  need no initializer. They propagate one W3C child and record only method,
70
71
  normalized host, status, duration, adapter source, sampled state, and exception
71
72
  type. Calls outside an active trace remain exact pass-throughs.
72
73
 
74
+ Set `LOGBREW_CAPTURE_RAILS_LOGS=true` to add application-owned `Rails.logger`
75
+ messages to the active request or ActiveJob trace. The adapter keeps the app's
76
+ configured logger, output, level, return value, and lazy block behavior intact.
77
+ It captures only calls originating from files below `Rails.root`; framework,
78
+ gem, startup, and out-of-trace logs remain local. Messages are limited to 2,048
79
+ characters and include `messageState: captured` or `messageState: truncated`.
80
+ Log messages are application-provided telemetry and can contain sensitive
81
+ values, so enable this only after the application applies its normal filtering.
82
+
73
83
  The explicit Rails capture settings are:
74
84
 
75
85
  | Environment variable | Default | Purpose |
@@ -84,6 +94,7 @@ The explicit Rails capture settings are:
84
94
  | `LOGBREW_FLUSH_INTERVAL_MS` | `5000` | Automatic delivery interval from 10 to 3600000 ms |
85
95
  | `LOGBREW_FLUSH_THRESHOLD` | `100` | Queue size from 1 to 1000 that requests an earlier flush |
86
96
  | `LOGBREW_CAPTURE_EXCEPTION_MESSAGES` | `false` | Opt in to exception message capture |
97
+ | `LOGBREW_CAPTURE_RAILS_LOGS` | `false` | Opt in to bounded application-origin logs on active request and job traces |
87
98
  | `LOGBREW_INCLUDE_EXCEPTION_BACKTRACE` | `false` | Opt in to raw exception backtrace text; sanitized structured frames are always captured |
88
99
 
89
100
  `LOGBREW_API_KEY` and `LOGBREW_INGEST_KEY` are not Rails aliases. If either is
@@ -735,7 +746,14 @@ Registration is app-owned, idempotent, and reversible with `unregister_client` a
735
746
 
736
747
  The client middleware adds one bounded `logbrew` carrier containing only a version, W3C `traceparent`, and enqueue time. Valid retries keep that carrier without creating another enqueue span. The server middleware continues a valid carrier or starts a fresh trace when it is absent or malformed, returns the caller trace state after every result, and records bounded queue-wait and execution timing. Set `max_retries` to the same default retry limit used by your Sidekiq configuration; per-job integer or disabled retry settings are honored. Retryable failures keep only error spans, while the terminal escaped failure adds one deduplicated fixed-title issue and preserves the original exception.
737
748
 
738
- Sidekiq spans contain only fixed source, sampled state, bounded retry count, bounded queue-wait duration, execution duration, status, and real cancellation. The integration does not capture job arguments, payload fields, job identifiers, worker names, queue values, connection data, exception messages or stacks, baggage, or tracestate. Capture failures are advisory and never replace job execution or retry behavior. Create fresh clients and instrumentation after `fork`; inherited instances fail closed without changing jobs.
749
+ Sidekiq producer spans use `queue.publish`, and worker spans use `queue.process`.
750
+ They contain only fixed source, sampled state, bounded retry count, bounded
751
+ queue-wait duration, execution duration, status, and real cancellation. The
752
+ integration does not capture job arguments, payload fields, job identifiers,
753
+ worker names, queue values, connection data, exception messages or stacks,
754
+ baggage, or tracestate. Capture failures are advisory and never replace job
755
+ execution or retry behavior. Create fresh clients and instrumentation after
756
+ `fork`; inherited instances fail closed without changing jobs.
739
757
 
740
758
  ## Persistent Worker Delivery
741
759
 
@@ -6,7 +6,11 @@ module LogBrew
6
6
  UNSAFE_METADATA = /authorization|body|broker|cache.?key|command|connection|cookie|dsn|header|host|
7
7
  \bjid\z|job.?id|key|message|param|pass#{'word'}|payload|query|sec#{'ret'}|sql|statement|
8
8
  to#{'ken'}|url|username|value/ix
9
- private_constant :UNSAFE_METADATA
9
+ QUEUE_SEMANTICS = {
10
+ "enqueue" => "queue.publish", "publish" => "queue.publish", "send" => "queue.publish",
11
+ "receive" => "queue.receive", "perform" => "queue.process", "process" => "queue.process", "consume" => "queue.process"
12
+ }.freeze
13
+ private_constant :UNSAFE_METADATA, :QUEUE_SEMANTICS
10
14
 
11
15
  module_function
12
16
 
@@ -28,20 +32,13 @@ module LogBrew
28
32
  Validation.require_non_empty("#{kind} operation name", name)
29
33
  started_at = monotonic_time
30
34
  context = child_context
31
- error = nil
32
- result = nil
33
-
34
- Trace.with_context(context) do
35
- begin
36
- result = yield context
37
- rescue StandardError => captured
38
- error = captured
39
- end
35
+ begin
36
+ result = Trace.with_context(context) { yield context }
37
+ rescue StandardError => error
38
+ capture_span(client, kind, name, context, started_at, options, error)
39
+ raise
40
40
  end
41
-
42
- capture_span(client, kind, name, context, started_at, options, error)
43
- raise error if error
44
-
41
+ capture_span(client, kind, name, context, started_at, options, nil)
45
42
  result
46
43
  end
47
44
 
@@ -88,6 +85,7 @@ module LogBrew
88
85
  metadata["source"] = read_option(options, :source) || "#{kind}.operation"
89
86
  add_option(metadata, "#{kind}.system", read_option(options, :system))
90
87
  add_option(metadata, "#{kind}.operation", read_option(options, :operation))
88
+ add_option(metadata, "operation", QUEUE_SEMANTICS[read_option(options, :operation)]) if kind == "queue"
91
89
  add_option(metadata, "#{kind}.target", read_option(options, :target))
92
90
  metadata["exceptionType"] = error.class.name if error
93
91
  end
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(*args, &block)
435
+ previous = Thread.current[ORIGIN_KEY]
436
+ origin = ApplicationLogCapture.application_caller?(instance_variable_get(ROOT_IVAR))
437
+ Thread.current[ORIGIN_KEY] = origin
438
+ super(*args, &block)
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
@@ -28,15 +28,12 @@ module LogBrew
28
28
 
29
29
  def around(terminal_failure: false)
30
30
  Trace.with_context(@context) do
31
- begin
32
- result = yield
33
- rescue Exception => error # rubocop:disable Lint/RescueException
34
- finish(error: error, terminal_failure: terminal_failure)
35
- raise
36
- else
37
- finish
38
- result
39
- end
31
+ result = yield
32
+ finish
33
+ result
34
+ rescue Exception => error # rubocop:disable Lint/RescueException
35
+ finish(error: error, terminal_failure: terminal_failure)
36
+ raise
40
37
  end
41
38
  end
42
39
 
@@ -64,6 +61,7 @@ module LogBrew
64
61
  def span_attributes(error)
65
62
  metadata = {
66
63
  "source" => @source,
64
+ "operation" => @source == "sidekiq.client" ? "queue.publish" : "queue.process",
67
65
  "sampled" => @context.sampled
68
66
  }
69
67
  metadata["retryCount"] = @retry_count unless @retry_count.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBrew
4
- VERSION = "0.1.11"
4
+ VERSION = "0.1.13"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logbrew-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogBrew
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-17 00:00:00.000000000 Z
11
+ date: 2026-09-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Public LogBrew Ruby SDK with typed issue diagnostics, automatic Rails
14
14
  request/error capture, and standard-library delivery.