diff --git a/lib/datadog/core/telemetry/event.rb b/lib/datadog/core/telemetry/event.rb index 49b292878bb..60a9886eb1e 100644 --- a/lib/datadog/core/telemetry/event.rb +++ b/lib/datadog/core/telemetry/event.rb @@ -286,6 +286,33 @@ def type 'app-closing' end end + + # Telemetry class for the 'generate-metrics' event + class GenerateMetrics < Base + def type + 'generate-metrics' + end + + def initialize(namespace, metric_series) + super() + @namespace = namespace + @metric_series = metric_series + end + + def payload(_) + { + namespace: @namespace, + series: @metric_series.map(&:to_h) + } + end + end + + # Telemetry class for the 'distributions' event + class Distributions < GenerateMetrics + def type + 'distributions' + end + end end end end diff --git a/lib/datadog/core/telemetry/metric.rb b/lib/datadog/core/telemetry/metric.rb new file mode 100644 index 00000000000..2c6ba8d97de --- /dev/null +++ b/lib/datadog/core/telemetry/metric.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module Datadog + module Core + module Telemetry + # Telemetry metrics data model (internal Datadog metrics for client libraries) + module Metric + def self.metric_id(type, name, tags = []) + "#{type}::#{name}::#{tags.join(',')}" + end + + # Base class for all metric types + class Base + attr_reader :name, :tags, :values, :common, :interval + + # @param name [String] metric name + # @param tags [Array|Hash{String=>String}] metric tags as hash of array of "tag:val" strings + # @param common [Boolean] true if the metric is common for all languages, false for Ruby-specific metric + # @param interval [Integer] metrics aggregation interval in seconds + def initialize(name, tags: {}, common: true, interval: nil) + @name = name + @values = [] + @tags = tags_to_array(tags) + @common = common + @interval = interval + end + + def track(value); end + + def type; end + + def to_h + # @type var res: Hash[Symbol, untyped] + res = { + metric: name, + points: values, + type: type, + tags: tags, + common: common + } + res[:interval] = interval if interval + res + end + + private + + def tags_to_array(tags) + return tags if tags.is_a?(Array) + + tags.map { |k, v| "#{k}:#{v}" } + end + end + + # Count metric adds up all the submitted values in a time interval. This would be suitable for a + # metric tracking the number of website hits, for instance. + class Count < Base + TYPE = 'count' + + def type + TYPE + end + + def inc(value = 1) + track(value) + end + + def dec(value = 1) + track(-value) + end + + def track(value) + if values.empty? + values << [Time.now.to_i, value] + else + values[0][0] = Time.now.to_i + values[0][1] += value + end + end + end + + # A gauge type takes the last value reported during the interval. This type would make sense for tracking RAM or + # CPU usage, where taking the last value provides a representative picture of the host’s behavior during the time + # interval. + class Gauge < Base + TYPE = 'gauge' + + def type + TYPE + end + + def track(value) + if values.empty? + values << [Time.now.to_i, value] + else + values[0][0] = Time.now.to_i + values[0][1] = value + end + end + end + + # The rate type takes the count and divides it by the length of the time interval. This is useful if you’re + # interested in the number of hits per second. + class Rate < Base + TYPE = 'rate' + + def initialize(name, tags: {}, common: true, interval: nil) + super + + @value = 0.0 + end + + def type + TYPE + end + + def track(value = 1.0) + @value += value + + rate = interval ? @value / interval : 0.0 + @values = [[Time.now.to_i, rate]] + end + end + + # Distribution metric represents the global statistical distribution of a set of values. + class Distribution < Base + TYPE = 'distributions' + + def type + TYPE + end + + def track(value) + values << value + end + + # distribution metric data does not have type field + def to_h + { + metric: name, + points: values, + tags: tags, + common: common + } + end + end + end + end + end +end diff --git a/sig/datadog/core/telemetry/event.rbs b/sig/datadog/core/telemetry/event.rbs index 4e0e3824109..791f014f9a4 100644 --- a/sig/datadog/core/telemetry/event.rbs +++ b/sig/datadog/core/telemetry/event.rbs @@ -55,6 +55,16 @@ module Datadog class AppClosing < Base end + + class GenerateMetrics < Base + @namespace: String + @metric_series: Enumerable[Datadog::Core::Telemetry::Metric::Base] + + def initialize: (String namespace, Enumerable[Datadog::Core::Telemetry::Metric::Base] metric_series) -> void + end + + class Distributions < GenerateMetrics + end end end end diff --git a/sig/datadog/core/telemetry/metric.rbs b/sig/datadog/core/telemetry/metric.rbs new file mode 100644 index 00000000000..3eda8cda131 --- /dev/null +++ b/sig/datadog/core/telemetry/metric.rbs @@ -0,0 +1,102 @@ +module Datadog + module Core + module Telemetry + module Metric + type metric_type = "count" | "gauge" | "rate" | "distributions" | nil + + type input_value = Integer | Float + + type metric_value = Array[input_value] + type distribution_value = input_value + + type tags_input = ::Hash[String, String] | Array[String] + + def self.metric_id: (metric_type type, String name, ?Array[String] tags) -> ::String + + class Base + @name: String + + @values: Array[untyped] + + @tags: Array[String] + + @common: bool + + @interval: Integer? + + attr_reader name: String + + attr_reader tags: Array[String] + + attr_reader values: Array[untyped] + + attr_reader common: bool + + attr_reader interval: Integer? + + def initialize: (String name, ?tags: tags_input, ?common: bool, ?interval: Integer?) -> void + + def track: (Numeric value) -> void + + def type: () -> metric_type + + def to_h: () -> Hash[Symbol, untyped] + + private + + def tags_to_array: (tags_input tags) -> Array[String] + end + + class Count < Base + TYPE: "count" + + @values: Array[metric_value] + attr_reader values: Array[metric_value] + + def type: () -> "count" + + def inc: (?::Integer value) -> void + + def dec: (?::Integer value) -> void + + def track: (Integer value) -> void + end + + class Gauge < Base + TYPE: "gauge" + + def type: () -> "gauge" + + def track: (input_value value) -> void + end + + class Rate < Base + @value: Float + + @values: Array[metric_value] + attr_reader values: Array[metric_value] + + TYPE: "rate" + + def initialize: (String name, ?tags: tags_input, ?common: bool, ?interval: Integer?) -> void + + def type: () -> "rate" + + def track: (?::Float value) -> void + end + + class Distribution < Base + TYPE: "distributions" + + @values: Array[distribution_value] + attr_reader values: Array[distribution_value] + + def type: () -> "distributions" + + def track: (input_value value) -> void + def to_h: () -> { metric: String, points: Array[distribution_value], tags: Array[String], common: bool } + end + end + end + end +end diff --git a/spec/datadog/core/telemetry/event_spec.rb b/spec/datadog/core/telemetry/event_spec.rb index 32d83e54fc4..f53bd9468ad 100644 --- a/spec/datadog/core/telemetry/event_spec.rb +++ b/spec/datadog/core/telemetry/event_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'datadog/core/telemetry/event' +require 'datadog/core/telemetry/metric' RSpec.describe Datadog::Core::Telemetry::Event do let(:id) { double('seq_id') } @@ -207,4 +208,48 @@ def contain_configuration(*array) is_expected.to eq({}) end end + + context 'GenerateMetrics' do + let(:event) { described_class::GenerateMetrics.new(namespace, metrics) } + + let(:namespace) { 'general' } + let(:metric_name) { 'request_count' } + let(:metric) do + Datadog::Core::Telemetry::Metric::Count.new(metric_name, tags: { status: '200' }) + end + let(:metrics) { [metric] } + + let(:expected_metric_series) { [metric.to_h] } + + it do + is_expected.to eq( + { + namespace: namespace, + series: expected_metric_series + } + ) + end + end + + context 'Distributions' do + let(:event) { described_class::Distributions.new(namespace, metrics) } + + let(:namespace) { 'general' } + let(:metric_name) { 'request_duration' } + let(:metric) do + Datadog::Core::Telemetry::Metric::Distribution.new(metric_name, tags: { status: '200' }) + end + let(:metrics) { [metric] } + + let(:expected_metric_series) { [metric.to_h] } + + it do + is_expected.to eq( + { + namespace: namespace, + series: expected_metric_series + } + ) + end + end end diff --git a/spec/datadog/core/telemetry/metric_spec.rb b/spec/datadog/core/telemetry/metric_spec.rb new file mode 100644 index 00000000000..8b77475a8f9 --- /dev/null +++ b/spec/datadog/core/telemetry/metric_spec.rb @@ -0,0 +1,277 @@ +require 'spec_helper' + +require 'datadog/core/telemetry/metric' + +RSpec.describe Datadog::Core::Telemetry::Metric do + let(:now) { 123123 } + before { allow(Time).to receive(:now).and_return(now, now + 1, now + 2, now + 3) } + + describe '.metric_id' do + subject(:metric_id) { described_class.metric_id(type, name, tags) } + + let(:type) { 'type' } + let(:name) { 'name' } + let(:tags) { ['tag1:val1', 'tag2:val2'] } + + it { is_expected.to eq('type::name::tag1:val1,tag2:val2') } + end + + describe Datadog::Core::Telemetry::Metric::Count do + subject(:metric) { described_class.new(name, tags: tags) } + + let(:name) { 'metric_name' } + let(:tags) { { tag1: 'val1', tag2: 'val2' } } + + it do + is_expected.to have_attributes( + name: name, + tags: ['tag1:val1', 'tag2:val2'], + interval: nil, + common: true, + values: [] + ) + end + + describe '#type' do + subject(:type) { metric.type } + + it { is_expected.to eq('count') } + end + + describe '#inc' do + subject(:inc) { metric.inc(value) } + + let(:value) { 5 } + + it 'tracks the value' do + expect { inc }.to change { metric.values }.from([]).to([[now, value]]) + end + + context 'incrementing again' do + it 'adds the value to the previous one and updates timestamp' do + metric.inc(value) + expect { inc }.to change { metric.values }.from([[now, value]]).to([[now + 1, value + value]]) + end + end + end + + describe '#dec' do + subject(:dec) { metric.dec(value) } + + let(:value) { 5 } + + it 'tracks the value' do + expect { dec }.to change { metric.values }.from([]).to([[now, -value]]) + end + end + + describe '#to_h' do + subject(:to_h) { metric.to_h } + let(:value) { 2 } + + before do + metric.inc(value) + end + + it do + is_expected.to eq( + metric: name, + points: [[now, 2]], + type: 'count', + tags: ['tag1:val1', 'tag2:val2'], + common: true + ) + end + end + end + + describe Datadog::Core::Telemetry::Metric::Gauge do + subject(:metric) { described_class.new(name, tags: tags, interval: interval) } + + let(:name) { 'metric_name' } + let(:tags) { { tag1: 'val1', tag2: 'val2' } } + let(:interval) { 10 } + + it do + is_expected.to have_attributes( + name: name, + tags: ['tag1:val1', 'tag2:val2'], + interval: interval, + common: true, + values: [] + ) + end + + describe '#type' do + subject(:type) { metric.type } + + it { is_expected.to eq('gauge') } + end + + describe '#track' do + subject(:track) { metric.track(value) } + + let(:value) { 5 } + + it 'tracks the value' do + expect { track }.to change { metric.values }.from([]).to([[now, value]]) + end + + context 'tracking again' do + it 'updates the value and timestamp' do + metric.track(value + 1) + expect { track }.to change { metric.values }.from([[now, value + 1]]).to([[now + 1, value]]) + end + end + end + + describe '#to_h' do + subject(:to_h) { metric.to_h } + let(:value) { 2 } + + before do + metric.track(value) + end + + it do + is_expected.to eq( + metric: name, + points: [[now, 2]], + type: 'gauge', + tags: ['tag1:val1', 'tag2:val2'], + common: true, + interval: interval + ) + end + end + end + + describe Datadog::Core::Telemetry::Metric::Rate do + subject(:metric) { described_class.new(name, tags: tags, interval: interval) } + + let(:name) { 'metric_name' } + let(:tags) { { tag1: 'val1', tag2: 'val2' } } + let(:interval) { 10 } + + it do + is_expected.to have_attributes( + name: name, + tags: ['tag1:val1', 'tag2:val2'], + interval: interval, + common: true, + values: [] + ) + end + + describe '#type' do + subject(:type) { metric.type } + + it { is_expected.to eq('rate') } + end + + describe '#track' do + subject(:track) { metric.track(value) } + + let(:value) { 5 } + + it 'tracks the rate value' do + expect { track }.to change { metric.values }.from([]).to([[now, value.to_f / interval]]) + end + + context 'tracking again' do + it 'updates the value and timestamp' do + metric.track(value) + expect { track }.to change { metric.values } + .from([[now, value.to_f / interval]]) + .to([[now + 1, (value + value).to_f / interval]]) + end + end + + context 'interval is nil' do + let(:interval) { nil } + + it 'sets rate to zero' do + expect { track }.to change { metric.values }.from([]).to([[now, 0.0]]) + end + end + end + + describe '#to_h' do + subject(:to_h) { metric.to_h } + let(:value) { 2 } + + before do + metric.track(value) + end + + it do + is_expected.to eq( + metric: name, + points: [[now, 0.2]], + type: 'rate', + tags: ['tag1:val1', 'tag2:val2'], + common: true, + interval: 10 + ) + end + end + end + + describe Datadog::Core::Telemetry::Metric::Distribution do + subject(:metric) { described_class.new(name, tags: tags) } + + let(:name) { 'metric_name' } + let(:tags) { { tag1: 'val1', tag2: 'val2' } } + + it do + is_expected.to have_attributes( + name: name, + tags: ['tag1:val1', 'tag2:val2'], + interval: nil, + common: true, + values: [] + ) + end + + describe '#type' do + subject(:type) { metric.type } + + it { is_expected.to eq('distributions') } + end + + describe '#track' do + subject(:track) { metric.track(value) } + + let(:value) { 5 } + + it 'tracks the value' do + expect { track }.to change { metric.values }.from([]).to([value]) + end + + context 'tracking again' do + it 'adds the value to the previous ones' do + metric.track(value) + expect { track }.to change { metric.values }.from([value]).to([value, value]) + end + end + end + + describe '#to_h' do + subject(:to_h) { metric.to_h } + let(:value) { 2 } + + before do + metric.track(value) + end + + it do + is_expected.to eq( + metric: name, + points: [2], + tags: ['tag1:val1', 'tag2:val2'], + common: true + ) + end + end + end +end