diff --git a/.env.example b/.env.example index 6d7d3c821e..489289cb3b 100644 --- a/.env.example +++ b/.env.example @@ -78,6 +78,7 @@ PERIODICAL=stop_expired_deploys:60,remove_expired_locks:60,report_system_stats:6 ## StatsD reporting # STATSD_HOST=192.168.1.1 # STATSD_PORT=8125 +# DATADOG_TRACER=1 # optional, enable datadog APM tracer # PROJECT_CREATED_NOTIFY_ADDRESS=bobby-the-security-auditor@yourcompany.com # PROJECT_DELETED_NOTIFY_ADDRESS=bobby-the-security-auditor@yourcompany.com # if not set uses PROJECT_CREATED_NOTIFY_ADDRESS diff --git a/.gitignore b/.gitignore index 968f646436..0269f75644 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,4 @@ # .bak files *.bak -/vendor/cache/**/ +/vendor/cache diff --git a/Gemfile.lock b/Gemfile.lock index f70a613d67..c440982773 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,6 +21,12 @@ PATH samson_aws_ecr (0.0.0) aws-sdk-ecr +PATH + remote: plugins/datadog_tracer + specs: + samson_datadog_tracer (0.0.1) + ddtrace + PATH remote: plugins/datadog specs: @@ -230,6 +236,8 @@ GEM safe_yaml (~> 1.0.0) crass (1.0.4) dalli (2.7.6) + ddtrace (0.14.2) + msgpack diffy (3.2.0) docker-api (1.33.6) excon (>= 0.38.0) @@ -611,6 +619,7 @@ DEPENDENCIES samson_assertible! samson_aws_ecr! samson_datadog! + samson_datadog_tracer! samson_deploy_env_vars! samson_docker_binary_builder! samson_env! diff --git a/app/models/deploy_service.rb b/app/models/deploy_service.rb index cfba4f925a..95cc61d929 100644 --- a/app/models/deploy_service.rb +++ b/app/models/deploy_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class DeployService - include ::NewRelic::Agent::MethodTracer + include ::Samson::PerformanceTracer attr_reader :user def initialize(user) @@ -99,7 +99,7 @@ def send_before_notifications(deploy) DeployMailer.bypass_email(deploy, user).deliver_now end end - add_method_tracer :send_before_notifications + add_tracer :send_before_notifications def send_after_notifications(deploy) Samson::Hooks.fire(:after_deploy, deploy, deploy.buddy) @@ -108,7 +108,7 @@ def send_after_notifications(deploy) execute_and_log_errors(deploy) { send_failed_deploy_email(deploy) } execute_and_log_errors(deploy) { notify_outbound_webhooks(deploy) } end - add_method_tracer :send_after_notifications + add_tracer :send_after_notifications # basically does the same as the hooks would do def execute_and_log_errors(deploy, &block) diff --git a/app/models/docker_builder_service.rb b/app/models/docker_builder_service.rb index dac4432dac..111bd1b501 100644 --- a/app/models/docker_builder_service.rb +++ b/app/models/docker_builder_service.rb @@ -2,7 +2,7 @@ require 'docker' class DockerBuilderService - include ::NewRelic::Agent::MethodTracer + include ::Samson::PerformanceTracer def initialize(build) @build = build @@ -61,7 +61,7 @@ def before_docker_build(tmp_dir) Samson::Hooks.fire(:before_docker_build, tmp_dir, @build, @output) execute_build_command(tmp_dir, @build.project.build_command) end - add_method_tracer :before_docker_build + add_tracer :before_docker_build def build_image(tmp_dir, tag_as_latest:) File.write("#{tmp_dir}/REVISION", @build.git_sha) @@ -83,5 +83,5 @@ def build_image(tmp_dir, tag_as_latest:) tmp_dir, @build, @execution.executor, tag_as_latest: tag_as_latest, cache_from: cache ) end - add_method_tracer :build_image + add_tracer :build_image end diff --git a/app/models/git_repository.rb b/app/models/git_repository.rb index ff2d694dc3..f70560e869 100644 --- a/app/models/git_repository.rb +++ b/app/models/git_repository.rb @@ -2,7 +2,7 @@ # Responsible for all git knowledge of a repo # Caches a local mirror (not a full checkout) and creates a workspace when deploying class GitRepository - include ::NewRelic::Agent::MethodTracer + include ::Samson::PerformanceTracer attr_accessor :executor # others set this to listen in on commands being executed @@ -65,7 +65,7 @@ def branches def clean! FileUtils.rm_rf(repo_cache_dir) end - add_method_tracer :clean! + add_tracer :clean! def valid_url? return false if repository_url.blank? @@ -127,17 +127,17 @@ def outside_caller def clone! executor.execute "git -c core.askpass=true clone --mirror #{repository_url} #{repo_cache_dir}" end - add_method_tracer :clone! + add_tracer :clone! def create_workspace(temp_dir) executor.execute "git clone #{repo_cache_dir} #{temp_dir}" end - add_method_tracer :create_workspace + add_tracer :create_workspace! def update! executor.execute("cd #{repo_cache_dir}", 'git fetch -p') end - add_method_tracer :update! + add_tracer :update! def sha_exist?(sha) !!capture_stdout("git", "cat-file", "-t", sha) diff --git a/app/models/image_builder.rb b/app/models/image_builder.rb index b8214eef9a..1f5df35e9c 100644 --- a/app/models/image_builder.rb +++ b/app/models/image_builder.rb @@ -6,7 +6,7 @@ class ImageBuilder class << self DIGEST_SHA_REGEX = /Digest:.*(sha256:[0-9a-f]{64})/i - include ::NewRelic::Agent::MethodTracer + include ::Samson::PerformanceTracer def build_image(dir, build, executor, tag_as_latest:, **args) if DockerRegistry.all.empty? @@ -86,7 +86,7 @@ def push_image(image_id, build, executor, tag_as_latest:) executor.output.puts("Docker push failed: #{e.message}\n") nil end - add_method_tracer :push_image + add_tracer :push_image def push_image_to_registries(image_id, build, executor, tag:, override_tag:) digest = nil diff --git a/app/models/job_execution.rb b/app/models/job_execution.rb index 7b6febda7f..9c6583882e 100644 --- a/app/models/job_execution.rb +++ b/app/models/job_execution.rb @@ -2,7 +2,7 @@ require 'shellwords' class JobExecution - include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation + include ::Samson::PerformanceTracer cattr_accessor(:cancel_timeout, instance_writer: false) { 15.seconds } @@ -140,7 +140,7 @@ def run ensure finish unless @cancelled end - add_transaction_tracer :run, + add_asynchronous_tracer :run, category: :task, params: '{ job_id: id, project: job.project.try(:name), reference: reference }' diff --git a/app/models/multi_lock.rb b/app/models/multi_lock.rb index dd8cfb2124..545b746bc1 100644 --- a/app/models/multi_lock.rb +++ b/app/models/multi_lock.rb @@ -4,7 +4,7 @@ class MultiLock cattr_accessor(:locks) { {} } class << self - include ::NewRelic::Agent::MethodTracer + include ::Samson::PerformanceTracer def lock(id, holder, options) locked = wait_for_lock(id, holder, options) @@ -25,7 +25,7 @@ def wait_for_lock(id, holder, options) end false end - add_method_tracer :wait_for_lock + add_tracer :wait_for_lock def try_lock(id, holder) mutex.synchronize do diff --git a/docs/plugins.md b/docs/plugins.md index d0ade732d7..3768f9d88b 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -12,6 +12,7 @@ Available plugins: - [Airbrake error handling](https://github.com/zendesk/samson/tree/master/plugins/airbrake) - [AWS ECR credential refresher](https://github.com/zendesk/samson/tree/master/plugins/aws_ecr) - [Datadog monitoring and deploy tracking](https://github.com/zendesk/samson/tree/master/plugins/datadog) + - [Datadog APM tracer](https://github.com/zendesk/samson/tree/master/plugins/datadog_tracer) - [Deploy Environment Variables](https://github.com/zendesk/samson/tree/master/plugins/deploy_env_vars) - [Docker binary builder](https://github.com/zendesk/samson/tree/master/plugins/docker_binary_builder) - [ENV var management](https://github.com/zendesk/samson/tree/master/plugins/env) diff --git a/lib/samson/hooks.rb b/lib/samson/hooks.rb index 46bb73718a..8db322af2c 100644 --- a/lib/samson/hooks.rb +++ b/lib/samson/hooks.rb @@ -57,7 +57,9 @@ class UserError < StandardError :ref_status, :release_deploy_conditions, :stage_clone, - :stage_permitted_params + :stage_permitted_params, + :performance_tracer, + :asynchronous_performance_tracer ].freeze KNOWN = VIEW_HOOKS + EVENT_HOOKS @@ -164,7 +166,7 @@ def only_callbacks_for_plugin(plugin_name, hook_name) # use def fire(name, *args) - NewRelic::Agent::MethodTracerHelpers.trace_execution_scoped("Custom/Hooks/#{name}") do + Samson::PerformanceTracer.trace_execution_scoped("Custom/Hooks/#{name}") do hooks(name).map { |hook| hook.call(*args) } end end diff --git a/lib/samson/performance_tracer.rb b/lib/samson/performance_tracer.rb new file mode 100644 index 0000000000..36e956e86d --- /dev/null +++ b/lib/samson/performance_tracer.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +module Samson + module PerformanceTracer + # It's used to trace the hook fire event, + # We can't use the samson hook to fetch the available plugins. + TRACER_PLUGINS = ['SamsonNewRelic', 'SamsonDatadogTracer::APM'].freeze + + class << self + def included(clazz) + clazz.extend ClassMethods + end + + def trace_execution_scoped(scope_name) + # Tracing the scope is restricted to avoid into slow startup + # Refer Samson::BootCheck + if ['staging', 'production'].include?(Rails.env) + plugins = TRACER_PLUGINS.map(&:safe_constantize).compact + execution = using_plugins plugins, scope_name do + yield + end + execution.call + else + yield + end + end + + def using_plugins(plugins, scope_name, &block) + plugins.inject(block) { |inner, plugin| plugin.trace_method_execution_scope(scope_name) { inner } } + end + end + + # for Newrelic and Datadog -> for tracer plugins. + module ClassMethods + def add_tracer(method) + Samson::Hooks.fire(:performance_tracer, self, method) + end + + # TODO: Add asynchronous tracer for Datadog. + def add_asynchronous_tracer(method, options) + Samson::Hooks.fire(:asynchronous_performance_tracer, self, method, options) + end + end + end +end diff --git a/plugins/datadog_tracer/README.md b/plugins/datadog_tracer/README.md new file mode 100644 index 0000000000..59d9ba33f0 --- /dev/null +++ b/plugins/datadog_tracer/README.md @@ -0,0 +1,3 @@ +# Datadog Plugin + +Plugin that trace requests and notify to Datadog APM diff --git a/plugins/datadog_tracer/config/initializers/datadog_tracer.rb b/plugins/datadog_tracer/config/initializers/datadog_tracer.rb new file mode 100644 index 0000000000..bfa520d7b9 --- /dev/null +++ b/plugins/datadog_tracer/config/initializers/datadog_tracer.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +if SamsonDatadogTracer.enabled? + + SamsonDatadogTracer::IGNORED_URLS = Set[ + '/ping', + '/cable', + ].freeze + + require 'ddtrace' + Datadog.configure do |c| + # Tracer + c.tracer( + hostname: ENV['STATSD_HOST'] || '127.0.0.1', + tags: { + env: ENV['RAILS_ENV'], + 'rails.version': Rails.version, + 'ruby.version': RUBY_VERSION + } + ) + + c.use :rails, + service_name: 'samson', + controller_service: 'samson-rails-controller', + cache_service: 'samson-cache', + database_service: 'samson-mysql', + distributed_tracing: true + + c.use :faraday, service_name: 'samson-faraday' + c.use :dalli, service_name: 'samson-dalli' + + require 'aws-sdk-ecr' + c.use :aws, service_name: 'samson-aws' + end + + # Span Filters + # Filter out the health checks, version checks, and diagnostics + uninteresting_controller_filter = Datadog::Pipeline::SpanFilter.new do |span| + span.name == 'rack.request' && + SamsonDatadogTracer::IGNORED_URLS.any? { |path| span.get_tag('http.url').include?(path) } + end + + Datadog::Pipeline.before_flush(uninteresting_controller_filter) +end diff --git a/plugins/datadog_tracer/lib/samson_datadog_tracer/apm.rb b/plugins/datadog_tracer/lib/samson_datadog_tracer/apm.rb new file mode 100644 index 0000000000..71f3f31133 --- /dev/null +++ b/plugins/datadog_tracer/lib/samson_datadog_tracer/apm.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true +module SamsonDatadogTracer + module APM + class << self + def included(clazz) + clazz.extend ClassMethods + end + + def trace_method_execution_scope(scope_name) + if SamsonDatadogTracer.enabled? + Datadog.tracer.trace("Custom/Hooks/#{scope_name}") do + yield + end + else + yield + end + end + end + + module ClassMethods + def trace_method(method) + return unless SamsonDatadogTracer.enabled? + # Wrap the helper methods and alias method into the module. + # prepend the wraped module to base class + @apm_module ||= begin + mod = Module.new + mod.extend(SamsonDatadogTracer::APM::Helpers) + prepend(mod) + mod + end + if method_defined?(method) || private_method_defined?(method) + _add_wrapped_method_to_module(method) + end + + @traced_methods ||= [] + @traced_methods << method + end + + private + + def _add_wrapped_method_to_module(method) + klass = self + + @apm_module.module_eval do + _wrap_method(method, klass) + end + end + end + + module Helpers + class << self + def sanitize_name(name) + name.to_s.parameterize.tr('-', '_') + end + + def tracer_method_name(method_name) + "#{sanitize_name(method_name)}_with_apm_tracer" + end + + def untracer_method_name(method_name) + "#{sanitize_name(method_name)}_with_apm_untracer" + end + end + + private + + def _wrap_method(method, klass) + visibility = _original_visibility(method, klass) + _define_traced_method(method, "#{klass}##{method}") + _set_visibility(method, visibility) + end + + def _original_visibility(method, klass) + if klass.protected_method_defined?(method) + :protected + elsif klass.private_method_defined?(method) + :private + else + :public + end + end + + def _define_traced_method(method, trace_name) + define_method(Helpers.tracer_method_name(method)) do |*args, &block| + Datadog.tracer.trace(trace_name) do + send(Helpers.untracer_method_name(method), *args, &block) + end + end + end + + def _set_visibility(method, visibility) + method_name = Helpers.tracer_method_name(method) + case visibility + when :protected + protected(method_name) + when :private + private(method_name) + else + public(method_name) + end + end + end + end +end diff --git a/plugins/datadog_tracer/lib/samson_datadog_tracer/samson_plugin.rb b/plugins/datadog_tracer/lib/samson_datadog_tracer/samson_plugin.rb new file mode 100644 index 0000000000..e4130fc4ec --- /dev/null +++ b/plugins/datadog_tracer/lib/samson_datadog_tracer/samson_plugin.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +module SamsonDatadogTracer + class Engine < Rails::Engine + end + + def self.enabled? + !!ENV['DATADOG_TRACER'] + end +end + +Samson::Hooks.callback :performance_tracer do |klass, method| + if SamsonDatadogTracer.enabled? + klass.class_eval do + include SamsonDatadogTracer::APM + helper = SamsonDatadogTracer::APM::Helpers + trace_method method + + if method_defined?(method) || private_method_defined?(method) + alias_method helper.untracer_method_name(method), method + alias_method method, helper.tracer_method_name(method) + end + end + end +end diff --git a/plugins/datadog_tracer/samson_datadog_tracer.gemspec b/plugins/datadog_tracer/samson_datadog_tracer.gemspec new file mode 100644 index 0000000000..9f85001d3c --- /dev/null +++ b/plugins/datadog_tracer/samson_datadog_tracer.gemspec @@ -0,0 +1,9 @@ +# frozen_string_literal: true +Gem::Specification.new 'samson_datadog_tracer', '0.0.1' do |s| + s.summary = 'Samson Datadog tracer plugin' + s.authors = ['Sathish Subramanian'] + s.email = ['ssubramanian@zendesk.com'] + s.files = Dir['{config,lib}/**/*'] + + s.add_runtime_dependency 'ddtrace' +end diff --git a/plugins/datadog_tracer/test/samson_datadog_tracer/apm_test.rb b/plugins/datadog_tracer/test/samson_datadog_tracer/apm_test.rb new file mode 100644 index 0000000000..ab3af11e5e --- /dev/null +++ b/plugins/datadog_tracer/test/samson_datadog_tracer/apm_test.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true +require_relative "../test_helper" + +SingleCov.covered! + +describe SamsonDatadogTracer::APM do + module DDTracer + def self.trace(*) + yield + end + end + + module Datadog + def self.tracer + DDTracer + end + end + + describe ".trace_method_execution_scope" do + it "skips tracer when disabled" do + with_env DATADOG_TRACER: nil do + Datadog.expects(:tracer).never + SamsonDatadogTracer::APM.trace_method_execution_scope("test") { "without tracer" } + end + end + + it "trigger tracer when enabled" do + with_env DATADOG_TRACER: "1" do + Rails.stubs(:env).returns("staging") + Datadog.expects(:tracer).returns(DDTracer) + SamsonDatadogTracer::APM.trace_method_execution_scope("test") { "with tracer" } + end + end + end + + class TestKlass1 + include SamsonDatadogTracer::APM + SamsonDatadogTracer::APM.module_eval { include Datadog } + + def pub_method + :pub + end + + trace_method :pub_method + end + + describe "skips APM trace methods" do + let(:klass) { TestKlass1.new } + it "skips tracker when apm is not enabled" do + Datadog.expects(:tracer).never + klass.send(:pub_method) + end + end + + ENV.store("DATADOG_TRACER", "1") + class TestKlass2 + include SamsonDatadogTracer::APM + SamsonDatadogTracer::APM.module_eval { include Datadog } + + def pub_method + :pub + end + + protected + + def pro_method + :pro + end + + private + + def pri_method + :pri + end + + trace_method :pub_method + trace_method :pri_method + trace_method :pro_method + trace_method :not_method + alias_method :pub_method_with_apm_untracer, :pub_method + end + + describe ".trace_method" do + let(:apm) { TestKlass2.new } + Datadog.expects(:tracer).returns(Datadog.tracer) + + it "wraps the private method in a trace call" do + apm.send(:pri_method).must_equal(:pri) + end + + it "wraps the public method in a trace call" do + apm.send(:pub_method).must_equal(:pub) + end + + it "raises with NoMethodError when undefined method call" do + assert_raise NoMethodError do + apm.send(:not_method) + end + end + + it "preserves method visibility" do + assert apm.class.public_method_defined?(:pub_method) + refute apm.class.public_method_defined?(:pri_method) + assert apm.class.private_method_defined?(:pri_method) + end + end + + describe "#Alias methods" do + let(:apm) { TestKlass2.new } + it "defines alias methods for trace" do + assert apm.class.method_defined?("pub_method_with_apm_tracer") + assert apm.class.private_method_defined?("pri_method_with_apm_tracer") + end + + it "defines alias methods for untrace" do + assert apm.class.method_defined?("pub_method_with_apm_untracer") + refute apm.class.private_method_defined?("pri_method_with_apm_untracer") + end + + it "reponds to alias methods" do + apm.send(:pub_method_with_apm_tracer).must_equal(:pub) + end + end + + describe "#Helpers" do + it "returns sanitize name" do + SamsonDatadogTracer::APM::Helpers.sanitize_name("Test@123").must_equal("test_123") + end + + it "returns tracer method name" do + SamsonDatadogTracer::APM::Helpers.tracer_method_name("test_method").must_equal("test_method_with_apm_tracer") + end + + it "returns untracer method name" do + SamsonDatadogTracer::APM::Helpers.untracer_method_name("test_method").must_equal("test_method_with_apm_untracer") + end + end +end diff --git a/plugins/datadog_tracer/test/samson_datadog_tracer/samson_plugin_test.rb b/plugins/datadog_tracer/test/samson_datadog_tracer/samson_plugin_test.rb new file mode 100644 index 0000000000..8fc41cf41a --- /dev/null +++ b/plugins/datadog_tracer/test/samson_datadog_tracer/samson_plugin_test.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true +require_relative "../test_helper" + +SingleCov.covered! + +describe SamsonDatadogTracer do + describe ".enabled?" do + before(:all) do + ENV.delete('DATADOG_TRACER') + end + context "in any environment" do + it "is false by default" do + refute SamsonDatadogTracer.enabled? + end + + it "is true when DATADOG_TRACER env var is set" do + with_env DATADOG_TRACER: "1" do + assert SamsonDatadogTracer.enabled? + end + with_env DATADOG_TRACER: nil do + refute SamsonDatadogTracer.enabled? + end + end + end + end + + describe "#performance_tracer" do + describe "when enabled" do + it "triggers Datadog tracer method" do + with_env DATADOG_TRACER: "1" do + class Klass + include ::Samson::PerformanceTracer + def with_role + end + add_tracer :with_role + end + Klass.expects(:trace_method) + Samson::Hooks.fire :performance_tracer, Klass, :with_role + end + end + it "skips tracer with missing method" do + with_env DATADOG_TRACER: "1" do + helper = SamsonDatadogTracer::APM::Helpers + method = :with_role + Samson::Hooks.fire :performance_tracer, User, method + refute User.method_defined?(helper.tracer_method_name(method)) + end + end + end + + it "skips Datadog tracer when disabled" do + with_env DATADOG_TRACER: nil do + User.expects(:trace_method).never + Samson::Hooks.fire :performance_tracer, User, :with_role + end + end + end +end diff --git a/plugins/datadog_tracer/test/test_helper.rb b/plugins/datadog_tracer/test/test_helper.rb new file mode 100644 index 0000000000..fc6717e320 --- /dev/null +++ b/plugins/datadog_tracer/test/test_helper.rb @@ -0,0 +1,2 @@ +# frozen_string_literal: true +require_relative '../../../test/test_helper' diff --git a/plugins/new_relic/lib/samson_new_relic/samson_plugin.rb b/plugins/new_relic/lib/samson_new_relic/samson_plugin.rb index 3c55de41a3..8f4b66571e 100644 --- a/plugins/new_relic/lib/samson_new_relic/samson_plugin.rb +++ b/plugins/new_relic/lib/samson_new_relic/samson_plugin.rb @@ -8,6 +8,20 @@ class Engine < Rails::Engine def self.enabled? KEY end + + def self.tracer_enabled? + !!ENV['NEW_RELIC_LICENSE_KEY'] + end + + def self.trace_method_execution_scope(scope_name) + if tracer_enabled? + NewRelic::Agent::MethodTracerHelpers.trace_execution_scoped("Custom/Hooks/#{scope_name}") do + yield + end + else + yield + end + end end Samson::Hooks.view :stage_form, "samson_new_relic/fields" @@ -24,3 +38,21 @@ def self.enabled? end new_stage.new_relic_applications.build(old_applications) end + +Samson::Hooks.callback :performance_tracer do |klass, method| + if SamsonNewRelic.tracer_enabled? + klass.class_eval do + include ::NewRelic::Agent::MethodTracer + add_method_tracer method + end + end +end + +Samson::Hooks.callback :asynchronous_performance_tracer do |klass, method, options| + if SamsonNewRelic.tracer_enabled? + klass.is_a?(Class) && klass.class_eval do + include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation + add_transaction_tracer method, options + end + end +end diff --git a/plugins/new_relic/test/samson_new_relic/samson_plugin_test.rb b/plugins/new_relic/test/samson_new_relic/samson_plugin_test.rb index aed36180a8..f6d33d569c 100644 --- a/plugins/new_relic/test/samson_new_relic/samson_plugin_test.rb +++ b/plugins/new_relic/test/samson_new_relic/samson_plugin_test.rb @@ -37,4 +37,75 @@ end end end + + describe ".tracer_enabled?" do + it "is disabled when env was not set" do + refute SamsonNewRelic.tracer_enabled? + end + + it "is enabled when env was set" do + with_env NEW_RELIC_LICENSE_KEY: "1" do + assert SamsonNewRelic.tracer_enabled? + end + end + end + + describe ".trace_method_execution_scope" do + it "skips method trace when tracer disabled" do + NewRelic::Agent::MethodTracerHelpers.expects(:trace_execution_scoped).never + SamsonNewRelic.trace_method_execution_scope("test") { "without tracer" } + end + + it "trace execution scope when enabled" do + with_env NEW_RELIC_LICENSE_KEY: "1" do + NewRelic::Agent::MethodTracerHelpers.expects(:trace_execution_scoped) + SamsonNewRelic.trace_method_execution_scope("test") { "with tracer" } + end + end + + it "trace scope and returns execution result" do + with_env NEW_RELIC_LICENSE_KEY: "1" do + SamsonNewRelic.trace_method_execution_scope("test") { "with tracer" }.must_equal("with tracer") + end + end + end + + class Klass + include ::Samson::PerformanceTracer + def with_role + end + add_tracer :with_role + end + + describe "#performance_tracer" do + it "triggers method tracer when enabled" do + with_env NEW_RELIC_LICENSE_KEY: "1" do + Klass.expects(:add_method_tracer) + Samson::Hooks.fire :performance_tracer, Klass, [:with_role] + end + end + + it "skips method tracer when disabled" do + with_env NEW_RELIC_LICENSE_KEY: nil do + Klass.expects(:add_method_tracer).never + Samson::Hooks.fire :performance_tracer, Klass, [:with_role] + end + end + end + + describe "#asynchronous_performance_tracer" do + it "triggers asynchronous tracer when enabled" do + with_env NEW_RELIC_LICENSE_KEY: "1" do + Klass.expects(:add_transaction_tracer) + Samson::Hooks.fire :asynchronous_performance_tracer, Klass, [:with_role] + end + end + + it "skips asynchronous tracer when disabled" do + with_env NEW_RELIC_LICENSE_KEY: nil do + Klass.expects(:add_transaction_tracer).never + Samson::Hooks.fire :asynchronous_performance_tracer, Klass, [:with_role] + end + end + end end diff --git a/test/lib/samson/performance_tracer_test.rb b/test/lib/samson/performance_tracer_test.rb new file mode 100644 index 0000000000..316b3d3dbe --- /dev/null +++ b/test/lib/samson/performance_tracer_test.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true +# +require_relative '../../test_helper' + +SingleCov.covered! + +describe Samson::PerformanceTracer do + describe '#ClassMethods' do + class TestKlass + include Samson::PerformanceTracer + + def pub_method1 + :pub1 + end + + def pub_method2 + :pub2 + end + end + + describe '.trace_execution_scoped' do + it 'add tracer for scope' do + Rails.stubs(:env).returns("staging") + trace_scope = proc {} + SamsonNewRelic.expects(:trace_method_execution_scope).returns(trace_scope) + SamsonDatadogTracer::APM.expects(:trace_method_execution_scope).returns(trace_scope) + Samson::PerformanceTracer.trace_execution_scoped('test_scope') { :scoped } + end + + it 'skips scope tracing' do + SamsonNewRelic.expects(:trace_method_execution_scope).never + SamsonDatadogTracer::APM.expects(:trace_method_execution_scope).never + Samson::PerformanceTracer.trace_execution_scoped('test_scope') { :scoped }.must_equal(:scoped) + end + end + + describe '.add_tracer' do + it 'add method tracer from performance_tracer hook' do + performance_tracer_callback = ->(_, _) { true } + Rails.stubs(:env).returns("staging") + Samson::Hooks.with_callback(:performance_tracer, performance_tracer_callback) do + assert TestKlass.add_tracer(:pub_method1) + assert TestKlass.add_tracer(:pub_method2) + end + end + + it 'raises with invalid arguments' do + performance_tracer_callback = ->(_) { true } + assert_raises ArgumentError do + Samson::Hooks.with_callback(:performance_tracer, performance_tracer_callback) do + assert TestKlass.add_tracer(:pub_method1) + end + end + end + end + + describe '.add_asynchronous_tracer' do + it 'add asynchronous tracer from asynchronous_performance_tracer hook' do + methods = [:pub_method1, :pub_method2] + asyn_tracer_callback = ->(_, _, _) { true } + + Samson::Hooks.with_callback(:asynchronous_performance_tracer, asyn_tracer_callback) do + assert TestKlass.add_asynchronous_tracer(methods, {}) + end + end + + it 'raises with invalid arguments' do + methods = [:pub_method1] + asyn_tracer_callback = ->(_, _) { true } + assert_raises ArgumentError do + Samson::Hooks.with_callback(:asynchronous_performance_tracer, asyn_tracer_callback) do + assert TestKlass.add_asynchronous_tracer(methods, {}) + end + end + end + end + end +end