logbrew-sdk 0.1.6 → 0.1.8
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 +101 -60
- data/lib/logbrew/trace.rb +4 -14
- data/lib/logbrew/version.rb +1 -1
- data/lib/logbrew.rb +52 -88
- 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: 23bb3971a708e01fb3baa93461beb30dda7ff5a6d1b807a6f21a2c4365b718ee
|
|
4
|
+
data.tar.gz: 2254bb2a7317aa32053e43ae2463ba527c8ec4bd9d7147d31a1c085df690857d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b516708afcb021d131aac74c02b0dae2b97d04875f45ec408ff431a5046d2c12af0e824ddddbd5e9280c934ed2a016615cabb4e9d035174c2da99e4806afbc14
|
|
7
|
+
data.tar.gz: '09b94b37beba32a8956b06295ca7ad0b4f16b0de21e1148a127797f1fd61f0c7763dc85b336ee7a8e249dbd38f02e2e878aadb855dc6beb4404584a5ae6b89d4'
|
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.8"
|
|
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
|
|
399
441
|
end
|
|
400
442
|
|
|
401
|
-
def
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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[
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
615
|
-
|
|
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
|
data/lib/logbrew/trace.rb
CHANGED
|
@@ -240,7 +240,7 @@ module LogBrew
|
|
|
240
240
|
|
|
241
241
|
private
|
|
242
242
|
|
|
243
|
-
def capture_request_span(env, status_code, elapsed_ms, status)
|
|
243
|
+
def capture_request_span(env, status_code, elapsed_ms, status, timestamp)
|
|
244
244
|
context = rack_trace_context(env)
|
|
245
245
|
attributes = {
|
|
246
246
|
name: request_name(env),
|
|
@@ -252,31 +252,21 @@ module LogBrew
|
|
|
252
252
|
}
|
|
253
253
|
attributes[:parentSpanId] = context.parent_span_id if context && context.parent_span_id
|
|
254
254
|
|
|
255
|
-
@client.span(next_event_id("span"),
|
|
255
|
+
@client.span(next_event_id("span"), timestamp, attributes)
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
def trace_id(env)
|
|
259
|
-
|
|
260
|
-
return context.trace_id if context
|
|
261
|
-
|
|
262
|
-
super
|
|
259
|
+
rack_trace_context(env)&.trace_id || super
|
|
263
260
|
end
|
|
264
261
|
|
|
265
262
|
def span_id(env)
|
|
266
|
-
|
|
267
|
-
return context.span_id if context
|
|
268
|
-
|
|
269
|
-
super
|
|
263
|
+
rack_trace_context(env)&.span_id || super
|
|
270
264
|
end
|
|
271
265
|
|
|
272
266
|
def request_metadata(env, status_code)
|
|
273
267
|
Trace.add_metadata(super, rack_trace_context(env))
|
|
274
268
|
end
|
|
275
269
|
|
|
276
|
-
def exception_metadata(env, error)
|
|
277
|
-
Trace.add_metadata(super, rack_trace_context(env))
|
|
278
|
-
end
|
|
279
|
-
|
|
280
270
|
def rack_trace_context(env)
|
|
281
271
|
trace = env["logbrew.trace"] if env.respond_to?(:[])
|
|
282
272
|
trace.is_a?(TraceContext) ? trace : Trace.current
|
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(precision = nil)
|
|
49
|
+
timestamp = @timestamp_provider.respond_to?(:call) ? @timestamp_provider.call : Time.now
|
|
50
|
+
timestamp.respond_to?(:iso8601) ? timestamp.iso8601(*[precision].compact) : 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
|
|
|
@@ -403,36 +412,42 @@ module LogBrew
|
|
|
403
412
|
|
|
404
413
|
def call(env)
|
|
405
414
|
started_at = monotonic_time
|
|
406
|
-
|
|
407
|
-
|
|
415
|
+
started_timestamp, timestamp_error = request_timestamp
|
|
416
|
+
response = begin
|
|
417
|
+
@app.call(env)
|
|
408
418
|
rescue StandardError => error
|
|
409
|
-
|
|
410
|
-
safely_capture do
|
|
411
|
-
capture_exception_issue(env, error) if status_code >= 500
|
|
412
|
-
capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok")
|
|
413
|
-
flush_if_configured
|
|
414
|
-
end
|
|
419
|
+
capture_request(env, exception_status_code(error), started_at, started_timestamp, timestamp_error, error)
|
|
415
420
|
raise
|
|
416
421
|
end
|
|
422
|
+
capture_request(env, rack_status(response), started_at, started_timestamp, timestamp_error)
|
|
423
|
+
response
|
|
424
|
+
end
|
|
417
425
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
private
|
|
427
|
+
|
|
428
|
+
def capture_request(env, status_code, started_at, timestamp, timestamp_error, error = nil)
|
|
429
|
+
capture_safely do
|
|
430
|
+
raise timestamp_error unless timestamp_error.nil?
|
|
431
|
+
capture_exception_issue(env, error, logbrew_timestamp(6)) if error && status_code >= 500
|
|
432
|
+
capture_request_span(env, status_code, duration_ms(started_at), status_code >= 500 ? "error" : "ok", timestamp)
|
|
421
433
|
flush_if_configured
|
|
422
434
|
end
|
|
423
|
-
response
|
|
424
435
|
end
|
|
425
436
|
|
|
426
|
-
|
|
437
|
+
def request_timestamp
|
|
438
|
+
[logbrew_timestamp(6), nil]
|
|
439
|
+
rescue StandardError => error
|
|
440
|
+
[nil, error]
|
|
441
|
+
end
|
|
427
442
|
|
|
428
443
|
def exception_status_code(_error)
|
|
429
444
|
500
|
|
430
445
|
end
|
|
431
446
|
|
|
432
|
-
def capture_request_span(env, status_code, elapsed_ms, status)
|
|
447
|
+
def capture_request_span(env, status_code, elapsed_ms, status, timestamp)
|
|
433
448
|
@client.span(
|
|
434
449
|
next_event_id("span"),
|
|
435
|
-
|
|
450
|
+
timestamp,
|
|
436
451
|
name: request_name(env),
|
|
437
452
|
traceId: trace_id(env),
|
|
438
453
|
spanId: span_id(env),
|
|
@@ -442,7 +457,7 @@ module LogBrew
|
|
|
442
457
|
)
|
|
443
458
|
end
|
|
444
459
|
|
|
445
|
-
def capture_exception_issue(env, error)
|
|
460
|
+
def capture_exception_issue(env, error, timestamp)
|
|
446
461
|
attributes = IssueDiagnostics.from_exception(
|
|
447
462
|
error,
|
|
448
463
|
message: @include_exception_message ? error.message : nil,
|
|
@@ -450,7 +465,7 @@ module LogBrew
|
|
|
450
465
|
handled: false,
|
|
451
466
|
metadata: exception_metadata(env, error)
|
|
452
467
|
)
|
|
453
|
-
@client.issue(next_event_id("issue"),
|
|
468
|
+
@client.issue(next_event_id("issue"), timestamp, attributes)
|
|
454
469
|
end
|
|
455
470
|
|
|
456
471
|
def next_event_id(kind)
|
|
@@ -460,13 +475,6 @@ module LogBrew
|
|
|
460
475
|
end
|
|
461
476
|
end
|
|
462
477
|
|
|
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
478
|
def request_name(env)
|
|
471
479
|
"#{request_method(env)} #{request_path(env)}"
|
|
472
480
|
end
|
|
@@ -562,19 +570,6 @@ module LogBrew
|
|
|
562
570
|
metadata[key] = value unless value.nil?
|
|
563
571
|
end
|
|
564
572
|
|
|
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
573
|
def monotonic_time
|
|
579
574
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
580
575
|
end
|
|
@@ -589,12 +584,6 @@ module LogBrew
|
|
|
589
584
|
@client.flush(@transport)
|
|
590
585
|
end
|
|
591
586
|
|
|
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
587
|
end
|
|
599
588
|
|
|
600
589
|
# Rails.error subscriber for handled and manually reported Rails exceptions.
|
|
@@ -603,6 +592,7 @@ module LogBrew
|
|
|
603
592
|
# initializer. This class avoids a hard Rails dependency so the core gem stays
|
|
604
593
|
# usable in plain Ruby and Rack apps.
|
|
605
594
|
class RailsErrorSubscriber
|
|
595
|
+
include CaptureHelpers
|
|
606
596
|
DEFAULT_EVENT_ID_PREFIX = "ruby_rails_error"
|
|
607
597
|
SEVERITY_TO_ISSUE_LEVEL = {
|
|
608
598
|
"info" => "info",
|
|
@@ -667,13 +657,6 @@ module LogBrew
|
|
|
667
657
|
|
|
668
658
|
private
|
|
669
659
|
|
|
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
660
|
def error_title(error)
|
|
678
661
|
return error.class.name if error.is_a?(Exception)
|
|
679
662
|
|
|
@@ -743,31 +726,12 @@ module LogBrew
|
|
|
743
726
|
"rails-exception-#{Digest::SHA256.hexdigest(material)}"
|
|
744
727
|
end
|
|
745
728
|
|
|
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
729
|
def flush_if_configured
|
|
760
730
|
return unless @flush_on_report && !@transport.nil? && @client.pending_events.positive?
|
|
761
731
|
|
|
762
732
|
@client.flush(@transport)
|
|
763
733
|
end
|
|
764
734
|
|
|
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
735
|
end
|
|
772
736
|
|
|
773
737
|
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.8
|
|
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.
|