Skip to content

feat(ruby): install Rails auto-instrumentation during boot (fix lazy-init 'failed to install')#582

Open
ccschmitz-launchdarkly wants to merge 2 commits into
fix/ruby-observability-plugin-warningsfrom
feat/ruby-rails-boot-instrumentation
Open

feat(ruby): install Rails auto-instrumentation during boot (fix lazy-init 'failed to install')#582
ccschmitz-launchdarkly wants to merge 2 commits into
fix/ruby-observability-plugin-warningsfrom
feat/ruby-rails-boot-instrumentation

Conversation

@ccschmitz-launchdarkly

@ccschmitz-launchdarkly ccschmitz-launchdarkly commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #581 (stacked on it — base will retarget to main after #581 merges). This fixes the remaining issue from the customer report: when the LaunchDarkly client is created lazily (e.g. from a model on first request, after Rails has booted), the OpenTelemetry Rails-family instrumentations log a flood of:

Instrumentation: OpenTelemetry::Instrumentation::ActionPack failed to install
Instrumentation: OpenTelemetry::Instrumentation::ActiveRecord failed to install
...

Root cause: those instrumentations patch via ActiveSupport.on_load hooks that fire during boot. We configured OpenTelemetry from Plugin#register (at LDClient.new), so a lazily-created client runs use_all after the hooks have fired → nothing attaches.

Fix (two parts)

  1. Auto-require at boot. The gem shipped only launchdarkly_observability.rb (underscore), so Bundler.require (which tries the hyphenated gem name) couldn't load it — users had to require it manually in an initializer, too late for the Railtie. Added lib/launchdarkly-observability.rb so Bundler.require loads the gem (and its Railtie) during boot.
  2. Install instrumentation at boot. A Railtie initializer installs auto-instrumentation during boot, decoupled from the client. project_id for the resource is resolved from LAUNCHDARKLY_SDK_KEY (present in the env at boot in the common case, even when the client object is built lazily). At register, the plugin then only attaches exporters to the existing provider instead of reconfiguring it.

It runs after: :load_config_initializers and no-ops if a provider is already configured, so boot-time-init apps keep their own configuration untouched. When the client registers after boot and boot-install didn't run (no SDK key in env at boot), the plugin logs one actionable warning instead of the upstream flood.

Verification

I confirmed the mechanism empirically: installing instrumentation before Rails.application.initialize! attaches everything (installed? == true); doing it after boot is the failure. In the demo app, boot-time-init installs at load_config_initializers (position 105/289) — so a Railtie initializer is comfortably early enough.

Tests

  • Gem unit tests (boot_instrumentation_test.rb): boot-flag default, no-op without a project id, no-op when a provider is already configured, and that configure attaches to the existing provider (doesn't replace it) when installed at boot. 120 runs, 0 failures; rubocop clean.
  • e2e lazy-init variant: the Rails demo app gains an LD_LAZY_INIT=1 mode (client created from LazyLdClient on first use, not at boot). A new integration test boots a separate Rails process in that mode and asserts the Rails instrumentations install anyway. Verified it fails ("failed to install") when the boot-time install is removed.

Docs

README now documents lazy-client init (and the LAUNCHDARKLY_SDK_KEY-at-boot requirement), and the instrumentation-options example no longer shows options the pinned versions reject.

Known limitation

In the lazy case, resource attributes that come from Plugin.new options (e.g. service_name) aren't applied at boot since the plugin instance doesn't exist yet — service_name can still be set via OTEL_SERVICE_NAME. Boot-time-init apps are unaffected.

🤖 Generated with Claude Code


Note

Medium Risk
Changes global OpenTelemetry setup timing and tracer-provider lifecycle in Rails; incorrect no-op or reconfigure logic could drop instrumentation or duplicate exporters, though guards and tests target the lazy-init and boot-init cases.

Overview
Fixes lazy LaunchDarkly client setups where OpenTelemetry Rails instrumentations previously logged "failed to install" because OTel was configured only when LDClient.new ran, after ActiveSupport.on_load hooks had already fired.

Boot-time path: Adds lib/launchdarkly-observability.rb so Bundler.require loads the Railtie early. The Railtie runs install_rails_instrumentation after load_config_initializers (uses LAUNCHDARKLY_SDK_KEY from ENV, no-ops if an SDK tracer provider already exists). OpenTelemetryConfig#install_instrumentation_only sets up the provider and instrumentations without exporters; when the plugin registers later, configure_traces only adds OTLP span processors instead of re-running OpenTelemetry::SDK.configure. If registration happens after boot without a prior boot install, the gem emits one actionable warning instead of upstream noise.

Verification: Unit tests for boot flags and provider reuse; Rails demo LD_LAZY_INIT=1 mode with a subprocess integration test asserting Rack/ActionPack/ActiveRecord/etc. stay installed when the client is created on first use. README documents lazy init and updates instrumentation option examples.

Reviewed by Cursor Bugbot for commit 6d3bf0a. Bugbot is set up for automated code reviews on this repo. Configure here.

ccschmitz-launchdarkly and others added 2 commits June 2, 2026 16:07
The OTel Rails-family instrumentations (ActionPack, ActiveRecord, ...) patch via
ActiveSupport.on_load hooks that fire while Rails boots. The plugin configured
OpenTelemetry from Plugin#register (at LDClient.new), so when an app creates the
client lazily — e.g. from a model on first request, after Rails has booted —
those hooks had already fired and every Rails instrumentation logged
"Instrumentation: ... failed to install".

Fix it in two parts:

1. Ship a hyphenated entry point (lib/launchdarkly-observability.rb) matching the
   gem name so Bundler.require auto-loads the gem — and its Railtie — during
   boot. Previously the gem was only loadable as 'launchdarkly_observability'
   (underscore), so Bundler couldn't auto-require it and users loaded it manually
   in an initializer, too late for the Railtie.

2. Add a Railtie initializer that installs auto-instrumentation during boot,
   decoupled from the LD client. project_id for the resource is resolved from
   LAUNCHDARKLY_SDK_KEY, which is present in the environment at boot in the common
   case even when the client object is built lazily. At register time the plugin
   then only attaches exporters to the existing provider instead of reconfiguring
   it (which would drop the boot-time instrumentation). It runs
   `after: :load_config_initializers` and no-ops if a provider is already
   configured, so boot-time-init apps keep their own configuration untouched.

When the client is registered after boot and boot-time install did not run (e.g.
no SDK key in the environment at boot), the plugin now logs a single actionable
warning instead of the upstream flood of "failed to install" lines.

Co-Authored-By: Claude <noreply@anthropic.com>
Add a lazy client-initialization mode to the Rails demo app (LD_LAZY_INIT=1):
the initializer skips creating the client at boot, and a LazyLdClient model
creates it on first use — mirroring apps that build the client after Rails has
booted. A new integration test boots a separate Rails process in this mode and
asserts the Rails-family instrumentations are still installed, which only holds
because the Railtie installs them during boot. Verified that removing the
boot-time install makes the test fail with "failed to install".

Co-Authored-By: Claude <noreply@anthropic.com>
@ccschmitz-launchdarkly ccschmitz-launchdarkly marked this pull request as ready for review June 3, 2026 14:25
@ccschmitz-launchdarkly ccschmitz-launchdarkly requested a review from a team as a code owner June 3, 2026 14:25

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6d3bf0a. Configure here.

def configure_traces
if LaunchDarklyObservability.instrumentation_installed_at_boot?
OpenTelemetry.tracer_provider.add_span_processor(create_batch_span_processor)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lazy init ignores instrumentations

Medium Severity

When boot-time install runs, later Plugin#register only adds a span processor and never reapplies instrumentations from Plugin.new. Lazy-init apps keep default use_all settings instead of user overrides such as custom untraced_endpoints.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d3bf0a. Configure here.

c.resource = create_resource
configure_instrumentations(c)
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boot install ignores enable_traces

Low Severity

install_instrumentation_only always enables auto-instrumentation during boot when LAUNCHDARKLY_SDK_KEY is set, even if the later Plugin is created with enable_traces: false. Register then skips configure_traces, so Rails-family instrumentation stays active without the intended opt-out.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d3bf0a. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants