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.
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)