otel_beacon 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +258 -0
- data/UPGRADING.md +60 -0
- data/lib/otel_beacon/breadcrumb.rb +30 -0
- data/lib/otel_beacon/client.rb +296 -0
- data/lib/otel_beacon/context.rb +75 -0
- data/lib/otel_beacon/integrations/sidekiq.rb +62 -0
- data/lib/otel_beacon/log_subscriber.rb +30 -0
- data/lib/otel_beacon/rails/controller_methods.rb +59 -0
- data/lib/otel_beacon/rails/job_methods.rb +49 -0
- data/lib/otel_beacon/rails/logger.rb +120 -0
- data/lib/otel_beacon/rails/mailer_methods.rb +37 -0
- data/lib/otel_beacon/rails/railtie.rb +52 -0
- data/lib/otel_beacon/runtime_context.rb +102 -0
- data/lib/otel_beacon/sanitizer.rb +50 -0
- data/lib/otel_beacon/scope.rb +37 -0
- data/lib/otel_beacon/version.rb +5 -0
- data/lib/otel_beacon.rb +169 -0
- metadata +162 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 87071db6d653ac58e3543e8913fb4fec68ead62da10b082d29df4febed98a9ee
|
|
4
|
+
data.tar.gz: 352b451298dff4b8c68974eb425f1197f590f0b43fad5433b191d04822b8025d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8c09144054ee299d399a2e5f88bc0de27af87383c45920a50ad358307eca420385a7c01f09dd88bd4e41765683b8abe689dc6ee5167d061adb4032317d4b1f45
|
|
7
|
+
data.tar.gz: c785d4c5067ca5cf533d970278c99989f10d4c617fddfa64d45cfa3ad3c004bff3ed5f1665a099dc6928293a4ed725717e9731d464df2ddf63a908627eeea792
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2024-01-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release
|
|
7
|
+
- Sentry-style user context (`set_user`)
|
|
8
|
+
- Tags support (`set_tags`)
|
|
9
|
+
- Extra data support (`set_extra`)
|
|
10
|
+
- Breadcrumbs (`add_breadcrumb`)
|
|
11
|
+
- Structured contexts (`set_context`)
|
|
12
|
+
- Fingerprinting for error grouping (`set_fingerprint`)
|
|
13
|
+
- Scoped context (`with_scope`)
|
|
14
|
+
- Manual exception capture (`capture_exception`)
|
|
15
|
+
- Message capture (`capture_message`)
|
|
16
|
+
- Custom spans (`in_span`)
|
|
17
|
+
- Rails integration (controllers, jobs, mailers)
|
|
18
|
+
- Sidekiq integration
|
|
19
|
+
- Runtime context (Ruby, OS, app, device, browser)
|
|
20
|
+
- Configurable current user detection
|
|
21
|
+
- Parameter sanitization
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# OtelBeacon
|
|
2
|
+
|
|
3
|
+
Sentry-style error tracking and context for OpenTelemetry. Get the familiar Sentry developer experience (breadcrumbs, user context, tags, fingerprinting) with any OpenTelemetry backend like SigNoz, Jaeger, Honeycomb, or Grafana Tempo.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'otel_beacon'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
OtelBeacon.service_name = "my-app"
|
|
17
|
+
OtelBeacon.service_version = "1.0.0"
|
|
18
|
+
OtelBeacon.environment = Rails.env
|
|
19
|
+
OtelBeacon.current_user_method = :current_user
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
User detection supports multiple formats:
|
|
23
|
+
- Symbol: `:current_user` (calls method on controller)
|
|
24
|
+
- String: `"Current.user"` (for CurrentAttributes)
|
|
25
|
+
- Proc: `-> (ctx) { ctx.session[:user] }` (custom logic)
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### User Context
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
OtelBeacon.set_user(
|
|
33
|
+
id: user.id,
|
|
34
|
+
email: user.email,
|
|
35
|
+
username: user.name,
|
|
36
|
+
subscription: "premium"
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Custom attributes beyond id/email/username are also supported.
|
|
41
|
+
|
|
42
|
+
### Tags
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
OtelBeacon.set_tags(
|
|
46
|
+
feature: "checkout",
|
|
47
|
+
priority: "high",
|
|
48
|
+
release: "1.2.3"
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Extra Data
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
OtelBeacon.set_extra(
|
|
56
|
+
cart_id: 123,
|
|
57
|
+
items_count: 5,
|
|
58
|
+
total_amount: 99.99
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Breadcrumbs
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
OtelBeacon.add_breadcrumb(:http, "GET /api/users", status: 200, duration: 150)
|
|
66
|
+
OtelBeacon.add_breadcrumb(:user_action, "Clicked checkout button")
|
|
67
|
+
OtelBeacon.add_breadcrumb(:navigation, "Navigated to /checkout")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Structured Contexts
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
OtelBeacon.set_context(:payment, provider: "stripe", amount: 99.99)
|
|
74
|
+
OtelBeacon.set_context(:feature_flags, dark_mode: true, beta: false)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Fingerprinting (Error Grouping)
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
OtelBeacon.set_fingerprint("payment", "stripe", "card-declined")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Scoped Context
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
OtelBeacon.with_scope do |scope|
|
|
87
|
+
scope.set_tags(feature: "checkout")
|
|
88
|
+
scope.set_extra(cart_id: 123)
|
|
89
|
+
scope.set_fingerprint("checkout", "payment-error")
|
|
90
|
+
process_checkout
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Any exception within the block gets the scoped context. Context is automatically restored after the block.
|
|
95
|
+
|
|
96
|
+
### Manual Exception Capture
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
begin
|
|
100
|
+
risky_operation
|
|
101
|
+
rescue => e
|
|
102
|
+
OtelBeacon.capture_exception(e,
|
|
103
|
+
extra: { operation: "payment" },
|
|
104
|
+
fingerprint: ["payment", "timeout"]
|
|
105
|
+
)
|
|
106
|
+
raise
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Custom Spans
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
OtelBeacon.in_span("payment.process", attributes: { "payment.provider" => "stripe" }) do |span|
|
|
114
|
+
result = process_payment
|
|
115
|
+
span.add_event("payment.complete", attributes: { "payment.id" => result.id })
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Message Capture
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
OtelBeacon.capture_message("User completed onboarding", level: :info, tags: { flow: "signup" })
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Rails Integration
|
|
126
|
+
|
|
127
|
+
OtelBeacon automatically integrates with Rails when the Railtie loads:
|
|
128
|
+
|
|
129
|
+
- **Controllers**: Auto-captures user, request context, and exceptions
|
|
130
|
+
- **Jobs**: Auto-captures job metadata and exceptions
|
|
131
|
+
- **Mailers**: Auto-captures mailer context and exceptions
|
|
132
|
+
|
|
133
|
+
### Configuration
|
|
134
|
+
|
|
135
|
+
**User detection:**
|
|
136
|
+
```ruby
|
|
137
|
+
OtelBeacon.current_user_method = :current_user
|
|
138
|
+
OtelBeacon.user_attributes = {
|
|
139
|
+
id: :id,
|
|
140
|
+
email: :email,
|
|
141
|
+
username: %i[username name]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `user_attributes` mapping tries each method in order until one returns a value.
|
|
146
|
+
|
|
147
|
+
**Sanitization:**
|
|
148
|
+
```ruby
|
|
149
|
+
OtelBeacon.sanitize_fields = %w[password token secret key auth credential api_key access_token]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Limits:**
|
|
153
|
+
```ruby
|
|
154
|
+
OtelBeacon.max_breadcrumbs = 50
|
|
155
|
+
OtelBeacon.max_stacktrace_lines = 20
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Hooks:**
|
|
159
|
+
```ruby
|
|
160
|
+
OtelBeacon.before_capture = ->(event) { ... }
|
|
161
|
+
OtelBeacon.before_breadcrumb = ->(crumb) { ... }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Sidekiq Integration
|
|
165
|
+
|
|
166
|
+
Automatically enabled when Sidekiq is present. For each job, OtelBeacon sets:
|
|
167
|
+
|
|
168
|
+
- **Tags**: job_class, queue, job_id
|
|
169
|
+
- **Context (:sidekiq)**: queue, retry_count, created_at, enqueued_at
|
|
170
|
+
- **On exceptions**: filtered job arguments are captured
|
|
171
|
+
|
|
172
|
+
## Runtime Context
|
|
173
|
+
|
|
174
|
+
Automatically captured at startup:
|
|
175
|
+
- **:runtime** - Ruby version, platform, engine, engine_version
|
|
176
|
+
- **:os** - OS name, version, kernel_version
|
|
177
|
+
- **:app** - Service name, version, environment, Rails version (if Rails)
|
|
178
|
+
|
|
179
|
+
Captured per request (in controllers):
|
|
180
|
+
- **:device** - User agent, IP address
|
|
181
|
+
- **:browser** - Browser name/version (parsed from user agent)
|
|
182
|
+
|
|
183
|
+
## Comparison with Sentry
|
|
184
|
+
|
|
185
|
+
| Feature | Sentry | OtelBeacon |
|
|
186
|
+
|---------|--------|------------|
|
|
187
|
+
| User context | ✅ | ✅ |
|
|
188
|
+
| Tags | ✅ | ✅ |
|
|
189
|
+
| Extra data | ✅ | ✅ |
|
|
190
|
+
| Breadcrumbs | ✅ | ✅ |
|
|
191
|
+
| Fingerprinting | ✅ | ✅ |
|
|
192
|
+
| Scopes | ✅ | ✅ |
|
|
193
|
+
| Contexts | ✅ | ✅ |
|
|
194
|
+
| Rails integration | ✅ | ✅ |
|
|
195
|
+
| Sidekiq integration | ✅ | ✅ |
|
|
196
|
+
| Backend | Sentry.io | Any OTel backend |
|
|
197
|
+
|
|
198
|
+
## Viewing Sentry-style Data in SigNoz
|
|
199
|
+
|
|
200
|
+
OtelBeacon stores all Sentry-style context as OpenTelemetry span attributes and events. Here's how to find them in SigNoz:
|
|
201
|
+
|
|
202
|
+
### Traces View
|
|
203
|
+
|
|
204
|
+
1. Go to **Traces** in SigNoz
|
|
205
|
+
2. Click on any trace/span to see details
|
|
206
|
+
3. Look in the **Attributes** tab for:
|
|
207
|
+
|
|
208
|
+
| Attribute | Description |
|
|
209
|
+
|-----------|-------------|
|
|
210
|
+
| `user.id`, `user.email`, `user.username` | User context |
|
|
211
|
+
| `tag.*` | Custom tags (e.g., `tag.feature`, `tag.priority`) |
|
|
212
|
+
| `extra` | Extra data as JSON |
|
|
213
|
+
| `error.fingerprint` | Custom error grouping |
|
|
214
|
+
| `context.*.* ` | Structured contexts (e.g., `context.payment.provider`) |
|
|
215
|
+
| `environment`, `service.name`, `service.version` | Environment info |
|
|
216
|
+
|
|
217
|
+
### Breadcrumbs
|
|
218
|
+
|
|
219
|
+
Breadcrumbs appear as **span events** named `breadcrumb`:
|
|
220
|
+
|
|
221
|
+
1. In trace details, look at the **Events** section
|
|
222
|
+
2. Each breadcrumb has attributes:
|
|
223
|
+
- `breadcrumb.category` - Category (http, user_action, etc.)
|
|
224
|
+
- `breadcrumb.message` - Description
|
|
225
|
+
- `breadcrumb.level` - Severity (info, warning, error)
|
|
226
|
+
- `breadcrumb.data` - Additional data as JSON
|
|
227
|
+
|
|
228
|
+
### Exceptions
|
|
229
|
+
|
|
230
|
+
Exceptions are recorded with full context:
|
|
231
|
+
|
|
232
|
+
1. Filter traces by `status: error` or look for spans with exceptions
|
|
233
|
+
2. In span details, find:
|
|
234
|
+
- `exception.type` - Exception class name
|
|
235
|
+
- `exception.message` - Error message
|
|
236
|
+
- `exception.stacktrace` - Stack trace
|
|
237
|
+
- All user/tags/extra context attached
|
|
238
|
+
|
|
239
|
+
### Querying in SigNoz
|
|
240
|
+
|
|
241
|
+
Use the query builder to filter by OtelBeacon attributes:
|
|
242
|
+
|
|
243
|
+
| Query | Description |
|
|
244
|
+
|-------|-------------|
|
|
245
|
+
| `user.id = "123" AND status = error` | Find all errors for a specific user |
|
|
246
|
+
| `tag.feature = "checkout"` | Find traces with specific tag |
|
|
247
|
+
| `error.fingerprint CONTAINS "payment"` | Find traces with fingerprint |
|
|
248
|
+
|
|
249
|
+
### Logs View
|
|
250
|
+
|
|
251
|
+
Messages from `capture_message` appear in **Logs** with:
|
|
252
|
+
- `message.text` - Log message
|
|
253
|
+
- `message.level` - Log level
|
|
254
|
+
- All context attributes attached
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Upgrading OtelBeacon
|
|
2
|
+
|
|
3
|
+
## Version 0.1.0
|
|
4
|
+
|
|
5
|
+
This is the initial public release following solid_queue Rails patterns.
|
|
6
|
+
|
|
7
|
+
### Configuration
|
|
8
|
+
|
|
9
|
+
Configuration uses direct attribute setters on the module:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
OtelBeacon.enabled = true
|
|
13
|
+
OtelBeacon.service_name = "my_app"
|
|
14
|
+
OtelBeacon.max_breadcrumbs = 100
|
|
15
|
+
OtelBeacon.sanitize_fields = %w[password token secret]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Rails Configuration
|
|
19
|
+
|
|
20
|
+
In Rails, you can also configure via `config/application.rb` or environment files:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
config.otel_beacon.enabled = true
|
|
24
|
+
config.otel_beacon.service_name = "my_app"
|
|
25
|
+
config.otel_beacon.max_breadcrumbs = 100
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Predicate Methods
|
|
29
|
+
|
|
30
|
+
Boolean configuration options have predicate methods:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
OtelBeacon.enabled?
|
|
34
|
+
OtelBeacon.capture_exceptions?
|
|
35
|
+
OtelBeacon.capture_logs?
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Error Reporting
|
|
39
|
+
|
|
40
|
+
Errors are reported via `Rails.error.report` when running in Rails. The `on_thread_error` callback is available for custom error handling:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
OtelBeacon.on_thread_error = ->(exception) { Sentry.capture_exception(exception) }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Instrumentation
|
|
47
|
+
|
|
48
|
+
ActiveSupport::Notifications events are available:
|
|
49
|
+
|
|
50
|
+
- `capture_exception.otel_beacon`
|
|
51
|
+
- `capture_message.otel_beacon`
|
|
52
|
+
- `add_breadcrumb.otel_beacon`
|
|
53
|
+
|
|
54
|
+
Subscribe to events:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
ActiveSupport::Notifications.subscribe("capture_exception.otel_beacon") do |event|
|
|
58
|
+
puts "Exception captured: #{event.payload[:exception].class}"
|
|
59
|
+
end
|
|
60
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OtelBeacon
|
|
4
|
+
class Breadcrumb
|
|
5
|
+
attr_reader :category, :message, :level, :data, :timestamp
|
|
6
|
+
|
|
7
|
+
def initialize(category, message = nil, level: :info, **data)
|
|
8
|
+
@category = category.to_s
|
|
9
|
+
@message = message
|
|
10
|
+
@level = level.to_s
|
|
11
|
+
@data = data
|
|
12
|
+
@timestamp = Time.now.iso8601
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{ category: @category, message: @message, level: @level, data: @data, timestamp: @timestamp }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def info(category, message = nil, **data) = new(category, message, level: :info, **data)
|
|
21
|
+
def warn(category, message = nil, **data) = new(category, message, level: :warn, **data)
|
|
22
|
+
def error(category, message = nil, **data) = new(category, message, level: :error, **data)
|
|
23
|
+
def debug(category, message = nil, **data) = new(category, message, level: :debug, **data)
|
|
24
|
+
|
|
25
|
+
def build(category:, message: nil, level: :info, data: {}, **extra)
|
|
26
|
+
new(category, message, level: level, **data.merge(extra))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OtelBeacon
|
|
4
|
+
module Client
|
|
5
|
+
class << self
|
|
6
|
+
def reset_context!
|
|
7
|
+
Context.reset!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def set_user(id: nil, email: nil, username: nil, ip_address: nil, **extra)
|
|
11
|
+
Context.current[:user] =
|
|
12
|
+
{ id: id, email: email, username: username, ip_address: ip_address }.compact.merge(extra)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def set_tags(**tags)
|
|
16
|
+
Context.tags.merge!(tags.transform_values(&:to_s))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_extra(**extra)
|
|
20
|
+
Context.extra.merge!(extra)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_context(name, **data)
|
|
24
|
+
Context.contexts[name.to_sym] = data
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def set_fingerprint(*fingerprint)
|
|
28
|
+
Context.fingerprint = fingerprint.flatten
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with_scope
|
|
32
|
+
Context.push_scope
|
|
33
|
+
scope = Scope.new
|
|
34
|
+
yield scope
|
|
35
|
+
ensure
|
|
36
|
+
Context.pop_scope
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_breadcrumb(category_or_crumb, message = nil, level: :info, **data)
|
|
40
|
+
crumb = if category_or_crumb.is_a?(Breadcrumb)
|
|
41
|
+
category_or_crumb.to_h
|
|
42
|
+
else
|
|
43
|
+
Breadcrumb.new(category_or_crumb, message, level: level, **data).to_h
|
|
44
|
+
end
|
|
45
|
+
Context.breadcrumbs << crumb
|
|
46
|
+
Context.breadcrumbs.shift if Context.breadcrumbs.size > OtelBeacon.max_breadcrumbs
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tracer
|
|
50
|
+
OpenTelemetry.tracer_provider.tracer(OtelBeacon.service_name, OtelBeacon::VERSION)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def in_span(name, attributes: {}, record_exception: true)
|
|
54
|
+
tracer.in_span(name, attributes: attributes) do |span|
|
|
55
|
+
yield span
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
if record_exception
|
|
58
|
+
span.record_exception(e)
|
|
59
|
+
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
60
|
+
end
|
|
61
|
+
raise
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def capture_message(message, level: :info, tags: nil, extra: nil, user: nil, request: nil, fingerprint: nil)
|
|
66
|
+
return unless OtelBeacon.enabled?
|
|
67
|
+
return unless otel_logger_available?
|
|
68
|
+
|
|
69
|
+
attrs = build_message_attributes(
|
|
70
|
+
message: message, level: level, tags: tags, extra: extra,
|
|
71
|
+
user: user, request: request, fingerprint: fingerprint
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
current_span = OpenTelemetry::Trace.current_span
|
|
75
|
+
emit_breadcrumbs_to_span(current_span) if current_span&.recording?
|
|
76
|
+
|
|
77
|
+
otel_logger.on_emit(
|
|
78
|
+
severity_text: level.to_s.upcase,
|
|
79
|
+
body: message,
|
|
80
|
+
attributes: attrs,
|
|
81
|
+
context: OpenTelemetry::Context.current
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
OtelBeacon.instrument(:capture_message, message: message, level: level)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def capture_exception(exception, request: nil, params: nil, user: nil, tags: nil, extra: nil, fingerprint: nil)
|
|
89
|
+
return unless OtelBeacon.enabled?
|
|
90
|
+
|
|
91
|
+
current_span = OpenTelemetry::Trace.current_span
|
|
92
|
+
return unless current_span&.recording?
|
|
93
|
+
|
|
94
|
+
exc_attrs = build_exception_attributes(
|
|
95
|
+
exception: exception, request: request, params: params,
|
|
96
|
+
user: user, tags: tags, extra: extra, fingerprint: fingerprint
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
set_span_attributes(current_span, exc_attrs)
|
|
100
|
+
emit_breadcrumbs_to_span(current_span)
|
|
101
|
+
current_span.record_exception(exception, attributes: exc_attrs)
|
|
102
|
+
current_span.status = OpenTelemetry::Trace::Status.error(exception.message)
|
|
103
|
+
|
|
104
|
+
log_exception(exception, exc_attrs)
|
|
105
|
+
OtelBeacon.instrument(:capture_exception, exception: exception)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def record_event(name, attributes: {})
|
|
109
|
+
current_span = OpenTelemetry::Trace.current_span
|
|
110
|
+
return unless current_span&.recording?
|
|
111
|
+
|
|
112
|
+
current_span.add_event(name, attributes: attributes)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def flush_context_to_span
|
|
116
|
+
current_span = OpenTelemetry::Trace.current_span
|
|
117
|
+
return unless current_span&.recording?
|
|
118
|
+
|
|
119
|
+
attrs = {}
|
|
120
|
+
add_user_attributes(attrs, nil)
|
|
121
|
+
add_tag_attributes(attrs, nil)
|
|
122
|
+
add_extra_attributes(attrs, nil)
|
|
123
|
+
add_context_attributes(attrs)
|
|
124
|
+
add_environment_attributes(attrs)
|
|
125
|
+
|
|
126
|
+
set_span_attributes(current_span, attrs)
|
|
127
|
+
emit_breadcrumbs_to_span(current_span)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def otel_logger_available?
|
|
133
|
+
OpenTelemetry.logger_provider&.respond_to?(:logger)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def otel_logger
|
|
137
|
+
OpenTelemetry.logger_provider.logger(name: OtelBeacon.service_name, version: OtelBeacon::VERSION)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def emit_breadcrumbs_to_span(span)
|
|
141
|
+
Context.breadcrumbs.each_with_index do |crumb, idx|
|
|
142
|
+
span.add_event("breadcrumb", attributes: {
|
|
143
|
+
"breadcrumb.index" => idx,
|
|
144
|
+
"breadcrumb.category" => crumb[:category],
|
|
145
|
+
"breadcrumb.message" => crumb[:message],
|
|
146
|
+
"breadcrumb.level" => crumb[:level],
|
|
147
|
+
"breadcrumb.timestamp" => crumb[:timestamp],
|
|
148
|
+
"breadcrumb.data" => crumb[:data].to_json
|
|
149
|
+
})
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_message_attributes(message:, level:, tags: nil, extra: nil, user: nil, request: nil, fingerprint: nil)
|
|
154
|
+
attrs = { "message.text" => message, "message.level" => level.to_s }
|
|
155
|
+
attrs["message.fingerprint"] = fingerprint.join(":") if fingerprint.is_a?(Array)
|
|
156
|
+
|
|
157
|
+
add_request_attributes(attrs, request) if request
|
|
158
|
+
add_user_attributes(attrs, user)
|
|
159
|
+
add_tag_attributes(attrs, tags)
|
|
160
|
+
add_extra_attributes(attrs, extra)
|
|
161
|
+
add_environment_attributes(attrs)
|
|
162
|
+
|
|
163
|
+
attrs.compact
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build_exception_attributes(exception:, request: nil, params: nil, user: nil, tags: nil, extra: nil,
|
|
167
|
+
fingerprint: nil)
|
|
168
|
+
attrs = {
|
|
169
|
+
"exception.type" => exception.class.name,
|
|
170
|
+
"exception.message" => exception.message
|
|
171
|
+
}
|
|
172
|
+
if exception.backtrace
|
|
173
|
+
attrs["exception.stacktrace"] = exception.backtrace.first(OtelBeacon.max_stacktrace_lines).join("\n")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
add_request_attributes(attrs, request) if request
|
|
177
|
+
add_params_attributes(attrs, params) if params
|
|
178
|
+
add_user_attributes(attrs, user)
|
|
179
|
+
add_tag_attributes(attrs, tags)
|
|
180
|
+
add_extra_attributes(attrs, extra)
|
|
181
|
+
add_context_attributes(attrs)
|
|
182
|
+
add_fingerprint_attributes(attrs, fingerprint)
|
|
183
|
+
add_environment_attributes(attrs)
|
|
184
|
+
attrs["service.version"] = OtelBeacon.service_version
|
|
185
|
+
|
|
186
|
+
attrs.compact
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def add_request_attributes(attrs, request)
|
|
190
|
+
attrs["http.method"] = begin
|
|
191
|
+
request.request_method
|
|
192
|
+
rescue StandardError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
attrs["http.url"] = begin
|
|
196
|
+
request.original_url
|
|
197
|
+
rescue StandardError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
attrs["http.host"] = begin
|
|
201
|
+
request.host
|
|
202
|
+
rescue StandardError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
attrs["http.path"] = begin
|
|
206
|
+
request.path
|
|
207
|
+
rescue StandardError
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
attrs["http.user_agent"] = begin
|
|
211
|
+
request.user_agent
|
|
212
|
+
rescue StandardError
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
attrs["http.referer"] = begin
|
|
216
|
+
request.referer
|
|
217
|
+
rescue StandardError
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
attrs["client.ip"] = begin
|
|
221
|
+
request.remote_ip
|
|
222
|
+
rescue StandardError
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
attrs["http.request_id"] = begin
|
|
226
|
+
request.request_id
|
|
227
|
+
rescue StandardError
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def add_params_attributes(attrs, params)
|
|
233
|
+
sanitized = Sanitizer.extract_params(params)
|
|
234
|
+
attrs["request.params"] = sanitized.to_json unless sanitized.empty?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def add_user_attributes(attrs, user)
|
|
238
|
+
merged = Context.user.merge(user || {})
|
|
239
|
+
return if merged.empty?
|
|
240
|
+
|
|
241
|
+
attrs["user.id"] = merged[:id].to_s if merged[:id]
|
|
242
|
+
attrs["user.email"] = merged[:email] if merged[:email]
|
|
243
|
+
attrs["user.username"] = merged[:username] if merged[:username]
|
|
244
|
+
attrs["user.ip_address"] = merged[:ip_address] if merged[:ip_address]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def add_tag_attributes(attrs, tags)
|
|
248
|
+
merged = Context.tags.merge(tags || {})
|
|
249
|
+
merged.each { |k, v| attrs["tag.#{k}"] = v.to_s }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def add_extra_attributes(attrs, extra)
|
|
253
|
+
merged = Context.extra.merge(extra || {})
|
|
254
|
+
attrs["extra"] = merged.to_json unless merged.empty?
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def add_context_attributes(attrs)
|
|
258
|
+
Context.contexts.each do |name, data|
|
|
259
|
+
data.each { |k, v| attrs["context.#{name}.#{k}"] = v.to_s }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def add_fingerprint_attributes(attrs, fingerprint)
|
|
264
|
+
fp = fingerprint || Context.fingerprint
|
|
265
|
+
attrs["error.fingerprint"] = fp.join(":") if fp.is_a?(Array) && fp.any?
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def add_environment_attributes(attrs)
|
|
269
|
+
attrs["environment"] = OtelBeacon.environment.to_s
|
|
270
|
+
attrs["service.name"] = OtelBeacon.service_name
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def set_span_attributes(span, attrs)
|
|
274
|
+
attrs.each do |key, value|
|
|
275
|
+
next if value.nil?
|
|
276
|
+
|
|
277
|
+
span.set_attribute(key.to_s, value.to_s)
|
|
278
|
+
rescue StandardError
|
|
279
|
+
next
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def log_exception(exception, attributes)
|
|
284
|
+
return unless otel_logger_available?
|
|
285
|
+
|
|
286
|
+
otel_logger.on_emit(
|
|
287
|
+
severity_text: "ERROR",
|
|
288
|
+
body: "#{exception.class}: #{exception.message}",
|
|
289
|
+
attributes: attributes,
|
|
290
|
+
context: OpenTelemetry::Context.current
|
|
291
|
+
)
|
|
292
|
+
rescue StandardError
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|