lapsoss 0.4.10 → 1.0.0
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 +125 -3
- data/lib/lapsoss/adapters/concerns/http_delivery.rb +19 -7
- data/lib/lapsoss/adapters/concerns/level_mapping.rb +10 -0
- data/lib/lapsoss/adapters/concerns/stacktrace_builder.rb +70 -0
- data/lib/lapsoss/adapters/concerns/trace_context.rb +52 -0
- data/lib/lapsoss/adapters/openobserve_adapter.rb +156 -0
- data/lib/lapsoss/adapters/otlp_adapter.rb +231 -0
- data/lib/lapsoss/adapters/rollbar_adapter.rb +4 -2
- data/lib/lapsoss/adapters/sentry_adapter.rb +2 -2
- data/lib/lapsoss/adapters/telebugs_adapter.rb +4 -4
- data/lib/lapsoss/client.rb +31 -8
- data/lib/lapsoss/configuration.rb +36 -1
- data/lib/lapsoss/current.rb +1 -0
- data/lib/lapsoss/middleware/sample_filter.rb +32 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +44 -0
- data/lib/lapsoss/rails_event_subscriber.rb +41 -0
- data/lib/lapsoss/railtie.rb +13 -0
- data/lib/lapsoss/router.rb +3 -2
- data/lib/lapsoss/scrubber.rb +90 -2
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +32 -0
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c8f5f1fbdb514808ad6accd57f448a1d997f86f8c53224456421879e78ee40e
|
|
4
|
+
data.tar.gz: ee76c128c4b6f7b0f7d51ff81700258d6a8e1fbde522400009fa682b49658c7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0591e240fabe7f68e179a09f55d61bc03f26374ca7abd28bc4f5a67789644670342ef43d7f56ebff6c3e409e7df18fd77ae11e82ae5d6fe44af582add4e20efb'
|
|
7
|
+
data.tar.gz: f85e9148f94abe28ff69693169fc600641c99cf1d2f0d0204b95c3c3582e039d5fab19cc9a7b8442e09dd72b2d2c2ce33f71339d0967b068a3c1363dab583bd5
|
data/README.md
CHANGED
|
@@ -39,7 +39,7 @@ end
|
|
|
39
39
|
## Requirements
|
|
40
40
|
|
|
41
41
|
- Ruby 3.3+
|
|
42
|
-
- Rails
|
|
42
|
+
- Rails 8.1+
|
|
43
43
|
|
|
44
44
|
## Installation
|
|
45
45
|
|
|
@@ -69,7 +69,7 @@ That's it. No 500-line examples needed.
|
|
|
69
69
|
|
|
70
70
|
## Built for Rails, Not Around It
|
|
71
71
|
|
|
72
|
-
Lapsoss integrates with Rails' native error reporting API
|
|
72
|
+
Lapsoss integrates with Rails' native error reporting API (`Rails.error`). No monkey-patching, no global error handlers:
|
|
73
73
|
|
|
74
74
|
```ruby
|
|
75
75
|
# It just works with Rails.error:
|
|
@@ -116,6 +116,51 @@ end
|
|
|
116
116
|
|
|
117
117
|
# Or use Rails.error directly with your configured services
|
|
118
118
|
Rails.error.report(e, context: { user_id: current_user.id })
|
|
119
|
+
|
|
120
|
+
# For "should never happen" paths, Rails.error.unexpected raises in
|
|
121
|
+
# development/test and reports in production - and routes through Lapsoss too
|
|
122
|
+
Rails.error.unexpected("Reached unreachable branch")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Structured Events as Breadcrumbs
|
|
126
|
+
|
|
127
|
+
Lapsoss automatically subscribes to the structured event reporter
|
|
128
|
+
(`Rails.event`) and records emitted events as breadcrumbs. When an error is
|
|
129
|
+
captured, the recent activity trail is attached to the report - no
|
|
130
|
+
monkey-patching, just Rails' native API:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
Rails.event.notify("order.checkout_started", cart_id: cart.id)
|
|
134
|
+
# ... an exception here includes that breadcrumb in the error report
|
|
135
|
+
|
|
136
|
+
# Tags and context flow into breadcrumb metadata
|
|
137
|
+
Rails.event.tagged(section: "checkout") do
|
|
138
|
+
Rails.event.notify("payment.authorized", amount: 42_00)
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Tune or disable it:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
Lapsoss.configure do |config|
|
|
146
|
+
config.capture_rails_events = false # opt out entirely
|
|
147
|
+
config.rails_event_filter = ->(event) { !event[:name].start_with?("noisy.") }
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Rails-side enrichment also composes: context added via `Rails.error.add_middleware`
|
|
152
|
+
flows into the `context:` Lapsoss receives for free.
|
|
153
|
+
|
|
154
|
+
### Silencing Capture
|
|
155
|
+
|
|
156
|
+
Suppress delivery for a block (thread-local) - useful in tests or around
|
|
157
|
+
expected-failure paths. Breadcrumbs and scope still accumulate; only delivery
|
|
158
|
+
is skipped:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
Lapsoss.silence do
|
|
162
|
+
retry_flaky_third_party_call
|
|
163
|
+
end
|
|
119
164
|
```
|
|
120
165
|
|
|
121
166
|
### No Global Patching Philosophy
|
|
@@ -220,6 +265,8 @@ All adapters are pure Ruby implementations with no external SDK dependencies:
|
|
|
220
265
|
- **AppSignal** - Error tracking and deploy markers
|
|
221
266
|
- **Insight Hub** (formerly Bugsnag) - Error tracking with breadcrumbs
|
|
222
267
|
- **Telebugs** - Sentry-compatible protocol (perfect for self-hosted alternatives)
|
|
268
|
+
- **OpenObserve** - Open-source observability platform (logs, metrics, traces)
|
|
269
|
+
- **OTLP** - OpenTelemetry Protocol (works with SigNoz, Jaeger, Tempo, Honeycomb, etc.)
|
|
223
270
|
|
|
224
271
|
## Configuration
|
|
225
272
|
|
|
@@ -254,6 +301,39 @@ Lapsoss.configure do |config|
|
|
|
254
301
|
end
|
|
255
302
|
```
|
|
256
303
|
|
|
304
|
+
### Using OpenObserve
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# OpenObserve - open-source observability platform
|
|
308
|
+
Lapsoss.configure do |config|
|
|
309
|
+
config.use_openobserve(
|
|
310
|
+
endpoint: ENV['OPENOBSERVE_ENDPOINT'], # e.g., "http://localhost:5080"
|
|
311
|
+
username: ENV['OPENOBSERVE_USERNAME'],
|
|
312
|
+
password: ENV['OPENOBSERVE_PASSWORD'],
|
|
313
|
+
org: "default", # optional, defaults to "default"
|
|
314
|
+
stream: "errors" # optional, defaults to "errors"
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Using OTLP (OpenTelemetry Protocol)
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
# Works with SigNoz, Jaeger, Tempo, Honeycomb, Datadog, etc.
|
|
323
|
+
Lapsoss.configure do |config|
|
|
324
|
+
config.use_otlp(
|
|
325
|
+
endpoint: ENV['OTLP_ENDPOINT'], # e.g., "http://localhost:4318"
|
|
326
|
+
service_name: "my-rails-app",
|
|
327
|
+
headers: { "X-Custom-Header" => "value" } # optional custom headers
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Or use convenience helpers for specific services
|
|
331
|
+
config.use_signoz(signoz_api_key: ENV['SIGNOZ_API_KEY'])
|
|
332
|
+
config.use_jaeger(endpoint: "http://jaeger:4318")
|
|
333
|
+
config.use_tempo(endpoint: "http://tempo:4318")
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
257
337
|
### Advanced Configuration
|
|
258
338
|
|
|
259
339
|
```ruby
|
|
@@ -276,9 +356,27 @@ Lapsoss.configure do |config|
|
|
|
276
356
|
end
|
|
277
357
|
```
|
|
278
358
|
|
|
359
|
+
### Pipeline & Sampling (optional)
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
Lapsoss.configure do |config|
|
|
363
|
+
# Build a middleware pipeline for every event
|
|
364
|
+
config.configure_pipeline do |pipeline|
|
|
365
|
+
pipeline.sample(rate: 0.1) # Drop 90% of events
|
|
366
|
+
|
|
367
|
+
pipeline.enhance_user_context(
|
|
368
|
+
provider: ->(event, _) { current_user&.slice(:id, :email) },
|
|
369
|
+
privacy_mode: true # keep only ids
|
|
370
|
+
)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
279
375
|
### Filtering Errors
|
|
280
376
|
|
|
281
|
-
You decide what errors to track. Lapsoss doesn't make assumptions
|
|
377
|
+
You decide what errors to track. Lapsoss doesn't make assumptions.
|
|
378
|
+
|
|
379
|
+
**Execution order:** `exclusion_filter` runs first, then `before_send`.
|
|
282
380
|
|
|
283
381
|
```ruby
|
|
284
382
|
Lapsoss.configure do |config|
|
|
@@ -351,6 +449,30 @@ Rails.application.config.filter_parameters += [:password, :token]
|
|
|
351
449
|
# Lapsoss automatically uses these filters - no additional configuration needed!
|
|
352
450
|
```
|
|
353
451
|
|
|
452
|
+
Additional controls:
|
|
453
|
+
|
|
454
|
+
```ruby
|
|
455
|
+
Lapsoss.configure do |config|
|
|
456
|
+
config.scrub_fields = %w[credit_card ssn api_key]
|
|
457
|
+
config.scrub_all = true # Mask everything by default
|
|
458
|
+
config.whitelist_fields = %w[user_id request_id] # Keep these fields as-is
|
|
459
|
+
config.randomize_scrub_length = true # Avoid fixed "[FILTERED]" marker
|
|
460
|
+
end
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Error Handler Hook
|
|
464
|
+
|
|
465
|
+
Get a callback when an adapter fails to deliver:
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
Lapsoss.configure do |config|
|
|
469
|
+
config.error_handler = lambda do |adapter, event, error|
|
|
470
|
+
Rails.logger.error("Delivery failed for #{adapter.name}: #{error.message}")
|
|
471
|
+
Rails.logger.error(event.to_h) if Rails.env.development?
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
354
476
|
### Custom Fingerprinting
|
|
355
477
|
|
|
356
478
|
Control how errors are grouped:
|
|
@@ -12,8 +12,8 @@ module Lapsoss
|
|
|
12
12
|
extend ActiveSupport::Concern
|
|
13
13
|
|
|
14
14
|
included do
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# Instance-level endpoint configuration for adapter isolation
|
|
16
|
+
attr_reader :api_endpoint, :api_path
|
|
17
17
|
|
|
18
18
|
# Memoized git info using AS
|
|
19
19
|
mattr_accessor :git_info_cache, default: {}
|
|
@@ -39,7 +39,7 @@ module Lapsoss
|
|
|
39
39
|
handle_response(response)
|
|
40
40
|
end
|
|
41
41
|
rescue => error
|
|
42
|
-
handle_delivery_error(error)
|
|
42
|
+
handle_delivery_error(error, event)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Common headers for all adapters
|
|
@@ -101,7 +101,7 @@ module Lapsoss
|
|
|
101
101
|
raise DeliveryError.new("Client error: #{message}", response: response)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def handle_delivery_error(error)
|
|
104
|
+
def handle_delivery_error(error, event = nil)
|
|
105
105
|
ActiveSupport::Notifications.instrument("error.lapsoss",
|
|
106
106
|
adapter: self.class.name,
|
|
107
107
|
error: error.class.name,
|
|
@@ -109,10 +109,22 @@ module Lapsoss
|
|
|
109
109
|
)
|
|
110
110
|
|
|
111
111
|
Lapsoss.configuration.logger&.error("[#{self.class.name}] Delivery failed: #{error.message}")
|
|
112
|
-
Lapsoss.
|
|
112
|
+
Lapsoss.call_error_handler(adapter: self, event: event, error: error)
|
|
113
|
+
mark_error_handled(error)
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
if error.is_a?(DeliveryError)
|
|
116
|
+
raise error
|
|
117
|
+
else
|
|
118
|
+
delivery_error = DeliveryError.new("Delivery failed: #{error.message}", cause: error)
|
|
119
|
+
mark_error_handled(delivery_error)
|
|
120
|
+
raise delivery_error
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def mark_error_handled(error)
|
|
125
|
+
error.instance_variable_set(:@lapsoss_error_handled, true)
|
|
126
|
+
rescue StandardError
|
|
127
|
+
# If setting the flag fails, we still continue
|
|
116
128
|
end
|
|
117
129
|
|
|
118
130
|
private
|
|
@@ -44,6 +44,16 @@ module Lapsoss
|
|
|
44
44
|
error: "error",
|
|
45
45
|
fatal: "error",
|
|
46
46
|
critical: "error"
|
|
47
|
+
}.with_indifferent_access,
|
|
48
|
+
|
|
49
|
+
openobserve: {
|
|
50
|
+
debug: "DEBUG",
|
|
51
|
+
info: "INFO",
|
|
52
|
+
warning: "WARN",
|
|
53
|
+
warn: "WARN",
|
|
54
|
+
error: "ERROR",
|
|
55
|
+
fatal: "FATAL",
|
|
56
|
+
critical: "FATAL"
|
|
47
57
|
}.with_indifferent_access
|
|
48
58
|
}.freeze
|
|
49
59
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "active_support/core_ext/object/blank"
|
|
5
|
+
|
|
6
|
+
module Lapsoss
|
|
7
|
+
module Adapters
|
|
8
|
+
module Concerns
|
|
9
|
+
# Shared stacktrace building logic for adapters
|
|
10
|
+
# Provides consistent frame formatting across Sentry, OpenObserve, OTLP, etc.
|
|
11
|
+
module StacktraceBuilder
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
# Build frames from event backtrace
|
|
15
|
+
# @param event [Lapsoss::Event] The event with backtrace_frames
|
|
16
|
+
# @param reverse [Boolean] Reverse frame order (Sentry expects oldest-to-newest)
|
|
17
|
+
# @return [Array<Hash>] Array of formatted frame hashes
|
|
18
|
+
def build_frames(event, reverse: false)
|
|
19
|
+
return [] unless event.has_backtrace?
|
|
20
|
+
|
|
21
|
+
frames = event.backtrace_frames.map { |frame| build_frame(frame) }
|
|
22
|
+
reverse ? frames.reverse : frames
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build a single frame hash from a BacktraceFrame
|
|
26
|
+
# @param frame [Lapsoss::BacktraceFrame] The frame to format
|
|
27
|
+
# @return [Hash] Formatted frame hash
|
|
28
|
+
def build_frame(frame)
|
|
29
|
+
frame_hash = {
|
|
30
|
+
filename: frame.filename,
|
|
31
|
+
abs_path: frame.absolute_path || frame.filename,
|
|
32
|
+
function: frame.method_name || frame.function,
|
|
33
|
+
lineno: frame.line_number,
|
|
34
|
+
in_app: frame.in_app
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
add_code_context(frame_hash, frame) if frame.code_context.present?
|
|
38
|
+
|
|
39
|
+
frame_hash.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Build raw stacktrace string for logging/simple formats
|
|
43
|
+
# @param event [Lapsoss::Event] The event with backtrace_frames
|
|
44
|
+
# @return [Array<String>] Array of formatted frame strings
|
|
45
|
+
def build_raw_stacktrace(event)
|
|
46
|
+
return [] unless event.has_backtrace?
|
|
47
|
+
|
|
48
|
+
event.backtrace_frames.map do |frame|
|
|
49
|
+
"#{frame.absolute_path || frame.filename}:#{frame.line_number} in `#{frame.method_name}`"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Build stacktrace as single string (for OTLP exception.stacktrace)
|
|
54
|
+
# @param event [Lapsoss::Event] The event with backtrace_frames
|
|
55
|
+
# @return [String] Newline-separated stacktrace
|
|
56
|
+
def build_stacktrace_string(event)
|
|
57
|
+
build_raw_stacktrace(event).join("\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def add_code_context(frame_hash, frame)
|
|
63
|
+
frame_hash[:pre_context] = frame.code_context[:pre_context]
|
|
64
|
+
frame_hash[:context_line] = frame.code_context[:context_line]
|
|
65
|
+
frame_hash[:post_context] = frame.code_context[:post_context]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Lapsoss
|
|
7
|
+
module Adapters
|
|
8
|
+
module Concerns
|
|
9
|
+
# Trace context utilities for OTLP and distributed tracing
|
|
10
|
+
# Provides trace/span ID generation and timestamp formatting
|
|
11
|
+
module TraceContext
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
# Generate a W3C Trace Context compliant trace ID (32 hex chars = 128 bits)
|
|
15
|
+
# @return [String] 32 character hex string
|
|
16
|
+
def generate_trace_id
|
|
17
|
+
SecureRandom.hex(16)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Generate a W3C Trace Context compliant span ID (16 hex chars = 64 bits)
|
|
21
|
+
# @return [String] 16 character hex string
|
|
22
|
+
def generate_span_id
|
|
23
|
+
SecureRandom.hex(8)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Convert time to nanoseconds since Unix epoch (OTLP format)
|
|
27
|
+
# @param time [Time] The time to convert
|
|
28
|
+
# @return [Integer] Nanoseconds since Unix epoch
|
|
29
|
+
def timestamp_nanos(time)
|
|
30
|
+
time ||= Time.current
|
|
31
|
+
(time.to_f * 1_000_000_000).to_i
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Convert time to microseconds since Unix epoch (OpenObserve format)
|
|
35
|
+
# @param time [Time] The time to convert
|
|
36
|
+
# @return [Integer] Microseconds since Unix epoch
|
|
37
|
+
def timestamp_micros(time)
|
|
38
|
+
time ||= Time.current
|
|
39
|
+
(time.to_f * 1_000_000).to_i
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Convert time to milliseconds since Unix epoch
|
|
43
|
+
# @param time [Time] The time to convert
|
|
44
|
+
# @return [Integer] Milliseconds since Unix epoch
|
|
45
|
+
def timestamp_millis(time)
|
|
46
|
+
time ||= Time.current
|
|
47
|
+
(time.to_f * 1_000).to_i
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Lapsoss
|
|
7
|
+
module Adapters
|
|
8
|
+
# OpenObserve adapter - sends errors as structured JSON logs
|
|
9
|
+
# OpenObserve is an observability platform that accepts logs via simple JSON API
|
|
10
|
+
# Docs: https://openobserve.ai/docs/ingestion/
|
|
11
|
+
class OpenobserveAdapter < Base
|
|
12
|
+
include Concerns::LevelMapping
|
|
13
|
+
include Concerns::HttpDelivery
|
|
14
|
+
|
|
15
|
+
self.level_mapping_type = :openobserve
|
|
16
|
+
|
|
17
|
+
DEFAULT_STREAM = "errors"
|
|
18
|
+
DEFAULT_ORG = "default"
|
|
19
|
+
|
|
20
|
+
def initialize(name, settings = {})
|
|
21
|
+
super
|
|
22
|
+
|
|
23
|
+
@endpoint = settings[:endpoint].presence || ENV["OPENOBSERVE_ENDPOINT"]
|
|
24
|
+
@username = settings[:username].presence || ENV["OPENOBSERVE_USERNAME"]
|
|
25
|
+
@password = settings[:password].presence || ENV["OPENOBSERVE_PASSWORD"]
|
|
26
|
+
@org = settings[:org].presence || ENV["OPENOBSERVE_ORG"] || DEFAULT_ORG
|
|
27
|
+
@stream = settings[:stream].presence || ENV["OPENOBSERVE_STREAM"] || DEFAULT_STREAM
|
|
28
|
+
|
|
29
|
+
if @endpoint.blank? || @username.blank? || @password.blank?
|
|
30
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss::OpenobserveAdapter] Missing endpoint, username or password - adapter disabled"
|
|
31
|
+
@enabled = false
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
setup_endpoint
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def capture(event)
|
|
39
|
+
deliver(event.scrubbed)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def capabilities
|
|
43
|
+
super.merge(
|
|
44
|
+
breadcrumbs: false,
|
|
45
|
+
code_context: true,
|
|
46
|
+
data_scrubbing: true
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def setup_endpoint
|
|
53
|
+
uri = URI.parse(@endpoint)
|
|
54
|
+
@api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
55
|
+
@api_path = "/api/#{@org}/#{@stream}/_json"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_payload(event)
|
|
59
|
+
# OpenObserve expects JSON array of log entries
|
|
60
|
+
[ build_log_entry(event) ]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_log_entry(event)
|
|
64
|
+
entry = {
|
|
65
|
+
_timestamp: timestamp_microseconds(event.timestamp),
|
|
66
|
+
level: map_level(event.level),
|
|
67
|
+
logger: "lapsoss",
|
|
68
|
+
environment: event.environment.presence || "production",
|
|
69
|
+
service: @settings[:service_name].presence || "rails"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case event.type
|
|
73
|
+
when :exception
|
|
74
|
+
entry.merge!(build_exception_entry(event))
|
|
75
|
+
when :message
|
|
76
|
+
entry[:message] = event.message
|
|
77
|
+
else
|
|
78
|
+
entry[:message] = event.message || "Unknown event"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Add optional context
|
|
82
|
+
entry[:user] = event.user_context if event.user_context.present?
|
|
83
|
+
entry[:tags] = event.tags if event.tags.present?
|
|
84
|
+
entry[:extra] = event.extra if event.extra.present?
|
|
85
|
+
entry[:request] = event.request_context if event.request_context.present?
|
|
86
|
+
entry[:transaction] = event.transaction if event.transaction.present?
|
|
87
|
+
entry[:fingerprint] = event.fingerprint if event.fingerprint.present?
|
|
88
|
+
|
|
89
|
+
entry.compact_blank
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_exception_entry(event)
|
|
93
|
+
entry = {
|
|
94
|
+
message: "#{event.exception_type}: #{event.exception_message}",
|
|
95
|
+
exception_type: event.exception_type,
|
|
96
|
+
exception_message: event.exception_message
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if event.has_backtrace?
|
|
100
|
+
entry[:stacktrace] = format_stacktrace(event)
|
|
101
|
+
entry[:stacktrace_raw] = event.backtrace_frames.map do |frame|
|
|
102
|
+
"#{frame.absolute_path || frame.filename}:#{frame.line_number} in `#{frame.method_name}`"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
entry
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def format_stacktrace(event)
|
|
110
|
+
event.backtrace_frames.map do |frame|
|
|
111
|
+
frame_entry = {
|
|
112
|
+
filename: frame.filename,
|
|
113
|
+
abs_path: frame.absolute_path || frame.filename,
|
|
114
|
+
function: frame.method_name || frame.function,
|
|
115
|
+
lineno: frame.line_number,
|
|
116
|
+
in_app: frame.in_app
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if frame.code_context.present?
|
|
120
|
+
frame_entry[:context_line] = frame.code_context[:context_line]
|
|
121
|
+
frame_entry[:pre_context] = frame.code_context[:pre_context]
|
|
122
|
+
frame_entry[:post_context] = frame.code_context[:post_context]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
frame_entry.compact
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def timestamp_microseconds(time)
|
|
130
|
+
# OpenObserve expects _timestamp in microseconds
|
|
131
|
+
(time.to_f * 1_000_000).to_i
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def serialize_payload(payload)
|
|
135
|
+
json = ActiveSupport::JSON.encode(payload)
|
|
136
|
+
|
|
137
|
+
if json.bytesize >= compress_threshold
|
|
138
|
+
[ ActiveSupport::Gzip.compress(json), true ]
|
|
139
|
+
else
|
|
140
|
+
[ json, false ]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def compress_threshold
|
|
145
|
+
@settings[:compress_threshold] || 1024
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def adapter_specific_headers
|
|
149
|
+
credentials = Base64.strict_encode64("#{@username}:#{@password}")
|
|
150
|
+
{
|
|
151
|
+
"Authorization" => "Basic #{credentials}"
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|