launchdarkly-observability 0.2.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d46f86b88f2c7b3dfd876ef07b2135e8d11f8939c66a1071246ebb627b387f0d
4
- data.tar.gz: 28d3c8ab27674a1e4ca3996c805d97136782198b2da0a96e8f64902d49ed1f4e
3
+ metadata.gz: cd8ffbd0f163139ada7ca0908969dbdc7f5b4b011c76e297bcf6c5c7c50cf438
4
+ data.tar.gz: 72ea24f86f9e22a0af99ee8ea84d175f29d9c8a9dc87451daaa31e770d5d111d
5
5
  SHA512:
6
- metadata.gz: cd64fc216a9af53ec0c78bc4af429edf051e0da37b8568e4611d78bd0863711d2ca0b94fb8de97c39b30de4f0828c4eb2a874ce5148e0491f0ba320f716ad96f
7
- data.tar.gz: 40114279e622a08632d68b63f5faebb4f77230cfe72f8a7a723b2a27898f81bf1c7a2179204068ebb3c178451339f8de7e0ca52840a77fe8d68c9e0721151663
6
+ metadata.gz: 42fe80077d434efa30061e01608781323ab215a36b49d1852326158025837ca04541605534c6d1ed52c707598795ba66889194bf3dac6f2f1aa8d8cc475ae1af
7
+ data.tar.gz: af53b3d86a7ad13b0f47b67a96ef14ae85706f7575c4b15fafe61524c0b1ec00b7ee9d9a8b986ee6ebd0790d50fe33c00cad07d49a1f21b98d06de7c52503708
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0](https://github.com/launchdarkly/observability-sdk/compare/launchdarkly-observability-ruby/0.2.2...launchdarkly-observability-ruby/0.3.0) (2026-06-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * **ruby:** boot-time OTel auto-instrumentation; keep it working on Rails 7.0 ([#643](https://github.com/launchdarkly/observability-sdk/issues/643)) ([350d549](https://github.com/launchdarkly/observability-sdk/commit/350d54994ff3745aba3d8f9a4c8602ce18dc6807))
14
+
8
15
  ## [0.2.2](https://github.com/launchdarkly/observability-sdk/compare/launchdarkly-observability-ruby/0.2.1...launchdarkly-observability-ruby/0.2.2) (2026-06-22)
9
16
 
10
17
 
data/README.md CHANGED
@@ -38,13 +38,43 @@ The gem includes everything needed for traces and logs out of the box:
38
38
  - `launchdarkly-server-sdk` >= 8.11.0 (plugin support was added in 8.11.0)
39
39
  - `opentelemetry-sdk` ~> 1.4
40
40
  - `opentelemetry-exporter-otlp` ~> 0.28
41
- - `opentelemetry-instrumentation-all` ~> 0.62
42
41
  - `opentelemetry-logs-sdk` ~> 0.1
43
42
  - `opentelemetry-exporter-otlp-logs` ~> 0.1
43
+ - A curated set of individual `opentelemetry-instrumentation-*` gems (see [Ruby & Rails compatibility](#ruby--rails-compatibility))
44
44
 
45
45
  For metrics support (optional):
46
46
  - `opentelemetry-metrics-sdk` ~> 0.1
47
47
 
48
+ ### Ruby & Rails compatibility
49
+
50
+ | Component | Supported |
51
+ |-----------|-----------|
52
+ | Ruby | >= 3.0 |
53
+ | Rails | >= 7.0 (auto-instrumentation); Rack / Sinatra / other Rack apps are unaffected by Rails version |
54
+
55
+ The plugin depends on **individual** `opentelemetry-instrumentation-*` gems rather
56
+ than the `opentelemetry-instrumentation-all` meta-gem. The meta-gem couples every
57
+ instrumentation to a single version, so when the Rails-family instrumentations
58
+ raised their minimum to Rails 7.1, Rails 7.0 apps silently lost *all*
59
+ auto-instrumentation. Listing gems individually keeps the Rails family
60
+ (`opentelemetry-instrumentation-rails`, `-action_pack`, `-active_record`,
61
+ `-active_support`, `-action_view`, `-active_job`, `-action_mailer`,
62
+ `-active_storage`) pinned below the releases that require Rails 7.1, while every
63
+ other instrumentation tracks the latest version. Those pinned releases are still
64
+ compatible with Rails 7.1+, so modern apps are unaffected.
65
+
66
+ If an instrumentation cannot attach on your framework version, the plugin emits a
67
+ **single actionable warning** (instead of a flurry of "failed to install" lines)
68
+ and keeps everything else working — flag-eval spans, manual instrumentation, logs,
69
+ and error capture are unaffected. You can add any other
70
+ `opentelemetry-instrumentation-*` gem to your own Gemfile and it is picked up
71
+ automatically (the plugin activates every instrumentation that is loaded), or pin
72
+ one to a framework-compatible release, e.g.:
73
+
74
+ ```ruby
75
+ gem 'opentelemetry-instrumentation-rails', '~> 0.41'
76
+ ```
77
+
48
78
  ## Quick Start
49
79
 
50
80
  ### Basic Usage (Non-Rails)
@@ -92,6 +122,15 @@ Rails.configuration.ld_client = LaunchDarkly::LDClient.new(
92
122
  at_exit { Rails.configuration.ld_client.close }
93
123
  ```
94
124
 
125
+ > **Lazy client initialization:** Creating the LaunchDarkly client during boot
126
+ > (as above) is recommended. If your app instead creates the client lazily —
127
+ > e.g. from a model on first request, after Rails has finished booting — the gem
128
+ > still installs the Rails auto-instrumentation during boot via its Railtie, as
129
+ > long as `LAUNCHDARKLY_SDK_KEY` is set in the environment before Rails boots. If
130
+ > it isn't, the OpenTelemetry Rails instrumentations can't attach (their load
131
+ > hooks have already fired) and the plugin logs a single warning explaining how
132
+ > to fix it.
133
+
95
134
  Use in controllers:
96
135
 
97
136
  ```ruby
@@ -145,10 +184,12 @@ LaunchDarklyObservability::Plugin.new(
145
184
  enable_logs: true, # default: true
146
185
  enable_metrics: true, # default: true
147
186
 
148
- # Optional: Custom instrumentation configuration
187
+ # Optional: Custom instrumentation configuration. Keys are instrumentation
188
+ # class names; values are that instrumentation's own options (only pass
189
+ # options the installed instrumentation version supports).
149
190
  instrumentations: {
150
- 'OpenTelemetry::Instrumentation::Rails' => { enable_recognize_route: true },
151
- 'OpenTelemetry::Instrumentation::ActiveRecord' => { db_statement: :include }
191
+ 'OpenTelemetry::Instrumentation::Rack' => { untraced_endpoints: ['/health'] },
192
+ 'OpenTelemetry::Instrumentation::Net::HTTP' => { untraced_hosts: ['internal.example.com'] }
152
193
  }
153
194
  )
154
195
  ```
@@ -308,12 +349,17 @@ This generates:
308
349
 
309
350
  By default, the plugin enables OpenTelemetry auto-instrumentation for common Ruby libraries:
310
351
 
311
- - **Rails**: Request tracing, route recognition
312
- - **ActiveRecord**: Database query tracing
313
- - **Net::HTTP**: Outbound HTTP request tracing
352
+ - **Rails** (and the Action/Active family): Request tracing, route recognition, DB queries, view rendering, jobs
353
+ - **Net::HTTP**, **HTTP**, **Faraday**: Outbound HTTP request tracing
314
354
  - **Rack**: Request/response tracing
355
+ - **PG**, **MySQL2**: Database query tracing
315
356
  - **Redis**: Cache operation tracing
316
357
  - **Sidekiq**: Background job tracing
358
+ - **GraphQL**: Query/field tracing
359
+ - **Sinatra**: Request tracing
360
+
361
+ Need another library instrumented? Add its `opentelemetry-instrumentation-*` gem
362
+ to your Gemfile — the plugin activates every loaded instrumentation automatically.
317
363
 
318
364
  ### Customizing Instrumentations
319
365
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point matching the gem name (`launchdarkly-observability`) so that
4
+ # Bundler.require auto-loads the gem during application boot. Without this,
5
+ # Bundler's default `require 'launchdarkly-observability'` fails (the real entry
6
+ # point is the underscore file) and users must require the gem manually — often
7
+ # in an initializer, which is too late for the Rails Railtie to install
8
+ # auto-instrumentation during boot.
9
+ require_relative 'launchdarkly_observability'
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LaunchDarklyObservability
4
+ # Wraps the OpenTelemetry logger to suppress the per-instrumentation install
5
+ # chatter ("... was successfully installed" / "... failed to install") and
6
+ # record the names of instrumentations that failed to install. Everything else
7
+ # (including level / level=) is delegated to the real logger.
8
+ #
9
+ # OpenTelemetryConfig uses this to replace the SDK's flurry of
10
+ # per-instrumentation warnings with a single actionable summary.
11
+ class InstrumentationLogFilter
12
+ FAILED_PATTERN = /Instrumentation: (\S+) failed to install/
13
+
14
+ # Run the block with OpenTelemetry.logger swapped for a filter that
15
+ # suppresses per-instrumentation install chatter, returning the names of any
16
+ # instrumentations that reported "failed to install". The SDK installs
17
+ # instrumentations after a configure block returns, so wrap the whole
18
+ # OpenTelemetry::SDK.configure call — not just use_all.
19
+ def self.capture_failures
20
+ original = OpenTelemetry.logger
21
+ failed = []
22
+ OpenTelemetry.logger = new(original, failed)
23
+ yield
24
+ failed
25
+ ensure
26
+ OpenTelemetry.logger = original
27
+ end
28
+
29
+ # Build ONE actionable warning naming the instrumentations that could not
30
+ # attach and how to resolve it. Telemetry that does not depend on those
31
+ # instrumentations (flag-eval spans, manual instrumentation, logs, errors)
32
+ # keeps working regardless.
33
+ def self.failure_warning(failed)
34
+ names = failed.map { |n| n.sub('OpenTelemetry::Instrumentation::', '') }.uniq
35
+ rails = defined?(::Rails) && ::Rails.respond_to?(:version) ? " on Rails #{::Rails.version}" : ''
36
+ "[LaunchDarklyObservability] #{names.size} OpenTelemetry instrumentation(s) could not attach" \
37
+ "#{rails} (Ruby #{RUBY_VERSION}): #{names.join(', ')}. Those libraries will not be " \
38
+ 'auto-instrumented; flag-eval spans, manual instrumentation, logs and error capture are ' \
39
+ 'unaffected. This usually means an instrumentation gem dropped support for your framework ' \
40
+ 'version — upgrade the framework, or pin the instrumentation gem to a compatible release ' \
41
+ '(e.g. gem "opentelemetry-instrumentation-rails", "~> 0.41").'
42
+ end
43
+
44
+ def initialize(delegate, failed)
45
+ @delegate = delegate
46
+ @failed = failed
47
+ end
48
+
49
+ # OTel logs installs via OpenTelemetry.logger.info / .warn, so intercept the
50
+ # level methods (not just #add) — otherwise the calls fall through to
51
+ # method_missing and bypass the filter.
52
+ %i[debug info warn error fatal unknown].each do |level|
53
+ define_method(level) do |message = nil, &block|
54
+ forward?(message || block&.call) ? @delegate.public_send(level, message, &block) : true
55
+ end
56
+ end
57
+
58
+ def add(severity, message = nil, progname = nil, &block)
59
+ forward?(message || progname || block&.call) ? @delegate.add(severity, message, progname, &block) : true
60
+ end
61
+
62
+ def method_missing(name, ...)
63
+ @delegate.send(name, ...)
64
+ end
65
+
66
+ def respond_to_missing?(name, include_private = false)
67
+ @delegate.respond_to?(name, include_private) || super
68
+ end
69
+
70
+ private
71
+
72
+ # Returns false when the message is install chatter that should be suppressed
73
+ # (recording failed-instrumentation names as a side effect), true when it
74
+ # should be forwarded to the real logger.
75
+ def forward?(message)
76
+ text = message.to_s
77
+ if (match = text.match(FAILED_PATTERN))
78
+ @failed << match[1]
79
+ return false
80
+ end
81
+
82
+ !text.include?('was successfully installed')
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loads the OpenTelemetry auto-instrumentations the plugin enables by default.
4
+ #
5
+ # We require INDIVIDUAL instrumentation gems instead of
6
+ # `opentelemetry/instrumentation/all` on purpose. The meta-gem couples every
7
+ # instrumentation to a single version, so when the Rails-family instrumentations
8
+ # raised their minimum to Rails 7.1 (opentelemetry-instrumentation-rails 0.42.0),
9
+ # the whole bundle moved with them and Rails 7.0 apps silently lost ALL
10
+ # auto-instrumentation. Requiring gems individually lets the Rails family stay on
11
+ # a Rails-7.0-compatible release (pinned in the gemspec) while everything else
12
+ # tracks the latest.
13
+ #
14
+ # `OpenTelemetry::SDK#use_all` activates every instrumentation that has been
15
+ # loaded, so any additional `opentelemetry-instrumentation-*` gem a consumer adds
16
+ # to their own Gemfile is picked up automatically alongside these defaults.
17
+
18
+ # Rails family. Requiring this pulls every Rails-family instrumentation —
19
+ # action_mailer, action_pack, action_view, active_job, active_record,
20
+ # active_storage and active_support — which is why each is capped in the gemspec
21
+ # (the Rails 7.0 floor has to hold on all of them, not just the rails meta-gem).
22
+ # It also pulls concurrent_ruby, which tracks latest and is required explicitly
23
+ # below.
24
+ require 'opentelemetry/instrumentation/rails'
25
+
26
+ # Common non-Rails instrumentations (latest; see gemspec for version policy).
27
+ # Note the require paths differ from gem names in places, e.g. the
28
+ # opentelemetry-instrumentation-net_http gem is required as 'net/http'.
29
+ %w[
30
+ concurrent_ruby
31
+ faraday
32
+ graphql
33
+ http
34
+ mysql2
35
+ net/http
36
+ pg
37
+ rack
38
+ redis
39
+ sidekiq
40
+ sinatra
41
+ ].each do |path|
42
+ require "opentelemetry/instrumentation/#{path}"
43
+ rescue LoadError => e
44
+ # A default instrumentation gem is unexpectedly absent. Don't abort the whole
45
+ # plugin over one missing instrumentation; the rest still load.
46
+ warn "[LaunchDarklyObservability] optional instrumentation '#{path}' not loaded: #{e.message}"
47
+ end
@@ -2,8 +2,9 @@
2
2
 
3
3
  require 'opentelemetry/sdk'
4
4
  require 'opentelemetry/exporter/otlp'
5
- require 'opentelemetry/instrumentation/all'
6
5
  require 'opentelemetry/semantic_conventions'
6
+ require_relative 'instrumentations'
7
+ require_relative 'instrumentation_log_filter'
7
8
 
8
9
  module LaunchDarklyObservability
9
10
  # Configures OpenTelemetry SDK with appropriate providers and exporters
@@ -61,6 +62,8 @@ module LaunchDarklyObservability
61
62
  def configure
62
63
  return if @configured
63
64
 
65
+ warn_ignored_boot_options if LaunchDarklyObservability.instrumentation_installed_at_boot?
66
+
64
67
  configure_traces if @options.fetch(:enable_traces, true)
65
68
  configure_logs if @options.fetch(:enable_logs, true)
66
69
  configure_metrics if @options.fetch(:enable_metrics, true)
@@ -70,6 +73,20 @@ module LaunchDarklyObservability
70
73
  @configured = true
71
74
  end
72
75
 
76
+ # Install the SDK tracer provider and auto-instrumentation WITHOUT exporters.
77
+ #
78
+ # Called from the Rails Railtie during boot so the Rails-family
79
+ # instrumentations (which patch via ActiveSupport.on_load hooks that fire
80
+ # during boot) attach even when the LaunchDarkly client — and therefore
81
+ # #register / #configure — is created lazily afterward. Exporters are added
82
+ # later by #configure_traces when the client registers the plugin.
83
+ def install_instrumentation_only
84
+ configure_sdk_capturing_failures do |c|
85
+ c.resource = create_resource
86
+ configure_instrumentations(c)
87
+ end
88
+ end
89
+
73
90
  # Flush all pending telemetry data
74
91
  def flush
75
92
  OpenTelemetry.tracer_provider&.force_flush
@@ -90,24 +107,93 @@ module LaunchDarklyObservability
90
107
 
91
108
  private
92
109
 
93
- # Configure OpenTelemetry traces with OTLP exporter
110
+ # Configure OpenTelemetry traces with OTLP exporter.
111
+ #
112
+ # If auto-instrumentation was already installed during Rails boot (see
113
+ # LaunchDarklyObservability.install_rails_instrumentation), the SDK tracer
114
+ # provider already exists with instrumentation attached — so we add the OTLP
115
+ # span exporter and refresh its resource, rather than re-running
116
+ # OpenTelemetry::SDK.configure (which would replace the provider and, in the
117
+ # lazy-init case, drop the Rails-family instrumentation that can only attach
118
+ # during boot).
94
119
  def configure_traces
95
- OpenTelemetry::SDK.configure do |c|
120
+ if LaunchDarklyObservability.instrumentation_installed_at_boot?
121
+ provider = OpenTelemetry.tracer_provider
122
+ provider.add_span_processor(create_batch_span_processor)
123
+
124
+ # The boot-time install built the provider's resource before this plugin
125
+ # (and its service_name/service_version options) existed, so it carries
126
+ # the inferred service name. configure_logs/configure_metrics below build
127
+ # a fresh resource from those options; without updating the trace
128
+ # resource too, spans would report a different service identity than logs
129
+ # and metrics. OTel exposes no resource setter, but the provider reads
130
+ # @resource live when creating each span — so update it in place.
131
+ provider.instance_variable_set(:@resource, create_resource)
132
+ return
133
+ end
134
+
135
+ configure_sdk_capturing_failures do |c|
96
136
  c.resource = create_resource
97
137
  c.add_span_processor(create_batch_span_processor)
98
138
 
99
139
  # Enable auto-instrumentation
100
140
  configure_instrumentations(c)
101
141
  end
142
+
143
+ warn_if_rails_instrumentation_missed
144
+ end
145
+
146
+ # Emit a single actionable warning when the plugin is registered after Rails
147
+ # has finished booting and boot-time instrumentation install did not run
148
+ # (e.g. LAUNCHDARKLY_SDK_KEY was not set in the environment at boot). In that
149
+ # case the OTel Rails-family instrumentations log a flurry of
150
+ # "failed to install" warnings because their load hooks have already fired.
151
+ def warn_if_rails_instrumentation_missed
152
+ return unless defined?(::Rails) && ::Rails.respond_to?(:application)
153
+ return unless ::Rails.application.respond_to?(:initialized?) && ::Rails.application.initialized?
154
+
155
+ warn '[LaunchDarklyObservability] The LaunchDarkly client was created after Rails finished ' \
156
+ 'booting, so the Rails auto-instrumentation (ActionPack, ActiveRecord, ...) could not be ' \
157
+ 'installed. To enable it, set LAUNCHDARKLY_SDK_KEY in the environment before Rails boots, ' \
158
+ 'or create the LaunchDarkly client from a config/initializer.'
159
+ end
160
+
161
+ # Emit a single actionable warning when the client is created lazily (so
162
+ # instrumentation was installed at boot, before this plugin existed) but was
163
+ # given options that can only take effect at install time. Custom
164
+ # `instrumentations` config and `enable_traces: false` cannot be applied
165
+ # retroactively: an OTel instrumentation patches its library during boot via
166
+ # ActiveSupport.on_load hooks and cannot be reconfigured or detached
167
+ # afterward. (service_name/service_version are the exception — the trace
168
+ # resource is refreshed in #configure_traces because it is read live per
169
+ # span.) To honor these options, create the client from a config/initializer
170
+ # so #configure runs during boot with them.
171
+ def warn_ignored_boot_options
172
+ ignored = []
173
+ ignored << 'instrumentations' unless @options.fetch(:instrumentations, {}).empty?
174
+ ignored << 'enable_traces: false' unless @options.fetch(:enable_traces, true)
175
+ return if ignored.empty?
176
+
177
+ warn '[LaunchDarklyObservability] Rails auto-instrumentation was installed at boot, so these ' \
178
+ "plugin option(s) cannot be applied and will be ignored: #{ignored.join(', ')}. Instrumentations " \
179
+ 'attach during boot, before the client exists, and cannot be reconfigured or detached afterward. ' \
180
+ 'To apply these options, create the LaunchDarkly client from a config/initializer instead of lazily.'
102
181
  end
103
182
 
104
183
  # Configure auto-instrumentations with sensible defaults.
105
184
  # User-provided instrumentation config is merged on top of defaults,
106
185
  # so users only need to specify the instrumentations they want to override.
107
186
  def configure_instrumentations(config)
187
+ # Only pass options that the instrumentations actually accept. Unknown
188
+ # options are not fatal but emit a warning on every boot ("ignored the
189
+ # following unknown configuration options [...]"):
190
+ # - `enable_recognize_route` is not an option on the Rails, Rack, or
191
+ # ActionPack instrumentations; route-based span naming (http.route) is
192
+ # handled automatically by the ActionPack instrumentation.
193
+ # - ActiveRecord has no `db_statement` option; SQL capture comes from the
194
+ # database adapter instrumentations (Mysql2, PG, ...) which default to
195
+ # obfuscating statements.
108
196
  defaults = {
109
- 'OpenTelemetry::Instrumentation::Rails' => { enable_recognize_route: true },
110
- 'OpenTelemetry::Instrumentation::ActiveRecord' => { db_statement: :include },
111
197
  'OpenTelemetry::Instrumentation::Net::HTTP' => { untraced_hosts: [] },
112
198
  'OpenTelemetry::Instrumentation::Rack' => { untraced_endpoints: ['/health', '/healthz', '/ready'] }
113
199
  }
@@ -118,6 +204,20 @@ module LaunchDarklyObservability
118
204
  warn "[LaunchDarklyObservability] Error configuring instrumentations: #{e.message}"
119
205
  end
120
206
 
207
+ # Run an OpenTelemetry::SDK.configure block, replacing the SDK's
208
+ # per-instrumentation install logging (a flurry of "Instrumentation: <X>
209
+ # failed to install" WARN lines when instrumentations are incompatible with
210
+ # the framework version — e.g. the Rails family below its Rails floor) with a
211
+ # single actionable summary. The SDK installs the instrumentations AFTER the
212
+ # configure block returns (use_all only queues them), so the filter wraps the
213
+ # whole call, not just use_all.
214
+ def configure_sdk_capturing_failures(&block)
215
+ failed = InstrumentationLogFilter.capture_failures do
216
+ OpenTelemetry::SDK.configure(&block)
217
+ end
218
+ warn InstrumentationLogFilter.failure_warning(failed) unless failed.empty?
219
+ end
220
+
121
221
  # Configure OpenTelemetry logs with OTLP exporter.
122
222
  # The log gems are runtime dependencies, so require should always succeed.
123
223
  # If anything goes wrong, we warn once and leave traces unaffected.
@@ -127,8 +127,22 @@ module LaunchDarklyObservability
127
127
  warn "[LaunchDarklyObservability] Could not attach log bridge to Rails.logger: #{e.message}"
128
128
  end
129
129
 
130
+ # The availability check is inlined here rather than delegating to
131
+ # LaunchDarklyObservability.otel_logger_provider_available? on purpose.
132
+ # rails.rb is required from launchdarkly_observability.rb *before* that
133
+ # file's `class << self` block (which defines the module method) has run.
134
+ # When the gem is lazily required after Rails has booted, the
135
+ # `config.after_initialize` hook above executes synchronously while this
136
+ # file is still loading, so the module method does not exist yet and the
137
+ # delegation raised "undefined method `otel_logger_provider_available?'".
130
138
  def otel_logger_provider_available?
131
- LaunchDarklyObservability.send(:otel_logger_provider_available?)
139
+ # `defined?` returns nil (not false) when the constant is absent, so
140
+ # guard first and return an explicit boolean — callers (and the
141
+ # railtie test) expect true/false, never nil.
142
+ return false unless defined?(OpenTelemetry::SDK::Logs::LoggerProvider)
143
+
144
+ OpenTelemetry.respond_to?(:logger_provider) &&
145
+ OpenTelemetry.logger_provider.is_a?(OpenTelemetry::SDK::Logs::LoggerProvider)
132
146
  end
133
147
  end
134
148
 
@@ -136,6 +150,20 @@ module LaunchDarklyObservability
136
150
  app.middleware.insert_before(0, LaunchDarklyObservability::Middleware)
137
151
  end
138
152
 
153
+ # Install OpenTelemetry auto-instrumentation during boot so the Rails-family
154
+ # instrumentations attach even when the LaunchDarkly client is created
155
+ # lazily (after boot) rather than in an initializer.
156
+ #
157
+ # Runs `after: :load_config_initializers` on purpose: if the client was
158
+ # created in a config/initializer during boot, it has already configured
159
+ # OpenTelemetry (with the user's options), so `install_rails_instrumentation`
160
+ # detects the existing provider and no-ops. Otherwise it installs the
161
+ # default instrumentation now — still during boot, before eager loading and
162
+ # the ActiveSupport.on_load hooks that the instrumentations depend on.
163
+ initializer 'launchdarkly_observability.install_instrumentation', after: :load_config_initializers do
164
+ LaunchDarklyObservability.install_rails_instrumentation
165
+ end
166
+
139
167
  config.after_initialize do
140
168
  if defined?(ActionController::Base)
141
169
  ActionController::Base.include(LaunchDarklyObservability::ControllerHelpers)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LaunchDarklyObservability
4
- VERSION = '0.2.2' # x-release-please-version
4
+ VERSION = '0.3.0' # x-release-please-version
5
5
  end
@@ -2,9 +2,13 @@
2
2
 
3
3
  require 'opentelemetry/sdk'
4
4
  require 'opentelemetry/exporter/otlp'
5
- require 'opentelemetry/instrumentation/all'
6
5
  require 'opentelemetry/semantic_conventions'
7
6
 
7
+ # Loads the individual OpenTelemetry instrumentation gems (not the
8
+ # opentelemetry-instrumentation-all meta-gem) so the Rails family can be pinned
9
+ # for old-Rails compatibility independently of everything else.
10
+ require_relative 'launchdarkly_observability/instrumentations'
11
+
8
12
  require_relative 'launchdarkly_observability/version'
9
13
  require_relative 'launchdarkly_observability/hook'
10
14
  require_relative 'launchdarkly_observability/opentelemetry_config'
@@ -13,7 +17,13 @@ require_relative 'launchdarkly_observability/source_context'
13
17
 
14
18
  require_relative 'launchdarkly_observability/middleware'
15
19
  require_relative 'launchdarkly_observability/otel_log_bridge'
16
- require_relative 'launchdarkly_observability/rails'
20
+
21
+ # NOTE: rails.rb is required at the *bottom* of this file, after the
22
+ # LaunchDarklyObservability module body has been fully defined. Its Railtie
23
+ # registers a `config.after_initialize` hook that runs synchronously when the
24
+ # gem is required lazily after Rails has booted. That hook references module
25
+ # constants and `class << self` methods, so they must already exist when the
26
+ # require runs. See the require at the end of this file.
17
27
 
18
28
  module LaunchDarklyObservability
19
29
  # Default OTLP endpoint for LaunchDarkly Observability
@@ -170,6 +180,60 @@ module LaunchDarklyObservability
170
180
  @instance = nil
171
181
  end
172
182
 
183
+ # @return [Boolean] whether auto-instrumentation was installed during Rails
184
+ # boot by {.install_rails_instrumentation}. When true, the plugin attaches
185
+ # exporters to the existing tracer provider at register time instead of
186
+ # reconfiguring it.
187
+ def instrumentation_installed_at_boot?
188
+ @instrumentation_installed_at_boot == true
189
+ end
190
+
191
+ # Install OpenTelemetry auto-instrumentation during Rails boot.
192
+ #
193
+ # The OTel Rails-family instrumentations (ActionPack, ActiveRecord, ...) patch
194
+ # via ActiveSupport.on_load hooks that fire while Rails is booting. If the
195
+ # LaunchDarkly client is created lazily (e.g. from a model on first request),
196
+ # the plugin's #register runs after those hooks have fired and the
197
+ # instrumentations report "failed to install". The Rails Railtie calls this
198
+ # during boot so instrumentation attaches regardless of when the client is
199
+ # created. Exporters are still configured later, when the client registers
200
+ # the plugin.
201
+ #
202
+ # The project_id needed for the resource is resolved from the
203
+ # LAUNCHDARKLY_SDK_KEY environment variable, which is present at boot in the
204
+ # common case even when the client object is created lazily. If it cannot be
205
+ # resolved, instrumentation is left to #register (which warns if it then runs
206
+ # after boot).
207
+ #
208
+ # @param project_id [String, nil] explicit project id; falls back to ENV
209
+ # @param otlp_endpoint [String] OTLP endpoint (only relevant for exporters)
210
+ # @param options [Hash] additional OpenTelemetryConfig options
211
+ # @return [Boolean] whether instrumentation was installed
212
+ def install_rails_instrumentation(project_id: nil, otlp_endpoint: DEFAULT_ENDPOINT, **options)
213
+ return false if @instrumentation_installed_at_boot
214
+
215
+ project_id ||= ENV.fetch('LAUNCHDARKLY_SDK_KEY', nil)
216
+ return false if project_id.nil? || project_id.empty?
217
+
218
+ # If something already configured an SDK tracer provider (e.g. the client
219
+ # was created in a config/initializer during boot), don't reconfigure —
220
+ # that would replace the provider and drop its exporters.
221
+ return false if OpenTelemetry.tracer_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
222
+
223
+ OpenTelemetryConfig.new(project_id: project_id, otlp_endpoint: otlp_endpoint, **options)
224
+ .install_instrumentation_only
225
+ @instrumentation_installed_at_boot = true
226
+ rescue StandardError => e
227
+ warn "[LaunchDarklyObservability] Could not install Rails auto-instrumentation at boot: #{e.message}"
228
+ false
229
+ end
230
+
231
+ # Reset boot-instrumentation state. Intended for tests only.
232
+ # @api private
233
+ def reset_instrumentation_state!
234
+ @instrumentation_installed_at_boot = false
235
+ end
236
+
173
237
  private
174
238
 
175
239
  def otel_logger_provider_available?
@@ -179,3 +243,9 @@ module LaunchDarklyObservability
179
243
  end
180
244
  end
181
245
  end
246
+
247
+ # Required last, on purpose: the Rails Railtie's `config.after_initialize` hook
248
+ # can run synchronously during this require (lazy require after Rails has
249
+ # booted), and it depends on the fully-defined LaunchDarklyObservability module
250
+ # above. See the note next to the other require_relative calls at the top.
251
+ require_relative 'launchdarkly_observability/rails'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: launchdarkly-observability
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-22 00:00:00.000000000 Z
11
+ date: 2026-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: launchdarkly-server-sdk
@@ -39,33 +39,291 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.28'
41
41
  - !ruby/object:Gem::Dependency
42
- name: opentelemetry-instrumentation-all
42
+ name: opentelemetry-sdk
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.62'
47
+ version: '1.4'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.62'
54
+ version: '1.4'
55
55
  - !ruby/object:Gem::Dependency
56
- name: opentelemetry-sdk
56
+ name: opentelemetry-instrumentation-action_mailer
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - "<"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.4'
61
+ version: '0.8'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - "<"
67
67
  - !ruby/object:Gem::Version
68
- version: '1.4'
68
+ version: '0.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: opentelemetry-instrumentation-action_pack
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.18'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.18'
83
+ - !ruby/object:Gem::Dependency
84
+ name: opentelemetry-instrumentation-action_view
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "<"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.13'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "<"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.13'
97
+ - !ruby/object:Gem::Dependency
98
+ name: opentelemetry-instrumentation-active_job
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "<"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.12'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "<"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.12'
111
+ - !ruby/object:Gem::Dependency
112
+ name: opentelemetry-instrumentation-active_record
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "<"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.13'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "<"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.13'
125
+ - !ruby/object:Gem::Dependency
126
+ name: opentelemetry-instrumentation-active_storage
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "<"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.5'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "<"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.5'
139
+ - !ruby/object:Gem::Dependency
140
+ name: opentelemetry-instrumentation-active_support
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "<"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.12'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "<"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.12'
153
+ - !ruby/object:Gem::Dependency
154
+ name: opentelemetry-instrumentation-rails
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0.34'
160
+ - - "<"
161
+ - !ruby/object:Gem::Version
162
+ version: '0.42'
163
+ type: :runtime
164
+ prerelease: false
165
+ version_requirements: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0.34'
170
+ - - "<"
171
+ - !ruby/object:Gem::Version
172
+ version: '0.42'
173
+ - !ruby/object:Gem::Dependency
174
+ name: opentelemetry-instrumentation-concurrent_ruby
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0.21'
180
+ type: :runtime
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0.21'
187
+ - !ruby/object:Gem::Dependency
188
+ name: opentelemetry-instrumentation-faraday
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0.24'
194
+ type: :runtime
195
+ prerelease: false
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0.24'
201
+ - !ruby/object:Gem::Dependency
202
+ name: opentelemetry-instrumentation-graphql
203
+ requirement: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0.28'
208
+ type: :runtime
209
+ prerelease: false
210
+ version_requirements: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0.28'
215
+ - !ruby/object:Gem::Dependency
216
+ name: opentelemetry-instrumentation-http
217
+ requirement: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0.23'
222
+ type: :runtime
223
+ prerelease: false
224
+ version_requirements: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0.23'
229
+ - !ruby/object:Gem::Dependency
230
+ name: opentelemetry-instrumentation-mysql2
231
+ requirement: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0.28'
236
+ type: :runtime
237
+ prerelease: false
238
+ version_requirements: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0.28'
243
+ - !ruby/object:Gem::Dependency
244
+ name: opentelemetry-instrumentation-net_http
245
+ requirement: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0.22'
250
+ type: :runtime
251
+ prerelease: false
252
+ version_requirements: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - ">="
255
+ - !ruby/object:Gem::Version
256
+ version: '0.22'
257
+ - !ruby/object:Gem::Dependency
258
+ name: opentelemetry-instrumentation-pg
259
+ requirement: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - ">="
262
+ - !ruby/object:Gem::Version
263
+ version: '0.29'
264
+ type: :runtime
265
+ prerelease: false
266
+ version_requirements: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - ">="
269
+ - !ruby/object:Gem::Version
270
+ version: '0.29'
271
+ - !ruby/object:Gem::Dependency
272
+ name: opentelemetry-instrumentation-rack
273
+ requirement: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - ">="
276
+ - !ruby/object:Gem::Version
277
+ version: '0.24'
278
+ type: :runtime
279
+ prerelease: false
280
+ version_requirements: !ruby/object:Gem::Requirement
281
+ requirements:
282
+ - - ">="
283
+ - !ruby/object:Gem::Version
284
+ version: '0.24'
285
+ - !ruby/object:Gem::Dependency
286
+ name: opentelemetry-instrumentation-redis
287
+ requirement: !ruby/object:Gem::Requirement
288
+ requirements:
289
+ - - ">="
290
+ - !ruby/object:Gem::Version
291
+ version: '0.25'
292
+ type: :runtime
293
+ prerelease: false
294
+ version_requirements: !ruby/object:Gem::Requirement
295
+ requirements:
296
+ - - ">="
297
+ - !ruby/object:Gem::Version
298
+ version: '0.25'
299
+ - !ruby/object:Gem::Dependency
300
+ name: opentelemetry-instrumentation-sidekiq
301
+ requirement: !ruby/object:Gem::Requirement
302
+ requirements:
303
+ - - ">="
304
+ - !ruby/object:Gem::Version
305
+ version: '0.25'
306
+ type: :runtime
307
+ prerelease: false
308
+ version_requirements: !ruby/object:Gem::Requirement
309
+ requirements:
310
+ - - ">="
311
+ - !ruby/object:Gem::Version
312
+ version: '0.25'
313
+ - !ruby/object:Gem::Dependency
314
+ name: opentelemetry-instrumentation-sinatra
315
+ requirement: !ruby/object:Gem::Requirement
316
+ requirements:
317
+ - - ">="
318
+ - !ruby/object:Gem::Version
319
+ version: '0.24'
320
+ type: :runtime
321
+ prerelease: false
322
+ version_requirements: !ruby/object:Gem::Requirement
323
+ requirements:
324
+ - - ">="
325
+ - !ruby/object:Gem::Version
326
+ version: '0.24'
69
327
  - !ruby/object:Gem::Dependency
70
328
  name: opentelemetry-semantic_conventions
71
329
  requirement: !ruby/object:Gem::Requirement
@@ -161,8 +419,11 @@ files:
161
419
  - CHANGELOG.md
162
420
  - LICENSE.txt
163
421
  - README.md
422
+ - lib/launchdarkly-observability.rb
164
423
  - lib/launchdarkly_observability.rb
165
424
  - lib/launchdarkly_observability/hook.rb
425
+ - lib/launchdarkly_observability/instrumentation_log_filter.rb
426
+ - lib/launchdarkly_observability/instrumentations.rb
166
427
  - lib/launchdarkly_observability/middleware.rb
167
428
  - lib/launchdarkly_observability/opentelemetry_config.rb
168
429
  - lib/launchdarkly_observability/otel_log_bridge.rb