From 8b682b628b4a00d0d07b8eccb47bc01b0a1efba5 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 24 Jun 2024 13:34:15 +0200 Subject: [PATCH 1/6] add generate-metrics event --- lib/datadog/core/telemetry/event.rb | 20 ++++++++++++++++++++ sig/datadog/core/telemetry/event.rbs | 7 +++++++ spec/datadog/core/telemetry/event_spec.rb | 18 ++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/lib/datadog/core/telemetry/event.rb b/lib/datadog/core/telemetry/event.rb index 49b292878bb..e04aaf0cc08 100644 --- a/lib/datadog/core/telemetry/event.rb +++ b/lib/datadog/core/telemetry/event.rb @@ -286,6 +286,26 @@ 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 end end end diff --git a/sig/datadog/core/telemetry/event.rbs b/sig/datadog/core/telemetry/event.rbs index 4e0e3824109..ac432ef0488 100644 --- a/sig/datadog/core/telemetry/event.rbs +++ b/sig/datadog/core/telemetry/event.rbs @@ -55,6 +55,13 @@ module Datadog class AppClosing < Base end + + class GenerateMetrics < Base + @namespace: String + @metric_series: Enumerable[Hash[Symbol, untyped]] + + def initialize: (String namespace, Enumerable[Hash[Symbol, untyped]] metric_series) -> void + end end end end diff --git a/spec/datadog/core/telemetry/event_spec.rb b/spec/datadog/core/telemetry/event_spec.rb index 32d83e54fc4..798d1e91e73 100644 --- a/spec/datadog/core/telemetry/event_spec.rb +++ b/spec/datadog/core/telemetry/event_spec.rb @@ -207,4 +207,22 @@ def contain_configuration(*array) is_expected.to eq({}) end end + + context 'GenerateMetrics' do + let(:event) { described_class::GenerateMetrics.new(namespace, metric_series) } + + let(:namespace) { 'general' } + let(:metric_name) { 'request_count' } + let(:points) { [[123123123, 33]] } + let(:metric_series) { [{ metric: metric_name, points: points }] } + + it do + is_expected.to eq( + { + namespace: namespace, + series: metric_series + } + ) + end + end end From f103ed5b4eb14288f6246f3dbf897c1116455130 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 24 Jun 2024 14:54:31 +0200 Subject: [PATCH 2/6] add distributions event --- lib/datadog/core/telemetry/event.rb | 7 +++++++ sig/datadog/core/telemetry/event.rbs | 3 +++ spec/datadog/core/telemetry/event_spec.rb | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/lib/datadog/core/telemetry/event.rb b/lib/datadog/core/telemetry/event.rb index e04aaf0cc08..60a9886eb1e 100644 --- a/lib/datadog/core/telemetry/event.rb +++ b/lib/datadog/core/telemetry/event.rb @@ -306,6 +306,13 @@ def payload(_) } end end + + # Telemetry class for the 'distributions' event + class Distributions < GenerateMetrics + def type + 'distributions' + end + end end end end diff --git a/sig/datadog/core/telemetry/event.rbs b/sig/datadog/core/telemetry/event.rbs index ac432ef0488..61b2e34588d 100644 --- a/sig/datadog/core/telemetry/event.rbs +++ b/sig/datadog/core/telemetry/event.rbs @@ -62,6 +62,9 @@ module Datadog def initialize: (String namespace, Enumerable[Hash[Symbol, untyped]] metric_series) -> void end + + class Distributions < GenerateMetrics + end end end end diff --git a/spec/datadog/core/telemetry/event_spec.rb b/spec/datadog/core/telemetry/event_spec.rb index 798d1e91e73..50ae1265539 100644 --- a/spec/datadog/core/telemetry/event_spec.rb +++ b/spec/datadog/core/telemetry/event_spec.rb @@ -225,4 +225,22 @@ def contain_configuration(*array) ) end end + + context 'Distributions' do + let(:event) { described_class::Distributions.new(namespace, metric_series) } + + let(:namespace) { 'general' } + let(:metric_name) { 'request_duration' } + let(:points) { [13, 14, 15, 16] } + let(:metric_series) { [{ metric: metric_name, points: points }] } + + it do + is_expected.to eq( + { + namespace: namespace, + series: metric_series + } + ) + end + end end From b9ea6f14e32a502e4a3aaa9b0c6de7cd77dbd0fa Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 25 Jun 2024 12:00:21 +0200 Subject: [PATCH 3/6] first pass on metrics data model --- lib/datadog/core/telemetry/metric.rb | 145 ++++++++++++++++++++++++++ sig/datadog/core/telemetry/metric.rbs | 102 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 lib/datadog/core/telemetry/metric.rb create mode 100644 sig/datadog/core/telemetry/metric.rbs diff --git a/lib/datadog/core/telemetry/metric.rb b/lib/datadog/core/telemetry/metric.rb new file mode 100644 index 00000000000..6ad519d9156 --- /dev/null +++ b/lib/datadog/core/telemetry/metric.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +module Datadog + module Core + module Telemetry + # Telemetry metrics data model + 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 + + 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, # move to method + 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 + end + + def type + TYPE + end + + def track(value = 1.0) + @value += value + + rate = interval ? @value.to_f / 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, # move to method + common: common + } + end + end + 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 From 703a1dee10f29dd093a928298a11ff8ec10f40d4 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 25 Jun 2024 12:06:23 +0200 Subject: [PATCH 4/6] remove stale comments --- lib/datadog/core/telemetry/metric.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/datadog/core/telemetry/metric.rb b/lib/datadog/core/telemetry/metric.rb index 6ad519d9156..025a2b7b43d 100644 --- a/lib/datadog/core/telemetry/metric.rb +++ b/lib/datadog/core/telemetry/metric.rb @@ -31,7 +31,7 @@ def to_h metric: name, points: values, type: type, - tags: tags, # move to method + tags: tags, common: common } res[:interval] = interval if interval @@ -134,7 +134,7 @@ def to_h { metric: name, points: values, - tags: tags, # move to method + tags: tags, common: common } end From a4d67ec8b72021339cc2cef22b42ca8576ab8c40 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 25 Jun 2024 13:18:57 +0200 Subject: [PATCH 5/6] specs for metric data model --- lib/datadog/core/telemetry/metric.rb | 8 +- spec/datadog/core/telemetry/metric_spec.rb | 277 +++++++++++++++++++++ 2 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 spec/datadog/core/telemetry/metric_spec.rb diff --git a/lib/datadog/core/telemetry/metric.rb b/lib/datadog/core/telemetry/metric.rb index 025a2b7b43d..ebe8ecbe533 100644 --- a/lib/datadog/core/telemetry/metric.rb +++ b/lib/datadog/core/telemetry/metric.rb @@ -3,10 +3,10 @@ module Datadog module Core module Telemetry - # Telemetry metrics data model + # Telemetry metrics data model (internal Datadog metrics for client libraries) module Metric def self.metric_id(type, name, tags = []) - "#{type}:#{name}:#{tags.join(',')}" + "#{type}::#{name}::#{tags.join(',')}" end # Base class for all metric types @@ -102,7 +102,7 @@ class Rate < Base def initialize(name, tags: {}, common: true, interval: nil) super - @value = 0 + @value = 0.0 end def type @@ -112,7 +112,7 @@ def type def track(value = 1.0) @value += value - rate = interval ? @value.to_f / interval : 0.0 + rate = interval ? @value / interval : 0.0 @values = [[Time.now.to_i, rate]] 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 From a7974f2dde89713cb7aa35b9fa21b820b351b9fe Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 25 Jun 2024 13:28:47 +0200 Subject: [PATCH 6/6] generate-metrics and distributions telemetry events expect collection of metrics as input --- lib/datadog/core/telemetry/metric.rb | 4 ++++ sig/datadog/core/telemetry/event.rbs | 4 ++-- spec/datadog/core/telemetry/event_spec.rb | 25 +++++++++++++++-------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/datadog/core/telemetry/metric.rb b/lib/datadog/core/telemetry/metric.rb index ebe8ecbe533..2c6ba8d97de 100644 --- a/lib/datadog/core/telemetry/metric.rb +++ b/lib/datadog/core/telemetry/metric.rb @@ -13,6 +13,10 @@ def self.metric_id(type, name, tags = []) 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 = [] diff --git a/sig/datadog/core/telemetry/event.rbs b/sig/datadog/core/telemetry/event.rbs index 61b2e34588d..791f014f9a4 100644 --- a/sig/datadog/core/telemetry/event.rbs +++ b/sig/datadog/core/telemetry/event.rbs @@ -58,9 +58,9 @@ module Datadog class GenerateMetrics < Base @namespace: String - @metric_series: Enumerable[Hash[Symbol, untyped]] + @metric_series: Enumerable[Datadog::Core::Telemetry::Metric::Base] - def initialize: (String namespace, Enumerable[Hash[Symbol, untyped]] metric_series) -> void + def initialize: (String namespace, Enumerable[Datadog::Core::Telemetry::Metric::Base] metric_series) -> void end class Distributions < GenerateMetrics diff --git a/spec/datadog/core/telemetry/event_spec.rb b/spec/datadog/core/telemetry/event_spec.rb index 50ae1265539..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') } @@ -209,36 +210,44 @@ def contain_configuration(*array) end context 'GenerateMetrics' do - let(:event) { described_class::GenerateMetrics.new(namespace, metric_series) } + let(:event) { described_class::GenerateMetrics.new(namespace, metrics) } let(:namespace) { 'general' } let(:metric_name) { 'request_count' } - let(:points) { [[123123123, 33]] } - let(:metric_series) { [{ metric: metric_name, points: points }] } + 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: metric_series + series: expected_metric_series } ) end end context 'Distributions' do - let(:event) { described_class::Distributions.new(namespace, metric_series) } + let(:event) { described_class::Distributions.new(namespace, metrics) } let(:namespace) { 'general' } let(:metric_name) { 'request_duration' } - let(:points) { [13, 14, 15, 16] } - let(:metric_series) { [{ metric: metric_name, points: points }] } + 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: metric_series + series: expected_metric_series } ) end