From c3127c8fb978426772a3a00ee25b57c67aa5494a Mon Sep 17 00:00:00 2001 From: zvkemp <zvkemp@gmail.com> Date: Thu, 30 Jan 2025 13:58:16 -0500 Subject: [PATCH] add metrics tests --- instrumentation/redis/Gemfile | 1 + .../instrumentation/redis/instrumentation.rb | 36 +++++-- .../redis/middlewares/redis_client_metrics.rb | 22 +++-- .../redis/patches/client_test.rb | 95 ++++++++++++++++++- instrumentation/redis/test/test_helper.rb | 2 + 5 files changed, 139 insertions(+), 17 deletions(-) diff --git a/instrumentation/redis/Gemfile b/instrumentation/redis/Gemfile index 8cc6b11452..9cfd4ca992 100644 --- a/instrumentation/redis/Gemfile +++ b/instrumentation/redis/Gemfile @@ -10,6 +10,7 @@ gemspec group :test do gem 'opentelemetry-instrumentation-base', path: '../base' + gem 'opentelemetry-metrics-test-helpers', path: '../../helpers/metrics-test-helpers', require: false end gem 'pry-byebug' diff --git a/instrumentation/redis/lib/opentelemetry/instrumentation/redis/instrumentation.rb b/instrumentation/redis/lib/opentelemetry/instrumentation/redis/instrumentation.rb index d201fd7355..328fc353fc 100644 --- a/instrumentation/redis/lib/opentelemetry/instrumentation/redis/instrumentation.rb +++ b/instrumentation/redis/lib/opentelemetry/instrumentation/redis/instrumentation.rb @@ -26,9 +26,9 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base # https://opentelemetry.io/docs/specs/semconv/database/database-metrics/#metric-dbclientoperationduration histogram 'db.client.operation.duration', - attributes: { 'db.system.name' => 'redis' }, + attributes: { 'db.system' => 'redis' }, unit: 's', - boundaries: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] + explicit_bucket_boundaries: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] def client_operation_duration_histogram histogram('db.client.operation.duration') @@ -37,8 +37,18 @@ def client_operation_duration_histogram private def require_dependencies - require_relative 'patches/redis_v4_client' if defined?(::Redis) && ::Redis::VERSION < '5' + require_redis_client_dependencies + require_redis_v4_dependencies + end + + def require_redis_v4_dependencies + return unless defined?(::Redis) && Gem::Version.new(Redis::VERSION) < Gem::Version.new('5.0.0') + + require_relative 'patches/redis_v4_client' + require_relative 'patches/redis_v4_client_metrics' + end + def require_redis_client_dependencies return unless defined?(::RedisClient) require_relative 'middlewares/redis_client' @@ -46,9 +56,23 @@ def require_dependencies end def patch_client - ::RedisClient.register(Middlewares::RedisClientInstrumentation) if defined?(::RedisClient) - ::RedisClient.register(Middlewares::RedisClientMetrics) if defined?(::RedisClient) - ::Redis::Client.prepend(Patches::RedisV4Client) if defined?(::Redis) && ::Redis::VERSION < '5' + patch_redis_v4_client + patch_redis_client + end + + def patch_redis_v4_client + return unless defined?(::Redis) && Gem::Version.new(Redis::VERSION) < Gem::Version.new('5.0.0') + + ::Redis::Client.prepend(Patches::RedisV4Client) + ::Redis::Client.prepend(Patches::RedisV4ClientMetrics) if metrics_defined? + end + + # Applies to redis-client or redis >= 5 + def patch_redis_client + return unless defined?(::RedisClient) + + ::RedisClient.register(Middlewares::RedisClientInstrumentation) + ::RedisClient.register(Middlewares::RedisClientMetrics) if metrics_defined? end end end diff --git a/instrumentation/redis/lib/opentelemetry/instrumentation/redis/middlewares/redis_client_metrics.rb b/instrumentation/redis/lib/opentelemetry/instrumentation/redis/middlewares/redis_client_metrics.rb index 1f82095913..8ecfd7a027 100644 --- a/instrumentation/redis/lib/opentelemetry/instrumentation/redis/middlewares/redis_client_metrics.rb +++ b/instrumentation/redis/lib/opentelemetry/instrumentation/redis/middlewares/redis_client_metrics.rb @@ -13,7 +13,8 @@ module RedisClientMetrics def call(command, redis_config) return super unless (histogram = instrumentation.client_operation_duration_histogram) - timed(histogram, command.first, redis_config) do + attributes = metric_attributes(redis_config, command.first) + otel_timed(histogram, attributes) do super end end @@ -21,21 +22,23 @@ def call(command, redis_config) def call_pipelined(commands, redis_config) return super unless (histogram = instrumentation.client_operation_duration_histogram) - timed(histogram, 'PIPELINE', redis_config) do + attributes = metric_attributes(redis_config, 'PIPELINED') + otel_timed(histogram, attributes) do super end end private - def timed(histogram, operation_name, redis_config) + def otel_timed(histogram, attributes) t0 = monotonic_now - - yield.tap do - duration = monotonic_now - t0 - - histogram.record(duration, attributes: metric_attributes(redis_config, operation_name)) - end + yield + rescue StandardError => e + attributes['error.type'] = e.class.to_s + raise + ensure + duration = monotonic_now - t0 + histogram.record(duration, attributes: attributes) end def monotonic_now @@ -44,7 +47,6 @@ def monotonic_now def metric_attributes(redis_config, operation_name) attributes = { - 'db.system' => 'redis', 'db.operation.name' => operation_name, 'net.peer.name' => redis_config.host, 'net.peer.port' => redis_config.port diff --git a/instrumentation/redis/test/opentelemetry/instrumentation/redis/patches/client_test.rb b/instrumentation/redis/test/opentelemetry/instrumentation/redis/patches/client_test.rb index 7fbf262c27..4a50b08f82 100644 --- a/instrumentation/redis/test/opentelemetry/instrumentation/redis/patches/client_test.rb +++ b/instrumentation/redis/test/opentelemetry/instrumentation/redis/patches/client_test.rb @@ -41,9 +41,10 @@ def redis_gte_5? redis_version_major&.>=(5) end + let(:config) { { db_statement: :include } } + before do # ensure obfuscation is off if it was previously set in a different test - config = { db_statement: :include } instrumentation.install(config) exporter.reset end @@ -389,4 +390,96 @@ def redis_gte_5? end end end + + if defined?(OpenTelemetry::Metrics) + describe 'metrics not enabled' do + it 'will not be enabled' do + assert(instrumentation.metrics_defined?) + refute(instrumentation.metrics_enabled?) + end + end + + describe 'metrics enabled' do + let(:config) { { db_statement: :include, metrics: true } } + let(:metric_snapshot) do + metrics_exporter.pull + metrics_exporter.metric_snapshots.last + end + + it 'will be enabled' do + assert(instrumentation.metrics_defined?) + assert(instrumentation.metrics_enabled?) + end + + it 'works', with_metrics_sdk: true do + skip if redis_gte_5? + + redis = redis_with_auth + key = SecureRandom.hex + 10.times { redis.incr(key) } + redis.expire(key, 1) + + _(metric_snapshot.data_points.length).must_equal(3) + + metric_snapshot.data_points.each do |data_point| + _(data_point.attributes['db.system']).must_equal('redis') + end + + by_operation_name = metric_snapshot.data_points.each_with_object({}) { |d, res| res[d.attributes['db.operation.name']] = d } + _(by_operation_name.keys.sort).must_equal(%w[auth expire incr]) + + _(by_operation_name['auth'].count).must_equal(1) + _(by_operation_name['incr'].count).must_equal(10) + _(by_operation_name['expire'].count).must_equal(1) + end + + it 'works v5', with_metrics_sdk: true do + skip unless redis_gte_5? + + redis = redis_with_auth + key = SecureRandom.hex + 10.times { redis.incr(key) } + redis.expire(key, 1) + + _(metric_snapshot.data_points.length).must_equal(3) + + metric_snapshot.data_points.each do |data_point| + _(data_point.attributes['db.system']).must_equal('redis') + end + + by_operation_name = metric_snapshot.data_points.each_with_object({}) { |d, res| res[d.attributes['db.operation.name']] = d } + _(by_operation_name.keys.sort).must_equal(%w[PIPELINED expire incr]) + + _(by_operation_name['PIPELINED'].count).must_equal(1) + _(by_operation_name['incr'].count).must_equal(10) + _(by_operation_name['expire'].count).must_equal(1) + end + + it 'adds errors', with_metrics_sdk: true do + skip if redis_gte_5? + + redis = redis_with_auth + key = SecureRandom.hex + redis.setex(key, 100, 'string_value') + expect { redis.incr(key) }.must_raise(Redis::CommandError) + + last_data_point = metric_snapshot.data_points.last + _(last_data_point.attributes['db.operation.name']).must_equal('incr') + _(last_data_point.attributes['error.type']).must_equal('RedisClient::CommandError') + end + + it 'adds errors v5', with_metrics_sdk: true do + skip unless redis_gte_5? + + redis = redis_with_auth + key = SecureRandom.hex + redis.setex(key, 100, 'string_value') + expect { redis.incr(key) }.must_raise(Redis::CommandError) + + last_data_point = metric_snapshot.data_points.last + _(last_data_point.attributes['db.operation.name']).must_equal('incr') + _(last_data_point.attributes['error.type']).must_equal('RedisClient::CommandError') + end + end + end end unless ENV['OMIT_SERVICES'] diff --git a/instrumentation/redis/test/test_helper.rb b/instrumentation/redis/test/test_helper.rb index 9375d9e5e6..1835b257e9 100644 --- a/instrumentation/redis/test/test_helper.rb +++ b/instrumentation/redis/test/test_helper.rb @@ -21,3 +21,5 @@ c.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym) c.add_span_processor span_processor end + +require 'opentelemetry-metrics-test-helpers'