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.
- checksums.yaml +4 -4
- data/gitlab-labkit.gemspec +5 -1
- data/lib/gitlab-labkit.rb +2 -0
- data/lib/labkit/logging/field_validator.rb +10 -11
- data/lib/labkit/middleware/sidekiq/tracing/client.rb +1 -1
- data/lib/labkit/middleware/sidekiq/tracing/server.rb +1 -1
- data/lib/labkit/tracing/README.md +716 -0
- data/lib/labkit/tracing/abstract_instrumenter.rb +1 -1
- data/lib/labkit/tracing/adapters/base_span.rb +35 -0
- data/lib/labkit/tracing/adapters/base_tracer.rb +39 -0
- data/lib/labkit/tracing/adapters/opentelemetry_span.rb +73 -0
- data/lib/labkit/tracing/adapters/opentelemetry_tracer.rb +102 -0
- data/lib/labkit/tracing/adapters/opentracing_span.rb +70 -0
- data/lib/labkit/tracing/adapters/opentracing_tracer.rb +50 -0
- data/lib/labkit/tracing/auto_initialize.rb +46 -0
- data/lib/labkit/tracing/factory.rb +26 -38
- data/lib/labkit/tracing/grpc/client_interceptor.rb +1 -1
- data/lib/labkit/tracing/grpc/server_interceptor.rb +2 -2
- data/lib/labkit/tracing/jaeger_factory.rb +12 -9
- data/lib/labkit/tracing/open_telemetry_factory.rb +218 -0
- data/lib/labkit/tracing/open_tracing_factory.rb +48 -0
- data/lib/labkit/tracing/rack_middleware.rb +1 -1
- data/lib/labkit/tracing/railtie.rb +15 -0
- data/lib/labkit/tracing/tracing_utils.rb +37 -34
- data/lib/labkit/tracing.rb +108 -5
- data/lib/labkit/user_experience_sli/null.rb +2 -0
- metadata +74 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/core_ext"
|
|
7
|
+
|
|
8
|
+
require "opentelemetry/sdk"
|
|
9
|
+
require "opentelemetry/exporter/otlp"
|
|
10
|
+
|
|
11
|
+
module Labkit
|
|
12
|
+
module Tracing
|
|
13
|
+
# OpenTelemetryFactory will configure OpenTelemetry distributed tracing
|
|
14
|
+
class OpenTelemetryFactory
|
|
15
|
+
OTLP_SCHEME = "otlp"
|
|
16
|
+
|
|
17
|
+
# The default endpoint for OTLP HTTP exporter
|
|
18
|
+
DEFAULT_HTTP_ENDPOINT = "http://localhost:4318/v1/traces"
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# @param service_name [String] The service name for the tracer
|
|
22
|
+
# @param connection_string [String] The connection string (e.g., "otlp://localhost:4318")
|
|
23
|
+
# @yield [config] Optional configuration block for OpenTelemetry SDK customization
|
|
24
|
+
# @yieldparam config [OpenTelemetry::SDK::Configurator] The SDK configurator
|
|
25
|
+
# @return [Tracer, nil] The configured tracer or nil if initialization fails
|
|
26
|
+
def create_tracer(service_name, connection_string, &)
|
|
27
|
+
return unless connection_string.present?
|
|
28
|
+
|
|
29
|
+
options = parse_otlp_connection_string(connection_string)
|
|
30
|
+
# The service_name parameter from GITLAB_TRACING takes precedence over the application one
|
|
31
|
+
service_name = options[:service_name] if options[:service_name]
|
|
32
|
+
|
|
33
|
+
# parse exporter headers as necessary
|
|
34
|
+
headers = build_headers(options)
|
|
35
|
+
|
|
36
|
+
# Get sampler and exporter from GITLAB_TRACING
|
|
37
|
+
sampler = get_sampler(options[:sampler], options[:sampler_param])
|
|
38
|
+
exporter = get_exporter(options[:http_endpoint], options[:udp_endpoint], headers)
|
|
39
|
+
|
|
40
|
+
# Build base resource
|
|
41
|
+
base_resource = OpenTelemetry::SDK::Resources::Resource.create(
|
|
42
|
+
OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => service_name
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
configure(service_name, base_resource, sampler, exporter, &)
|
|
46
|
+
|
|
47
|
+
extra_params = options.except(
|
|
48
|
+
:sampler,
|
|
49
|
+
:sampler_param,
|
|
50
|
+
:http_endpoint,
|
|
51
|
+
:udp_endpoint,
|
|
52
|
+
:strict_parsing,
|
|
53
|
+
:debug,
|
|
54
|
+
:service_name
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if extra_params.present?
|
|
58
|
+
message = "opentelemetry tracer: invalid option: #{extra_params.keys.join(', ')}"
|
|
59
|
+
|
|
60
|
+
raise message if options[:strict_parsing]
|
|
61
|
+
|
|
62
|
+
warn message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
OpenTelemetry.tracer_provider.tracer(service_name)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def configure(service_name, base_resource, sampler, exporter)
|
|
71
|
+
if block_given?
|
|
72
|
+
OpenTelemetry::SDK.configure do |c|
|
|
73
|
+
c.service_name = service_name
|
|
74
|
+
c.resource = base_resource
|
|
75
|
+
|
|
76
|
+
processor = create_span_processor(exporter)
|
|
77
|
+
c.add_span_processor(processor) if processor
|
|
78
|
+
|
|
79
|
+
yield(c)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# SDK.configure doesn't expose sampler configuration directly
|
|
83
|
+
# We need to replace the tracer provider to set the sampler
|
|
84
|
+
# This is a known limitation of the OpenTelemetry Ruby SDK
|
|
85
|
+
current_provider = OpenTelemetry.tracer_provider
|
|
86
|
+
return unless current_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
|
|
87
|
+
|
|
88
|
+
# Create new provider with sampler, preserving resource from SDK.configure
|
|
89
|
+
create_and_configure_provider(current_provider.resource, sampler, exporter)
|
|
90
|
+
else
|
|
91
|
+
create_and_configure_provider(base_resource, sampler, exporter)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_span_processor(exporter)
|
|
96
|
+
return nil unless exporter
|
|
97
|
+
|
|
98
|
+
if exporter.is_a?(OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter)
|
|
99
|
+
OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
|
|
100
|
+
else
|
|
101
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def create_and_configure_provider(resource, sampler, exporter)
|
|
106
|
+
tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new(
|
|
107
|
+
resource: resource,
|
|
108
|
+
sampler: sampler
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
processor = create_span_processor(exporter)
|
|
112
|
+
tracer_provider.add_span_processor(processor) if processor
|
|
113
|
+
|
|
114
|
+
OpenTelemetry.tracer_provider = tracer_provider
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_headers(options)
|
|
118
|
+
return {} unless options&.key?(:http_endpoint)
|
|
119
|
+
|
|
120
|
+
endpoint = options[:http_endpoint]
|
|
121
|
+
return {} unless endpoint
|
|
122
|
+
|
|
123
|
+
parsed = URI.parse(endpoint)
|
|
124
|
+
|
|
125
|
+
headers = {}
|
|
126
|
+
user = parsed.user
|
|
127
|
+
password = parsed.password
|
|
128
|
+
if user.present? && password.present?
|
|
129
|
+
credentials = Base64.strict_encode64("#{CGI.unescape(user)}:#{CGI.unescape(password)}")
|
|
130
|
+
headers["Authorization"] = "Basic #{credentials}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
headers
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get_sampler(sampler_type, sampler_param)
|
|
137
|
+
case sampler_type
|
|
138
|
+
when "probabilistic"
|
|
139
|
+
sampler_rate = sampler_param ? sampler_param.to_f : Factory::DEFAULT_PROBABILISTIC_RATE
|
|
140
|
+
OpenTelemetry::SDK::Trace::Samplers::TraceIdRatioBased.new(sampler_rate)
|
|
141
|
+
when "const"
|
|
142
|
+
if sampler_param == "1"
|
|
143
|
+
OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON
|
|
144
|
+
else
|
|
145
|
+
OpenTelemetry::SDK::Trace::Samplers::ALWAYS_OFF
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
OpenTelemetry::SDK::Trace::Samplers::TraceIdRatioBased.new(Factory::DEFAULT_PROBABILISTIC_RATE)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def get_exporter(http_endpoint, udp_endpoint, headers)
|
|
153
|
+
# OpenTelemetry doesn't support UDP, warn if specified
|
|
154
|
+
# https://github.com/open-telemetry/opentelemetry-collector/discussions/6016
|
|
155
|
+
warn "opentelemetry tracer: UDP endpoint not supported, ignoring udp_endpoint option" if udp_endpoint.present?
|
|
156
|
+
|
|
157
|
+
# Check for console exporter (for development/testing)
|
|
158
|
+
return get_console_exporter if http_endpoint&.include?("://console")
|
|
159
|
+
|
|
160
|
+
get_http_exporter(http_endpoint, headers) if http_endpoint.present?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def get_http_exporter(endpoint, headers)
|
|
164
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
165
|
+
endpoint: endpoint,
|
|
166
|
+
headers: headers
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def get_console_exporter
|
|
171
|
+
OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter.new
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_otlp_connection_string(connection_string)
|
|
175
|
+
parsed = URI.parse(connection_string)
|
|
176
|
+
|
|
177
|
+
# Parse query parameters for additional options
|
|
178
|
+
options = parse_query(parsed.query)
|
|
179
|
+
|
|
180
|
+
# Handle console exporter (special case - no endpoint needed)
|
|
181
|
+
if parsed.host == "console"
|
|
182
|
+
options[:http_endpoint] = "http://console"
|
|
183
|
+
return options
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
endpoint = build_otlp_endpoint(parsed)
|
|
187
|
+
options[:http_endpoint] = endpoint
|
|
188
|
+
|
|
189
|
+
options
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_otlp_endpoint(uri)
|
|
193
|
+
# Reconstruct the endpoint URL with scheme, host, port, and path
|
|
194
|
+
scheme = uri.scheme == OTLP_SCHEME ? "http" : uri.scheme
|
|
195
|
+
host = uri.host
|
|
196
|
+
port = uri.port
|
|
197
|
+
path = uri.path.empty? ? "" : uri.path
|
|
198
|
+
|
|
199
|
+
# Include userinfo (username:password) if present
|
|
200
|
+
userinfo = uri.userinfo ? "#{uri.userinfo}@" : ""
|
|
201
|
+
|
|
202
|
+
# Build the endpoint
|
|
203
|
+
endpoint = "#{scheme}://#{userinfo}#{host}"
|
|
204
|
+
endpoint += ":#{port}" if port
|
|
205
|
+
endpoint += path
|
|
206
|
+
|
|
207
|
+
endpoint
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def parse_query(query)
|
|
211
|
+
return {} unless query
|
|
212
|
+
|
|
213
|
+
CGI.parse(query).symbolize_keys.transform_values(&:first)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module Labkit
|
|
6
|
+
module Tracing
|
|
7
|
+
class OpenTracingFactory
|
|
8
|
+
OPENTRACING_SCHEME = "opentracing"
|
|
9
|
+
|
|
10
|
+
def self.create_tracer(service_name, connection_string)
|
|
11
|
+
return unless connection_string.present?
|
|
12
|
+
|
|
13
|
+
opentracing_details = parse_connection_string(connection_string)
|
|
14
|
+
driver_name = opentracing_details[:driver_name]
|
|
15
|
+
|
|
16
|
+
case driver_name
|
|
17
|
+
when "jaeger"
|
|
18
|
+
JaegerFactory.create_tracer(service_name, opentracing_details[:options])
|
|
19
|
+
else
|
|
20
|
+
raise "Unknown driver: #{driver_name}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.parse_connection_string(connection_string)
|
|
25
|
+
parsed = URI.parse(connection_string)
|
|
26
|
+
|
|
27
|
+
raise "Invalid tracing connection string" unless valid_uri?(parsed)
|
|
28
|
+
|
|
29
|
+
{ driver_name: parsed.host, options: parse_query(parsed.query) }
|
|
30
|
+
end
|
|
31
|
+
private_class_method :parse_connection_string
|
|
32
|
+
|
|
33
|
+
def self.parse_query(query)
|
|
34
|
+
return {} unless query
|
|
35
|
+
|
|
36
|
+
CGI.parse(query).symbolize_keys.transform_values(&:first)
|
|
37
|
+
end
|
|
38
|
+
private_class_method :parse_query
|
|
39
|
+
|
|
40
|
+
def self.valid_uri?(uri)
|
|
41
|
+
return false unless uri
|
|
42
|
+
|
|
43
|
+
uri.scheme == OPENTRACING_SCHEME && uri.host.to_s =~ /^[a-z0-9_]+$/ && uri.path.empty?
|
|
44
|
+
end
|
|
45
|
+
private_class_method :valid_uri?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -18,7 +18,7 @@ module Labkit
|
|
|
18
18
|
def call(env)
|
|
19
19
|
method = env[REQUEST_METHOD]
|
|
20
20
|
|
|
21
|
-
context = TracingUtils.tracer.
|
|
21
|
+
context = TracingUtils.tracer.extract_context(env, format: OpenTracing::FORMAT_RACK)
|
|
22
22
|
tags = { "component" => "rack", "span.kind" => "server", "http.method" => method, "http.url" => self.class.build_sanitized_url_from_env(env) }
|
|
23
23
|
|
|
24
24
|
TracingUtils.with_tracing(operation_name: "http:#{method}", child_of: context, tags: tags) do |span|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labkit
|
|
4
|
+
module Tracing
|
|
5
|
+
class Railtie < ::Rails::Railtie
|
|
6
|
+
initializer "labkit.tracing.insert_middleware", after: :load_config_initializers do |app|
|
|
7
|
+
next unless Labkit::Tracing.enabled?
|
|
8
|
+
|
|
9
|
+
# Insert after Labkit::Middleware::Rack for proper correlation ID propagation.
|
|
10
|
+
app.middleware.insert_after Labkit::Middleware::Rack, Labkit::Tracing::RackMiddleware
|
|
11
|
+
::Rails.logger.info "Labkit::Tracing::RackMiddleware automatically inserted after Labkit::Middleware::Rack"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -10,64 +10,67 @@ module Labkit
|
|
|
10
10
|
class TracingUtils
|
|
11
11
|
# Convience method for running a block with a span
|
|
12
12
|
def self.with_tracing(operation_name:, tags:, child_of: nil)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
tracer.in_span(operation_name, child_of: child_of, tags: tags) do |span|
|
|
14
|
+
log_common_fields_on_span(span, operation_name)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
raise e
|
|
23
|
-
ensure
|
|
24
|
-
scope.close
|
|
16
|
+
begin
|
|
17
|
+
yield span
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
log_exception_on_span(span, e)
|
|
20
|
+
raise e
|
|
21
|
+
end
|
|
25
22
|
end
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
# Obtain a tracer instance
|
|
26
|
+
#
|
|
27
|
+
# Returns the appropriate tracer based on connection string configuration:
|
|
28
|
+
# - OpenTelemetry (OTLP): When GITLAB_TRACING uses otlp:// scheme
|
|
29
|
+
# - OpenTracing: When GITLAB_TRACING uses opentracing:// scheme or not set
|
|
30
|
+
#
|
|
31
|
+
# For OpenTelemetry, the tracer's instrumentation scope name is determined by:
|
|
32
|
+
# - The service_name passed to Factory.create_tracer
|
|
33
|
+
# - Falls back to DEFAULT_SERVICE_NAME if Factory was not used
|
|
34
|
+
#
|
|
35
|
+
# Both OpenTelemetry and OpenTracing provide no-op tracers by default:
|
|
36
|
+
# - OpenTelemetry: Uses ProxyTracerProvider until SDK is configured
|
|
37
|
+
# - OpenTracing: Uses default no-op global_tracer
|
|
38
|
+
#
|
|
39
|
+
# This allows spans to be created but not exported when tracing is not initialized,
|
|
40
|
+
# letting the application run safely while producing no trace data.
|
|
29
41
|
def self.tracer
|
|
30
|
-
|
|
42
|
+
@tracer ||=
|
|
43
|
+
if Tracing.otlp_connection?
|
|
44
|
+
require "opentelemetry/sdk"
|
|
45
|
+
|
|
46
|
+
otel_tracer = OpenTelemetry.tracer_provider.tracer(Tracing.configured_service_name)
|
|
47
|
+
Adapters::OpentelemetryTracer.new(otel_tracer)
|
|
48
|
+
else
|
|
49
|
+
Adapters::OpentracingTracer.new(OpenTracing.global_tracer)
|
|
50
|
+
end
|
|
31
51
|
end
|
|
32
52
|
|
|
33
53
|
# Generate a span retrospectively
|
|
34
54
|
def self.postnotify_span(operation_name, start_time, end_time, tags: nil, child_of: nil, exception: nil)
|
|
35
|
-
span =
|
|
55
|
+
span = tracer.start_span(operation_name, child_of: child_of, tags: tags, start_time: start_time)
|
|
36
56
|
|
|
37
57
|
log_common_fields_on_span(span, operation_name)
|
|
38
58
|
log_exception_on_span(span, exception) if exception
|
|
39
59
|
|
|
40
|
-
span.finish(
|
|
60
|
+
span.finish(end_timestamp: end_time)
|
|
41
61
|
end
|
|
42
62
|
|
|
43
63
|
# Add common fields to a span
|
|
44
64
|
def self.log_common_fields_on_span(span, operation_name)
|
|
45
65
|
correlation_id = Labkit::Correlation::CorrelationId.current_id
|
|
66
|
+
|
|
46
67
|
span.set_tag("correlation_id", correlation_id) if correlation_id
|
|
47
|
-
span.
|
|
68
|
+
span.log_event("stack", stack: caller.join('\n')) if include_stacktrace?(operation_name)
|
|
48
69
|
end
|
|
49
70
|
|
|
50
71
|
# Add exception logging to a span
|
|
51
72
|
def self.log_exception_on_span(span, exception)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
span.set_tag("error", true)
|
|
55
|
-
span.log_kv(**kv_tags_for_exception(exception))
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Generate key-value tags for an exception
|
|
59
|
-
def self.kv_tags_for_exception(exception)
|
|
60
|
-
case exception
|
|
61
|
-
when Exception
|
|
62
|
-
{
|
|
63
|
-
:"event" => "error",
|
|
64
|
-
:"error.kind" => exception.class.to_s,
|
|
65
|
-
:"message" => Labkit::Logging::Sanitizer.sanitize_field(exception.message),
|
|
66
|
-
:"stack" => exception.backtrace&.join('\n'),
|
|
67
|
-
}
|
|
68
|
-
else
|
|
69
|
-
{ :"event" => "error", :"error.kind" => exception.class.to_s, :"error.object" => Labkit::Logging::Sanitizer.sanitize_field(exception.to_s) }
|
|
70
|
-
end
|
|
73
|
+
span.set_error(exception)
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
def self.include_stacktrace?(operation_name)
|
data/lib/labkit/tracing.rb
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
4
|
+
|
|
3
5
|
module Labkit
|
|
4
6
|
# Tracing provides distributed tracing functionality
|
|
5
7
|
module Tracing
|
|
6
8
|
autoload :AbstractInstrumenter, "labkit/tracing/abstract_instrumenter"
|
|
9
|
+
autoload :AutoInitialize, "labkit/tracing/auto_initialize"
|
|
7
10
|
autoload :TracingCommon, "labkit/tracing/tracing_common"
|
|
8
11
|
autoload :Factory, "labkit/tracing/factory"
|
|
9
12
|
autoload :GRPC, "labkit/tracing/grpc"
|
|
10
13
|
autoload :GRPCInterceptor, "labkit/tracing/grpc_interceptor" # Deprecated
|
|
11
14
|
autoload :JaegerFactory, "labkit/tracing/jaeger_factory"
|
|
15
|
+
autoload :OpenTelemetryFactory, "labkit/tracing/open_telemetry_factory"
|
|
16
|
+
autoload :OpenTracingFactory, "labkit/tracing/open_tracing_factory"
|
|
12
17
|
autoload :RackMiddleware, "labkit/tracing/rack_middleware"
|
|
13
18
|
autoload :Rails, "labkit/tracing/rails"
|
|
14
19
|
autoload :Redis, "labkit/tracing/redis"
|
|
@@ -16,6 +21,21 @@ module Labkit
|
|
|
16
21
|
autoload :Sidekiq, "labkit/tracing/sidekiq"
|
|
17
22
|
autoload :TracingUtils, "labkit/tracing/tracing_utils"
|
|
18
23
|
|
|
24
|
+
module Adapters
|
|
25
|
+
autoload :BaseSpan, "labkit/tracing/adapters/base_span"
|
|
26
|
+
autoload :OpentelemetrySpan, "labkit/tracing/adapters/opentelemetry_span"
|
|
27
|
+
autoload :OpentracingSpan, "labkit/tracing/adapters/opentracing_span"
|
|
28
|
+
autoload :BaseTracer, "labkit/tracing/adapters/base_tracer"
|
|
29
|
+
autoload :OpentelemetryTracer, "labkit/tracing/adapters/opentelemetry_tracer"
|
|
30
|
+
autoload :OpentracingTracer, "labkit/tracing/adapters/opentracing_tracer"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
DEFAULT_SERVICE_NAME = :'labkit-service'
|
|
34
|
+
|
|
35
|
+
# Module-level attribute for storing the configured service name
|
|
36
|
+
# Set by Factory.create_tracer when a tracer is created
|
|
37
|
+
mattr_accessor :configured_service_name, default: DEFAULT_SERVICE_NAME
|
|
38
|
+
|
|
19
39
|
# Tracing is only enabled when the `GITLAB_TRACING` env var is configured.
|
|
20
40
|
def self.enabled?
|
|
21
41
|
connection_string.present?
|
|
@@ -25,13 +45,28 @@ module Labkit
|
|
|
25
45
|
ENV["GITLAB_TRACING"]
|
|
26
46
|
end
|
|
27
47
|
|
|
48
|
+
def self.otlp_connection?(connection_string = ENV["GITLAB_TRACING"])
|
|
49
|
+
connection_string.to_s.start_with?("#{OpenTelemetryFactory::OTLP_SCHEME}://")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.opentracing_connection?(connection_string = ENV["GITLAB_TRACING"])
|
|
53
|
+
connection_string.to_s.start_with?("#{OpenTracingFactory::OPENTRACING_SCHEME}://")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the tracing URL template from the GITLAB_TRACING_URL environment variable.
|
|
57
|
+
#
|
|
58
|
+
# @note This method is only useful with OpenTracing and Jaeger. It does not work with
|
|
59
|
+
# OpenTelemetry backends, as they expect trace_id (W3C standard) rather than
|
|
60
|
+
# correlation_id (GitLab-specific) for trace lookups.
|
|
61
|
+
#
|
|
62
|
+
# @return [String, nil] The URL template with placeholders for {{ correlation_id }} and {{ service }}
|
|
28
63
|
def self.tracing_url_template
|
|
29
64
|
ENV["GITLAB_TRACING_URL"]
|
|
30
65
|
end
|
|
31
66
|
|
|
32
67
|
# Check if the current request is being traced.
|
|
33
68
|
def self.sampled?
|
|
34
|
-
context =
|
|
69
|
+
context = TracingUtils.tracer.active_span&.context
|
|
35
70
|
context&.respond_to?(:sampled?) && context&.sampled?
|
|
36
71
|
end
|
|
37
72
|
|
|
@@ -39,12 +74,36 @@ module Labkit
|
|
|
39
74
|
@stacktrace_operations ||= Set.new(ENV["GITLAB_TRACING_INCLUDE_STACKTRACE"].to_s.split(",").map(&:strip))
|
|
40
75
|
end
|
|
41
76
|
|
|
77
|
+
# Checks if tracing URL generation is enabled.
|
|
78
|
+
#
|
|
79
|
+
# @note This method is only useful with OpenTracing and Jaeger. It does not work with
|
|
80
|
+
# OpenTelemetry backends, as they expect trace_id (W3C standard) rather than
|
|
81
|
+
# correlation_id (GitLab-specific) for trace lookups.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean] true if both tracing and URL template are configured
|
|
84
|
+
# @see tracing_url
|
|
42
85
|
def self.tracing_url_enabled?
|
|
43
86
|
enabled? && tracing_url_template.present?
|
|
44
87
|
end
|
|
45
88
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
89
|
+
# Generates a URL to view the current trace in a tracing UI.
|
|
90
|
+
#
|
|
91
|
+
# This method substitutes {{ correlation_id }} and {{ service }} placeholders in the
|
|
92
|
+
# GITLAB_TRACING_URL template with the current correlation ID and provided service name.
|
|
93
|
+
#
|
|
94
|
+
# @note This method is only useful with OpenTracing and Jaeger. It does not work with
|
|
95
|
+
# OpenTelemetry backends because:
|
|
96
|
+
# - Uses correlation_id (GitLab-specific request ID) instead of trace_id (W3C standard)
|
|
97
|
+
# - Modern tracing UIs (Jaeger with OTLP, Grafana Tempo, etc.) expect trace_id for lookups
|
|
98
|
+
# - Only Jaeger's legacy OpenTracing integration supports correlation_id-based URLs
|
|
99
|
+
#
|
|
100
|
+
# @param service_name [String] The name of the service to include in the URL
|
|
101
|
+
# @return [String, nil] The generated URL, or nil if tracing URL is not enabled
|
|
102
|
+
#
|
|
103
|
+
# @example With OpenTracing/Jaeger (supported)
|
|
104
|
+
# ENV["GITLAB_TRACING_URL"] = "https://jaeger.example.com/trace?correlationId={{ correlation_id }}"
|
|
105
|
+
# Labkit::Tracing.tracing_url("my-service")
|
|
106
|
+
# # => "https://jaeger.example.com/trace?correlationId=abc123"
|
|
48
107
|
def self.tracing_url(service_name)
|
|
49
108
|
return unless tracing_url_enabled?
|
|
50
109
|
|
|
@@ -53,8 +112,50 @@ module Labkit
|
|
|
53
112
|
# Avoid using `format` since it can throw TypeErrors
|
|
54
113
|
# which we want to avoid on unsanitised env var input
|
|
55
114
|
tracing_url_template.to_s
|
|
56
|
-
|
|
57
|
-
|
|
115
|
+
.gsub("{{ correlation_id }}", correlation_id)
|
|
116
|
+
.gsub("{{ service }}", service_name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns the underlying tracer implementation from the tracing library in use.
|
|
120
|
+
# This provides direct access to the native tracer API when LabKit's abstraction
|
|
121
|
+
# is insufficient for advanced use cases.
|
|
122
|
+
#
|
|
123
|
+
# @example Using OpenTelemetry-specific APIs
|
|
124
|
+
# tracer = Labkit::Tracing.tracer
|
|
125
|
+
# tracer.in_span("custom-span") do |span|
|
|
126
|
+
# span.add_event("custom-event", attributes: { "key" => "value" })
|
|
127
|
+
# end
|
|
128
|
+
#
|
|
129
|
+
# @return [Object] The tracer implementation:
|
|
130
|
+
# - OpenTelemetry::SDK::Trace::Tracer when using OTLP connection
|
|
131
|
+
# - No-op tracer when tracing is not configured
|
|
132
|
+
def self.tracer
|
|
133
|
+
TracingUtils.tracer.tracer
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns the currently active span from OpenTelemetry.
|
|
137
|
+
# This provides direct access to the OpenTelemetry span API.
|
|
138
|
+
#
|
|
139
|
+
# @example Adding attributes to the current span
|
|
140
|
+
# span = Labkit::Tracing.current_span
|
|
141
|
+
# span.set_attribute("user.id", user.id) if span.recording?
|
|
142
|
+
#
|
|
143
|
+
# @example Adding events
|
|
144
|
+
# span = Labkit::Tracing.current_span
|
|
145
|
+
# span.add_event("cache_miss", attributes: { "key" => cache_key })
|
|
146
|
+
#
|
|
147
|
+
# @example Conditional expensive operations
|
|
148
|
+
# span = Labkit::Tracing.current_span
|
|
149
|
+
# if span.recording?
|
|
150
|
+
# span.set_attribute("expensive_data", compute_expensive_data)
|
|
151
|
+
# end
|
|
152
|
+
#
|
|
153
|
+
# @return [OpenTelemetry::Trace::Span, nil] The current span (may be a no-op span when tracing is disabled, or nil when using OpenTracing)
|
|
154
|
+
def self.current_span
|
|
155
|
+
return nil if opentracing_connection?
|
|
156
|
+
|
|
157
|
+
require "opentelemetry/sdk"
|
|
158
|
+
OpenTelemetry::Trace.current_span
|
|
58
159
|
end
|
|
59
160
|
|
|
60
161
|
# This will run a block with a span
|
|
@@ -68,3 +169,5 @@ module Labkit
|
|
|
68
169
|
end
|
|
69
170
|
end
|
|
70
171
|
end
|
|
172
|
+
|
|
173
|
+
require "labkit/tracing/railtie" if defined?(Rails::Railtie)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-labkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Newdigate
|
|
@@ -131,6 +131,48 @@ dependencies:
|
|
|
131
131
|
- - "~>"
|
|
132
132
|
- !ruby/object:Gem::Version
|
|
133
133
|
version: 3.3.2
|
|
134
|
+
- !ruby/object:Gem::Dependency
|
|
135
|
+
name: opentelemetry-sdk
|
|
136
|
+
requirement: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - "~>"
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '1.10'
|
|
141
|
+
type: :runtime
|
|
142
|
+
prerelease: false
|
|
143
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - "~>"
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: '1.10'
|
|
148
|
+
- !ruby/object:Gem::Dependency
|
|
149
|
+
name: opentelemetry-instrumentation-all
|
|
150
|
+
requirement: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - "~>"
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: 0.89.1
|
|
155
|
+
type: :runtime
|
|
156
|
+
prerelease: false
|
|
157
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
158
|
+
requirements:
|
|
159
|
+
- - "~>"
|
|
160
|
+
- !ruby/object:Gem::Version
|
|
161
|
+
version: 0.89.1
|
|
162
|
+
- !ruby/object:Gem::Dependency
|
|
163
|
+
name: opentelemetry-exporter-otlp
|
|
164
|
+
requirement: !ruby/object:Gem::Requirement
|
|
165
|
+
requirements:
|
|
166
|
+
- - "~>"
|
|
167
|
+
- !ruby/object:Gem::Version
|
|
168
|
+
version: 0.31.1
|
|
169
|
+
type: :runtime
|
|
170
|
+
prerelease: false
|
|
171
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
172
|
+
requirements:
|
|
173
|
+
- - "~>"
|
|
174
|
+
- !ruby/object:Gem::Version
|
|
175
|
+
version: 0.31.1
|
|
134
176
|
- !ruby/object:Gem::Dependency
|
|
135
177
|
name: opentracing
|
|
136
178
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -359,6 +401,26 @@ dependencies:
|
|
|
359
401
|
- - "~>"
|
|
360
402
|
- !ruby/object:Gem::Version
|
|
361
403
|
version: '2.0'
|
|
404
|
+
- !ruby/object:Gem::Dependency
|
|
405
|
+
name: railties
|
|
406
|
+
requirement: !ruby/object:Gem::Requirement
|
|
407
|
+
requirements:
|
|
408
|
+
- - ">="
|
|
409
|
+
- !ruby/object:Gem::Version
|
|
410
|
+
version: 5.0.0
|
|
411
|
+
- - "<"
|
|
412
|
+
- !ruby/object:Gem::Version
|
|
413
|
+
version: 8.1.0
|
|
414
|
+
type: :development
|
|
415
|
+
prerelease: false
|
|
416
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
417
|
+
requirements:
|
|
418
|
+
- - ">="
|
|
419
|
+
- !ruby/object:Gem::Version
|
|
420
|
+
version: 5.0.0
|
|
421
|
+
- - "<"
|
|
422
|
+
- !ruby/object:Gem::Version
|
|
423
|
+
version: 8.1.0
|
|
362
424
|
- !ruby/object:Gem::Dependency
|
|
363
425
|
name: rake
|
|
364
426
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -540,7 +602,15 @@ files:
|
|
|
540
602
|
- lib/labkit/rspec/matchers/user_experience_matchers.rb
|
|
541
603
|
- lib/labkit/system.rb
|
|
542
604
|
- lib/labkit/tracing.rb
|
|
605
|
+
- lib/labkit/tracing/README.md
|
|
543
606
|
- lib/labkit/tracing/abstract_instrumenter.rb
|
|
607
|
+
- lib/labkit/tracing/adapters/base_span.rb
|
|
608
|
+
- lib/labkit/tracing/adapters/base_tracer.rb
|
|
609
|
+
- lib/labkit/tracing/adapters/opentelemetry_span.rb
|
|
610
|
+
- lib/labkit/tracing/adapters/opentelemetry_tracer.rb
|
|
611
|
+
- lib/labkit/tracing/adapters/opentracing_span.rb
|
|
612
|
+
- lib/labkit/tracing/adapters/opentracing_tracer.rb
|
|
613
|
+
- lib/labkit/tracing/auto_initialize.rb
|
|
544
614
|
- lib/labkit/tracing/external_http.rb
|
|
545
615
|
- lib/labkit/tracing/external_http/request_instrumenter.rb
|
|
546
616
|
- lib/labkit/tracing/factory.rb
|
|
@@ -549,6 +619,8 @@ files:
|
|
|
549
619
|
- lib/labkit/tracing/grpc/server_interceptor.rb
|
|
550
620
|
- lib/labkit/tracing/grpc_interceptor.rb
|
|
551
621
|
- lib/labkit/tracing/jaeger_factory.rb
|
|
622
|
+
- lib/labkit/tracing/open_telemetry_factory.rb
|
|
623
|
+
- lib/labkit/tracing/open_tracing_factory.rb
|
|
552
624
|
- lib/labkit/tracing/rack_middleware.rb
|
|
553
625
|
- lib/labkit/tracing/rails.rb
|
|
554
626
|
- lib/labkit/tracing/rails/action_view.rb
|
|
@@ -566,6 +638,7 @@ files:
|
|
|
566
638
|
- lib/labkit/tracing/rails/active_support/cache_read_instrumenter.rb
|
|
567
639
|
- lib/labkit/tracing/rails/active_support/cache_write_instrumenter.rb
|
|
568
640
|
- lib/labkit/tracing/rails/active_support/subscriber.rb
|
|
641
|
+
- lib/labkit/tracing/railtie.rb
|
|
569
642
|
- lib/labkit/tracing/redis.rb
|
|
570
643
|
- lib/labkit/tracing/redis/redis_interceptor.rb
|
|
571
644
|
- lib/labkit/tracing/redis/redis_interceptor_helper.rb
|