Skip to content

Commit 04b7c55

Browse files
committed
add metrics tests
1 parent 97ee174 commit 04b7c55

File tree

9 files changed

+234
-46
lines changed

9 files changed

+234
-46
lines changed

instrumentation/redis/Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ group :test do
1212
gem 'opentelemetry-instrumentation-base', path: '../base'
1313
end
1414

15+
gem 'opentelemetry-test-helpers', github: 'zvkemp/opentelemetry-ruby', glob: 'test_helpers/*.gemspec', require: false, ref: 'test-helpers-metrics'
16+
1517
gem 'pry-byebug'

instrumentation/redis/lib/opentelemetry/instrumentation/redis/instrumentation.rb

+30-6
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
2626

2727
# https://opentelemetry.io/docs/specs/semconv/database/database-metrics/#metric-dbclientoperationduration
2828
histogram 'db.client.operation.duration',
29-
attributes: { 'db.system.name' => 'redis' },
29+
attributes: { 'db.system' => 'redis' },
3030
unit: 's',
31-
boundaries: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10]
31+
explicit_bucket_boundaries: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10]
3232

3333
def client_operation_duration_histogram
3434
histogram('db.client.operation.duration')
@@ -37,18 +37,42 @@ def client_operation_duration_histogram
3737
private
3838

3939
def require_dependencies
40-
require_relative 'patches/redis_v4_client' if defined?(::Redis) && ::Redis::VERSION < '5'
40+
require_redis_client_dependencies
41+
require_redis_v4_dependencies
42+
end
43+
44+
def require_redis_v4_dependencies
45+
return unless defined?(::Redis) && Gem::Version.new(Redis::VERSION) < Gem::Version.new('5.0.0')
46+
47+
require_relative 'patches/redis_v4_client'
48+
require_relative 'patches/redis_v4_client_metrics'
49+
end
4150

51+
def require_redis_client_dependencies
4252
return unless defined?(::RedisClient)
4353

4454
require_relative 'middlewares/redis_client'
4555
require_relative 'middlewares/redis_client_metrics'
4656
end
4757

4858
def patch_client
49-
::RedisClient.register(Middlewares::RedisClientInstrumentation) if defined?(::RedisClient)
50-
::RedisClient.register(Middlewares::RedisClientMetrics) if defined?(::RedisClient)
51-
::Redis::Client.prepend(Patches::RedisV4Client) if defined?(::Redis) && ::Redis::VERSION < '5'
59+
patch_redis_v4_client
60+
patch_redis_client
61+
end
62+
63+
def patch_redis_v4_client
64+
return unless defined?(::Redis) && Gem::Version.new(Redis::VERSION) < Gem::Version.new('5.0.0')
65+
66+
::Redis::Client.prepend(Patches::RedisV4Client)
67+
::Redis::Client.prepend(Patches::RedisV4ClientMetrics) if metrics_defined?
68+
end
69+
70+
# Applies to redis-client or redis >= 5
71+
def patch_redis_client
72+
return unless defined?(::RedisClient)
73+
74+
::RedisClient.register(Middlewares::RedisClientInstrumentation)
75+
::RedisClient.register(Middlewares::RedisClientMetrics) if metrics_defined?
5276
end
5377
end
5478
end

instrumentation/redis/lib/opentelemetry/instrumentation/redis/middlewares/redis_client_metrics.rb

+12-26
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,41 @@
44
#
55
# SPDX-License-Identifier: Apache-2.0
66

7+
require_relative '../patches/metrics_helpers'
8+
79
module OpenTelemetry
810
module Instrumentation
911
module Redis
1012
module Middlewares
1113
# Adapter for redis-client instrumentation interface
1214
module RedisClientMetrics
15+
def self.included(base)
16+
base.include(OpenTelemetry::Instrumentation::Redis::Patches::MetricsHelpers)
17+
end
18+
1319
def call(command, redis_config)
1420
return super unless (histogram = instrumentation.client_operation_duration_histogram)
1521

16-
timed(histogram, command.first, redis_config) do
22+
attributes = metric_attributes(redis_config, command.first)
23+
otel_record_histogram(histogram, attributes) do
1724
super
1825
end
1926
end
2027

2128
def call_pipelined(commands, redis_config)
2229
return super unless (histogram = instrumentation.client_operation_duration_histogram)
2330

24-
timed(histogram, 'PIPELINE', redis_config) do
31+
attributes = metric_attributes(redis_config, 'PIPELINED')
32+
otel_record_histogram(histogram, attributes) do
2533
super
2634
end
2735
end
2836

2937
private
3038

31-
def timed(histogram, operation_name, redis_config)
32-
t0 = monotonic_now
33-
34-
yield.tap do
35-
duration = monotonic_now - t0
36-
37-
histogram.record(duration, attributes: metric_attributes(redis_config, operation_name))
38-
end
39-
end
40-
41-
def monotonic_now
42-
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
43-
end
44-
4539
def metric_attributes(redis_config, operation_name)
46-
attributes = {
47-
'db.system' => 'redis',
48-
'db.operation.name' => operation_name,
49-
'net.peer.name' => redis_config.host,
50-
'net.peer.port' => redis_config.port
51-
}
52-
53-
attributes['db.redis.database_index'] = redis_config.db unless redis_config.db.zero?
54-
attributes['peer.service'] = instrumentation.config[:peer_service] if instrumentation.config[:peer_service]
55-
attributes.merge!(OpenTelemetry::Instrumentation::Redis.attributes)
40+
attributes = span_attributes(redis_config)
41+
attributes['db.operation.name'] = operation_name
5642
attributes
5743
end
5844

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module Redis
10+
module Patches
11+
# Common logic for tracking histograms and other metrics instruments
12+
module MetricsHelpers
13+
private
14+
15+
def otel_record_histogram(histogram, attributes)
16+
t0 = otel_monotonic_now
17+
yield.tap do |result|
18+
attributes['error.type'] = result.class.to_s if result.is_a?(StandardError)
19+
end
20+
rescue StandardError => e
21+
attributes['error.type'] = e.class.to_s
22+
raise
23+
ensure
24+
duration = otel_monotonic_now - t0
25+
histogram.record(duration, attributes: attributes)
26+
end
27+
28+
def otel_monotonic_now
29+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
30+
end
31+
end
32+
end
33+
end
34+
end
35+
end

instrumentation/redis/lib/opentelemetry/instrumentation/redis/patches/redis_v4_client.rb

+23-12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#
55
# SPDX-License-Identifier: Apache-2.0
66

7+
require_relative 'metrics_helpers'
8+
79
module OpenTelemetry
810
module Instrumentation
911
module Redis
@@ -16,18 +18,7 @@ module RedisV4Client
1618
def process(commands)
1719
return super unless instrumentation_config[:trace_root_spans] || OpenTelemetry::Trace.current_span.context.valid?
1820

19-
host = options[:host]
20-
port = options[:port]
21-
22-
attributes = {
23-
'db.system' => 'redis',
24-
'net.peer.name' => host,
25-
'net.peer.port' => port
26-
}
27-
28-
attributes['db.redis.database_index'] = options[:db] unless options[:db].zero?
29-
attributes['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service]
30-
attributes.merge!(OpenTelemetry::Instrumentation::Redis.attributes)
21+
attributes = otel_base_attributes
3122

3223
unless instrumentation_config[:db_statement] == :omit
3324
parsed_commands = parse_commands(commands)
@@ -88,6 +79,26 @@ def instrumentation_tracer
8879
def instrumentation_config
8980
Redis::Instrumentation.instance.config
9081
end
82+
83+
def instrumentation
84+
Redis::Instrumentation.instance
85+
end
86+
87+
def otel_base_attributes
88+
host = options[:host]
89+
port = options[:port]
90+
91+
attributes = {
92+
'db.system' => 'redis',
93+
'net.peer.name' => host,
94+
'net.peer.port' => port
95+
}
96+
97+
attributes['db.redis.database_index'] = options[:db] unless options[:db].zero?
98+
attributes['peer.service'] = instrumentation_config[:peer_service] if instrumentation_config[:peer_service]
99+
attributes.merge!(OpenTelemetry::Instrumentation::Redis.attributes)
100+
attributes
101+
end
91102
end
92103
end
93104
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module Redis
10+
module Patches
11+
# Module to prepend to Redis::Client for metrics
12+
module RedisV4ClientMetrics
13+
def self.prepended(base)
14+
base.prepend(OpenTelemetry::Instrumentation::Redis::Patches::MetricsHelpers)
15+
end
16+
17+
def process(commands)
18+
return super unless (histogram = instrumentation.client_operation_duration_histogram)
19+
20+
attributes = otel_base_attributes
21+
22+
attributes['db.operation.name'] =
23+
if commands.length == 1
24+
commands[0][0].to_s
25+
else
26+
'PIPELINED'
27+
end
28+
29+
otel_record_histogram(histogram, attributes) { super }
30+
end
31+
end
32+
end
33+
end
34+
end
35+
end

instrumentation/redis/opentelemetry-instrumentation-redis.gemspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
3232
spec.add_development_dependency 'bundler', '~> 2.4'
3333
spec.add_development_dependency 'minitest', '~> 5.0'
3434
spec.add_development_dependency 'opentelemetry-sdk', '~> 1.1'
35-
spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.3'
35+
spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.5'
3636
spec.add_development_dependency 'rubocop', '~> 1.71.0'
3737
spec.add_development_dependency 'rubocop-performance', '~> 1.23.0'
3838
spec.add_development_dependency 'simplecov', '~> 0.17.1'

instrumentation/redis/test/opentelemetry/instrumentation/redis/patches/client_test.rb

+95-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ def redis_gte_5?
4141
redis_version_major&.>=(5)
4242
end
4343

44+
let(:config) { { db_statement: :include } }
45+
4446
before do
4547
# ensure obfuscation is off if it was previously set in a different test
46-
config = { db_statement: :include }
4748
instrumentation.install(config)
4849
exporter.reset
4950
end
@@ -389,4 +390,97 @@ def redis_gte_5?
389390
end
390391
end
391392
end
393+
394+
if defined?(OpenTelemetry::Metrics)
395+
describe 'metrics not enabled' do
396+
it 'will not be enabled' do
397+
assert(instrumentation.metrics_defined?)
398+
refute(instrumentation.metrics_enabled?)
399+
end
400+
end
401+
402+
describe 'metrics enabled' do
403+
let(:config) { { db_statement: :include, metrics: true } }
404+
let(:metric_snapshot) do
405+
metrics_exporter.pull
406+
metrics_exporter.metric_snapshots.last
407+
end
408+
409+
it 'will be enabled' do
410+
assert(instrumentation.metrics_defined?)
411+
assert(instrumentation.metrics_enabled?)
412+
end
413+
414+
it 'works', with_metrics_sdk: true do
415+
skip if redis_gte_5?
416+
417+
redis = redis_with_auth
418+
key = SecureRandom.hex
419+
10.times { redis.incr(key) }
420+
redis.expire(key, 1)
421+
422+
_(metric_snapshot.data_points.length).must_equal(3)
423+
424+
metric_snapshot.data_points.each do |data_point|
425+
_(data_point.attributes['db.system']).must_equal('redis')
426+
end
427+
428+
by_operation_name = metric_snapshot.data_points.each_with_object({}) { |d, res| res[d.attributes['db.operation.name']] = d }
429+
_(by_operation_name.keys.sort).must_equal(%w[auth expire incr])
430+
431+
_(by_operation_name['auth'].count).must_equal(1)
432+
_(by_operation_name['incr'].count).must_equal(10)
433+
_(by_operation_name['expire'].count).must_equal(1)
434+
end
435+
436+
it 'works v5', with_metrics_sdk: true do
437+
skip unless redis_gte_5?
438+
439+
redis = redis_with_auth
440+
key = SecureRandom.hex
441+
10.times { redis.incr(key) }
442+
redis.expire(key, 1)
443+
444+
_(metric_snapshot.data_points.length).must_equal(3)
445+
446+
metric_snapshot.data_points.each do |data_point|
447+
_(data_point.attributes['db.system']).must_equal('redis')
448+
end
449+
450+
by_operation_name = metric_snapshot.data_points.each_with_object({}) { |d, res| res[d.attributes['db.operation.name']] = d }
451+
_(by_operation_name.keys.sort).must_equal(%w[PIPELINED expire incr])
452+
453+
_(by_operation_name['PIPELINED'].count).must_equal(1)
454+
_(by_operation_name['incr'].count).must_equal(10)
455+
_(by_operation_name['expire'].count).must_equal(1)
456+
end
457+
458+
it 'adds errors', with_metrics_sdk: true do
459+
skip if redis_gte_5?
460+
461+
redis = redis_with_auth
462+
key = SecureRandom.hex
463+
redis.setex(key, 100, 'string_value')
464+
expect { redis.incr(key) }.must_raise(Redis::CommandError)
465+
466+
last_data_point = metric_snapshot.data_points.last
467+
_(last_data_point.attributes['db.operation.name']).must_equal('incr')
468+
_(last_data_point.attributes['error.type']).must_be_instance_of(String)
469+
_(last_data_point.attributes['error.type']).must_equal('Redis::CommandError')
470+
end
471+
472+
it 'adds errors v5', with_metrics_sdk: true do
473+
skip unless redis_gte_5?
474+
475+
redis = redis_with_auth
476+
key = SecureRandom.hex
477+
redis.setex(key, 100, 'string_value')
478+
expect { redis.incr(key) }.must_raise(Redis::CommandError)
479+
480+
last_data_point = metric_snapshot.data_points.last
481+
_(last_data_point.attributes['db.operation.name']).must_equal('incr')
482+
_(last_data_point.attributes['error.type']).must_equal('RedisClient::CommandError')
483+
end
484+
end
485+
end
392486
end unless ENV['OMIT_SERVICES']

instrumentation/redis/test/test_helper.rb

+1
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@
2121
c.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym)
2222
c.add_span_processor span_processor
2323
end
24+
require 'opentelemetry/test_helpers/metrics'

0 commit comments

Comments
 (0)