Commit ccddf45d authored by Andrew Newdigate's avatar Andrew Newdigate

Migrate correlation and tracing code to LabKit

This change is a fairly straightforward refactor to extract the tracing
and correlation-id code from the gitlab rails codebase into the new
LabKit-Ruby project.

The corresponding import into LabKit-Ruby was in
https://gitlab.com/gitlab-org/labkit-ruby/merge_requests/1

The code itself remains very similar for now.

Extracting it allows us to reuse it in other projects, such as
Gitaly-Ruby. This will give us the advantages of correlation-ids and
distributed tracing in that project too.
parent 14fa660f
...@@ -286,6 +286,9 @@ gem 'sentry-raven', '~> 2.7' ...@@ -286,6 +286,9 @@ gem 'sentry-raven', '~> 2.7'
gem 'premailer-rails', '~> 1.9.7' gem 'premailer-rails', '~> 1.9.7'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '~> 0.1.2'
# I18n # I18n
gem 'ruby_parser', '~> 3.8', require: false gem 'ruby_parser', '~> 3.8', require: false
gem 'rails-i18n', '~> 5.1' gem 'rails-i18n', '~> 5.1'
...@@ -316,12 +319,6 @@ group :metrics do ...@@ -316,12 +319,6 @@ group :metrics do
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
group :tracing do
# OpenTracing
gem 'opentracing', '~> 0.4.3'
gem 'jaeger-client', '~> 0.10.0'
end
group :development do group :development do
gem 'foreman', '~> 0.84.0' gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 4.2', require: false gem 'brakeman', '~> 4.2', require: false
......
...@@ -312,6 +312,12 @@ GEM ...@@ -312,6 +312,12 @@ GEM
gitlab-default_value_for (3.1.1) gitlab-default_value_for (3.1.1)
activerecord (>= 3.2.0, < 6.0) activerecord (>= 3.2.0, < 6.0)
gitlab-license (1.0.0) gitlab-license (1.0.0)
gitlab-labkit (0.1.2)
actionpack (~> 5)
activesupport (~> 5)
grpc (~> 1.15)
jaeger-client (~> 0.10)
opentracing (~> 0.4)
gitlab-markup (1.7.0) gitlab-markup (1.7.0)
gitlab-sidekiq-fetcher (0.4.0) gitlab-sidekiq-fetcher (0.4.0)
sidekiq (~> 5) sidekiq (~> 5)
...@@ -602,7 +608,7 @@ GEM ...@@ -602,7 +608,7 @@ GEM
validate_email validate_email
validate_url validate_url
webfinger (>= 1.0.1) webfinger (>= 1.0.1)
opentracing (0.4.3) opentracing (0.5.0)
optimist (3.0.0) optimist (3.0.0)
org-ruby (0.9.12) org-ruby (0.9.12)
rubypants (~> 0.2) rubypants (~> 0.2)
...@@ -1089,6 +1095,7 @@ DEPENDENCIES ...@@ -1089,6 +1095,7 @@ DEPENDENCIES
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1) gitlab-default_value_for (~> 3.1.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
gitlab-labkit (~> 0.1.2)
gitlab-markup (~> 1.7.0) gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (~> 0.4.0) gitlab-sidekiq-fetcher (~> 0.4.0)
gitlab-styles (~> 2.5) gitlab-styles (~> 2.5)
...@@ -1116,7 +1123,6 @@ DEPENDENCIES ...@@ -1116,7 +1123,6 @@ DEPENDENCIES
httparty (~> 0.16.4) httparty (~> 0.16.4)
icalendar icalendar
influxdb (~> 0.2) influxdb (~> 0.2)
jaeger-client (~> 0.10.0)
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
js_regex (~> 3.1) js_regex (~> 3.1)
json-schema (~> 2.8.0) json-schema (~> 2.8.0)
...@@ -1159,7 +1165,6 @@ DEPENDENCIES ...@@ -1159,7 +1165,6 @@ DEPENDENCIES
omniauth-twitter (~> 1.4) omniauth-twitter (~> 1.4)
omniauth-ultraauth (~> 0.0.1) omniauth-ultraauth (~> 0.0.1)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
opentracing (~> 0.4.3)
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
peek (~> 1.0.1) peek (~> 1.0.1)
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
......
...@@ -128,7 +128,7 @@ class ApplicationController < ActionController::Base ...@@ -128,7 +128,7 @@ class ApplicationController < ActionController::Base
payload[:ua] = request.env["HTTP_USER_AGENT"] payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip payload[:remote_ip] = request.remote_ip
payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
logged_user = auth_user logged_user = auth_user
......
---
title: Migrate correlation and tracing code to LabKit
merge_request: 25379
author:
type: other
...@@ -35,7 +35,7 @@ unless Sidekiq.server? ...@@ -35,7 +35,7 @@ unless Sidekiq.server?
end end
payload[:response] = event.payload[:response] if event.payload[:response] payload[:response] = event.payload[:response] if event.payload[:response]
payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
payload payload
end end
......
...@@ -19,7 +19,7 @@ Peek.into Peek::Views::Gitaly ...@@ -19,7 +19,7 @@ Peek.into Peek::Views::Gitaly
Peek.into Peek::Views::Rblineprof Peek.into Peek::Views::Rblineprof
Peek.into Peek::Views::Redis Peek.into Peek::Views::Redis
Peek.into Peek::Views::GC Peek.into Peek::Views::GC
Peek.into Peek::Views::Tracing if Gitlab::Tracing.tracing_url_enabled? Peek.into Peek::Views::Tracing if Labkit::Tracing.tracing_url_enabled?
# rubocop:disable Naming/ClassAndModuleCamelCase # rubocop:disable Naming/ClassAndModuleCamelCase
class PEEK_DB_CLIENT class PEEK_DB_CLIENT
......
# frozen_string_literal: true # frozen_string_literal: true
if Gitlab::Tracing.enabled? if Labkit::Tracing.enabled?
require 'opentracing'
Rails.application.configure do |config| Rails.application.configure do |config|
config.middleware.insert_after Gitlab::Middleware::CorrelationId, ::Gitlab::Tracing::RackMiddleware config.middleware.insert_after Gitlab::Middleware::CorrelationId, ::Labkit::Tracing::RackMiddleware
end end
# Instrument the Sidekiq client # Instrument the Sidekiq client
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.client_middleware do |chain| config.client_middleware do |chain|
chain.add Gitlab::Tracing::Sidekiq::ClientMiddleware chain.add Labkit::Tracing::Sidekiq::ClientMiddleware
end end
end end
...@@ -18,20 +16,20 @@ if Gitlab::Tracing.enabled? ...@@ -18,20 +16,20 @@ if Gitlab::Tracing.enabled?
if Sidekiq.server? if Sidekiq.server?
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.server_middleware do |chain| config.server_middleware do |chain|
chain.add Gitlab::Tracing::Sidekiq::ServerMiddleware chain.add Labkit::Tracing::Sidekiq::ServerMiddleware
end end
end end
end end
# Instrument Rails # Instrument Rails
Gitlab::Tracing::Rails::ActiveRecordSubscriber.instrument Labkit::Tracing::Rails::ActiveRecordSubscriber.instrument
Gitlab::Tracing::Rails::ActionViewSubscriber.instrument Labkit::Tracing::Rails::ActionViewSubscriber.instrument
# In multi-processed clustered architectures (puma, unicorn) don't # In multi-processed clustered architectures (puma, unicorn) don't
# start tracing until the worker processes are spawned. This works # start tracing until the worker processes are spawned. This works
# around issues when the opentracing implementation spawns threads # around issues when the opentracing implementation spawns threads
Gitlab::Cluster::LifecycleEvents.on_worker_start do Gitlab::Cluster::LifecycleEvents.on_worker_start do
tracer = Gitlab::Tracing::Factory.create_tracer(Gitlab.process_name, Gitlab::Tracing.connection_string) tracer = Labkit::Tracing::Factory.create_tracer(Gitlab.process_name, Labkit::Tracing.connection_string)
OpenTracing.global_tracer = tracer if tracer OpenTracing.global_tracer = tracer if tracer
end end
end end
...@@ -52,9 +52,9 @@ module Gitlab ...@@ -52,9 +52,9 @@ module Gitlab
end end
def self.interceptors def self.interceptors
return [] unless Gitlab::Tracing.enabled? return [] unless Labkit::Tracing.enabled?
[Gitlab::Tracing::GRPCInterceptor.instance] [Labkit::Tracing::GRPCInterceptor.instance]
end end
private_class_method :interceptors private_class_method :interceptors
...@@ -218,7 +218,7 @@ module Gitlab ...@@ -218,7 +218,7 @@ module Gitlab
feature = feature_stack && feature_stack[0] feature = feature_stack && feature_stack[0]
metadata['call_site'] = feature.to_s if feature metadata['call_site'] = feature.to_s if feature
metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
metadata['x-gitlab-correlation-id'] = Gitlab::CorrelationId.current_id if Gitlab::CorrelationId.current_id metadata['x-gitlab-correlation-id'] = Labkit::Correlation::CorrelationId.current_id if Labkit::Correlation::CorrelationId.current_id
metadata.merge!(server_feature_flags) metadata.merge!(server_feature_flags)
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
module Loggers module Loggers
class CorrelationIdLogger < ::GrapeLogging::Loggers::Base class CorrelationIdLogger < ::GrapeLogging::Loggers::Base
def parameters(_, _) def parameters(_, _)
{ Gitlab::CorrelationId::LOG_KEY => Gitlab::CorrelationId.current_id } { Labkit::Correlation::CorrelationId::LOG_KEY => Labkit::Correlation::CorrelationId.current_id }
end end
end end
end end
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
data = {} data = {}
data[:severity] = severity data[:severity] = severity
data[:time] = timestamp.utc.iso8601(3) data[:time] = timestamp.utc.iso8601(3)
data[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id data[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
case message case message
when String when String
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
end end
def call(env) def call(env)
::Gitlab::CorrelationId.use_id(correlation_id(env)) do ::Labkit::Correlation::CorrelationId.use_id(correlation_id(env)) do
@app.call(env) @app.call(env)
end end
end end
......
...@@ -45,7 +45,7 @@ module Gitlab ...@@ -45,7 +45,7 @@ module Gitlab
context # Make sure we've set everything we know in the context context # Make sure we've set everything we know in the context
tags = { tags = {
Gitlab::CorrelationId::LOG_KEY.to_sym => Gitlab::CorrelationId.current_id Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id
} }
Raven.capture_exception(exception, tags: tags, extra: extra) Raven.capture_exception(exception, tags: tags, extra: extra)
......
...@@ -4,8 +4,8 @@ module Gitlab ...@@ -4,8 +4,8 @@ module Gitlab
module SidekiqMiddleware module SidekiqMiddleware
class CorrelationInjector class CorrelationInjector
def call(worker_class, job, queue, redis_pool) def call(worker_class, job, queue, redis_pool)
job[Gitlab::CorrelationId::LOG_KEY] ||= job[Labkit::Correlation::CorrelationId::LOG_KEY] ||=
Gitlab::CorrelationId.current_or_new_id Labkit::Correlation::CorrelationId.current_or_new_id
yield yield
end end
......
...@@ -4,9 +4,9 @@ module Gitlab ...@@ -4,9 +4,9 @@ module Gitlab
module SidekiqMiddleware module SidekiqMiddleware
class CorrelationLogger class CorrelationLogger
def call(worker, job, queue) def call(worker, job, queue)
correlation_id = job[Gitlab::CorrelationId::LOG_KEY] correlation_id = job[Labkit::Correlation::CorrelationId::LOG_KEY]
Gitlab::CorrelationId.use_id(correlation_id) do Labkit::Correlation::CorrelationId.use_id(correlation_id) do
yield yield
end end
end end
......
# frozen_string_literal: true
require 'opentracing'
module Gitlab
module Tracing
module Common
def tracer
OpenTracing.global_tracer
end
# Convience method for running a block with a span
def in_tracing_span(operation_name:, tags:, child_of: nil)
scope = tracer.start_active_span(
operation_name,
child_of: child_of,
tags: tags
)
span = scope.span
# Add correlation details to the span if we have them
correlation_id = Gitlab::CorrelationId.current_id
if correlation_id
span.set_tag('correlation_id', correlation_id)
end
begin
yield span
rescue => e
log_exception_on_span(span, e)
raise e
ensure
scope.close
end
end
def postnotify_span(operation_name, start_time, end_time, tags: nil, child_of: nil, exception: nil)
span = OpenTracing.start_span(operation_name, start_time: start_time, tags: tags, child_of: child_of)
log_exception_on_span(span, exception) if exception
span.finish(end_time: end_time)
end
def log_exception_on_span(span, exception)
span.set_tag('error', true)
span.log_kv(kv_tags_for_exception(exception))
end
def kv_tags_for_exception(exception)
case exception
when Exception
{
'event': 'error',
'error.kind': exception.class.to_s,
'message': Gitlab::UrlSanitizer.sanitize(exception.message),
'stack': exception.backtrace&.join("\n")
}
else
{
'event': 'error',
'error.kind': exception.class.to_s,
'error.object': Gitlab::UrlSanitizer.sanitize(exception.to_s)
}
end
end
end
end
end
# frozen_string_literal: true
require "cgi"
module Gitlab
module Tracing
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])
else
raise "Unknown driver: #{driver_name}"
end
rescue => e
# Can't create the tracer? Warn and continue sans tracer
warn "Unable to instantiate tracer: #{e}"
nil
end
end
def self.parse_connection_string(connection_string)
parsed = URI.parse(connection_string)
unless valid_uri?(parsed)
raise "Invalid tracing connection string"
end
{
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
# frozen_string_literal: true
require 'opentracing'
require 'grpc'
module Gitlab
module Tracing
class GRPCInterceptor < GRPC::ClientInterceptor
include Common
include Singleton
def request_response(request:, call:, method:, metadata:)
wrap_with_tracing(method, 'unary', metadata) do
yield
end
end
def client_streamer(requests:, call:, method:, metadata:)
wrap_with_tracing(method, 'client_stream', metadata) do
yield
end
end
def server_streamer(request:, call:, method:, metadata:)
wrap_with_tracing(method, 'server_stream', metadata) do
yield
end
end
def bidi_streamer(requests:, call:, method:, metadata:)
wrap_with_tracing(method, 'bidi_stream', metadata) do
yield
end
end
private
def wrap_with_tracing(method, grpc_type, metadata)
tags = {
'component' => 'grpc',
'span.kind' => 'client',
'grpc.method' => method,
'grpc.type' => grpc_type
}
in_tracing_span(operation_name: "grpc:#{method}", tags: tags) do |span|
OpenTracing.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, metadata)
yield
end
end
end
end
end
# frozen_string_literal: true
require 'jaeger/client'
module Gitlab
module Tracing
class JaegerFactory
# When the probabilistic sampler is used, by default 0.1% of requests will be traced
DEFAULT_PROBABILISTIC_RATE = 0.001
# The default port for the Jaeger agent UDP listener
DEFAULT_UDP_PORT = 6831
# Reduce this from default of 10 seconds as the Ruby jaeger
# client doesn't have overflow control, leading to very large
# messages which fail to send over UDP (max packet = 64k)
# Flush more often, with smaller packets
FLUSH_INTERVAL = 5
def self.create_tracer(service_name, options)
kwargs = {
service_name: service_name,
sampler: get_sampler(options[:sampler], options[:sampler_param]),
reporter: get_reporter(service_name, options[:http_endpoint], options[:udp_endpoint])
}.compact
extra_params = options.except(:sampler, :sampler_param, :http_endpoint, :udp_endpoint, :strict_parsing, :debug) # rubocop: disable CodeReuse/ActiveRecord
if extra_params.present?
message = "jaeger tracer: invalid option: #{extra_params.keys.join(", ")}"
if options[:strict_parsing]
raise message
else
warn message
end
end
Jaeger::Client.build(kwargs)
end
def self.get_sampler(sampler_type, sampler_param)
case sampler_type
when "probabilistic"
sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE
Jaeger::Samplers::Probabilistic.new(rate: sampler_rate)
when "const"
const_value = sampler_param == "1"
Jaeger::Samplers::Const.new(const_value)
else
nil
end
end
private_class_method :get_sampler
def self.get_reporter(service_name, http_endpoint, udp_endpoint)
encoder = Jaeger::Encoders::ThriftEncoder.new(service_name: service_name)
if http_endpoint.present?
sender = get_http_sender(encoder, http_endpoint)
elsif udp_endpoint.present?
sender = get_udp_sender(encoder, udp_endpoint)
else
return
end
Jaeger::Reporters::RemoteReporter.new(
sender: sender,
flush_interval: FLUSH_INTERVAL
)
end
private_class_method :get_reporter
def self.get_http_sender(encoder, address)
Jaeger::HttpSender.new(
url: address,
encoder: encoder,
logger: Logger.new(STDOUT)
)
end
private_class_method :get_http_sender
def self.get_udp_sender(encoder, address)
pair = address.split(":", 2)
host = pair[0]
port = pair[1] ? pair[1].to_i : DEFAULT_UDP_PORT
Jaeger::UdpSender.new(
host: host,
port: port,
encoder: encoder,
logger: Logger.new(STDOUT)
)
end
private_class_method :get_udp_sender
end
end
end
# frozen_string_literal: true
require 'opentracing'
module Gitlab
module Tracing
class RackMiddleware
include Common
REQUEST_METHOD = 'REQUEST_METHOD'
def initialize(app)
@app = app
end
def call(env)
method = env[REQUEST_METHOD]
context = tracer.extract(OpenTracing::FORMAT_RACK, env)
tags = {
'component' => 'rack',
'span.kind' => 'server',
'http.method' => method,
'http.url' => self.class.build_sanitized_url_from_env(env)
}
in_tracing_span(operation_name: "http:#{method}", child_of: context, tags: tags) do |span|
@app.call(env).tap do |status_code, _headers, _body|
span.set_tag('http.status_code', status_code)
end
end
end
# Generate a sanitized (safe) request URL from the rack environment
def self.build_sanitized_url_from_env(env)
request = ActionDispatch::Request.new(env)
original_url = request.original_url
uri = URI.parse(original_url)
uri.query = request.filtered_parameters.to_query if uri.query.present?
uri.to_s
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Tracing
module Rails
class ActionViewSubscriber
include RailsCommon
COMPONENT_TAG = 'ActionView'
RENDER_TEMPLATE_NOTIFICATION_TOPIC = 'render_template.action_view'
RENDER_COLLECTION_NOTIFICATION_TOPIC = 'render_collection.action_view'
RENDER_PARTIAL_NOTIFICATION_TOPIC = 'render_partial.action_view'
# Instruments Rails ActionView events for opentracing.
# Returns a lambda, which, when called will unsubscribe from the notifications
def self.instrument
subscriber = new
subscriptions = [
ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
subscriber.notify_render_template(start, finish, payload)
end,
ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
subscriber.notify_render_collection(start, finish, payload)
end,
ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
subscriber.notify_render_partial(start, finish, payload)
end
]
create_unsubscriber subscriptions
end
# For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html
def notify_render_template(start, finish, payload)
generate_span_for_notification("render_template", start, finish, payload, tags_for_render_template(payload))
end
def notify_render_collection(start, finish, payload)
generate_span_for_notification("render_collection", start, finish, payload, tags_for_render_collection(payload))
end
def notify_render_partial(start, finish, payload)
generate_span_for_notification("render_partial", start, finish, payload, tags_for_render_partial(payload))
end
private
def tags_for_render_template(payload)
{
'component' => COMPONENT_TAG,
'template.id' => payload[:identifier],
'template.layout' => payload[:layout]
}
end
def tags_for_render_collection(payload)
{
'component' => COMPONENT_TAG,
'template.id' => payload[:identifier],
'template.count' => payload[:count] || 0,
'template.cache.hits' => payload[:cache_hits] || 0
}
end
def tags_for_render_partial(payload)
{
'component' => COMPONENT_TAG,
'template.id' => payload[:identifier]
}
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Tracing
module Rails
class ActiveRecordSubscriber
include RailsCommon
ACTIVE_RECORD_NOTIFICATION_TOPIC = 'sql.active_record'
OPERATION_NAME_PREFIX = 'active_record:'
DEFAULT_OPERATION_NAME = 'sqlquery'
# Instruments Rails ActiveRecord events for opentracing.
# Returns a lambda, which, when called will unsubscribe from the notifications
def self.instrument
subscriber = new
subscription = ActiveSupport::Notifications.subscribe(ACTIVE_RECORD_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
subscriber.notify(start, finish, payload)
end
create_unsubscriber [subscription]
end
# For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html
def notify(start, finish, payload)
generate_span_for_notification(notification_name(payload), start, finish, payload, tags_for_notification(payload))
end
private
def notification_name(payload)
OPERATION_NAME_PREFIX + (payload[:name].presence || DEFAULT_OPERATION_NAME)
end
def tags_for_notification(payload)
{
'component' => 'ActiveRecord',
'span.kind' => 'client',
'db.type' => 'sql',
'db.connection_id' => payload[:connection_id],
'db.cached' => payload[:cached] || false,
'db.statement' => payload[:sql]
}
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Tracing
module Rails
module RailsCommon
extend ActiveSupport::Concern
include Gitlab::Tracing::Common
class_methods do
def create_unsubscriber(subscriptions)
-> { subscriptions.each { |subscriber| ActiveSupport::Notifications.unsubscribe(subscriber) } }
end
end
def generate_span_for_notification(operation_name, start, finish, payload, tags)
exception = payload[:exception]
postnotify_span(operation_name, start, finish, tags: tags, exception: exception)
end
end
end
end
end
# frozen_string_literal: true
require 'opentracing'
module Gitlab
module Tracing
module Sidekiq
class ClientMiddleware
include SidekiqCommon
SPAN_KIND = 'client'
def call(worker_class, job, queue, redis_pool)
in_tracing_span(
operation_name: "sidekiq:#{job['class']}",
tags: tags_from_job(job, SPAN_KIND)) do |span|
# Inject the details directly into the job
tracer.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, job)
yield
end
end
end
end
end
end
# frozen_string_literal: true
require 'opentracing'
module Gitlab
module Tracing
module Sidekiq
class ServerMiddleware
include SidekiqCommon
SPAN_KIND = 'server'
def call(worker, job, queue)
context = tracer.extract(OpenTracing::FORMAT_TEXT_MAP, job)
in_tracing_span(
operation_name: "sidekiq:#{job['class']}",
child_of: context,
tags: tags_from_job(job, SPAN_KIND)) do |span|
yield
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Tracing
module Sidekiq
module SidekiqCommon
include Gitlab::Tracing::Common
def tags_from_job(job, kind)
{
'component' => 'sidekiq',
'span.kind' => kind,
'sidekiq.queue' => job['queue'],
'sidekiq.jid' => job['jid'],
'sidekiq.retry' => job['retry'].to_s,
'sidekiq.args' => job['args']&.join(", ")
}
end
end
end
end
end
...@@ -4,9 +4,9 @@ module Peek ...@@ -4,9 +4,9 @@ module Peek
module Views module Views
class Tracing < View class Tracing < View
def results def results
{ tracing_url = Labkit::Tracing.tracing_url(Gitlab.process_name)
tracing_url: Gitlab::Tracing.tracing_url
} { tracing_url: tracing_url }
end end
end end
end end
......
...@@ -462,7 +462,7 @@ describe ApplicationController do ...@@ -462,7 +462,7 @@ describe ApplicationController do
end end
it 'does log correlation id' do it 'does log correlation id' do
Gitlab::CorrelationId.use_id('new-id') do Labkit::Correlation::CorrelationId.use_id('new-id') do
get :index get :index
end end
......
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::CorrelationId do
describe '.use_id' do
it 'yields when executed' do
expect { |blk| described_class.use_id('id', &blk) }.to yield_control
end
it 'stacks correlation ids' do
described_class.use_id('id1') do
described_class.use_id('id2') do |current_id|
expect(current_id).to eq('id2')
end
end
end
it 'for missing correlation id it generates random one' do
described_class.use_id('id1') do
described_class.use_id(nil) do |current_id|
expect(current_id).not_to be_empty
expect(current_id).not_to eq('id1')
end
end
end
end
describe '.current_id' do
subject { described_class.current_id }
it 'returns last correlation id' do
described_class.use_id('id1') do
described_class.use_id('id2') do
is_expected.to eq('id2')
end
end
end
end
describe '.current_or_new_id' do
subject { described_class.current_or_new_id }
context 'when correlation id is set' do
it 'returns last correlation id' do
described_class.use_id('id1') do
is_expected.to eq('id1')
end
end
end
context 'when correlation id is missing' do
it 'returns a new correlation id' do
expect(described_class).to receive(:new_id)
.and_call_original
is_expected.not_to be_empty
end
end
end
describe '.ids' do
subject { described_class.send(:ids) }
it 'returns empty list if not correlation is used' do
is_expected.to be_empty
end
it 'returns list if correlation ids are used' do
described_class.use_id('id1') do
described_class.use_id('id2') do
is_expected.to eq(%w(id1 id2))
end
end
end
end
end
...@@ -8,7 +8,7 @@ describe Gitlab::JsonLogger do ...@@ -8,7 +8,7 @@ describe Gitlab::JsonLogger do
describe '#format_message' do describe '#format_message' do
before do before do
allow(Gitlab::CorrelationId).to receive(:current_id).and_return('new-correlation-id') allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
end end
it 'formats strings' do it 'formats strings' do
......
...@@ -27,7 +27,7 @@ describe Gitlab::Sentry do ...@@ -27,7 +27,7 @@ describe Gitlab::Sentry do
context 'when exceptions should not be raised' do context 'when exceptions should not be raised' do
before do before do
allow(described_class).to receive(:should_raise_for_dev?).and_return(false) allow(described_class).to receive(:should_raise_for_dev?).and_return(false)
allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid') allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
end end
it 'logs the exception with all attributes passed' do it 'logs the exception with all attributes passed' do
...@@ -65,7 +65,7 @@ describe Gitlab::Sentry do ...@@ -65,7 +65,7 @@ describe Gitlab::Sentry do
before do before do
allow(described_class).to receive(:enabled?).and_return(true) allow(described_class).to receive(:enabled?).and_return(true)
allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid') allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
end end
it 'calls Raven.capture_exception' do it 'calls Raven.capture_exception' do
......
...@@ -30,7 +30,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationInjector do ...@@ -30,7 +30,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationInjector do
it 'injects into payload the correlation id' do it 'injects into payload the correlation id' do
expect_any_instance_of(described_class).to receive(:call).and_call_original expect_any_instance_of(described_class).to receive(:call).and_call_original
Gitlab::CorrelationId.use_id('new-correlation-id') do Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
TestWorker.perform_async(1234) TestWorker.perform_async(1234)
end end
......
...@@ -23,7 +23,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationLogger do ...@@ -23,7 +23,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationLogger do
expect_any_instance_of(described_class).to receive(:call).and_call_original expect_any_instance_of(described_class).to receive(:call).and_call_original
expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do
expect(Gitlab::CorrelationId.current_id).to eq('new-correlation-id') expect(Labkit::Correlation::CorrelationId.current_id).to eq('new-correlation-id')
end end
Sidekiq::Client.push( Sidekiq::Client.push(
......
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::Tracing::Factory do
describe '.create_tracer' do
let(:service_name) { 'rspec' }
context "when tracing is not configured" do
it 'ignores null connection strings' do
expect(described_class.create_tracer(service_name, nil)).to be_nil
end
it 'ignores empty connection strings' do
expect(described_class.create_tracer(service_name, '')).to be_nil
end
it 'ignores unknown implementations' do
expect(described_class.create_tracer(service_name, 'opentracing://invalid_driver')).to be_nil
end
it 'ignores invalid connection strings' do
expect(described_class.create_tracer(service_name, 'open?tracing')).to be_nil
end
end
context "when tracing is configured with jaeger" do
let(:mock_tracer) { double('tracer') }
it 'processes default connections' do
expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).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(Gitlab::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
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::Tracing::GRPCInterceptor do
subject { described_class.instance }
shared_examples_for "a grpc interceptor method" do
let(:custom_error) { Class.new(StandardError) }
it 'yields' do
expect { |b| method.call(kwargs, &b) }.to yield_control
end
it 'propagates exceptions' do
expect { method.call(kwargs) { raise custom_error } }.to raise_error(custom_error)
end
end
describe '#request_response' do
let(:method) { subject.method(:request_response) }
let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } }
it_behaves_like 'a grpc interceptor method'
end
describe '#client_streamer' do
let(:method) { subject.method(:client_streamer) }
let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } }
it_behaves_like 'a grpc interceptor method'
end
describe '#server_streamer' do
let(:method) { subject.method(:server_streamer) }
let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } }
it_behaves_like 'a grpc interceptor method'
end
describe '#bidi_streamer' do
let(:method) { subject.method(:bidi_streamer) }
let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } }
it_behaves_like 'a grpc interceptor method'
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::Tracing::JaegerFactory do
describe '.create_tracer' do
let(:service_name) { 'rspec' }
shared_examples_for 'a jaeger tracer' do
it 'responds to active_span methods' do
expect(tracer).to respond_to(:active_span)
end
it 'yields control' do
expect { |b| tracer.start_active_span('operation_name', &b) }.to yield_control
end
end
context 'processes default connections' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, {}) }
end
end
context 'handles debug options' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, { debug: "1" }) }
end
end
context 'handles const sampler' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, { sampler: "const", sampler_param: "1" }) }
end
end
context 'handles probabilistic sampler' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, { sampler: "probabilistic", sampler_param: "0.5" }) }
end
end
context 'handles http_endpoint configurations' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, { http_endpoint: "http://localhost:1234" }) }
end
end
context 'handles udp_endpoint configurations' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, { udp_endpoint: "localhost:4321" }) }
end
end
context 'ignores invalid parameters' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, { invalid: "true" }) }
end
end
context 'accepts the debug parameter when strict_parser is set' do
it_behaves_like 'a jaeger tracer' do
let(:tracer) { described_class.create_tracer(service_name, { debug: "1", strict_parsing: "1" }) }
end
end
it 'rejects invalid parameters when strict_parser is set' do
expect { described_class.create_tracer(service_name, { invalid: "true", strict_parsing: "1" }) }.to raise_error(StandardError)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Tracing::RackMiddleware do
using RSpec::Parameterized::TableSyntax
describe '#call' do
context 'for normal middleware flow' do
let(:fake_app) { -> (env) { fake_app_response } }
subject { described_class.new(fake_app) }
let(:request) { }
context 'for 200 responses' do
let(:fake_app_response) { [200, { 'Content-Type': 'text/plain' }, ['OK']] }
it 'delegates correctly' do
expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response)
end
end
context 'for 500 responses' do
let(:fake_app_response) { [500, { 'Content-Type': 'text/plain' }, ['Error']] }
it 'delegates correctly' do
expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response)
end
end
end
context 'when an application is raising an exception' do
let(:custom_error) { Class.new(StandardError) }
let(:fake_app) { ->(env) { raise custom_error } }
subject { described_class.new(fake_app) }
it 'delegates propagates exceptions correctly' do
expect { subject.call(Rack::MockRequest.env_for("/")) }.to raise_error(custom_error)
end
end
end
describe '.build_sanitized_url_from_env' do
def env_for_url(url)
env = Rack::MockRequest.env_for(input_url)
env['action_dispatch.parameter_filter'] = [/token/]
env
end
where(:input_url, :output_url) do
'/gitlab-org/gitlab-ce' | 'http://example.org/gitlab-org/gitlab-ce'
'/gitlab-org/gitlab-ce?safe=1' | 'http://example.org/gitlab-org/gitlab-ce?safe=1'
'/gitlab-org/gitlab-ce?private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?private_token=%5BFILTERED%5D'
'/gitlab-org/gitlab-ce?mixed=1&private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?mixed=1&private_token=%5BFILTERED%5D'
end
with_them do
it { expect(described_class.build_sanitized_url_from_env(env_for_url(input_url))).to eq(output_url) }
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
describe Gitlab::Tracing::Rails::ActionViewSubscriber do
using RSpec::Parameterized::TableSyntax
shared_examples 'an actionview notification' do
it 'notifies the tracer when the hash contains null values' do
expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
subject.public_send(notify_method, start, finish, payload)
end
it 'notifies the tracer when the payload is missing values' do
expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
subject.public_send(notify_method, start, finish, payload.compact)
end
it 'does not throw exceptions when with the default tracer' do
expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error
end
end
describe '.instrument' do
it 'is unsubscribeable' do
unsubscribe = described_class.instrument
expect(unsubscribe).not_to be_nil
expect { unsubscribe.call }.not_to raise_error
end
end
describe '#notify_render_template' do
subject { described_class.new }
let(:start) { Time.now }
let(:finish) { Time.now }
let(:notification_name) { 'render_template' }
let(:notify_method) { :notify_render_template }
where(:identifier, :layout, :exception) do
nil | nil | nil
"" | nil | nil
"show.haml" | nil | nil
nil | "" | nil
nil | "layout.haml" | nil
nil | nil | StandardError.new
end
with_them do
let(:payload) do
{
exception: exception,
identifier: identifier,
layout: layout
}
end
let(:expected_tags) do
{
'component' => 'ActionView',
'template.id' => identifier,
'template.layout' => layout
}
end
it_behaves_like 'an actionview notification'
end
end
describe '#notify_render_collection' do
subject { described_class.new }
let(:start) { Time.now }
let(:finish) { Time.now }
let(:notification_name) { 'render_collection' }
let(:notify_method) { :notify_render_collection }
where(
:identifier, :count, :expected_count, :cache_hits, :expected_cache_hits, :exception) do
nil | nil | 0 | nil | 0 | nil
"" | nil | 0 | nil | 0 | nil
"show.haml" | nil | 0 | nil | 0 | nil
nil | 0 | 0 | nil | 0 | nil
nil | 1 | 1 | nil | 0 | nil
nil | nil | 0 | 0 | 0 | nil
nil | nil | 0 | 1 | 1 | nil
nil | nil | 0 | nil | 0 | StandardError.new
end
with_them do
let(:payload) do
{
exception: exception,
identifier: identifier,
count: count,
cache_hits: cache_hits
}
end
let(:expected_tags) do
{
'component' => 'ActionView',
'template.id' => identifier,
'template.count' => expected_count,
'template.cache.hits' => expected_cache_hits
}
end
it_behaves_like 'an actionview notification'
end
end
describe '#notify_render_partial' do
subject { described_class.new }
let(:start) { Time.now }
let(:finish) { Time.now }
let(:notification_name) { 'render_partial' }
let(:notify_method) { :notify_render_partial }
where(:identifier, :exception) do
nil | nil
"" | nil
"show.haml" | nil
nil | StandardError.new
end
with_them do
let(:payload) do
{
exception: exception,
identifier: identifier
}
end
let(:expected_tags) do
{
'component' => 'ActionView',
'template.id' => identifier
}
end
it_behaves_like 'an actionview notification'
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do
using RSpec::Parameterized::TableSyntax
describe '.instrument' do
it 'is unsubscribeable' do
unsubscribe = described_class.instrument
expect(unsubscribe).not_to be_nil
expect { unsubscribe.call }.not_to raise_error
end
end
describe '#notify' do
subject { described_class.new }
let(:start) { Time.now }
let(:finish) { Time.now }
where(:name, :operation_name, :exception, :connection_id, :cached, :cached_response, :sql) do
nil | "active_record:sqlquery" | nil | nil | nil | false | nil
"" | "active_record:sqlquery" | nil | nil | nil | false | nil
"User Load" | "active_record:User Load" | nil | nil | nil | false | nil
"Repo Load" | "active_record:Repo Load" | StandardError.new | nil | nil | false | nil
nil | "active_record:sqlquery" | nil | 123 | nil | false | nil
nil | "active_record:sqlquery" | nil | nil | false | false | nil
nil | "active_record:sqlquery" | nil | nil | true | true | nil
nil | "active_record:sqlquery" | nil | nil | true | true | "SELECT * FROM users"
end
with_them do
def payload
{
name: name,
exception: exception,
connection_id: connection_id,
cached: cached,
sql: sql
}
end
def expected_tags
{
"component" => "ActiveRecord",
"span.kind" => "client",
"db.type" => "sql",
"db.connection_id" => connection_id,
"db.cached" => cached_response,
"db.statement" => sql
}
end
it 'notifies the tracer when the hash contains null values' do
expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
subject.notify(start, finish, payload)
end
it 'notifies the tracer when the payload is missing values' do
expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
subject.notify(start, finish, payload.compact)
end
it 'does not throw exceptions when with the default tracer' do
expect { subject.notify(start, finish, payload) }.not_to raise_error
end
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::Tracing::Sidekiq::ClientMiddleware do
describe '#call' do
let(:worker_class) { 'test_worker_class' }
let(:job) do
{
'class' => "jobclass",
'queue' => "jobqueue",
'retry' => 0,
'args' => %w{1 2 3}
}
end
let(:queue) { 'test_queue' }
let(:redis_pool) { double("redis_pool") }
let(:custom_error) { Class.new(StandardError) }
let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) }
subject { described_class.new }
it 'yields' do
expect(subject).to receive(:in_tracing_span).with(
operation_name: "sidekiq:jobclass",
tags: {
"component" => "sidekiq",
"span.kind" => "client",
"sidekiq.queue" => "jobqueue",
"sidekiq.jid" => nil,
"sidekiq.retry" => "0",
"sidekiq.args" => "1, 2, 3"
}
).and_yield(span)
expect { |b| subject.call(worker_class, job, queue, redis_pool, &b) }.to yield_control
end
it 'propagates exceptions' do
expect { subject.call(worker_class, job, queue, redis_pool) { raise custom_error } }.to raise_error(custom_error)
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::Tracing::Sidekiq::ServerMiddleware do
describe '#call' do
let(:worker_class) { 'test_worker_class' }
let(:job) do
{
'class' => "jobclass",
'queue' => "jobqueue",
'retry' => 0,
'args' => %w{1 2 3}
}
end
let(:queue) { 'test_queue' }
let(:custom_error) { Class.new(StandardError) }
let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) }
subject { described_class.new }
it 'yields' do
expect(subject).to receive(:in_tracing_span).with(
hash_including(
operation_name: "sidekiq:jobclass",
tags: {
"component" => "sidekiq",
"span.kind" => "server",
"sidekiq.queue" => "jobqueue",
"sidekiq.jid" => nil,
"sidekiq.retry" => "0",
"sidekiq.args" => "1, 2, 3"
}
)
).and_yield(span)
expect { |b| subject.call(worker_class, job, queue, &b) }.to yield_control
end
it 'propagates exceptions' do
expect { subject.call(worker_class, job, queue) { raise custom_error } }.to raise_error(custom_error)
end
end
end
...@@ -251,7 +251,7 @@ describe API::Helpers do ...@@ -251,7 +251,7 @@ describe API::Helpers do
correlation_id: 'new-correlation-id' correlation_id: 'new-correlation-id'
}, extra: {}) }, extra: {})
Gitlab::CorrelationId.use_id('new-correlation-id') do Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
handle_api_exception(exception) handle_api_exception(exception)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment