Verified Commit ee176c11 authored by Hercules Merscher's avatar Hercules Merscher 🌴
Browse files

feat: Automatically enable LabKit's instrumentation

parent 6818bb30
Loading
Loading
Loading
Loading
+34 −15
Original line number Diff line number Diff line
@@ -41,7 +41,11 @@ When `GITLAB_TRACING` is set, LabKit automatically creates and configures a trac
Auto-initialization provides:
- Automatic tracer creation with connection string settings
- Service name from `service_name` query parameter (defaults to `"labkit-service"`)
- All available OpenTelemetry instrumentation enabled by default
- All available OpenTelemetry instrumentation enabled by default (`c.use_all()`)
- LabKit-specific instrumentation enabled automatically:
  - Rails components (ActiveRecord, ActionView, ActiveSupport) - if Rails is available
  - Redis instrumentation - if Redis gem is loaded
  - External HTTP instrumentation (Net::HTTP, Excon, HTTPClient)
- Safe fallback if initialization fails (no-op tracer)

### Optional Configuration
@@ -146,9 +150,8 @@ When using Rails, the `Labkit::Tracing::RackMiddleware` is automatically inserte

**How it works:**
- Detected automatically via Rails Railtie
- Inserted after `Rails::Rack::Logger` (or at available position if Rails::Rack::Logger not found)
- Inserted after `Labkit::Middleware::Rack` for proper correlation ID propagation
- Only activates when `GITLAB_TRACING` is set
- Skips insertion if middleware is already manually configured
- Logs insertion to Rails logger for visibility

**Middleware Positioning:**
@@ -306,9 +309,16 @@ This automatically:

### Rails Components

**Note:** With auto-initialization enabled (when `GITLAB_TRACING` is set), these LabKit-specific instrumentations are automatically enabled alongside OpenTelemetry's instrumentation. You only need to call these methods manually if you've disabled auto-initialization or need to control when instrumentation starts.

Both OpenTelemetry and LabKit instrumentation run side-by-side, providing complementary trace data:
- **OpenTelemetry Rails instrumentation**: Provides standardized, method-level traces using semantic conventions
- **LabKit instrumentation**: Adds GitLab-specific details like SQL fingerprints, sanitized queries, and enhanced metadata

#### ActiveRecord (Database Queries)

```ruby
# Only needed if auto-initialization is disabled or you need manual control
unsubscribe = Labkit::Tracing::Rails::ActiveRecord::Subscriber.instrument

# Later, to stop instrumentation:
@@ -317,13 +327,14 @@ unsubscribe.call

Traces:
- SQL queries with sanitized statements
- Query fingerprints
- Query fingerprints (via PgQuery)
- Connection IDs
- Cached query indicators

#### ActionView (Template Rendering)

```ruby
# Only needed if auto-initialization is disabled or you need manual control
unsubscribe = Labkit::Tracing::Rails::ActionView::Subscriber.instrument

# Later, to stop instrumentation:
@@ -339,6 +350,7 @@ Traces:
#### ActiveSupport (Caching)

```ruby
# Only needed if auto-initialization is disabled or you need manual control
unsubscribe = Labkit::Tracing::Rails::ActiveSupport::Subscriber.instrument

# Later, to stop instrumentation:
@@ -354,9 +366,10 @@ Traces:

### Redis

Instrument Redis operations:
**Note:** Redis instrumentation is automatically enabled when `GITLAB_TRACING` is set and the Redis gem is loaded.

```ruby
# Only needed if auto-initialization is disabled or you need manual control
Labkit::Tracing::Redis.instrument
```

@@ -395,9 +408,12 @@ Traces incoming gRPC calls with:

### External HTTP Requests

**Note:** External HTTP instrumentation is automatically enabled when `GITLAB_TRACING` is set.

Instrument outgoing HTTP requests made by Net::HTTP, Excon, and HTTPClient:

```ruby
# Only needed if auto-initialization is disabled or you need manual control
unsubscribe = Labkit::Tracing::ExternalHttp.instrument

# Later, to stop instrumentation:
@@ -566,26 +582,29 @@ Note: Middleware is still automatically inserted even with custom tracer initial

If you want to control exactly which components are instrumented instead of using `c.use_all()`, override tracer initialization:

**Important:** When you manually call `Factory.create_tracer`, auto-initialization still runs but is replaced by your manual configuration. The LabKit-specific instrumentation (Rails, Redis, ExternalHttp) is still automatically enabled unless you explicitly disable auto-initialization.

```ruby
# config/initializers/tracing.rb
# Override auto-initialization for selective instrumentation
# Override auto-initialization for selective OpenTelemetry instrumentation
Labkit::Tracing::Factory.create_tracer("my-rails-app", ENV["GITLAB_TRACING"]) do |c|
  # Selective OpenTelemetry instrumentation (instead of c.use_all())
  c.use 'OpenTelemetry::Instrumentation::Rails'
  c.use 'OpenTelemetry::Instrumentation::Sidekiq'
end

# Additional LabKit-specific instrumentation
Rails.application.config.after_initialize do
  Labkit::Tracing::Rails::ActiveRecord::Subscriber.instrument
  Labkit::Tracing::Rails::ActionView::Subscriber.instrument
  Labkit::Tracing::Rails::ActiveSupport::Subscriber.instrument
  Labkit::Tracing::ExternalHttp.instrument
  Labkit::Tracing::Redis.instrument
end
# LabKit instrumentation is already enabled automatically during auto-initialization.
# Only call these manually if you need to control timing or disable auto-initialization:
# Rails.application.config.after_initialize do
#   Labkit::Tracing::Rails::ActiveRecord::Subscriber.instrument
#   Labkit::Tracing::Rails::ActionView::Subscriber.instrument
#   Labkit::Tracing::Rails::ActiveSupport::Subscriber.instrument
#   Labkit::Tracing::ExternalHttp.instrument
#   Labkit::Tracing::Redis.instrument
# end
```

Note: Middleware is still automatically inserted - no need to configure it manually.
Note: Both middleware insertion and LabKit instrumentation are automatically handled even with manual tracer initialization.

## Troubleshooting

+13 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

require "cgi"
require "active_support/core_ext/object/blank"

module Labkit
  module Tracing
@@ -27,9 +28,21 @@ module Labkit
          require "opentelemetry/instrumentation/all"
          c.use_all
        end

        enable_labkit_instrumentation
      rescue StandardError => e
        warn "Labkit::Tracing auto-initialization failed: #{e.message}"
      end

      def self.enable_labkit_instrumentation
        Rails::ActiveRecord::Subscriber.instrument if defined?(::ActiveRecord)
        Rails::ActionView::Subscriber.instrument if defined?(::ActionView)
        Rails::ActiveSupport::Subscriber.instrument if defined?(::ActiveSupport)
        Redis.instrument if defined?(::Redis)
        ExternalHttp.instrument
      rescue StandardError => e
        warn "Labkit::Tracing: LabKit instrumentation setup failed: #{e.message}"
      end
    end
  end
end
+3 −11
Original line number Diff line number Diff line
@@ -6,17 +6,9 @@ module Labkit
      initializer "labkit.tracing.insert_middleware", after: :load_config_initializers do |app|
        next unless Labkit::Tracing.enabled?

        next if app.middleware.include?(Labkit::Tracing::RackMiddleware)

        begin
          # Try to insert after Rails::Rack::Logger, fall back to simple use if not found
          app.middleware.insert_after ::Rails::Rack::Logger, Labkit::Tracing::RackMiddleware
          ::Rails.logger.info "Labkit::Tracing::RackMiddleware automatically inserted after Rails::Rack::Logger"
        rescue ArgumentError
          # Rails::Rack::Logger not found in stack - add middleware at the current position
          app.middleware.use Labkit::Tracing::RackMiddleware
          ::Rails.logger.info "Labkit::Tracing::RackMiddleware automatically inserted"
        end
        # Insert after Labkit::Middleware::Rack for proper correlation ID propagation.
        app.middleware.insert_after Labkit::Middleware::Rack, Labkit::Tracing::RackMiddleware
        ::Rails.logger.info "Labkit::Tracing::RackMiddleware automatically inserted after Labkit::Middleware::Rack"
      end
    end
  end
+86 −0
Original line number Diff line number Diff line
@@ -46,6 +46,11 @@ describe Labkit::Tracing::AutoInitialize do

    before do
      allow(Labkit::Tracing::Factory).to receive(:create_tracer)
      allow(Labkit::Tracing::Rails::ActiveRecord::Subscriber).to receive(:instrument)
      allow(Labkit::Tracing::Rails::ActionView::Subscriber).to receive(:instrument)
      allow(Labkit::Tracing::Rails::ActiveSupport::Subscriber).to receive(:instrument)
      allow(Labkit::Tracing::ExternalHttp).to receive(:instrument)
      allow(Labkit::Tracing::Redis).to receive(:instrument)
    end

    context "when GITLAB_TRACING is not set" do
@@ -111,6 +116,21 @@ describe Labkit::Tracing::AutoInitialize do

        described_class.initialize!
      end

      it "enables all labkit instrumentation" do
        stub_const("ActiveRecord", Module.new)
        stub_const("ActionView", Module.new)
        stub_const("ActiveSupport", Module.new)
        stub_const("Redis", Module.new)

        described_class.initialize!

        expect(Labkit::Tracing::Rails::ActiveRecord::Subscriber).to have_received(:instrument).once
        expect(Labkit::Tracing::Rails::ActionView::Subscriber).to have_received(:instrument).once
        expect(Labkit::Tracing::Rails::ActiveSupport::Subscriber).to have_received(:instrument).once
        expect(Labkit::Tracing::ExternalHttp).to have_received(:instrument).once
        expect(Labkit::Tracing::Redis).to have_received(:instrument).once
      end
    end

    context "when Factory.create_tracer raises an error" do
@@ -124,4 +144,70 @@ describe Labkit::Tracing::AutoInitialize do
      end
    end
  end

  describe ".enable_labkit_instrumentation" do
    before do
      allow(Labkit::Tracing::Rails::ActiveRecord::Subscriber).to receive(:instrument)
      allow(Labkit::Tracing::Rails::ActionView::Subscriber).to receive(:instrument)
      allow(Labkit::Tracing::Rails::ActiveSupport::Subscriber).to receive(:instrument)
      allow(Labkit::Tracing::ExternalHttp).to receive(:instrument)
      allow(Labkit::Tracing::Redis).to receive(:instrument)
    end

    context "when all dependencies are available" do
      before do
        stub_const("ActiveRecord", Module.new)
        stub_const("ActionView", Module.new)
        stub_const("ActiveSupport", Module.new) unless defined?(ActiveSupport)
        stub_const("Redis", Module.new)
      end

      it "instruments all available components" do
        described_class.enable_labkit_instrumentation

        expect(Labkit::Tracing::Rails::ActiveRecord::Subscriber).to have_received(:instrument).once
        expect(Labkit::Tracing::Rails::ActionView::Subscriber).to have_received(:instrument).once
        expect(Labkit::Tracing::Rails::ActiveSupport::Subscriber).to have_received(:instrument).once
        expect(Labkit::Tracing::ExternalHttp).to have_received(:instrument).once
        expect(Labkit::Tracing::Redis).to have_received(:instrument).once
      end
    end

    context "when Rails components are not available" do
      before do
        hide_const("ActiveRecord") if defined?(ActiveRecord)
        hide_const("ActionView") if defined?(ActionView)
      end

      it "does not instrument unavailable components but instruments available ones" do
        described_class.enable_labkit_instrumentation

        expect(Labkit::Tracing::Rails::ActiveRecord::Subscriber).not_to have_received(:instrument)
        expect(Labkit::Tracing::Rails::ActionView::Subscriber).not_to have_received(:instrument)
        expect(Labkit::Tracing::ExternalHttp).to have_received(:instrument).once
      end
    end

    context "when Redis is not available" do
      before do
        hide_const("Redis") if defined?(Redis)
      end

      it "does not instrument Redis" do
        described_class.enable_labkit_instrumentation

        expect(Labkit::Tracing::Redis).not_to have_received(:instrument)
      end
    end

    context "when instrumentation raises an error" do
      before do
        allow(Labkit::Tracing::ExternalHttp).to receive(:instrument).and_raise(StandardError.new("Test error"))
      end

      it "handles the error gracefully and warns" do
        expect { described_class.enable_labkit_instrumentation }.to output(/instrumentation setup failed.*Test error/).to_stderr
      end
    end
  end
end
+8 −44
Original line number Diff line number Diff line
@@ -11,70 +11,34 @@ describe "Labkit::Tracing::Railtie" do
  end

  let(:railtie_class) { Labkit::Tracing::Railtie }
  let(:app) { instance_double("Rails::Application", middleware: middleware_stack) } # rubocop:disable RSpec/VerifiedDoubleReference
  let(:middleware_stack) { instance_double("ActionDispatch::MiddlewareStack") } # rubocop:disable RSpec/VerifiedDoubleReference
  let(:rails_logger) { instance_double("Logger") } # rubocop:disable RSpec/VerifiedDoubleReference
  let(:app) { instance_double(Rails::Application, middleware: middleware_stack) }
  let(:middleware_stack) { instance_double(Rails::Configuration::MiddlewareStackProxy) }
  let(:rails_logger) { instance_double(Logger) }
  let(:tracing_enabled) { true }
  let(:already_included) { false }

  before do
    allow(Labkit::Tracing).to receive(:enabled?).and_return(tracing_enabled)
    allow(middleware_stack).to receive(:include?).and_return(already_included)
    allow(Rails).to receive(:logger).and_return(rails_logger)
    allow(rails_logger).to receive(:info)
    allow(rails_logger).to receive(:debug)
  end

  describe "middleware insertion" do
    context "when tracing is enabled" do
      let(:tracing_enabled) { true }

      context "when middleware is not already included" do
        let(:already_included) { false }

        it "inserts middleware after Rails::Rack::Logger" do
      it "inserts middleware after Labkit::Middleware::Rack" do
        expect(middleware_stack).to receive(:insert_after)
            .with(Rails::Rack::Logger, Labkit::Tracing::RackMiddleware)
          expect(rails_logger).to receive(:info).with("Labkit::Tracing::RackMiddleware automatically inserted after Rails::Rack::Logger")

          # Simulate Rails initializer running
          railtie_class.initializers.first.run(app)
        end

        context "when Rails::Rack::Logger is not found" do
          before do
            allow(middleware_stack).to receive(:insert_after).and_raise(ArgumentError.new("not found"))
          end

          it "falls back to using middleware" do
            expect(middleware_stack).to receive(:use).with(Labkit::Tracing::RackMiddleware)
            expect(rails_logger).to receive(:info).with("Labkit::Tracing::RackMiddleware automatically inserted")

            railtie_class.initializers.first.run(app)
          end
        end
      end

      context "when middleware is already included" do
        let(:already_included) { true }

        it "does not insert middleware again" do
          expect(middleware_stack).not_to receive(:insert_after)
          expect(middleware_stack).not_to receive(:use)

          railtie_class.initializers.first.run(app)
        end

        it "does not log anything" do
          expect(rails_logger).not_to receive(:info)
          .with(Labkit::Middleware::Rack, Labkit::Tracing::RackMiddleware)
        expect(rails_logger).to receive(:info)
          .with("Labkit::Tracing::RackMiddleware automatically inserted after Labkit::Middleware::Rack")

        railtie_class.initializers.first.run(app)
      end
    end
    end

    context "when tracing is disabled" do
      let(:tracing_enabled) { false }
      let(:already_included) { false }

      it "does not insert middleware" do
        expect(middleware_stack).not_to receive(:insert_after)