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 +4 -4
- data/README.md +10 -4
- data/lib/logbrew/operation_tracing.rb +26 -61
- data/lib/logbrew/rails.rb +3 -0
- data/lib/logbrew/rails_integration.rb +112 -66
- data/lib/logbrew/version.rb +1 -1
- data/lib/logbrew.rb +38 -76
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f086729efb0a0a98821ef3bce8d96b71066b202beff5656917d958e4ca119479
|
|
4
|
+
data.tar.gz: 7c5eafb18cda7d001dc8c72c9646852e857a46bd4e06508abeda5a23365489a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
/
|
|
9
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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]
|
|
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 :
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
610
|
-
|
|
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
|
data/lib/logbrew/version.rb
CHANGED
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
|
-
|
|
410
|
-
|
|
411
|
-
capture_exception_issue(env, error)
|
|
412
|
-
capture_request_span(env,
|
|
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
|
-
|
|
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.
|
|
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
|
+
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.
|