logbrew-sdk 0.1.6 → 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: 4e89660190354962ae56323ce67dd3192d2b6ac7fc52f73fefe8cca3e4cffe31
4
- data.tar.gz: d92f16257ab1911e72f025f0b1fa29a85138e522653a68fdd4466e8f5c0311c7
3
+ metadata.gz: f086729efb0a0a98821ef3bce8d96b71066b202beff5656917d958e4ca119479
4
+ data.tar.gz: 7c5eafb18cda7d001dc8c72c9646852e857a46bd4e06508abeda5a23365489a1
5
5
  SHA512:
6
- metadata.gz: 83d428f770034749b2c6717eb7fae953a21671bd050406822f341e7f06494af15849d1ac58b259ab08c2910f161e7bf8321fa3b3b6867b8bb14b048883b0a684
7
- data.tar.gz: b60cd4f0f853c45fd656d16605b23bafa4fd2396f56be9c2be7ba8da3aa358a33510082f0ca49a45222690caea01bc1b5c3997ab30819ada8a8bde311963a617
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.6"
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
399
441
  end
400
442
 
401
- def logbrew_timestamp
402
- timestamp = @timestamp_provider.call
403
- return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
443
+ def snapshot
444
+ Thread.current[STATE_KEY] || [0, []]
445
+ end
446
+
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
@@ -442,17 +502,21 @@ module LogBrew
442
502
  end
443
503
 
444
504
  def request_metadata(env, status_code)
445
- metadata = super
446
- metadata.delete("http.path")
447
- metadata.delete("action_dispatch.request_id")
448
- metadata.delete("HTTP_X_REQUEST_ID")
449
- metadata["source"] = "rails"
450
- metadata["http.route"] = route_template(env)
451
- metadata["http.status_class"] = "#{status_code.to_i / 100}xx"
452
- controller, action = controller_and_action(env)
453
- metadata["rails.controller"] = controller unless controller.nil?
454
- metadata["rails.action"] = action unless action.nil?
455
- 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
456
520
  end
457
521
 
458
522
  def route_template(env)
@@ -486,8 +550,7 @@ module LogBrew
486
550
  parameters = env["action_dispatch.request.path_parameters"]
487
551
  return [nil, nil] unless parameters.is_a?(Hash)
488
552
 
489
- [bounded_identifier(parameters[:controller] || parameters["controller"]),
490
- bounded_identifier(parameters[:action] || parameters["action"])]
553
+ %i[controller action].map { |key| bounded_identifier(parameters[key] || parameters[key.to_s]) }
491
554
  end
492
555
 
493
556
  def bounded_route(value)
@@ -500,12 +563,7 @@ module LogBrew
500
563
  end
501
564
 
502
565
  def bounded_identifier(value)
503
- return nil if value.nil?
504
-
505
- identifier = value.to_s
506
- return nil unless identifier.match?(/\A[a-zA-Z0-9_\/.-]{1,128}\z/)
507
-
508
- identifier
566
+ value.to_s[/\A[a-zA-Z0-9_\/.-]{1,128}\z/]
509
567
  end
510
568
  end
511
569
  private_constant :RailsRackMiddleware
@@ -530,7 +588,7 @@ module LogBrew
530
588
  adapter = adapter_for(active_client)
531
589
  return @app.call(environment) if adapter.nil?
532
590
 
533
- adapter.call(environment)
591
+ RequestOperations.within { adapter.call(environment) }
534
592
  end
535
593
 
536
594
  private
@@ -543,7 +601,7 @@ module LogBrew
543
601
  @app,
544
602
  client: active_client,
545
603
  flush_on_response: false,
546
- metadata: base_metadata,
604
+ metadata: @runtime.metadata,
547
605
  include_exception_message: @runtime.configuration.capture_exception_messages?,
548
606
  include_exception_backtrace: @runtime.configuration.include_exception_backtrace?,
549
607
  on_error: ->(error) { @runtime.report_error("request_capture", error) }
@@ -556,17 +614,6 @@ module LogBrew
556
614
  nil
557
615
  end
558
616
 
559
- def base_metadata
560
- configuration = @runtime.configuration
561
- {
562
- "service" => configuration.service_name,
563
- "environment" => configuration.app_environment,
564
- "framework" => "rails",
565
- "framework.version" => configuration.rails_version
566
- }.tap do |metadata|
567
- metadata["release"] = configuration.release unless configuration.release.nil?
568
- end
569
- end
570
617
  end
571
618
 
572
619
  # Rails.error subscriber for handled reports. Unhandled errors stay owned
@@ -606,18 +653,12 @@ module LogBrew
606
653
  @mutex.synchronize do
607
654
  return @subscriber if @subscriber_client.equal?(active_client) && !@subscriber.nil?
608
655
 
609
- configuration = @runtime.configuration
610
656
  @subscriber = LogBrew::RailsErrorSubscriber.new(
611
657
  client: active_client,
612
658
  flush_on_report: false,
613
- metadata: {
614
- "service" => configuration.service_name,
615
- "environment" => configuration.app_environment,
616
- "framework" => "rails",
617
- "framework.version" => configuration.rails_version
618
- },
619
- include_exception_message: configuration.capture_exception_messages?,
620
- 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?,
621
662
  on_error: ->(error) { @runtime.report_error("handled_error_capture", error) }
622
663
  )
623
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.6"
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
 
@@ -407,7 +416,7 @@ module LogBrew
407
416
  response = @app.call(env)
408
417
  rescue StandardError => error
409
418
  status_code = exception_status_code(error)
410
- safely_capture do
419
+ capture_safely do
411
420
  capture_exception_issue(env, error) if status_code >= 500
412
421
  capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok")
413
422
  flush_if_configured
@@ -416,7 +425,7 @@ module LogBrew
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
@@ -460,13 +469,6 @@ module LogBrew
460
469
  end
461
470
  end
462
471
 
463
- def logbrew_timestamp
464
- timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
465
- return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
466
-
467
- timestamp.to_s
468
- end
469
-
470
472
  def request_name(env)
471
473
  "#{request_method(env)} #{request_path(env)}"
472
474
  end
@@ -562,19 +564,6 @@ module LogBrew
562
564
  metadata[key] = value unless value.nil?
563
565
  end
564
566
 
565
- def copy_metadata(metadata)
566
- metadata.each_with_object({}) do |(key, value), copied|
567
- copied[key.to_s] = value if primitive_metadata_value?(value)
568
- end
569
- end
570
-
571
- def primitive_metadata_value?(value)
572
- return true if value.nil? || value == true || value == false
573
- return true if value.is_a?(String) || value.is_a?(Integer)
574
-
575
- value.is_a?(Float) && value.finite?
576
- end
577
-
578
567
  def monotonic_time
579
568
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
580
569
  end
@@ -589,12 +578,6 @@ module LogBrew
589
578
  @client.flush(@transport)
590
579
  end
591
580
 
592
- def safely_capture
593
- yield
594
- rescue StandardError => error
595
- @on_error.call(error) if @on_error.respond_to?(:call)
596
- raise error if @raise_errors
597
- end
598
581
  end
599
582
 
600
583
  # Rails.error subscriber for handled and manually reported Rails exceptions.
@@ -603,6 +586,7 @@ module LogBrew
603
586
  # initializer. This class avoids a hard Rails dependency so the core gem stays
604
587
  # usable in plain Ruby and Rack apps.
605
588
  class RailsErrorSubscriber
589
+ include CaptureHelpers
606
590
  DEFAULT_EVENT_ID_PREFIX = "ruby_rails_error"
607
591
  SEVERITY_TO_ISSUE_LEVEL = {
608
592
  "info" => "info",
@@ -667,13 +651,6 @@ module LogBrew
667
651
 
668
652
  private
669
653
 
670
- def logbrew_timestamp
671
- timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
672
- return timestamp.iso8601 if timestamp.respond_to?(:iso8601)
673
-
674
- timestamp.to_s
675
- end
676
-
677
654
  def error_title(error)
678
655
  return error.class.name if error.is_a?(Exception)
679
656
 
@@ -743,31 +720,12 @@ module LogBrew
743
720
  "rails-exception-#{Digest::SHA256.hexdigest(material)}"
744
721
  end
745
722
 
746
- def copy_metadata(metadata)
747
- metadata.each_with_object({}) do |(key, value), copied|
748
- copied[key.to_s] = value if primitive_metadata_value?(value)
749
- end
750
- end
751
-
752
- def primitive_metadata_value?(value)
753
- return true if value.nil? || value == true || value == false
754
- return true if value.is_a?(String) || value.is_a?(Integer)
755
-
756
- value.is_a?(Float) && value.finite?
757
- end
758
-
759
723
  def flush_if_configured
760
724
  return unless @flush_on_report && !@transport.nil? && @client.pending_events.positive?
761
725
 
762
726
  @client.flush(@transport)
763
727
  end
764
728
 
765
- def capture_safely
766
- yield
767
- rescue StandardError => error
768
- @on_error.call(error) if @on_error.respond_to?(:call)
769
- raise error if @raise_errors
770
- end
771
729
  end
772
730
 
773
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.6
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-11 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.