Loading lib/labkit/tracing/README.md +78 −10 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 Loading lib/labkit/tracing/open_telemetry_factory.rb +36 −7 Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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 Loading @@ -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, Loading Loading @@ -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? Loading Loading @@ -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 Loading spec/labkit/tracing/open_telemetry_factory_spec.rb +73 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading
lib/labkit/tracing/README.md +78 −10 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 Loading
lib/labkit/tracing/open_telemetry_factory.rb +36 −7 Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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 Loading @@ -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, Loading Loading @@ -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? Loading Loading @@ -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 Loading
spec/labkit/tracing/open_telemetry_factory_spec.rb +73 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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