From f8085ebb75c75ced85695952ca1c39f49720feeb Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Tue, 2 Jun 2026 15:42:30 -0500 Subject: [PATCH 01/18] fix(ruby): attach Rails log bridge independent of require order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../lib/launchdarkly_observability.rb | 14 ++++++++++- .../lib/launchdarkly_observability/rails.rb | 12 ++++++++- .../test/rails_railtie_test.rb | 25 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb index dec63a7c40..f1f421d83a 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb @@ -13,7 +13,13 @@ require_relative 'launchdarkly_observability/middleware' require_relative 'launchdarkly_observability/otel_log_bridge' -require_relative 'launchdarkly_observability/rails' + +# NOTE: rails.rb is required at the *bottom* of this file, after the +# LaunchDarklyObservability module body has been fully defined. Its Railtie +# registers a `config.after_initialize` hook that runs synchronously when the +# gem is required lazily after Rails has booted. That hook references module +# constants and `class << self` methods, so they must already exist when the +# require runs. See the require at the end of this file. module LaunchDarklyObservability # Default OTLP endpoint for LaunchDarkly Observability @@ -179,3 +185,9 @@ def otel_logger_provider_available? end end end + +# Required last, on purpose: the Rails Railtie's `config.after_initialize` hook +# can run synchronously during this require (lazy require after Rails has +# booted), and it depends on the fully-defined LaunchDarklyObservability module +# above. See the note next to the other require_relative calls at the top. +require_relative 'launchdarkly_observability/rails' diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb index fa50f2d389..c6238dd549 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb @@ -127,8 +127,18 @@ def attach_otel_log_bridge warn "[LaunchDarklyObservability] Could not attach log bridge to Rails.logger: #{e.message}" end + # The availability check is inlined here rather than delegating to + # LaunchDarklyObservability.otel_logger_provider_available? on purpose. + # rails.rb is required from launchdarkly_observability.rb *before* that + # file's `class << self` block (which defines the module method) has run. + # When the gem is lazily required after Rails has booted, the + # `config.after_initialize` hook above executes synchronously while this + # file is still loading, so the module method does not exist yet and the + # delegation raised "undefined method `otel_logger_provider_available?'". def otel_logger_provider_available? - LaunchDarklyObservability.send(:otel_logger_provider_available?) + defined?(OpenTelemetry::SDK::Logs::LoggerProvider) && + OpenTelemetry.respond_to?(:logger_provider) && + OpenTelemetry.logger_provider.is_a?(OpenTelemetry::SDK::Logs::LoggerProvider) end end diff --git a/sdk/@launchdarkly/observability-ruby/test/rails_railtie_test.rb b/sdk/@launchdarkly/observability-ruby/test/rails_railtie_test.rb index c8a163f766..da4c7072e0 100644 --- a/sdk/@launchdarkly/observability-ruby/test/rails_railtie_test.rb +++ b/sdk/@launchdarkly/observability-ruby/test/rails_railtie_test.rb @@ -114,5 +114,30 @@ def test_rails_file_loads_when_after_initialize_runs_immediately 'ViewHelpers should be defined after loading rails.rb' assert defined?(LaunchDarklyObservability::Railtie), 'Railtie should be defined after loading rails.rb' + + # Regression: the Railtie's `attach_otel_log_bridge` runs from the synchronous + # `config.after_initialize` block *during* the require of launchdarkly_observability.rb, + # before that file's `class << self` block (which defines the module-level + # `otel_logger_provider_available?`) has executed. The Railtie used to delegate to + # that not-yet-defined method, so the bridge attach failed with: + # + # Could not attach log bridge to Rails.logger: undefined method + # `otel_logger_provider_available?' for module LaunchDarklyObservability + # + # The Railtie's check must be self-contained. Simulate the load-order state by + # removing the module method, then assert the Railtie check still works. + sc = LaunchDarklyObservability.singleton_class + saved = sc.instance_method(:otel_logger_provider_available?) + sc.send(:remove_method, :otel_logger_provider_available?) + begin + result = nil + assert_silent do + result = LaunchDarklyObservability::Railtie.send(:otel_logger_provider_available?) + end + assert_includes [true, false], result + ensure + sc.send(:define_method, :otel_logger_provider_available?, saved) + sc.send(:private, :otel_logger_provider_available?) + end end end From 05d2fbab9fec4c0d78a75b05a32f9bd29ff40994 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Tue, 2 Jun 2026 15:42:48 -0500 Subject: [PATCH 02/18] fix(ruby): drop auto-instrumentation options unsupported by pinned versions 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 --- .../opentelemetry_config.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb index 7c0603e2b3..4ad8215e5b 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb @@ -105,9 +105,16 @@ def configure_traces # User-provided instrumentation config is merged on top of defaults, # so users only need to specify the instrumentations they want to override. def configure_instrumentations(config) + # Only pass options that the instrumentations actually accept. Unknown + # options are not fatal but emit a warning on every boot ("ignored the + # following unknown configuration options [...]"): + # - `enable_recognize_route` is not an option on the Rails, Rack, or + # ActionPack instrumentations; route-based span naming (http.route) is + # handled automatically by the ActionPack instrumentation. + # - ActiveRecord has no `db_statement` option; SQL capture comes from the + # database adapter instrumentations (Mysql2, PG, ...) which default to + # obfuscating statements. defaults = { - 'OpenTelemetry::Instrumentation::Rails' => { enable_recognize_route: true }, - 'OpenTelemetry::Instrumentation::ActiveRecord' => { db_statement: :include }, 'OpenTelemetry::Instrumentation::Net::HTTP' => { untraced_hosts: [] }, 'OpenTelemetry::Instrumentation::Rack' => { untraced_endpoints: ['/health', '/healthz', '/ready'] } } From e09f22f49895b303f077b16d1337d2de23caae53 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Tue, 2 Jun 2026 15:42:57 -0500 Subject: [PATCH 03/18] test(ruby): assert Rails auto-instrumentation installs on boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../observability_instrumentation_test.rb | 45 +++++++++++++++++++ e2e/ruby/rails/demo/test/test_helper.rb | 8 ++++ 2 files changed, 53 insertions(+) create mode 100644 e2e/ruby/rails/demo/test/integration/observability_instrumentation_test.rb diff --git a/e2e/ruby/rails/demo/test/integration/observability_instrumentation_test.rb b/e2e/ruby/rails/demo/test/integration/observability_instrumentation_test.rb new file mode 100644 index 0000000000..9c79a41206 --- /dev/null +++ b/e2e/ruby/rails/demo/test/integration/observability_instrumentation_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'test_helper' + +# End-to-end coverage for the LaunchDarkly observability plugin's Rails +# auto-instrumentation. +# +# Background: the plugin configures OpenTelemetry from `Plugin#register`, which +# runs when the LaunchDarkly client is created. In this app that happens in +# `config/initializers/launchdarkly.rb` — i.e. DURING Rails boot — so the OTel +# Rails-family instrumentations (Rack, ActionPack, ActiveRecord, ...) install +# correctly. A customer who instead creates the client lazily AFTER boot sees +# "Instrumentation: OpenTelemetry::Instrumentation::ActionPack failed to install" +# because the ActiveSupport.on_load hooks those instrumentations rely on have +# already fired. These tests pin the working boot-time behavior so a regression +# (or a change that breaks instrumentation install) is caught in CI. +class ObservabilityInstrumentationTest < ActionDispatch::IntegrationTest + # The Rails-family instrumentations that must attach during a boot-time init. + # These are exactly the ones that report "failed to install" on the lazy path. + RAILS_INSTRUMENTATIONS = %w[Rack ActionPack ActiveRecord ActiveSupport Rails].freeze + + def instrumentation_instance(name) + Object.const_get("OpenTelemetry::Instrumentation::#{name}::Instrumentation").instance + end + + test 'rails auto-instrumentation installed during boot' do + RAILS_INSTRUMENTATIONS.each do |name| + assert instrumentation_instance(name).installed?, + "#{name} instrumentation should be installed after a boot-time plugin init " \ + '(it reports "failed to install" when the client is created lazily after boot)' + end + end + + test 'http request produces a server span via the rack instrumentation' do + exporter = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new + processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter) + OpenTelemetry.tracer_provider.add_span_processor(processor) + + get pages_home_url + assert_response :success + + server_spans = exporter.finished_spans.select { |s| s.kind == :server } + refute_empty server_spans, 'expected an HTTP server span from the Rack/ActionPack instrumentation' + end +end diff --git a/e2e/ruby/rails/demo/test/test_helper.rb b/e2e/ruby/rails/demo/test/test_helper.rb index 0c92e8e881..967eed2a76 100644 --- a/e2e/ruby/rails/demo/test/test_helper.rb +++ b/e2e/ruby/rails/demo/test/test_helper.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true ENV['RAILS_ENV'] ||= 'test' + +# The observability plugin only configures OpenTelemetry (and installs the Rails +# auto-instrumentation) when the LaunchDarkly client registers it, which requires +# a non-empty SDK key. Set a dummy key BEFORE the app boots so the instrumentation +# attaches during initialization. The key is invalid, so the client never connects +# (background connection attempts fail gracefully and do not affect tests). +ENV['LAUNCHDARKLY_SDK_KEY'] ||= 'sdk-test-0000000000000000000000' + require_relative '../config/environment' require 'rails/test_help' From 46805df3c54571699aeab17b272c3ff77fc228ec Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Tue, 2 Jun 2026 16:07:15 -0500 Subject: [PATCH 04/18] feat(ruby): install Rails auto-instrumentation during boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../observability-ruby/.rubocop.yml | 6 ++ .../observability-ruby/README.md | 17 ++++- .../lib/launchdarkly-observability.rb | 9 +++ .../lib/launchdarkly_observability.rb | 54 +++++++++++++ .../opentelemetry_config.rb | 45 ++++++++++- .../lib/launchdarkly_observability/rails.rb | 14 ++++ .../test/boot_instrumentation_test.rb | 76 +++++++++++++++++++ 7 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 sdk/@launchdarkly/observability-ruby/lib/launchdarkly-observability.rb create mode 100644 sdk/@launchdarkly/observability-ruby/test/boot_instrumentation_test.rb diff --git a/sdk/@launchdarkly/observability-ruby/.rubocop.yml b/sdk/@launchdarkly/observability-ruby/.rubocop.yml index 1b4837d74c..2f60a9fe00 100644 --- a/sdk/@launchdarkly/observability-ruby/.rubocop.yml +++ b/sdk/@launchdarkly/observability-ruby/.rubocop.yml @@ -24,6 +24,12 @@ Lint/DuplicateBranch: Naming/MethodParameterName: Enabled: false +Naming/FileName: + # launchdarkly-observability.rb is intentionally hyphenated to match the gem + # name so Bundler.require can auto-load the gem (and its Railtie) at boot. + Exclude: + - 'lib/launchdarkly-observability.rb' + Naming/PredicateMethod: Enabled: false diff --git a/sdk/@launchdarkly/observability-ruby/README.md b/sdk/@launchdarkly/observability-ruby/README.md index 7ab8cd8788..3602ba3e80 100644 --- a/sdk/@launchdarkly/observability-ruby/README.md +++ b/sdk/@launchdarkly/observability-ruby/README.md @@ -92,6 +92,15 @@ Rails.configuration.ld_client = LaunchDarkly::LDClient.new( at_exit { Rails.configuration.ld_client.close } ``` +> **Lazy client initialization:** Creating the LaunchDarkly client during boot +> (as above) is recommended. If your app instead creates the client lazily — +> e.g. from a model on first request, after Rails has finished booting — the gem +> still installs the Rails auto-instrumentation during boot via its Railtie, as +> long as `LAUNCHDARKLY_SDK_KEY` is set in the environment before Rails boots. If +> it isn't, the OpenTelemetry Rails instrumentations can't attach (their load +> hooks have already fired) and the plugin logs a single warning explaining how +> to fix it. + Use in controllers: ```ruby @@ -145,10 +154,12 @@ LaunchDarklyObservability::Plugin.new( enable_logs: true, # default: true enable_metrics: true, # default: true - # Optional: Custom instrumentation configuration + # Optional: Custom instrumentation configuration. Keys are instrumentation + # class names; values are that instrumentation's own options (only pass + # options the installed instrumentation version supports). instrumentations: { - 'OpenTelemetry::Instrumentation::Rails' => { enable_recognize_route: true }, - 'OpenTelemetry::Instrumentation::ActiveRecord' => { db_statement: :include } + 'OpenTelemetry::Instrumentation::Rack' => { untraced_endpoints: ['/health'] }, + 'OpenTelemetry::Instrumentation::Net::HTTP' => { untraced_hosts: ['internal.example.com'] } } ) ``` diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly-observability.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly-observability.rb new file mode 100644 index 0000000000..83a97cf4da --- /dev/null +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly-observability.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Entry point matching the gem name (`launchdarkly-observability`) so that +# Bundler.require auto-loads the gem during application boot. Without this, +# Bundler's default `require 'launchdarkly-observability'` fails (the real entry +# point is the underscore file) and users must require the gem manually — often +# in an initializer, which is too late for the Rails Railtie to install +# auto-instrumentation during boot. +require_relative 'launchdarkly_observability' diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb index f1f421d83a..364dcd0914 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb @@ -176,6 +176,60 @@ def shutdown @instance = nil end + # @return [Boolean] whether auto-instrumentation was installed during Rails + # boot by {.install_rails_instrumentation}. When true, the plugin attaches + # exporters to the existing tracer provider at register time instead of + # reconfiguring it. + def instrumentation_installed_at_boot? + @instrumentation_installed_at_boot == true + end + + # Install OpenTelemetry auto-instrumentation during Rails boot. + # + # The OTel Rails-family instrumentations (ActionPack, ActiveRecord, ...) patch + # via ActiveSupport.on_load hooks that fire while Rails is booting. If the + # LaunchDarkly client is created lazily (e.g. from a model on first request), + # the plugin's #register runs after those hooks have fired and the + # instrumentations report "failed to install". The Rails Railtie calls this + # during boot so instrumentation attaches regardless of when the client is + # created. Exporters are still configured later, when the client registers + # the plugin. + # + # The project_id needed for the resource is resolved from the + # LAUNCHDARKLY_SDK_KEY environment variable, which is present at boot in the + # common case even when the client object is created lazily. If it cannot be + # resolved, instrumentation is left to #register (which warns if it then runs + # after boot). + # + # @param project_id [String, nil] explicit project id; falls back to ENV + # @param otlp_endpoint [String] OTLP endpoint (only relevant for exporters) + # @param options [Hash] additional OpenTelemetryConfig options + # @return [Boolean] whether instrumentation was installed + def install_rails_instrumentation(project_id: nil, otlp_endpoint: DEFAULT_ENDPOINT, **options) + return false if @instrumentation_installed_at_boot + + project_id ||= ENV.fetch('LAUNCHDARKLY_SDK_KEY', nil) + return false if project_id.nil? || project_id.empty? + + # If something already configured an SDK tracer provider (e.g. the client + # was created in a config/initializer during boot), don't reconfigure — + # that would replace the provider and drop its exporters. + return false if OpenTelemetry.tracer_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider) + + OpenTelemetryConfig.new(project_id: project_id, otlp_endpoint: otlp_endpoint, **options) + .install_instrumentation_only + @instrumentation_installed_at_boot = true + rescue StandardError => e + warn "[LaunchDarklyObservability] Could not install Rails auto-instrumentation at boot: #{e.message}" + false + end + + # Reset boot-instrumentation state. Intended for tests only. + # @api private + def reset_instrumentation_state! + @instrumentation_installed_at_boot = false + end + private def otel_logger_provider_available? diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb index 4ad8215e5b..268418f3c6 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb @@ -70,6 +70,20 @@ def configure @configured = true end + # Install the SDK tracer provider and auto-instrumentation WITHOUT exporters. + # + # Called from the Rails Railtie during boot so the Rails-family + # instrumentations (which patch via ActiveSupport.on_load hooks that fire + # during boot) attach even when the LaunchDarkly client — and therefore + # #register / #configure — is created lazily afterward. Exporters are added + # later by #configure_traces when the client registers the plugin. + def install_instrumentation_only + OpenTelemetry::SDK.configure do |c| + c.resource = create_resource + configure_instrumentations(c) + end + end + # Flush all pending telemetry data def flush OpenTelemetry.tracer_provider&.force_flush @@ -90,8 +104,20 @@ def shutdown private - # Configure OpenTelemetry traces with OTLP exporter + # Configure OpenTelemetry traces with OTLP exporter. + # + # If auto-instrumentation was already installed during Rails boot (see + # LaunchDarklyObservability.install_rails_instrumentation), the SDK tracer + # provider already exists with instrumentation attached — so we only add the + # OTLP span exporter. Re-running OpenTelemetry::SDK.configure here would + # replace that provider and, in the lazy-init case, drop the Rails-family + # instrumentation that can only attach during boot. def configure_traces + if LaunchDarklyObservability.instrumentation_installed_at_boot? + OpenTelemetry.tracer_provider.add_span_processor(create_batch_span_processor) + return + end + OpenTelemetry::SDK.configure do |c| c.resource = create_resource c.add_span_processor(create_batch_span_processor) @@ -99,6 +125,23 @@ def configure_traces # Enable auto-instrumentation configure_instrumentations(c) end + + warn_if_rails_instrumentation_missed + end + + # Emit a single actionable warning when the plugin is registered after Rails + # has finished booting and boot-time instrumentation install did not run + # (e.g. LAUNCHDARKLY_SDK_KEY was not set in the environment at boot). In that + # case the OTel Rails-family instrumentations log a flurry of + # "failed to install" warnings because their load hooks have already fired. + def warn_if_rails_instrumentation_missed + return unless defined?(::Rails) && ::Rails.respond_to?(:application) + return unless ::Rails.application.respond_to?(:initialized?) && ::Rails.application.initialized? + + warn '[LaunchDarklyObservability] The LaunchDarkly client was created after Rails finished ' \ + 'booting, so the Rails auto-instrumentation (ActionPack, ActiveRecord, ...) could not be ' \ + 'installed. To enable it, set LAUNCHDARKLY_SDK_KEY in the environment before Rails boots, ' \ + 'or create the LaunchDarkly client from a config/initializer.' end # Configure auto-instrumentations with sensible defaults. diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb index c6238dd549..90045b7df4 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb @@ -146,6 +146,20 @@ def otel_logger_provider_available? app.middleware.insert_before(0, LaunchDarklyObservability::Middleware) end + # Install OpenTelemetry auto-instrumentation during boot so the Rails-family + # instrumentations attach even when the LaunchDarkly client is created + # lazily (after boot) rather than in an initializer. + # + # Runs `after: :load_config_initializers` on purpose: if the client was + # created in a config/initializer during boot, it has already configured + # OpenTelemetry (with the user's options), so `install_rails_instrumentation` + # detects the existing provider and no-ops. Otherwise it installs the + # default instrumentation now — still during boot, before eager loading and + # the ActiveSupport.on_load hooks that the instrumentations depend on. + initializer 'launchdarkly_observability.install_instrumentation', after: :load_config_initializers do + LaunchDarklyObservability.install_rails_instrumentation + end + config.after_initialize do if defined?(ActionController::Base) ActionController::Base.include(LaunchDarklyObservability::ControllerHelpers) diff --git a/sdk/@launchdarkly/observability-ruby/test/boot_instrumentation_test.rb b/sdk/@launchdarkly/observability-ruby/test/boot_instrumentation_test.rb new file mode 100644 index 0000000000..5706d15975 --- /dev/null +++ b/sdk/@launchdarkly/observability-ruby/test/boot_instrumentation_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests for boot-time auto-instrumentation install +# (LaunchDarklyObservability.install_rails_instrumentation), which lets the OTel +# Rails-family instrumentations attach during Rails boot even when the LD client +# is created lazily afterward. See the Railtie in rails.rb. +class BootInstrumentationTest < Minitest::Test + include TestHelper + + def setup + LaunchDarklyObservability.reset_instrumentation_state! + @saved_key = ENV.fetch('LAUNCHDARKLY_SDK_KEY', nil) + end + + def teardown + LaunchDarklyObservability.reset_instrumentation_state! + if @saved_key.nil? + ENV.delete('LAUNCHDARKLY_SDK_KEY') + else + ENV['LAUNCHDARKLY_SDK_KEY'] = @saved_key + end + end + + def test_not_installed_at_boot_by_default + refute LaunchDarklyObservability.instrumentation_installed_at_boot? + end + + def test_returns_false_without_project_id_or_env_key + ENV.delete('LAUNCHDARKLY_SDK_KEY') + refute LaunchDarklyObservability.install_rails_instrumentation(project_id: nil) + refute LaunchDarklyObservability.instrumentation_installed_at_boot? + end + + def test_skips_when_sdk_provider_already_configured + # Mirrors a boot-time init: the client already configured OpenTelemetry in a + # config/initializer, so the Railtie must not reconfigure (which would drop + # the existing exporters). + reset_opentelemetry + assert_kind_of OpenTelemetry::SDK::Trace::TracerProvider, OpenTelemetry.tracer_provider + + refute LaunchDarklyObservability.install_rails_instrumentation(project_id: 'my-project'), + 'should skip install when an SDK tracer provider is already configured' + refute LaunchDarklyObservability.instrumentation_installed_at_boot? + end + + def test_reset_clears_flag + LaunchDarklyObservability.instance_variable_set(:@instrumentation_installed_at_boot, true) + assert LaunchDarklyObservability.instrumentation_installed_at_boot? + LaunchDarklyObservability.reset_instrumentation_state! + refute LaunchDarklyObservability.instrumentation_installed_at_boot? + end + + def test_configure_does_not_replace_provider_when_installed_at_boot + # When boot already installed instrumentation, registering the plugin must + # only attach exporters to the existing provider — not reconfigure it, which + # would drop the boot-time Rails instrumentation in the lazy-init case. + reset_opentelemetry + provider_before = OpenTelemetry.tracer_provider + + config = LaunchDarklyObservability::OpenTelemetryConfig.new( + project_id: 'my-project', + otlp_endpoint: 'http://localhost:4318', + enable_logs: false, + enable_metrics: false + ) + + LaunchDarklyObservability.stub(:instrumentation_installed_at_boot?, true) do + config.configure + end + + assert_same provider_before, OpenTelemetry.tracer_provider, + 'configure must attach to the existing provider, not replace it, when installed at boot' + end +end From 6d5a4a587bf51bdbb495b59d081be8a408ab19d0 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Tue, 2 Jun 2026 16:07:25 -0500 Subject: [PATCH 05/18] test(ruby): add lazy-init e2e variant proving boot-time instrumentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../rails/demo/app/models/lazy_ld_client.rb | 20 ++++++++ .../demo/config/initializers/launchdarkly.rb | 28 +++++++---- .../lazy_init_instrumentation_test.rb | 50 +++++++++++++++++++ 3 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 e2e/ruby/rails/demo/app/models/lazy_ld_client.rb create mode 100644 e2e/ruby/rails/demo/test/integration/lazy_init_instrumentation_test.rb diff --git a/e2e/ruby/rails/demo/app/models/lazy_ld_client.rb b/e2e/ruby/rails/demo/app/models/lazy_ld_client.rb new file mode 100644 index 0000000000..dc70f01581 --- /dev/null +++ b/e2e/ruby/rails/demo/app/models/lazy_ld_client.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Mirrors an application that creates the LaunchDarkly client lazily — e.g. from +# a model on first use — instead of during boot in a config/initializer. Enabled +# with LD_LAZY_INIT=1 (see config/initializers/launchdarkly.rb) and exercised by +# the lazy-init e2e test, which verifies the Railtie installs OpenTelemetry +# auto-instrumentation at boot even though the client is created afterward. +class LazyLdClient + def self.instance + @instance ||= begin + plugin = LaunchDarklyObservability::Plugin.new( + otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT), + service_name: 'rails-demo-app', + service_version: '1.0.0' + ) + config = LaunchDarkly::Config.new(plugins: [plugin]) + LaunchDarkly::LDClient.new(ENV.fetch('LAUNCHDARKLY_SDK_KEY', ''), config) + end + end +end diff --git a/e2e/ruby/rails/demo/config/initializers/launchdarkly.rb b/e2e/ruby/rails/demo/config/initializers/launchdarkly.rb index 61c9fb58f2..e67dce87fe 100644 --- a/e2e/ruby/rails/demo/config/initializers/launchdarkly.rb +++ b/e2e/ruby/rails/demo/config/initializers/launchdarkly.rb @@ -1,16 +1,22 @@ require 'launchdarkly-server-sdk' require 'launchdarkly_observability' -observability_plugin = LaunchDarklyObservability::Plugin.new( - otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT), - service_name: 'rails-demo-app', - service_version: '1.0.0' -) +# Set LD_LAZY_INIT=1 to defer LaunchDarkly client creation until first use +# (see app/models/lazy_ld_client.rb) instead of creating it here during boot. This +# mirrors apps that build the client lazily — e.g. from a model on first request +# — and exercises the Railtie's boot-time auto-instrumentation install path. +unless ENV['LD_LAZY_INIT'] + observability_plugin = LaunchDarklyObservability::Plugin.new( + otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT), + service_name: 'rails-demo-app', + service_version: '1.0.0' + ) -sdk_key = ENV.fetch('LAUNCHDARKLY_SDK_KEY') do - Rails.logger.warn '[LaunchDarkly] LAUNCHDARKLY_SDK_KEY not set, client will not connect' - '' -end + sdk_key = ENV.fetch('LAUNCHDARKLY_SDK_KEY') do + Rails.logger.warn '[LaunchDarkly] LAUNCHDARKLY_SDK_KEY not set, client will not connect' + '' + end -config = LaunchDarkly::Config.new(plugins: [observability_plugin]) -Rails.configuration.ld_client = LaunchDarkly::LDClient.new(sdk_key, config) + config = LaunchDarkly::Config.new(plugins: [observability_plugin]) + Rails.configuration.ld_client = LaunchDarkly::LDClient.new(sdk_key, config) +end diff --git a/e2e/ruby/rails/demo/test/integration/lazy_init_instrumentation_test.rb b/e2e/ruby/rails/demo/test/integration/lazy_init_instrumentation_test.rb new file mode 100644 index 0000000000..466e3c6854 --- /dev/null +++ b/e2e/ruby/rails/demo/test/integration/lazy_init_instrumentation_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'open3' + +# Regression test for lazy LaunchDarkly client initialization. +# +# When the client is created lazily (after Rails has booted) rather than in a +# config/initializer, the OTel Rails-family instrumentations used to report +# "Instrumentation: ... failed to install" — their ActiveSupport.on_load hooks +# had already fired by the time the plugin configured OpenTelemetry. The gem's +# Railtie now installs auto-instrumentation during boot, independent of when the +# client is created, so it attaches regardless. +# +# This must run in a SEPARATE process because instrumentation install is a +# one-time, global side effect: the main test suite boots with the client +# created during boot, so we boot a fresh Rails process with LD_LAZY_INIT=1 (no +# client created at boot — see config/initializers/launchdarkly.rb) and assert +# the Rails instrumentations are installed anyway. +class LazyInitInstrumentationTest < ActiveSupport::TestCase + CHECK_SCRIPT = <<~'RUBY' + names = %w[Rack ActionPack ActiveRecord ActiveSupport Rails] + installed = names.all? do |n| + Object.const_get("OpenTelemetry::Instrumentation::#{n}::Instrumentation").instance.installed? + end + # Creating the client lazily (post-boot) must still work without raising. + LazyLdClient.instance + puts(installed ? 'LAZY_INSTRUMENTATION_OK' : 'LAZY_INSTRUMENTATION_FAILED') + RUBY + + test 'rails auto-instrumentation installs at boot even when the client is created lazily' do + output = boot_lazy_and_run(CHECK_SCRIPT) + + assert_includes output, 'LAZY_INSTRUMENTATION_OK', + "Rails auto-instrumentation should install at boot in lazy mode.\n--- subprocess output ---\n#{output}" + end + + private + + def boot_lazy_and_run(script) + env = { + 'LD_LAZY_INIT' => '1', + 'LAUNCHDARKLY_SDK_KEY' => 'sdk-test-0000000000000000000000', + 'RAILS_ENV' => 'test' + } + rails_bin = Rails.root.join('bin/rails').to_s + stdout, stderr, _status = Open3.capture3(env, rails_bin, 'runner', script, chdir: Rails.root.to_s) + "#{stdout}\n#{stderr}" + end +end From 5752583121f3bc387c9758711a030eb55a657e12 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 18 Jun 2026 12:41:48 -0500 Subject: [PATCH 06/18] test(ruby): add Rails 7.0 e2e repro for old-Rails instrumentation failure (red) Reproduces the CardFlight failure: on Rails 7.0 with the current gem, opentelemetry-instrumentation-all resolves to the latest (0.94 -> instrumentation-rails 0.42), whose Rails-family members raised their floor to Rails 7.1. They fail their runtime compatible? check, log a flurry of 'failed to install' warnings, and never attach, so no autoinstrumented HTTP server span is produced. - e2e/ruby/rails/demo-rails70: copy of demo pinned to rails ~> 7.0 (Ruby 3.3.4 kept so bundler still resolves the latest instrumentation, faithfully matching a current-Ruby Rails 7.0 customer). - test/support/otlp_sink.rb: in-process OTLP/protobuf sink (pure Ruby, decodes via the exporter gems' proto classes) so the e2e test asserts telemetry over the real export path under bundle exec rake (no Docker/Node). - test/integration/otlp_export_e2e_test.rb: asserts traces (server span), a log record, and a captured exception all reach the sink. RED here. Boot logs show 8 Rails-family 'failed to install' warnings; the new test and the existing observability_instrumentation_test both fail (missing server span). --- e2e/ruby/rails/demo-rails70/.gitattributes | 7 + e2e/ruby/rails/demo-rails70/.gitignore | 35 ++ e2e/ruby/rails/demo-rails70/.rubocop.yml | 37 ++ e2e/ruby/rails/demo-rails70/.ruby-version | 1 + e2e/ruby/rails/demo-rails70/Gemfile | 93 +++ e2e/ruby/rails/demo-rails70/Gemfile.lock | 551 ++++++++++++++++++ e2e/ruby/rails/demo-rails70/README.md | 24 + e2e/ruby/rails/demo-rails70/Rakefile | 8 + .../app/assets/config/manifest.js | 4 + .../demo-rails70/app/assets/images/.keep | 0 .../app/assets/stylesheets/application.css | 458 +++++++++++++++ .../app/channels/application_cable/channel.rb | 6 + .../channels/application_cable/connection.rb | 6 + .../app/controllers/application_controller.rb | 17 + .../app/controllers/concerns/.keep | 0 .../app/controllers/errors_controller.rb | 31 + .../app/controllers/flags_controller.rb | 126 ++++ .../app/controllers/logs_controller.rb | 23 + .../app/controllers/pages_controller.rb | 26 + .../app/controllers/telemetry_controller.rb | 8 + .../app/controllers/traces_controller.rb | 30 + .../app/helpers/application_helper.rb | 4 + .../demo-rails70/app/helpers/pages_helper.rb | 4 + .../app/javascript/application.js | 3 + .../app/javascript/controllers/application.js | 9 + .../controllers/hello_controller.js | 7 + .../app/javascript/controllers/index.js | 11 + .../demo-rails70/app/jobs/application_job.rb | 9 + .../app/mailers/application_mailer.rb | 6 + .../app/models/application_record.rb | 5 + .../demo-rails70/app/models/concerns/.keep | 0 .../demo-rails70/app/models/lazy_ld_client.rb | 20 + .../rails/demo-rails70/app/models/trace.rb | 2 + .../app/views/flags/index.html.erb | 86 +++ .../app/views/flags/show.html.erb | 30 + .../app/views/layouts/application.html.erb | 35 ++ .../app/views/layouts/mailer.html.erb | 13 + .../app/views/layouts/mailer.text.erb | 1 + .../app/views/pages/home.html.erb | 114 ++++ e2e/ruby/rails/demo-rails70/bin/bundle | 113 ++++ e2e/ruby/rails/demo-rails70/bin/importmap | 5 + e2e/ruby/rails/demo-rails70/bin/rails | 6 + e2e/ruby/rails/demo-rails70/bin/rake | 6 + e2e/ruby/rails/demo-rails70/bin/setup | 35 ++ e2e/ruby/rails/demo-rails70/config.ru | 8 + .../rails/demo-rails70/config/application.rb | 24 + e2e/ruby/rails/demo-rails70/config/boot.rb | 13 + e2e/ruby/rails/demo-rails70/config/cable.yml | 11 + .../rails/demo-rails70/config/database.yml | 25 + .../rails/demo-rails70/config/environment.rb | 7 + .../config/environments/development.rb | 72 +++ .../config/environments/production.rb | 95 +++ .../demo-rails70/config/environments/test.rb | 62 ++ .../rails/demo-rails70/config/importmap.rb | 9 + .../config/initializers/assets.rb | 14 + .../initializers/content_security_policy.rb | 27 + .../initializers/filter_parameter_logging.rb | 10 + .../config/initializers/inflections.rb | 18 + .../config/initializers/launchdarkly.rb | 22 + .../config/initializers/permissions_policy.rb | 13 + .../rails/demo-rails70/config/locales/en.yml | 33 ++ e2e/ruby/rails/demo-rails70/config/puma.rb | 51 ++ e2e/ruby/rails/demo-rails70/config/routes.rb | 30 + .../rails/demo-rails70/config/storage.yml | 33 ++ .../migrate/20240829164231_create_traces.rb | 10 + e2e/ruby/rails/demo-rails70/db/schema.rb | 21 + e2e/ruby/rails/demo-rails70/db/seeds.rb | 9 + e2e/ruby/rails/demo-rails70/lib/assets/.keep | 0 e2e/ruby/rails/demo-rails70/lib/tasks/.keep | 0 e2e/ruby/rails/demo-rails70/public/404.html | 73 +++ e2e/ruby/rails/demo-rails70/public/422.html | 73 +++ e2e/ruby/rails/demo-rails70/public/500.html | 69 +++ .../public/apple-touch-icon-precomposed.png | 0 .../demo-rails70/public/apple-touch-icon.png | 0 .../rails/demo-rails70/public/favicon.ico | 0 e2e/ruby/rails/demo-rails70/public/robots.txt | 1 + e2e/ruby/rails/demo-rails70/storage/.keep | 0 .../test/application_system_test_case.rb | 7 + .../application_cable/connection_test.rb | 15 + .../rails/demo-rails70/test/controllers/.keep | 0 .../test/controllers/pages_controller_test.rb | 10 + .../demo-rails70/test/fixtures/files/.keep | 0 .../rails/demo-rails70/test/helpers/.keep | 0 .../rails/demo-rails70/test/integration/.keep | 0 .../lazy_init_instrumentation_test.rb | 50 ++ .../observability_instrumentation_test.rb | 45 ++ .../test/integration/otlp_export_e2e_test.rb | 69 +++ .../rails/demo-rails70/test/mailers/.keep | 0 e2e/ruby/rails/demo-rails70/test/models/.keep | 0 .../demo-rails70/test/support/otlp_sink.rb | 151 +++++ e2e/ruby/rails/demo-rails70/test/system/.keep | 0 .../rails/demo-rails70/test/test_helper.rb | 38 ++ e2e/ruby/rails/demo-rails70/vendor/.keep | 0 .../demo-rails70/vendor/javascript/.keep | 0 94 files changed, 3233 insertions(+) create mode 100644 e2e/ruby/rails/demo-rails70/.gitattributes create mode 100644 e2e/ruby/rails/demo-rails70/.gitignore create mode 100644 e2e/ruby/rails/demo-rails70/.rubocop.yml create mode 100644 e2e/ruby/rails/demo-rails70/.ruby-version create mode 100644 e2e/ruby/rails/demo-rails70/Gemfile create mode 100644 e2e/ruby/rails/demo-rails70/Gemfile.lock create mode 100644 e2e/ruby/rails/demo-rails70/README.md create mode 100644 e2e/ruby/rails/demo-rails70/Rakefile create mode 100644 e2e/ruby/rails/demo-rails70/app/assets/config/manifest.js create mode 100644 e2e/ruby/rails/demo-rails70/app/assets/images/.keep create mode 100644 e2e/ruby/rails/demo-rails70/app/assets/stylesheets/application.css create mode 100644 e2e/ruby/rails/demo-rails70/app/channels/application_cable/channel.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/channels/application_cable/connection.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/application_controller.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/concerns/.keep create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/errors_controller.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/flags_controller.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/logs_controller.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/pages_controller.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/telemetry_controller.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/controllers/traces_controller.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/helpers/application_helper.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/helpers/pages_helper.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/javascript/application.js create mode 100644 e2e/ruby/rails/demo-rails70/app/javascript/controllers/application.js create mode 100644 e2e/ruby/rails/demo-rails70/app/javascript/controllers/hello_controller.js create mode 100644 e2e/ruby/rails/demo-rails70/app/javascript/controllers/index.js create mode 100644 e2e/ruby/rails/demo-rails70/app/jobs/application_job.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/mailers/application_mailer.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/models/application_record.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/models/concerns/.keep create mode 100644 e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/models/trace.rb create mode 100644 e2e/ruby/rails/demo-rails70/app/views/flags/index.html.erb create mode 100644 e2e/ruby/rails/demo-rails70/app/views/flags/show.html.erb create mode 100644 e2e/ruby/rails/demo-rails70/app/views/layouts/application.html.erb create mode 100644 e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.html.erb create mode 100644 e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.text.erb create mode 100644 e2e/ruby/rails/demo-rails70/app/views/pages/home.html.erb create mode 100755 e2e/ruby/rails/demo-rails70/bin/bundle create mode 100755 e2e/ruby/rails/demo-rails70/bin/importmap create mode 100755 e2e/ruby/rails/demo-rails70/bin/rails create mode 100755 e2e/ruby/rails/demo-rails70/bin/rake create mode 100755 e2e/ruby/rails/demo-rails70/bin/setup create mode 100644 e2e/ruby/rails/demo-rails70/config.ru create mode 100644 e2e/ruby/rails/demo-rails70/config/application.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/boot.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/cable.yml create mode 100644 e2e/ruby/rails/demo-rails70/config/database.yml create mode 100644 e2e/ruby/rails/demo-rails70/config/environment.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/environments/development.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/environments/production.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/environments/test.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/importmap.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/initializers/assets.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/initializers/content_security_policy.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/initializers/filter_parameter_logging.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/initializers/inflections.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/initializers/permissions_policy.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/locales/en.yml create mode 100644 e2e/ruby/rails/demo-rails70/config/puma.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/routes.rb create mode 100644 e2e/ruby/rails/demo-rails70/config/storage.yml create mode 100644 e2e/ruby/rails/demo-rails70/db/migrate/20240829164231_create_traces.rb create mode 100644 e2e/ruby/rails/demo-rails70/db/schema.rb create mode 100644 e2e/ruby/rails/demo-rails70/db/seeds.rb create mode 100644 e2e/ruby/rails/demo-rails70/lib/assets/.keep create mode 100644 e2e/ruby/rails/demo-rails70/lib/tasks/.keep create mode 100644 e2e/ruby/rails/demo-rails70/public/404.html create mode 100644 e2e/ruby/rails/demo-rails70/public/422.html create mode 100644 e2e/ruby/rails/demo-rails70/public/500.html create mode 100644 e2e/ruby/rails/demo-rails70/public/apple-touch-icon-precomposed.png create mode 100644 e2e/ruby/rails/demo-rails70/public/apple-touch-icon.png create mode 100644 e2e/ruby/rails/demo-rails70/public/favicon.ico create mode 100644 e2e/ruby/rails/demo-rails70/public/robots.txt create mode 100644 e2e/ruby/rails/demo-rails70/storage/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/application_system_test_case.rb create mode 100644 e2e/ruby/rails/demo-rails70/test/channels/application_cable/connection_test.rb create mode 100644 e2e/ruby/rails/demo-rails70/test/controllers/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/controllers/pages_controller_test.rb create mode 100644 e2e/ruby/rails/demo-rails70/test/fixtures/files/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/helpers/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/integration/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/integration/lazy_init_instrumentation_test.rb create mode 100644 e2e/ruby/rails/demo-rails70/test/integration/observability_instrumentation_test.rb create mode 100644 e2e/ruby/rails/demo-rails70/test/integration/otlp_export_e2e_test.rb create mode 100644 e2e/ruby/rails/demo-rails70/test/mailers/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/models/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/support/otlp_sink.rb create mode 100644 e2e/ruby/rails/demo-rails70/test/system/.keep create mode 100644 e2e/ruby/rails/demo-rails70/test/test_helper.rb create mode 100644 e2e/ruby/rails/demo-rails70/vendor/.keep create mode 100644 e2e/ruby/rails/demo-rails70/vendor/javascript/.keep diff --git a/e2e/ruby/rails/demo-rails70/.gitattributes b/e2e/ruby/rails/demo-rails70/.gitattributes new file mode 100644 index 0000000000..31eeee0b6a --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/.gitattributes @@ -0,0 +1,7 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/e2e/ruby/rails/demo-rails70/.gitignore b/e2e/ruby/rails/demo-rails70/.gitignore new file mode 100644 index 0000000000..886f714b42 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/e2e/ruby/rails/demo-rails70/.rubocop.yml b/e2e/ruby/rails/demo-rails70/.rubocop.yml new file mode 100644 index 0000000000..c3ba33221b --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/.rubocop.yml @@ -0,0 +1,37 @@ +inherit_from: '../../../../sdk/highlight-ruby/highlight/.rubocop.yml' + +AllCops: + SuggestExtensions: false + TargetRubyVersion: 3.0 + Exclude: + - 'db/**/*' + - 'spec/**/*' + - 'test/**/*' + - 'config/**/*' + - 'bin/**/*' + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/Documentation: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Layout/LineLength: + Exclude: + - 'bin/bundle' + +Style/IfUnlessModifier: + Exclude: + - 'bin/bundle' diff --git a/e2e/ruby/rails/demo-rails70/.ruby-version b/e2e/ruby/rails/demo-rails70/.ruby-version new file mode 100644 index 0000000000..a0891f563f --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/.ruby-version @@ -0,0 +1 @@ +3.3.4 diff --git a/e2e/ruby/rails/demo-rails70/Gemfile b/e2e/ruby/rails/demo-rails70/Gemfile new file mode 100644 index 0000000000..0ba87cc288 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/Gemfile @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '~> 3.3.4' + +# Rails 7.0 on purpose: this app reproduces the CardFlight failure where the +# OTel Rails-family instrumentations (opentelemetry-instrumentation-rails >= 0.35) +# raised their floor to Rails 7.1, so on Rails 7.0 they fail their runtime +# `compatible?` check and never attach. Ruby stays at 3.3.4 (>= 3.2) so bundler +# resolves the LATEST opentelemetry-instrumentation-all — the same versions a +# real Rails 7.0 customer on a current Ruby gets. (On Ruby < 3.2 bundler would +# self-heal to an older instrumentation-all and the bug would not reproduce.) +gem 'rails', '~> 7.0.0' + +# Rails 7.0's test runner (railties test_unit line filtering) is incompatible +# with minitest 6.x (the `run` arity changed). Pin to 5.x so the suite runs. +# Unrelated to the instrumentation bug — just a Rails-7.0 test-toolchain pin. +gem 'minitest', '~> 5.0' + +# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] +gem 'sprockets-rails' + +# Use sqlite3 as the database for Active Record +gem 'sqlite3', '~> 1.4' + +# Use the Puma web server [https://github.com/puma/puma] +gem 'puma', '~> 6.0' + +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem 'importmap-rails' + +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem 'turbo-rails' + +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem 'stimulus-rails' + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem 'jbuilder' + +# Use Redis adapter to run Action Cable in production +gem 'redis', '~> 4.0' + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] + +# Reduces boot times through caching; required in config/boot.rb +gem 'bootsnap', require: false + +# Use Sass to process CSS +# gem "sassc-rails" + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem 'debug', platforms: %i[mri mingw x64_mingw] +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem 'web-console' + + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] + # gem "rack-mini-profiler" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem 'capybara' + gem 'selenium-webdriver' + + # In-process OTLP sink (test/support/otlp_sink.rb) uses WEBrick, which is no + # longer a default gem since Ruby 3.0, so it must be declared to be available + # under `bundle exec`. + gem 'webrick' +end + +# LaunchDarkly SDK and Observability Plugin +gem 'launchdarkly-server-sdk', '~> 8.0' +gem 'launchdarkly-observability', path: '../../../../sdk/@launchdarkly/observability-ruby' diff --git a/e2e/ruby/rails/demo-rails70/Gemfile.lock b/e2e/ruby/rails/demo-rails70/Gemfile.lock new file mode 100644 index 0000000000..5ed1ea150f --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/Gemfile.lock @@ -0,0 +1,551 @@ +PATH + remote: ../../../../sdk/@launchdarkly/observability-ruby + specs: + launchdarkly-observability (0.2.1) + launchdarkly-server-sdk (>= 8.11.0) + opentelemetry-exporter-otlp (~> 0.28) + opentelemetry-exporter-otlp-logs (~> 0.1) + opentelemetry-instrumentation-all (~> 0.62) + opentelemetry-logs-sdk (~> 0.1) + opentelemetry-sdk (~> 1.4) + opentelemetry-semantic_conventions (~> 1.10) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.10) + actionpack (= 7.0.10) + activesupport (= 7.0.10) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.10) + actionpack (= 7.0.10) + activejob (= 7.0.10) + activerecord (= 7.0.10) + activestorage (= 7.0.10) + activesupport (= 7.0.10) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.10) + actionpack (= 7.0.10) + actionview (= 7.0.10) + activejob (= 7.0.10) + activesupport (= 7.0.10) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.0) + actionpack (7.0.10) + actionview (= 7.0.10) + activesupport (= 7.0.10) + racc + rack (~> 2.0, >= 2.2.4) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.10) + actionpack (= 7.0.10) + activerecord (= 7.0.10) + activestorage (= 7.0.10) + activesupport (= 7.0.10) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.10) + activesupport (= 7.0.10) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.10) + activesupport (= 7.0.10) + globalid (>= 0.3.6) + activemodel (7.0.10) + activesupport (= 7.0.10) + activerecord (7.0.10) + activemodel (= 7.0.10) + activesupport (= 7.0.10) + activestorage (7.0.10) + actionpack (= 7.0.10) + activejob (= 7.0.10) + activerecord (= 7.0.10) + activesupport (= 7.0.10) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.10) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) + tzinfo (~> 2.0) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + bindex (0.8.1) + bootsnap (1.24.6) + msgpack (~> 1.2) + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + concurrent-ruby (1.3.7) + crass (1.0.6) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + domain_name (0.6.20240107) + drb (2.2.3) + erb (6.0.4) + erubi (1.13.1) + globalid (1.3.0) + activesupport (>= 6.1) + google-protobuf (4.35.1) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) + googleapis-common-protos-types (1.23.0) + google-protobuf (~> 4.26) + http (6.0.3) + http-cookie (~> 1.0) + llhttp (~> 0.6.1) + http-cookie (1.1.6) + domain_name (~> 0.5) + i18n (1.15.1) + concurrent-ruby (~> 1.0) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.15.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.19.9) + launchdarkly-server-sdk (8.14.0) + benchmark (~> 0.1, >= 0.1.1) + concurrent-ruby (~> 1.1) + http (>= 4.4.0, < 7.0.0) + json (~> 2.3) + ld-eventsource (= 2.6.0) + observer (~> 0.1.2) + openssl (>= 3.1.2, < 5.0) + semantic (~> 1.6) + zlib (~> 3.1) + ld-eventsource (2.6.0) + concurrent-ruby (~> 1.0) + http (>= 4.4.1, < 7.0.0) + llhttp (0.6.1) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.2.1) + matrix (0.4.3) + method_source (1.1.0) + mini_mime (1.1.5) + minitest (5.27.0) + msgpack (1.8.3) + mutex_m (0.3.0) + net-imap (0.6.4.1) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-musl) + racc (~> 1.4) + observer (0.1.2) + openssl (4.0.2) + opentelemetry-api (1.10.0) + logger + opentelemetry-common (0.25.0) + opentelemetry-api (~> 1.0) + opentelemetry-exporter-otlp (0.34.0) + google-protobuf (>= 3.18) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-sdk (~> 1.10) + opentelemetry-semantic_conventions + opentelemetry-exporter-otlp-logs (0.5.1) + google-protobuf (>= 3.18) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-logs-api (~> 0.1) + opentelemetry-logs-sdk (~> 0.1) + opentelemetry-sdk + opentelemetry-semantic_conventions + opentelemetry-helpers-mysql (0.6.0) + opentelemetry-api (~> 1.7) + opentelemetry-common (~> 0.21) + opentelemetry-helpers-sql (0.4.0) + opentelemetry-api (~> 1.7) + opentelemetry-helpers-sql-processor (0.5.0) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.21) + opentelemetry-instrumentation-action_mailer (0.8.1) + opentelemetry-instrumentation-active_support (~> 0.10) + opentelemetry-instrumentation-action_pack (0.18.0) + opentelemetry-instrumentation-rack (~> 0.29) + opentelemetry-instrumentation-action_view (0.13.0) + opentelemetry-instrumentation-active_support (~> 0.10) + opentelemetry-instrumentation-active_job (0.13.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-active_model_serializers (0.25.0) + opentelemetry-instrumentation-active_support (>= 0.7.0) + opentelemetry-instrumentation-active_record (0.13.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-active_storage (0.5.1) + opentelemetry-instrumentation-active_support (~> 0.10) + opentelemetry-instrumentation-active_support (0.12.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-all (0.94.0) + opentelemetry-instrumentation-active_model_serializers (~> 0.25.0) + opentelemetry-instrumentation-anthropic (~> 0.5.0) + opentelemetry-instrumentation-aws_lambda (~> 0.7.0) + opentelemetry-instrumentation-aws_sdk (~> 0.12.0) + opentelemetry-instrumentation-bunny (~> 0.25.0) + opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0) + opentelemetry-instrumentation-dalli (~> 0.30.0) + opentelemetry-instrumentation-delayed_job (~> 0.26.0) + opentelemetry-instrumentation-ethon (~> 0.29.0) + opentelemetry-instrumentation-excon (~> 0.29.0) + opentelemetry-instrumentation-faraday (~> 0.33.0) + opentelemetry-instrumentation-grape (~> 0.7.0) + opentelemetry-instrumentation-graphql (~> 0.32.0) + opentelemetry-instrumentation-grpc (~> 0.5.0) + opentelemetry-instrumentation-gruf (~> 0.6.0) + opentelemetry-instrumentation-http (~> 0.30.0) + opentelemetry-instrumentation-http_client (~> 0.29.0) + opentelemetry-instrumentation-httpx (~> 0.8.0) + opentelemetry-instrumentation-koala (~> 0.24.0) + opentelemetry-instrumentation-lmdb (~> 0.26.0) + opentelemetry-instrumentation-mongo (~> 0.26.0) + opentelemetry-instrumentation-mysql2 (~> 0.34.0) + opentelemetry-instrumentation-net_http (~> 0.29.0) + opentelemetry-instrumentation-pg (~> 0.36.0) + opentelemetry-instrumentation-que (~> 0.13.0) + opentelemetry-instrumentation-racecar (~> 0.7.0) + opentelemetry-instrumentation-rack (~> 0.31.0) + opentelemetry-instrumentation-rails (~> 0.42.0) + opentelemetry-instrumentation-rake (~> 0.6.0) + opentelemetry-instrumentation-rdkafka (~> 0.10.0) + opentelemetry-instrumentation-redis (~> 0.29.0) + opentelemetry-instrumentation-resque (~> 0.9.0) + opentelemetry-instrumentation-restclient (~> 0.28.0) + opentelemetry-instrumentation-ruby_kafka (~> 0.25.0) + opentelemetry-instrumentation-sidekiq (~> 0.29.0) + opentelemetry-instrumentation-sinatra (~> 0.30.0) + opentelemetry-instrumentation-trilogy (~> 0.69.0) + opentelemetry-instrumentation-anthropic (0.5.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-aws_lambda (0.7.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-aws_sdk (0.12.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-base (0.26.1) + opentelemetry-api (~> 1.7) + opentelemetry-common (~> 0.21) + opentelemetry-registry (~> 0.1) + opentelemetry-instrumentation-bunny (0.25.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-concurrent_ruby (0.25.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-dalli (0.30.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-delayed_job (0.26.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-ethon (0.29.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-excon (0.29.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-faraday (0.33.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-grape (0.7.0) + opentelemetry-instrumentation-rack (~> 0.29) + opentelemetry-instrumentation-graphql (0.32.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-grpc (0.5.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-gruf (0.6.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-http (0.30.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-http_client (0.29.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-httpx (0.8.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-koala (0.24.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-lmdb (0.26.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-mongo (0.26.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-mysql2 (0.34.0) + opentelemetry-helpers-mysql + opentelemetry-helpers-sql + opentelemetry-helpers-sql-processor + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-net_http (0.29.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-pg (0.36.0) + opentelemetry-helpers-sql + opentelemetry-helpers-sql-processor + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-que (0.13.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-racecar (0.7.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-rack (0.31.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-rails (0.42.0) + opentelemetry-instrumentation-action_mailer (~> 0.7) + opentelemetry-instrumentation-action_pack (~> 0.17) + opentelemetry-instrumentation-action_view (~> 0.12) + opentelemetry-instrumentation-active_job (~> 0.11) + opentelemetry-instrumentation-active_record (~> 0.12) + opentelemetry-instrumentation-active_storage (~> 0.4) + opentelemetry-instrumentation-active_support (~> 0.11) + opentelemetry-instrumentation-concurrent_ruby (~> 0.25) + opentelemetry-instrumentation-rake (0.6.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-rdkafka (0.10.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-redis (0.29.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-resque (0.9.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-restclient (0.28.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-ruby_kafka (0.25.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-sidekiq (0.29.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-sinatra (0.30.0) + opentelemetry-instrumentation-rack (~> 0.29) + opentelemetry-instrumentation-trilogy (0.69.0) + opentelemetry-helpers-mysql + opentelemetry-helpers-sql + opentelemetry-helpers-sql-processor + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-semantic_conventions (>= 1.8.0) + opentelemetry-logs-api (0.4.0) + opentelemetry-api (~> 1.0) + opentelemetry-logs-sdk (0.6.0) + opentelemetry-api (~> 1.2) + opentelemetry-logs-api (~> 0.1) + opentelemetry-sdk (~> 1.3) + opentelemetry-registry (0.6.0) + opentelemetry-api (~> 1.1) + opentelemetry-sdk (1.12.0) + logger + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.41.0) + opentelemetry-api (~> 1.0) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.4.0) + date + stringio + public_suffix (7.0.5) + puma (6.6.1) + nio4r (~> 2.0) + racc (1.8.1) + rack (2.2.23) + rack-test (2.2.0) + rack (>= 1.3) + rails (7.0.10) + actioncable (= 7.0.10) + actionmailbox (= 7.0.10) + actionmailer (= 7.0.10) + actionpack (= 7.0.10) + actiontext (= 7.0.10) + actionview (= 7.0.10) + activejob (= 7.0.10) + activemodel (= 7.0.10) + activerecord (= 7.0.10) + activestorage (= 7.0.10) + activesupport (= 7.0.10) + bundler (>= 1.15.0) + railties (= 7.0.10) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.0.10) + actionpack (= 7.0.10) + activesupport (= 7.0.10) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rake (13.4.2) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + redis (4.8.1) + regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rubyzip (3.4.0) + securerandom (0.4.1) + selenium-webdriver (4.45.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + semantic (1.6.1) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (1.7.3-aarch64-linux) + sqlite3 (1.7.3-arm-linux) + sqlite3 (1.7.3-arm64-darwin) + sqlite3 (1.7.3-x86_64-darwin) + sqlite3 (1.7.3-x86_64-linux) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.2.0) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) + turbo-rails (2.0.12) + actionpack (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webrick (1.9.2) + websocket (1.2.11) + websocket-driver (0.8.1) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.8.2) + zlib (3.2.3) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bootsnap + capybara + debug + importmap-rails + jbuilder + launchdarkly-observability! + launchdarkly-server-sdk (~> 8.0) + minitest (~> 5.0) + puma (~> 6.0) + rails (~> 7.0.0) + redis (~> 4.0) + selenium-webdriver + sprockets-rails + sqlite3 (~> 1.4) + stimulus-rails + turbo-rails + tzinfo-data + web-console + webrick + +RUBY VERSION + ruby 3.3.4p94 + +BUNDLED WITH + 2.5.11 diff --git a/e2e/ruby/rails/demo-rails70/README.md b/e2e/ruby/rails/demo-rails70/README.md new file mode 100644 index 0000000000..28feefee53 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/README.md @@ -0,0 +1,24 @@ +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +- Ruby version + +- System dependencies + +- Configuration + +- Database creation + +- Database initialization + +- How to run the test suite + +- Services (job queues, cache servers, search engines, etc.) + +- Deployment instructions + +- ... diff --git a/e2e/ruby/rails/demo-rails70/Rakefile b/e2e/ruby/rails/demo-rails70/Rakefile new file mode 100644 index 0000000000..488c551fee --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/e2e/ruby/rails/demo-rails70/app/assets/config/manifest.js b/e2e/ruby/rails/demo-rails70/app/assets/config/manifest.js new file mode 100644 index 0000000000..ddd546a0be --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/assets/config/manifest.js @@ -0,0 +1,4 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css +//= link_tree ../../javascript .js +//= link_tree ../../../vendor/javascript .js diff --git a/e2e/ruby/rails/demo-rails70/app/assets/images/.keep b/e2e/ruby/rails/demo-rails70/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/app/assets/stylesheets/application.css b/e2e/ruby/rails/demo-rails70/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..1f8be2055a --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/assets/stylesheets/application.css @@ -0,0 +1,458 @@ +/* + *= require_tree . + *= require_self + */ + +:root { + --color-brand: #405bff; + --color-brand-dark: #3148cc; + --color-header-bg: #282828; + --color-header-text: #ffffff; + --color-bg: #f5f6f8; + --color-surface: #ffffff; + --color-text: #282828; + --color-text-muted: #6b7280; + --color-border: #e0e2e6; + --color-success: #16a34a; + --color-success-bg: #f0fdf4; + --color-danger: #dc2626; + --color-danger-bg: #fef2f2; + --color-code-bg: #f1f3f5; + --radius: 8px; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); + --max-width: 960px; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 1.6; + color: var(--color-text); + background: var(--color-bg); +} + +/* --- Header / Nav --- */ + +.site-header { + background: var(--color-header-bg); + color: var(--color-header-text); + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; +} + +.site-header a { + color: var(--color-header-text); + text-decoration: none; +} + +.site-logo { + font-weight: 700; + font-size: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.site-nav { + display: flex; + gap: 24px; +} + +.site-nav a { + font-size: 14px; + opacity: 0.8; + transition: opacity 0.15s; +} + +.site-nav a:hover, +.site-nav a.active { + opacity: 1; +} + +/* --- Container --- */ + +.container { + max-width: var(--max-width); + margin: 0 auto; + padding: 32px 24px 64px; +} + +/* --- Typography --- */ + +h1 { + font-size: 28px; + font-weight: 700; + margin: 0 0 8px; +} + +h2 { + font-size: 20px; + font-weight: 600; + margin: 32px 0 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--color-border); +} + +h3 { + font-size: 16px; + font-weight: 600; + margin: 24px 0 8px; +} + +p { + margin: 0 0 16px; +} + +a { + color: var(--color-brand); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.subtitle { + color: var(--color-text-muted); + margin-bottom: 24px; +} + +/* --- Cards --- */ + +.card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 20px 24px; + margin-bottom: 24px; +} + +.card h2 { + margin-top: 0; + border-bottom: none; + padding-bottom: 0; +} + +/* --- Status Badges --- */ + +.badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + padding: 4px 10px; + border-radius: 999px; +} + +.badge-success { + background: var(--color-success-bg); + color: var(--color-success); +} + +.badge-danger { + background: var(--color-danger-bg); + color: var(--color-danger); +} + +/* --- Status Info --- */ + +.status-list { + list-style: none; + padding: 0; + margin: 0 0 16px; +} + +.status-list li { + padding: 6px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.status-list li + li { + border-top: 1px solid var(--color-border); +} + +.status-label { + font-weight: 600; + min-width: 120px; + color: var(--color-text-muted); + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +/* --- Tables --- */ + +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius); + overflow: hidden; + margin-bottom: 16px; + background: var(--color-surface); +} + +thead th { + background: var(--color-bg); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-text-muted); + text-align: left; + padding: 10px 16px; + border-bottom: 1px solid var(--color-border); +} + +tbody td { + padding: 10px 16px; + border-bottom: 1px solid var(--color-border); + font-size: 14px; +} + +tbody tr:last-child td { + border-bottom: none; +} + +tbody tr:hover { + background: #f9fafb; +} + +/* --- Code / Pre --- */ + +code { + font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace; + font-size: 13px; + background: var(--color-code-bg); + padding: 2px 6px; + border-radius: 4px; +} + +pre { + background: var(--color-code-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 16px; + overflow-x: auto; + font-size: 13px; + line-height: 1.5; +} + +pre code { + background: none; + padding: 0; + border-radius: 0; +} + +.code-block { + background: var(--color-code-bg); + padding: 12px 16px; + border-radius: var(--radius); + display: block; + font-size: 14px; +} + +.value-block { + background: #eff6ff; + padding: 12px 16px; + border-radius: var(--radius); + display: block; +} + +/* --- Buttons --- */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + transition: background 0.15s, box-shadow 0.15s; + text-decoration: none; + line-height: 1.4; +} + +.btn:hover { + text-decoration: none; +} + +.btn-primary { + background: var(--color-brand); + color: #fff; + border-color: var(--color-brand); +} + +.btn-primary:hover { + background: var(--color-brand-dark); +} + +.btn-secondary { + background: var(--color-surface); + color: var(--color-text); + border-color: var(--color-border); +} + +.btn-secondary:hover { + background: var(--color-bg); +} + +.btn-danger { + background: var(--color-danger); + color: #fff; + border-color: var(--color-danger); +} + +.btn-danger:hover { + background: #b91c1c; +} + +.btn-group { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +/* Style button_to forms inside button groups */ +.btn-group form { + display: inline-block; +} + +.btn-group form button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + border-radius: 6px; + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text); + cursor: pointer; + transition: background 0.15s; +} + +.btn-group form button:hover { + background: var(--color-bg); +} + +/* --- Action Sections --- */ + +.action-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; + margin-top: 16px; +} + +.action-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 20px; +} + +.action-card h3 { + margin: 0 0 4px; + border-bottom: none; + padding-bottom: 0; + font-size: 15px; +} + +.action-card p { + color: var(--color-text-muted); + font-size: 13px; + margin: 0 0 12px; +} + +/* --- Detail Fields --- */ + +.detail-field { + margin-bottom: 20px; +} + +.detail-field h2 { + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-text-muted); + margin: 0 0 6px; + padding-bottom: 0; + border-bottom: none; +} + +/* --- Endpoint List --- */ + +.endpoint-list { + list-style: none; + padding: 0; + margin: 0; +} + +.endpoint-list li { + padding: 8px 0; + border-bottom: 1px solid var(--color-border); + font-size: 14px; +} + +.endpoint-list li:last-child { + border-bottom: none; +} + +.endpoint-method { + display: inline-block; + background: var(--color-code-bg); + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + font-family: monospace; + margin-right: 6px; +} + +/* --- Footer --- */ + +.site-footer { + text-align: center; + padding: 24px; + color: var(--color-text-muted); + font-size: 13px; + border-top: 1px solid var(--color-border); + margin-top: 48px; +} + +/* --- Feedback --- */ + +.feedback { + display: inline-flex; + align-items: center; + font-size: 13px; + color: var(--color-success); + padding: 8px 0; +} + +/* --- Links row --- */ + +.link-row { + display: flex; + gap: 12px; + margin-top: 16px; +} diff --git a/e2e/ruby/rails/demo-rails70/app/channels/application_cable/channel.rb b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/channel.rb new file mode 100644 index 0000000000..9aec230539 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/channel.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/channels/application_cable/connection.rb b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/connection.rb new file mode 100644 index 0000000000..8d6c2a1bf4 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/connection.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/application_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/application_controller.rb new file mode 100644 index 0000000000..1389b9442f --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/controllers/application_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::Base + private + + def ld_client + Rails.configuration.ld_client + end + helper_method :ld_client + + # Render a turbo-frame-compatible success message for POST action buttons + def render_turbo_feedback(frame_id, message) + timestamp = Time.current.strftime('%H:%M:%S') + html = %() + render html: html.html_safe, layout: false + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/concerns/.keep b/e2e/ruby/rails/demo-rails70/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/errors_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/errors_controller.rb new file mode 100644 index 0000000000..bdc1a85776 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/controllers/errors_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ErrorsController < ApplicationController + # Uses the module-level LaunchDarklyObservability.record_exception + def create + LaunchDarklyObservability.in_span('error-handling-example', attributes: { 'foo' => 'bar' }) do |span| + begin + 1 / 0 + rescue StandardError => e + LaunchDarklyObservability.record_exception(e) + Rails.logger.error "Exception occurred: #{e.message}" + end + end + + render_turbo_feedback('error_feedback', 'Error recorded (module helper)') + end + + # Uses the controller-level record_launchdarkly_exception helper + def create_with_helper + with_launchdarkly_span('error-controller-helper-example', attributes: { 'source' => 'controller_helper' }) do + begin + raise ArgumentError, 'demo error via controller helper' + rescue StandardError => e + record_launchdarkly_exception(e) + Rails.logger.error "Exception occurred: #{e.message}" + end + end + + render_turbo_feedback('error_feedback', 'Error recorded (controller helper)') + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/flags_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/flags_controller.rb new file mode 100644 index 0000000000..5759bf6bb5 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/controllers/flags_controller.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +# Controller to demonstrate LaunchDarkly feature flag evaluations +# with observability instrumentation +class FlagsController < ApplicationController + # GET /flags + # Returns all flag evaluations for the current context + def index + context = build_context + + # Get all flags from LaunchDarkly + state = Rails.configuration.ld_client.all_flags_state(context, details_only_for_tracked_flags: false) + @all_flags_valid = state.valid? + + # Build evaluations from all available flags + @evaluations = {} + state.values_map.each_key do |flag_key| + @evaluations[flag_key] = Rails.configuration.ld_client.variation_detail(flag_key, context, nil) + end + + respond_to do |format| + format.html + format.json { render json: { valid: @all_flags_valid, evaluations: format_evaluations(@evaluations) } } + end + end + + # GET /flags/:key + # Returns a single flag evaluation + def show + context = build_context + flag_key = params[:id] + + detail = Rails.configuration.ld_client.variation_detail(flag_key, context, nil) + + respond_to do |format| + format.html { @flag_key = flag_key; @detail = detail } + format.json { render json: format_detail(flag_key, detail) } + end + end + + # POST /flags/evaluate + # Evaluate a flag with a custom context + def evaluate + flag_key = params[:flag_key] + context_data = params[:context] || {} + + context = LaunchDarkly::LDContext.create({ + key: context_data[:key] || 'anonymous', + kind: context_data[:kind] || 'user', + **context_data.except(:key, :kind).to_h.symbolize_keys + }) + + detail = Rails.configuration.ld_client.variation_detail(flag_key, context, params[:default]) + + render json: format_detail(flag_key, detail) + end + + # POST /flags/batch + # Evaluate multiple flags at once (demonstrates multiple spans) + def batch + context = build_context + flag_keys = params[:flag_keys] + + unless flag_keys.present? + return render json: { error: 'flag_keys parameter required' }, status: :bad_request + end + + results = flag_keys.each_with_object({}) do |key, hash| + hash[key] = Rails.configuration.ld_client.variation(key, context, nil) + end + + render json: { evaluations: results, context_key: context.key } + end + + # GET /flags/all_flags + # Get all flag states (demonstrates all_flags_state method) + def all_flags + context = build_context + state = Rails.configuration.ld_client.all_flags_state(context) + + render json: { + valid: state.valid?, + flags: JSON.parse(state.to_json) + } + end + + private + + def build_context + # Build context from request parameters or session + user_key = params[:user_key] || session.id.to_s.presence || 'anonymous' + user_kind = params[:user_kind] || 'user' + + attrs = { + key: user_key, + kind: user_kind, + anonymous: user_key == 'anonymous' + } + + # Add optional attributes + attrs[:email] = params[:email] if params[:email].present? + attrs[:name] = params[:name] if params[:name].present? + attrs[:plan] = params[:plan] if params[:plan].present? + + LaunchDarkly::LDContext.create(attrs) + end + + def format_evaluations(evaluations) + evaluations.transform_values { |detail| format_detail_hash(detail) } + end + + def format_detail(key, detail) + { + flag_key: key, + **format_detail_hash(detail) + } + end + + def format_detail_hash(detail) + { + value: detail.value, + variation_index: detail.variation_index, + reason: detail.reason ? JSON.parse(detail.reason.to_json) : nil + } + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/logs_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/logs_controller.rb new file mode 100644 index 0000000000..b105191a08 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/controllers/logs_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class LogsController < ApplicationController + def create + Rails.logger.info "hello, world! foo=bar" + render_turbo_feedback('log_feedback', 'Info log created') + end + + def create_with_hash + Rails.logger.info(test: 'ing', foo: 'bar') + render_turbo_feedback('log_feedback', 'Info log (hash) created') + end + + def create_warn + Rails.logger.warn "warning: something looks off level=warn" + render_turbo_feedback('log_feedback', 'Warn log created') + end + + def create_error + Rails.logger.error "error: something went wrong level=error" + render_turbo_feedback('log_feedback', 'Error log created') + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/pages_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/pages_controller.rb new file mode 100644 index 0000000000..093befaf4b --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/controllers/pages_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class PagesController < ApplicationController + def home + # Create LaunchDarkly context for current user/session + @context = LaunchDarkly::LDContext.create({ + key: session.id.to_s.presence || 'anonymous', + kind: 'user', + anonymous: session.id.blank? + }) + + state = ld_client.all_flags_state(@context) + @flags_valid = state.valid? + @flag_count = state.values_map.size + @sample_evaluations = state.values_map.first(5).to_h + + # Make an HTTP request (auto-instrumented by OpenTelemetry) + @http_url = 'http://www.example.com/?test=1' + with_launchdarkly_span('pages-home-fetch', attributes: { 'custom.source' => 'demo' }) do + response = Net::HTTP.get_response(URI.parse(@http_url)) + @http_status = "#{response.code} #{response.message}" + end + + Rails.logger.info "[LaunchDarkly] Loaded #{@flag_count} flags, valid=#{@flags_valid}" + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/telemetry_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/telemetry_controller.rb new file mode 100644 index 0000000000..c6e01bb003 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/controllers/telemetry_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class TelemetryController < ApplicationController + def flush + LaunchDarklyObservability.flush + render_turbo_feedback('flush_feedback', 'Telemetry flushed') + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/traces_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/traces_controller.rb new file mode 100644 index 0000000000..e84d704432 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/controllers/traces_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class TracesController < ApplicationController + def create + LaunchDarklyObservability.in_span('example-trace-outer') do |outer_span| + sleep(0.1) + + trace = Trace.new(name: 'trace', kind: 'internal') + + LaunchDarklyObservability.in_span('example-trace-inner', attributes: { 'trace.operation' => 'save' }) do |inner_span| + sleep(0.2) + trace.save! + end + + outer_span.set_attribute('trace.operation', 'update') + trace.update!(name: 'trace-updated') + end + + render_turbo_feedback('trace_feedback', 'Trace created') + end + + # Uses the controller-level with_launchdarkly_span helper + def create_with_helper + with_launchdarkly_span('example-trace-controller-helper', attributes: { 'source' => 'controller_helper' }) do + sleep(0.1) + end + + render_turbo_feedback('trace_feedback', 'Trace created (controller helper)') + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/helpers/application_helper.rb b/e2e/ruby/rails/demo-rails70/app/helpers/application_helper.rb new file mode 100644 index 0000000000..15b06f0f67 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/helpers/application_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module ApplicationHelper +end diff --git a/e2e/ruby/rails/demo-rails70/app/helpers/pages_helper.rb b/e2e/ruby/rails/demo-rails70/app/helpers/pages_helper.rb new file mode 100644 index 0000000000..15d8b3e442 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/helpers/pages_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module PagesHelper +end diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/application.js b/e2e/ruby/rails/demo-rails70/app/javascript/application.js new file mode 100644 index 0000000000..9c394a0c73 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import '@hotwired/turbo-rails' +import 'controllers' diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/controllers/application.js b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/application.js new file mode 100644 index 0000000000..c030eb8c7d --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from '@hotwired/stimulus' + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/controllers/hello_controller.js b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..184aa2d69c --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + connect() { + this.element.textContent = 'Hello World!' + } +} diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/controllers/index.js b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/index.js new file mode 100644 index 0000000000..390177a3ce --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/index.js @@ -0,0 +1,11 @@ +// Import and register all your controllers from the importmap under controllers/* + +import { application } from 'controllers/application' + +// Eager load all controllers defined in the import map under controllers/**/*_controller +import { eagerLoadControllersFrom } from '@hotwired/stimulus-loading' +eagerLoadControllersFrom('controllers', application) + +// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) +// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" +// lazyLoadControllersFrom("controllers", application) diff --git a/e2e/ruby/rails/demo-rails70/app/jobs/application_job.rb b/e2e/ruby/rails/demo-rails70/app/jobs/application_job.rb new file mode 100644 index 0000000000..bef395997d --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/jobs/application_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/e2e/ruby/rails/demo-rails70/app/mailers/application_mailer.rb b/e2e/ruby/rails/demo-rails70/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..d84cb6e71e --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/mailers/application_mailer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/e2e/ruby/rails/demo-rails70/app/models/application_record.rb b/e2e/ruby/rails/demo-rails70/app/models/application_record.rb new file mode 100644 index 0000000000..08dc537989 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/models/application_record.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/e2e/ruby/rails/demo-rails70/app/models/concerns/.keep b/e2e/ruby/rails/demo-rails70/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb b/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb new file mode 100644 index 0000000000..dc70f01581 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Mirrors an application that creates the LaunchDarkly client lazily — e.g. from +# a model on first use — instead of during boot in a config/initializer. Enabled +# with LD_LAZY_INIT=1 (see config/initializers/launchdarkly.rb) and exercised by +# the lazy-init e2e test, which verifies the Railtie installs OpenTelemetry +# auto-instrumentation at boot even though the client is created afterward. +class LazyLdClient + def self.instance + @instance ||= begin + plugin = LaunchDarklyObservability::Plugin.new( + otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT), + service_name: 'rails-demo-app', + service_version: '1.0.0' + ) + config = LaunchDarkly::Config.new(plugins: [plugin]) + LaunchDarkly::LDClient.new(ENV.fetch('LAUNCHDARKLY_SDK_KEY', ''), config) + end + end +end diff --git a/e2e/ruby/rails/demo-rails70/app/models/trace.rb b/e2e/ruby/rails/demo-rails70/app/models/trace.rb new file mode 100644 index 0000000000..7b3b40ba9f --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/models/trace.rb @@ -0,0 +1,2 @@ +class Trace < ApplicationRecord +end diff --git a/e2e/ruby/rails/demo-rails70/app/views/flags/index.html.erb b/e2e/ruby/rails/demo-rails70/app/views/flags/index.html.erb new file mode 100644 index 0000000000..7ded0c08a6 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/views/flags/index.html.erb @@ -0,0 +1,86 @@ +

Feature Flags

+

+ Each flag evaluation is automatically traced via OpenTelemetry. +

+ +
+
    +
  • + Connection + <% if @all_flags_valid %> + Connected + <% else %> + Not connected + <% end %> +
  • +
  • + Total flags + <%= @evaluations.size %> +
  • +
+
+ +<% if @evaluations.any? %> +

All Flag Evaluations

+ + + + + + + + + + + + <% @evaluations.each do |key, detail| %> + + + + + + + <% end %> + +
Flag KeyValueVariation IndexReason
<%= key %><%= detail.value.inspect %><%= detail.variation_index %><%= detail.reason&.kind %>
+<% else %> +
+

No flags available. Make sure LAUNCHDARKLY_SDK_KEY is set correctly.

+
+<% end %> + +

Test Endpoints

+ +
+

GET Endpoints

+
    +
  • GET /flags.json — All flag evaluations as JSON
  • +
  • GET /flags/all_flags — Raw flags state
  • +
  • GET /flags/:flag_key — Evaluate a specific flag
  • +
+ +

POST Endpoints

+
# Evaluate a specific flag with custom context
+POST /flags/evaluate
+{
+  "flag_key": "your-flag-key",
+  "context": { "key": "user-123", "kind": "user" }
+}
+
+# Batch evaluate multiple flags
+POST /flags/batch
+{
+  "flag_keys": ["flag-1", "flag-2", "flag-3"]
+}
+
+ +

Trace Information

+ +
+
    +
  • + Trace ID + <%= launchdarkly_trace_id || 'N/A' %> +
  • +
+
diff --git a/e2e/ruby/rails/demo-rails70/app/views/flags/show.html.erb b/e2e/ruby/rails/demo-rails70/app/views/flags/show.html.erb new file mode 100644 index 0000000000..9f59278b17 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/views/flags/show.html.erb @@ -0,0 +1,30 @@ +

Flag Evaluation

+ +
+
+

Flag Key

+ <%= @flag_key %> +
+ +
+

Value

+ <%= @detail.value.inspect %> +
+ +
+

Variation Index

+

<%= @detail.variation_index || 'N/A' %>

+
+ + <% if @detail.reason %> +
+

Reason

+
<%= JSON.pretty_generate(JSON.parse(@detail.reason.to_json)) %>
+
+ <% end %> +
+ + diff --git a/e2e/ruby/rails/demo-rails70/app/views/layouts/application.html.erb b/e2e/ruby/rails/demo-rails70/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..ba8801adea --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/views/layouts/application.html.erb @@ -0,0 +1,35 @@ + + + + LaunchDarkly Observability Ruby Demo + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= launchdarkly_traceparent_meta_tag %> + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + + +
+ <%= yield %> +
+ +
+ LaunchDarkly Ruby SDK · Observability Plugin Demo +
+ + diff --git a/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.html.erb b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..cbd34d2e9d --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.text.erb b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/e2e/ruby/rails/demo-rails70/app/views/pages/home.html.erb b/e2e/ruby/rails/demo-rails70/app/views/pages/home.html.erb new file mode 100644 index 0000000000..a9e1f7530a --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/app/views/pages/home.html.erb @@ -0,0 +1,114 @@ +

Ruby Observability Demo

+

+ Connected to LaunchDarkly. Each flag evaluation creates an OpenTelemetry span. +

+ +
+

Feature Flags

+ +
    +
  • + Status + <% if @flags_valid %> + Connected + <% else %> + Not connected + <% end %> +
  • +
  • + Total flags + <%= @flag_count %> +
  • +
  • + Context key + <%= @context.key %> +
  • +
+ + <% if @sample_evaluations.any? %> +

Sample Flag Values

+ + + + + + + + + <% @sample_evaluations.each do |key, value| %> + + + + + <% end %> + +
Flag KeyValue
<%= key %><%= value.inspect %>
+ <% else %> +

No flags available. Make sure LAUNCHDARKLY_SDK_KEY is set.

+ <% end %> + + View all flags → +
+ +

Observability Actions

+

+ Trigger traces, logs, and errors to verify OpenTelemetry instrumentation. +

+ +
+
+

Traces

+

Create spans with nested operations and custom attributes.

+
+ <%= button_to 'Create Trace', traces_path, method: :post, data: { turbo_frame: "trace_feedback" } %> + <%= button_to 'Create Trace (Controller)', create_with_helper_traces_path, method: :post, data: { turbo_frame: "trace_feedback" } %> +
+ <%= turbo_frame_tag "trace_feedback" %> +
+ +
+

Logs

+

Emit log entries at different levels through Rails logger.

+
+ <%= button_to 'Info', logs_path, method: :post, data: { turbo_frame: "log_feedback" } %> + <%= button_to 'Info (Hash)', create_with_hash_logs_path, method: :post, data: { turbo_frame: "log_feedback" } %> + <%= button_to 'Warn', create_warn_logs_path, method: :post, data: { turbo_frame: "log_feedback" } %> + <%= button_to 'Error', create_error_logs_path, method: :post, data: { turbo_frame: "log_feedback" } %> +
+ <%= turbo_frame_tag "log_feedback" %> +
+ +
+

Errors

+

Record exceptions via module and controller helpers.

+
+ <%= button_to 'Record (Module)', errors_path, method: :post, data: { turbo_frame: "error_feedback" } %> + <%= button_to 'Record (Controller)', create_with_helper_errors_path, method: :post, data: { turbo_frame: "error_feedback" } %> +
+ <%= turbo_frame_tag "error_feedback" %> +
+ +
+

Flush

+

Force-flush all pending telemetry to the collector.

+
+ <%= button_to 'Flush Telemetry', flush_telemetry_path, method: :post, data: { turbo_frame: "flush_feedback" } %> +
+ <%= turbo_frame_tag "flush_feedback" %> +
+
+ +

HTTP Request

+
+

Auto-instrumented outbound HTTP call via Net::HTTP

+
    +
  • + URL + <%= @http_url %> +
  • +
  • + Response + <%= @http_status %> +
  • +
+
diff --git a/e2e/ruby/rails/demo-rails70/bin/bundle b/e2e/ruby/rails/demo-rails70/bin/bundle new file mode 100755 index 0000000000..ef688ecc73 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/bin/bundle @@ -0,0 +1,113 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) + end + + def env_var_version + ENV['BUNDLER_VERSION'] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` + + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + bundler_version = a if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + + bundler_version = Regexp.last_match(1) + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV['BUNDLE_GEMFILE'] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path('../Gemfile', __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when 'gems.rb' then gemfile.sub(/\.rb$/, '.locked') + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV['BUNDLE_GEMFILE'] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem 'bundler', bundler_requirement + end + return if gem_error.nil? + + require_error = activation_error_handling do + require 'bundler/version' + end + if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + return + end + + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? diff --git a/e2e/ruby/rails/demo-rails70/bin/importmap b/e2e/ruby/rails/demo-rails70/bin/importmap new file mode 100755 index 0000000000..d4238647bb --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/bin/importmap @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../config/application' +require 'importmap/commands' diff --git a/e2e/ruby/rails/demo-rails70/bin/rails b/e2e/ruby/rails/demo-rails70/bin/rails new file mode 100755 index 0000000000..a31728ab97 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/bin/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/e2e/ruby/rails/demo-rails70/bin/rake b/e2e/ruby/rails/demo-rails70/bin/rake new file mode 100755 index 0000000000..c199955006 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/bin/rake @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/e2e/ruby/rails/demo-rails70/bin/setup b/e2e/ruby/rails/demo-rails70/bin/setup new file mode 100755 index 0000000000..516b651e39 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'fileutils' + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/e2e/ruby/rails/demo-rails70/config.ru b/e2e/ruby/rails/demo-rails70/config.ru new file mode 100644 index 0000000000..6dc8321802 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config.ru @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application +Rails.application.load_server diff --git a/e2e/ruby/rails/demo-rails70/config/application.rb b/e2e/ruby/rails/demo-rails70/config/application.rb new file mode 100644 index 0000000000..7ffc784f68 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/application.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative 'boot' + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Demo + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.0 + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/e2e/ruby/rails/demo-rails70/config/boot.rb b/e2e/ruby/rails/demo-rails70/config/boot.rb new file mode 100644 index 0000000000..18a50e22ca --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/boot.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Ruby 3.3 + Rails 7.0 boot fix: concurrent-ruby >= 1.3.5 dropped its implicit +# `require 'logger'`, which makes Rails < 7.1 raise +# "NameError: uninitialized constant Logger" during boot. Requiring it here (the +# documented workaround) is unrelated to the instrumentation bug under test; it +# just lets this Rails 7.0 app boot on the Ruby 3.3.4 toolchain. +require 'logger' + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/e2e/ruby/rails/demo-rails70/config/cable.yml b/e2e/ruby/rails/demo-rails70/config/cable.yml new file mode 100644 index 0000000000..9890b7388d --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/cable.yml @@ -0,0 +1,11 @@ +development: + adapter: redis + url: redis://localhost:6379/1 + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: highlight_ruby_demo_production diff --git a/e2e/ruby/rails/demo-rails70/config/database.yml b/e2e/ruby/rails/demo-rails70/config/database.yml new file mode 100644 index 0000000000..06488320fe --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/database.yml @@ -0,0 +1,25 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: db/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: db/test.sqlite3 + +production: + <<: *default + database: db/production.sqlite3 diff --git a/e2e/ruby/rails/demo-rails70/config/environment.rb b/e2e/ruby/rails/demo-rails70/config/environment.rb new file mode 100644 index 0000000000..d5abe55806 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/environment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/e2e/ruby/rails/demo-rails70/config/environments/development.rb b/e2e/ruby/rails/demo-rails70/config/environments/development.rb new file mode 100644 index 0000000000..84a57f4010 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/environments/development.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/integer/time' + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true +end diff --git a/e2e/ruby/rails/demo-rails70/config/environments/production.rb b/e2e/ruby/rails/demo-rails70/config/environments/production.rb new file mode 100644 index 0000000000..5d44d657a0 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/environments/production.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/integer/time' + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [:request_id] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "highlight_ruby_demo_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") + + if ENV['RAILS_LOG_TO_STDOUT'].present? + logger = ActiveSupport::Logger.new($stdout) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/e2e/ruby/rails/demo-rails70/config/environments/test.rb b/e2e/ruby/rails/demo-rails70/config/environments/test.rb new file mode 100644 index 0000000000..b8f9f0b906 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/environments/test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/integer/time' + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Turn false under Spring and add config.action_view.cache_template_loading = true. + config.cache_classes = true + + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV['CI'].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = :none + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/e2e/ruby/rails/demo-rails70/config/importmap.rb b/e2e/ruby/rails/demo-rails70/config/importmap.rb new file mode 100644 index 0000000000..15fd62707a --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/importmap.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Pin npm packages by running ./bin/importmap + +pin 'application' +pin '@hotwired/turbo-rails', to: 'turbo.min.js' +pin '@hotwired/stimulus', to: 'stimulus.min.js' +pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js' +pin_all_from 'app/javascript/controllers', under: 'controllers' diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/assets.rb b/e2e/ruby/rails/demo-rails70/config/initializers/assets.rb new file mode 100644 index 0000000000..bcafccdd33 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/initializers/assets.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/content_security_policy.rb b/e2e/ruby/rails/demo-rails70/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..53538c1498 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/initializers/content_security_policy.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap and inline scripts +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/filter_parameter_logging.rb b/e2e/ruby/rails/demo-rails70/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..3df77c5bee --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn +] diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/inflections.rb b/e2e/ruby/rails/demo-rails70/config/initializers/inflections.rb new file mode 100644 index 0000000000..9e049dcc91 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/initializers/inflections.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb b/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb new file mode 100644 index 0000000000..e67dce87fe --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb @@ -0,0 +1,22 @@ +require 'launchdarkly-server-sdk' +require 'launchdarkly_observability' + +# Set LD_LAZY_INIT=1 to defer LaunchDarkly client creation until first use +# (see app/models/lazy_ld_client.rb) instead of creating it here during boot. This +# mirrors apps that build the client lazily — e.g. from a model on first request +# — and exercises the Railtie's boot-time auto-instrumentation install path. +unless ENV['LD_LAZY_INIT'] + observability_plugin = LaunchDarklyObservability::Plugin.new( + otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT), + service_name: 'rails-demo-app', + service_version: '1.0.0' + ) + + sdk_key = ENV.fetch('LAUNCHDARKLY_SDK_KEY') do + Rails.logger.warn '[LaunchDarkly] LAUNCHDARKLY_SDK_KEY not set, client will not connect' + '' + end + + config = LaunchDarkly::Config.new(plugins: [observability_plugin]) + Rails.configuration.ld_client = LaunchDarkly::LDClient.new(sdk_key, config) +end diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/permissions_policy.rb b/e2e/ruby/rails/demo-rails70/config/initializers/permissions_policy.rb new file mode 100644 index 0000000000..810aadeb98 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/initializers/permissions_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/e2e/ruby/rails/demo-rails70/config/locales/en.yml b/e2e/ruby/rails/demo-rails70/config/locales/en.yml new file mode 100644 index 0000000000..ecf0a11878 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# "true": "foo" +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: 'Hello world' diff --git a/e2e/ruby/rails/demo-rails70/config/puma.rb b/e2e/ruby/rails/demo-rails70/config/puma.rb new file mode 100644 index 0000000000..ffa06610fa --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/puma.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) +min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch('PORT', 2343) + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch('RAILS_ENV', 'development') + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Reinitialize LaunchDarkly client after forking workers +# This is required when using workers (clustered mode) +# on_worker_boot do +# Rails.configuration.ld_client.postfork +# end + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/e2e/ruby/rails/demo-rails70/config/routes.rb b/e2e/ruby/rails/demo-rails70/config/routes.rb new file mode 100644 index 0000000000..b1152efa9f --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/routes.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do + get 'pages/home' + resources :traces, only: [:create] do + post :create_with_helper, on: :collection + end + resources :logs, only: [:create] do + collection do + post :create_with_hash + post :create_warn + post :create_error + end + end + resources :errors, only: [:create] do + post :create_with_helper, on: :collection + end + post 'telemetry/flush', to: 'telemetry#flush', as: :flush_telemetry + + # LaunchDarkly feature flag routes + resources :flags, only: %i[index show] do + collection do + post :evaluate + post :batch + get :all_flags + end + end + + root to: 'pages#home' +end diff --git a/e2e/ruby/rails/demo-rails70/config/storage.yml b/e2e/ruby/rails/demo-rails70/config/storage.yml new file mode 100644 index 0000000000..a0f8d3aa23 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/config/storage.yml @@ -0,0 +1,33 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/e2e/ruby/rails/demo-rails70/db/migrate/20240829164231_create_traces.rb b/e2e/ruby/rails/demo-rails70/db/migrate/20240829164231_create_traces.rb new file mode 100644 index 0000000000..7a09c21362 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/db/migrate/20240829164231_create_traces.rb @@ -0,0 +1,10 @@ +class CreateTraces < ActiveRecord::Migration[7.0] + def change + create_table :traces do |t| + t.string :name + t.string :kind + + t.timestamps + end + end +end diff --git a/e2e/ruby/rails/demo-rails70/db/schema.rb b/e2e/ruby/rails/demo-rails70/db/schema.rb new file mode 100644 index 0000000000..ab6ec88dca --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/db/schema.rb @@ -0,0 +1,21 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.0].define(version: 2024_08_29_164231) do + create_table "traces", force: :cascade do |t| + t.string "name" + t.string "kind" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end diff --git a/e2e/ruby/rails/demo-rails70/db/seeds.rb b/e2e/ruby/rails/demo-rails70/db/seeds.rb new file mode 100644 index 0000000000..6533326431 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/db/seeds.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) +# Character.create(name: "Luke", movie: movies.first) diff --git a/e2e/ruby/rails/demo-rails70/lib/assets/.keep b/e2e/ruby/rails/demo-rails70/lib/assets/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/lib/tasks/.keep b/e2e/ruby/rails/demo-rails70/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/public/404.html b/e2e/ruby/rails/demo-rails70/public/404.html new file mode 100644 index 0000000000..7e25f8ae14 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/public/404.html @@ -0,0 +1,73 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

+ You may have mistyped the address or the page may have + moved. +

+
+

+ If you are the application owner check the logs for more + information. +

+
+ + diff --git a/e2e/ruby/rails/demo-rails70/public/422.html b/e2e/ruby/rails/demo-rails70/public/422.html new file mode 100644 index 0000000000..d7a5b95445 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/public/422.html @@ -0,0 +1,73 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

+ Maybe you tried to change something you didn't have access + to. +

+
+

+ If you are the application owner check the logs for more + information. +

+
+ + diff --git a/e2e/ruby/rails/demo-rails70/public/500.html b/e2e/ruby/rails/demo-rails70/public/500.html new file mode 100644 index 0000000000..af124c77cc --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/public/500.html @@ -0,0 +1,69 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

+ If you are the application owner check the logs for more + information. +

+
+ + diff --git a/e2e/ruby/rails/demo-rails70/public/apple-touch-icon-precomposed.png b/e2e/ruby/rails/demo-rails70/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/public/apple-touch-icon.png b/e2e/ruby/rails/demo-rails70/public/apple-touch-icon.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/public/favicon.ico b/e2e/ruby/rails/demo-rails70/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/public/robots.txt b/e2e/ruby/rails/demo-rails70/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/e2e/ruby/rails/demo-rails70/storage/.keep b/e2e/ruby/rails/demo-rails70/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/application_system_test_case.rb b/e2e/ruby/rails/demo-rails70/test/application_system_test_case.rb new file mode 100644 index 0000000000..652febbd68 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/application_system_test_case.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/e2e/ruby/rails/demo-rails70/test/channels/application_cable/connection_test.rb b/e2e/ruby/rails/demo-rails70/test/channels/application_cable/connection_test.rb new file mode 100644 index 0000000000..4aee9b3353 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/channels/application_cable/connection_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'test_helper' + +module ApplicationCable + class ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end + end +end diff --git a/e2e/ruby/rails/demo-rails70/test/controllers/.keep b/e2e/ruby/rails/demo-rails70/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/controllers/pages_controller_test.rb b/e2e/ruby/rails/demo-rails70/test/controllers/pages_controller_test.rb new file mode 100644 index 0000000000..819f8c03e9 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/controllers/pages_controller_test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PagesControllerTest < ActionDispatch::IntegrationTest + test 'should get home' do + get pages_home_url + assert_response :success + end +end diff --git a/e2e/ruby/rails/demo-rails70/test/fixtures/files/.keep b/e2e/ruby/rails/demo-rails70/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/helpers/.keep b/e2e/ruby/rails/demo-rails70/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/integration/.keep b/e2e/ruby/rails/demo-rails70/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/integration/lazy_init_instrumentation_test.rb b/e2e/ruby/rails/demo-rails70/test/integration/lazy_init_instrumentation_test.rb new file mode 100644 index 0000000000..466e3c6854 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/integration/lazy_init_instrumentation_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'open3' + +# Regression test for lazy LaunchDarkly client initialization. +# +# When the client is created lazily (after Rails has booted) rather than in a +# config/initializer, the OTel Rails-family instrumentations used to report +# "Instrumentation: ... failed to install" — their ActiveSupport.on_load hooks +# had already fired by the time the plugin configured OpenTelemetry. The gem's +# Railtie now installs auto-instrumentation during boot, independent of when the +# client is created, so it attaches regardless. +# +# This must run in a SEPARATE process because instrumentation install is a +# one-time, global side effect: the main test suite boots with the client +# created during boot, so we boot a fresh Rails process with LD_LAZY_INIT=1 (no +# client created at boot — see config/initializers/launchdarkly.rb) and assert +# the Rails instrumentations are installed anyway. +class LazyInitInstrumentationTest < ActiveSupport::TestCase + CHECK_SCRIPT = <<~'RUBY' + names = %w[Rack ActionPack ActiveRecord ActiveSupport Rails] + installed = names.all? do |n| + Object.const_get("OpenTelemetry::Instrumentation::#{n}::Instrumentation").instance.installed? + end + # Creating the client lazily (post-boot) must still work without raising. + LazyLdClient.instance + puts(installed ? 'LAZY_INSTRUMENTATION_OK' : 'LAZY_INSTRUMENTATION_FAILED') + RUBY + + test 'rails auto-instrumentation installs at boot even when the client is created lazily' do + output = boot_lazy_and_run(CHECK_SCRIPT) + + assert_includes output, 'LAZY_INSTRUMENTATION_OK', + "Rails auto-instrumentation should install at boot in lazy mode.\n--- subprocess output ---\n#{output}" + end + + private + + def boot_lazy_and_run(script) + env = { + 'LD_LAZY_INIT' => '1', + 'LAUNCHDARKLY_SDK_KEY' => 'sdk-test-0000000000000000000000', + 'RAILS_ENV' => 'test' + } + rails_bin = Rails.root.join('bin/rails').to_s + stdout, stderr, _status = Open3.capture3(env, rails_bin, 'runner', script, chdir: Rails.root.to_s) + "#{stdout}\n#{stderr}" + end +end diff --git a/e2e/ruby/rails/demo-rails70/test/integration/observability_instrumentation_test.rb b/e2e/ruby/rails/demo-rails70/test/integration/observability_instrumentation_test.rb new file mode 100644 index 0000000000..9c79a41206 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/integration/observability_instrumentation_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'test_helper' + +# End-to-end coverage for the LaunchDarkly observability plugin's Rails +# auto-instrumentation. +# +# Background: the plugin configures OpenTelemetry from `Plugin#register`, which +# runs when the LaunchDarkly client is created. In this app that happens in +# `config/initializers/launchdarkly.rb` — i.e. DURING Rails boot — so the OTel +# Rails-family instrumentations (Rack, ActionPack, ActiveRecord, ...) install +# correctly. A customer who instead creates the client lazily AFTER boot sees +# "Instrumentation: OpenTelemetry::Instrumentation::ActionPack failed to install" +# because the ActiveSupport.on_load hooks those instrumentations rely on have +# already fired. These tests pin the working boot-time behavior so a regression +# (or a change that breaks instrumentation install) is caught in CI. +class ObservabilityInstrumentationTest < ActionDispatch::IntegrationTest + # The Rails-family instrumentations that must attach during a boot-time init. + # These are exactly the ones that report "failed to install" on the lazy path. + RAILS_INSTRUMENTATIONS = %w[Rack ActionPack ActiveRecord ActiveSupport Rails].freeze + + def instrumentation_instance(name) + Object.const_get("OpenTelemetry::Instrumentation::#{name}::Instrumentation").instance + end + + test 'rails auto-instrumentation installed during boot' do + RAILS_INSTRUMENTATIONS.each do |name| + assert instrumentation_instance(name).installed?, + "#{name} instrumentation should be installed after a boot-time plugin init " \ + '(it reports "failed to install" when the client is created lazily after boot)' + end + end + + test 'http request produces a server span via the rack instrumentation' do + exporter = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new + processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter) + OpenTelemetry.tracer_provider.add_span_processor(processor) + + get pages_home_url + assert_response :success + + server_spans = exporter.finished_spans.select { |s| s.kind == :server } + refute_empty server_spans, 'expected an HTTP server span from the Rack/ActionPack instrumentation' + end +end diff --git a/e2e/ruby/rails/demo-rails70/test/integration/otlp_export_e2e_test.rb b/e2e/ruby/rails/demo-rails70/test/integration/otlp_export_e2e_test.rb new file mode 100644 index 0000000000..8f22306243 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/integration/otlp_export_e2e_test.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'test_helper' + +# End-to-end proof that the Rails 7.0 app EXPORTS telemetry over the real OTLP +# protobuf pipeline to a local sink (test/support/otlp_sink.rb), which test_helper +# points the exporter at via OTEL_EXPORTER_OTLP_ENDPOINT. +# +# This is the CardFlight repro's headline assertion. On Rails 7.0 with the +# unpinned gem, the Rails-family OTel instrumentations "failed to install", so no +# autoinstrumented HTTP *server* span is ever produced or exported — assertion +# (1) below fails. After the gem is fixed to pin the Rails-family instrumentation +# to a Rails-7.0-compatible version, the server span appears and the test passes. +class OtlpExportE2ETest < ActionDispatch::IntegrationTest + # OTLP Span.kind for SERVER (proto enum symbol or its int value). + SERVER_KINDS = [:SPAN_KIND_SERVER, 2].freeze + + def setup + OTLP_SINK.reset + end + + test 'traces, a log, and a captured exception are exported to the OTLP sink' do + post traces_path # manual spans + an autoinstrumented HTTP server span + post logs_path # Rails.logger.info "hello, world! foo=bar" + post errors_path # 1 / 0 -> LaunchDarklyObservability.record_exception + flush_telemetry + + # 1) TRACES — an autoinstrumented HTTP *server* span proves the Rails/Rack + # instrumentation attached. This is the assertion that is RED on Rails 7.0 + # with the unpinned gem and GREEN after the fix. + assert wait_until { OTLP_SINK.spans.any? { |s| SERVER_KINDS.include?(s.kind) } }, + 'expected an autoinstrumented HTTP server span at the OTLP sink ' \ + "(got span names: #{OTLP_SINK.spans.map(&:name).inspect})" + + # 2) LOGS — the info log reached the sink via the OTel log bridge. + assert wait_until { OTLP_SINK.logs.any? { |l| l.body.to_s.include?('hello, world! foo=bar') } }, + "expected the info log at the OTLP sink (got log bodies: #{OTLP_SINK.logs.map(&:body).inspect})" + + # 3) EXCEPTION — the ZeroDivisionError was recorded as an exception event. + exception_events = OTLP_SINK.spans.flat_map(&:events).select { |e| e[:name] == 'exception' } + refute_empty exception_events, 'expected a recorded exception event at the OTLP sink' + assert exception_events.any? { |e| e[:attributes]['exception.type'].to_s.include?('ZeroDivisionError') }, + 'expected a ZeroDivisionError exception event ' \ + "(got types: #{exception_events.map { |e| e[:attributes]['exception.type'] }.inspect})" + end + + private + + def flush_telemetry + OpenTelemetry.tracer_provider.force_flush + return unless OpenTelemetry.respond_to?(:logger_provider) && + OpenTelemetry.logger_provider.respond_to?(:force_flush) + + OpenTelemetry.logger_provider.force_flush + end + + # Poll until the block is truthy or the timeout elapses; returns the final value. + def wait_until(timeout: 5.0) + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout + loop do + result = yield + return result if result + break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline + + sleep 0.1 + end + yield + end +end diff --git a/e2e/ruby/rails/demo-rails70/test/mailers/.keep b/e2e/ruby/rails/demo-rails70/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/models/.keep b/e2e/ruby/rails/demo-rails70/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/support/otlp_sink.rb b/e2e/ruby/rails/demo-rails70/test/support/otlp_sink.rb new file mode 100644 index 0000000000..5bfce0d5c5 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/support/otlp_sink.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'webrick' +require 'zlib' +require 'stringio' + +# Load the OTLP protobuf message classes. These ship with the exporter gems the +# plugin already depends on, so we can decode the EXACT bytes the Ruby OTLP +# exporter puts on the wire. +require 'opentelemetry-exporter-otlp' +require 'opentelemetry-exporter-otlp-logs' + +# A minimal, in-process OTLP/HTTP sink used by the E2E tests to assert that the +# Rails app actually EXPORTED telemetry over the wire. +# +# Why not e2e/mock-otel-server? That server parses JSON and gates reads on a +# browser-only `highlight.session_id` attribute. The Ruby OTLP exporter sends +# gzip-compressed binary protobuf with no session id, so it cannot be parsed +# there. This sink decodes the real OTLP protobuf using the proto classes +# shipped with the exporter gems — pure Ruby, no Docker/Node — so the whole +# repro runs under `bundle exec rake`, identically locally and in CI. +module OtlpSink + # A decoded span (only the fields the tests assert on). + Span = Struct.new(:name, :kind, :scope, :attributes, :events, keyword_init: true) + # A decoded log record. + LogRecord = Struct.new(:body, :severity, :attributes, keyword_init: true) + + class Server + attr_reader :port + + def initialize(port: 4327) + @port = port + @spans = [] + @logs = [] + @mutex = Mutex.new + @server = WEBrick::HTTPServer.new( + Port: port, + BindAddress: '127.0.0.1', + Logger: WEBrick::Log.new(File::NULL), + AccessLog: [] + ) + @server.mount_proc('/v1/traces') { |req, res| ingest_traces(req); ok(res) } + @server.mount_proc('/v1/logs') { |req, res| ingest_logs(req); ok(res) } + # Respond 200 to metrics so the exporter never sees an error, even though + # the tests do not assert on metrics. + @server.mount_proc('/v1/metrics') { |_req, res| ok(res) } + end + + def start + @thread = Thread.new { @server.start } + self + end + + def stop + @server.shutdown + @thread&.join(2) + end + + def spans + @mutex.synchronize { @spans.dup } + end + + def logs + @mutex.synchronize { @logs.dup } + end + + def reset + @mutex.synchronize do + @spans.clear + @logs.clear + end + end + + private + + def ok(res) + res.status = 200 + res.body = 'OK' + end + + def body_bytes(req) + raw = req.body.to_s + if req['content-encoding'].to_s.include?('gzip') + Zlib::GzipReader.new(StringIO.new(raw)).read + else + raw + end + end + + def ingest_traces(req) + req_msg = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(body_bytes(req)) + parsed = [] + req_msg.resource_spans.each do |rs| + rs.scope_spans.each do |ss| + scope = ss.scope&.name + ss.spans.each do |s| + parsed << Span.new( + name: s.name, + kind: s.kind, + scope: scope, + attributes: kv(s.attributes), + events: s.events.map { |e| { name: e.name, attributes: kv(e.attributes) } } + ) + end + end + end + @mutex.synchronize { @spans.concat(parsed) } + rescue StandardError => e + warn "[OtlpSink] trace decode error: #{e.class}: #{e.message}" + end + + def ingest_logs(req) + req_msg = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(body_bytes(req)) + parsed = [] + req_msg.resource_logs.each do |rl| + rl.scope_logs.each do |sl| + sl.log_records.each do |lr| + parsed << LogRecord.new( + body: any_value(lr.body), + severity: lr.severity_text, + attributes: kv(lr.attributes) + ) + end + end + end + @mutex.synchronize { @logs.concat(parsed) } + rescue StandardError => e + warn "[OtlpSink] log decode error: #{e.class}: #{e.message}" + end + + # Flatten a repeated KeyValue list into a plain Ruby hash. + def kv(attributes) + attributes.each_with_object({}) do |a, h| + h[a.key] = any_value(a.value) + end + end + + # Extract the set field of an OTLP AnyValue. + def any_value(value) + return nil if value.nil? + + case value.value + when :string_value then value.string_value + when :bool_value then value.bool_value + when :int_value then value.int_value + when :double_value then value.double_value + else value.string_value + end + end + end +end diff --git a/e2e/ruby/rails/demo-rails70/test/system/.keep b/e2e/ruby/rails/demo-rails70/test/system/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/test/test_helper.rb b/e2e/ruby/rails/demo-rails70/test/test_helper.rb new file mode 100644 index 0000000000..5fa624cf94 --- /dev/null +++ b/e2e/ruby/rails/demo-rails70/test/test_helper.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +ENV['RAILS_ENV'] ||= 'test' + +# The observability plugin only configures OpenTelemetry (and installs the Rails +# auto-instrumentation) when the LaunchDarkly client registers it, which requires +# a non-empty SDK key. Set a dummy key BEFORE the app boots so the instrumentation +# attaches during initialization. The key is invalid, so the client never connects +# (background connection attempts fail gracefully and do not affect tests). +ENV['LAUNCHDARKLY_SDK_KEY'] ||= 'sdk-test-0000000000000000000000' + +# Point the OTLP exporter at the in-process sink (test/support/otlp_sink.rb) +# BEFORE the app boots — the plugin reads this when it builds the exporters at +# boot. This keeps the E2E test fully self-contained: no external collector, +# no network egress, no LaunchDarkly backend. +OTLP_SINK_PORT = (ENV['OTLP_SINK_PORT'] || '4327').to_i +ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ||= "http://127.0.0.1:#{OTLP_SINK_PORT}" + +require_relative '../config/environment' +require 'rails/test_help' +require_relative 'support/otlp_sink' + +# Start the sink once for the whole suite and tear it down at exit. +OTLP_SINK = OtlpSink::Server.new(port: OTLP_SINK_PORT).start +Minitest.after_run { OTLP_SINK.stop } + +module ActiveSupport + class TestCase + # Run tests in a single process: the OTLP sink binds a port in THIS process, + # so forked parallel workers would not share its collected telemetry. + parallelize(workers: 1) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/e2e/ruby/rails/demo-rails70/vendor/.keep b/e2e/ruby/rails/demo-rails70/vendor/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/ruby/rails/demo-rails70/vendor/javascript/.keep b/e2e/ruby/rails/demo-rails70/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2 From 439d8b2091abb28f2c4b13d53c5d4f128805a3dd Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 18 Jun 2026 12:53:59 -0500 Subject: [PATCH 07/18] fix(ruby): keep OTel auto-instrumentation working on Rails 7.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The observability plugin depended on opentelemetry-instrumentation-all, whose Rails-family members raised their floor to Rails 7.1. On Rails 7.0 every Rails-family instrumentation failed its compatible? check, logged a flurry of 'failed to install' warnings, and never attached — so apps lost all Rails auto-instrumentation (no HTTP server spans, DB spans, etc.) and got only manual instrumentation. The meta gem couples versions, so the only fix is to stop using it. - Replace opentelemetry-instrumentation-all with individual instrumentation gems (lib/launchdarkly_observability/instrumentations.rb). The Rails family is capped below each member's Rails-7.1-enforcing release (rails <0.42, action_pack <0.18, active_record/action_view <0.13, active_support <0.12, active_job <0.12, action_mailer <0.8, active_storage <0.5); everything else tracks the latest. These capped releases still work on Rails 7.1+, so modern apps are unaffected, and use_all still activates any extra instrumentation gem a consumer adds. - Replace the per-instrumentation 'failed to install' log flurry with a single actionable summary via a logger filter (InstrumentationLogFilter), naming the instrumentations that could not attach and how to resolve it. Verified on the demo-rails70 repro: 0 'failed to install', all 13 instrumentations install, and traces (server span) + a log + a captured exception are exported to the OTLP sink. 5 runs, 16 assertions, 0 failures. --- e2e/ruby/rails/demo-rails70/Gemfile.lock | 128 ++++-------------- .../launchdarkly-observability.gemspec | 43 +++++- .../lib/launchdarkly_observability.rb | 6 +- .../instrumentations.rb | 43 ++++++ .../opentelemetry_config.rb | 90 +++++++++++- 5 files changed, 205 insertions(+), 105 deletions(-) create mode 100644 sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb diff --git a/e2e/ruby/rails/demo-rails70/Gemfile.lock b/e2e/ruby/rails/demo-rails70/Gemfile.lock index 5ed1ea150f..3b1ce4551d 100644 --- a/e2e/ruby/rails/demo-rails70/Gemfile.lock +++ b/e2e/ruby/rails/demo-rails70/Gemfile.lock @@ -5,7 +5,25 @@ PATH launchdarkly-server-sdk (>= 8.11.0) opentelemetry-exporter-otlp (~> 0.28) opentelemetry-exporter-otlp-logs (~> 0.1) - opentelemetry-instrumentation-all (~> 0.62) + opentelemetry-instrumentation-action_mailer (< 0.8) + opentelemetry-instrumentation-action_pack (< 0.18) + opentelemetry-instrumentation-action_view (< 0.13) + opentelemetry-instrumentation-active_job (< 0.12) + opentelemetry-instrumentation-active_record (< 0.13) + opentelemetry-instrumentation-active_storage (< 0.5) + opentelemetry-instrumentation-active_support (< 0.12) + opentelemetry-instrumentation-concurrent_ruby (>= 0.21) + opentelemetry-instrumentation-faraday (>= 0.24) + opentelemetry-instrumentation-graphql (>= 0.28) + opentelemetry-instrumentation-http (>= 0.23) + opentelemetry-instrumentation-mysql2 (>= 0.28) + opentelemetry-instrumentation-net_http (>= 0.22) + opentelemetry-instrumentation-pg (>= 0.29) + opentelemetry-instrumentation-rack (>= 0.24) + opentelemetry-instrumentation-rails (>= 0.34, < 0.42) + opentelemetry-instrumentation-redis (>= 0.25) + opentelemetry-instrumentation-sidekiq (>= 0.25) + opentelemetry-instrumentation-sinatra (>= 0.24) opentelemetry-logs-sdk (~> 0.1) opentelemetry-sdk (~> 1.4) opentelemetry-semantic_conventions (~> 1.10) @@ -247,104 +265,32 @@ GEM opentelemetry-helpers-sql-processor (0.5.0) opentelemetry-api (~> 1.0) opentelemetry-common (~> 0.21) - opentelemetry-instrumentation-action_mailer (0.8.1) + opentelemetry-instrumentation-action_mailer (0.7.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-action_pack (0.18.0) + opentelemetry-instrumentation-action_pack (0.17.0) opentelemetry-instrumentation-rack (~> 0.29) - opentelemetry-instrumentation-action_view (0.13.0) + opentelemetry-instrumentation-action_view (0.12.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-active_job (0.13.0) + opentelemetry-instrumentation-active_job (0.11.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-active_model_serializers (0.25.0) - opentelemetry-instrumentation-active_support (>= 0.7.0) - opentelemetry-instrumentation-active_record (0.13.0) + opentelemetry-instrumentation-active_record (0.12.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-active_storage (0.5.1) + opentelemetry-instrumentation-active_storage (0.4.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-active_support (0.12.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-all (0.94.0) - opentelemetry-instrumentation-active_model_serializers (~> 0.25.0) - opentelemetry-instrumentation-anthropic (~> 0.5.0) - opentelemetry-instrumentation-aws_lambda (~> 0.7.0) - opentelemetry-instrumentation-aws_sdk (~> 0.12.0) - opentelemetry-instrumentation-bunny (~> 0.25.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0) - opentelemetry-instrumentation-dalli (~> 0.30.0) - opentelemetry-instrumentation-delayed_job (~> 0.26.0) - opentelemetry-instrumentation-ethon (~> 0.29.0) - opentelemetry-instrumentation-excon (~> 0.29.0) - opentelemetry-instrumentation-faraday (~> 0.33.0) - opentelemetry-instrumentation-grape (~> 0.7.0) - opentelemetry-instrumentation-graphql (~> 0.32.0) - opentelemetry-instrumentation-grpc (~> 0.5.0) - opentelemetry-instrumentation-gruf (~> 0.6.0) - opentelemetry-instrumentation-http (~> 0.30.0) - opentelemetry-instrumentation-http_client (~> 0.29.0) - opentelemetry-instrumentation-httpx (~> 0.8.0) - opentelemetry-instrumentation-koala (~> 0.24.0) - opentelemetry-instrumentation-lmdb (~> 0.26.0) - opentelemetry-instrumentation-mongo (~> 0.26.0) - opentelemetry-instrumentation-mysql2 (~> 0.34.0) - opentelemetry-instrumentation-net_http (~> 0.29.0) - opentelemetry-instrumentation-pg (~> 0.36.0) - opentelemetry-instrumentation-que (~> 0.13.0) - opentelemetry-instrumentation-racecar (~> 0.7.0) - opentelemetry-instrumentation-rack (~> 0.31.0) - opentelemetry-instrumentation-rails (~> 0.42.0) - opentelemetry-instrumentation-rake (~> 0.6.0) - opentelemetry-instrumentation-rdkafka (~> 0.10.0) - opentelemetry-instrumentation-redis (~> 0.29.0) - opentelemetry-instrumentation-resque (~> 0.9.0) - opentelemetry-instrumentation-restclient (~> 0.28.0) - opentelemetry-instrumentation-ruby_kafka (~> 0.25.0) - opentelemetry-instrumentation-sidekiq (~> 0.29.0) - opentelemetry-instrumentation-sinatra (~> 0.30.0) - opentelemetry-instrumentation-trilogy (~> 0.69.0) - opentelemetry-instrumentation-anthropic (0.5.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-aws_lambda (0.7.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-aws_sdk (0.12.0) + opentelemetry-instrumentation-active_support (0.11.0) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-base (0.26.1) opentelemetry-api (~> 1.7) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-bunny (0.25.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-concurrent_ruby (0.25.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-dalli (0.30.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-delayed_job (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-ethon (0.29.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-excon (0.29.1) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-faraday (0.33.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-grape (0.7.0) - opentelemetry-instrumentation-rack (~> 0.29) opentelemetry-instrumentation-graphql (0.32.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-grpc (0.5.1) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-gruf (0.6.1) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-http (0.30.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http_client (0.29.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-httpx (0.8.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-koala (0.24.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-lmdb (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-mongo (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-mysql2 (0.34.0) opentelemetry-helpers-mysql opentelemetry-helpers-sql @@ -356,13 +302,9 @@ GEM opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-que (0.13.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-racecar (0.7.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-rack (0.31.1) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rails (0.42.0) + opentelemetry-instrumentation-rails (0.41.0) opentelemetry-instrumentation-action_mailer (~> 0.7) opentelemetry-instrumentation-action_pack (~> 0.17) opentelemetry-instrumentation-action_view (~> 0.12) @@ -371,28 +313,12 @@ GEM opentelemetry-instrumentation-active_storage (~> 0.4) opentelemetry-instrumentation-active_support (~> 0.11) opentelemetry-instrumentation-concurrent_ruby (~> 0.25) - opentelemetry-instrumentation-rake (0.6.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rdkafka (0.10.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-redis (0.29.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-resque (0.9.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-restclient (0.28.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-ruby_kafka (0.25.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-sidekiq (0.29.0) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-sinatra (0.30.0) opentelemetry-instrumentation-rack (~> 0.29) - opentelemetry-instrumentation-trilogy (0.69.0) - opentelemetry-helpers-mysql - opentelemetry-helpers-sql - opentelemetry-helpers-sql-processor - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-semantic_conventions (>= 1.8.0) opentelemetry-logs-api (0.4.0) opentelemetry-api (~> 1.0) opentelemetry-logs-sdk (0.6.0) diff --git a/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec b/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec index 15ee881f51..47a5e2ee32 100644 --- a/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec +++ b/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec @@ -32,8 +32,49 @@ Gem::Specification.new do |spec| # versions raise "uninitialized constant LaunchDarkly::Interfaces::Plugins" on require. spec.add_dependency 'launchdarkly-server-sdk', '>= 8.11.0' spec.add_dependency 'opentelemetry-exporter-otlp', '~> 0.28' - spec.add_dependency 'opentelemetry-instrumentation-all', '~> 0.62' spec.add_dependency 'opentelemetry-sdk', '~> 1.4' + + # OpenTelemetry auto-instrumentation. + # + # We depend on INDIVIDUAL instrumentation gems instead of the + # opentelemetry-instrumentation-all meta-gem on purpose. The meta-gem couples + # every instrumentation to one version, so when the Rails-family + # instrumentations raised their floor to Rails 7.1 + # (opentelemetry-instrumentation-rails 0.42.0), the whole bundle moved with + # them and Rails 7.0 apps silently lost ALL auto-instrumentation. Listing gems + # individually keeps the Rails family on a Rails-7.0-compatible release while + # everything else tracks the latest. See lib/launchdarkly_observability/ + # instrumentations.rb. + # + # Rails family. Each of these gems independently enforces a Rails 7.1 floor in + # its latest release (the coordinated "Min Rails 7.1 enforced" bump), so the + # meta gem (opentelemetry-instrumentation-rails) is NOT enough — each member + # must be capped below its enforcing version to keep attaching on Rails 7.0. + # These releases are still compatible with Rails 7.1+, so modern apps are + # unaffected. Revisit when the plugin drops Rails 7.0 support. + spec.add_dependency 'opentelemetry-instrumentation-rails', '>= 0.34', '< 0.42' + spec.add_dependency 'opentelemetry-instrumentation-action_pack', '< 0.18' + spec.add_dependency 'opentelemetry-instrumentation-action_view', '< 0.13' + spec.add_dependency 'opentelemetry-instrumentation-active_record', '< 0.13' + spec.add_dependency 'opentelemetry-instrumentation-active_support', '< 0.12' + spec.add_dependency 'opentelemetry-instrumentation-active_job', '< 0.12' + spec.add_dependency 'opentelemetry-instrumentation-action_mailer', '< 0.8' + spec.add_dependency 'opentelemetry-instrumentation-active_storage', '< 0.5' + + # Non-Rails instrumentations: latest. Consumers can add any other + # opentelemetry-instrumentation-* gem to their Gemfile and it is picked up + # automatically (the plugin activates every loaded instrumentation). + spec.add_dependency 'opentelemetry-instrumentation-concurrent_ruby', '>= 0.21' + spec.add_dependency 'opentelemetry-instrumentation-faraday', '>= 0.24' + spec.add_dependency 'opentelemetry-instrumentation-graphql', '>= 0.28' + spec.add_dependency 'opentelemetry-instrumentation-http', '>= 0.23' + spec.add_dependency 'opentelemetry-instrumentation-mysql2', '>= 0.28' + spec.add_dependency 'opentelemetry-instrumentation-net_http', '>= 0.22' + spec.add_dependency 'opentelemetry-instrumentation-pg', '>= 0.29' + spec.add_dependency 'opentelemetry-instrumentation-rack', '>= 0.24' + spec.add_dependency 'opentelemetry-instrumentation-redis', '>= 0.25' + spec.add_dependency 'opentelemetry-instrumentation-sidekiq', '>= 0.25' + spec.add_dependency 'opentelemetry-instrumentation-sinatra', '>= 0.24' spec.add_dependency 'opentelemetry-semantic_conventions', '~> 1.10' # Logs support (included by default for out-of-box DX; opt out via enable_logs: false) diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb index 364dcd0914..6b9929c6da 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb @@ -2,9 +2,13 @@ require 'opentelemetry/sdk' require 'opentelemetry/exporter/otlp' -require 'opentelemetry/instrumentation/all' require 'opentelemetry/semantic_conventions' +# Loads the individual OpenTelemetry instrumentation gems (not the +# opentelemetry-instrumentation-all meta-gem) so the Rails family can be pinned +# for old-Rails compatibility independently of everything else. +require_relative 'launchdarkly_observability/instrumentations' + require_relative 'launchdarkly_observability/version' require_relative 'launchdarkly_observability/hook' require_relative 'launchdarkly_observability/opentelemetry_config' diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb new file mode 100644 index 0000000000..919df28927 --- /dev/null +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Loads the OpenTelemetry auto-instrumentations the plugin enables by default. +# +# We require INDIVIDUAL instrumentation gems instead of +# `opentelemetry/instrumentation/all` on purpose. The meta-gem couples every +# instrumentation to a single version, so when the Rails-family instrumentations +# raised their minimum to Rails 7.1 (opentelemetry-instrumentation-rails 0.42.0), +# the whole bundle moved with them and Rails 7.0 apps silently lost ALL +# auto-instrumentation. Requiring gems individually lets the Rails family stay on +# a Rails-7.0-compatible release (pinned in the gemspec) while everything else +# tracks the latest. +# +# `OpenTelemetry::SDK#use_all` activates every instrumentation that has been +# loaded, so any additional `opentelemetry-instrumentation-*` gem a consumer adds +# to their own Gemfile is picked up automatically alongside these defaults. + +# Rails family. Requiring this pulls action_pack, active_record, active_support, +# action_view and active_job. +require 'opentelemetry/instrumentation/rails' + +# Common non-Rails instrumentations (latest; see gemspec for version policy). +# Note the require paths differ from gem names in places, e.g. the +# opentelemetry-instrumentation-net_http gem is required as 'net/http'. +%w[ + concurrent_ruby + faraday + graphql + http + mysql2 + net/http + pg + rack + redis + sidekiq + sinatra +].each do |path| + require "opentelemetry/instrumentation/#{path}" +rescue LoadError => e + # A default instrumentation gem is unexpectedly absent. Don't abort the whole + # plugin over one missing instrumentation; the rest still load. + warn "[LaunchDarklyObservability] optional instrumentation '#{path}' not loaded: #{e.message}" +end diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb index 268418f3c6..2112742df1 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb @@ -2,8 +2,8 @@ require 'opentelemetry/sdk' require 'opentelemetry/exporter/otlp' -require 'opentelemetry/instrumentation/all' require 'opentelemetry/semantic_conventions' +require_relative 'instrumentations' module LaunchDarklyObservability # Configures OpenTelemetry SDK with appropriate providers and exporters @@ -24,6 +24,55 @@ class OpenTelemetryConfig # Metrics export interval METRICS_EXPORT_INTERVAL_MS = 60_000 + # Wraps the OpenTelemetry logger to suppress the per-instrumentation install + # chatter ("... was successfully installed" / "... failed to install") and + # record the names of instrumentations that failed to install. Everything + # else (including level / level=) is delegated to the real logger. + class InstrumentationLogFilter + FAILED_PATTERN = /Instrumentation: (\S+) failed to install/.freeze + + def initialize(delegate, failed) + @delegate = delegate + @failed = failed + end + + # OTel logs installs via OpenTelemetry.logger.info / .warn, so intercept + # the level methods (not just #add) — otherwise the calls fall through to + # method_missing and bypass the filter. + %i[debug info warn error fatal unknown].each do |level| + define_method(level) do |message = nil, &block| + forward?(message || (block && block.call)) ? @delegate.public_send(level, message, &block) : true + end + end + + def add(severity, message = nil, progname = nil, &block) + forward?(message || progname || (block&.call)) ? @delegate.add(severity, message, progname, &block) : true + end + + def method_missing(name, *args, &block) + @delegate.send(name, *args, &block) + end + + def respond_to_missing?(name, include_private = false) + @delegate.respond_to?(name, include_private) || super + end + + private + + # Returns false when the message is install chatter that should be + # suppressed (recording failed-instrumentation names as a side effect), + # true when it should be forwarded to the real logger. + def forward?(message) + text = message.to_s + if (match = text.match(FAILED_PATTERN)) + @failed << match[1] + return false + end + + !text.include?('was successfully installed') + end + end + # @return [String] The LaunchDarkly project ID attr_reader :project_id @@ -163,11 +212,48 @@ def configure_instrumentations(config) } user_config = @options.fetch(:instrumentations, {}) - config.use_all(defaults.merge(user_config)) + + # Replace the OTel SDK's per-instrumentation install logging (a flurry of + # "Instrumentation: failed to install" WARN lines when instrumentations + # are incompatible with the framework version — e.g. the Rails family on a + # Rails version below its floor) with a single, actionable summary. + failed = with_captured_instrumentation_failures do + config.use_all(defaults.merge(user_config)) + end + warn_failed_instrumentations(failed) unless failed.empty? rescue StandardError => e warn "[LaunchDarklyObservability] Error configuring instrumentations: #{e.message}" end + # Temporarily swap OpenTelemetry.logger for a filter that suppresses the + # per-instrumentation install chatter and records the names of any + # instrumentations that report "failed to install", returning them so the + # caller can emit a single summary instead of a noisy flurry. + def with_captured_instrumentation_failures + original = OpenTelemetry.logger + failed = [] + OpenTelemetry.logger = InstrumentationLogFilter.new(original, failed) + yield + failed + ensure + OpenTelemetry.logger = original + end + + # Emit ONE actionable warning naming the instrumentations that could not + # attach and how to resolve it. Telemetry that does not depend on those + # instrumentations (flag-eval spans, manual instrumentation, logs, errors) + # keeps working regardless. + def warn_failed_instrumentations(failed) + names = failed.map { |n| n.sub('OpenTelemetry::Instrumentation::', '') }.uniq + rails_part = defined?(::Rails) && ::Rails.respond_to?(:version) ? " on Rails #{::Rails.version}" : '' + warn "[LaunchDarklyObservability] #{names.size} OpenTelemetry instrumentation(s) could not " \ + "attach#{rails_part} (Ruby #{RUBY_VERSION}): #{names.join(', ')}. Those libraries will not " \ + 'be auto-instrumented; flag-eval spans, manual instrumentation, logs and error capture are ' \ + 'unaffected. This usually means an instrumentation gem dropped support for your framework ' \ + 'version — upgrade the framework, or pin the instrumentation gem to a compatible release ' \ + '(e.g. gem "opentelemetry-instrumentation-rails", "~> 0.41").' + end + # Configure OpenTelemetry logs with OTLP exporter. # The log gems are runtime dependencies, so require should always succeed. # If anything goes wrong, we warn once and leave traces unaffected. From cbb316dabac44ae3d94278a55231dfbcfb566f13 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 18 Jun 2026 13:09:12 -0500 Subject: [PATCH 08/18] refactor(ruby): extract InstrumentationLogFilter; capture install failures correctly - Move the log-suppression filter into its own file (instrumentation_log_filter.rb) with class-level capture_failures/ failure_warning helpers (keeps OpenTelemetryConfig under the class-length limit and is unit-testable). - Wrap the whole OpenTelemetry::SDK.configure call rather than just use_all: the SDK installs instrumentations AFTER the configure block returns, so the earlier placement around use_all never saw the install logging. Now the per-instrumentation 'failed to install' / 'successfully installed' chatter is actually suppressed and failures are collected for the single summary. - rails.rb: Railtie#otel_logger_provider_available? now returns an explicit boolean instead of nil when the logs constant is absent. Fixes a pre-existing order-dependent failure in rails_railtie_test (verified failing identically on the unmodified gem at the same seed). Updated e2e Gemfile.locks for the new individual-gem resolution. demo (7.2) and api-only (7.2) suites pass; demo-rails70 (7.0) passes with 0 'failed to install'. --- e2e/ruby/rails/api-only/Gemfile.lock | 128 ++++-------------- e2e/ruby/rails/demo/Gemfile.lock | 128 ++++-------------- .../instrumentation_log_filter.rb | 85 ++++++++++++ .../opentelemetry_config.rb | 103 +++----------- .../lib/launchdarkly_observability/rails.rb | 8 +- 5 files changed, 161 insertions(+), 291 deletions(-) create mode 100644 sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentation_log_filter.rb diff --git a/e2e/ruby/rails/api-only/Gemfile.lock b/e2e/ruby/rails/api-only/Gemfile.lock index ed80c2059f..6344c7b83a 100644 --- a/e2e/ruby/rails/api-only/Gemfile.lock +++ b/e2e/ruby/rails/api-only/Gemfile.lock @@ -5,7 +5,25 @@ PATH launchdarkly-server-sdk (>= 8.11.0) opentelemetry-exporter-otlp (~> 0.28) opentelemetry-exporter-otlp-logs (~> 0.1) - opentelemetry-instrumentation-all (~> 0.62) + opentelemetry-instrumentation-action_mailer (< 0.8) + opentelemetry-instrumentation-action_pack (< 0.18) + opentelemetry-instrumentation-action_view (< 0.13) + opentelemetry-instrumentation-active_job (< 0.12) + opentelemetry-instrumentation-active_record (< 0.13) + opentelemetry-instrumentation-active_storage (< 0.5) + opentelemetry-instrumentation-active_support (< 0.12) + opentelemetry-instrumentation-concurrent_ruby (>= 0.21) + opentelemetry-instrumentation-faraday (>= 0.24) + opentelemetry-instrumentation-graphql (>= 0.28) + opentelemetry-instrumentation-http (>= 0.23) + opentelemetry-instrumentation-mysql2 (>= 0.28) + opentelemetry-instrumentation-net_http (>= 0.22) + opentelemetry-instrumentation-pg (>= 0.29) + opentelemetry-instrumentation-rack (>= 0.24) + opentelemetry-instrumentation-rails (>= 0.34, < 0.42) + opentelemetry-instrumentation-redis (>= 0.25) + opentelemetry-instrumentation-sidekiq (>= 0.25) + opentelemetry-instrumentation-sinatra (>= 0.24) opentelemetry-logs-sdk (~> 0.1) opentelemetry-sdk (~> 1.4) opentelemetry-semantic_conventions (~> 1.10) @@ -222,104 +240,32 @@ GEM opentelemetry-helpers-sql-processor (0.5.0) opentelemetry-api (~> 1.0) opentelemetry-common (~> 0.21) - opentelemetry-instrumentation-action_mailer (0.8.0) + opentelemetry-instrumentation-action_mailer (0.7.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-action_pack (0.18.0) + opentelemetry-instrumentation-action_pack (0.17.0) opentelemetry-instrumentation-rack (~> 0.29) - opentelemetry-instrumentation-action_view (0.13.0) + opentelemetry-instrumentation-action_view (0.12.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-active_job (0.12.0) + opentelemetry-instrumentation-active_job (0.11.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-active_model_serializers (0.25.0) - opentelemetry-instrumentation-active_support (>= 0.7.0) - opentelemetry-instrumentation-active_record (0.13.0) + opentelemetry-instrumentation-active_record (0.12.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-active_storage (0.5.0) + opentelemetry-instrumentation-active_storage (0.4.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-active_support (0.12.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-all (0.93.0) - opentelemetry-instrumentation-active_model_serializers (~> 0.25.0) - opentelemetry-instrumentation-anthropic (~> 0.5.0) - opentelemetry-instrumentation-aws_lambda (~> 0.7.0) - opentelemetry-instrumentation-aws_sdk (~> 0.12.0) - opentelemetry-instrumentation-bunny (~> 0.25.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0) - opentelemetry-instrumentation-dalli (~> 0.30.0) - opentelemetry-instrumentation-delayed_job (~> 0.26.0) - opentelemetry-instrumentation-ethon (~> 0.29.0) - opentelemetry-instrumentation-excon (~> 0.29.0) - opentelemetry-instrumentation-faraday (~> 0.33.0) - opentelemetry-instrumentation-grape (~> 0.7.0) - opentelemetry-instrumentation-graphql (~> 0.32.0) - opentelemetry-instrumentation-grpc (~> 0.5.0) - opentelemetry-instrumentation-gruf (~> 0.6.0) - opentelemetry-instrumentation-http (~> 0.30.0) - opentelemetry-instrumentation-http_client (~> 0.29.0) - opentelemetry-instrumentation-httpx (~> 0.8.0) - opentelemetry-instrumentation-koala (~> 0.24.0) - opentelemetry-instrumentation-lmdb (~> 0.26.0) - opentelemetry-instrumentation-mongo (~> 0.26.0) - opentelemetry-instrumentation-mysql2 (~> 0.34.0) - opentelemetry-instrumentation-net_http (~> 0.29.0) - opentelemetry-instrumentation-pg (~> 0.36.0) - opentelemetry-instrumentation-que (~> 0.13.0) - opentelemetry-instrumentation-racecar (~> 0.7.0) - opentelemetry-instrumentation-rack (~> 0.31.0) - opentelemetry-instrumentation-rails (~> 0.42.0) - opentelemetry-instrumentation-rake (~> 0.6.0) - opentelemetry-instrumentation-rdkafka (~> 0.10.0) - opentelemetry-instrumentation-redis (~> 0.29.0) - opentelemetry-instrumentation-resque (~> 0.9.0) - opentelemetry-instrumentation-restclient (~> 0.28.0) - opentelemetry-instrumentation-ruby_kafka (~> 0.25.0) - opentelemetry-instrumentation-sidekiq (~> 0.29.0) - opentelemetry-instrumentation-sinatra (~> 0.30.0) - opentelemetry-instrumentation-trilogy (~> 0.68.0) - opentelemetry-instrumentation-anthropic (0.5.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-aws_lambda (0.7.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-aws_sdk (0.12.0) + opentelemetry-instrumentation-active_support (0.11.0) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-base (0.26.0) opentelemetry-api (~> 1.7) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-bunny (0.25.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-concurrent_ruby (0.25.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-dalli (0.30.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-delayed_job (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-ethon (0.29.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-excon (0.29.1) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-faraday (0.33.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-grape (0.7.0) - opentelemetry-instrumentation-rack (~> 0.29) opentelemetry-instrumentation-graphql (0.32.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-grpc (0.5.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-gruf (0.6.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-http (0.30.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http_client (0.29.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-httpx (0.8.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-koala (0.24.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-lmdb (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-mongo (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-mysql2 (0.34.0) opentelemetry-helpers-mysql opentelemetry-helpers-sql @@ -331,13 +277,9 @@ GEM opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-que (0.13.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-racecar (0.7.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-rack (0.31.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rails (0.42.0) + opentelemetry-instrumentation-rails (0.41.0) opentelemetry-instrumentation-action_mailer (~> 0.7) opentelemetry-instrumentation-action_pack (~> 0.17) opentelemetry-instrumentation-action_view (~> 0.12) @@ -346,28 +288,12 @@ GEM opentelemetry-instrumentation-active_storage (~> 0.4) opentelemetry-instrumentation-active_support (~> 0.11) opentelemetry-instrumentation-concurrent_ruby (~> 0.25) - opentelemetry-instrumentation-rake (0.6.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rdkafka (0.10.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-redis (0.29.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-resque (0.9.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-restclient (0.28.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-ruby_kafka (0.25.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-sidekiq (0.29.0) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-sinatra (0.30.0) opentelemetry-instrumentation-rack (~> 0.29) - opentelemetry-instrumentation-trilogy (0.68.0) - opentelemetry-helpers-mysql - opentelemetry-helpers-sql - opentelemetry-helpers-sql-processor - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-semantic_conventions (>= 1.8.0) opentelemetry-logs-api (0.4.0) opentelemetry-api (~> 1.0) opentelemetry-logs-sdk (0.6.0) diff --git a/e2e/ruby/rails/demo/Gemfile.lock b/e2e/ruby/rails/demo/Gemfile.lock index c0fba9c358..00b19b9028 100644 --- a/e2e/ruby/rails/demo/Gemfile.lock +++ b/e2e/ruby/rails/demo/Gemfile.lock @@ -5,7 +5,25 @@ PATH launchdarkly-server-sdk (>= 8.11.0) opentelemetry-exporter-otlp (~> 0.28) opentelemetry-exporter-otlp-logs (~> 0.1) - opentelemetry-instrumentation-all (~> 0.62) + opentelemetry-instrumentation-action_mailer (< 0.8) + opentelemetry-instrumentation-action_pack (< 0.18) + opentelemetry-instrumentation-action_view (< 0.13) + opentelemetry-instrumentation-active_job (< 0.12) + opentelemetry-instrumentation-active_record (< 0.13) + opentelemetry-instrumentation-active_storage (< 0.5) + opentelemetry-instrumentation-active_support (< 0.12) + opentelemetry-instrumentation-concurrent_ruby (>= 0.21) + opentelemetry-instrumentation-faraday (>= 0.24) + opentelemetry-instrumentation-graphql (>= 0.28) + opentelemetry-instrumentation-http (>= 0.23) + opentelemetry-instrumentation-mysql2 (>= 0.28) + opentelemetry-instrumentation-net_http (>= 0.22) + opentelemetry-instrumentation-pg (>= 0.29) + opentelemetry-instrumentation-rack (>= 0.24) + opentelemetry-instrumentation-rails (>= 0.34, < 0.42) + opentelemetry-instrumentation-redis (>= 0.25) + opentelemetry-instrumentation-sidekiq (>= 0.25) + opentelemetry-instrumentation-sinatra (>= 0.24) opentelemetry-logs-sdk (~> 0.1) opentelemetry-sdk (~> 1.4) opentelemetry-semantic_conventions (~> 1.10) @@ -242,104 +260,32 @@ GEM opentelemetry-helpers-sql-processor (0.5.0) opentelemetry-api (~> 1.0) opentelemetry-common (~> 0.21) - opentelemetry-instrumentation-action_mailer (0.8.0) + opentelemetry-instrumentation-action_mailer (0.7.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-action_pack (0.18.0) + opentelemetry-instrumentation-action_pack (0.17.0) opentelemetry-instrumentation-rack (~> 0.29) - opentelemetry-instrumentation-action_view (0.13.0) + opentelemetry-instrumentation-action_view (0.12.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-active_job (0.12.0) + opentelemetry-instrumentation-active_job (0.11.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-active_model_serializers (0.25.0) - opentelemetry-instrumentation-active_support (>= 0.7.0) - opentelemetry-instrumentation-active_record (0.13.0) + opentelemetry-instrumentation-active_record (0.12.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-active_storage (0.5.0) + opentelemetry-instrumentation-active_storage (0.4.0) opentelemetry-instrumentation-active_support (~> 0.10) - opentelemetry-instrumentation-active_support (0.12.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-all (0.93.0) - opentelemetry-instrumentation-active_model_serializers (~> 0.25.0) - opentelemetry-instrumentation-anthropic (~> 0.5.0) - opentelemetry-instrumentation-aws_lambda (~> 0.7.0) - opentelemetry-instrumentation-aws_sdk (~> 0.12.0) - opentelemetry-instrumentation-bunny (~> 0.25.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0) - opentelemetry-instrumentation-dalli (~> 0.30.0) - opentelemetry-instrumentation-delayed_job (~> 0.26.0) - opentelemetry-instrumentation-ethon (~> 0.29.0) - opentelemetry-instrumentation-excon (~> 0.29.0) - opentelemetry-instrumentation-faraday (~> 0.33.0) - opentelemetry-instrumentation-grape (~> 0.7.0) - opentelemetry-instrumentation-graphql (~> 0.32.0) - opentelemetry-instrumentation-grpc (~> 0.5.0) - opentelemetry-instrumentation-gruf (~> 0.6.0) - opentelemetry-instrumentation-http (~> 0.30.0) - opentelemetry-instrumentation-http_client (~> 0.29.0) - opentelemetry-instrumentation-httpx (~> 0.8.0) - opentelemetry-instrumentation-koala (~> 0.24.0) - opentelemetry-instrumentation-lmdb (~> 0.26.0) - opentelemetry-instrumentation-mongo (~> 0.26.0) - opentelemetry-instrumentation-mysql2 (~> 0.34.0) - opentelemetry-instrumentation-net_http (~> 0.29.0) - opentelemetry-instrumentation-pg (~> 0.36.0) - opentelemetry-instrumentation-que (~> 0.13.0) - opentelemetry-instrumentation-racecar (~> 0.7.0) - opentelemetry-instrumentation-rack (~> 0.31.0) - opentelemetry-instrumentation-rails (~> 0.42.0) - opentelemetry-instrumentation-rake (~> 0.6.0) - opentelemetry-instrumentation-rdkafka (~> 0.10.0) - opentelemetry-instrumentation-redis (~> 0.29.0) - opentelemetry-instrumentation-resque (~> 0.9.0) - opentelemetry-instrumentation-restclient (~> 0.28.0) - opentelemetry-instrumentation-ruby_kafka (~> 0.25.0) - opentelemetry-instrumentation-sidekiq (~> 0.29.0) - opentelemetry-instrumentation-sinatra (~> 0.30.0) - opentelemetry-instrumentation-trilogy (~> 0.68.0) - opentelemetry-instrumentation-anthropic (0.5.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-aws_lambda (0.7.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-aws_sdk (0.12.0) + opentelemetry-instrumentation-active_support (0.11.0) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-base (0.26.0) opentelemetry-api (~> 1.7) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-bunny (0.25.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-concurrent_ruby (0.25.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-dalli (0.30.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-delayed_job (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-ethon (0.29.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-excon (0.29.1) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-faraday (0.33.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-grape (0.7.0) - opentelemetry-instrumentation-rack (~> 0.29) opentelemetry-instrumentation-graphql (0.32.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-grpc (0.5.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-gruf (0.6.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-http (0.30.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http_client (0.29.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-httpx (0.8.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-koala (0.24.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-lmdb (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-mongo (0.26.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-mysql2 (0.34.0) opentelemetry-helpers-mysql opentelemetry-helpers-sql @@ -351,13 +297,9 @@ GEM opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-que (0.13.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-racecar (0.7.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-rack (0.31.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rails (0.42.0) + opentelemetry-instrumentation-rails (0.41.0) opentelemetry-instrumentation-action_mailer (~> 0.7) opentelemetry-instrumentation-action_pack (~> 0.17) opentelemetry-instrumentation-action_view (~> 0.12) @@ -366,28 +308,12 @@ GEM opentelemetry-instrumentation-active_storage (~> 0.4) opentelemetry-instrumentation-active_support (~> 0.11) opentelemetry-instrumentation-concurrent_ruby (~> 0.25) - opentelemetry-instrumentation-rake (0.6.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rdkafka (0.10.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-redis (0.29.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-resque (0.9.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-restclient (0.28.0) - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-ruby_kafka (0.25.0) - opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-sidekiq (0.29.0) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-sinatra (0.30.0) opentelemetry-instrumentation-rack (~> 0.29) - opentelemetry-instrumentation-trilogy (0.68.0) - opentelemetry-helpers-mysql - opentelemetry-helpers-sql - opentelemetry-helpers-sql-processor - opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-semantic_conventions (>= 1.8.0) opentelemetry-logs-api (0.4.0) opentelemetry-api (~> 1.0) opentelemetry-logs-sdk (0.6.0) diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentation_log_filter.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentation_log_filter.rb new file mode 100644 index 0000000000..76611b52a5 --- /dev/null +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentation_log_filter.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module LaunchDarklyObservability + # Wraps the OpenTelemetry logger to suppress the per-instrumentation install + # chatter ("... was successfully installed" / "... failed to install") and + # record the names of instrumentations that failed to install. Everything else + # (including level / level=) is delegated to the real logger. + # + # OpenTelemetryConfig uses this to replace the SDK's flurry of + # per-instrumentation warnings with a single actionable summary. + class InstrumentationLogFilter + FAILED_PATTERN = /Instrumentation: (\S+) failed to install/ + + # Run the block with OpenTelemetry.logger swapped for a filter that + # suppresses per-instrumentation install chatter, returning the names of any + # instrumentations that reported "failed to install". The SDK installs + # instrumentations after a configure block returns, so wrap the whole + # OpenTelemetry::SDK.configure call — not just use_all. + def self.capture_failures + original = OpenTelemetry.logger + failed = [] + OpenTelemetry.logger = new(original, failed) + yield + failed + ensure + OpenTelemetry.logger = original + end + + # Build ONE actionable warning naming the instrumentations that could not + # attach and how to resolve it. Telemetry that does not depend on those + # instrumentations (flag-eval spans, manual instrumentation, logs, errors) + # keeps working regardless. + def self.failure_warning(failed) + names = failed.map { |n| n.sub('OpenTelemetry::Instrumentation::', '') }.uniq + rails = defined?(::Rails) && ::Rails.respond_to?(:version) ? " on Rails #{::Rails.version}" : '' + "[LaunchDarklyObservability] #{names.size} OpenTelemetry instrumentation(s) could not attach" \ + "#{rails} (Ruby #{RUBY_VERSION}): #{names.join(', ')}. Those libraries will not be " \ + 'auto-instrumented; flag-eval spans, manual instrumentation, logs and error capture are ' \ + 'unaffected. This usually means an instrumentation gem dropped support for your framework ' \ + 'version — upgrade the framework, or pin the instrumentation gem to a compatible release ' \ + '(e.g. gem "opentelemetry-instrumentation-rails", "~> 0.41").' + end + + def initialize(delegate, failed) + @delegate = delegate + @failed = failed + end + + # OTel logs installs via OpenTelemetry.logger.info / .warn, so intercept the + # level methods (not just #add) — otherwise the calls fall through to + # method_missing and bypass the filter. + %i[debug info warn error fatal unknown].each do |level| + define_method(level) do |message = nil, &block| + forward?(message || block&.call) ? @delegate.public_send(level, message, &block) : true + end + end + + def add(severity, message = nil, progname = nil, &block) + forward?(message || progname || block&.call) ? @delegate.add(severity, message, progname, &block) : true + end + + def method_missing(name, ...) + @delegate.send(name, ...) + end + + def respond_to_missing?(name, include_private = false) + @delegate.respond_to?(name, include_private) || super + end + + private + + # Returns false when the message is install chatter that should be suppressed + # (recording failed-instrumentation names as a side effect), true when it + # should be forwarded to the real logger. + def forward?(message) + text = message.to_s + if (match = text.match(FAILED_PATTERN)) + @failed << match[1] + return false + end + + !text.include?('was successfully installed') + end + end +end diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb index 2112742df1..3e96b8f659 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb @@ -4,6 +4,7 @@ require 'opentelemetry/exporter/otlp' require 'opentelemetry/semantic_conventions' require_relative 'instrumentations' +require_relative 'instrumentation_log_filter' module LaunchDarklyObservability # Configures OpenTelemetry SDK with appropriate providers and exporters @@ -24,55 +25,6 @@ class OpenTelemetryConfig # Metrics export interval METRICS_EXPORT_INTERVAL_MS = 60_000 - # Wraps the OpenTelemetry logger to suppress the per-instrumentation install - # chatter ("... was successfully installed" / "... failed to install") and - # record the names of instrumentations that failed to install. Everything - # else (including level / level=) is delegated to the real logger. - class InstrumentationLogFilter - FAILED_PATTERN = /Instrumentation: (\S+) failed to install/.freeze - - def initialize(delegate, failed) - @delegate = delegate - @failed = failed - end - - # OTel logs installs via OpenTelemetry.logger.info / .warn, so intercept - # the level methods (not just #add) — otherwise the calls fall through to - # method_missing and bypass the filter. - %i[debug info warn error fatal unknown].each do |level| - define_method(level) do |message = nil, &block| - forward?(message || (block && block.call)) ? @delegate.public_send(level, message, &block) : true - end - end - - def add(severity, message = nil, progname = nil, &block) - forward?(message || progname || (block&.call)) ? @delegate.add(severity, message, progname, &block) : true - end - - def method_missing(name, *args, &block) - @delegate.send(name, *args, &block) - end - - def respond_to_missing?(name, include_private = false) - @delegate.respond_to?(name, include_private) || super - end - - private - - # Returns false when the message is install chatter that should be - # suppressed (recording failed-instrumentation names as a side effect), - # true when it should be forwarded to the real logger. - def forward?(message) - text = message.to_s - if (match = text.match(FAILED_PATTERN)) - @failed << match[1] - return false - end - - !text.include?('was successfully installed') - end - end - # @return [String] The LaunchDarkly project ID attr_reader :project_id @@ -127,7 +79,7 @@ def configure # #register / #configure — is created lazily afterward. Exporters are added # later by #configure_traces when the client registers the plugin. def install_instrumentation_only - OpenTelemetry::SDK.configure do |c| + configure_sdk_capturing_failures do |c| c.resource = create_resource configure_instrumentations(c) end @@ -167,7 +119,7 @@ def configure_traces return end - OpenTelemetry::SDK.configure do |c| + configure_sdk_capturing_failures do |c| c.resource = create_resource c.add_span_processor(create_batch_span_processor) @@ -212,46 +164,23 @@ def configure_instrumentations(config) } user_config = @options.fetch(:instrumentations, {}) - - # Replace the OTel SDK's per-instrumentation install logging (a flurry of - # "Instrumentation: failed to install" WARN lines when instrumentations - # are incompatible with the framework version — e.g. the Rails family on a - # Rails version below its floor) with a single, actionable summary. - failed = with_captured_instrumentation_failures do - config.use_all(defaults.merge(user_config)) - end - warn_failed_instrumentations(failed) unless failed.empty? + config.use_all(defaults.merge(user_config)) rescue StandardError => e warn "[LaunchDarklyObservability] Error configuring instrumentations: #{e.message}" end - # Temporarily swap OpenTelemetry.logger for a filter that suppresses the - # per-instrumentation install chatter and records the names of any - # instrumentations that report "failed to install", returning them so the - # caller can emit a single summary instead of a noisy flurry. - def with_captured_instrumentation_failures - original = OpenTelemetry.logger - failed = [] - OpenTelemetry.logger = InstrumentationLogFilter.new(original, failed) - yield - failed - ensure - OpenTelemetry.logger = original - end - - # Emit ONE actionable warning naming the instrumentations that could not - # attach and how to resolve it. Telemetry that does not depend on those - # instrumentations (flag-eval spans, manual instrumentation, logs, errors) - # keeps working regardless. - def warn_failed_instrumentations(failed) - names = failed.map { |n| n.sub('OpenTelemetry::Instrumentation::', '') }.uniq - rails_part = defined?(::Rails) && ::Rails.respond_to?(:version) ? " on Rails #{::Rails.version}" : '' - warn "[LaunchDarklyObservability] #{names.size} OpenTelemetry instrumentation(s) could not " \ - "attach#{rails_part} (Ruby #{RUBY_VERSION}): #{names.join(', ')}. Those libraries will not " \ - 'be auto-instrumented; flag-eval spans, manual instrumentation, logs and error capture are ' \ - 'unaffected. This usually means an instrumentation gem dropped support for your framework ' \ - 'version — upgrade the framework, or pin the instrumentation gem to a compatible release ' \ - '(e.g. gem "opentelemetry-instrumentation-rails", "~> 0.41").' + # Run an OpenTelemetry::SDK.configure block, replacing the SDK's + # per-instrumentation install logging (a flurry of "Instrumentation: + # failed to install" WARN lines when instrumentations are incompatible with + # the framework version — e.g. the Rails family below its Rails floor) with a + # single actionable summary. The SDK installs the instrumentations AFTER the + # configure block returns (use_all only queues them), so the filter wraps the + # whole call, not just use_all. + def configure_sdk_capturing_failures(&block) + failed = InstrumentationLogFilter.capture_failures do + OpenTelemetry::SDK.configure(&block) + end + warn InstrumentationLogFilter.failure_warning(failed) unless failed.empty? end # Configure OpenTelemetry logs with OTLP exporter. diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb index 90045b7df4..c92b004751 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb @@ -136,8 +136,12 @@ def attach_otel_log_bridge # file is still loading, so the module method does not exist yet and the # delegation raised "undefined method `otel_logger_provider_available?'". def otel_logger_provider_available? - defined?(OpenTelemetry::SDK::Logs::LoggerProvider) && - OpenTelemetry.respond_to?(:logger_provider) && + # `defined?` returns nil (not false) when the constant is absent, so + # guard first and return an explicit boolean — callers (and the + # railtie test) expect true/false, never nil. + return false unless defined?(OpenTelemetry::SDK::Logs::LoggerProvider) + + OpenTelemetry.respond_to?(:logger_provider) && OpenTelemetry.logger_provider.is_a?(OpenTelemetry::SDK::Logs::LoggerProvider) end end From 23afe799aa8b1e476c2bb3c05f7c9e77d93c0bb1 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 18 Jun 2026 13:12:32 -0500 Subject: [PATCH 09/18] ci(ruby): add Rails 7.0 legacy e2e job as an old-Rails regression guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds e2e-rails-legacy to ruby-plugin.yml, running e2e/ruby/rails/demo-rails70 (Rails 7.0) under bundle exec rake. The existing e2e jobs run Rails 7.2 — above the floor the OTel Rails-family instrumentations enforce — so they cannot catch an old-Rails compatibility break. This job fails if Rails-family auto-instrumentation stops attaching on Rails 7.0. Demonstrated red-then-green with the job's exact command: - pre-fix gemspec (instrumentation-all -> instrumentation-rails 0.42.0): 5 runs, 9 assertions, 4 failures (ActionPack not installed, no server span) - fixed gemspec (instrumentation-rails 0.41.0): 5 runs, 16 assertions, 0 failures --- .github/workflows/ruby-plugin.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/ruby-plugin.yml b/.github/workflows/ruby-plugin.yml index 01a3ec982e..4456ef5f56 100644 --- a/.github/workflows/ruby-plugin.yml +++ b/.github/workflows/ruby-plugin.yml @@ -96,3 +96,32 @@ jobs: run: bundle install - name: Test run: bundle exec rake + + e2e-rails-legacy: + # Regression guard for the CardFlight failure. The other e2e apps run on + # Rails 7.2, which is above the floor the OTel Rails-family instrumentations + # enforce, so they can never catch an old-Rails compatibility break. This + # app pins Rails 7.0 (on current Ruby, so bundler still resolves the latest + # instrumentation gems) and fails if the Rails-family auto-instrumentation + # stops attaching — i.e. if a future dependency bump re-breaks Rails 7.0. + # See e2e/ruby/rails/demo-rails70/README.md. + name: Rails 7.0 (legacy) E2E Tests + runs-on: ubuntu-22.04-8core-32gb + defaults: + run: + working-directory: ./e2e/ruby/rails/demo-rails70 + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Ruby + uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1 + with: + ruby-version: '3.3' + # See the note above: install non-frozen to absorb release-please version bumps. + bundler-cache: false + working-directory: ./e2e/ruby/rails/demo-rails70 + - name: Install dependencies + run: bundle install + - name: Test + run: bundle exec rake From 2fe07bafdcfc44bb29a1496758fdec4c930d8508 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 18 Jun 2026 13:14:40 -0500 Subject: [PATCH 10/18] docs(ruby): document old-Rails compatibility + repro app - README: replace the opentelemetry-instrumentation-all dependency note with a 'Ruby & Rails compatibility' section (Ruby >= 3.0, Rails >= 7.0 matrix, the individual-gem rationale, the single-warning behavior, and the per-gem pin escape hatch). Update the auto-instrumentation list to the curated set. - CHANGELOG: add an Unreleased Bug Fixes entry for the Rails 7.0 fix. - demo-rails70/README: explain what the repro proves and how to run it. --- e2e/ruby/rails/demo-rails70/README.md | 55 ++++++++++++++----- .../observability-ruby/CHANGELOG.md | 6 ++ .../observability-ruby/README.md | 43 +++++++++++++-- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/e2e/ruby/rails/demo-rails70/README.md b/e2e/ruby/rails/demo-rails70/README.md index 28feefee53..fa45d7ed0d 100644 --- a/e2e/ruby/rails/demo-rails70/README.md +++ b/e2e/ruby/rails/demo-rails70/README.md @@ -1,24 +1,53 @@ -# README +# demo-rails70 — Rails 7.0 observability regression repro -This README would normally document whatever steps are necessary to get the -application up and running. +A Rails **7.0** application that pins the LaunchDarkly observability plugin to a +known-broken scenario and guards against it regressing. It is a copy of +[`../demo`](../demo) (Rails 7.2) with Rails pinned to `~> 7.0`. -Things you may want to cover: +## Why this exists -- Ruby version +CardFlight runs Rails 7.0. The plugin used to depend on +`opentelemetry-instrumentation-all`, whose Rails-family members raised their +minimum to **Rails 7.1**. On Rails 7.0 every Rails-family instrumentation failed +its runtime `compatible?` check, logged a flurry of `"... failed to install"` +warnings, and never attached — so the app lost all Rails auto-instrumentation +(no HTTP server spans, DB spans, etc.) and got only manual instrumentation. -- System dependencies +The other e2e apps run Rails 7.2, which is **above** that floor, so they can +never catch this class of break. This app reproduces it on Rails 7.0. -- Configuration +Ruby stays at **3.3.4** on purpose: on a current Ruby, Bundler still resolves the +*latest* instrumentation gems, faithfully matching a real Rails 7.0 customer. (On +Ruby < 3.2, Bundler would self-heal to an older instrumentation set via +`required_ruby_version` and the bug would not reproduce.) -- Database creation +## How the telemetry assertion works -- Database initialization +The OTLP exporter is pointed at a small **in-process OTLP sink** +([`test/support/otlp_sink.rb`](test/support/otlp_sink.rb)) via +`OTEL_EXPORTER_OTLP_ENDPOINT`. The sink decodes the real OTLP protobuf using the +proto classes shipped with the exporter gems, so the test asserts that traces (an +auto-instrumented HTTP **server** span), a log record, and a captured exception +are actually **exported over the wire** — in pure Ruby, no Docker or Node, under +`bundle exec rake`. -- How to run the test suite +`test/integration/otlp_export_e2e_test.rb` is the headline test: -- Services (job queues, cache servers, search engines, etc.) +- **Before the fix** (instrumentation-all → instrumentation-rails 0.42): no server + span is produced or exported — the test fails. +- **After the fix** (instrumentation-rails pinned to 0.41): the server span, + log, and exception all reach the sink — the test passes. -- Deployment instructions +## Running it -- ... +```bash +cd e2e/ruby/rails/demo-rails70 +bundle install +bundle exec rake # runs the test suite (this is what CI runs) +``` + +Requires Ruby 3.3.x. The suite needs no external services: the OTLP sink runs +in-process and a dummy `LAUNCHDARKLY_SDK_KEY` is set by `test/test_helper.rb`. + +In CI this app runs as the `e2e-rails-legacy` job in +[`.github/workflows/ruby-plugin.yml`](../../../../.github/workflows/ruby-plugin.yml). diff --git a/sdk/@launchdarkly/observability-ruby/CHANGELOG.md b/sdk/@launchdarkly/observability-ruby/CHANGELOG.md index f488a231e3..115faa5f29 100644 --- a/sdk/@launchdarkly/observability-ruby/CHANGELOG.md +++ b/sdk/@launchdarkly/observability-ruby/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Bug Fixes + +* **ruby:** keep OpenTelemetry auto-instrumentation working on Rails 7.0. The plugin now depends on individual `opentelemetry-instrumentation-*` gems instead of `opentelemetry-instrumentation-all`, pinning the Rails-family instrumentations below the releases that require Rails 7.1 (so Rails 7.0 keeps working) while every other instrumentation tracks the latest. Instrumentations that cannot attach now produce a single actionable warning instead of a flurry of "failed to install" lines. + ## [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) diff --git a/sdk/@launchdarkly/observability-ruby/README.md b/sdk/@launchdarkly/observability-ruby/README.md index 3602ba3e80..a16574d38d 100644 --- a/sdk/@launchdarkly/observability-ruby/README.md +++ b/sdk/@launchdarkly/observability-ruby/README.md @@ -38,13 +38,43 @@ The gem includes everything needed for traces and logs out of the box: - `launchdarkly-server-sdk` >= 8.11.0 (plugin support was added in 8.11.0) - `opentelemetry-sdk` ~> 1.4 - `opentelemetry-exporter-otlp` ~> 0.28 -- `opentelemetry-instrumentation-all` ~> 0.62 - `opentelemetry-logs-sdk` ~> 0.1 - `opentelemetry-exporter-otlp-logs` ~> 0.1 +- A curated set of individual `opentelemetry-instrumentation-*` gems (see [Ruby & Rails compatibility](#ruby--rails-compatibility)) For metrics support (optional): - `opentelemetry-metrics-sdk` ~> 0.1 +### Ruby & Rails compatibility + +| Component | Supported | +|-----------|-----------| +| Ruby | >= 3.0 | +| Rails | >= 7.0 (auto-instrumentation); Rack / Sinatra / other Rack apps are unaffected by Rails version | + +The plugin depends on **individual** `opentelemetry-instrumentation-*` gems rather +than the `opentelemetry-instrumentation-all` meta-gem. The meta-gem couples every +instrumentation to a single version, so when the Rails-family instrumentations +raised their minimum to Rails 7.1, Rails 7.0 apps silently lost *all* +auto-instrumentation. Listing gems individually keeps the Rails family +(`opentelemetry-instrumentation-rails`, `-action_pack`, `-active_record`, +`-active_support`, `-action_view`, `-active_job`, `-action_mailer`, +`-active_storage`) pinned below the releases that require Rails 7.1, while every +other instrumentation tracks the latest version. Those pinned releases are still +compatible with Rails 7.1+, so modern apps are unaffected. + +If an instrumentation cannot attach on your framework version, the plugin emits a +**single actionable warning** (instead of a flurry of "failed to install" lines) +and keeps everything else working — flag-eval spans, manual instrumentation, logs, +and error capture are unaffected. You can add any other +`opentelemetry-instrumentation-*` gem to your own Gemfile and it is picked up +automatically (the plugin activates every instrumentation that is loaded), or pin +one to a framework-compatible release, e.g.: + +```ruby +gem 'opentelemetry-instrumentation-rails', '~> 0.41' +``` + ## Quick Start ### Basic Usage (Non-Rails) @@ -319,12 +349,17 @@ This generates: By default, the plugin enables OpenTelemetry auto-instrumentation for common Ruby libraries: -- **Rails**: Request tracing, route recognition -- **ActiveRecord**: Database query tracing -- **Net::HTTP**: Outbound HTTP request tracing +- **Rails** (and the Action/Active family): Request tracing, route recognition, DB queries, view rendering, jobs +- **Net::HTTP**, **HTTP**, **Faraday**: Outbound HTTP request tracing - **Rack**: Request/response tracing +- **PG**, **MySQL2**: Database query tracing - **Redis**: Cache operation tracing - **Sidekiq**: Background job tracing +- **GraphQL**: Query/field tracing +- **Sinatra**: Request tracing + +Need another library instrumented? Add its `opentelemetry-instrumentation-*` gem +to your Gemfile — the plugin activates every loaded instrumentation automatically. ### Customizing Instrumentations From e7184ae53a9fa8fab73b58fb553c69cde86dbd85 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 18 Jun 2026 13:16:54 -0500 Subject: [PATCH 11/18] style(ruby): alphabetize gemspec dependencies (Gemspec/OrderedDependencies) rubocop's Gemspec/OrderedDependencies (run by the build CI job) requires alphabetical order within each section. No functional change. --- .../observability-ruby/launchdarkly-observability.gemspec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec b/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec index 47a5e2ee32..1a3533fdab 100644 --- a/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec +++ b/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec @@ -52,14 +52,14 @@ Gem::Specification.new do |spec| # must be capped below its enforcing version to keep attaching on Rails 7.0. # These releases are still compatible with Rails 7.1+, so modern apps are # unaffected. Revisit when the plugin drops Rails 7.0 support. - spec.add_dependency 'opentelemetry-instrumentation-rails', '>= 0.34', '< 0.42' + spec.add_dependency 'opentelemetry-instrumentation-action_mailer', '< 0.8' spec.add_dependency 'opentelemetry-instrumentation-action_pack', '< 0.18' spec.add_dependency 'opentelemetry-instrumentation-action_view', '< 0.13' - spec.add_dependency 'opentelemetry-instrumentation-active_record', '< 0.13' - spec.add_dependency 'opentelemetry-instrumentation-active_support', '< 0.12' spec.add_dependency 'opentelemetry-instrumentation-active_job', '< 0.12' - spec.add_dependency 'opentelemetry-instrumentation-action_mailer', '< 0.8' + spec.add_dependency 'opentelemetry-instrumentation-active_record', '< 0.13' spec.add_dependency 'opentelemetry-instrumentation-active_storage', '< 0.5' + spec.add_dependency 'opentelemetry-instrumentation-active_support', '< 0.12' + spec.add_dependency 'opentelemetry-instrumentation-rails', '>= 0.34', '< 0.42' # Non-Rails instrumentations: latest. Consumers can add any other # opentelemetry-instrumentation-* gem to their Gemfile and it is picked up From aaf9f8dac5fffc863710dd320be0ebf6355f0f79 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Tue, 23 Jun 2026 14:32:07 -0500 Subject: [PATCH 12/18] chore(ruby): bump demo-rails70 lockfile to observability 0.2.2 (rebased onto main) --- e2e/ruby/rails/demo-rails70/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/ruby/rails/demo-rails70/Gemfile.lock b/e2e/ruby/rails/demo-rails70/Gemfile.lock index 3b1ce4551d..dc84228d35 100644 --- a/e2e/ruby/rails/demo-rails70/Gemfile.lock +++ b/e2e/ruby/rails/demo-rails70/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: ../../../../sdk/@launchdarkly/observability-ruby specs: - launchdarkly-observability (0.2.1) + launchdarkly-observability (0.2.2) launchdarkly-server-sdk (>= 8.11.0) opentelemetry-exporter-otlp (~> 0.28) opentelemetry-exporter-otlp-logs (~> 0.1) From 6757802249117d44f0fafdb5050494b8241ec4a2 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 25 Jun 2026 13:40:11 -0500 Subject: [PATCH 13/18] fix(ruby): keep trace resource consistent with logs/metrics in lazy-init When auto-instrumentation installs during Rails boot (the LD_LAZY_INIT path), the tracer provider's resource is built before the plugin's service_name/service_version options exist, so it carries the inferred service name. configure_traces then only attached an exporter and left that resource in place, while logs and metrics built a fresh resource from the options -- so trace spans reported a different service identity than logs/metrics. Update the existing provider's resource in place (it reads @resource live per span) instead of reconfiguring the SDK, which would drop the boot-time instrumentation. Flagged by Cursor Bugbot on #643. --- .../opentelemetry_config.rb | 21 +++++++++--- .../test/opentelemetry_config_test.rb | 33 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb index 3e96b8f659..5c8415d168 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb @@ -109,13 +109,24 @@ def shutdown # # If auto-instrumentation was already installed during Rails boot (see # LaunchDarklyObservability.install_rails_instrumentation), the SDK tracer - # provider already exists with instrumentation attached — so we only add the - # OTLP span exporter. Re-running OpenTelemetry::SDK.configure here would - # replace that provider and, in the lazy-init case, drop the Rails-family - # instrumentation that can only attach during boot. + # provider already exists with instrumentation attached — so we add the OTLP + # span exporter and refresh its resource, rather than re-running + # OpenTelemetry::SDK.configure (which would replace the provider and, in the + # lazy-init case, drop the Rails-family instrumentation that can only attach + # during boot). def configure_traces if LaunchDarklyObservability.instrumentation_installed_at_boot? - OpenTelemetry.tracer_provider.add_span_processor(create_batch_span_processor) + provider = OpenTelemetry.tracer_provider + provider.add_span_processor(create_batch_span_processor) + + # The boot-time install built the provider's resource before this plugin + # (and its service_name/service_version options) existed, so it carries + # the inferred service name. configure_logs/configure_metrics below build + # a fresh resource from those options; without updating the trace + # resource too, spans would report a different service identity than logs + # and metrics. OTel exposes no resource setter, but the provider reads + # @resource live when creating each span — so update it in place. + provider.instance_variable_set(:@resource, create_resource) return end diff --git a/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb b/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb index 5336aa5483..180607a5d9 100644 --- a/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb +++ b/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb @@ -140,6 +140,39 @@ def test_resource_includes_service_version_when_provided assert_equal '2.0.0', attrs['service.version'] end + # Regression: in the lazy-init path the Railtie installs instrumentation at + # boot with no plugin options, so the trace provider's resource carries the + # inferred service name. When the client is later created with service_name, + # configure must update the existing trace resource too — otherwise spans keep + # the boot identity while logs/metrics use the configured one (mismatch across + # signals). See configure_traces' boot-installed branch. + def test_boot_installed_traces_resource_picks_up_configured_service_name + # Simulate boot-time install: a resource with the inferred/boot service name. + boot = LaunchDarklyObservability::OpenTelemetryConfig.new( + project_id: @project_id, + otlp_endpoint: @otlp_endpoint, + service_name: 'boot-inferred-service' + ) + boot.install_instrumentation_only + LaunchDarklyObservability.instance_variable_set(:@instrumentation_installed_at_boot, true) + assert_equal 'boot-inferred-service', resource_attributes['service.name'] + + # Later: the client is created and the plugin configures with its own name. + config = LaunchDarklyObservability::OpenTelemetryConfig.new( + project_id: @project_id, + otlp_endpoint: @otlp_endpoint, + service_name: 'lazy-configured-service', + enable_logs: false, + enable_metrics: false + ) + config.configure + + # Trace spans now report the configured identity, matching logs/metrics. + assert_equal 'lazy-configured-service', resource_attributes['service.name'] + ensure + LaunchDarklyObservability.reset_instrumentation_state! + end + def test_flush_does_not_raise config = LaunchDarklyObservability::OpenTelemetryConfig.new( project_id: @project_id, From ef2f79795467183999352f6dd113445b558c174e Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 25 Jun 2026 13:40:12 -0500 Subject: [PATCH 14/18] test(ruby): cover the instrumentation install-log failure-capture path FAILED_PATTERN capture and the single summary warning only fire when an instrumentation is below its framework floor (the pre-fix red state), so the green e2e suite never exercises them. Add direct unit coverage. --- .../test/instrumentation_log_filter_test.rb | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 sdk/@launchdarkly/observability-ruby/test/instrumentation_log_filter_test.rb diff --git a/sdk/@launchdarkly/observability-ruby/test/instrumentation_log_filter_test.rb b/sdk/@launchdarkly/observability-ruby/test/instrumentation_log_filter_test.rb new file mode 100644 index 0000000000..8e294701a1 --- /dev/null +++ b/sdk/@launchdarkly/observability-ruby/test/instrumentation_log_filter_test.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Unit tests for the install-log filter. The failure-capture path +# (FAILED_PATTERN + the single summary warning) only fires in the pre-fix red +# state — a Rails version below an instrumentation's floor — so the green e2e +# suite never exercises it. These tests cover it directly. +class InstrumentationLogFilterTest < Minitest::Test + Filter = LaunchDarklyObservability::InstrumentationLogFilter + + # Records what the real logger was asked to emit, so we can assert which + # messages the filter forwarded vs suppressed. + class FakeLogger + attr_reader :messages + + def initialize + @messages = [] + end + + %i[debug info warn error fatal unknown].each do |level| + define_method(level) { |message = nil, &block| @messages << [level, message || block&.call] } + end + + def add(_severity, message = nil, progname = nil, &block) + @messages << [:add, message || progname || block&.call] + end + + # Used to prove unknown methods are delegated. + def level + :delegated_level + end + end + + def test_capture_failures_restores_the_original_logger + original = OpenTelemetry.logger + Filter.capture_failures { OpenTelemetry.logger.info('noop') } + assert_same original, OpenTelemetry.logger + end + + def test_capture_failures_restores_logger_even_when_block_raises + original = OpenTelemetry.logger + assert_raises(RuntimeError) do + Filter.capture_failures { raise 'boom' } + end + assert_same original, OpenTelemetry.logger + end + + def test_capture_failures_collects_failed_instrumentation_names + failed = Filter.capture_failures do + OpenTelemetry.logger.warn('Instrumentation: OpenTelemetry::Instrumentation::ActionView failed to install') + OpenTelemetry.logger.warn('Instrumentation: OpenTelemetry::Instrumentation::ActiveRecord failed to install') + OpenTelemetry.logger.info('OpenTelemetry::Instrumentation::Rack was successfully installed') + OpenTelemetry.logger.warn('an unrelated warning') + end + + assert_equal %w[OpenTelemetry::Instrumentation::ActionView OpenTelemetry::Instrumentation::ActiveRecord], failed + end + + def test_suppresses_install_chatter_but_forwards_real_messages + fake = FakeLogger.new + failed = [] + filter = Filter.new(fake, failed) + + filter.warn('Instrumentation: OpenTelemetry::Instrumentation::ActionView failed to install') + filter.info('OpenTelemetry::Instrumentation::Rack was successfully installed') + filter.warn('a real warning') + + # Only the non-chatter message reaches the delegate. + assert_equal [[:warn, 'a real warning']], fake.messages + # ...and the failure was recorded as a side effect. + assert_equal ['OpenTelemetry::Instrumentation::ActionView'], failed + end + + def test_add_path_is_filtered_too + fake = FakeLogger.new + failed = [] + filter = Filter.new(fake, failed) + + filter.add(Logger::WARN, 'Instrumentation: OpenTelemetry::Instrumentation::ActiveJob failed to install') + filter.add(Logger::INFO, 'something worth keeping') + + assert_equal [[:add, 'something worth keeping']], fake.messages + assert_equal ['OpenTelemetry::Instrumentation::ActiveJob'], failed + end + + def test_block_form_messages_are_filtered + fake = FakeLogger.new + failed = [] + filter = Filter.new(fake, failed) + + filter.warn { 'Instrumentation: OpenTelemetry::Instrumentation::ActionMailer failed to install' } + + assert_empty fake.messages + assert_equal ['OpenTelemetry::Instrumentation::ActionMailer'], failed + end + + def test_delegates_unknown_methods + fake = FakeLogger.new + filter = Filter.new(fake, []) + + assert_equal :delegated_level, filter.level + assert_respond_to filter, :level + end + + def test_failure_warning_is_a_single_summary_with_unique_stripped_names + warning = Filter.failure_warning( + %w[ + OpenTelemetry::Instrumentation::ActionView + OpenTelemetry::Instrumentation::ActionView + OpenTelemetry::Instrumentation::ActiveRecord + ] + ) + + assert_includes warning, '2 OpenTelemetry instrumentation(s) could not attach' + assert_includes warning, 'ActionView, ActiveRecord' + # The verbose OTel namespace prefix is stripped from the user-facing summary. + refute_includes warning, 'OpenTelemetry::Instrumentation::' + end +end From 564e2f7fb852b267f1938fac3e3ad5ea41c4ee7d Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 25 Jun 2026 13:40:12 -0500 Subject: [PATCH 15/18] docs(ruby): list the full Rails-family set in instrumentations.rb Requiring opentelemetry/instrumentation/rails pulls all seven Rails-family instrumentations (incl. action_mailer and active_storage); spell them out so the comment matches the gemspec caps and explains where those caps come from. --- .../lib/launchdarkly_observability/instrumentations.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb index 919df28927..196c8b7934 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb @@ -15,8 +15,12 @@ # loaded, so any additional `opentelemetry-instrumentation-*` gem a consumer adds # to their own Gemfile is picked up automatically alongside these defaults. -# Rails family. Requiring this pulls action_pack, active_record, active_support, -# action_view and active_job. +# Rails family. Requiring this pulls every Rails-family instrumentation — +# action_mailer, action_pack, action_view, active_job, active_record, +# active_storage and active_support — which is why each is capped in the gemspec +# (the Rails 7.0 floor has to hold on all of them, not just the rails meta-gem). +# It also pulls concurrent_ruby, which tracks latest and is required explicitly +# below. require 'opentelemetry/instrumentation/rails' # Common non-Rails instrumentations (latest; see gemspec for version policy). From 3d3ca2f8fadd6527e8be9dec768fad5440352ef3 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 25 Jun 2026 13:40:12 -0500 Subject: [PATCH 16/18] chore(ruby): give demo-rails70 a distinct service_name demo and demo-rails70 both reported service_name 'rails-demo-app', so their telemetry collided. Name the Rails 7.0 repro 'rails7-demo-app' so its spans are distinguishable from the Rails 7.2 demo. --- e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb | 2 +- e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb b/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb index dc70f01581..79030bf4ba 100644 --- a/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb +++ b/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb @@ -10,7 +10,7 @@ def self.instance @instance ||= begin plugin = LaunchDarklyObservability::Plugin.new( otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT), - service_name: 'rails-demo-app', + service_name: 'rails7-demo-app', service_version: '1.0.0' ) config = LaunchDarkly::Config.new(plugins: [plugin]) diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb b/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb index e67dce87fe..3da0ec8927 100644 --- a/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb +++ b/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb @@ -8,7 +8,7 @@ unless ENV['LD_LAZY_INIT'] observability_plugin = LaunchDarklyObservability::Plugin.new( otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT), - service_name: 'rails-demo-app', + service_name: 'rails7-demo-app', service_version: '1.0.0' ) From 9da2c1a24f23c78270a4f74fca43e343249f65c8 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 25 Jun 2026 13:40:12 -0500 Subject: [PATCH 17/18] chore(ruby): sync demo Gemfile.lock to observability 0.2.2 The lockfile still pinned the path gem at 0.2.1 while the gem is 0.2.2 (and demo-rails70 was already updated). Align it. --- e2e/ruby/rails/demo/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/ruby/rails/demo/Gemfile.lock b/e2e/ruby/rails/demo/Gemfile.lock index 00b19b9028..16777afd30 100644 --- a/e2e/ruby/rails/demo/Gemfile.lock +++ b/e2e/ruby/rails/demo/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: ../../../../sdk/@launchdarkly/observability-ruby specs: - launchdarkly-observability (0.2.1) + launchdarkly-observability (0.2.2) launchdarkly-server-sdk (>= 8.11.0) opentelemetry-exporter-otlp (~> 0.28) opentelemetry-exporter-otlp-logs (~> 0.1) From 6980ff95b0c39e8b1f851b4334245441eb6054e4 Mon Sep 17 00:00:00 2001 From: ccschmitz Date: Thu, 25 Jun 2026 15:23:06 -0500 Subject: [PATCH 18/18] fix(ruby): warn when lazy-init options can't apply after boot install In the lazy-init path the Railtie installs auto-instrumentation at boot with defaults, before the plugin exists. Options that only take effect at install time -- custom instrumentations config and enable_traces: false -- can't be applied retroactively: an instrumentation patches its library at boot (via ActiveSupport.on_load hooks) and can't be reconfigured or detached afterward. Emit a single actionable warning instead of silently dropping them, matching the existing warn_if_rails_instrumentation_missed pattern. (service_name/service_version remain the exception -- the trace resource is read live per span and is refreshed in configure_traces.) Flagged by Cursor Bugbot on #643. --- .../opentelemetry_config.rb | 24 ++++++++++ .../test/opentelemetry_config_test.rb | 44 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb index 5c8415d168..575539ef56 100644 --- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb +++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb @@ -62,6 +62,8 @@ def initialize(project_id:, otlp_endpoint:, environment: nil, sdk_metadata: nil, def configure return if @configured + warn_ignored_boot_options if LaunchDarklyObservability.instrumentation_installed_at_boot? + configure_traces if @options.fetch(:enable_traces, true) configure_logs if @options.fetch(:enable_logs, true) configure_metrics if @options.fetch(:enable_metrics, true) @@ -156,6 +158,28 @@ def warn_if_rails_instrumentation_missed 'or create the LaunchDarkly client from a config/initializer.' end + # Emit a single actionable warning when the client is created lazily (so + # instrumentation was installed at boot, before this plugin existed) but was + # given options that can only take effect at install time. Custom + # `instrumentations` config and `enable_traces: false` cannot be applied + # retroactively: an OTel instrumentation patches its library during boot via + # ActiveSupport.on_load hooks and cannot be reconfigured or detached + # afterward. (service_name/service_version are the exception — the trace + # resource is refreshed in #configure_traces because it is read live per + # span.) To honor these options, create the client from a config/initializer + # so #configure runs during boot with them. + def warn_ignored_boot_options + ignored = [] + ignored << 'instrumentations' unless @options.fetch(:instrumentations, {}).empty? + ignored << 'enable_traces: false' unless @options.fetch(:enable_traces, true) + return if ignored.empty? + + warn '[LaunchDarklyObservability] Rails auto-instrumentation was installed at boot, so these ' \ + "plugin option(s) cannot be applied and will be ignored: #{ignored.join(', ')}. Instrumentations " \ + 'attach during boot, before the client exists, and cannot be reconfigured or detached afterward. ' \ + 'To apply these options, create the LaunchDarkly client from a config/initializer instead of lazily.' + end + # Configure auto-instrumentations with sensible defaults. # User-provided instrumentation config is merged on top of defaults, # so users only need to specify the instrumentations they want to override. diff --git a/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb b/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb index 180607a5d9..14b696ebaa 100644 --- a/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb +++ b/sdk/@launchdarkly/observability-ruby/test/opentelemetry_config_test.rb @@ -173,6 +173,50 @@ def test_boot_installed_traces_resource_picks_up_configured_service_name LaunchDarklyObservability.reset_instrumentation_state! end + # Install-time options (custom instrumentations config, disabling traces) can't + # be applied once instrumentation has attached at boot, so the lazy-init path + # warns instead of silently dropping them. + def test_warns_about_options_that_cannot_apply_in_boot_installed_path + LaunchDarklyObservability::OpenTelemetryConfig.new( + project_id: @project_id, + otlp_endpoint: @otlp_endpoint + ).install_instrumentation_only + LaunchDarklyObservability.instance_variable_set(:@instrumentation_installed_at_boot, true) + + config = LaunchDarklyObservability::OpenTelemetryConfig.new( + project_id: @project_id, + otlp_endpoint: @otlp_endpoint, + instrumentations: { 'OpenTelemetry::Instrumentation::Redis' => { enabled: false } }, + enable_traces: false, + enable_logs: false, + enable_metrics: false + ) + + _out, err = capture_io { config.configure } + + assert_match(/cannot be applied and will be ignored/, err) + assert_match(/instrumentations/, err) + assert_match(/enable_traces: false/, err) + ensure + LaunchDarklyObservability.reset_instrumentation_state! + end + + def test_does_not_warn_about_options_when_not_installed_at_boot + LaunchDarklyObservability.reset_instrumentation_state! + + config = LaunchDarklyObservability::OpenTelemetryConfig.new( + project_id: @project_id, + otlp_endpoint: @otlp_endpoint, + instrumentations: { 'OpenTelemetry::Instrumentation::Redis' => { enabled: false } }, + enable_logs: false, + enable_metrics: false + ) + + _out, err = capture_io { config.configure } + + refute_match(/will be ignored/, err) + end + def test_flush_does_not_raise config = LaunchDarklyObservability::OpenTelemetryConfig.new( project_id: @project_id,