Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for redis-clustering gem #2720

Merged
merged 15 commits into from
Jul 8, 2024
Merged
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# New Relic Ruby Agent Release Notes

## <dev>

Version <dev> improves instrumentation for the `redis-clustering` gem.

- **Feature: Add instrumentation for redis-clustering**

Version 5.x of the `redis` gem moved cluster behavior into a different gem, `redis-clustering`. This gem can access instrumentation registered through `RedisClient::Middleware`. Previously, the agent only instrumented the `call_pipelined` method through this approach, but now users of the `redis-clustering` gem will also have instrumentation registered for `connect` and `call` methods. In addition, the way the `database_name` attribute is set for Redis datastore spans is now compatible with all versions of Redis supported by the New Relic Ruby agent. Thank you, [@praveen-ks](https://github.com/praveen-ks) for bringing this to our attention. [Issue#2444](https://github.com/newrelic/newrelic-ruby-agent/issues/2444) [PR#2720](https://github.com/newrelic/newrelic-ruby-agent/pull/2720)

## v9.11.0

Version 9.11.0 introduces instrumentation for the aws-sdk-sqs gem, fixes a bug related to expected errors not bearing a "true" value for the "expected" attribute if expected as a result of an HTTP status code match and changes the way Stripe instrumentation metrics are named to prevent high-cardinality issues.
Expand Down
5 changes: 5 additions & 0 deletions lib/new_relic/agent/instrumentation/redis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require_relative 'redis/constants'
require_relative 'redis/prepend'
require_relative 'redis/middleware'
require_relative 'redis/cluster_middleware'

DependencyDetection.defer do
# Why not :redis? newrelic-redis used that name, so avoid conflicting
Expand All @@ -33,6 +34,10 @@
NewRelic::Agent.logger.info('Installing Redis Instrumentation')
if NewRelic::Agent::Instrumentation::Redis::Constants::HAS_REDIS_CLIENT
RedisClient.register(NewRelic::Agent::Instrumentation::RedisClient::Middleware)

if defined?(Redis::Cluster::Client)
return RedisClient.register(NewRelic::Agent::Instrumentation::RedisClient::ClusterMiddleware)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redis-clustering doesn't need prepend nor chain instrumentation, those methods are instrumented in the ClusterMiddleware.

end
end

if use_prepend?
Expand Down
26 changes: 26 additions & 0 deletions lib/new_relic/agent/instrumentation/redis/cluster_middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

module NewRelic::Agent::Instrumentation
module RedisClient
module ClusterMiddleware
include NewRelic::Agent::Instrumentation::Redis

# Until we decide to move our Redis instrumentation entirely off patches
# keep the middleware instrumentation for the call and connect methods
# limited to the redis-clustering instrumentation.
#
# Redis's middleware option does not capture errors as high in the stack
# as our patches. Leaving the patches for call and connect on the main
# Redis gem limits the feature disparity our customers experience.
Comment on lines +10 to +16
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this as a way to have a clean break for new Redis instrumentation. However, it does provide an inferior experience for error handling. We can look more closely into monkey-patching these methods, but we'll need to patch methods differently than we do for the main Redis gem.

def call(*args, &block)
call_with_tracing(args[0]) { super }
end

def connect(*args, &block)
connect_with_tracing { super }
end
end
end
end
25 changes: 14 additions & 11 deletions lib/new_relic/agent/instrumentation/redis/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,30 @@ module Redis
INSTRUMENTATION_NAME = NewRelic::Agent.base_name(name)

def connect_with_tracing
with_tracing(Constants::CONNECT, database: db) { yield }
with_tracing(Constants::CONNECT, database: _nr_db) { yield }
end

def call_with_tracing(command, &block)
operation = command[0]
statement = ::NewRelic::Agent::Datastores::Redis.format_command(command)

with_tracing(operation, statement: statement, database: db) { yield }
with_tracing(operation, statement: statement, database: _nr_db) { yield }
end

# Used for Redis 4.x and 3.x
def call_pipeline_with_tracing(pipeline)
operation = pipeline.is_a?(::Redis::Pipeline::Multi) ? Constants::MULTI_OPERATION : Constants::PIPELINE_OPERATION
statement = ::NewRelic::Agent::Datastores::Redis.format_pipeline_commands(pipeline.commands)

with_tracing(operation, statement: statement, database: db) { yield }
with_tracing(operation, statement: statement, database: _nr_db) { yield }
end

# Used for Redis 5.x+
def call_pipelined_with_tracing(pipeline)
db = begin
_nr_redis_client_config.db
rescue StandardError => e
NewRelic::Agent.logger.error("Failed to determine configured Redis db value: #{e.class} - #{e.message}")
nil
end

operation = pipeline.flatten.include?('MULTI') ? Constants::MULTI_OPERATION : Constants::PIPELINE_OPERATION
statement = ::NewRelic::Agent::Datastores::Redis.format_pipeline_commands(pipeline)

with_tracing(operation, statement: statement, database: db) { yield }
with_tracing(operation, statement: statement, database: _nr_db) { yield }
end

private
Expand Down Expand Up @@ -94,5 +87,15 @@ def _nr_redis_client_config
config
end
end

def _nr_db
# db is a method on the Redis client in versions < 5.x
return db if respond_to?(:db)
# db is accessible through the RedisClient::Config object in versions > 5.x
return _nr_redis_client_config.db if _nr_redis_client_config.respond_to?(:db)
rescue StandardError => e
NewRelic::Agent.logger.debug("Failed to determine configured Redis db value: #{e.class} - #{e.message}")
nil
end
end
end
3 changes: 3 additions & 0 deletions lib/new_relic/agent/instrumentation/redis/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module NewRelic::Agent::Instrumentation
module RedisClient
module Middleware
# This module is used to instrument Redis 5.x+
#
# It only instruments call_pipelined because connect and call are accessed
# too late in the stack to capture all errors
include NewRelic::Agent::Instrumentation::Redis

def call_pipelined(*args, &block)
Expand Down
13 changes: 12 additions & 1 deletion test/multiverse/suites/redis/Envfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@ def gem_list(redis_version = nil)
<<~RB
gem 'rack'
gem 'redis'#{redis_version}

RB
end

create_gemfiles(REDIS_VERSIONS)

# We do not spin up a full Redis cluster in the testing environment, which
# limits our ability to run unit tests on the redis-clustering behavior.
# Since the testing capability is limited, only test the latest version of the
# redis-clustering gem, which itself requires Ruby v2.7+.
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0')
gemfile <<~RB
gem 'rack'
gem 'redis-clustering'
RB
end
16 changes: 16 additions & 0 deletions test/multiverse/suites/redis/redis_instrumentation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def test_records_metrics_for_connect
end

def test_records_connect_tt_node_within_call_that_triggered_it
skip_for_redis_clustering_gem
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end, it seemed the least intrusive to add skips for the six tests that are not compatible with redis-clustering instrumentation:

  • We can't test with Redis::Cluster.new because we don't have a cluster mode enabled service running for our tests, so we're faking the "cluster" testing regardless
  • We don't have a different configuration option for redis-clustering than we do for redis, so the prepend/chain configurations seemed excessive
  • Skipping the tests feels easier to read and understand the feature disparity between the two gems quickly

Open to further discussion to take a different approach!


in_transaction do
redis = Redis.new
redis.get('foo')
Expand Down Expand Up @@ -277,6 +279,8 @@ def test_records_instance_parameters_on_tt_node_for_get
end

def test_records_hostname_on_tt_node_for_get_with_unix_domain_socket
skip_for_redis_clustering_gem

redis = Redis.new
redis.send(client).stubs(:path).returns('/tmp/redis.sock')

Expand Down Expand Up @@ -309,6 +313,8 @@ def test_records_instance_parameters_on_tt_node_for_multi
end

def test_records_hostname_on_tt_node_for_multi_with_unix_domain_socket
skip_for_redis_clustering_gem

redis = Redis.new
redis.send(client).stubs(:path).returns('/tmp/redis.sock')

Expand All @@ -327,6 +333,8 @@ def test_records_hostname_on_tt_node_for_multi_with_unix_domain_socket
end

def test_records_unknown_unknown_metric_when_error_gathering_instance_data
skip_for_redis_clustering_gem

redis = Redis.new
redis.send(client).stubs(:path).raises(StandardError.new)
in_transaction do
Expand All @@ -347,6 +355,8 @@ def simulate_read_error
end

def test_noticed_error_at_segment_and_txn_on_error
skip_for_redis_clustering_gem

txn = nil
begin
in_transaction do |redis_txn|
Expand All @@ -362,6 +372,8 @@ def test_noticed_error_at_segment_and_txn_on_error
end

def test_noticed_error_only_at_segment_on_error
skip_for_redis_clustering_gem

txn = nil
in_transaction do |redis_txn|
begin
Expand Down Expand Up @@ -472,4 +484,8 @@ def either_hostname
[NewRelic::Agent::Hostname.get, 'redis']
end
end

def skip_for_redis_clustering_gem
skip 'Incompatible with redis-clustering' if defined?(Redis::Cluster::Client)
end
end
Loading