launchdarkly-observability 0.2.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 +31 -0
- data/LICENSE.txt +190 -0
- data/README.md +685 -0
- data/lib/launchdarkly_observability/hook.rb +199 -0
- data/lib/launchdarkly_observability/middleware.rb +116 -0
- data/lib/launchdarkly_observability/opentelemetry_config.rb +272 -0
- data/lib/launchdarkly_observability/otel_log_bridge.rb +108 -0
- data/lib/launchdarkly_observability/plugin.rb +133 -0
- data/lib/launchdarkly_observability/rails.rb +141 -0
- data/lib/launchdarkly_observability/source_context.rb +112 -0
- data/lib/launchdarkly_observability/version.rb +5 -0
- data/lib/launchdarkly_observability.rb +181 -0
- metadata +200 -0
data/README.md
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
# LaunchDarkly Observability Plugin for Ruby
|
|
2
|
+
|
|
3
|
+
OpenTelemetry-based observability instrumentation for the LaunchDarkly Ruby SDK with full Rails support.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This plugin automatically instruments LaunchDarkly feature flag evaluations with OpenTelemetry traces and logs, providing visibility into:
|
|
8
|
+
|
|
9
|
+
- Flag evaluation timing and results
|
|
10
|
+
- Evaluation reasons and rule matches
|
|
11
|
+
- Context information (user/organization)
|
|
12
|
+
- Error tracking for failed evaluations
|
|
13
|
+
- Correlation with HTTP requests in Rails applications
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'launchdarkly-observability'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then execute:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install it yourself as:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install launchdarkly-observability
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Dependencies
|
|
36
|
+
|
|
37
|
+
The gem includes everything needed for traces and logs out of the box:
|
|
38
|
+
- `launchdarkly-server-sdk` >= 8.0
|
|
39
|
+
- `opentelemetry-sdk` ~> 1.4
|
|
40
|
+
- `opentelemetry-exporter-otlp` ~> 0.28
|
|
41
|
+
- `opentelemetry-instrumentation-all` ~> 0.62
|
|
42
|
+
- `opentelemetry-logs-sdk` ~> 0.1
|
|
43
|
+
- `opentelemetry-exporter-otlp-logs` ~> 0.1
|
|
44
|
+
|
|
45
|
+
For metrics support (optional):
|
|
46
|
+
- `opentelemetry-metrics-sdk` ~> 0.1
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### Basic Usage (Non-Rails)
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
require 'launchdarkly-server-sdk'
|
|
54
|
+
require 'launchdarkly_observability'
|
|
55
|
+
|
|
56
|
+
# Create observability plugin (SDK key and environment automatically inferred)
|
|
57
|
+
observability = LaunchDarklyObservability::Plugin.new
|
|
58
|
+
|
|
59
|
+
# Initialize LaunchDarkly client with plugin
|
|
60
|
+
config = LaunchDarkly::Config.new(plugins: [observability])
|
|
61
|
+
client = LaunchDarkly::LDClient.new('your-sdk-key', config)
|
|
62
|
+
|
|
63
|
+
# Flag evaluations are now automatically instrumented
|
|
64
|
+
context = LaunchDarkly::LDContext.create({ key: 'user-123', kind: 'user' })
|
|
65
|
+
value = client.variation('my-feature-flag', context, false)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **Note**: The plugin automatically extracts the SDK key from the LaunchDarkly client during initialization. The backend derives both the project and environment from the SDK key for telemetry routing, so you don't need to configure them explicitly.
|
|
69
|
+
|
|
70
|
+
### Rails Usage
|
|
71
|
+
|
|
72
|
+
Create an initializer at `config/initializers/launchdarkly.rb`:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
require 'launchdarkly-server-sdk'
|
|
76
|
+
require 'launchdarkly_observability'
|
|
77
|
+
|
|
78
|
+
# Setup observability plugin (SDK key and environment automatically inferred)
|
|
79
|
+
observability = LaunchDarklyObservability::Plugin.new(
|
|
80
|
+
service_name: 'my-rails-app',
|
|
81
|
+
service_version: '1.0.0'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Initialize LaunchDarkly client using Rails configuration
|
|
85
|
+
config = LaunchDarkly::Config.new(plugins: [observability])
|
|
86
|
+
Rails.configuration.ld_client = LaunchDarkly::LDClient.new(
|
|
87
|
+
ENV['LAUNCHDARKLY_SDK_KEY'],
|
|
88
|
+
config
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Ensure clean shutdown
|
|
92
|
+
at_exit { Rails.configuration.ld_client.close }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use in controllers:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class ApplicationController < ActionController::Base
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Helper method for accessing the LaunchDarkly client
|
|
102
|
+
def ld_client
|
|
103
|
+
Rails.configuration.ld_client
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def current_ld_context
|
|
107
|
+
@current_ld_context ||= LaunchDarkly::LDContext.create({
|
|
108
|
+
key: current_user&.id || 'anonymous',
|
|
109
|
+
kind: 'user',
|
|
110
|
+
email: current_user&.email,
|
|
111
|
+
name: current_user&.name
|
|
112
|
+
})
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class HomeController < ApplicationController
|
|
117
|
+
def index
|
|
118
|
+
# This evaluation is automatically traced and correlated with the HTTP request
|
|
119
|
+
@show_new_feature = ld_client.variation('new-feature', current_ld_context, false)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
### Plugin Options
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
LaunchDarklyObservability::Plugin.new(
|
|
130
|
+
# All parameters are optional - SDK key and environment are automatically inferred
|
|
131
|
+
|
|
132
|
+
# Optional: Custom OTLP endpoint (default: LaunchDarkly's endpoint)
|
|
133
|
+
otlp_endpoint: 'https://otel.observability.app.launchdarkly.com:4318',
|
|
134
|
+
|
|
135
|
+
# Optional: Environment override (default: inferred from SDK key)
|
|
136
|
+
# Only specify for advanced scenarios like deployment-specific suffixes
|
|
137
|
+
environment: 'production-canary',
|
|
138
|
+
|
|
139
|
+
# Optional: Service identification
|
|
140
|
+
service_name: 'my-service',
|
|
141
|
+
service_version: '1.0.0',
|
|
142
|
+
|
|
143
|
+
# Optional: Enable/disable signal types
|
|
144
|
+
enable_traces: true, # default: true
|
|
145
|
+
enable_logs: true, # default: true
|
|
146
|
+
enable_metrics: true, # default: true
|
|
147
|
+
|
|
148
|
+
# Optional: Custom instrumentation configuration
|
|
149
|
+
instrumentations: {
|
|
150
|
+
'OpenTelemetry::Instrumentation::Rails' => { enable_recognize_route: true },
|
|
151
|
+
'OpenTelemetry::Instrumentation::ActiveRecord' => { db_statement: :include }
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
> **Advanced**: You can explicitly pass `sdk_key` or `project_id` for testing scenarios, but this is rarely needed since they're automatically extracted from the client.
|
|
157
|
+
|
|
158
|
+
### Environment Variables
|
|
159
|
+
|
|
160
|
+
You can configure via environment variables:
|
|
161
|
+
|
|
162
|
+
| Variable | Description |
|
|
163
|
+
|----------|-------------|
|
|
164
|
+
| `LAUNCHDARKLY_SDK_KEY` | LaunchDarkly SDK key (automatically extracted from client during initialization) |
|
|
165
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Custom OTLP endpoint |
|
|
166
|
+
| `OTEL_SERVICE_NAME` | Service name (if not specified in plugin options) |
|
|
167
|
+
|
|
168
|
+
> **Note**: The environment associated with your SDK key is automatically determined by the backend, so you don't need to configure it separately.
|
|
169
|
+
|
|
170
|
+
## Telemetry Details
|
|
171
|
+
|
|
172
|
+
### Cross-SDK Compatibility
|
|
173
|
+
|
|
174
|
+
This Ruby SDK is designed for compatibility with other LaunchDarkly observability SDKs (Android, Node.js, Python, Go, .NET). Key compatibility features:
|
|
175
|
+
|
|
176
|
+
- **Span name**: `"evaluation"` (consistent across all SDKs)
|
|
177
|
+
- **Event name**: `"feature_flag"` (matches Android and Node SDKs)
|
|
178
|
+
- **Provider name**: `"LaunchDarkly"` (consistent across all SDKs)
|
|
179
|
+
- **Attribute naming**: Follows OpenTelemetry semantic conventions
|
|
180
|
+
|
|
181
|
+
### Span Attributes
|
|
182
|
+
|
|
183
|
+
Each flag evaluation creates a span with the following attributes, following [OpenTelemetry semantic conventions for feature flags](https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-events/):
|
|
184
|
+
|
|
185
|
+
### Standard Semantic Convention Attributes
|
|
186
|
+
|
|
187
|
+
| Attribute | Status | Description | Example |
|
|
188
|
+
|-----------|--------|-------------|---------|
|
|
189
|
+
| `feature_flag.key` | Release Candidate | Flag key | `"my-feature"` |
|
|
190
|
+
| `feature_flag.provider.name` | Release Candidate | Provider name | `"LaunchDarkly"` |
|
|
191
|
+
| `feature_flag.result.value` | Release Candidate | Evaluated value | `"true"` |
|
|
192
|
+
| `feature_flag.result.variant` | Release Candidate | Variation index | `"1"` |
|
|
193
|
+
| `feature_flag.result.reason` | Release Candidate | Evaluation reason | `"default"`, `"targeting_match"`, `"error"` |
|
|
194
|
+
| `feature_flag.context.id` | Release Candidate | Context identifier | `"user-123"` |
|
|
195
|
+
| `error.type` | Stable | Error type (when applicable) | `"flag_not_found"` |
|
|
196
|
+
| `error.message` | Development | Error message (when applicable) | `"Flag evaluation error: FLAG_NOT_FOUND"` |
|
|
197
|
+
|
|
198
|
+
### LaunchDarkly-Specific Attributes
|
|
199
|
+
|
|
200
|
+
These custom attributes provide additional LaunchDarkly-specific details:
|
|
201
|
+
|
|
202
|
+
| Attribute | Description | Example |
|
|
203
|
+
|-----------|-------------|---------|
|
|
204
|
+
| `launchdarkly.context.kind` | Context kind | `"user"` |
|
|
205
|
+
| `launchdarkly.context.key` | Context key | `"user-123"` |
|
|
206
|
+
| `launchdarkly.reason.kind` | LaunchDarkly reason kind | `"FALLTHROUGH"`, `"RULE_MATCH"`, `"ERROR"` |
|
|
207
|
+
| `launchdarkly.reason.rule_index` | Rule index (for RULE_MATCH) | `0` |
|
|
208
|
+
| `launchdarkly.reason.rule_id` | Rule ID (for RULE_MATCH) | `"rule-key"` |
|
|
209
|
+
| `launchdarkly.reason.prerequisite_key` | Prerequisite key (for PREREQUISITE_FAILED) | `"other-flag"` |
|
|
210
|
+
| `launchdarkly.reason.in_experiment` | In experiment flag | `true` |
|
|
211
|
+
| `launchdarkly.reason.error_kind` | LaunchDarkly error kind (for ERROR) | `"FLAG_NOT_FOUND"` |
|
|
212
|
+
| `launchdarkly.evaluation.duration_ms` | Evaluation time in milliseconds | `0.5` |
|
|
213
|
+
| `launchdarkly.evaluation.method` | SDK method called | `"variation"`, `"variation_detail"` |
|
|
214
|
+
|
|
215
|
+
### Feature Flag Event
|
|
216
|
+
|
|
217
|
+
In addition to span attributes, each evaluation adds a `"feature_flag"` event to the span. This matches the pattern used by other LaunchDarkly observability SDKs (Android, Node.js) and follows OpenTelemetry semantic conventions for feature flag events.
|
|
218
|
+
|
|
219
|
+
The event contains the core evaluation data:
|
|
220
|
+
|
|
221
|
+
| Event Attribute | Description | Example |
|
|
222
|
+
|-----------------|-------------|---------|
|
|
223
|
+
| `feature_flag.key` | Flag key | `"my-feature"` |
|
|
224
|
+
| `feature_flag.provider.name` | Provider name | `"LaunchDarkly"` |
|
|
225
|
+
| `feature_flag.context.id` | Context identifier | `"user-123"` |
|
|
226
|
+
| `feature_flag.result.value` | Evaluated value | `"true"` |
|
|
227
|
+
| `feature_flag.result.variant` | Variation index | `"1"` |
|
|
228
|
+
| `feature_flag.result.reason` | Evaluation reason | `"default"` |
|
|
229
|
+
| `launchdarkly.reason.in_experiment` | In experiment flag (if applicable) | `true` |
|
|
230
|
+
|
|
231
|
+
**Why both span attributes and events?**
|
|
232
|
+
|
|
233
|
+
- **Span attributes** provide detailed context for the entire evaluation span, including timing, method, and LaunchDarkly-specific details
|
|
234
|
+
- **Span events** represent a point-in-time record of the evaluation result, which is the standard OpenTelemetry pattern for feature flag evaluations
|
|
235
|
+
- This dual approach matches other LaunchDarkly SDKs and maximizes compatibility with observability backends
|
|
236
|
+
|
|
237
|
+
### Error Tracking
|
|
238
|
+
|
|
239
|
+
When evaluation errors occur, the plugin follows [OpenTelemetry error semantic conventions](https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-events/):
|
|
240
|
+
|
|
241
|
+
- **`error.type`**: Mapped from LaunchDarkly error kinds to standard values (`flag_not_found`, `type_mismatch`, `provider_not_ready`, `general`)
|
|
242
|
+
- **`error.message`**: Human-readable error description
|
|
243
|
+
- **`feature_flag.result.reason`**: Set to `"error"`
|
|
244
|
+
- **`launchdarkly.reason.error_kind`**: Original LaunchDarkly error kind (`FLAG_NOT_FOUND`, `WRONG_TYPE`, etc.)
|
|
245
|
+
|
|
246
|
+
The span status is also set to `ERROR` with a descriptive message.
|
|
247
|
+
|
|
248
|
+
#### Error Type Mapping
|
|
249
|
+
|
|
250
|
+
| LaunchDarkly Error | OpenTelemetry `error.type` |
|
|
251
|
+
|-------------------|----------------------------|
|
|
252
|
+
| `FLAG_NOT_FOUND` | `flag_not_found` |
|
|
253
|
+
| `WRONG_TYPE` | `type_mismatch` |
|
|
254
|
+
| `CLIENT_NOT_READY` | `provider_not_ready` |
|
|
255
|
+
| `MALFORMED_FLAG` | `parse_error` |
|
|
256
|
+
| Others | `general` |
|
|
257
|
+
|
|
258
|
+
### Rails Integration
|
|
259
|
+
|
|
260
|
+
When used with Rails, the plugin provides:
|
|
261
|
+
|
|
262
|
+
1. **Rack Middleware**: Automatically traces HTTP requests and provides context propagation
|
|
263
|
+
2. **Controller Helpers**: Convenient methods for custom tracing
|
|
264
|
+
3. **View Helpers**: Generate traceparent meta tags for client-side correlation
|
|
265
|
+
|
|
266
|
+
#### Controller Helpers
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
class MyController < ApplicationController
|
|
270
|
+
def index
|
|
271
|
+
# Get current trace ID for logging
|
|
272
|
+
trace_id = launchdarkly_trace_id
|
|
273
|
+
Rails.logger.info "Processing request with trace: #{trace_id}"
|
|
274
|
+
|
|
275
|
+
# Create custom spans
|
|
276
|
+
with_launchdarkly_span('custom-operation', attributes: { 'custom.key' => 'value' }) do |span|
|
|
277
|
+
# Your code here
|
|
278
|
+
span.set_attribute('result', 'success')
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def create
|
|
283
|
+
# Record exceptions
|
|
284
|
+
begin
|
|
285
|
+
process_something
|
|
286
|
+
rescue => e
|
|
287
|
+
record_launchdarkly_exception(e)
|
|
288
|
+
raise
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### View Helpers
|
|
295
|
+
|
|
296
|
+
```erb
|
|
297
|
+
<head>
|
|
298
|
+
<%= launchdarkly_traceparent_meta_tag %>
|
|
299
|
+
</head>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
This generates:
|
|
303
|
+
```html
|
|
304
|
+
<meta name="traceparent" content="00-abc123...-def456...-01">
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Auto-Instrumentation
|
|
308
|
+
|
|
309
|
+
By default, the plugin enables OpenTelemetry auto-instrumentation for common Ruby libraries:
|
|
310
|
+
|
|
311
|
+
- **Rails**: Request tracing, route recognition
|
|
312
|
+
- **ActiveRecord**: Database query tracing
|
|
313
|
+
- **Net::HTTP**: Outbound HTTP request tracing
|
|
314
|
+
- **Rack**: Request/response tracing
|
|
315
|
+
- **Redis**: Cache operation tracing
|
|
316
|
+
- **Sidekiq**: Background job tracing
|
|
317
|
+
|
|
318
|
+
### Customizing Instrumentations
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
LaunchDarklyObservability::Plugin.new(
|
|
322
|
+
instrumentations: {
|
|
323
|
+
# Disable specific instrumentations
|
|
324
|
+
'OpenTelemetry::Instrumentation::Redis' => { enabled: false },
|
|
325
|
+
|
|
326
|
+
# Configure instrumentations
|
|
327
|
+
'OpenTelemetry::Instrumentation::ActiveRecord' => {
|
|
328
|
+
db_statement: :obfuscate, # Mask sensitive data
|
|
329
|
+
obfuscation_limit: 2000
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
# Skip certain endpoints
|
|
333
|
+
'OpenTelemetry::Instrumentation::Rack' => {
|
|
334
|
+
untraced_endpoints: ['/health', '/metrics']
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Manual Instrumentation
|
|
341
|
+
|
|
342
|
+
### Creating Custom Spans
|
|
343
|
+
|
|
344
|
+
The LaunchDarkly Observability plugin provides a clean API matching OpenTelemetry conventions for creating custom spans:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
# Simple span creation
|
|
348
|
+
LaunchDarklyObservability.in_span('database-query') do |span|
|
|
349
|
+
span.set_attribute('db.table', 'users')
|
|
350
|
+
span.set_attribute('db.operation', 'select')
|
|
351
|
+
|
|
352
|
+
# Your code here
|
|
353
|
+
results = execute_query
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# With initial attributes
|
|
357
|
+
LaunchDarklyObservability.in_span('api-call', attributes: { 'api.endpoint' => '/users' }) do |span|
|
|
358
|
+
response = make_api_call
|
|
359
|
+
span.set_attribute('api.status', response.code)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Nested spans
|
|
363
|
+
LaunchDarklyObservability.in_span('process-order') do |outer_span|
|
|
364
|
+
outer_span.set_attribute('order.id', order_id)
|
|
365
|
+
|
|
366
|
+
LaunchDarklyObservability.in_span('validate-payment') do |inner_span|
|
|
367
|
+
validate_payment(order)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
LaunchDarklyObservability.in_span('update-inventory') do |inner_span|
|
|
371
|
+
update_inventory(order)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Non-Rails Examples
|
|
377
|
+
|
|
378
|
+
The module-level methods work in any Ruby application:
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
# Sinatra example
|
|
382
|
+
require 'sinatra'
|
|
383
|
+
require 'launchdarkly_observability'
|
|
384
|
+
|
|
385
|
+
# Create a logger that writes to stdout AND exports via OTLP.
|
|
386
|
+
# Must be called after LDClient.new so the OTel logger provider is ready.
|
|
387
|
+
$logger = LaunchDarklyObservability.logger
|
|
388
|
+
|
|
389
|
+
$logger.info 'App booted' # stdout + OTLP log record
|
|
390
|
+
$logger.info(user: 'alice', action: 'login') # hash keys become OTel attributes
|
|
391
|
+
|
|
392
|
+
get '/users/:id' do
|
|
393
|
+
LaunchDarklyObservability.in_span('fetch-user', attributes: { 'user.id' => params[:id] }) do |span|
|
|
394
|
+
user = User.find(params[:id])
|
|
395
|
+
span.set_attribute('user.name', user.name)
|
|
396
|
+
$logger.info "Fetched user #{user.name}" # correlated with the active span
|
|
397
|
+
user.to_json
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Plain Ruby script
|
|
402
|
+
LaunchDarklyObservability.in_span('data-processing') do |span|
|
|
403
|
+
files = Dir.glob('data/*.csv')
|
|
404
|
+
span.set_attribute('files.count', files.length)
|
|
405
|
+
|
|
406
|
+
files.each do |file|
|
|
407
|
+
LaunchDarklyObservability.in_span('process-file', attributes: { 'file.name' => file }) do |file_span|
|
|
408
|
+
process_csv(file)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Recording Exceptions
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
LaunchDarklyObservability.in_span('risky-operation') do |span|
|
|
418
|
+
begin
|
|
419
|
+
perform_operation
|
|
420
|
+
rescue StandardError => e
|
|
421
|
+
LaunchDarklyObservability.record_exception(e, attributes: {
|
|
422
|
+
'retry_count' => 3,
|
|
423
|
+
'operation_id' => operation_id
|
|
424
|
+
})
|
|
425
|
+
raise
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Getting Current Trace ID
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
# Get the current trace ID for logging or debugging
|
|
434
|
+
trace_id = LaunchDarklyObservability.current_trace_id
|
|
435
|
+
logger.info "Processing request: #{trace_id}"
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Logging with Trace Context
|
|
439
|
+
|
|
440
|
+
In **Rails** applications, `Rails.logger` is automatically bridged to the OpenTelemetry
|
|
441
|
+
Logs pipeline. Every log entry is exported as an OTLP LogRecord with the active
|
|
442
|
+
trace and span IDs attached for correlation.
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
Rails.logger.info "Processing flag evaluation" # Automatically includes trace_id, span_id
|
|
446
|
+
Rails.logger.warn "Slow query detected" # Same correlation, different severity
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
In **non-Rails** applications (Sinatra, Grape, plain Ruby), call
|
|
450
|
+
`LaunchDarklyObservability.logger` after the LaunchDarkly client is initialized.
|
|
451
|
+
The returned logger writes to stdout (or any IO you pass) and exports every
|
|
452
|
+
entry as an OTLP LogRecord with trace/span correlation.
|
|
453
|
+
|
|
454
|
+
```ruby
|
|
455
|
+
$logger = LaunchDarklyObservability.logger # defaults to $stdout
|
|
456
|
+
$logger = LaunchDarklyObservability.logger($stderr) # or any IO
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
To disable log export while keeping traces, pass `enable_logs: false`:
|
|
460
|
+
|
|
461
|
+
```ruby
|
|
462
|
+
plugin = LaunchDarklyObservability::Plugin.new(enable_logs: false)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Comparison: Plugin API vs Raw OpenTelemetry
|
|
466
|
+
|
|
467
|
+
#### Using the Plugin API (Recommended)
|
|
468
|
+
|
|
469
|
+
The plugin API matches OpenTelemetry's naming but eliminates boilerplate:
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
LaunchDarklyObservability.in_span('operation', attributes: { 'key' => 'value' }) do |span|
|
|
473
|
+
# Your code
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
LaunchDarklyObservability.record_exception(error)
|
|
477
|
+
LaunchDarklyObservability.current_trace_id
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Using Raw OpenTelemetry API
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
tracer = OpenTelemetry.tracer_provider.tracer('my-component', '1.0.0')
|
|
484
|
+
|
|
485
|
+
tracer.in_span('operation', attributes: { 'key' => 'value' }) do |span|
|
|
486
|
+
# Your code
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
span = OpenTelemetry::Trace.current_span
|
|
490
|
+
span.record_exception(error)
|
|
491
|
+
span.status = OpenTelemetry::Trace::Status.error(error.message)
|
|
492
|
+
|
|
493
|
+
span = OpenTelemetry::Trace.current_span
|
|
494
|
+
span.context.hex_trace_id if span&.context&.valid?
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The plugin API provides the same familiar `in_span` method name while eliminating boilerplate.
|
|
498
|
+
|
|
499
|
+
### Best Practices
|
|
500
|
+
|
|
501
|
+
1. **Use descriptive span names**: Use kebab-case names that describe the operation
|
|
502
|
+
```ruby
|
|
503
|
+
LaunchDarklyObservability.in_span('validate-payment') # Good
|
|
504
|
+
LaunchDarklyObservability.in_span('do_stuff') # Bad
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
2. **Add meaningful attributes**: Include relevant context as span attributes
|
|
508
|
+
```ruby
|
|
509
|
+
LaunchDarklyObservability.in_span('database-query', attributes: {
|
|
510
|
+
'db.table' => 'users',
|
|
511
|
+
'db.operation' => 'select',
|
|
512
|
+
'db.rows_returned' => results.count
|
|
513
|
+
})
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
3. **Always re-raise exceptions**: After recording an exception, re-raise it unless you're handling it
|
|
517
|
+
```ruby
|
|
518
|
+
rescue => e
|
|
519
|
+
LaunchDarklyObservability.record_exception(e)
|
|
520
|
+
raise # Important!
|
|
521
|
+
end
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
4. **Keep spans focused**: Create separate spans for distinct operations rather than one large span
|
|
525
|
+
```ruby
|
|
526
|
+
# Good - separate spans
|
|
527
|
+
LaunchDarklyObservability.in_span('fetch-data') { fetch }
|
|
528
|
+
LaunchDarklyObservability.in_span('process-data') { process }
|
|
529
|
+
|
|
530
|
+
# Bad - one large span
|
|
531
|
+
LaunchDarklyObservability.in_span('fetch-and-process') do
|
|
532
|
+
fetch
|
|
533
|
+
process
|
|
534
|
+
end
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
5. **Include trace IDs in logs**: Use `current_trace_id` for log correlation
|
|
538
|
+
```ruby
|
|
539
|
+
trace_id = LaunchDarklyObservability.current_trace_id
|
|
540
|
+
Rails.logger.info "Starting processing [trace: #{trace_id}]"
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Troubleshooting
|
|
544
|
+
|
|
545
|
+
### Spans Not Appearing
|
|
546
|
+
|
|
547
|
+
1. Verify the OTLP endpoint is accessible:
|
|
548
|
+
```ruby
|
|
549
|
+
puts LaunchDarklyObservability.instance&.otlp_endpoint
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
2. Check if OpenTelemetry is configured:
|
|
553
|
+
```ruby
|
|
554
|
+
puts OpenTelemetry.tracer_provider.class
|
|
555
|
+
# Should be: OpenTelemetry::SDK::Trace::TracerProvider
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
3. Ensure the plugin is registered:
|
|
559
|
+
```ruby
|
|
560
|
+
puts LaunchDarklyObservability.instance&.registered?
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Missing Flag Evaluations
|
|
564
|
+
|
|
565
|
+
Verify the hook is receiving evaluations by checking logs:
|
|
566
|
+
```ruby
|
|
567
|
+
# Set environment variable for debug output
|
|
568
|
+
ENV['OTEL_LOG_LEVEL'] = 'debug'
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Rails Middleware Not Active
|
|
572
|
+
|
|
573
|
+
Ensure the gem is loaded in your Gemfile and the initializer runs before controllers:
|
|
574
|
+
```ruby
|
|
575
|
+
# Gemfile
|
|
576
|
+
gem 'launchdarkly-observability'
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## Testing
|
|
580
|
+
|
|
581
|
+
When testing, you may want to use an in-memory exporter:
|
|
582
|
+
|
|
583
|
+
```ruby
|
|
584
|
+
# test/test_helper.rb
|
|
585
|
+
require 'opentelemetry/sdk'
|
|
586
|
+
|
|
587
|
+
class ActiveSupport::TestCase
|
|
588
|
+
setup do
|
|
589
|
+
@exporter = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
|
|
590
|
+
OpenTelemetry::SDK.configure do |c|
|
|
591
|
+
c.add_span_processor(
|
|
592
|
+
OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(@exporter)
|
|
593
|
+
)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
teardown do
|
|
598
|
+
@exporter.reset
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def finished_spans
|
|
602
|
+
@exporter.finished_spans
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## API Reference
|
|
608
|
+
|
|
609
|
+
### LaunchDarklyObservability Module
|
|
610
|
+
|
|
611
|
+
#### `LaunchDarklyObservability.init`
|
|
612
|
+
|
|
613
|
+
Initialize the plugin (alternative to creating `Plugin` directly).
|
|
614
|
+
|
|
615
|
+
#### `LaunchDarklyObservability.initialized?`
|
|
616
|
+
|
|
617
|
+
Returns `true` if the plugin has been initialized.
|
|
618
|
+
|
|
619
|
+
#### `LaunchDarklyObservability.in_span(name, attributes: {})`
|
|
620
|
+
|
|
621
|
+
Creates a new span and executes the given block within its context. Matches the OpenTelemetry `tracer.in_span` API.
|
|
622
|
+
|
|
623
|
+
**Parameters:**
|
|
624
|
+
- `name` (String): The name of the span
|
|
625
|
+
- `attributes` (Hash): Optional initial attributes for the span
|
|
626
|
+
|
|
627
|
+
**Yields:** `span` (OpenTelemetry::Trace::Span) -- the created span object
|
|
628
|
+
|
|
629
|
+
**Returns:** The result of the block
|
|
630
|
+
|
|
631
|
+
#### `LaunchDarklyObservability.record_exception(exception, attributes: {})`
|
|
632
|
+
|
|
633
|
+
Records an exception in the current span and sets the span status to error.
|
|
634
|
+
|
|
635
|
+
**Parameters:**
|
|
636
|
+
- `exception` (Exception): The exception to record
|
|
637
|
+
- `attributes` (Hash): Optional additional attributes
|
|
638
|
+
|
|
639
|
+
#### `LaunchDarklyObservability.current_trace_id`
|
|
640
|
+
|
|
641
|
+
Returns the current trace ID in hexadecimal format.
|
|
642
|
+
|
|
643
|
+
**Returns:** String (32 hex characters) or `nil` if no active span
|
|
644
|
+
|
|
645
|
+
#### `LaunchDarklyObservability.flush`
|
|
646
|
+
|
|
647
|
+
Flushes all pending telemetry data to the configured exporter.
|
|
648
|
+
|
|
649
|
+
#### `LaunchDarklyObservability.shutdown`
|
|
650
|
+
|
|
651
|
+
Flushes pending data and stops the plugin.
|
|
652
|
+
|
|
653
|
+
### Plugin Class
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
# SDK key and environment are automatically inferred
|
|
657
|
+
plugin = LaunchDarklyObservability::Plugin.new(service_name: 'my-service')
|
|
658
|
+
|
|
659
|
+
plugin.project_id # => nil (extracted from client during registration)
|
|
660
|
+
plugin.otlp_endpoint # => 'https://otel...'
|
|
661
|
+
plugin.environment # => nil (inferred from SDK key by backend)
|
|
662
|
+
plugin.registered? # => false (true after client initialization)
|
|
663
|
+
plugin.flush # Flush pending data
|
|
664
|
+
plugin.shutdown # Stop the plugin
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## Contributing
|
|
668
|
+
|
|
669
|
+
1. Fork the repository
|
|
670
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
671
|
+
3. Write tests for your changes
|
|
672
|
+
4. Run tests (`bundle exec rake test`)
|
|
673
|
+
5. Commit your changes (`git commit -am 'Add amazing feature'`)
|
|
674
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
675
|
+
7. Open a Pull Request
|
|
676
|
+
|
|
677
|
+
## License
|
|
678
|
+
|
|
679
|
+
This project is licensed under the Apache 2.0 License - see the [LICENSE.txt](LICENSE.txt) file for details.
|
|
680
|
+
|
|
681
|
+
## Support
|
|
682
|
+
|
|
683
|
+
- [LaunchDarkly Documentation](https://docs.launchdarkly.com)
|
|
684
|
+
- [OpenTelemetry Ruby Documentation](https://opentelemetry.io/docs/instrumentation/ruby/)
|
|
685
|
+
- [GitHub Issues](https://github.com/launchdarkly/observability-sdk/issues)
|