logbrew-sdk 0.1.5 → 0.1.7

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: bb21d5bb9dcb7781c910e2b7ba05b27c15a6c43d6d8d309dbd9349646530a9e3
4
- data.tar.gz: 01b26764ad7ff0b40e75649ae1481a6697e4b2811bf5ac10a72df7b7fb2b2eaa
3
+ metadata.gz: f086729efb0a0a98821ef3bce8d96b71066b202beff5656917d958e4ca119479
4
+ data.tar.gz: 7c5eafb18cda7d001dc8c72c9646852e857a46bd4e06508abeda5a23365489a1
5
5
  SHA512:
6
- metadata.gz: dcaa0c49f1305d004f66605e07e9bc552f02dab4feca7a18fff90f34d02899b58661c1b5f786b75719f2368fe2dcd080cc2b3c7a95037b423bab52b2d3386f0d
7
- data.tar.gz: a4bc7c482321d08c040d2b815454e70382a45d73ade264025855a1f4a06fc9d0b48c69a540e21f2dd21e26379f320cc76ba80d98c7a207bbd0592e149d093a99
6
+ metadata.gz: 583a7cdc7ccc4ea1435a31454c0d90cea390a7b9f37e1322c3f39bc84bf0b2c731aaa08abff5384675871f62082fd8fc98c5c2c7284ad89a1fb6c9b1221982e0
7
+ data.tar.gz: 5093fb1d5ea5540302c6a99c8d6fa7ce1b66142a55010c7d867e1f0539d1fe57525270486c59e58acd687a68bb51e44aaa62f480a3c119681648d9a876d70fbb
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/error capture, standard-library `Net::HTTP` 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, and error capture; standard-library `Net::HTTP` 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.5"
26
+ gem "logbrew-sdk", "~> 0.1.7"
27
27
  ```
28
28
 
29
29
  ```bash
@@ -40,8 +40,14 @@ disabled and the application behaves normally. Set `LOGBREW_ENABLED=false` to
40
40
  disable it explicitly. If the Gemfile uses `require: false`, load
41
41
  `logbrew/rails` yourself after Rails.
42
42
 
43
- The automatic integration records route-template request spans and typed
44
- exception issues. Escaped request failures include exception identity,
43
+ The automatic integration records route-template request spans and up to eight
44
+ slowest request-local Active Record, Rails cache, and Action View child spans.
45
+ The request span reports exact observed and captured operation counts plus
46
+ truncation. Operation evidence contains only the operation type, duration,
47
+ cache-hit state, relative `app/views` template path, and exception type when
48
+ present. It never contains SQL, cache keys or values, or absolute paths.
49
+
50
+ Escaped request failures include exception identity,
45
51
  `rails.middleware` with `handled: false`, and up to 32 newest-first sanitized
46
52
  frames. Handled Rails reports use `rails.error_reporter` with `handled: true`.
47
53
  It does not record concrete request paths, query strings, request or response
@@ -3,34 +3,10 @@
3
3
  module LogBrew
4
4
  # Explicit dependency spans for app-owned database, cache, and queue work.
5
5
  module OperationTracing
6
- UNSAFE_METADATA_PATTERNS = [
7
- /authorization/i,
8
- /body/i,
9
- /broker/i,
10
- /cache.?key/i,
11
- /command/i,
12
- /connection/i,
13
- /cookie/i,
14
- /dsn/i,
15
- /header/i,
16
- /host/i,
17
- /\bjid\z/i,
18
- /job.?id/i,
19
- /key/i,
20
- /message/i,
21
- /param/i,
22
- /pass#{'word'}/i,
23
- /payload/i,
24
- /query/i,
25
- /sec#{'ret'}/i,
26
- /sql/i,
27
- /statement/i,
28
- /to#{'ken'}/i,
29
- /url/i,
30
- /username/i,
31
- /value/i
32
- ].freeze
33
- private_constant :UNSAFE_METADATA_PATTERNS
6
+ UNSAFE_METADATA = /authorization|body|broker|cache.?key|command|connection|cookie|dsn|header|host|
7
+ \bjid\z|job.?id|key|message|param|pass#{'word'}|payload|query|sec#{'ret'}|sql|statement|
8
+ to#{'ken'}|url|username|value/ix
9
+ private_constant :UNSAFE_METADATA
34
10
 
35
11
  module_function
36
12
 
@@ -70,20 +46,22 @@ module LogBrew
70
46
  end
71
47
 
72
48
  def capture_span(client, kind, name, context, started_at, options, error)
73
- client.span(
74
- event_id(kind, context, options),
75
- timestamp(options),
76
- {
77
- name: "#{kind}.operation:#{name}",
78
- traceId: context.trace_id,
79
- spanId: context.span_id,
80
- parentSpanId: context.parent_span_id,
81
- status: error ? "error" : "ok",
82
- durationMs: duration_ms(started_at, options),
83
- metadata: span_metadata(kind, options, error),
84
- events: span_events(error)
85
- }
86
- )
49
+ Trace.with_context(context) do
50
+ client.span(
51
+ event_id(kind, context, options),
52
+ timestamp(options),
53
+ {
54
+ name: "#{kind}.operation:#{name}",
55
+ traceId: context.trace_id,
56
+ spanId: context.span_id,
57
+ parentSpanId: context.parent_span_id,
58
+ status: error ? "error" : "ok",
59
+ durationMs: duration_ms(started_at, options),
60
+ metadata: span_metadata(kind, options, error),
61
+ events: span_events(error)
62
+ }
63
+ )
64
+ end
87
65
  rescue StandardError => capture_error
88
66
  on_error = read_option(options, :on_error)
89
67
  begin
@@ -99,7 +77,7 @@ module LogBrew
99
77
 
100
78
  Trace.create(
101
79
  trace_id: parent.trace_id,
102
- span_id: generate_span_id,
80
+ span_id: Trace.generate_span_id,
103
81
  parent_span_id: parent.span_id,
104
82
  trace_flags: parent.trace_flags
105
83
  )
@@ -107,7 +85,7 @@ module LogBrew
107
85
 
108
86
  def span_metadata(kind, options, error)
109
87
  sanitized_metadata(read_option(options, :metadata)).tap do |metadata|
110
- metadata["source"] = "#{kind}.operation"
88
+ metadata["source"] = read_option(options, :source) || "#{kind}.operation"
111
89
  add_option(metadata, "#{kind}.system", read_option(options, :system))
112
90
  add_option(metadata, "#{kind}.operation", read_option(options, :operation))
113
91
  add_option(metadata, "#{kind}.target", read_option(options, :target))
@@ -137,29 +115,22 @@ module LogBrew
137
115
  normalized_key = key.to_s
138
116
  next if normalized_key.strip.empty?
139
117
  next if unsafe_metadata_key?(normalized_key)
140
- next unless primitive_metadata_value?(value)
118
+ next unless CaptureHelpers.primitive_metadata_value?(value)
141
119
 
142
120
  copied[normalized_key] = value
143
121
  end
144
122
  end
145
123
 
146
124
  def unsafe_metadata_key?(key)
147
- UNSAFE_METADATA_PATTERNS.any? { |pattern| key.match?(pattern) }
148
- end
149
-
150
- def primitive_metadata_value?(value)
151
- return true if value.nil? || value == true || value == false
152
- return true if value.is_a?(String) || value.is_a?(Integer)
153
-
154
- value.is_a?(Float) && value.finite?
125
+ key.match?(UNSAFE_METADATA)
155
126
  end
156
127
 
157
128
  def add_option(metadata, key, value)
158
- metadata[key] = value if primitive_metadata_value?(value) && !(value.is_a?(String) && value.strip.empty?)
129
+ metadata[key] = value if CaptureHelpers.primitive_metadata_value?(value) && !(value.is_a?(String) && value.strip.empty?)
159
130
  end
160
131
 
161
132
  def read_option(options, key)
162
- options[key] || options[key.to_s]
133
+ options.key?(key) ? options[key] : options[key.to_s]
163
134
  end
164
135
 
165
136
  def event_id(kind, context, options)
@@ -184,11 +155,5 @@ module LogBrew
184
155
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
185
156
  end
186
157
 
187
- def generate_span_id
188
- loop do
189
- value = SecureRandom.hex(8)
190
- return value unless value.delete("0").empty?
191
- end
192
- end
193
158
  end
194
159
  end
data/lib/logbrew/rails.rb CHANGED
@@ -77,6 +77,9 @@ module LogBrew
77
77
  class RailsRailtie < ::Rails::Railtie
78
78
  initializer "logbrew.install", after: :load_config_initializers do |application|
79
79
  runtime = LogBrew::Rails.install(application: application)
80
+ if runtime.configuration.enabled?
81
+ LogBrew::Rails.const_get(:RequestOperations).install(::ActiveSupport::Notifications)
82
+ end
80
83
  middleware = application.config.middleware
81
84
  if defined?(::ActionDispatch::ShowExceptions)
82
85
  middleware.insert_after(
@@ -120,7 +120,6 @@ module LogBrew
120
120
  text = value.to_s.strip
121
121
  text.empty? ? nil : text
122
122
  end
123
- private_class_method :optional_text
124
123
 
125
124
  def self.server_key_value(value)
126
125
  text = optional_text(value)
@@ -131,7 +130,6 @@ module LogBrew
131
130
 
132
131
  text
133
132
  end
134
- private_class_method :server_key_value
135
133
 
136
134
  def self.bounded_label(value, label)
137
135
  text = optional_text(value)
@@ -142,7 +140,6 @@ module LogBrew
142
140
 
143
141
  text.freeze
144
142
  end
145
- private_class_method :bounded_label
146
143
 
147
144
  def self.optional_bounded_label(value, label)
148
145
  text = optional_text(value)
@@ -150,7 +147,6 @@ module LogBrew
150
147
 
151
148
  bounded_label(text, label)
152
149
  end
153
- private_class_method :optional_bounded_label
154
150
 
155
151
  def self.boolean_value(environment, name, default)
156
152
  value = optional_text(environment[name])
@@ -163,7 +159,6 @@ module LogBrew
163
159
  raise SdkError.new("configuration_error", "#{name} must be true or false")
164
160
  end
165
161
  end
166
- private_class_method :boolean_value
167
162
 
168
163
  def self.integer_value(environment, name, default, minimum, maximum)
169
164
  text = optional_text(environment[name])
@@ -179,7 +174,6 @@ module LogBrew
179
174
  "#{name} must be an integer between #{minimum} and #{maximum}"
180
175
  )
181
176
  end
182
- private_class_method :integer_value
183
177
 
184
178
  def self.endpoint_value(value)
185
179
  text = optional_text(value) || DEFAULT_ENDPOINT
@@ -203,7 +197,8 @@ module LogBrew
203
197
  rescue URI::InvalidURIError
204
198
  raise SdkError.new("configuration_error", "LOGBREW_ENDPOINT must be a valid HTTP URL")
205
199
  end
206
- private_class_method :endpoint_value
200
+ private_class_method :optional_text, :server_key_value, :bounded_label,
201
+ :optional_bounded_label, :boolean_value, :integer_value, :endpoint_value
207
202
 
208
203
  def initialize(
209
204
  enabled:,
@@ -249,6 +244,7 @@ module LogBrew
249
244
 
250
245
  # Owns one lazy automatic-delivery client per operating-system process.
251
246
  class Runtime
247
+ include CaptureHelpers
252
248
  attr_reader :configuration
253
249
 
254
250
  def initialize(
@@ -320,6 +316,16 @@ module LogBrew
320
316
  nil
321
317
  end
322
318
 
319
+ def metadata
320
+ @metadata ||= {
321
+ "service" => @configuration.service_name,
322
+ "environment" => @configuration.app_environment,
323
+ "framework" => "rails",
324
+ "framework.version" => @configuration.rails_version,
325
+ "release" => @configuration.release
326
+ }.compact.freeze
327
+ end
328
+
323
329
  private
324
330
 
325
331
  def prepare_process_state
@@ -372,7 +378,6 @@ module LogBrew
372
378
 
373
379
  def record_process_context(created)
374
380
  timestamp = logbrew_timestamp
375
- metadata = base_metadata
376
381
  created.environment(
377
382
  "ruby_rails_environment_#{SecureRandom.hex(8)}",
378
383
  timestamp,
@@ -388,29 +393,84 @@ module LogBrew
388
393
  metadata: metadata
389
394
  )
390
395
  end
396
+ end
391
397
 
392
- def base_metadata
393
- {
394
- "service" => @configuration.service_name,
395
- "environment" => @configuration.app_environment,
396
- "framework" => "rails",
397
- "framework.version" => @configuration.rails_version
398
- }
398
+ # Buffers only the slowest request-local framework operations without raw
399
+ # SQL, cache keys, absolute paths, or exception messages.
400
+ module RequestOperations
401
+ LIMIT = 8
402
+ STATE_KEY = :logbrew_rails_request_operations
403
+ EVENTS = /\A(?:(sql)\.active_record|(cache)_(read|write|delete|exist\?|fetch_hit|generate)\.active_support|(render)_(template|partial|collection)\.action_view)\z/
404
+ private_constant :LIMIT, :STATE_KEY, :EVENTS
405
+
406
+ module_function
407
+
408
+ def install(notifications)
409
+ @mutex ||= Mutex.new
410
+ @mutex.synchronize { @subscription ||= notifications.subscribe(EVENTS) { |*event| record(*event) } }
411
+ end
412
+
413
+ def within
414
+ previous = Thread.current[STATE_KEY]
415
+ Thread.current[STATE_KEY] = [0, []]
416
+ yield
417
+ ensure
418
+ Thread.current[STATE_KEY] = previous
419
+ end
420
+
421
+ def record(name, started_at, finished_at, _id = nil, payload = {})
422
+ state = Thread.current[STATE_KEY]
423
+ match = EVENTS.match(name.to_s)
424
+ return if state.nil? || match.nil? || !payload.is_a?(Hash)
425
+ duration_ms = ((finished_at - started_at) * 1_000).round(3)
426
+ return unless duration_ms.finite? && !duration_ms.negative?
427
+ kind, operation = match[1] ? %w[database query] : [match[2] ? "cache" : "view", match[3] || match[5]]
428
+ metadata = { "rails.notification" => name.to_s }
429
+ metadata["cache.hit"] = payload[:hit] if kind == "cache" && [true, false].include?(payload[:hit])
430
+ template = template_path(payload[:identifier]) if kind == "view"
431
+ metadata["view.template"] = template unless template.nil?
432
+ error = payload[:exception_object]
433
+ state[0] += 1
434
+ timestamp = started_at.iso8601(6) if started_at.respond_to?(:iso8601)
435
+ timestamp ||= Time.now.utc.iso8601(6)
436
+ state[1] << [duration_ms, kind, operation, metadata, error.is_a?(Exception) ? error : nil, timestamp, started_at]
437
+ state[1].sort_by! { |item| [item[0], item[1], item[2]] }
438
+ state[1].shift if state[1].length > LIMIT
439
+ rescue StandardError
440
+ nil
441
+ end
442
+
443
+ def snapshot
444
+ Thread.current[STATE_KEY] || [0, []]
399
445
  end
400
446
 
401
- def logbrew_timestamp
402
- timestamp = @timestamp_provider.call
403
- return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
447
+ def template_path(identifier)
448
+ text = identifier.to_s.tr("\\", "/")
449
+ relative = text.split("/app/views/", 2)[1]
450
+ return if relative.nil? || relative.empty? || relative.bytesize > 255 || relative.match?(/[[:cntrl:]]/) || relative.split("/").include?("..")
404
451
 
405
- timestamp.to_s
452
+ relative
406
453
  end
454
+ private_class_method :template_path
407
455
  end
456
+ private_constant :RequestOperations
408
457
 
409
458
  # Internal Rack adapter that replaces concrete request paths with Rails
410
459
  # route templates while reusing the core request/error lifecycle.
411
460
  class RailsRackMiddleware < LogBrew::RackMiddleware
412
461
  private
413
462
 
463
+ def capture_request_span(*arguments)
464
+ super.tap do
465
+ RequestOperations.snapshot[1].sort_by(&:last).each do |duration_ms, kind, name, metadata, error, timestamp|
466
+ LogBrew::OperationTracing.capture_span(
467
+ @client, kind, name, LogBrew::OperationTracing.child_context, nil,
468
+ { duration_ms: duration_ms, timestamp: timestamp, source: "rails.active_support", system: kind == "database" ? "active_record" : "rails", operation: name, metadata: metadata, on_error: @on_error }, error
469
+ )
470
+ end
471
+ end
472
+ end
473
+
414
474
  def exception_mechanism_type
415
475
  "rails.middleware"
416
476
  end
@@ -419,6 +479,15 @@ module LogBrew
419
479
  "rails-exception"
420
480
  end
421
481
 
482
+ def exception_status_code(error)
483
+ return super unless defined?(::ActionDispatch::ExceptionWrapper)
484
+
485
+ status = ::ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
486
+ status.is_a?(Integer) && status.between?(400, 599) ? status : super
487
+ rescue StandardError
488
+ super
489
+ end
490
+
422
491
  def request_name(env)
423
492
  "#{request_method(env)} #{route_template(env)}"
424
493
  end
@@ -433,26 +502,26 @@ module LogBrew
433
502
  end
434
503
 
435
504
  def request_metadata(env, status_code)
436
- metadata = super
437
- metadata.delete("http.path")
438
- metadata.delete("action_dispatch.request_id")
439
- metadata.delete("HTTP_X_REQUEST_ID")
440
- metadata["source"] = "rails"
441
- metadata["http.method"] = request_method(env)
442
- metadata["http.route"] = route_template(env)
443
- metadata["http.status_code"] = status_code
444
- metadata["http.status_class"] = "#{status_code.to_i / 100}xx"
445
- controller, action = controller_and_action(env)
446
- metadata["rails.controller"] = controller unless controller.nil?
447
- metadata["rails.action"] = action unless action.nil?
448
- metadata
505
+ super.tap do |metadata|
506
+ metadata.delete("http.path")
507
+ metadata.delete("action_dispatch.request_id")
508
+ metadata.delete("HTTP_X_REQUEST_ID")
509
+ metadata["source"] = "rails"
510
+ metadata["http.route"] = route_template(env)
511
+ metadata["http.status_class"] = "#{status_code.to_i / 100}xx"
512
+ controller, action = controller_and_action(env)
513
+ metadata["rails.controller"] = controller unless controller.nil?
514
+ metadata["rails.action"] = action unless action.nil?
515
+ observed, operations = RequestOperations.snapshot
516
+ metadata["rails.operations.observed"] = observed
517
+ metadata["rails.operations.captured"] = operations.length
518
+ metadata["rails.operations.truncated"] = observed > operations.length
519
+ end
449
520
  end
450
521
 
451
522
  def route_template(env)
452
- route = bounded_route(env_value(env, "action_dispatch.route_uri_pattern"))
453
- return route unless route.nil?
454
-
455
- route = bounded_route(matched_route_pattern(env))
523
+ route = bounded_route(env_value(env, "action_dispatch.route_uri_pattern")) ||
524
+ bounded_route(matched_route_pattern(env))
456
525
  return route unless route.nil?
457
526
 
458
527
  controller, action = controller_and_action(env)
@@ -481,8 +550,7 @@ module LogBrew
481
550
  parameters = env["action_dispatch.request.path_parameters"]
482
551
  return [nil, nil] unless parameters.is_a?(Hash)
483
552
 
484
- [bounded_identifier(parameters[:controller] || parameters["controller"]),
485
- bounded_identifier(parameters[:action] || parameters["action"])]
553
+ %i[controller action].map { |key| bounded_identifier(parameters[key] || parameters[key.to_s]) }
486
554
  end
487
555
 
488
556
  def bounded_route(value)
@@ -495,12 +563,7 @@ module LogBrew
495
563
  end
496
564
 
497
565
  def bounded_identifier(value)
498
- return nil if value.nil?
499
-
500
- identifier = value.to_s
501
- return nil unless identifier.match?(/\A[a-zA-Z0-9_\/.-]{1,128}\z/)
502
-
503
- identifier
566
+ value.to_s[/\A[a-zA-Z0-9_\/.-]{1,128}\z/]
504
567
  end
505
568
  end
506
569
  private_constant :RailsRackMiddleware
@@ -525,7 +588,7 @@ module LogBrew
525
588
  adapter = adapter_for(active_client)
526
589
  return @app.call(environment) if adapter.nil?
527
590
 
528
- adapter.call(environment)
591
+ RequestOperations.within { adapter.call(environment) }
529
592
  end
530
593
 
531
594
  private
@@ -538,7 +601,7 @@ module LogBrew
538
601
  @app,
539
602
  client: active_client,
540
603
  flush_on_response: false,
541
- metadata: base_metadata,
604
+ metadata: @runtime.metadata,
542
605
  include_exception_message: @runtime.configuration.capture_exception_messages?,
543
606
  include_exception_backtrace: @runtime.configuration.include_exception_backtrace?,
544
607
  on_error: ->(error) { @runtime.report_error("request_capture", error) }
@@ -551,17 +614,6 @@ module LogBrew
551
614
  nil
552
615
  end
553
616
 
554
- def base_metadata
555
- configuration = @runtime.configuration
556
- {
557
- "service" => configuration.service_name,
558
- "environment" => configuration.app_environment,
559
- "framework" => "rails",
560
- "framework.version" => configuration.rails_version
561
- }.tap do |metadata|
562
- metadata["release"] = configuration.release unless configuration.release.nil?
563
- end
564
- end
565
617
  end
566
618
 
567
619
  # Rails.error subscriber for handled reports. Unhandled errors stay owned
@@ -601,18 +653,12 @@ module LogBrew
601
653
  @mutex.synchronize do
602
654
  return @subscriber if @subscriber_client.equal?(active_client) && !@subscriber.nil?
603
655
 
604
- configuration = @runtime.configuration
605
656
  @subscriber = LogBrew::RailsErrorSubscriber.new(
606
657
  client: active_client,
607
658
  flush_on_report: false,
608
- metadata: {
609
- "service" => configuration.service_name,
610
- "environment" => configuration.app_environment,
611
- "framework" => "rails",
612
- "framework.version" => configuration.rails_version
613
- },
614
- include_exception_message: configuration.capture_exception_messages?,
615
- include_exception_backtrace: configuration.include_exception_backtrace?,
659
+ metadata: @runtime.metadata,
660
+ include_exception_message: @runtime.configuration.capture_exception_messages?,
661
+ include_exception_backtrace: @runtime.configuration.include_exception_backtrace?,
616
662
  on_error: ->(error) { @runtime.report_error("handled_error_capture", error) }
617
663
  )
618
664
  @subscriber_client = active_client
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBrew
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/logbrew.rb CHANGED
@@ -33,6 +33,32 @@ module LogBrew
33
33
  METRIC_KINDS = METRIC_TEMPORALITIES_BY_KIND.keys.freeze
34
34
  NON_NEGATIVE_METRIC_KINDS = %w[counter histogram].freeze
35
35
 
36
+ module CaptureHelpers
37
+ module_function
38
+
39
+ def primitive_metadata_value?(value)
40
+ value.nil? || value == true || value == false || value.is_a?(String) || value.is_a?(Integer) ||
41
+ (value.is_a?(Float) && value.finite?)
42
+ end
43
+
44
+ def copy_metadata(metadata)
45
+ metadata.each_with_object({}) { |(key, value), copied| copied[key.to_s] = value if primitive_metadata_value?(value) }
46
+ end
47
+
48
+ def logbrew_timestamp
49
+ timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
50
+ timestamp.respond_to?(:iso8601) ? timestamp.iso8601 : timestamp.to_s
51
+ end
52
+
53
+ def capture_safely
54
+ yield
55
+ rescue StandardError => error
56
+ @on_error.call(error) if @on_error.respond_to?(:call)
57
+ raise error if @raise_errors
58
+ end
59
+ end
60
+ private_constant :CaptureHelpers
61
+
36
62
  class SdkError < StandardError
37
63
  attr_reader :code
38
64
 
@@ -202,6 +228,7 @@ module LogBrew
202
228
  end
203
229
 
204
230
  class Logger < ::Logger
231
+ include CaptureHelpers
205
232
  DEFAULT_LOGGER_NAME = "ruby-logger"
206
233
  SEVERITY_TO_LOGBREW_LEVEL = {
207
234
  ::Logger::DEBUG => "info",
@@ -306,13 +333,6 @@ module LogBrew
306
333
  raise error if @raise_errors
307
334
  end
308
335
 
309
- def logbrew_timestamp
310
- timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
311
- return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
312
-
313
- timestamp.to_s
314
- end
315
-
316
336
  def logbrew_message(message)
317
337
  return message.message if message.is_a?(Exception)
318
338
  return message if message.is_a?(String)
@@ -347,18 +367,6 @@ module LogBrew
347
367
  metadata["exceptionBacktrace"] = exception.backtrace.join("\n") if @include_exception_backtrace && exception.backtrace
348
368
  end
349
369
 
350
- def copy_metadata(metadata)
351
- metadata.each_with_object({}) do |(key, value), copied|
352
- copied[key.to_s] = value if primitive_metadata_value?(value)
353
- end
354
- end
355
-
356
- def primitive_metadata_value?(value)
357
- return true if value.nil? || value == true || value == false
358
- return true if value.is_a?(String) || value.is_a?(Integer)
359
-
360
- value.is_a?(Float) && value.finite?
361
- end
362
370
  end
363
371
  # Rack-compatible middleware for Rails, Sinatra, and other Rack-based Ruby apps.
364
372
  #
@@ -366,6 +374,7 @@ module LogBrew
366
374
  # exceptions as issue plus error-span events. It does not require the rack or
367
375
  # rails gems at runtime; any app object that responds to `call(env)` is enough.
368
376
  class RackMiddleware
377
+ include CaptureHelpers
369
378
  DEFAULT_EVENT_ID_PREFIX = "ruby_rack"
370
379
  DEFAULT_SPAN_LOGGER = "rack"
371
380
 
@@ -406,17 +415,17 @@ module LogBrew
406
415
  begin
407
416
  response = @app.call(env)
408
417
  rescue StandardError => error
409
- safely_capture do
410
- elapsed_ms = duration_ms(started_at)
411
- capture_exception_issue(env, error)
412
- capture_request_span(env, 500, elapsed_ms, "error")
418
+ status_code = exception_status_code(error)
419
+ capture_safely do
420
+ capture_exception_issue(env, error) if status_code >= 500
421
+ capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok")
413
422
  flush_if_configured
414
423
  end
415
424
  raise
416
425
  end
417
426
 
418
427
  status_code = rack_status(response)
419
- safely_capture do
428
+ capture_safely do
420
429
  capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok")
421
430
  flush_if_configured
422
431
  end
@@ -425,6 +434,10 @@ module LogBrew
425
434
 
426
435
  private
427
436
 
437
+ def exception_status_code(_error)
438
+ 500
439
+ end
440
+
428
441
  def capture_request_span(env, status_code, elapsed_ms, status)
429
442
  @client.span(
430
443
  next_event_id("span"),
@@ -456,13 +469,6 @@ module LogBrew
456
469
  end
457
470
  end
458
471
 
459
- def logbrew_timestamp
460
- timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
461
- return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
462
-
463
- timestamp.to_s
464
- end
465
-
466
472
  def request_name(env)
467
473
  "#{request_method(env)} #{request_path(env)}"
468
474
  end
@@ -558,19 +564,6 @@ module LogBrew
558
564
  metadata[key] = value unless value.nil?
559
565
  end
560
566
 
561
- def copy_metadata(metadata)
562
- metadata.each_with_object({}) do |(key, value), copied|
563
- copied[key.to_s] = value if primitive_metadata_value?(value)
564
- end
565
- end
566
-
567
- def primitive_metadata_value?(value)
568
- return true if value.nil? || value == true || value == false
569
- return true if value.is_a?(String) || value.is_a?(Integer)
570
-
571
- value.is_a?(Float) && value.finite?
572
- end
573
-
574
567
  def monotonic_time
575
568
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
576
569
  end
@@ -585,12 +578,6 @@ module LogBrew
585
578
  @client.flush(@transport)
586
579
  end
587
580
 
588
- def safely_capture
589
- yield
590
- rescue StandardError => error
591
- @on_error.call(error) if @on_error.respond_to?(:call)
592
- raise error if @raise_errors
593
- end
594
581
  end
595
582
 
596
583
  # Rails.error subscriber for handled and manually reported Rails exceptions.
@@ -599,6 +586,7 @@ module LogBrew
599
586
  # initializer. This class avoids a hard Rails dependency so the core gem stays
600
587
  # usable in plain Ruby and Rack apps.
601
588
  class RailsErrorSubscriber
589
+ include CaptureHelpers
602
590
  DEFAULT_EVENT_ID_PREFIX = "ruby_rails_error"
603
591
  SEVERITY_TO_ISSUE_LEVEL = {
604
592
  "info" => "info",
@@ -663,13 +651,6 @@ module LogBrew
663
651
 
664
652
  private
665
653
 
666
- def logbrew_timestamp
667
- timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
668
- return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
669
-
670
- timestamp.to_s
671
- end
672
-
673
654
  def error_title(error)
674
655
  return error.class.name if error.is_a?(Exception)
675
656
 
@@ -739,31 +720,12 @@ module LogBrew
739
720
  "rails-exception-#{Digest::SHA256.hexdigest(material)}"
740
721
  end
741
722
 
742
- def copy_metadata(metadata)
743
- metadata.each_with_object({}) do |(key, value), copied|
744
- copied[key.to_s] = value if primitive_metadata_value?(value)
745
- end
746
- end
747
-
748
- def primitive_metadata_value?(value)
749
- return true if value.nil? || value == true || value == false
750
- return true if value.is_a?(String) || value.is_a?(Integer)
751
-
752
- value.is_a?(Float) && value.finite?
753
- end
754
-
755
723
  def flush_if_configured
756
724
  return unless @flush_on_report && !@transport.nil? && @client.pending_events.positive?
757
725
 
758
726
  @client.flush(@transport)
759
727
  end
760
728
 
761
- def capture_safely
762
- yield
763
- rescue StandardError => error
764
- @on_error.call(error) if @on_error.respond_to?(:call)
765
- raise error if @raise_errors
766
- end
767
729
  end
768
730
 
769
731
  module Validation
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.5
4
+ version: 0.1.7
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-10 00:00:00.000000000 Z
11
+ date: 2026-08-13 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.