diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index b574a34189..af81db7539 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -1757,20 +1757,17 @@ If metrics are configured, the trace library will automatically collect and send These include: -| Name | Type | Description | -| ------------------------------------- | ------- | ---------------------------------- | -| `datadog.tracer.runtime.class_count` | `gauge` | Number of classes in memory space. | -| `datadog.tracer.runtime.heap_size`. | `gauge` | Size of memory heap, in bytes. | -| `datadog.tracer.runtime.thread_count` | `gauge` | Number of threads. | +| 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 | -| --------------------------------- | ------------------------------------------------------------------- | -| `datadog.tracer.lang` | Programming language traced. (e.g. `ruby`) | -| `datadog.tracer.lang_interpreter` | Language interpreter used, if available. (e.g. `ruby-x86_64-linux`) | -| `datadog.tracer.lang_version` | Version of language traced. (e.g. `2.3.7`) | -| `datadog.tracer.version` | Version of tracer library/module. (e.g. `0.16.1` ) | +| Name | Description | +| ---------- | ------------------------------------------ | +| `language` | Programming language traced. (e.g. `ruby`) | ### OpenTracing diff --git a/lib/ddtrace/configuration.rb b/lib/ddtrace/configuration.rb index ca4d070ef0..73d7c68fa7 100644 --- a/lib/ddtrace/configuration.rb +++ b/lib/ddtrace/configuration.rb @@ -23,6 +23,10 @@ def metrics configuration.metrics end + def runtime_metrics + configuration.runtime_metrics + end + def tracer configuration.tracer end diff --git a/lib/ddtrace/configuration/settings.rb b/lib/ddtrace/configuration/settings.rb index 6d6d65a846..c9696e264e 100644 --- a/lib/ddtrace/configuration/settings.rb +++ b/lib/ddtrace/configuration/settings.rb @@ -17,6 +17,7 @@ class Settings lazy: true option :metrics, default: Metrics.new + option :runtime_metrics, default: Runtime::Metrics.new option :tracer, default: Tracer.new def initialize(options = {}) @@ -32,6 +33,7 @@ def configure(options = {}) yield(self) if block_given? end + remove_method :metrics def metrics(options = nil) metrics = get_option(:metrics) return metrics if options.nil? @@ -39,7 +41,16 @@ def metrics(options = nil) metrics.configure(options) end + remove_method :runtime_metrics + def runtime_metrics(options = nil) + runtime_metrics = get_option(: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/ext/runtime.rb b/lib/ddtrace/ext/runtime.rb index 455ed45488..778c8d9666 100644 --- a/lib/ddtrace/ext/runtime.rb +++ b/lib/ddtrace/ext/runtime.rb @@ -15,10 +15,17 @@ module Runtime LANG_VERSION = RUBY_VERSION TRACER_VERSION = Datadog::VERSION::STRING + TAG_LANG = 'language'.freeze + TAG_RUNTIME_ID = 'runtime-id'.freeze + # Metrics - METRIC_CLASS_COUNT = 'datadog.tracer.runtime.class_count'.freeze - METRIC_HEAP_SIZE = 'datadog.tracer.runtime.heap_size'.freeze - METRIC_THREAD_COUNT = 'datadog.tracer.runtime.thread_count'.freeze + module Metrics + 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 index b86a1a9360..67e0cf449f 100644 --- a/lib/ddtrace/metrics.rb +++ b/lib/ddtrace/metrics.rb @@ -1,5 +1,6 @@ require 'ddtrace/ext/metrics' +require 'set' require 'ddtrace/utils/time' require 'ddtrace/runtime/identity' diff --git a/lib/ddtrace/runtime/gc.rb b/lib/ddtrace/runtime/gc.rb new file mode 100644 index 0000000000..90d329fba1 --- /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/heap_size.rb b/lib/ddtrace/runtime/heap_size.rb deleted file mode 100644 index 8eedbd4c60..0000000000 --- a/lib/ddtrace/runtime/heap_size.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Datadog - module Runtime - # Retrieves heap size (in bytes) from runtime - module HeapSize - module_function - - def value - GC.stat[:heap_allocated_pages] * 40 * 408 - end - - def available? - GC.stat.key?(:heap_allocated_pages) - end - end - end -end diff --git a/lib/ddtrace/runtime/metrics.rb b/lib/ddtrace/runtime/metrics.rb index 930ba72bc7..e4af2fd653 100644 --- a/lib/ddtrace/runtime/metrics.rb +++ b/lib/ddtrace/runtime/metrics.rb @@ -1,19 +1,60 @@ require 'ddtrace/ext/runtime' + +require 'ddtrace/metrics' require 'ddtrace/runtime/class_count' -require 'ddtrace/runtime/heap_size' +require 'ddtrace/runtime/gc' +require 'ddtrace/runtime/identity' require 'ddtrace/runtime/thread_count' module Datadog module Runtime # For generating runtime metrics - module Metrics - module_function + class Metrics < Datadog::Metrics + def initialize(*args) + super + + # Initialize service list + @services = Set.new + @service_tags = nil + end - # Flush all runtime metrics to a Datadog::Metrics instance. - def flush(metrics = Datadog.metrics) - try_flush { metrics.gauge(Ext::Runtime::METRIC_CLASS_COUNT, ClassCount.value) if ClassCount.available? } - try_flush { metrics.gauge(Ext::Runtime::METRIC_HEAP_SIZE, HeapSize.value) if HeapSize.available? } - try_flush { metrics.gauge(Ext::Runtime::METRIC_THREAD_COUNT, ThreadCount.value) if ThreadCount.available? } + 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_metric(Ext::Runtime::TAG_LANG, Runtime::Identity.lang) + span.set_metric(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 + 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 + GC.stat.map do |k, v| + ["#{Ext::Runtime::Metrics::METRIC_GC_PREFIX}.#{k}", v] + end.to_h end def try_flush @@ -21,6 +62,29 @@ def try_flush 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/workers.rb b/lib/ddtrace/workers.rb index 54ea673b50..8fee44ca08 100644 --- a/lib/ddtrace/workers.rb +++ b/lib/ddtrace/workers.rb @@ -64,10 +64,10 @@ def callback_services end def callback_runtime_metrics - return true unless Datadog.metrics.send_stats? + return true unless Datadog.runtime_metrics.send_stats? begin - Datadog::Runtime::Metrics.flush(Datadog.metrics) + Datadog.runtime_metrics.flush 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 diff --git a/spec/ddtrace/runtime/gc_spec.rb b/spec/ddtrace/runtime/gc_spec.rb new file mode 100644 index 0000000000..c45cddfe11 --- /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/heap_size_spec.rb b/spec/ddtrace/runtime/heap_size_spec.rb deleted file mode 100644 index 58a4dd34b2..0000000000 --- a/spec/ddtrace/runtime/heap_size_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' -require 'ddtrace/runtime/heap_size' - -RSpec.describe Datadog::Runtime::HeapSize 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/metrics_spec.rb b/spec/ddtrace/runtime/metrics_spec.rb index 42c9eef604..564eaafdc0 100644 --- a/spec/ddtrace/runtime/metrics_spec.rb +++ b/spec/ddtrace/runtime/metrics_spec.rb @@ -5,22 +5,48 @@ require 'ddtrace/runtime/metrics' RSpec.describe Datadog::Runtime::Metrics do - describe '::flush' do - let(:metrics) { spy(Datadog::Metrics) } + 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_metric) + .with(Datadog::Ext::Runtime::TAG_LANG, Datadog::Runtime::Identity.lang) + + expect(span).to receive(:set_metric) + .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) - expect(metrics).to receive(:gauge) - .with(metric_name, metric_value) flush + + expect(runtime_metrics).to have_received(:gauge) + .with(metric_name, metric_value) + .once end end @@ -29,7 +55,7 @@ allow(metric).to receive(:available?) .and_return(false) expect(metric).to_not receive(:value) - expect(metrics).to_not receive(:gauge) + expect(runtime_metrics).to_not receive(:gauge) .with(metric_name, anything) flush @@ -52,33 +78,73 @@ context 'including ClassCount' do it_behaves_like 'runtime metric flush', Datadog::Runtime::ClassCount, - Datadog::Ext::Runtime::METRIC_CLASS_COUNT - end - - context 'including HeapSize' do - it_behaves_like 'runtime metric flush', - Datadog::Runtime::HeapSize, - Datadog::Ext::Runtime::METRIC_HEAP_SIZE + Datadog::Ext::Runtime::Metrics::METRIC_CLASS_COUNT end context 'including ThreadCount' do it_behaves_like 'runtime metric flush', Datadog::Runtime::ThreadCount, - Datadog::Ext::Runtime::METRIC_THREAD_COUNT + 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 - context 'given no arguments' do - subject(:flush) { described_class.flush } + it_behaves_like 'a flush of all runtime metrics' + end + + describe '#gc_metrics' do + subject(:gc_metrics) { runtime_metrics.gc_metrics } - it_behaves_like 'a flush of all runtime metrics' do - before(:each) { allow(Datadog).to receive(:metrics).and_return(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 } - context 'given a Datadog::Metrics object' do - subject(:flush) { described_class.flush(metrics) } - it_behaves_like 'a flush of all runtime metrics' + 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