From 985ec1e3ef94658d5001dc146d668ca41ee26ece Mon Sep 17 00:00:00 2001 From: ericmustin Date: Wed, 20 Mar 2019 01:05:17 +0100 Subject: [PATCH 1/5] add warning log if integration not patched and update specs --- lib/ddtrace/contrib/patchable.rb | 6 +++++- spec/ddtrace/contrib/patchable_spec.rb | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/ddtrace/contrib/patchable.rb b/lib/ddtrace/contrib/patchable.rb index 409535b2c7a..dfdabe9e818 100644 --- a/lib/ddtrace/contrib/patchable.rb +++ b/lib/ddtrace/contrib/patchable.rb @@ -29,7 +29,11 @@ def patcher end def patch - return if !self.class.compatible? || patcher.nil? + if !self.class.compatible? || patcher.nil? + Datadog::Tracer.log.warn("Unable to patch #{self.class.name}") + return + end + patcher.patch end end diff --git a/spec/ddtrace/contrib/patchable_spec.rb b/spec/ddtrace/contrib/patchable_spec.rb index 720531cb1ae..b8ee4fb024b 100644 --- a/spec/ddtrace/contrib/patchable_spec.rb +++ b/spec/ddtrace/contrib/patchable_spec.rb @@ -3,6 +3,8 @@ require 'ddtrace' RSpec.describe Datadog::Contrib::Patchable do + include_context 'tracer logging' + describe 'implemented' do subject(:patchable_class) do Class.new.tap do |klass| @@ -62,6 +64,12 @@ subject(:patch) { patchable_object.patch } context 'when the patchable object' do + let(:unpatched_warnings) do + [ + /.*Unable to patch*/ + ] + end + context 'is compatible' do before(:each) { allow(patchable_class).to receive(:compatible?).and_return(true) } @@ -78,6 +86,7 @@ context 'and the patcher is nil' do it 'does not applies the patch' do is_expected.to be nil + expect(log_buffer).to contain_line_with(*unpatched_warnings) end end end @@ -85,6 +94,7 @@ context 'is not compatible' do it 'does not applies the patch' do is_expected.to be nil + expect(log_buffer).to contain_line_with(*unpatched_warnings) end end end From 3d43a294546bf5ef2cd0424117a0883422d817c6 Mon Sep 17 00:00:00 2001 From: David Elner Date: Mon, 15 Apr 2019 10:03:00 -0400 Subject: [PATCH 2/5] Collect runtime metrics from Ruby environment (#677) * Added: Datadog::Metrics. * Added: Datadog::Runtime::Metrics * Added: Datadog::Runtime::Identity * Fixed: Metrics specs to allow instead of expect #available? * Added: rspec_opts to RSpec rake tasks. * Changed: Metric tag names and defaults. * Refactored: Runtime::Metrics into Metrics class. * Fixed: RSpec method redefined warnings. * Changed: Associate local root spans with runtime metrics. * Changed: Associate Rack spans with runtime metrics. * Refactored: Runtime metrics into Writer. * Added: #enabled flag to Metrics. * Added: #runtime_metrics_enabled flag to configuration. * Added: service tag to runtime metrics documentation. * Added: Runtime metrics to Rails controller spans. --- .env | 1 + Rakefile | 27 +- ddtrace.gemspec | 8 +- docker-compose.yml | 2 + docs/GettingStarted.md | 52 +++ lib/ddtrace.rb | 1 + lib/ddtrace/configuration.rb | 4 + lib/ddtrace/configuration/settings.rb | 18 +- lib/ddtrace/context.rb | 4 +- lib/ddtrace/contrib/rack/middlewares.rb | 4 + .../contrib/rails/action_controller.rb | 3 + lib/ddtrace/ext/metrics.rb | 11 + lib/ddtrace/ext/runtime.rb | 33 ++ lib/ddtrace/metrics.rb | 131 ++++++++ lib/ddtrace/runtime/class_count.rb | 17 + lib/ddtrace/runtime/gc.rb | 16 + lib/ddtrace/runtime/identity.rb | 41 +++ lib/ddtrace/runtime/metrics.rb | 94 ++++++ lib/ddtrace/runtime/thread_count.rb | 16 + lib/ddtrace/utils/time.rb | 14 + lib/ddtrace/workers.rb | 42 ++- lib/ddtrace/writer.rb | 37 ++- .../contrib/rack/configuration_spec.rb | 8 + spec/ddtrace/metrics_spec.rb | 312 ++++++++++++++++++ .../propagation/distributed_headers_spec.rb | 30 +- spec/ddtrace/runtime/class_count_spec.rb | 16 + spec/ddtrace/runtime/gc_spec.rb | 16 + spec/ddtrace/runtime/identity_spec.rb | 39 +++ spec/ddtrace/runtime/metrics_spec.rb | 154 +++++++++ spec/ddtrace/runtime/thread_count_spec.rb | 16 + spec/ddtrace/tracer_integration_spec.rb | 14 + spec/ddtrace/workers_integration_spec.rb | 21 +- spec/ddtrace/workers_spec.rb | 8 +- spec/ddtrace/writer_spec.rb | 34 ++ spec/spec_helper.rb | 2 + spec/support/metric_helpers.rb | 79 +++++ spec/support/statsd_helpers.rb | 38 +++ test/tracer_test.rb | 2 +- 38 files changed, 1309 insertions(+), 56 deletions(-) create mode 100644 lib/ddtrace/ext/metrics.rb create mode 100644 lib/ddtrace/ext/runtime.rb create mode 100644 lib/ddtrace/metrics.rb create mode 100644 lib/ddtrace/runtime/class_count.rb create mode 100644 lib/ddtrace/runtime/gc.rb create mode 100644 lib/ddtrace/runtime/identity.rb create mode 100644 lib/ddtrace/runtime/metrics.rb create mode 100644 lib/ddtrace/runtime/thread_count.rb create mode 100644 lib/ddtrace/utils/time.rb create mode 100644 spec/ddtrace/metrics_spec.rb create mode 100644 spec/ddtrace/runtime/class_count_spec.rb create mode 100644 spec/ddtrace/runtime/gc_spec.rb create mode 100644 spec/ddtrace/runtime/identity_spec.rb create mode 100644 spec/ddtrace/runtime/metrics_spec.rb create mode 100644 spec/ddtrace/runtime/thread_count_spec.rb create mode 100644 spec/support/metric_helpers.rb create mode 100644 spec/support/statsd_helpers.rb diff --git a/.env b/.env index 51aae70321a..53a970c1bcb 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ DD_AGENT_HOST=localhost +DD_METRIC_AGENT_PORT=8125 DD_TRACE_AGENT_PORT=8126 TEST_DDAGENT_API_KEY=invalid_key_but_this_is_fine TEST_ELASTICSEARCH_HOST=127.0.0.1 diff --git a/Rakefile b/Rakefile index 1925123328a..213e8a8a48f 100644 --- a/Rakefile +++ b/Rakefile @@ -15,39 +15,47 @@ namespace :spec do :rails, :railsredis, :railssidekiq, :railsactivejob, :elasticsearch, :http, :redis, :sidekiq, :sinatra] - RSpec::Core::RakeTask.new(:main) do |t| + RSpec::Core::RakeTask.new(:main) do |t, args| t.pattern = 'spec/**/*_spec.rb' t.exclude_pattern = 'spec/**/{contrib,benchmark,redis,opentracer}/**/*_spec.rb' + t.rspec_opts = args.to_a.join(' ') end - RSpec::Core::RakeTask.new(:opentracer) do |t| + RSpec::Core::RakeTask.new(:opentracer) do |t, args| t.pattern = 'spec/ddtrace/opentracer/**/*_spec.rb' + t.rspec_opts = args.to_a.join(' ') end - RSpec::Core::RakeTask.new(:rails) do |t| + RSpec::Core::RakeTask.new(:rails) do |t, args| t.pattern = 'spec/ddtrace/contrib/rails/**/*_spec.rb' t.exclude_pattern = 'spec/ddtrace/contrib/rails/**/*{sidekiq,active_job,disable_env}*_spec.rb' + t.rspec_opts = args.to_a.join(' ') end - RSpec::Core::RakeTask.new(:railsredis) do |t| + RSpec::Core::RakeTask.new(:railsredis) do |t, args| t.pattern = 'spec/ddtrace/contrib/rails/**/*redis*_spec.rb' + t.rspec_opts = args.to_a.join(' ') end - RSpec::Core::RakeTask.new(:railssidekiq) do |t| + RSpec::Core::RakeTask.new(:railssidekiq) do |t, args| t.pattern = 'spec/ddtrace/contrib/rails/**/*sidekiq*_spec.rb' + t.rspec_opts = args.to_a.join(' ') end - RSpec::Core::RakeTask.new(:railsactivejob) do |t| + RSpec::Core::RakeTask.new(:railsactivejob) do |t, args| t.pattern = 'spec/ddtrace/contrib/rails/**/*active_job*_spec.rb' + t.rspec_opts = args.to_a.join(' ') end - RSpec::Core::RakeTask.new(:railsdisableenv) do |t| + RSpec::Core::RakeTask.new(:railsdisableenv) do |t, args| t.pattern = 'spec/ddtrace/contrib/rails/**/*disable_env*_spec.rb' + t.rspec_opts = args.to_a.join(' ') end - RSpec::Core::RakeTask.new(:contrib) do |t| + RSpec::Core::RakeTask.new(:contrib) do |t, args| # rubocop:disable Metrics/LineLength t.pattern = 'spec/**/contrib/{analytics,configurable,integration,patchable,patcher,registerable,registry,configuration/*}_spec.rb' + t.rspec_opts = args.to_a.join(' ') end [ @@ -79,8 +87,9 @@ namespace :spec do :sucker_punch, :shoryuken ].each do |contrib| - RSpec::Core::RakeTask.new(contrib) do |t| + RSpec::Core::RakeTask.new(contrib) do |t, args| t.pattern = "spec/ddtrace/contrib/#{contrib}/**/*_spec.rb" + t.rspec_opts = args.to_a.join(' ') end end end diff --git a/ddtrace.gemspec b/ddtrace.gemspec index ac12d70dc22..7ba14f10b72 100644 --- a/ddtrace.gemspec +++ b/ddtrace.gemspec @@ -33,10 +33,13 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_dependency 'msgpack' + + # Optional extensions # TODO: Move this to Appraisals? - spec.add_dependency 'opentracing', '>= 0.4.1' + spec.add_development_dependency 'dogstatsd-ruby', '>= 3.3.0' + spec.add_development_dependency 'opentracing', '>= 0.4.1' - spec.add_development_dependency 'climate_control', '~> 0.2.0' + # Development dependencies spec.add_development_dependency 'rake', '>= 10.5' spec.add_development_dependency 'rubocop', '= 0.49.1' if RUBY_VERSION >= '2.1.0' spec.add_development_dependency 'rspec', '~> 3.0' @@ -48,6 +51,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'builder' spec.add_development_dependency 'ruby-prof' spec.add_development_dependency 'sqlite3', '~> 1.3.6' + spec.add_development_dependency 'climate_control', '~> 0.2.0' # locking transitive dependency of webmock spec.add_development_dependency 'addressable', '~> 2.4.0' diff --git a/docker-compose.yml b/docker-compose.yml index 71c9d44efa3..0fbef75f7bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -175,8 +175,10 @@ services: - DD_BIND_HOST=0.0.0.0 - DD_API_KEY=invalid_key_but_this_is_fine expose: + - "8125/udp" - "8126" ports: + - "${DD_METRIC_AGENT_PORT}:8125/udp" - "${DD_TRACE_AGENT_PORT}:8126" elasticsearch: # Note: ES 5.0 dies with error: diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index facef4c0b61..27e18fbeb74 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -63,6 +63,8 @@ For descriptions of terminology used in APM, take a look at the [official docume - [Filtering](#filtering) - [Processing](#processing) - [Trace correlation](#trace-correlation) + - [Metrics](#metrics) + - [For application runtime](#for-application-runtime) - [OpenTracing](#opentracing) ## Compatibility @@ -1725,6 +1727,56 @@ Datadog.tracer.trace('my.operation') { logger.warn('This is a traced operation.' # [2019-01-16 18:38:41 +0000][my_app][WARN][dd.trace_id=8545847825299552251 dd.span_id=3711755234730770098] This is a traced operation. ``` +### Metrics + +The tracer and its integrations can produce some additional metrics that can provide useful insight into the performance of your application. These metrics are collected with `dogstatsd-ruby`, and can be sent to the same Datadog agent to which you send your traces. + +To configure your application for metrics collection: + +1. [Configure your Datadog agent for StatsD](https://docs.datadoghq.com/developers/dogstatsd/#setup) +2. Add `gem 'dogstatsd-ruby'` to your Gemfile + +#### For application runtime + +If runtime metrics are configured, the trace library will automatically collect and send metrics about the health of your application. + +To configure runtime metrics, add the following configuration: + +```ruby +# config/initializers/datadog.rb +require 'datadog/statsd' +require 'ddtrace' + +Datadog.configure do |c| + # To enable runtime metrics collection, set `true`. Defaults to `false` + # You can also set DD_RUNTIME_METRICS_ENABLED=true to configure this. + c.runtime_metrics_enabled = true + + # Optionally, you can configure the Statsd instance used for sending runtime metrics. + # Statsd is automatically configured with default settings if `dogstatsd-ruby` is available. + # You can configure with host and port of Datadog agent; defaults to 'localhost:8125'. + c.runtime_metrics statsd: Datadog::Statsd.new +end +``` + +See the [Dogstatsd documentation](https://www.rubydoc.info/github/DataDog/dogstatsd-ruby/master/frames) for more details about configuring `Datadog::Statsd`. + +The stats sent will include: + +| Name | Type | Description | +| -------------------------- | ------- | -------------------------------------------------------- | +| `runtime.ruby.class_count` | `gauge` | Number of classes in memory space. | +| `runtime.ruby.thread_count` | `gauge` | Number of threads. | +| `runtime.ruby.gc.*`. | `gauge` | Garbage collection statistics (one per value in GC.stat) | + +In addition, all metrics will include the following tags: + +| Name | Description | +| ------------ | ------------------------------------------------------- | +| `language` | Programming language traced. (e.g. `ruby`) | +| `runtime-id` | Unique identifier of runtime environment (i.e. process) | +| `service` | List of services this metric is associated with. | + ### OpenTracing For setting up Datadog with OpenTracing, see out [Quickstart for OpenTracing](#quickstart-for-opentracing) section for details. diff --git a/lib/ddtrace.rb b/lib/ddtrace.rb index 6c480e7236b..6cb0cebd9a6 100644 --- a/lib/ddtrace.rb +++ b/lib/ddtrace.rb @@ -9,6 +9,7 @@ require 'ddtrace/configuration' require 'ddtrace/patcher' require 'ddtrace/augmentation' +require 'ddtrace/metrics' # \Datadog global namespace that includes all tracing functionality for Tracer and Span classes. module Datadog diff --git a/lib/ddtrace/configuration.rb b/lib/ddtrace/configuration.rb index 9b5b2baf8b3..dec2e92a7c7 100644 --- a/lib/ddtrace/configuration.rb +++ b/lib/ddtrace/configuration.rb @@ -22,5 +22,9 @@ def configure(target = configuration, opts = {}) def tracer configuration.tracer end + + def runtime_metrics + tracer.writer.runtime_metrics + end end end diff --git a/lib/ddtrace/configuration/settings.rb b/lib/ddtrace/configuration/settings.rb index eba16462b34..22d120e57e9 100644 --- a/lib/ddtrace/configuration/settings.rb +++ b/lib/ddtrace/configuration/settings.rb @@ -1,7 +1,11 @@ require 'ddtrace/ext/analytics' -require 'ddtrace/environment' +require 'ddtrace/ext/runtime' require 'ddtrace/configuration/options' +require 'ddtrace/environment' +require 'ddtrace/tracer' +require 'ddtrace/metrics' + module Datadog module Configuration # Global configuration settings for the trace library. @@ -13,6 +17,10 @@ class Settings default: -> { env_to_bool(Ext::Analytics::ENV_TRACE_ANALYTICS_ENABLED, nil) }, lazy: true + option :runtime_metrics_enabled, + default: -> { env_to_bool(Ext::Runtime::Metrics::ENV_ENABLED, false) }, + lazy: true + option :tracer, default: Tracer.new def initialize(options = {}) @@ -28,7 +36,15 @@ def configure(options = {}) yield(self) if block_given? end + def runtime_metrics(options = nil) + runtime_metrics = get_option(:tracer).writer.runtime_metrics + return runtime_metrics if options.nil? + + runtime_metrics.configure(options) + end + # Backwards compatibility for configuring tracer e.g. `c.tracer debug: true` + remove_method :tracer def tracer(options = nil) tracer = options && options.key?(:instance) ? set_option(:tracer, options[:instance]) : get_option(:tracer) diff --git a/lib/ddtrace/context.rb b/lib/ddtrace/context.rb index 1193e974e2a..0afaba5741f 100644 --- a/lib/ddtrace/context.rb +++ b/lib/ddtrace/context.rb @@ -200,14 +200,14 @@ def check_finished_spans end def attach_sampling_priority - @trace.first.set_metric( + @current_root_span.set_metric( Ext::DistributedTracing::SAMPLING_PRIORITY_KEY, @sampling_priority ) end def attach_origin - @trace.first.set_tag( + @current_root_span.set_tag( Ext::DistributedTracing::ORIGIN_KEY, @origin ) diff --git a/lib/ddtrace/contrib/rack/middlewares.rb b/lib/ddtrace/contrib/rack/middlewares.rb index a116a13d22e..9cb10633153 100644 --- a/lib/ddtrace/contrib/rack/middlewares.rb +++ b/lib/ddtrace/contrib/rack/middlewares.rb @@ -124,6 +124,7 @@ def resource_name_for(env, status) end end + # rubocop:disable Metrics/AbcSize def set_request_tags!(request_span, env, status, headers, response, original_env) # http://www.rubydoc.info/github/rack/rack/file/SPEC # The source of truth in Rack is the PATH_INFO key that holds the @@ -142,6 +143,9 @@ def set_request_tags!(request_span, env, status, headers, response, original_env request_span.resource ||= resource_name_for(env, status) + # Associate with runtime metrics + Datadog.runtime_metrics.associate_with_span(request_span) + # Set analytics sample rate if Contrib::Analytics.enabled?(configuration[:analytics_enabled]) Contrib::Analytics.set_sample_rate(request_span, configuration[:analytics_sample_rate]) diff --git a/lib/ddtrace/contrib/rails/action_controller.rb b/lib/ddtrace/contrib/rails/action_controller.rb index b0afecd8797..c0dcf4bb213 100644 --- a/lib/ddtrace/contrib/rails/action_controller.rb +++ b/lib/ddtrace/contrib/rails/action_controller.rb @@ -53,6 +53,9 @@ def self.finish_processing(payload) # Set analytics sample rate Utils.set_analytics_sample_rate(span) + # Associate with runtime metrics + Datadog.runtime_metrics.associate_with_span(span) + span.set_tag(Ext::TAG_ROUTE_ACTION, payload.fetch(:action)) span.set_tag(Ext::TAG_ROUTE_CONTROLLER, payload.fetch(:controller)) diff --git a/lib/ddtrace/ext/metrics.rb b/lib/ddtrace/ext/metrics.rb new file mode 100644 index 00000000000..cfb78f79c91 --- /dev/null +++ b/lib/ddtrace/ext/metrics.rb @@ -0,0 +1,11 @@ +module Datadog + module Ext + module Metrics + TAG_LANG = 'language'.freeze + TAG_LANG_INTERPRETER = 'language-interpreter'.freeze + TAG_LANG_VERSION = 'language-version'.freeze + TAG_RUNTIME_ID = 'runtime-id'.freeze + TAG_TRACER_VERSION = 'tracer-version'.freeze + end + end +end diff --git a/lib/ddtrace/ext/runtime.rb b/lib/ddtrace/ext/runtime.rb new file mode 100644 index 00000000000..81705970033 --- /dev/null +++ b/lib/ddtrace/ext/runtime.rb @@ -0,0 +1,33 @@ +require 'ddtrace/version' + +module Datadog + module Ext + module Runtime + # Identity + LANG = 'ruby'.freeze + LANG_INTERPRETER = begin + if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('1.9') + (RUBY_ENGINE + '-' + RUBY_PLATFORM) + else + ('ruby-' + RUBY_PLATFORM) + end + end.freeze + LANG_VERSION = RUBY_VERSION + TRACER_VERSION = Datadog::VERSION::STRING + + TAG_LANG = 'language'.freeze + TAG_RUNTIME_ID = 'runtime-id'.freeze + + # Metrics + module Metrics + ENV_ENABLED = 'DD_RUNTIME_METRICS_ENABLED'.freeze + + METRIC_CLASS_COUNT = 'runtime.ruby.class_count'.freeze + METRIC_GC_PREFIX = 'runtime.ruby.gc'.freeze + METRIC_THREAD_COUNT = 'runtime.ruby.thread_count'.freeze + + TAG_SERVICE = 'service'.freeze + end + end + end +end diff --git a/lib/ddtrace/metrics.rb b/lib/ddtrace/metrics.rb new file mode 100644 index 00000000000..d115ad08bd0 --- /dev/null +++ b/lib/ddtrace/metrics.rb @@ -0,0 +1,131 @@ +require 'ddtrace/ext/metrics' + +require 'set' +require 'ddtrace/utils/time' +require 'ddtrace/runtime/identity' + +module Datadog + # Acts as client for sending metrics (via Statsd) + # Wraps a Statsd client with default tags and additional configuration. + class Metrics + DEFAULT_AGENT_HOST = '127.0.0.1'.freeze + DEFAULT_METRIC_AGENT_PORT = '8125'.freeze + + attr_reader :statsd + + def initialize(options = {}) + @statsd = options.fetch(:statsd) { default_statsd_client if supported? } + @enabled = options.fetch(:enabled, true) + end + + def supported? + Gem.loaded_specs['dogstatsd-ruby'] \ + && Gem.loaded_specs['dogstatsd-ruby'].version >= Gem::Version.new('3.3.0') + end + + def enabled? + @enabled + end + + def enabled=(enabled) + @enabled = (enabled == true) + end + + def default_statsd_client + require 'datadog/statsd' unless defined?(::Datadog::Statsd) + + # Create a StatsD client that points to the agent. + Datadog::Statsd.new( + ENV.fetch('DD_AGENT_HOST', DEFAULT_AGENT_HOST), + ENV.fetch('DD_METRIC_AGENT_PORT', DEFAULT_METRIC_AGENT_PORT) + ) + end + + def configure(options = {}) + @statsd = options[:statsd] if options.key?(:statsd) + @enabled = options[:enabled] if options.key?(:enabled) + end + + def send_stats? + enabled? && !statsd.nil? + end + + def distribution(stat, value, options = nil) + return unless send_stats? && statsd.respond_to?(:distribution) + statsd.distribution(stat, value, metric_options(options)) + rescue StandardError => e + Datadog::Tracer.log.error("Failed to send distribution stat. Cause: #{e.message} Source: #{e.backtrace.first}") + end + + def increment(stat, options = nil) + return unless send_stats? && statsd.respond_to?(:increment) + statsd.increment(stat, metric_options(options)) + rescue StandardError => e + Datadog::Tracer.log.error("Failed to send increment stat. Cause: #{e.message} Source: #{e.backtrace.first}") + end + + def gauge(stat, value, options = nil) + return unless send_stats? && statsd.respond_to?(:gauge) + statsd.gauge(stat, value, metric_options(options)) + rescue StandardError => e + Datadog::Tracer.log.error("Failed to send gauge stat. Cause: #{e.message} Source: #{e.backtrace.first}") + end + + def time(stat, options = nil) + return yield unless send_stats? + + # Calculate time, send it as a distribution. + start = Utils::Time.get_time + return yield + ensure + begin + if send_stats? && !start.nil? + finished = Utils::Time.get_time + distribution(stat, ((finished - start) * 1000), options) + end + rescue StandardError => e + Datadog::Tracer.log.error("Failed to send time stat. Cause: #{e.message} Source: #{e.backtrace.first}") + end + end + + # For defining and adding default options to metrics + module Options + DEFAULT = { + tags: DEFAULT_TAGS = [ + "#{Ext::Metrics::TAG_LANG}:#{Runtime::Identity.lang}".freeze + # "#{Ext::Metrics::TAG_LANG_INTERPRETER}:#{Runtime::Identity.lang_interpreter}".freeze, + # "#{Ext::Metrics::TAG_LANG_VERSION}:#{Runtime::Identity.lang_version}".freeze, + # "#{Ext::Metrics::TAG_TRACER_VERSION}:#{Runtime::Identity.tracer_version}".freeze + ].freeze + }.freeze + + def metric_options(options = nil) + return default_metric_options if options.nil? + + default_metric_options.merge(options) do |key, old_value, new_value| + case key + when :tags + old_value.dup.concat(new_value).uniq + else + new_value + end + end + end + + def default_metric_options + # Return dupes, so that the constant isn't modified, + # and defaults are unfrozen for mutation in Statsd. + DEFAULT.dup.tap do |options| + options[:tags] = options[:tags].dup + + # Add runtime ID dynamically because it might change during fork. + options[:tags] << "#{Ext::Metrics::TAG_RUNTIME_ID}:#{Runtime::Identity.id}".freeze + end + end + end + + # Make available on for both class and instance. + include Options + extend Options + end +end diff --git a/lib/ddtrace/runtime/class_count.rb b/lib/ddtrace/runtime/class_count.rb new file mode 100644 index 00000000000..eb00c6ad27e --- /dev/null +++ b/lib/ddtrace/runtime/class_count.rb @@ -0,0 +1,17 @@ +module Datadog + module Runtime + # Retrieves number of classes from runtime + module ClassCount + module_function + + def value + ObjectSpace.count_objects[:T_CLASS] + end + + def available? + ObjectSpace.respond_to?(:count_objects) \ + && ObjectSpace.count_objects.key?(:T_CLASS) + end + end + end +end diff --git a/lib/ddtrace/runtime/gc.rb b/lib/ddtrace/runtime/gc.rb new file mode 100644 index 00000000000..90d329fba10 --- /dev/null +++ b/lib/ddtrace/runtime/gc.rb @@ -0,0 +1,16 @@ +module Datadog + module Runtime + # Retrieves garbage collection statistics + module GC + module_function + + def stat + ::GC.stat + end + + def available? + defined?(::GC) && ::GC.respond_to?(:stat) + end + end + end +end diff --git a/lib/ddtrace/runtime/identity.rb b/lib/ddtrace/runtime/identity.rb new file mode 100644 index 00000000000..5ea524d6ff2 --- /dev/null +++ b/lib/ddtrace/runtime/identity.rb @@ -0,0 +1,41 @@ +require 'securerandom' +require 'ddtrace/ext/runtime' + +module Datadog + module Runtime + # For runtime identity + module Identity + module_function + + # Retrieves number of classes from runtime + def id + @pid ||= Process.pid + @id ||= SecureRandom.uuid + + # Check if runtime has changed, e.g. forked. + if Process.pid != @pid + @pid = Process.pid + @id = SecureRandom.uuid + end + + @id + end + + def lang + Ext::Runtime::LANG + end + + def lang_interpreter + Ext::Runtime::LANG_INTERPRETER + end + + def lang_version + Ext::Runtime::LANG_VERSION + end + + def tracer_version + Ext::Runtime::TRACER_VERSION + end + end + end +end diff --git a/lib/ddtrace/runtime/metrics.rb b/lib/ddtrace/runtime/metrics.rb new file mode 100644 index 00000000000..51b4f3171f0 --- /dev/null +++ b/lib/ddtrace/runtime/metrics.rb @@ -0,0 +1,94 @@ +require 'ddtrace/ext/runtime' + +require 'ddtrace/metrics' +require 'ddtrace/runtime/class_count' +require 'ddtrace/runtime/gc' +require 'ddtrace/runtime/identity' +require 'ddtrace/runtime/thread_count' + +module Datadog + module Runtime + # For generating runtime metrics + class Metrics < Datadog::Metrics + def initialize(options = {}) + super + + # Initialize service list + @services = Set.new + @service_tags = nil + end + + def associate_with_span(span) + return if span.nil? + + # Register service as associated with metrics + register_service(span.service) unless span.service.nil? + + # Tag span with language and runtime ID for association with metrics + span.set_tag(Ext::Runtime::TAG_LANG, Runtime::Identity.lang) + span.set_tag(Ext::Runtime::TAG_RUNTIME_ID, Runtime::Identity.id) + end + + # Associate service with runtime metrics + def register_service(service) + return if service.nil? + + service = service.to_s + + unless @services.include?(service) + # Add service to list and update services tag + services << service + + # Recompile the service tags + compile_service_tags! + end + end + + # Flush all runtime metrics to Statsd client + def flush + return unless enabled? + + try_flush { gauge(Ext::Runtime::Metrics::METRIC_CLASS_COUNT, ClassCount.value) if ClassCount.available? } + try_flush { gauge(Ext::Runtime::Metrics::METRIC_THREAD_COUNT, ThreadCount.value) if ThreadCount.available? } + try_flush { gc_metrics.each { |metric, value| gauge(metric, value) } if GC.available? } + end + + def gc_metrics + Hash[ + GC.stat.map do |k, v| + ["#{Ext::Runtime::Metrics::METRIC_GC_PREFIX}.#{k}", v] + end + ] + end + + def try_flush + yield + rescue StandardError => e + Datadog::Tracer.log.error("Error while sending runtime metric. Cause: #{e.message}") + end + + def default_metric_options + # Return dupes, so that the constant isn't modified, + # and defaults are unfrozen for mutation in Statsd. + super.tap do |options| + options[:tags] = options[:tags].dup + + # Add services dynamically because they might change during runtime. + options[:tags].concat(service_tags) unless service_tags.nil? + end + end + + private + + attr_reader \ + :service_tags, + :services + + def compile_service_tags! + @service_tags = services.to_a.collect do |service| + "#{Ext::Runtime::Metrics::TAG_SERVICE}:#{service}".freeze + end + end + end + end +end diff --git a/lib/ddtrace/runtime/thread_count.rb b/lib/ddtrace/runtime/thread_count.rb new file mode 100644 index 00000000000..d769178dc52 --- /dev/null +++ b/lib/ddtrace/runtime/thread_count.rb @@ -0,0 +1,16 @@ +module Datadog + module Runtime + # Retrieves number of threads from runtime + module ThreadCount + module_function + + def value + Thread.list.count + end + + def available? + Thread.respond_to?(:list) + end + end + end +end diff --git a/lib/ddtrace/utils/time.rb b/lib/ddtrace/utils/time.rb new file mode 100644 index 00000000000..fcda31a3f5b --- /dev/null +++ b/lib/ddtrace/utils/time.rb @@ -0,0 +1,14 @@ +module Datadog + module Utils + # Common database-related utility functions. + module Time + PROCESS_TIME_SUPPORTED = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1.0') + + module_function + + def get_time + PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : ::Time.now.to_f + end + end + end +end diff --git a/lib/ddtrace/workers.rb b/lib/ddtrace/workers.rb index 4d182fde5f5..6e05a560a55 100644 --- a/lib/ddtrace/workers.rb +++ b/lib/ddtrace/workers.rb @@ -1,6 +1,7 @@ require 'time' require 'ddtrace/buffer' +require 'ddtrace/runtime/metrics' module Datadog module Workers @@ -14,19 +15,31 @@ class AsyncTransport BACK_OFF_MAX = 5 SHUTDOWN_TIMEOUT = 1 - attr_reader :trace_buffer, :service_buffer + attr_reader \ + :service_buffer, + :trace_buffer - def initialize(transport, buff_size, trace_task, service_task, interval) - @trace_task = trace_task - @service_task = service_task + def initialize(options = {}) + @transport = options[:transport] + + # Callbacks + @trace_task = options[:on_trace] + @service_task = options[:on_service] + @runtime_metrics_task = options[:on_runtime_metrics] + + # Intervals + interval = options.fetch(:interval, 1) @flush_interval = interval @back_off = interval - @trace_buffer = TraceBuffer.new(buff_size) - @service_buffer = TraceBuffer.new(buff_size) - @transport = transport + + # Buffers + buffer_size = options.fetch(:buffer_size, 100) + @trace_buffer = TraceBuffer.new(buffer_size) + @service_buffer = TraceBuffer.new(buffer_size) + + # Threading @shutdown = ConditionVariable.new @mutex = Mutex.new - @worker = nil @run = false end @@ -36,9 +49,9 @@ def callback_traces return true if @trace_buffer.empty? begin - traces = @trace_buffer.pop() + traces = @trace_buffer.pop traces = Pipeline.process!(traces) - @trace_task.call(traces, @transport) + @trace_task.call(traces, @transport) unless @trace_task.nil? rescue StandardError => e # ensures that the thread will not die because of an exception. # TODO[manu]: findout the reason and reschedule the send if it's not @@ -53,7 +66,7 @@ def callback_services begin services = @service_buffer.pop() - @service_task.call(services[0], @transport) + @service_task.call(services[0], @transport) unless @service_task.nil? rescue StandardError => e # ensures that the thread will not die because of an exception. # TODO[manu]: findout the reason and reschedule the send if it's not @@ -62,6 +75,12 @@ def callback_services end end + def callback_runtime_metrics + @runtime_metrics_task.call unless @runtime_metrics_task.nil? + rescue StandardError => e + Datadog::Tracer.log.error("Error during runtime metrics flush. Cause: #{e}") + end + # Start the timer execution. def start @mutex.synchronize do @@ -113,6 +132,7 @@ def perform @back_off = flush_data ? @flush_interval : [@back_off * BACK_OFF_RATIO, BACK_OFF_MAX].min callback_services + callback_runtime_metrics @mutex.synchronize do return if !@run && @trace_buffer.empty? && @service_buffer.empty? diff --git a/lib/ddtrace/writer.rb b/lib/ddtrace/writer.rb index 628e08b34ea..cbed7d0ac31 100644 --- a/lib/ddtrace/writer.rb +++ b/lib/ddtrace/writer.rb @@ -3,15 +3,20 @@ require 'ddtrace/workers' module Datadog - # Traces and services writer that periodically sends data to the trace-agent + # Processor that sends traces and metadata to the agent class Writer - attr_reader :transport, :worker, :priority_sampler + attr_reader \ + :priority_sampler, + :runtime_metrics, + :transport, + :worker def initialize(options = {}) # writer and transport parameters @buff_size = options.fetch(:buffer_size, 100) @flush_interval = options.fetch(:flush_interval, 1) transport_options = options.fetch(:transport_options, {}) + # priority sampling if options[:priority_sampler] @priority_sampler = options[:priority_sampler] @@ -24,6 +29,11 @@ def initialize(options = {}) HTTPTransport.new(transport_options) end + # Runtime metrics + @runtime_metrics = options.fetch(:runtime_metrics) do + Runtime::Metrics.new + end + @services = {} # handles the thread creation after an eventual fork @@ -43,11 +53,15 @@ def start @pid = Process.pid @trace_handler = ->(items, transport) { send_spans(items, transport) } @service_handler = ->(items, transport) { send_services(items, transport) } - @worker = Datadog::Workers::AsyncTransport.new(@transport, - @buff_size, - @trace_handler, - @service_handler, - @flush_interval) + @runtime_metrics_handler = -> { send_runtime_metrics } + @worker = Datadog::Workers::AsyncTransport.new( + transport: @transport, + buffer_size: @buff_size, + on_trace: @trace_handler, + on_service: @service_handler, + on_runtime_metrics: @runtime_metrics_handler, + interval: @flush_interval + ) @worker.start() end @@ -80,6 +94,12 @@ def send_services(services, transport) status end + def send_runtime_metrics + return unless Datadog.configuration.runtime_metrics_enabled + + runtime_metrics.flush + end + # enqueue the trace for submission to the API def write(trace, services) # In multiprocess environments, the main process initializes the +Writer+ instance and if @@ -98,6 +118,9 @@ def write(trace, services) end end + # Associate root span with runtime metrics + runtime_metrics.associate_with_span(trace.first) unless trace.empty? + @worker.enqueue_trace(trace) @worker.enqueue_service(services) end diff --git a/spec/ddtrace/contrib/rack/configuration_spec.rb b/spec/ddtrace/contrib/rack/configuration_spec.rb index 233d28dfd98..ed6e7edb82e 100644 --- a/spec/ddtrace/contrib/rack/configuration_spec.rb +++ b/spec/ddtrace/contrib/rack/configuration_spec.rb @@ -75,6 +75,10 @@ expect(queue_span.name).to eq('http_server.queue') expect(queue_span.service).to eq(Datadog.configuration[:rack][:web_service_name]) expect(queue_span.start_time.to_i).to eq(queue_time) + # Queue span gets tagged for runtime metrics because its a local root span. + # TODO: It probably shouldn't get tagged like this in the future; it's not part of the runtime. + expect(queue_span.get_tag(Datadog::Ext::Runtime::TAG_LANG)).to eq('ruby') + expect(queue_span.get_tag(Datadog::Ext::Runtime::TAG_RUNTIME_ID)).to eq(Datadog::Runtime::Identity.id) expect(rack_span.name).to eq('rack.request') expect(rack_span.span_type).to eq('http') @@ -83,6 +87,8 @@ expect(rack_span.get_tag('http.method')).to eq('GET') expect(rack_span.get_tag('http.status_code')).to eq('200') expect(rack_span.get_tag('http.url')).to eq('/') + expect(rack_span.get_tag(Datadog::Ext::Runtime::TAG_LANG)).to eq('ruby') + expect(rack_span.get_tag(Datadog::Ext::Runtime::TAG_RUNTIME_ID)).to eq(Datadog::Runtime::Identity.id) expect(rack_span.status).to eq(0) expect(queue_span.span_id).to eq(rack_span.parent_id) @@ -102,6 +108,8 @@ expect(span.get_tag('http.method')).to eq('GET') expect(span.get_tag('http.status_code')).to eq('200') expect(span.get_tag('http.url')).to eq('/') + expect(span.get_tag(Datadog::Ext::Runtime::TAG_LANG)).to eq('ruby') + expect(span.get_tag(Datadog::Ext::Runtime::TAG_RUNTIME_ID)).to eq(Datadog::Runtime::Identity.id) expect(span.status).to eq(0) expect(span.parent_id).to eq(0) diff --git a/spec/ddtrace/metrics_spec.rb b/spec/ddtrace/metrics_spec.rb new file mode 100644 index 00000000000..6d8253a7ecb --- /dev/null +++ b/spec/ddtrace/metrics_spec.rb @@ -0,0 +1,312 @@ +require 'spec_helper' + +require 'ddtrace' +require 'ddtrace/metrics' +require 'benchmark' + +RSpec.describe Datadog::Metrics do + include_context 'metrics' + + subject(:metrics) { described_class.new(options) } + let(:options) { { statsd: statsd } } + + it { is_expected.to have_attributes(statsd: statsd) } + + describe '#supported?' do + # WIP + end + + describe '#enabled?' do + subject(:enabled) { metrics.enabled? } + + context 'by default' do + it { is_expected.to be true } + end + + context 'when initialized as enabled' do + let(:options) { super().merge(enabled: true) } + it { is_expected.to be true } + end + + context 'when initialized as disabled' do + let(:options) { super().merge(enabled: false) } + it { is_expected.to be false } + end + end + + describe '#enabled=' do + subject(:enabled) { metrics.enabled? } + before { metrics.enabled = status } + + context 'is given true' do + let(:status) { true } + it { is_expected.to be true } + end + + context 'is given false' do + let(:status) { false } + it { is_expected.to be false } + end + + context 'is given nil' do + let(:status) { nil } + it { is_expected.to be false } + end + end + + describe '#default_statsd_client' do + # WIP + end + + describe '#configure' do + # WIP + end + + describe '#send_stats?' do + # WIP + end + + describe '#distribution' do + subject(:distribution) { metrics.distribution(stat, value, stat_options) } + let(:stat) { :foo } + let(:value) { 100 } + let(:stat_options) { nil } + + context 'when #statsd is nil' do + before(:each) do + allow(metrics).to receive(:statsd).and_return(nil) + expect { distribution }.to_not raise_error + end + + it { expect(statsd).to_not have_received_distribution_metric(stat) } + end + + context 'when #statsd is a Datadog::Statsd' do + context 'and given no options' do + before(:each) { expect { distribution }.to_not raise_error } + it { expect(statsd).to have_received_distribution_metric(stat) } + end + + context 'and given options' do + before(:each) { expect { distribution }.to_not raise_error } + + context 'that are empty' do + let(:stat_options) { {} } + it { expect(statsd).to have_received_distribution_metric(stat) } + end + + context 'that are frozen' do + let(:stat_options) { {}.freeze } + it { expect(statsd).to have_received_distribution_metric(stat) } + end + + context 'that contain :tags' do + let(:stat_options) { { tags: tags } } + let(:tags) { %w[foo bar] } + it { expect(statsd).to have_received_distribution_metric(stat, kind_of(Numeric), stat_options) } + + context 'which are frozen' do + let(:tags) { super().freeze } + it { expect(statsd).to have_received_distribution_metric(stat, kind_of(Numeric), stat_options) } + end + end + end + + context 'which raises an error' do + before(:each) do + expect(statsd).to receive(:distribution).and_raise(StandardError) + expect(Datadog::Tracer.log).to receive(:error) + end + + it { expect { distribution }.to_not raise_error } + end + end + end + + describe '#gauge' do + subject(:gauge) { metrics.gauge(stat, value, stat_options) } + let(:stat) { :foo } + let(:value) { 100 } + let(:stat_options) { nil } + + context 'when #statsd is nil' do + before(:each) do + allow(metrics).to receive(:statsd).and_return(nil) + expect { gauge }.to_not raise_error + end + + it { expect(statsd).to_not have_received_gauge_metric(stat) } + end + + context 'when #statsd is a Datadog::Statsd' do + context 'and given no options' do + before(:each) { expect { gauge }.to_not raise_error } + it { expect(statsd).to have_received_gauge_metric(stat) } + end + + context 'and given options' do + before(:each) { expect { gauge }.to_not raise_error } + + context 'that are empty' do + let(:stat_options) { {} } + it { expect(statsd).to have_received_gauge_metric(stat) } + end + + context 'that are frozen' do + let(:stat_options) { {}.freeze } + it { expect(statsd).to have_received_gauge_metric(stat) } + end + + context 'that contain :tags' do + let(:stat_options) { { tags: tags } } + let(:tags) { %w[foo bar] } + it { expect(statsd).to have_received_gauge_metric(stat, kind_of(Numeric), stat_options) } + + context 'which are frozen' do + let(:tags) { super().freeze } + it { expect(statsd).to have_received_gauge_metric(stat, kind_of(Numeric), stat_options) } + end + end + end + + context 'which raises an error' do + before(:each) do + expect(statsd).to receive(:gauge).and_raise(StandardError) + expect(Datadog::Tracer.log).to receive(:error) + end + + it { expect { gauge }.to_not raise_error } + end + end + end + + describe '#increment' do + subject(:increment) { metrics.increment(stat, stat_options) } + let(:stat) { :foo } + let(:stat_options) { nil } + + context 'when #statsd is nil' do + before(:each) do + allow(metrics).to receive(:statsd).and_return(nil) + expect { increment }.to_not raise_error + end + + it { expect(statsd).to_not have_received_increment_metric(stat) } + end + + context 'when #statsd is a Datadog::Statsd' do + context 'and given no options' do + before(:each) { expect { increment }.to_not raise_error } + it { expect(statsd).to have_received_increment_metric(stat) } + end + + context 'and given options' do + before(:each) { expect { increment }.to_not raise_error } + + context 'that are empty' do + let(:stat_options) { {} } + it { expect(statsd).to have_received_increment_metric(stat) } + end + + context 'that are frozen' do + let(:stat_options) { {}.freeze } + it { expect(statsd).to have_received_increment_metric(stat) } + end + + context 'that contain :by' do + let(:stat_options) { { by: count } } + let(:count) { 1 } + it { expect(statsd).to have_received_increment_metric(stat, stat_options) } + end + + context 'that contain :tags' do + let(:stat_options) { { tags: tags } } + let(:tags) { %w[foo bar] } + it { expect(statsd).to have_received_increment_metric(stat, stat_options) } + + context 'which are frozen' do + let(:tags) { super().freeze } + it { expect(statsd).to have_received_increment_metric(stat, stat_options) } + end + end + end + + context 'which raises an error' do + before(:each) do + expect(statsd).to receive(:increment).and_raise(StandardError) + expect(Datadog::Tracer.log).to receive(:error) + end + + it { expect { increment }.to_not raise_error } + end + end + end + + describe '#time' do + subject(:time) { metrics.time(stat, stat_options, &block) } + let(:stat) { :foo } + let(:stat_options) { nil } + let(:block) { proc {} } + + context 'when #statsd is nil' do + before(:each) do + allow(metrics).to receive(:statsd).and_return(nil) + expect { time }.to_not raise_error + end + + it { expect(statsd).to_not have_received_time_metric(stat) } + end + + context 'when #statsd is a Datadog::Statsd' do + context 'and given a block' do + it { expect { |b| metrics.time(stat, &b) }.to yield_control } + + context 'which raises an error' do + let(:block) { proc { raise error } } + let(:error) { RuntimeError.new } + # Expect the given block to raise its errors through + it { expect { time }.to raise_error(error) } + end + end + + context 'and given no options' do + before(:each) { expect { time }.to_not raise_error } + it { expect(statsd).to have_received_time_metric(stat) } + end + + context 'and given options' do + before(:each) { expect { time }.to_not raise_error } + + context 'that are empty' do + let(:stat_options) { {} } + it { expect(statsd).to have_received_time_metric(stat) } + end + + context 'that are frozen' do + let(:stat_options) { {}.freeze } + it { expect(statsd).to have_received_time_metric(stat) } + end + + context 'that contain :tags' do + let(:stat_options) { { tags: tags } } + let(:tags) { %w[foo bar] } + it { expect(statsd).to have_received_time_metric(stat, stat_options) } + + context 'which are frozen' do + let(:tags) { super().freeze } + it { expect(statsd).to have_received_time_metric(stat, stat_options) } + end + end + end + + context 'which raises an error' do + before(:each) do + expect(statsd).to receive(:distribution).and_raise(StandardError) + expect(Datadog::Tracer.log).to receive(:error) + end + + it { expect { time }.to_not raise_error } + end + end + end +end diff --git a/spec/ddtrace/propagation/distributed_headers_spec.rb b/spec/ddtrace/propagation/distributed_headers_spec.rb index 279650ce8f0..27e8fa7f3af 100644 --- a/spec/ddtrace/propagation/distributed_headers_spec.rb +++ b/spec/ddtrace/propagation/distributed_headers_spec.rb @@ -56,16 +56,18 @@ def env_header(name) end context 'incorrect header' do + shared_examples_for 'ignored trace ID header' do |header| + let(:env) { { env_header(header) => '100' } } + it { expect(headers.trace_id).to be_nil } + end + [ 'X-DATADOG-TRACE-ID-TYPO', 'X-DATDOG-TRACE-ID', 'X-TRACE-ID', 'TRACE-ID' ].each do |header| - # '100' is a valid value - let(:env) { { env_header(header) => '100' } } - - it { expect(headers.trace_id).to be_nil } + it_behaves_like 'ignored trace ID header', header end end @@ -101,16 +103,18 @@ def env_header(name) end context 'incorrect header' do + shared_examples_for 'ignored parent ID header' do |header| + let(:env) { { env_header(header) => '100' } } + it { expect(headers.parent_id).to be_nil } + end + [ 'X-DATADOG-PARENT-ID-TYPO', 'X-DATDOG-PARENT-ID', 'X-PARENT-ID', 'PARENT-ID' ].each do |header| - # '100' is a valid value - let(:env) { { env_header(header) => '100' } } - - it { expect(headers.parent_id).to be_nil } + it_behaves_like 'ignored parent ID header', header end end @@ -146,16 +150,18 @@ def env_header(name) end context 'incorrect header' do + shared_examples_for 'ignored sampling priority header' do |header| + let(:env) { { env_header(header) => '100' } } + it { expect(headers.sampling_priority).to be_nil } + end + [ 'X-DATADOG-SAMPLING-PRIORITY-TYPO', 'X-DATDOG-SAMPLING-PRIORITY', 'X-SAMPLING-PRIORITY', 'SAMPLING-PRIORITY' ].each do |header| - # '100' is a valid value - let(:env) { { env_header(header) => '100' } } - - it { expect(headers.sampling_priority).to be_nil } + it_behaves_like 'ignored sampling priority header', header end end diff --git a/spec/ddtrace/runtime/class_count_spec.rb b/spec/ddtrace/runtime/class_count_spec.rb new file mode 100644 index 00000000000..ace7bd82e32 --- /dev/null +++ b/spec/ddtrace/runtime/class_count_spec.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'ddtrace/runtime/class_count' + +RSpec.describe Datadog::Runtime::ClassCount do + describe '::value' do + subject(:value) { described_class.value } + it { is_expected.to be_a_kind_of(Integer) } + end + + describe '::available?' do + subject(:available?) { described_class.available? } + it { is_expected.to be true } + end +end diff --git a/spec/ddtrace/runtime/gc_spec.rb b/spec/ddtrace/runtime/gc_spec.rb new file mode 100644 index 00000000000..c45cddfe11e --- /dev/null +++ b/spec/ddtrace/runtime/gc_spec.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'ddtrace/runtime/gc' + +RSpec.describe Datadog::Runtime::GC do + describe '::stat' do + subject(:stat) { described_class.stat } + it { is_expected.to be_a_kind_of(Hash) } + end + + describe '::available?' do + subject(:available?) { described_class.available? } + it { is_expected.to be true } + end +end diff --git a/spec/ddtrace/runtime/identity_spec.rb b/spec/ddtrace/runtime/identity_spec.rb new file mode 100644 index 00000000000..910907bab49 --- /dev/null +++ b/spec/ddtrace/runtime/identity_spec.rb @@ -0,0 +1,39 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'ddtrace/runtime/identity' + +RSpec.describe Datadog::Runtime::Identity do + describe '::id' do + subject(:id) { described_class.id } + + it { is_expected.to be_a_kind_of(String) } + + context 'when invoked twice' do + it { expect(described_class.id).to eq(described_class.id) } + end + + context 'when invoked around a fork' do + let(:before_fork_id) { described_class.id } + let(:inside_fork_id) { described_class.id } + let(:after_fork_id) { described_class.id } + + it do + # Check before forking + expect(before_fork_id).to be_a_kind_of(String) + + # Invoke in fork, make sure expectations run before continuing. + Timeout.timeout(1) do + fork do + expect(inside_fork_id).to be_a_kind_of(String) + expect(inside_fork_id).to_not eq(before_fork_id) + end + Process.wait + end + + # Check after forking + expect(after_fork_id).to eq(before_fork_id) + end + end + end +end diff --git a/spec/ddtrace/runtime/metrics_spec.rb b/spec/ddtrace/runtime/metrics_spec.rb new file mode 100644 index 00000000000..592a98cf518 --- /dev/null +++ b/spec/ddtrace/runtime/metrics_spec.rb @@ -0,0 +1,154 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'ddtrace' +require 'ddtrace/runtime/metrics' + +RSpec.describe Datadog::Runtime::Metrics do + subject(:runtime_metrics) { described_class.new } + + describe '#associate_with_span' do + subject(:associate_with_span) { runtime_metrics.associate_with_span(span) } + let(:span) { instance_double(Datadog::Span, service: service) } + let(:service) { 'parser' } + + before do + expect(span).to receive(:set_tag) + .with(Datadog::Ext::Runtime::TAG_LANG, Datadog::Runtime::Identity.lang) + + expect(span).to receive(:set_tag) + .with(Datadog::Ext::Runtime::TAG_RUNTIME_ID, Datadog::Runtime::Identity.id) + + associate_with_span + end + + it 'registers the span\'s service' do + expect(runtime_metrics.default_metric_options[:tags]).to include("service:#{service}") + end + end + + describe '#flush' do + subject(:flush) { runtime_metrics.flush } + + shared_examples_for 'runtime metric flush' do |metric, metric_name| + let(:metric_value) { double('metric_value') } + + context 'when available' do + before(:each) { allow(runtime_metrics).to receive(:gauge) } + + it do + allow(metric).to receive(:available?) + .and_return(true) + allow(metric).to receive(:value) + .and_return(metric_value) + + flush + + expect(runtime_metrics).to have_received(:gauge) + .with(metric_name, metric_value) + .once + end + end + + context 'when unavailable' do + it do + allow(metric).to receive(:available?) + .and_return(false) + expect(metric).to_not receive(:value) + expect(runtime_metrics).to_not receive(:gauge) + .with(metric_name, anything) + + flush + end + end + + context 'when an error is thrown' do + before(:each) { allow(Datadog::Tracer.log).to receive(:error) } + + it do + allow(metric).to receive(:available?) + .and_raise(RuntimeError) + + flush + + expect(Datadog::Tracer.log).to have_received(:error) + .with(/Error while sending runtime metric./) + .at_least(:once) + end + end + end + + shared_examples_for 'a flush of all runtime metrics' do + context 'including ClassCount' do + it_behaves_like 'runtime metric flush', + Datadog::Runtime::ClassCount, + Datadog::Ext::Runtime::Metrics::METRIC_CLASS_COUNT + end + + context 'including ThreadCount' do + it_behaves_like 'runtime metric flush', + Datadog::Runtime::ThreadCount, + Datadog::Ext::Runtime::Metrics::METRIC_THREAD_COUNT + end + + context 'including GC stats' do + before(:each) { allow(runtime_metrics).to receive(:gauge) } + + it do + flush + + runtime_metrics.gc_metrics.each do |metric_name, _metric_value| + expect(runtime_metrics).to have_received(:gauge) + .with(metric_name, kind_of(Numeric)) + .once + end + end + end + end + + it_behaves_like 'a flush of all runtime metrics' + end + + describe '#gc_metrics' do + subject(:gc_metrics) { runtime_metrics.gc_metrics } + + it 'has a metric for each value in GC.stat' do + is_expected.to have(GC.stat.keys.count).items + + gc_metrics.each do |metric, value| + expect(metric).to start_with(Datadog::Ext::Runtime::Metrics::METRIC_GC_PREFIX) + expect(value).to be_a_kind_of(Numeric) + end + end + end + + describe '#default_metric_options' do + subject(:default_metric_options) { runtime_metrics.default_metric_options } + + describe ':tags' do + subject(:default_tags) { default_metric_options[:tags] } + + context 'when no services have been registered' do + it do + is_expected.to have(2).items + + is_expected.to include('language:ruby') + is_expected.to include("runtime-id:#{Datadog::Runtime::Identity.id}") + end + end + + context 'when services have been registered' do + let(:services) { %w[parser serializer] } + before(:each) { services.each { |service| runtime_metrics.register_service(service) } } + + it do + is_expected.to have(4).items + + is_expected.to include('language:ruby') + is_expected.to include("runtime-id:#{Datadog::Runtime::Identity.id}") + is_expected.to include(*services.collect { |service| "service:#{service}" }) + end + end + end + end +end diff --git a/spec/ddtrace/runtime/thread_count_spec.rb b/spec/ddtrace/runtime/thread_count_spec.rb new file mode 100644 index 00000000000..67dc2bdb34d --- /dev/null +++ b/spec/ddtrace/runtime/thread_count_spec.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'ddtrace/runtime/thread_count' + +RSpec.describe Datadog::Runtime::ThreadCount do + describe '::value' do + subject(:value) { described_class.value } + it { is_expected.to be_a_kind_of(Integer) } + end + + describe '::available?' do + subject(:available?) { described_class.available? } + it { is_expected.to be true } + end +end diff --git a/spec/ddtrace/tracer_integration_spec.rb b/spec/ddtrace/tracer_integration_spec.rb index eae8735a150..08f3a1d47ef 100644 --- a/spec/ddtrace/tracer_integration_spec.rb +++ b/spec/ddtrace/tracer_integration_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' require 'ddtrace' +require 'ddtrace/ext/runtime' +require 'ddtrace/runtime/identity' require 'ddtrace/propagation/http_propagator' RSpec.describe Datadog::Tracer do @@ -15,6 +17,14 @@ def origin_tag(span) span.get_tag(Datadog::Ext::DistributedTracing::ORIGIN_KEY) end + def lang_tag(span) + span.get_tag(Datadog::Ext::Runtime::TAG_LANG) + end + + def runtime_id_tag(span) + span.get_tag(Datadog::Ext::Runtime::TAG_RUNTIME_ID) + end + describe '#active_root_span' do subject(:active_root_span) { tracer.active_root_span } @@ -56,12 +66,16 @@ def origin_tag(span) it { expect(parent_span.parent_id).to eq(0) } it { expect(sampling_priority_metric(parent_span)).to eq(1) } it { expect(origin_tag(parent_span)).to eq('synthetics') } + it { expect(lang_tag(parent_span)).to eq('ruby') } + it { expect(runtime_id_tag(parent_span)).to eq(Datadog::Runtime::Identity.id) } it { expect(child_span.name).to eq(child_span_name) } it { expect(child_span.finished?).to be(true) } it { expect(child_span.trace_id).to eq(parent_span.trace_id) } it { expect(child_span.parent_id).to eq(parent_span.span_id) } it { expect(sampling_priority_metric(child_span)).to eq(1) } it { expect(origin_tag(child_span)).to eq('synthetics') } + it { expect(lang_tag(child_span)).to eq('ruby') } + it { expect(runtime_id_tag(child_span)).to eq(Datadog::Runtime::Identity.id) } # This is expected to be child_span because when propagated, we don't # propagate the root span, only its ID. Therefore the span reference # should be the first span on the other end of the distributed trace. diff --git a/spec/ddtrace/workers_integration_spec.rb b/spec/ddtrace/workers_integration_spec.rb index c2653accbf8..e30e350887a 100644 --- a/spec/ddtrace/workers_integration_spec.rb +++ b/spec/ddtrace/workers_integration_spec.rb @@ -3,6 +3,7 @@ require 'time' require 'json' +require 'ddtrace' require 'ddtrace/tracer' require 'ddtrace/workers' require 'ddtrace/writer' @@ -31,11 +32,11 @@ w.instance_variable_set( :@worker, Datadog::Workers::AsyncTransport.new( - transport, - buffer_size, - w.instance_variable_get(:@trace_handler), - w.instance_variable_get(:@service_handler), - flush_interval + transport: transport, + buffer_size: buffer_size, + on_trace: w.instance_variable_get(:@trace_handler), + on_service: w.instance_variable_get(:@service_handler), + interval: flush_interval ) ) w.worker.start @@ -217,11 +218,11 @@ def wait_for_flush(num = 1, period = 0.1) let(:worker) do Datadog::Workers::AsyncTransport.new( - nil, - 100, - trace_task, - service_task, - interval + transport: nil, + buffer_size: 100, + on_trace: trace_task, + on_service: service_task, + interval: interval ) end diff --git a/spec/ddtrace/workers_spec.rb b/spec/ddtrace/workers_spec.rb index d987ce3d03a..e143bb7f496 100644 --- a/spec/ddtrace/workers_spec.rb +++ b/spec/ddtrace/workers_spec.rb @@ -7,7 +7,13 @@ buf = StringIO.new Datadog::Tracer.log = Datadog::Logger.new(buf) task = proc { raise StandardError } - worker = Datadog::Workers::AsyncTransport.new(nil, 100, task, task, 0.5) + worker = Datadog::Workers::AsyncTransport.new( + transport: nil, + buffer_size: 100, + on_trace: task, + on_service: task, + interval: 0.5 + ) worker.enqueue_trace(get_test_traces(1)) worker.enqueue_service(get_test_services) diff --git a/spec/ddtrace/writer_spec.rb b/spec/ddtrace/writer_spec.rb index c03b25ba27c..c240141725f 100644 --- a/spec/ddtrace/writer_spec.rb +++ b/spec/ddtrace/writer_spec.rb @@ -140,6 +140,40 @@ end end + describe '#send_runtime_metrics' do + subject(:send_runtime_metrics) { writer.send_runtime_metrics } + + context 'when runtime metrics are' do + context 'enabled' do + around do |example| + Datadog.configuration.runtime_metrics_enabled = Datadog.configuration.runtime_metrics_enabled.tap do + Datadog.configuration.runtime_metrics_enabled = true + example.run + end + end + + it do + expect(writer.runtime_metrics).to receive(:flush) + send_runtime_metrics + end + end + + context 'disabled' do + around do |example| + Datadog.configuration.runtime_metrics_enabled = Datadog.configuration.runtime_metrics_enabled.tap do + Datadog.configuration.runtime_metrics_enabled = false + example.run + end + end + + it do + expect(writer.runtime_metrics).to_not receive(:flush) + send_runtime_metrics + end + end + end + end + describe '#sampling_updater' do subject(:result) { writer.send(:sampling_updater, action, response, api) } let(:options) { { priority_sampler: sampler } } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5810beb84a9..111616eaefe 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,6 +20,7 @@ require 'support/synchronization_helpers' require 'support/log_helpers' require 'support/http_helpers' +require 'support/metric_helpers' WebMock.allow_net_connect! WebMock.disable! @@ -30,6 +31,7 @@ config.include ConfigurationHelpers config.include SynchronizationHelpers config.include LogHelpers + config.include MetricHelpers config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true diff --git a/spec/support/metric_helpers.rb b/spec/support/metric_helpers.rb new file mode 100644 index 00000000000..6352b68cb79 --- /dev/null +++ b/spec/support/metric_helpers.rb @@ -0,0 +1,79 @@ +require 'support/statsd_helpers' + +module MetricHelpers + include RSpec::Mocks::ArgumentMatchers + + shared_context 'metrics' do + include_context 'statsd' + + def metric_options(options = nil) + return options unless options.nil? || options.is_a?(Hash) + Datadog::Metrics.metric_options(options) + end + + def check_options!(options) + if options.is_a?(Hash) + expect(options.frozen?).to be false + expect(options[:tags].frozen?).to be false if options.key?(:tags) + end + end + + # Define matchers for use in examples + def have_received_distribution_metric(stat, value = kind_of(Numeric), options = {}) + options = metric_options(options) + check_options!(options) + have_received(:distribution).with(stat, value, options) + end + + def have_received_gauge_metric(stat, value = kind_of(Numeric), options = {}) + options = metric_options(options) + check_options!(options) + have_received(:gauge).with(stat, value, options) + end + + def have_received_increment_metric(stat, options = {}) + options = metric_options(options) + check_options!(options) + have_received(:increment).with(stat, options) + end + + def have_received_time_metric(stat, options = {}) + options = metric_options(options) + check_options!(options) + have_received(:distribution).with(stat, kind_of(Numeric), options) + end + + # Define shared examples + shared_examples_for 'an operation that sends distribution metric' do |stat, options = {}| + let(:value) { kind_of(Numeric) } + + it do + subject + expect(statsd).to have_received_distribution_metric(stat, value, options) + end + end + + shared_examples_for 'an operation that sends gauge metric' do |stat, options = {}| + let(:value) { kind_of(Numeric) } + + it do + subject + expect(statsd).to have_received_gauge_metric(stat, value, options) + end + end + + shared_examples_for 'an operation that sends increment metric' do |stat, options = {}| + it do + subject + expect(statsd).to have_received_increment_metric(stat, options) + end + end + + shared_examples_for 'an operation that sends time metric' do |stat, options = {}| + it do + subject + expect(statsd).to have_received_time_metric(stat, options) + end + end + end +end diff --git a/spec/support/statsd_helpers.rb b/spec/support/statsd_helpers.rb new file mode 100644 index 00000000000..9cb594d4c49 --- /dev/null +++ b/spec/support/statsd_helpers.rb @@ -0,0 +1,38 @@ +module StatsdHelpers + shared_context 'statsd' do + let(:statsd) { spy('statsd') } # TODO: Make this an instance double. + let(:stats) { Hash.new(0) } + let(:stats_mutex) { Mutex.new } + + before(:each) do + allow(statsd).to receive(:distribution) do |name, _value, _options = {}| + stats_mutex.synchronize do + stats[name] = 0 unless stats.key?(name) + stats[name] += 1 + end + end + + allow(statsd).to receive(:gauge) do |name, _value, _options = {}| + stats_mutex.synchronize do + stats[name] = 0 unless stats.key?(name) + stats[name] += 1 + end + end + + allow(statsd).to receive(:increment) do |name, options = {}| + stats_mutex.synchronize do + stats[name] = 0 unless stats.key?(name) + stats[name] += options.key?(:by) ? options[:by] : 1 + end + end + + allow(statsd).to receive(:time) do |name, _options = {}, &block| + stats_mutex.synchronize do + stats[name] = 0 unless stats.key?(name) + stats[name] += 1 + end + block.call + end + end + end +end diff --git a/test/tracer_test.rb b/test/tracer_test.rb index 2bcf38894a6..c5d3de70c2c 100644 --- a/test/tracer_test.rb +++ b/test/tracer_test.rb @@ -191,7 +191,7 @@ def test_trace_all_args assert_equal('special-service', span.service) assert_equal('extra-resource', span.resource) assert_equal('my-type', span.span_type) - assert_equal(5, span.meta.length) + assert_equal(7, span.meta.length) assert_equal('test', span.get_tag('env')) assert_equal('cool', span.get_tag('temp')) assert_equal('value1', span.get_tag('tag1')) From f08be0136e7964ac4a9721e303f46f042a09d5d7 Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Mon, 15 Apr 2019 16:27:56 -0400 Subject: [PATCH 3/5] Update release notes for 0.22.0 --- CHANGELOG.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 907d9401d91..e4e2e73583d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ ## [Unreleased (beta)] +## [0.22.0] - 2019-04-15 + +Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.22.0 + +Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.21.2...v0.22.0 + +In this release we are adding initial support for the **beta** [Runtime metrics collection](https://docs.datadoghq.com/tracing/advanced/runtime_metrics/?tab=ruby) feature. + +### Changed + +- Add warning log if an integration is incompatible (#722) (@ericmustin) + +### Added + +- Initial beta support for Runtime metrics colletion (#677) + ## [0.21.2] - 2019-04-10 Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.21.2 @@ -751,8 +767,9 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1 Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1 -[Unreleased (stable)]: https://github.com/DataDog/dd-trace-rb/compare/v0.21.2...master -[Unreleased (beta)]: https://github.com/DataDog/dd-trace-rb/compare/v0.21.2...0.22-dev +[Unreleased (stable)]: https://github.com/DataDog/dd-trace-rb/compare/v0.22.0...master +[Unreleased (beta)]: https://github.com/DataDog/dd-trace-rb/compare/v0.22.0...0.23-dev +[0.22.0]: https://github.com/DataDog/dd-trace-rb/compare/v0.21.2...v0.22.0 [0.21.2]: https://github.com/DataDog/dd-trace-rb/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/DataDog/dd-trace-rb/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/DataDog/dd-trace-rb/compare/v0.20.0...v0.21.0 From 99de3f7bf1cb0af7ed8a75848ac358b2aefc4fd7 Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Mon, 15 Apr 2019 16:29:07 -0400 Subject: [PATCH 4/5] Bump version to 0.22.0 --- lib/ddtrace/version.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ddtrace/version.rb b/lib/ddtrace/version.rb index b24f57d7cc8..f9fd87df76e 100644 --- a/lib/ddtrace/version.rb +++ b/lib/ddtrace/version.rb @@ -1,8 +1,8 @@ module Datadog module VERSION MAJOR = 0 - MINOR = 21 - PATCH = 2 + MINOR = 22 + PATCH = 0 PRE = nil STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.') From 8d7d69142841779e5c61cc89bf470f60e96061d5 Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Mon, 15 Apr 2019 16:33:13 -0400 Subject: [PATCH 5/5] Fix changelog spelling --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e2e73583d..30bf2aae4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ In this release we are adding initial support for the **beta** [Runtime metrics ### Added -- Initial beta support for Runtime metrics colletion (#677) +- Initial beta support for Runtime metrics collection (#677) ## [0.21.2] - 2019-04-10