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

feat: OTel console exporter

parent 6c442d90
Loading
Loading
Loading
Loading
+78 −10
Original line number Diff line number Diff line
@@ -40,6 +40,9 @@ Example:
# OpenTelemetry with HTTP endpoint (Recommended)
export GITLAB_TRACING="otlp://localhost:4318"

# Console exporter for development/testing (outputs to stdout)
export GITLAB_TRACING="otlp://console"

# Jaeger with UDP endpoint (OpenTracing - Deprecated)
export GITLAB_TRACING="opentracing://jaeger?udp_endpoint=localhost:6831"

@@ -84,13 +87,52 @@ export GITLAB_TRACING_INCLUDE_STACKTRACE="redis,active_record"

## OTLP Configuration Options

<!-- TODO: Add detailed OTLP configuration options similar to Jaeger section below.
     Include examples for:
     - HTTP vs gRPC endpoints
     - Sampling configuration
     - Custom paths and authentication
     - Protocol override parameter
-->
When using OpenTelemetry (OTLP) as the tracing backend, the following connection string formats and query parameters are supported:

### Endpoints

- **HTTP Endpoint** - OTLP HTTP collector endpoint (default port: 4318)
  - Example: `otlp://localhost:4318`
  - Example with custom path: `otlp://localhost:4318/v1/traces`
  - Example with authentication: `otlp://user:password@collector.example.com:4318`

- **gRPC Endpoint** - OTLP gRPC collector endpoint (port 4317)
  - Example: `otlp://localhost:4317`
  - Note: Ruby SDK lacks native gRPC exporter, falls back to HTTP with protobuf encoding

- **Console Exporter** - Output spans to stdout (development/testing only)
  - Example: `otlp://console`
  - **Use Case**: Local development, debugging, automated testing
  - **Behavior**: Outputs spans immediately to stdout with no remote export
  - **Benefits**: No external collector required, instant feedback, simple debugging

### Sampling

- **`sampler`** - Sampling strategy (`probabilistic` or `const`)
  - `probabilistic` - Sample a percentage of traces (default: 0.1%)
  - `const` - Sample all traces (when `sampler_param=1`) or none (when `sampler_param=0`)

- **`sampler_param`** - Parameter for the sampler
  - For `probabilistic`: rate between 0.0 and 1.0 (e.g., `0.1` = 10%)
  - For `const`: `1` (sample all) or `0` (sample none)

- **`service_name`** - Override the service name for this tracer

### Examples

```bash
# Development: Console output with full sampling
export GITLAB_TRACING="otlp://console?sampler=const&sampler_param=1"

# Staging: HTTP endpoint with probabilistic sampling (1%)
export GITLAB_TRACING="otlp://collector.staging.example.com:4318?sampler=probabilistic&sampler_param=0.01"

# Production: HTTP endpoint with custom path and authentication
export GITLAB_TRACING="otlp://user:pass@collector.prod.example.com:4318/v1/traces?sampler=probabilistic&sampler_param=0.001"

# Testing: Console output with no sampling (useful for test verification)
export GITLAB_TRACING="otlp://console?sampler=const&sampler_param=0"
```

## Jaeger Configuration Options

@@ -512,7 +554,33 @@ That's it! The migration is transparent thanks to Labkit's protocol-agnostic API

## Example: Complete Setup

### With Automatic Instrumentation (Recommended for OpenTelemetry)
### Development Environment with Console Exporter

```ruby
# config/initializers/tracing.rb
if Labkit::Tracing.enabled?
  Labkit::Tracing::Factory.create_tracer("my-rails-app", ENV["GITLAB_TRACING"])
end

# config/application.rb
module MyApp
  class Application < Rails::Application
    # Add Rack middleware for HTTP request tracing
    config.middleware.insert_after Rails::Rack::Logger, Labkit::Tracing::RackMiddleware
  end
end

# .env.development
# GITLAB_TRACING="otlp://console?sampler=const&sampler_param=1"
```

This setup outputs all trace spans directly to your development console/logs, making it easy to:
- Debug request flows without external tools
- Verify instrumentation is working correctly
- Test tracing configuration changes
- Develop and debug trace-dependent features

### Production Environment with Automatic Instrumentation

```ruby
# config/initializers/tracing.rb
@@ -541,8 +609,8 @@ module MyApp
  end
end

# .env or environment variables
# GITLAB_TRACING="otlp://localhost:4318?sampler=probabilistic&sampler_param=0.01"
# .env.production
# GITLAB_TRACING="otlp://collector.example.com:4318?sampler=probabilistic&sampler_param=0.01"
```

### With Manual Instrumentation Selection
+36 −7
Original line number Diff line number Diff line
@@ -90,9 +90,13 @@ module Labkit

            # Add span processor with our exporter from GITLAB_TRACING
            if exporter
              c.add_span_processor(
              processor = if exporter.is_a?(OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter)
                            OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
                          else
                            OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
              )
                          end

              c.add_span_processor(processor)
            end

            # Yield to user configuration block (runs last, can override/extend)
@@ -113,9 +117,13 @@ module Labkit

          # Re-add span processor with our exporter
          if exporter
            new_provider.add_span_processor(
            processor = if exporter.is_a?(OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter)
                          OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
                        else
                          OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
            )
                        end

            new_provider.add_span_processor(processor)
          end

          # Set globally
@@ -125,7 +133,15 @@ module Labkit
        def configure_manually(_service_name, base_resource, sampler, exporter)
          # Manual configuration (backward compatible)
          span_processors = []
          span_processors << OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) if exporter
          if exporter
            processor = if exporter.is_a?(OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter)
                          OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
                        else
                          OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
                        end

            span_processors << processor
          end

          tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new(
            resource: base_resource,
@@ -176,6 +192,9 @@ module Labkit
          # https://github.com/open-telemetry/opentelemetry-collector/discussions/6016
          warn "opentelemetry tracer: UDP endpoint not supported, ignoring udp_endpoint option" if udp_endpoint.present?

          # Check for console exporter (for development/testing)
          return get_console_exporter if http_endpoint&.include?("://console") || grpc_endpoint&.include?("://console")

          if http_endpoint.present?
            get_http_exporter(http_endpoint, headers)
          elsif grpc_endpoint.present?
@@ -209,14 +228,24 @@ module Labkit
          get_http_exporter(http_endpoint, headers)
        end

        def get_console_exporter
          OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter.new
        end

        def parse_otlp_connection_string(connection_string)
          parsed = URI.parse(connection_string)

          endpoint = build_otlp_endpoint(parsed)

          # Parse query parameters for additional options
          options = parse_query(parsed.query)

          # Handle console exporter (special case - no endpoint needed)
          if parsed.host == "console"
            options[:http_endpoint] = "http://console"
            return options
          end

          endpoint = build_otlp_endpoint(parsed)

          # Determine the endpoint type and set the appropriate option
          if parsed.port == 4317 || options[:protocol] == "grpc"
            options[:grpc_endpoint] = endpoint
+73 −0
Original line number Diff line number Diff line
@@ -219,6 +219,51 @@ describe Labkit::Tracing::OpenTelemetryFactory do
        expect(options[:protocol]).to eq("grpc")
      end
    end

    context "when using console exporter" do
      it_behaves_like "an opentelemetry tracer" do
        let(:tracer) { described_class.create_tracer(service_name, "otlp://console") }
      end

      it "configures tracer with console exporter" do
        tracer = described_class.create_tracer(service_name, "otlp://console")
        expect(tracer).to be_a(OpenTelemetry::Trace::Tracer)
        expect(OpenTelemetry.tracer_provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider)
      end

      it "supports sampler configuration" do
        tracer = described_class.create_tracer(service_name, "otlp://console?sampler=const&sampler_param=1")
        expect(tracer).to be_a(OpenTelemetry::Trace::Tracer)

        provider = OpenTelemetry.tracer_provider
        expect(provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider)
      end

      it "supports probabilistic sampler" do
        tracer = described_class.create_tracer(service_name, "otlp://console?sampler=probabilistic&sampler_param=0.5")
        expect(tracer).to be_a(OpenTelemetry::Trace::Tracer)
      end

      it "outputs spans to stdout" do
        tracer = described_class.create_tracer(service_name, "otlp://console?sampler=const&sampler_param=1")

        expect do
          tracer.in_span("test_operation") do |span|
            span.set_attribute("test_key", "test_value")
          end
        end.to output(/test_operation/).to_stdout
      end

      it "works with configuration block" do
        tracer = described_class.create_tracer(service_name, "otlp://console") do |_c|
          # Configuration block is called successfully
        end

        expect(tracer).to be_a(OpenTelemetry::Trace::Tracer)
        provider = OpenTelemetry.tracer_provider
        expect(provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider)
      end
    end
  end

  describe ".parse_otlp_connection_string" do
@@ -449,4 +494,32 @@ describe Labkit::Tracing::OpenTelemetryFactory do
      expect(tracer2).to be_a(OpenTelemetry::Trace::Tracer)
    end
  end

  describe ".get_console_exporter" do
    it "returns a ConsoleSpanExporter instance" do
      exporter = described_class.send(:get_console_exporter)
      expect(exporter).to be_a(OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter)
    end
  end

  describe ".get_exporter for console" do
    it "creates console exporter for console endpoint" do
      exporter = described_class.send(:get_exporter, "http://console", nil, nil, {})
      expect(exporter).to be_a(OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter)
    end
  end

  describe ".parse_otlp_connection_string for console" do
    it "handles console host" do
      options = described_class.send(:parse_otlp_connection_string, "otlp://console")
      expect(options[:http_endpoint]).to eq("http://console")
    end

    it "handles console with query parameters" do
      options = described_class.send(:parse_otlp_connection_string, "otlp://console?sampler=const&sampler_param=1")
      expect(options[:http_endpoint]).to eq("http://console")
      expect(options[:sampler]).to eq("const")
      expect(options[:sampler_param]).to eq("1")
    end
  end
end