-
Notifications
You must be signed in to change notification settings - Fork 600
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2180 from newrelic/stripe_instrumentation
Add Stripe instrumentation
- Loading branch information
Showing
7 changed files
with
372 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# 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 | ||
|
||
require 'new_relic/agent/instrumentation/stripe_subscriber' | ||
|
||
DependencyDetection.defer do | ||
named :stripe | ||
|
||
depends_on do | ||
NewRelic::Agent.config[:'instrumentation.stripe'] == 'enabled' | ||
end | ||
|
||
depends_on do | ||
defined?(Stripe) && | ||
Gem::Version.new(Stripe::VERSION) >= Gem::Version.new('5.38.0') | ||
end | ||
|
||
executes do | ||
NewRelic::Agent.logger.info('Installing Stripe instrumentation') | ||
end | ||
|
||
executes do | ||
newrelic_subscriber = NewRelic::Agent::Instrumentation::StripeSubscriber.new | ||
Stripe::Instrumentation.subscribe(:request_begin) { |event| newrelic_subscriber.start_segment(event) } | ||
Stripe::Instrumentation.subscribe(:request_end) { |event| newrelic_subscriber.finish_segment(event) } | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# 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 | ||
module Agent | ||
module Instrumentation | ||
class StripeSubscriber | ||
DEFAULT_DESTINATIONS = AttributeFilter::DST_SPAN_EVENTS | ||
EVENT_ATTRIBUTES = %i[http_status method num_retries path request_id].freeze | ||
ATTRIBUTE_NAMESPACE = 'stripe.user_data' | ||
ATTRIBUTE_FILTER_TYPES = %i[include exclude].freeze | ||
|
||
def start_segment(event) | ||
return unless is_execution_traced? | ||
|
||
segment = NewRelic::Agent::Tracer.start_segment(name: metric_name(event)) | ||
event.user_data[:newrelic_segment] = segment | ||
rescue => e | ||
NewRelic::Agent.logger.error("Error starting New Relic Stripe segment: #{e}") | ||
end | ||
|
||
def finish_segment(event) | ||
return unless is_execution_traced? | ||
|
||
segment = remove_and_return_nr_segment(event) | ||
add_stripe_attributes(segment, event) | ||
add_custom_attributes(segment, event) | ||
rescue => e | ||
NewRelic::Agent.logger.error("Error finishing New Relic Stripe segment: #{e}") | ||
ensure | ||
segment&.finish | ||
end | ||
|
||
private | ||
|
||
def is_execution_traced? | ||
NewRelic::Agent::Tracer.state.is_execution_traced? | ||
end | ||
|
||
def metric_name(event) | ||
"Stripe#{event.path}/#{event.method}" | ||
end | ||
|
||
def add_stripe_attributes(segment, event) | ||
EVENT_ATTRIBUTES.each do |attribute| | ||
segment.add_agent_attribute("stripe_#{attribute}", event.send(attribute), DEFAULT_DESTINATIONS) | ||
end | ||
end | ||
|
||
def add_custom_attributes(segment, event) | ||
return if NewRelic::Agent.config[:'stripe.user_data.include'].empty? | ||
|
||
filtered_attributes = NewRelic::Agent::AttributePreFiltering.pre_filter_hash(event.user_data, nr_attribute_options) | ||
filtered_attributes.each do |key, value| | ||
segment.add_agent_attribute("stripe_user_data_#{key}", value, DEFAULT_DESTINATIONS) | ||
end | ||
end | ||
|
||
def nr_attribute_options | ||
ATTRIBUTE_FILTER_TYPES.each_with_object({}) do |type, opts| | ||
pattern = | ||
NewRelic::Agent::AttributePreFiltering.formulate_regexp_union(:"#{ATTRIBUTE_NAMESPACE}.#{type}") | ||
opts[type] = pattern if pattern | ||
end | ||
end | ||
|
||
def remove_and_return_nr_segment(event) | ||
segment = event.user_data[:newrelic_segment] | ||
event.user_data.delete(:newrelic_segment) | ||
|
||
segment | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# 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 | ||
|
||
# While Stripe instrumentation doesn't do any monkey patching, we need to | ||
# include an instrumentation method for multiverse to run the tests | ||
instrumentation_methods :chain | ||
|
||
STRIPE_VERSIONS = [ | ||
[nil, 2.4], | ||
['5.38.0', 2.4] | ||
] | ||
|
||
def gem_list(stripe_version = nil) | ||
"gem 'stripe'#{stripe_version}" | ||
end | ||
|
||
create_gemfiles(STRIPE_VERSIONS) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
--- | ||
development: | ||
error_collector: | ||
enabled: true | ||
apdex_t: 0.5 | ||
agent_enabled: true | ||
monitor_mode: true | ||
license_key: bootstrap_newrelic_admin_license_key_000 | ||
app_name: test | ||
host: localhost | ||
api_host: localhost | ||
port: <%= $collector && $collector.port %> | ||
transaction_tracer: | ||
record_sql: obfuscated | ||
enabled: true | ||
stack_trace_threshold: 0.5 | ||
transaction_threshold: 1.0 | ||
capture_params: false | ||
disable_serialization: false |
191 changes: 191 additions & 0 deletions
191
test/multiverse/suites/stripe/stripe_instrumentation_test.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
# 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 | ||
|
||
require 'json' | ||
require 'stripe' | ||
require 'net/http' | ||
|
||
class StripeInstrumentation < Minitest::Test | ||
API_KEY = '123456789' | ||
STRIPE_URL = 'Stripe/v1/customers/get' | ||
|
||
def setup | ||
Stripe.api_key = API_KEY | ||
# Creating a new connection and response, which both get stubbed | ||
# later, helps us get around needing to provide a valid API key | ||
@connection = Stripe::ConnectionManager.new | ||
@response = Net::HTTPResponse.new('1.1', '200', 'OK') | ||
# Bypass #stream_check ("attempt to read body out of block") | ||
@response.instance_variable_set(:@read, true) | ||
@response.body = { | ||
object: 'list', | ||
data: [{'id': '12134'}], | ||
has_more: false, | ||
url: STRIPE_URL | ||
}.to_json | ||
end | ||
|
||
def test_version_supported | ||
assert(Gem::Version.new(Stripe::VERSION) >= Gem::Version.new('5.38.0')) | ||
end | ||
|
||
def test_subscribed_request_begin | ||
subcribers = Stripe::Instrumentation.send(:subscribers) | ||
newrelic_begin_subscriber = subcribers[:request_begin].detect { |_k, v| v.to_s.include?('instrumentation/stripe') } | ||
|
||
assert(newrelic_begin_subscriber) | ||
end | ||
|
||
def test_subscribed_request_end | ||
subcribers = Stripe::Instrumentation.send(:subscribers) | ||
newrelic_begin_subscriber = subcribers[:request_end].detect { |_k, v| v.to_s.include?('instrumentation/stripe') } | ||
|
||
assert(newrelic_begin_subscriber) | ||
end | ||
|
||
def test_newrelic_segment | ||
with_stubbed_connection_manager do | ||
in_transaction do |txn| | ||
start_stripe_event | ||
stripe_segment = stripe_segment_from_transaction(txn) | ||
|
||
assert(stripe_segment) | ||
end | ||
end | ||
end | ||
|
||
def test_agent_collects_user_data_attributes_when_configured | ||
Stripe::Instrumentation.subscribe(:request_begin) do |events| | ||
events.user_data[:cat] = 'meow' | ||
events.user_data[:dog] = 'woof' | ||
end | ||
|
||
with_config(:'stripe.user_data.include' => '.') do | ||
with_stubbed_connection_manager do | ||
in_transaction do |txn| | ||
start_stripe_event | ||
stripe_segment = stripe_segment_from_transaction(txn) | ||
|
||
assert(stripe_segment) | ||
stripe_attributes = stripe_segment.attributes.agent_attributes_for(NewRelic::Agent::AttributeFilter::DST_SPAN_EVENTS) | ||
|
||
assert_equal('meow', stripe_attributes['stripe_user_data_cat']) | ||
assert_equal('woof', stripe_attributes['stripe_user_data_dog']) | ||
end | ||
end | ||
end | ||
end | ||
|
||
def test_agent_collects_select_user_data_attributes | ||
Stripe::Instrumentation.subscribe(:request_begin) do |events| | ||
events.user_data[:frog] = 'ribbit' | ||
events.user_data[:sheep] = 'baa' | ||
events.user_data[:cow] = 'moo' | ||
end | ||
|
||
with_config(:'stripe.user_data.include' => 'frog, sheep') do | ||
with_stubbed_connection_manager do | ||
in_transaction do |txn| | ||
start_stripe_event | ||
stripe_segment = stripe_segment_from_transaction(txn) | ||
|
||
assert(stripe_segment) | ||
stripe_attributes = stripe_segment.attributes.agent_attributes_for(NewRelic::Agent::AttributeFilter::DST_SPAN_EVENTS) | ||
|
||
assert_equal('ribbit', stripe_attributes['stripe_user_data_frog']) | ||
assert_equal('baa', stripe_attributes['stripe_user_data_sheep']) | ||
assert_nil(stripe_attributes['stripe_user_data_cow']) | ||
end | ||
end | ||
end | ||
end | ||
|
||
def test_agent_ignores_user_data_attributes | ||
Stripe::Instrumentation.subscribe(:request_begin) do |events| | ||
events.user_data[:bird] = 'tweet' | ||
end | ||
|
||
with_config('stripe.user_data.include': %w[.], | ||
'stripe.user_data.exclude': %w[bird]) do | ||
with_stubbed_connection_manager do | ||
in_transaction do |txn| | ||
start_stripe_event | ||
stripe_segment = stripe_segment_from_transaction(txn) | ||
|
||
assert(stripe_segment) | ||
stripe_attributes = stripe_segment.attributes.agent_attributes_for(NewRelic::Agent::AttributeFilter::DST_SPAN_EVENTS) | ||
|
||
assert_nil(stripe_attributes['stripe_user_data_bird']) | ||
end | ||
end | ||
end | ||
end | ||
|
||
def test_agent_ignores_user_data_values | ||
Stripe::Instrumentation.subscribe(:request_begin) do |events| | ||
events.user_data[:contact_name] = 'Jenny' | ||
events.user_data[:contact_phone] = '867-5309' | ||
end | ||
|
||
with_config('stripe.user_data.include': %w[.], | ||
'stripe.user_data.exclude': ['^\d{3}-\d{4}$']) do | ||
with_stubbed_connection_manager do | ||
in_transaction do |txn| | ||
start_stripe_event | ||
stripe_segment = stripe_segment_from_transaction(txn) | ||
|
||
assert(stripe_segment) | ||
stripe_attributes = stripe_segment.attributes.agent_attributes_for(NewRelic::Agent::AttributeFilter::DST_SPAN_EVENTS) | ||
|
||
assert(stripe_attributes['stripe_user_data_contact_name']) | ||
assert_nil(stripe_attributes['stripe_user_data_contact_phone']) | ||
end | ||
end | ||
end | ||
end | ||
|
||
def test_start_when_not_traced | ||
with_stubbed_connection_manager do | ||
NewRelic::Agent::Tracer.state.stub(:is_execution_traced?, false) do | ||
in_transaction do |txn| | ||
start_stripe_event | ||
stripe_segment = stripe_segment_from_transaction(txn) | ||
|
||
refute stripe_segment | ||
end | ||
end | ||
end | ||
end | ||
|
||
def test_start_segment_records_error | ||
NewRelic::Agent.stub(:logger, NewRelic::Agent::MemoryLogger.new) do | ||
bad_event = OpenStruct.new(path: 'v1/charges', method: 'get') | ||
NewRelic::Agent::Instrumentation::StripeSubscriber.new.start_segment(bad_event) | ||
|
||
assert_logged(/Error starting New Relic Stripe segment/m) | ||
end | ||
end | ||
|
||
def start_stripe_event | ||
Stripe::Customer.list({limit: 3}) | ||
end | ||
|
||
def stripe_segment_from_transaction(txn) | ||
txn.segments.detect { |s| s.name == STRIPE_URL } | ||
end | ||
|
||
def with_stubbed_connection_manager(&block) | ||
Stripe::StripeClient.stub(:default_connection_manager, @connection) do | ||
@connection.stub(:execute_request, @response) do | ||
yield | ||
end | ||
end | ||
end | ||
|
||
def assert_logged(expected) | ||
found = NewRelic::Agent.logger.messages.any? { |m| m[1][0].match?(expected) } | ||
|
||
assert(found, "Didn't see log message: '#{expected}'") | ||
end | ||
end |