Skip to content

Commit

Permalink
feat: metrics integration for sidekiq
Browse files Browse the repository at this point in the history
  • Loading branch information
zvkemp committed Jan 7, 2025
1 parent 7def994 commit 04c4243
Show file tree
Hide file tree
Showing 12 changed files with 463 additions and 48 deletions.
10 changes: 10 additions & 0 deletions instrumentation/base/lib/opentelemetry/instrumentation/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,16 @@ def metrics_env_var_name
end
end

def metrics_enabled_by_env_var?
var_name = name.dup
var_name.upcase!
var_name.gsub!('::', '_')
var_name.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
var_name << '_METRICS_ENABLED'

ENV.key?(var_name) && ENV[var_name] != 'false'
end

# Checks to see if the user has passed any environment variables that set options
# for instrumentation. By convention, the environment variable will be the name
# of the instrumentation, uppercased, with '::' replaced by underscores,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = '>= 3.0'

spec.add_dependency 'opentelemetry-api', '~> 1.0'
spec.add_dependency 'opentelemetry-metrics-api', '~> 1.0'
spec.add_dependency 'opentelemetry-instrumentation-base', '~> 0.22.1'

spec.add_development_dependency 'appraisal', '~> 2.5'
Expand Down
53 changes: 34 additions & 19 deletions instrumentation/sidekiq/Appraisals
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
# frozen_string_literal: true

appraise 'sidekiq-7.0' do
gem 'sidekiq', '~> 7.0'
end

appraise 'sidekiq-6.5' do
gem 'sidekiq', '>= 6.5', '< 7.0'
end
{
'sidekiq-7.0' => [['sidekiq', '~> 7.0']],
'sidekiq-6.5' => [['sidekiq', '>= 6.5', '< 7.0']],
'sidekiq-6.0' => [
['sidekiq', '>= 6.0', '< 6.5'],
['redis', '< 4.8']
],
'sidekiq-5.2' => [
['sidekiq', '~> 5.2'],
['redis', '< 4.8']
],
'sidekiq-4.2' => [
['sidekiq', '~> 4.2'],
['redis', '< 4.8']
]
}.each do |gemfile_name, specs|
appraise gemfile_name do
specs.each do |spec|
gem *spec
remove_gem 'opentelemetry-metrics-api'
remove_gem 'opentelemetry-metrics-sdk'
end
end

appraise 'sidekiq-6.0' do
gem 'sidekiq', '>= 6.0', '< 6.5'
gem 'redis', '< 4.8'
end

appraise 'sidekiq-5.2' do
gem 'sidekiq', '~> 5.2'
gem 'redis', '< 4.8'
end
appraise "#{gemfile_name}-metrics-api" do
specs.each do |spec|
gem *spec
remove_gem 'opentelemetry-metrics-sdk'
end
end

appraise 'sidekiq-4.2' do
gem 'sidekiq', '~> 4.2'
gem 'redis', '< 4.8'
appraise "#{gemfile_name}-metrics-sdk" do
specs.each do |spec|
gem *spec
end
end
end
14 changes: 14 additions & 0 deletions instrumentation/sidekiq/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,22 @@ source 'https://rubygems.org'

gemspec

# FIXME: the metrics-api is behind the metrics-sdk gem for some reason; bundle from git for now
OTEL_RUBY_GEM = lambda do |short_name|
short_name = short_name.split(/-|_/)
long_name = ['opentelemetry', *short_name].join('-')

gem long_name,
git: 'https://www.github.com/open-telemetry/opentelemetry-ruby',
glob: "#{short_name.join('_')}/*.gemspec",
ref: '035c32ad9791f6200733e087f2ee49e0a615879a'
end

OTEL_RUBY_GEM['metrics-api']

group :test do
gem 'opentelemetry-instrumentation-base', path: '../base'
gem 'opentelemetry-instrumentation-redis', path: '../redis'
OTEL_RUBY_GEM['metrics-sdk']
gem 'pry-byebug'
end
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,63 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
option :trace_poller_wait, default: false, validate: :boolean
option :trace_processor_process_one, default: false, validate: :boolean
option :peer_service, default: nil, validate: :string
option :metrics, default: false, validate: :boolean

# FIXME: descriptions?

if defined?(OpenTelemetry::Metrics)
counter 'messaging.client.sent.messages'
histogram 'messaging.client.operation.duration', unit: 's' # FIXME: UCUM::S
counter 'messaging.client.consumed.messages'
histogram 'messaging.process.duration', unit: 's'

# FIXME: not semconv
gauge 'messaging.queue.latency', unit: 's'
end

# FIXME: upstream
def counter(name)
get_instrument(:counter, name)
end

# FIXME: upstream
def histogram(name)
get_instrument(:histogram, name)
end

# FIXME: upstream
def gauge(name)
get_instrument(:gauge, name)
end

private

def get_instrument(kind, name)
return unless metrics_enabled?

@instruments ||= {}
@instruments[[kind, name]] ||= create_configured_instrument(kind, name)
end

def create_configured_instrument(kind, name)
config = @instrument_configs[[kind, name]]

if config.nil?
Kernel.warn("unconfigured instrument requested: #{kind} of '#{name}'")
return
end

# FIXME: some of these have different opts;
# should verify that they work before this point.
meter.public_send(:"create_#{kind}", name, **config)
end

def gem_version
Gem::Version.new(::Sidekiq::VERSION)
end

def require_dependencies
require_relative 'middlewares/common'
require_relative 'middlewares/client/tracer_middleware'
require_relative 'middlewares/server/tracer_middleware'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#
# SPDX-License-Identifier: Apache-2.0

require_relative '../common'

module OpenTelemetry
module Instrumentation
module Sidekiq
Expand All @@ -12,6 +14,7 @@ module Client
# TracerMiddleware propagates context and instruments Sidekiq client
# by way of its middleware system
class TracerMiddleware
include Common
include ::Sidekiq::ClientMiddleware if defined?(::Sidekiq::ClientMiddleware)

def call(_worker_class, job, _queue, _redis_pool)
Expand All @@ -33,17 +36,50 @@ def call(_worker_class, job, _queue, _redis_pool)
OpenTelemetry.propagation.inject(job)
span.add_event('created_at', timestamp: job['created_at'])
yield
end.tap do
# FIXME: is it possible/necessary to detect failures here? Does sidekiq bubble them up the middlewares?
count_sent_message(job)
end
end

private

def instrumentation_config
Sidekiq::Instrumentation.instance.config
def count_sent_message(job)
with_meter do |_meter|
counter_attributes = metrics_attributes(job).merge(
{
'messaging.operation.name' => 'create'
# server.address => # FIXME: required if available
# messaging.destination.partition.id => FIXME: recommended
# server.port => # FIXME: recommended
}
)

counter = messaging_client_sent_messages_counter
counter.add(1, attributes: counter_attributes)
end
end

def messaging_client_sent_messages_counter
instrumentation.counter('messaging.client.sent.messages')
end

def tracer
Sidekiq::Instrumentation.instance.tracer
instrumentation.tracer
end

def with_meter(&block)
instrumentation.with_meter(&block)
end

def metrics_attributes(job)
{
'messaging.system' => 'sidekiq', # FIXME: metrics semconv
'messaging.destination.name' => job['queue'] # FIXME: metrics semconv
# server.address => # FIXME: required if available
# messaging.destination.partition.id => FIXME: recommended
# server.port => # FIXME: recommended
}
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

module OpenTelemetry
module Instrumentation
module Sidekiq
module Middlewares
module Common
private

def instrumentation
Sidekiq::Instrumentation.instance
end

def instrumentation_config
Sidekiq::Instrumentation.instance.config
end

# Bypasses _all_ enclosed logic unless metrics are enabled
def with_meter(&block)
instrumentation.with_meter(&block)
end

# time an inner block and yield the duration to the given callback
def timed(callback)
return yield unless metrics_enabled?

t0 = monotonic_now

yield.tap do
callback.call(monotonic_now - t0)
end
end

# FIXME: is this a util somewhere
def realtime_now
Process.clock_gettime(Process::CLOCK_REALTIME)
end

def monotonic_now
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end

def tracer
instrumentation.tracer
end

def metrics_enabled?
instrumentation.metrics_enabled?
end
end
end
end
end
end
Loading

0 comments on commit 04c4243

Please sign in to comment.