Skip to content

fix(ruby): silence observability plugin warnings (log bridge, nil context id, instrumentation options)#581

Open
ccschmitz-launchdarkly wants to merge 5 commits into
mainfrom
fix/ruby-observability-plugin-warnings
Open

fix(ruby): silence observability plugin warnings (log bridge, nil context id, instrumentation options)#581
ccschmitz-launchdarkly wants to merge 5 commits into
mainfrom
fix/ruby-observability-plugin-warnings

Conversation

@ccschmitz-launchdarkly

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

Copy link
Copy Markdown
Contributor

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.id OTel 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:

Log line Cause Fix
Could not attach log bridge to Rails.logger: undefined method 'otel_logger_provider_available?' Railtie's after_initialize runs synchronously during a lazy (post-boot) require, before the module method it delegated to was defined Inline the check in the Railtie; move the rails.rb require to the bottom of the main file
Instrumentation ... ignored the following unknown configuration options [:db_statement] / [:enable_recognize_route] Options that don't exist on the pinned instrumentation versions Remove them (route naming is automatic via ActionPack; SQL capture is via the DB adapter instrumentations)

The OpenTelemetry error: invalid ... attribute value type NilClass for key 'feature_flag.context.id' line is already fixed and released in 0.2.2 (#641). This PR builds on top of that fix.

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_SPECIFIED now maps to the OpenTelemetry feature-flag error.type = invalid_context. It was provider_not_ready, which is misleading — the provider is fine, the context isn't; provider_not_ready is now reserved for CLIENT_NOT_READY.
  • The span's error.message now includes the context's own validation reason (e.g. Flag evaluation error: USER_NOT_SPECIFIED (context kind must be a string)), recovered from LDContext#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 with defined?(...), which returns a string or nil — so when OpenTelemetry::SDK::Logs wasn'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

  • Gem suite: regression tests for the log-bridge load order and the new invalid-context error.type/message. 118 runs, 0 failures across repeated runs; rubocop clean.
  • e2e: new ObservabilityInstrumentationTest in the Rails demo app asserts the Rails-family instrumentations report installed? == true after 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 install warnings are a separate issue: the plugin configures auto-instrumentation from Plugin#register (at LDClient.new), so when a client is created lazily after Rails boots, the Rails instrumentations' ActiveSupport.on_load hooks have already fired. Fixing that (install instrumentation during Rails boot) plus a lazy-init e2e variant is tracked separately.

🤖 Generated with Claude Code

@ccschmitz-launchdarkly ccschmitz-launchdarkly force-pushed the fix/ruby-observability-plugin-warnings branch from a825d63 to 200474c Compare June 22, 2026 17:20
ccschmitz-launchdarkly and others added 5 commits June 22, 2026 12:22
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>
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