Loading lib/labkit/tracing/factory.rb +15 −2 Original line number Diff line number Diff line Loading @@ -6,12 +6,18 @@ module Labkit # distributed tracing system within the process, given the # tracing connection string class Factory def self.create_tracer(service_name, connection_string) # @param service_name [String] The service name for the tracer # @param connection_string [String] The connection string (e.g., "otlp://localhost:4318") # @yield [config] Optional configuration block for OpenTelemetry SDK customization (OTLP only) # @yieldparam config [OpenTelemetry::SDK::Configurator] The SDK configurator # @return [Tracer, nil] The configured tracer or nil if initialization fails def self.create_tracer(service_name, connection_string, &config_block) return unless connection_string.present? if Tracing.otlp_connection?(connection_string) OpenTelemetryFactory.create_tracer(service_name, connection_string) OpenTelemetryFactory.create_tracer(service_name, connection_string, &config_block) elsif Tracing.opentracing_connection?(connection_string) warn_opentracing_block_ignored if config_block OpenTracingFactory.create_tracer(service_name, connection_string) else raise "Unknown protocol" Loading @@ -20,6 +26,13 @@ module Labkit warn "Unable to instantiate tracer: #{e}" nil end def self.warn_opentracing_block_ignored warn "Warning: Configuration block provided but ignored - " \ "OpenTracing connection strings don't support SDK customization. " \ "Use OTLP (otlp://) for OpenTelemetry SDK features." end private_class_method :warn_opentracing_block_ignored end end end lib/labkit/tracing/open_telemetry_factory.rb +76 −18 Original line number Diff line number Diff line Loading @@ -24,7 +24,12 @@ module Labkit DEFAULT_GRPC_ENDPOINT = "http://localhost:4317" class << self def create_tracer(service_name, connection_string) # @param service_name [String] The service name for the tracer # @param connection_string [String] The connection string (e.g., "otlp://localhost:4318") # @yield [config] Optional configuration block for OpenTelemetry SDK customization # @yieldparam config [OpenTelemetry::SDK::Configurator] The SDK configurator # @return [Tracer, nil] The configured tracer or nil if initialization fails def create_tracer(service_name, connection_string, &config_block) return unless connection_string.present? options = parse_otlp_connection_string(connection_string) Loading @@ -34,27 +39,22 @@ module Labkit # parse exporter headers as necessary headers = build_headers(options) # Get sampler and exporter # Get sampler and exporter from GITLAB_TRACING sampler = get_sampler(options[:sampler], options[:sampler_param]) exporter = get_exporter(options[:http_endpoint], options[:grpc_endpoint], options[:udp_endpoint], headers) # Build the tracer provider manually to have more control span_processors = [] span_processors << OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) if exporter resource = OpenTelemetry::SDK::Resources::Resource.create( # Build base resource base_resource = OpenTelemetry::SDK::Resources::Resource.create( OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => service_name ) tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new( resource: resource, sampler: sampler ) span_processors.each { |processor| tracer_provider.add_span_processor(processor) } # Register the tracer provider globally OpenTelemetry.tracer_provider = tracer_provider # If user provided a config block, use SDK.configure for full configurability # Otherwise, use the manual configuration for backward compatibility if config_block configure_with_sdk(service_name, base_resource, sampler, exporter, &config_block) else configure_manually(service_name, base_resource, sampler, exporter) end extra_params = options.except( :sampler, Loading @@ -64,7 +64,8 @@ module Labkit :udp_endpoint, :strict_parsing, :debug, :service_name :service_name, :protocol ) if extra_params.present? Loading @@ -75,11 +76,68 @@ module Labkit warn message end tracer_provider.tracer(service_name) OpenTelemetry.tracer_provider.tracer(service_name) end private def configure_with_sdk(service_name, base_resource, sampler, exporter) # Use OpenTelemetry::SDK.configure for user customization # We'll create the provider manually first with sampler, then allow customization OpenTelemetry::SDK.configure do |c| c.service_name = service_name c.resource = base_resource # Add span processor with our exporter from GITLAB_TRACING if exporter c.add_span_processor( OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) ) end # Yield to user configuration block (runs last, can override/extend) yield(c) if block_given? end # SDK.configure doesn't expose sampler configuration directly # We need to replace the tracer provider to set the sampler # This is a known limitation of the OpenTelemetry Ruby SDK current_provider = OpenTelemetry.tracer_provider return unless current_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider) # Create new provider with sampler, preserving resource new_provider = OpenTelemetry::SDK::Trace::TracerProvider.new( resource: current_provider.resource, sampler: sampler ) # Re-add span processor with our exporter if exporter new_provider.add_span_processor( OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) ) end # Set globally OpenTelemetry.tracer_provider = new_provider end 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 tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new( resource: base_resource, sampler: sampler ) span_processors.each { |processor| tracer_provider.add_span_processor(processor) } # Register the tracer provider globally OpenTelemetry.tracer_provider = tracer_provider end def build_headers(options) return {} unless options&.key?(:http_endpoint) || options&.key?(:grpc_endpoint) Loading spec/labkit/tracing/factory_spec.rb +53 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,59 @@ describe Labkit::Tracing::Factory do expect(described_class.create_tracer(service_name, connection_string)).to be(mock_tracer) end it "passes config block to OpenTelemetryFactory" do connection_string = "otlp://localhost:4318" config_block = proc { |c| c.service_name = "custom" } expect(Labkit::Tracing::OpenTelemetryFactory).to receive(:create_tracer) do |name, conn, &block| expect(name).to eq(service_name) expect(conn).to eq(connection_string) expect(block).to eq(config_block) mock_tracer end result = described_class.create_tracer(service_name, connection_string, &config_block) expect(result).to be(mock_tracer) end end context "when config block is provided" do it "works without block for backward compatibility" do connection_string = "otlp://localhost:4318" expect(Labkit::Tracing::OpenTelemetryFactory).to receive(:create_tracer).with( service_name, connection_string ).and_call_original expect { described_class.create_tracer(service_name, connection_string) }.not_to raise_error end it "warns when block provided with OpenTracing connection" do connection_string = "opentracing://jaeger" expect(Labkit::Tracing::OpenTracingFactory).to receive(:create_tracer).with( service_name, connection_string ).and_return(double("tracer")) expect do described_class.create_tracer(service_name, connection_string) do |c| c.service_name = "custom" end end.to output(/Configuration block provided but ignored/).to_stderr end it "does not warn when no block provided with OpenTracing" do connection_string = "opentracing://jaeger" expect(Labkit::Tracing::OpenTracingFactory).to receive(:create_tracer).with( service_name, connection_string ).and_return(double("tracer")) expect do described_class.create_tracer(service_name, connection_string) end.not_to output(/Configuration block/).to_stderr end end end end spec/labkit/tracing/open_telemetry_factory_spec.rb +92 −0 Original line number Diff line number Diff line Loading @@ -357,4 +357,96 @@ describe Labkit::Tracing::OpenTelemetryFactory do described_class.send(:get_exporter, "http://localhost:4318", "http://localhost:4317", nil, headers) end end describe "with config block" do let(:service_name) { "test-service" } let(:connection_string) { "otlp://localhost:4318" } after do OpenTelemetry.tracer_provider = OpenTelemetry::Internal::ProxyTracerProvider.new end it "yields to the block with configurator" do block_called = false received_configurator = nil described_class.create_tracer(service_name, connection_string) do |c| block_called = true received_configurator = c end expect(block_called).to be true expect(received_configurator).to be_a(OpenTelemetry::SDK::Configurator) end it "applies GITLAB_TRACING settings before block" do tracer = described_class.create_tracer(service_name, connection_string) do |c| # Block runs after GITLAB_TRACING settings expect(c.service_name).to eq(service_name) end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) end it "allows block to add span processors" do custom_processor = instance_double(OpenTelemetry::SDK::Trace::SpanProcessor) allow(custom_processor).to receive(:on_start) allow(custom_processor).to receive(:on_finish) allow(custom_processor).to receive(:shutdown) allow(custom_processor).to receive(:force_flush) tracer = described_class.create_tracer(service_name, connection_string) do |c| c.add_span_processor(custom_processor) end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) expect(OpenTelemetry.tracer_provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider) end it "preserves GITLAB_TRACING exporter configuration" do expect(OpenTelemetry::Exporter::OTLP::Exporter).to receive(:new).with( endpoint: "http://localhost:4318", headers: {} ).and_call_original.at_least(:once) tracer = described_class.create_tracer(service_name, connection_string) do |c| # User config shouldn't remove GITLAB_TRACING exporter end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) end it "works without config block for backward compatibility" do tracer = described_class.create_tracer(service_name, connection_string) expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) expect(OpenTelemetry.tracer_provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider) end it "applies sampler from connection string" do sampler_connection = "otlp://localhost:4318?sampler=const&sampler_param=0" tracer = described_class.create_tracer(service_name, sampler_connection) do |c| # Sampler from connection string should be applied end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) provider = OpenTelemetry.tracer_provider expect(provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider) end it "handles multiple calls (last wins)" do # First call tracer1 = described_class.create_tracer("service1", connection_string) do |c| c.service_name = "service1" end # Second call should reconfigure tracer2 = described_class.create_tracer("service2", connection_string) do |c| c.service_name = "service2" end expect(tracer1).to be_a(OpenTelemetry::Trace::Tracer) expect(tracer2).to be_a(OpenTelemetry::Trace::Tracer) end end end Loading
lib/labkit/tracing/factory.rb +15 −2 Original line number Diff line number Diff line Loading @@ -6,12 +6,18 @@ module Labkit # distributed tracing system within the process, given the # tracing connection string class Factory def self.create_tracer(service_name, connection_string) # @param service_name [String] The service name for the tracer # @param connection_string [String] The connection string (e.g., "otlp://localhost:4318") # @yield [config] Optional configuration block for OpenTelemetry SDK customization (OTLP only) # @yieldparam config [OpenTelemetry::SDK::Configurator] The SDK configurator # @return [Tracer, nil] The configured tracer or nil if initialization fails def self.create_tracer(service_name, connection_string, &config_block) return unless connection_string.present? if Tracing.otlp_connection?(connection_string) OpenTelemetryFactory.create_tracer(service_name, connection_string) OpenTelemetryFactory.create_tracer(service_name, connection_string, &config_block) elsif Tracing.opentracing_connection?(connection_string) warn_opentracing_block_ignored if config_block OpenTracingFactory.create_tracer(service_name, connection_string) else raise "Unknown protocol" Loading @@ -20,6 +26,13 @@ module Labkit warn "Unable to instantiate tracer: #{e}" nil end def self.warn_opentracing_block_ignored warn "Warning: Configuration block provided but ignored - " \ "OpenTracing connection strings don't support SDK customization. " \ "Use OTLP (otlp://) for OpenTelemetry SDK features." end private_class_method :warn_opentracing_block_ignored end end end
lib/labkit/tracing/open_telemetry_factory.rb +76 −18 Original line number Diff line number Diff line Loading @@ -24,7 +24,12 @@ module Labkit DEFAULT_GRPC_ENDPOINT = "http://localhost:4317" class << self def create_tracer(service_name, connection_string) # @param service_name [String] The service name for the tracer # @param connection_string [String] The connection string (e.g., "otlp://localhost:4318") # @yield [config] Optional configuration block for OpenTelemetry SDK customization # @yieldparam config [OpenTelemetry::SDK::Configurator] The SDK configurator # @return [Tracer, nil] The configured tracer or nil if initialization fails def create_tracer(service_name, connection_string, &config_block) return unless connection_string.present? options = parse_otlp_connection_string(connection_string) Loading @@ -34,27 +39,22 @@ module Labkit # parse exporter headers as necessary headers = build_headers(options) # Get sampler and exporter # Get sampler and exporter from GITLAB_TRACING sampler = get_sampler(options[:sampler], options[:sampler_param]) exporter = get_exporter(options[:http_endpoint], options[:grpc_endpoint], options[:udp_endpoint], headers) # Build the tracer provider manually to have more control span_processors = [] span_processors << OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) if exporter resource = OpenTelemetry::SDK::Resources::Resource.create( # Build base resource base_resource = OpenTelemetry::SDK::Resources::Resource.create( OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => service_name ) tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new( resource: resource, sampler: sampler ) span_processors.each { |processor| tracer_provider.add_span_processor(processor) } # Register the tracer provider globally OpenTelemetry.tracer_provider = tracer_provider # If user provided a config block, use SDK.configure for full configurability # Otherwise, use the manual configuration for backward compatibility if config_block configure_with_sdk(service_name, base_resource, sampler, exporter, &config_block) else configure_manually(service_name, base_resource, sampler, exporter) end extra_params = options.except( :sampler, Loading @@ -64,7 +64,8 @@ module Labkit :udp_endpoint, :strict_parsing, :debug, :service_name :service_name, :protocol ) if extra_params.present? Loading @@ -75,11 +76,68 @@ module Labkit warn message end tracer_provider.tracer(service_name) OpenTelemetry.tracer_provider.tracer(service_name) end private def configure_with_sdk(service_name, base_resource, sampler, exporter) # Use OpenTelemetry::SDK.configure for user customization # We'll create the provider manually first with sampler, then allow customization OpenTelemetry::SDK.configure do |c| c.service_name = service_name c.resource = base_resource # Add span processor with our exporter from GITLAB_TRACING if exporter c.add_span_processor( OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) ) end # Yield to user configuration block (runs last, can override/extend) yield(c) if block_given? end # SDK.configure doesn't expose sampler configuration directly # We need to replace the tracer provider to set the sampler # This is a known limitation of the OpenTelemetry Ruby SDK current_provider = OpenTelemetry.tracer_provider return unless current_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider) # Create new provider with sampler, preserving resource new_provider = OpenTelemetry::SDK::Trace::TracerProvider.new( resource: current_provider.resource, sampler: sampler ) # Re-add span processor with our exporter if exporter new_provider.add_span_processor( OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) ) end # Set globally OpenTelemetry.tracer_provider = new_provider end 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 tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new( resource: base_resource, sampler: sampler ) span_processors.each { |processor| tracer_provider.add_span_processor(processor) } # Register the tracer provider globally OpenTelemetry.tracer_provider = tracer_provider end def build_headers(options) return {} unless options&.key?(:http_endpoint) || options&.key?(:grpc_endpoint) Loading
spec/labkit/tracing/factory_spec.rb +53 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,59 @@ describe Labkit::Tracing::Factory do expect(described_class.create_tracer(service_name, connection_string)).to be(mock_tracer) end it "passes config block to OpenTelemetryFactory" do connection_string = "otlp://localhost:4318" config_block = proc { |c| c.service_name = "custom" } expect(Labkit::Tracing::OpenTelemetryFactory).to receive(:create_tracer) do |name, conn, &block| expect(name).to eq(service_name) expect(conn).to eq(connection_string) expect(block).to eq(config_block) mock_tracer end result = described_class.create_tracer(service_name, connection_string, &config_block) expect(result).to be(mock_tracer) end end context "when config block is provided" do it "works without block for backward compatibility" do connection_string = "otlp://localhost:4318" expect(Labkit::Tracing::OpenTelemetryFactory).to receive(:create_tracer).with( service_name, connection_string ).and_call_original expect { described_class.create_tracer(service_name, connection_string) }.not_to raise_error end it "warns when block provided with OpenTracing connection" do connection_string = "opentracing://jaeger" expect(Labkit::Tracing::OpenTracingFactory).to receive(:create_tracer).with( service_name, connection_string ).and_return(double("tracer")) expect do described_class.create_tracer(service_name, connection_string) do |c| c.service_name = "custom" end end.to output(/Configuration block provided but ignored/).to_stderr end it "does not warn when no block provided with OpenTracing" do connection_string = "opentracing://jaeger" expect(Labkit::Tracing::OpenTracingFactory).to receive(:create_tracer).with( service_name, connection_string ).and_return(double("tracer")) expect do described_class.create_tracer(service_name, connection_string) end.not_to output(/Configuration block/).to_stderr end end end end
spec/labkit/tracing/open_telemetry_factory_spec.rb +92 −0 Original line number Diff line number Diff line Loading @@ -357,4 +357,96 @@ describe Labkit::Tracing::OpenTelemetryFactory do described_class.send(:get_exporter, "http://localhost:4318", "http://localhost:4317", nil, headers) end end describe "with config block" do let(:service_name) { "test-service" } let(:connection_string) { "otlp://localhost:4318" } after do OpenTelemetry.tracer_provider = OpenTelemetry::Internal::ProxyTracerProvider.new end it "yields to the block with configurator" do block_called = false received_configurator = nil described_class.create_tracer(service_name, connection_string) do |c| block_called = true received_configurator = c end expect(block_called).to be true expect(received_configurator).to be_a(OpenTelemetry::SDK::Configurator) end it "applies GITLAB_TRACING settings before block" do tracer = described_class.create_tracer(service_name, connection_string) do |c| # Block runs after GITLAB_TRACING settings expect(c.service_name).to eq(service_name) end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) end it "allows block to add span processors" do custom_processor = instance_double(OpenTelemetry::SDK::Trace::SpanProcessor) allow(custom_processor).to receive(:on_start) allow(custom_processor).to receive(:on_finish) allow(custom_processor).to receive(:shutdown) allow(custom_processor).to receive(:force_flush) tracer = described_class.create_tracer(service_name, connection_string) do |c| c.add_span_processor(custom_processor) end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) expect(OpenTelemetry.tracer_provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider) end it "preserves GITLAB_TRACING exporter configuration" do expect(OpenTelemetry::Exporter::OTLP::Exporter).to receive(:new).with( endpoint: "http://localhost:4318", headers: {} ).and_call_original.at_least(:once) tracer = described_class.create_tracer(service_name, connection_string) do |c| # User config shouldn't remove GITLAB_TRACING exporter end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) end it "works without config block for backward compatibility" do tracer = described_class.create_tracer(service_name, connection_string) expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) expect(OpenTelemetry.tracer_provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider) end it "applies sampler from connection string" do sampler_connection = "otlp://localhost:4318?sampler=const&sampler_param=0" tracer = described_class.create_tracer(service_name, sampler_connection) do |c| # Sampler from connection string should be applied end expect(tracer).to be_a(OpenTelemetry::Trace::Tracer) provider = OpenTelemetry.tracer_provider expect(provider).to be_a(OpenTelemetry::SDK::Trace::TracerProvider) end it "handles multiple calls (last wins)" do # First call tracer1 = described_class.create_tracer("service1", connection_string) do |c| c.service_name = "service1" end # Second call should reconfigure tracer2 = described_class.create_tracer("service2", connection_string) do |c| c.service_name = "service2" end expect(tracer1).to be_a(OpenTelemetry::Trace::Tracer) expect(tracer2).to be_a(OpenTelemetry::Trace::Tracer) end end end