fix(ruby): silence observability plugin warnings (log bridge, nil context id, instrumentation options)#581
Open
ccschmitz-launchdarkly wants to merge 5 commits into
Open
Conversation
Vadman97
approved these changes
Jun 3, 2026
a825d63 to
200474c
Compare
The Railtie's `attach_otel_log_bridge` delegated to a module method (`LaunchDarklyObservability.otel_logger_provider_available?`) defined in the `class << self` block of launchdarkly_observability.rb. That block runs *after* the `require_relative '.../rails'` near the top of the file. When the gem is required lazily after Rails has booted, the Railtie's `config.after_initialize` hook fires synchronously during the require — before the module method exists — so the bridge attach failed with: Could not attach log bridge to Rails.logger: undefined method `otel_logger_provider_available?' for module LaunchDarklyObservability Inline the availability check in the Railtie so it no longer depends on load order, and move the `rails.rb` require to the bottom of the main file (after the module body is fully defined) so future Railtie code can't reintroduce the same class of bug. Adds a regression test that removes the module method to simulate the load-order state. Co-Authored-By: Claude <noreply@anthropic.com>
…rsions The default instrumentation config passed `enable_recognize_route: true` to the Rails instrumentation and `db_statement: :include` to ActiveRecord. Neither option exists on those instrumentations (verified against the pinned gem versions: rails 0.39.1, active_record 0.11.1, rack 0.29.0), so they were no-ops that emitted a warning on every boot: Instrumentation ... ignored the following unknown configuration options [...] Remove them. Route-based span naming (http.route) is handled automatically by the ActionPack instrumentation, and SQL capture comes from the database adapter instrumentations (Mysql2, PG, ...) which already obfuscate statements by default. Co-Authored-By: Claude <noreply@anthropic.com>
The Rails e2e demo app only verified that a controller responds; nothing checked
that the OTel Rails-family instrumentations actually attached. That gap is why a
recent customer report ("Instrumentation: ... failed to install") went unnoticed
— the app initializes the plugin during Rails boot (the happy path) and never
asserted the result.
Add an integration test that asserts Rack, ActionPack, ActiveRecord,
ActiveSupport, and Rails report `installed? == true` after boot (the exact
inverse of the "failed to install" warning), and that an HTTP request produces a
server span. The plugin only configures OpenTelemetry when the LD client
registers it, which needs a non-empty SDK key, so test_helper sets a dummy key
before boot (invalid, so the client never connects).
Co-Authored-By: Claude <noreply@anthropic.com>
When a flag is evaluated with an invalid context the SDK returns USER_NOT_SPECIFIED. The hook recorded error.type=provider_not_ready with a bare "Flag evaluation error: USER_NOT_SPECIFIED" message — but the provider is fine, the context is the problem, and *why* it was invalid was dropped from the trace entirely. Map USER_NOT_SPECIFIED to the OpenTelemetry feature-flag `invalid_context` error.type (provider_not_ready is now reserved for CLIENT_NOT_READY), and append the context's own validation error to the span error message so the failure is debuggable from the trace alone (e.g. "context kind must be a string"). The SDK's validation messages are static and structural — never key/kind/attribute values — so no context data is leaked. Co-Authored-By: Claude <noreply@anthropic.com>
`defined?` returns a string or nil, so when OpenTelemetry::SDK::Logs was not yet loaded the `&&` chain short-circuited to nil instead of false. The railtie regression test asserts the predicate returns true/false and flaked (~3/8 runs) depending on test order. Guard the constant check first and return an explicit boolean, in both the Railtie's inlined copy and the module-level method. Co-Authored-By: Claude <noreply@anthropic.com>
200474c to
bdb3178
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #575. A customer integrating the Ruby observability plugin reported a flood of warnings/errors in their logs after traces started flowing. The most urgent one — the nil
feature_flag.context.idOTel error — shipped separately in 0.2.2 (#641). This PR carries the rest of the fixes (each with a regression test), sharpens how invalid contexts surface on the trace, closes the e2e gap that let them through, and de-flakes the railtie test.Diagnosed from the customer's log dump:
Could not attach log bridge to Rails.logger: undefined method 'otel_logger_provider_available?'after_initializeruns synchronously during a lazy (post-boot)require, before the module method it delegated to was definedrails.rbrequire to the bottom of the main fileInstrumentation ... ignored the following unknown configuration options [:db_statement] / [:enable_recognize_route]Better invalid-context diagnostics
A nil
feature_flag.context.id(the thing #641 silenced) is itself a signal: that evaluation ran against an invalid context and fell back to the flag's default value. Two changes make that legible on the trace instead of silent:USER_NOT_SPECIFIEDnow maps to the OpenTelemetry feature-flagerror.type = invalid_context. It wasprovider_not_ready, which is misleading — the provider is fine, the context isn't;provider_not_readyis now reserved forCLIENT_NOT_READY.error.messagenow includes the context's own validation reason (e.g.Flag evaluation error: USER_NOT_SPECIFIED (context kind must be a string)), recovered fromLDContext#error. The SDK's validation messages are static and structural — never key/kind/attribute values — so no context data is leaked.De-flake the railtie test
otel_logger_provider_available?began withdefined?(...), which returns a string or nil — so whenOpenTelemetry::SDK::Logswasn't loaded yet, the&&chain returned nil instead of false and the railtie regression test (which asserts true/false) flaked ~3/8 runs depending on test order. Both copies of the predicate — the Railtie's inlined one and the module-level one — now guard the constant first and return a strict boolean.Tests
error.type/message. 118 runs, 0 failures across repeated runs; rubocop clean.ObservabilityInstrumentationTestin the Rails demo app asserts the Rails-family instrumentations reportinstalled? == trueafter boot (the inverse of "failed to install") and that a request produces a server span. The demo app only ever exercised the boot-time-init happy path, which is why this slipped through.Not included (follow-up)
The
Instrumentation: ... failed to installwarnings are a separate issue: the plugin configures auto-instrumentation fromPlugin#register(atLDClient.new), so when a client is created lazily after Rails boots, the Rails instrumentations'ActiveSupport.on_loadhooks have already fired. Fixing that (install instrumentation during Rails boot) plus a lazy-init e2e variant is tracked separately.🤖 Generated with Claude Code