feat(ruby): install Rails auto-instrumentation during boot (fix lazy-init 'failed to install')#582
Conversation
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ 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 |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 6d3bf0a. Configure here.
| c.resource = create_resource | ||
| configure_instrumentations(c) | ||
| end | ||
| end |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 6d3bf0a. Configure here.


Summary
Follow-up to #581 (stacked on it — base will retarget to
mainafter #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:Root cause: those instrumentations patch via
ActiveSupport.on_loadhooks that fire during boot. We configured OpenTelemetry fromPlugin#register(atLDClient.new), so a lazily-created client runsuse_allafter the hooks have fired → nothing attaches.Fix (two parts)
launchdarkly_observability.rb(underscore), soBundler.require(which tries the hyphenated gem name) couldn't load it — users had torequireit manually in an initializer, too late for the Railtie. Addedlib/launchdarkly-observability.rbsoBundler.requireloads the gem (and its Railtie) during boot.project_idfor the resource is resolved fromLAUNCHDARKLY_SDK_KEY(present in the env at boot in the common case, even when the client object is built lazily). Atregister, the plugin then only attaches exporters to the existing provider instead of reconfiguring it.It runs
after: :load_config_initializersand 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 atload_config_initializers(position 105/289) — so a Railtie initializer is comfortably early enough.Tests
boot_instrumentation_test.rb): boot-flag default, no-op without a project id, no-op when a provider is already configured, and thatconfigureattaches to the existing provider (doesn't replace it) when installed at boot. 120 runs, 0 failures; rubocop clean.LD_LAZY_INIT=1mode (client created fromLazyLdClienton 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.newoptions (e.g.service_name) aren't applied at boot since the plugin instance doesn't exist yet —service_namecan still be set viaOTEL_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.newran, afterActiveSupport.on_loadhooks had already fired.Boot-time path: Adds
lib/launchdarkly-observability.rbsoBundler.requireloads the Railtie early. The Railtie runsinstall_rails_instrumentationafterload_config_initializers(usesLAUNCHDARKLY_SDK_KEYfrom ENV, no-ops if an SDK tracer provider already exists).OpenTelemetryConfig#install_instrumentation_onlysets up the provider and instrumentations without exporters; when the plugin registers later,configure_tracesonly adds OTLP span processors instead of re-runningOpenTelemetry::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=1mode 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.