gitlab-labkit 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ module Labkit
7
7
  # https://edgeapi.rubyonrails.org/classes/ActiveSupport/Notifications/Instrumenter.html#method-c-new
8
8
  class AbstractInstrumenter
9
9
  def start(_name, _id, payload)
10
- scope = OpenTracing.start_active_span(span_name(payload))
10
+ scope = Labkit::Tracing::TracingUtils.tracer.start_active_span(span_name(payload))
11
11
 
12
12
  scope_stack.push scope
13
13
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labkit
4
+ module Tracing
5
+ module Adapters
6
+ class BaseSpan
7
+ attr_reader :span
8
+
9
+ def initialize(span)
10
+ @span = span
11
+ end
12
+
13
+ def set_tag(_key, _value)
14
+ raise NotImplementedError, "#{self.class.name}#set_tag must be implemented"
15
+ end
16
+
17
+ def log_event(_name, **_attributes)
18
+ raise NotImplementedError, "#{self.class.name}#log_event must be implemented"
19
+ end
20
+
21
+ def set_error(_exception)
22
+ raise NotImplementedError, "#{self.class.name}#set_error must be implemented"
23
+ end
24
+
25
+ def finish(**_opts)
26
+ raise NotImplementedError, "#{self.class.name}#finish must be implemented"
27
+ end
28
+
29
+ def context
30
+ raise NotImplementedError, "#{self.class.name}#context must be implemented"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labkit
4
+ module Tracing
5
+ module Adapters
6
+ class BaseTracer
7
+ attr_reader :tracer
8
+
9
+ def initialize(tracer)
10
+ @tracer = tracer
11
+ end
12
+
13
+ def start_span(_operation_name, child_of: nil, tags: {}, start_time: nil)
14
+ raise NotImplementedError, "#{self.class.name}#start_span must be implemented"
15
+ end
16
+
17
+ def in_span(_operation_name, child_of: nil, tags: {}, &_block)
18
+ raise NotImplementedError, "#{self.class.name}#in_span must be implemented"
19
+ end
20
+
21
+ def extract_context(_carrier, format: nil)
22
+ raise NotImplementedError, "#{self.class.name}#extract_context must be implemented"
23
+ end
24
+
25
+ def inject_context(_span_context, _carrier, format: nil)
26
+ raise NotImplementedError, "#{self.class.name}#inject_context must be implemented"
27
+ end
28
+
29
+ def active_span
30
+ raise NotImplementedError, "#{self.class.name}#active_span must be implemented"
31
+ end
32
+
33
+ def start_active_span(_operation_name, tags: nil)
34
+ raise NotImplementedError, "#{self.class.name}#start_active_span must be implemented"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Internal adapter for OpenTelemetry span compatibility.
4
+ # This is not part of the public Labkit API.
5
+ #
6
+ # Applications should use OpenTelemetry APIs directly via:
7
+ # - Labkit::Tracing.tracer (returns OpenTelemetry::Trace::Tracer)
8
+ # - Labkit::Tracing.current_span (returns OpenTelemetry::Trace::Span)
9
+ #
10
+ # These adapters exist for:
11
+ # - Internal Labkit instrumentation (Rails, Redis, etc.)
12
+ # - Backward compatibility with OpenTracing connections
13
+ #
14
+ # @api private
15
+
16
+ module Labkit
17
+ module Tracing
18
+ module Adapters
19
+ class OpentelemetrySpan < BaseSpan
20
+ def set_tag(key, value)
21
+ span.set_attribute(key, value)
22
+ end
23
+
24
+ def log_event(name, **attributes)
25
+ span.add_event(name, attributes: stringify_keys(attributes))
26
+ end
27
+
28
+ def set_error(exception)
29
+ return if exception.blank?
30
+
31
+ span.set_attribute("error", true)
32
+ span.add_event("exception", attributes: stringify_keys(kv_tags_for_exception(exception)))
33
+ end
34
+
35
+ def finish(**opts)
36
+ if opts[:end_timestamp]
37
+ span.finish(end_timestamp: opts[:end_timestamp])
38
+ else
39
+ span.finish
40
+ end
41
+ end
42
+
43
+ def context
44
+ span.context
45
+ end
46
+
47
+ private
48
+
49
+ def stringify_keys(hash)
50
+ hash.transform_keys(&:to_s)
51
+ end
52
+
53
+ def kv_tags_for_exception(exception)
54
+ case exception
55
+ when Exception
56
+ {
57
+ event: "error",
58
+ 'error.kind': exception.class.to_s,
59
+ message: Labkit::Logging::Sanitizer.sanitize_field(exception.message),
60
+ stack: exception.backtrace&.join('\n')
61
+ }
62
+ else
63
+ {
64
+ event: "error",
65
+ 'error.kind': exception.class.to_s,
66
+ 'error.object': Labkit::Logging::Sanitizer.sanitize_field(exception.to_s)
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Internal adapter for OpenTelemetry tracer compatibility.
4
+ # This is not part of the public Labkit API.
5
+ #
6
+ # Applications should use OpenTelemetry APIs directly via:
7
+ # - Labkit::Tracing.tracer (returns OpenTelemetry::Trace::Tracer)
8
+ # - Labkit::Tracing.current_span (returns OpenTelemetry::Trace::Span)
9
+ #
10
+ # These adapters exist for:
11
+ # - Internal Labkit instrumentation (Rails, Redis, etc.)
12
+ # - Backward compatibility with OpenTracing connections
13
+ #
14
+ # @api private
15
+
16
+ module Labkit
17
+ module Tracing
18
+ module Adapters
19
+ class OpentelemetryTracer < BaseTracer
20
+ def start_span(operation_name, child_of: nil, tags: {}, start_time: nil)
21
+ attributes = tags || {}
22
+ opts = { attributes: attributes }
23
+ opts[:with_parent] = child_of if child_of
24
+ opts[:start_timestamp] = start_time if start_time
25
+
26
+ span = tracer.start_span(operation_name, **opts)
27
+ OpentelemetrySpan.new(span)
28
+ end
29
+
30
+ def in_span(operation_name, child_of: nil, tags: {})
31
+ attributes = tags || {}
32
+
33
+ if child_of
34
+ span = tracer.start_span(operation_name, with_parent: child_of, attributes: attributes)
35
+ ctx = OpenTelemetry::Trace.context_with_span(span)
36
+
37
+ result = nil
38
+ OpenTelemetry::Context.with_current(ctx) do
39
+ adapter = OpentelemetrySpan.new(span)
40
+ result = yield(adapter)
41
+ end
42
+
43
+ span.finish
44
+ result
45
+ else
46
+ tracer.in_span(operation_name, attributes: attributes) do |span|
47
+ adapter = OpentelemetrySpan.new(span)
48
+ yield(adapter)
49
+ end
50
+ end
51
+ end
52
+
53
+ def extract_context(carrier, format: nil) # rubocop:disable Lint/UnusedMethodArgument
54
+ # Format parameter is ignored for OpenTelemetry - propagation format is configured
55
+ # globally via OTEL_PROPAGATORS environment variable, not per-call like OpenTracing.
56
+ # The parameter exists only for BaseTracer API compatibility.
57
+ OpenTelemetry.propagation.extract(carrier)
58
+ end
59
+
60
+ def inject_context(span_wrapper, carrier, format: nil) # rubocop:disable Lint/UnusedMethodArgument
61
+ # Format parameter is ignored for OpenTelemetry - propagation format is configured
62
+ # globally via OTEL_PROPAGATORS environment variable, not per-call like OpenTracing.
63
+ # The parameter exists only for BaseTracer API compatibility.
64
+ #
65
+ # span_wrapper is expected to be a BaseSpan (e.g., OpentelemetrySpan) that wraps
66
+ # the actual OpenTelemetry span. We unwrap it to get the OTel span object.
67
+ span = span_wrapper.respond_to?(:span) ? span_wrapper.span : span_wrapper
68
+ context = OpenTelemetry::Trace.context_with_span(span)
69
+ OpenTelemetry.propagation.inject(carrier, context: context)
70
+ end
71
+
72
+ def active_span
73
+ OpenTelemetry::Trace.current_span
74
+ end
75
+
76
+ def start_active_span(operation_name, tags: nil)
77
+ attributes = tags || {}
78
+ raw_span = tracer.start_span(operation_name, attributes: attributes)
79
+ ctx = OpenTelemetry::Trace.context_with_span(raw_span)
80
+ token = OpenTelemetry::Context.attach(ctx)
81
+
82
+ OpenTelemetryScope.new(raw_span, token)
83
+ end
84
+
85
+ class OpenTelemetryScope
86
+ attr_reader :span
87
+
88
+ def initialize(raw_span, token)
89
+ @span = OpentelemetrySpan.new(raw_span)
90
+ @raw_span = raw_span
91
+ @token = token
92
+ end
93
+
94
+ def close
95
+ @raw_span.finish
96
+ OpenTelemetry::Context.detach(@token)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labkit
4
+ module Tracing
5
+ module Adapters
6
+ class OpentracingSpan < BaseSpan
7
+ attr_reader :scope
8
+
9
+ def initialize(span_or_scope)
10
+ if span_or_scope.respond_to?(:span)
11
+ @scope = span_or_scope
12
+ @span = span_or_scope.span
13
+ else
14
+ @scope = nil
15
+ @span = span_or_scope
16
+ end
17
+ end
18
+
19
+ def set_tag(key, value)
20
+ span.set_tag(key, value)
21
+ end
22
+
23
+ def log_event(name, **attributes)
24
+ span.log_kv(**attributes, event: name)
25
+ end
26
+
27
+ def set_error(exception)
28
+ return if exception.blank?
29
+
30
+ span.set_tag("error", true)
31
+ span.log_kv(**kv_tags_for_exception(exception))
32
+ end
33
+
34
+ def finish(**opts)
35
+ if opts[:end_timestamp]
36
+ span.finish(end_time: opts[:end_timestamp])
37
+ else
38
+ span.finish
39
+ end
40
+
41
+ scope&.close
42
+ end
43
+
44
+ def context
45
+ span.context
46
+ end
47
+
48
+ private
49
+
50
+ def kv_tags_for_exception(exception)
51
+ case exception
52
+ when Exception
53
+ {
54
+ event: "error",
55
+ 'error.kind': exception.class.to_s,
56
+ message: Labkit::Logging::Sanitizer.sanitize_field(exception.message),
57
+ stack: exception.backtrace&.join('\n')
58
+ }
59
+ else
60
+ {
61
+ event: "error",
62
+ 'error.kind': exception.class.to_s,
63
+ 'error.object': Labkit::Logging::Sanitizer.sanitize_field(exception.to_s)
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labkit
4
+ module Tracing
5
+ module Adapters
6
+ class OpentracingTracer < BaseTracer
7
+ def start_span(operation_name, child_of: nil, tags: {}, start_time: nil)
8
+ opts = {}
9
+ opts[:child_of] = child_of if child_of
10
+ opts[:tags] = tags if tags
11
+ opts[:start_time] = start_time if start_time
12
+
13
+ span = tracer.start_span(operation_name, **opts)
14
+ OpentracingSpan.new(span)
15
+ end
16
+
17
+ def in_span(operation_name, child_of: nil, tags: {})
18
+ scope = tracer.start_active_span(operation_name, child_of: child_of, tags: tags)
19
+ adapter = OpentracingSpan.new(scope)
20
+
21
+ begin
22
+ yield(adapter)
23
+ ensure
24
+ adapter.finish
25
+ end
26
+ end
27
+
28
+ def extract_context(carrier, format: OpenTracing::FORMAT_TEXT_MAP)
29
+ tracer.extract(format, carrier)
30
+ end
31
+
32
+ def inject_context(span_context, carrier, format: OpenTracing::FORMAT_TEXT_MAP)
33
+ tracer.inject(span_context, format, carrier)
34
+ end
35
+
36
+ def active_span
37
+ OpenTracing.active_span
38
+ end
39
+
40
+ def start_active_span(operation_name, tags: nil)
41
+ if tags
42
+ OpenTracing.start_active_span(operation_name, tags: tags)
43
+ else
44
+ OpenTracing.start_active_span(operation_name)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "active_support/core_ext/object/blank"
5
+
6
+ module Labkit
7
+ module Tracing
8
+ module AutoInitialize
9
+ def self.detect_service_name(connection_string)
10
+ return Labkit::Tracing::DEFAULT_SERVICE_NAME unless connection_string
11
+
12
+ if connection_string =~ /[?&]service_name=([^&]+)/
13
+ CGI.unescape(Regexp.last_match(1))
14
+ else
15
+ Labkit::Tracing::DEFAULT_SERVICE_NAME
16
+ end
17
+ end
18
+
19
+ def self.initialize!
20
+ connection_string = ENV.fetch("GITLAB_TRACING", nil)
21
+ return if connection_string.nil? || connection_string.empty?
22
+
23
+ service_name = detect_service_name(connection_string)
24
+
25
+ Factory.create_tracer(service_name, connection_string) do |c|
26
+ require "opentelemetry/instrumentation/all"
27
+ c.use_all
28
+ end
29
+
30
+ enable_labkit_instrumentation
31
+ rescue StandardError => e
32
+ warn "Labkit::Tracing auto-initialization failed: #{e.message}"
33
+ end
34
+
35
+ def self.enable_labkit_instrumentation
36
+ Rails::ActiveRecord::Subscriber.instrument if defined?(::ActiveRecord)
37
+ Rails::ActionView::Subscriber.instrument if defined?(::ActionView)
38
+ Rails::ActiveSupport::Subscriber.instrument if defined?(::ActiveSupport)
39
+ Redis.instrument if defined?(::Redis)
40
+ ExternalHttp.instrument
41
+ rescue StandardError => e
42
+ warn "Labkit::Tracing: LabKit instrumentation setup failed: #{e.message}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,58 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cgi"
4
-
5
3
  module Labkit
6
4
  module Tracing
7
5
  # Factory provides tools for setting up and configuring the
8
6
  # distributed tracing system within the process, given the
9
7
  # tracing connection string
10
8
  class Factory
11
- OPENTRACING_SCHEME = "opentracing"
12
-
13
- def self.create_tracer(service_name, connection_string)
9
+ # When the probabilistic sampler is used, by default 0.1% of requests will be traced
10
+ DEFAULT_PROBABILISTIC_RATE = 0.001
11
+
12
+ # @param service_name [String] The service name for the tracer
13
+ # @param connection_string [String] The connection string (e.g., "otlp://localhost:4318")
14
+ # @yield [config] Optional configuration block for OpenTelemetry SDK customization (OTLP only)
15
+ # @yieldparam config [OpenTelemetry::SDK::Configurator] The SDK configurator
16
+ # @return [Tracer, nil] The configured tracer or nil if initialization fails
17
+ def self.create_tracer(service_name, connection_string, &config_block)
14
18
  return unless connection_string.present?
15
19
 
16
- begin
17
- opentracing_details = parse_connection_string(connection_string)
18
- driver_name = opentracing_details[:driver_name]
19
-
20
- case driver_name
21
- when "jaeger"
22
- JaegerFactory.create_tracer(service_name, opentracing_details[:options])
20
+ tracer =
21
+ if Tracing.otlp_connection?(connection_string)
22
+ OpenTelemetryFactory.create_tracer(service_name, connection_string, &config_block)
23
+ elsif Tracing.opentracing_connection?(connection_string)
24
+ warn_opentracing_block_ignored if config_block
25
+ OpenTracingFactory.create_tracer(service_name, connection_string)
23
26
  else
24
- raise "Unknown driver: #{driver_name}"
27
+ raise "Unknown protocol"
25
28
  end
26
29
 
27
- # Can't create the tracer? Warn and continue sans tracer
28
- rescue StandardError => e
29
- warn "Unable to instantiate tracer: #{e}"
30
- nil
31
- end
32
- end
33
-
34
- def self.parse_connection_string(connection_string)
35
- parsed = URI.parse(connection_string)
30
+ Tracing.configured_service_name = service_name
36
31
 
37
- raise "Invalid tracing connection string" unless valid_uri?(parsed)
38
-
39
- { driver_name: parsed.host, options: parse_query(parsed.query) }
32
+ tracer
33
+ rescue StandardError => e
34
+ warn "Unable to instantiate tracer: #{e}"
35
+ nil
40
36
  end
41
- private_class_method :parse_connection_string
42
-
43
- def self.parse_query(query)
44
- return {} unless query
45
-
46
- CGI.parse(query).symbolize_keys.transform_values(&:first)
47
- end
48
- private_class_method :parse_query
49
-
50
- def self.valid_uri?(uri)
51
- return false unless uri
52
37
 
53
- uri.scheme == OPENTRACING_SCHEME && uri.host.to_s =~ /^[a-z0-9_]+$/ && uri.path.empty?
38
+ def self.warn_opentracing_block_ignored
39
+ warn "Warning: Configuration block provided but ignored - " \
40
+ "OpenTracing connection strings don't support SDK customization. " \
41
+ "Use OTLP (otlp://) for OpenTelemetry SDK features."
54
42
  end
55
- private_class_method :valid_uri?
43
+ private_class_method :warn_opentracing_block_ignored
56
44
  end
57
45
  end
58
46
  end
@@ -36,7 +36,7 @@ module Labkit
36
36
  tags = { "component" => "grpc", "span.kind" => "client", "grpc.method" => method, "grpc.type" => grpc_type }
37
37
 
38
38
  TracingUtils.with_tracing(operation_name: "grpc:#{method}", tags: tags) do |span|
39
- OpenTracing.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, metadata)
39
+ TracingUtils.tracer.inject_context(span, metadata)
40
40
 
41
41
  yield
42
42
  end
@@ -42,13 +42,13 @@ module Labkit
42
42
  private
43
43
 
44
44
  def wrap_with_tracing(call, method, grpc_type)
45
- context = TracingUtils.tracer.extract(OpenTracing::FORMAT_TEXT_MAP, call.metadata)
45
+ context = TracingUtils.tracer.extract_context(call.metadata)
46
46
  method_name = "/#{rpc_split(method).join("/")}"
47
47
  tags = {
48
48
  "component" => "grpc",
49
49
  "span.kind" => "server",
50
50
  "grpc.method" => method_name,
51
- "grpc.type" => grpc_type,
51
+ "grpc.type" => grpc_type
52
52
  }
53
53
 
54
54
  TracingUtils.with_tracing(operation_name: "grpc:#{method_name}", child_of: context, tags: tags) do |_span|
@@ -4,14 +4,12 @@ require "active_support"
4
4
  require "active_support/core_ext"
5
5
 
6
6
  require "jaeger/client"
7
+ require "opentracing"
7
8
 
8
9
  module Labkit
9
10
  module Tracing
10
11
  # JaegerFactory will configure Jaeger distributed tracing
11
12
  class JaegerFactory
12
- # When the probabilistic sampler is used, by default 0.1% of requests will be traced
13
- DEFAULT_PROBABILISTIC_RATE = 0.001
14
-
15
13
  # The default port for the Jaeger agent UDP listener
16
14
  DEFAULT_UDP_PORT = 6831
17
15
 
@@ -31,7 +29,7 @@ module Labkit
31
29
  kwargs = {
32
30
  service_name: service_name,
33
31
  sampler: get_sampler(options[:sampler], options[:sampler_param]),
34
- reporter: get_reporter(service_name, options[:http_endpoint], options[:udp_endpoint], headers),
32
+ reporter: get_reporter(service_name, options[:http_endpoint], options[:udp_endpoint], headers)
35
33
  }.compact
36
34
 
37
35
  extra_params = options.except(:sampler, :sampler_param, :http_endpoint, :udp_endpoint, :strict_parsing, :debug)
@@ -43,7 +41,12 @@ module Labkit
43
41
  warn message
44
42
  end
45
43
 
46
- Jaeger::Client.build(**kwargs)
44
+ tracer = Jaeger::Client.build(**kwargs)
45
+
46
+ # Set as global tracer for consistency with OpenTelemetry behavior
47
+ OpenTracing.global_tracer = tracer if tracer
48
+
49
+ tracer
47
50
  end
48
51
 
49
52
  def self.build_headers(options)
@@ -60,14 +63,14 @@ module Labkit
60
63
  headers["Authorization"] = "Basic " + Base64.strict_encode64("#{user}:#{password}")
61
64
  end
62
65
 
63
- return headers
66
+ headers
64
67
  end
65
68
  private_class_method :build_headers
66
69
 
67
70
  def self.get_sampler(sampler_type, sampler_param)
68
71
  case sampler_type
69
72
  when "probabilistic"
70
- sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE
73
+ sampler_rate = sampler_param ? sampler_param.to_f : Factory::DEFAULT_PROBABILISTIC_RATE
71
74
  Jaeger::Samplers::Probabilistic.new(rate: sampler_rate)
72
75
  when "const"
73
76
  const_value = sampler_param == "1"
@@ -92,7 +95,7 @@ module Labkit
92
95
  private_class_method :get_reporter
93
96
 
94
97
  def self.get_http_sender(encoder, address, headers)
95
- Jaeger::HttpSender.new(url: address, headers: headers, encoder: encoder, logger: Logger.new(STDOUT))
98
+ Jaeger::HttpSender.new(url: address, headers: headers, encoder: encoder, logger: Logger.new($stdout))
96
99
  end
97
100
  private_class_method :get_http_sender
98
101
 
@@ -101,7 +104,7 @@ module Labkit
101
104
  host = pair[0]
102
105
  port = pair[1] ? pair[1].to_i : DEFAULT_UDP_PORT
103
106
 
104
- Jaeger::UdpSender.new(host: host, port: port, encoder: encoder, logger: Logger.new(STDOUT))
107
+ Jaeger::UdpSender.new(host: host, port: port, encoder: encoder, logger: Logger.new($stdout))
105
108
  end
106
109
  private_class_method :get_udp_sender
107
110
  end