Loading lib/labkit/tracing.rb +2 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,8 @@ module Labkit autoload :GRPC, "labkit/tracing/grpc" autoload :GRPCInterceptor, "labkit/tracing/grpc_interceptor" # Deprecated autoload :JaegerFactory, "labkit/tracing/jaeger_factory" autoload :OpenTelemetryFactory, "labkit/tracing/open_telemetry_factory" autoload :OpenTracingFactory, "labkit/tracing/open_tracing_factory" autoload :RackMiddleware, "labkit/tracing/rack_middleware" autoload :Rails, "labkit/tracing/rails" autoload :Redis, "labkit/tracing/redis" Loading lib/labkit/tracing/factory.rb +11 −34 Original line number Diff line number Diff line # frozen_string_literal: true require "cgi" module Labkit module Tracing # Factory provides tools for setting up and configuring the # distributed tracing system within the process, given the # tracing connection string class Factory OPENTRACING_SCHEME = "opentracing" def self.create_tracer(service_name, connection_string) return unless connection_string.present? begin opentracing_details = parse_connection_string(connection_string) driver_name = opentracing_details[:driver_name] case driver_name when "jaeger" JaegerFactory.create_tracer(service_name, opentracing_details[:options]) if otlp_connection?(connection_string) OpenTelemetryFactory.create_tracer(service_name, connection_string) elsif opentracing_connection?(connection_string) OpenTracingFactory.create_tracer(service_name, connection_string) else raise "Unknown driver: #{driver_name}" raise "Unknown protocol" end # Can't create the tracer? Warn and continue sans tracer rescue StandardError => e warn "Unable to instantiate tracer: #{e}" nil end end def self.parse_connection_string(connection_string) parsed = URI.parse(connection_string) raise "Invalid tracing connection string" unless valid_uri?(parsed) { driver_name: parsed.host, options: parse_query(parsed.query) } def self.otlp_connection?(connection_string) connection_string.to_s.start_with?("#{OpenTelemetryFactory::OTLP_SCHEME}://") end private_class_method :parse_connection_string def self.parse_query(query) return {} unless query CGI.parse(query).symbolize_keys.transform_values(&:first) end private_class_method :parse_query def self.valid_uri?(uri) return false unless uri private_class_method :otlp_connection? uri.scheme == OPENTRACING_SCHEME && uri.host.to_s =~ /^[a-z0-9_]+$/ && uri.path.empty? def self.opentracing_connection?(connection_string) connection_string.to_s.start_with?("#{OpenTracingFactory::OPENTRACING_SCHEME}://") end private_class_method :valid_uri? private_class_method :opentracing_connection? end end end lib/labkit/tracing/open_telemetry_factory.rb 0 → 100644 +199 −0 Original line number Diff line number Diff line # frozen_string_literal: true require "base64" require "cgi" require "active_support" require "active_support/core_ext" require "opentelemetry/sdk" require "opentelemetry/exporter/otlp" module Labkit module Tracing # OpenTelemetryFactory will configure OpenTelemetry distributed tracing class OpenTelemetryFactory OTLP_SCHEME = "otlp" # When the probabilistic sampler is used, by default 0.1% of requests will be traced DEFAULT_PROBABILISTIC_RATE = 0.001 # The default endpoint for OTLP HTTP exporter DEFAULT_HTTP_ENDPOINT = "http://localhost:4318/v1/traces" # The default endpoint for OTLP gRPC exporter DEFAULT_GRPC_ENDPOINT = "http://localhost:4317" class << self def create_tracer(service_name, connection_string) return unless connection_string.present? options = parse_otlp_connection_string(connection_string) # The service_name parameter from GITLAB_TRACING takes precedence over the application one service_name = options[:service_name] if options[:service_name] # parse exporter headers as necessary headers = build_headers(options) # Get sampler and exporter 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( 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 extra_params = options.except( :sampler, :sampler_param, :http_endpoint, :grpc_endpoint, :udp_endpoint, :strict_parsing, :debug, :service_name ) if extra_params.present? message = "opentelemetry tracer: invalid option: #{extra_params.keys.join(', ')}" raise message if options[:strict_parsing] warn message end tracer_provider.tracer(service_name) end private def build_headers(options) return {} unless options&.key?(:http_endpoint) || options&.key?(:grpc_endpoint) endpoint = options[:http_endpoint] || options[:grpc_endpoint] return {} unless endpoint parsed = URI.parse(endpoint) headers = {} # add basic auth header only when both user and password are setup correctly user = parsed.user password = parsed.password headers["Authorization"] = "Basic #{Base64.strict_encode64("#{user}:#{password}")}" if user.present? && password.present? headers end def get_sampler(sampler_type, sampler_param) case sampler_type when "probabilistic" sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE OpenTelemetry::SDK::Trace::Samplers::TraceIdRatioBased.new(sampler_rate) when "const" if sampler_param == "1" OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON else OpenTelemetry::SDK::Trace::Samplers::ALWAYS_OFF end else OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON end end def get_exporter(http_endpoint, grpc_endpoint, udp_endpoint, headers) # OpenTelemetry doesn't support UDP, warn if specified # https://github.com/open-telemetry/opentelemetry-collector/discussions/6016 warn "opentelemetry tracer: UDP endpoint not supported, ignoring udp_endpoint option" if udp_endpoint.present? if http_endpoint.present? get_http_exporter(http_endpoint, headers) elsif grpc_endpoint.present? get_grpc_exporter(grpc_endpoint, headers) end end def get_http_exporter(endpoint, headers) OpenTelemetry::Exporter::OTLP::Exporter.new( endpoint: endpoint, headers: headers ) end def get_grpc_exporter(endpoint, headers) # OpenTelemetry Ruby lacks native gRPC exporter support. Fall back to HTTP exporter with protobuf encoding, # which is the standard approach and compatible with most OTLP collectors that accept gRPC-style endpoints. warn "opentelemetry tracer: gRPC endpoint specified but gRPC exporter not available, using HTTP" parsed = URI.parse(endpoint) http_port = parsed.port == 4317 ? 4318 : parsed.port http_endpoint = URI::HTTP.build( scheme: parsed.scheme, host: parsed.host, port: http_port, path: "/v1/traces" ).to_s get_http_exporter(http_endpoint, headers) 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) # Determine the endpoint type and set the appropriate option if parsed.port == 4317 || options[:protocol] == "grpc" options[:grpc_endpoint] = endpoint else # Default to HTTP (port 4318 or custom) options[:http_endpoint] = endpoint end options end def build_otlp_endpoint(uri) # Reconstruct the endpoint URL with scheme, host, port, and path scheme = uri.scheme == OTLP_SCHEME ? "http" : uri.scheme host = uri.host port = uri.port path = uri.path.empty? ? "" : uri.path # Include userinfo (username:password) if present userinfo = uri.userinfo ? "#{uri.userinfo}@" : "" # Build the endpoint endpoint = "#{scheme}://#{userinfo}#{host}" endpoint += ":#{port}" if port endpoint += path endpoint end def parse_query(query) return {} unless query CGI.parse(query).symbolize_keys.transform_values(&:first) end end end end end lib/labkit/tracing/open_tracing_factory.rb 0 → 100644 +48 −0 Original line number Diff line number Diff line # frozen_string_literal: true require "cgi" module Labkit module Tracing class OpenTracingFactory OPENTRACING_SCHEME = "opentracing" def self.create_tracer(service_name, connection_string) return unless connection_string.present? opentracing_details = parse_connection_string(connection_string) driver_name = opentracing_details[:driver_name] case driver_name when "jaeger" JaegerFactory.create_tracer(service_name, opentracing_details[:options]) else raise "Unknown driver: #{driver_name}" end end def self.parse_connection_string(connection_string) parsed = URI.parse(connection_string) raise "Invalid tracing connection string" unless valid_uri?(parsed) { driver_name: parsed.host, options: parse_query(parsed.query) } end private_class_method :parse_connection_string def self.parse_query(query) return {} unless query CGI.parse(query).symbolize_keys.transform_values(&:first) end private_class_method :parse_query def self.valid_uri?(uri) return false unless uri uri.scheme == OPENTRACING_SCHEME && uri.host.to_s =~ /^[a-z0-9_]+$/ && uri.path.empty? end private_class_method :valid_uri? end end end spec/labkit/tracing/factory_spec.rb +11 −17 Original line number Diff line number Diff line Loading @@ -22,33 +22,27 @@ describe Labkit::Tracing::Factory do end end context "when tracing is configured with jaeger" do context "when tracing is configured with opentracing" do let(:mock_tracer) { double("tracer") } it "processes default connections" do expect(Labkit::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer) it "delegates to OpenTracingFactory" do expect(Labkit::Tracing::OpenTracingFactory).to receive(:create_tracer).with(service_name, "opentracing://jaeger").and_return(mock_tracer) expect(described_class.create_tracer(service_name, "opentracing://jaeger")).to be(mock_tracer) end it "processes connections with parameters" do expect(Labkit::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: "1", b: "2", c: "3" }).and_return(mock_tracer) expect(described_class.create_tracer(service_name, "opentracing://jaeger?a=1&b=2&c=3")).to be(mock_tracer) end it "processes connections with basic auth credentials" do expect(Labkit::Tracing::JaegerFactory).to receive(:create_tracer).with( context "when tracing is configured with OpenTelemetry" do let(:mock_tracer) { double("tracer") } it "delegates to OpenTelemetryFactory with connection string" do connection_string = "otlp://localhost:4318" expect(Labkit::Tracing::OpenTelemetryFactory).to receive(:create_tracer).with( service_name, { a: "1", b: "2", c: "3", http_endpoint: "https://foo:bar@observe.gitlab.com", } connection_string ).and_return(mock_tracer) expect(described_class.create_tracer(service_name, "opentracing://jaeger?http_endpoint=https://foo:bar@observe.gitlab.com&a=1&b=2&c=3")).to be(mock_tracer) expect(described_class.create_tracer(service_name, connection_string)).to be(mock_tracer) end end end Loading Loading
lib/labkit/tracing.rb +2 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,8 @@ module Labkit autoload :GRPC, "labkit/tracing/grpc" autoload :GRPCInterceptor, "labkit/tracing/grpc_interceptor" # Deprecated autoload :JaegerFactory, "labkit/tracing/jaeger_factory" autoload :OpenTelemetryFactory, "labkit/tracing/open_telemetry_factory" autoload :OpenTracingFactory, "labkit/tracing/open_tracing_factory" autoload :RackMiddleware, "labkit/tracing/rack_middleware" autoload :Rails, "labkit/tracing/rails" autoload :Redis, "labkit/tracing/redis" Loading
lib/labkit/tracing/factory.rb +11 −34 Original line number Diff line number Diff line # frozen_string_literal: true require "cgi" module Labkit module Tracing # Factory provides tools for setting up and configuring the # distributed tracing system within the process, given the # tracing connection string class Factory OPENTRACING_SCHEME = "opentracing" def self.create_tracer(service_name, connection_string) return unless connection_string.present? begin opentracing_details = parse_connection_string(connection_string) driver_name = opentracing_details[:driver_name] case driver_name when "jaeger" JaegerFactory.create_tracer(service_name, opentracing_details[:options]) if otlp_connection?(connection_string) OpenTelemetryFactory.create_tracer(service_name, connection_string) elsif opentracing_connection?(connection_string) OpenTracingFactory.create_tracer(service_name, connection_string) else raise "Unknown driver: #{driver_name}" raise "Unknown protocol" end # Can't create the tracer? Warn and continue sans tracer rescue StandardError => e warn "Unable to instantiate tracer: #{e}" nil end end def self.parse_connection_string(connection_string) parsed = URI.parse(connection_string) raise "Invalid tracing connection string" unless valid_uri?(parsed) { driver_name: parsed.host, options: parse_query(parsed.query) } def self.otlp_connection?(connection_string) connection_string.to_s.start_with?("#{OpenTelemetryFactory::OTLP_SCHEME}://") end private_class_method :parse_connection_string def self.parse_query(query) return {} unless query CGI.parse(query).symbolize_keys.transform_values(&:first) end private_class_method :parse_query def self.valid_uri?(uri) return false unless uri private_class_method :otlp_connection? uri.scheme == OPENTRACING_SCHEME && uri.host.to_s =~ /^[a-z0-9_]+$/ && uri.path.empty? def self.opentracing_connection?(connection_string) connection_string.to_s.start_with?("#{OpenTracingFactory::OPENTRACING_SCHEME}://") end private_class_method :valid_uri? private_class_method :opentracing_connection? end end end
lib/labkit/tracing/open_telemetry_factory.rb 0 → 100644 +199 −0 Original line number Diff line number Diff line # frozen_string_literal: true require "base64" require "cgi" require "active_support" require "active_support/core_ext" require "opentelemetry/sdk" require "opentelemetry/exporter/otlp" module Labkit module Tracing # OpenTelemetryFactory will configure OpenTelemetry distributed tracing class OpenTelemetryFactory OTLP_SCHEME = "otlp" # When the probabilistic sampler is used, by default 0.1% of requests will be traced DEFAULT_PROBABILISTIC_RATE = 0.001 # The default endpoint for OTLP HTTP exporter DEFAULT_HTTP_ENDPOINT = "http://localhost:4318/v1/traces" # The default endpoint for OTLP gRPC exporter DEFAULT_GRPC_ENDPOINT = "http://localhost:4317" class << self def create_tracer(service_name, connection_string) return unless connection_string.present? options = parse_otlp_connection_string(connection_string) # The service_name parameter from GITLAB_TRACING takes precedence over the application one service_name = options[:service_name] if options[:service_name] # parse exporter headers as necessary headers = build_headers(options) # Get sampler and exporter 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( 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 extra_params = options.except( :sampler, :sampler_param, :http_endpoint, :grpc_endpoint, :udp_endpoint, :strict_parsing, :debug, :service_name ) if extra_params.present? message = "opentelemetry tracer: invalid option: #{extra_params.keys.join(', ')}" raise message if options[:strict_parsing] warn message end tracer_provider.tracer(service_name) end private def build_headers(options) return {} unless options&.key?(:http_endpoint) || options&.key?(:grpc_endpoint) endpoint = options[:http_endpoint] || options[:grpc_endpoint] return {} unless endpoint parsed = URI.parse(endpoint) headers = {} # add basic auth header only when both user and password are setup correctly user = parsed.user password = parsed.password headers["Authorization"] = "Basic #{Base64.strict_encode64("#{user}:#{password}")}" if user.present? && password.present? headers end def get_sampler(sampler_type, sampler_param) case sampler_type when "probabilistic" sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE OpenTelemetry::SDK::Trace::Samplers::TraceIdRatioBased.new(sampler_rate) when "const" if sampler_param == "1" OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON else OpenTelemetry::SDK::Trace::Samplers::ALWAYS_OFF end else OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON end end def get_exporter(http_endpoint, grpc_endpoint, udp_endpoint, headers) # OpenTelemetry doesn't support UDP, warn if specified # https://github.com/open-telemetry/opentelemetry-collector/discussions/6016 warn "opentelemetry tracer: UDP endpoint not supported, ignoring udp_endpoint option" if udp_endpoint.present? if http_endpoint.present? get_http_exporter(http_endpoint, headers) elsif grpc_endpoint.present? get_grpc_exporter(grpc_endpoint, headers) end end def get_http_exporter(endpoint, headers) OpenTelemetry::Exporter::OTLP::Exporter.new( endpoint: endpoint, headers: headers ) end def get_grpc_exporter(endpoint, headers) # OpenTelemetry Ruby lacks native gRPC exporter support. Fall back to HTTP exporter with protobuf encoding, # which is the standard approach and compatible with most OTLP collectors that accept gRPC-style endpoints. warn "opentelemetry tracer: gRPC endpoint specified but gRPC exporter not available, using HTTP" parsed = URI.parse(endpoint) http_port = parsed.port == 4317 ? 4318 : parsed.port http_endpoint = URI::HTTP.build( scheme: parsed.scheme, host: parsed.host, port: http_port, path: "/v1/traces" ).to_s get_http_exporter(http_endpoint, headers) 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) # Determine the endpoint type and set the appropriate option if parsed.port == 4317 || options[:protocol] == "grpc" options[:grpc_endpoint] = endpoint else # Default to HTTP (port 4318 or custom) options[:http_endpoint] = endpoint end options end def build_otlp_endpoint(uri) # Reconstruct the endpoint URL with scheme, host, port, and path scheme = uri.scheme == OTLP_SCHEME ? "http" : uri.scheme host = uri.host port = uri.port path = uri.path.empty? ? "" : uri.path # Include userinfo (username:password) if present userinfo = uri.userinfo ? "#{uri.userinfo}@" : "" # Build the endpoint endpoint = "#{scheme}://#{userinfo}#{host}" endpoint += ":#{port}" if port endpoint += path endpoint end def parse_query(query) return {} unless query CGI.parse(query).symbolize_keys.transform_values(&:first) end end end end end
lib/labkit/tracing/open_tracing_factory.rb 0 → 100644 +48 −0 Original line number Diff line number Diff line # frozen_string_literal: true require "cgi" module Labkit module Tracing class OpenTracingFactory OPENTRACING_SCHEME = "opentracing" def self.create_tracer(service_name, connection_string) return unless connection_string.present? opentracing_details = parse_connection_string(connection_string) driver_name = opentracing_details[:driver_name] case driver_name when "jaeger" JaegerFactory.create_tracer(service_name, opentracing_details[:options]) else raise "Unknown driver: #{driver_name}" end end def self.parse_connection_string(connection_string) parsed = URI.parse(connection_string) raise "Invalid tracing connection string" unless valid_uri?(parsed) { driver_name: parsed.host, options: parse_query(parsed.query) } end private_class_method :parse_connection_string def self.parse_query(query) return {} unless query CGI.parse(query).symbolize_keys.transform_values(&:first) end private_class_method :parse_query def self.valid_uri?(uri) return false unless uri uri.scheme == OPENTRACING_SCHEME && uri.host.to_s =~ /^[a-z0-9_]+$/ && uri.path.empty? end private_class_method :valid_uri? end end end
spec/labkit/tracing/factory_spec.rb +11 −17 Original line number Diff line number Diff line Loading @@ -22,33 +22,27 @@ describe Labkit::Tracing::Factory do end end context "when tracing is configured with jaeger" do context "when tracing is configured with opentracing" do let(:mock_tracer) { double("tracer") } it "processes default connections" do expect(Labkit::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer) it "delegates to OpenTracingFactory" do expect(Labkit::Tracing::OpenTracingFactory).to receive(:create_tracer).with(service_name, "opentracing://jaeger").and_return(mock_tracer) expect(described_class.create_tracer(service_name, "opentracing://jaeger")).to be(mock_tracer) end it "processes connections with parameters" do expect(Labkit::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: "1", b: "2", c: "3" }).and_return(mock_tracer) expect(described_class.create_tracer(service_name, "opentracing://jaeger?a=1&b=2&c=3")).to be(mock_tracer) end it "processes connections with basic auth credentials" do expect(Labkit::Tracing::JaegerFactory).to receive(:create_tracer).with( context "when tracing is configured with OpenTelemetry" do let(:mock_tracer) { double("tracer") } it "delegates to OpenTelemetryFactory with connection string" do connection_string = "otlp://localhost:4318" expect(Labkit::Tracing::OpenTelemetryFactory).to receive(:create_tracer).with( service_name, { a: "1", b: "2", c: "3", http_endpoint: "https://foo:bar@observe.gitlab.com", } connection_string ).and_return(mock_tracer) expect(described_class.create_tracer(service_name, "opentracing://jaeger?http_endpoint=https://foo:bar@observe.gitlab.com&a=1&b=2&c=3")).to be(mock_tracer) expect(described_class.create_tracer(service_name, connection_string)).to be(mock_tracer) end end end Loading