diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb index b649500d7..81f7aee6f 100644 --- a/sentry-ruby/lib/sentry/hub.rb +++ b/sentry-ruby/lib/sentry/hub.rb @@ -240,8 +240,9 @@ def capture_log_event(message, **options) # @param value [Numeric] the metric value # @param unit [String, nil] (optional) the metric unit # @param attributes [Hash, nil] (optional) additional attributes for the metric + # @param integration [String, Symbol, nil] (optional) registered integration emitting the metric # @return [void] - def capture_metric(name:, type:, value:, unit: nil, attributes: nil) + def capture_metric(name:, type:, value:, unit: nil, attributes: nil, integration: nil) return unless current_client metric = MetricEvent.new( @@ -250,6 +251,7 @@ def capture_metric(name:, type:, value:, unit: nil, attributes: nil) type: type, unit: unit, attributes: attributes&.dup, + integration_meta: Sentry.integrations[integration.to_s] ) current_client.buffer_metric_event(metric, current_scope) diff --git a/sentry-ruby/lib/sentry/integrable.rb b/sentry-ruby/lib/sentry/integrable.rb index a45dd7839..4fdeeb3fe 100644 --- a/sentry-ruby/lib/sentry/integrable.rb +++ b/sentry-ruby/lib/sentry/integrable.rb @@ -32,5 +32,17 @@ def capture_check_in(slug, status, **options, &block) options[:hint][:integration] = integration_name Sentry.capture_check_in(slug, status, **options, &block) end + + def count(name, value: 1, attributes: nil) + Sentry.metrics.count(name, value: value, attributes: attributes, integration: integration_name) + end + + def gauge(name, value, unit: nil, attributes: nil) + Sentry.metrics.gauge(name, value, unit: unit, attributes: attributes, integration: integration_name) + end + + def distribution(name, value, unit: nil, attributes: nil) + Sentry.metrics.distribution(name, value, unit: unit, attributes: attributes, integration: integration_name) + end end end diff --git a/sentry-ruby/lib/sentry/metric_event.rb b/sentry-ruby/lib/sentry/metric_event.rb index 0116fee76..ec45df900 100644 --- a/sentry-ruby/lib/sentry/metric_event.rb +++ b/sentry-ruby/lib/sentry/metric_event.rb @@ -6,7 +6,7 @@ module Sentry class MetricEvent include Sentry::Utils::TelemetryAttributes - attr_reader :name, :type, :value, :unit, :timestamp, :trace_id, :span_id, :attributes + attr_reader :name, :type, :value, :unit, :timestamp, :trace_id, :span_id, :attributes, :integration_meta attr_writer :trace_id, :span_id, :attributes def initialize( @@ -14,13 +14,15 @@ def initialize( type:, value:, unit: nil, - attributes: nil + attributes: nil, + integration_meta: nil ) @name = name @type = type @value = value @unit = unit @attributes = attributes || {} + @integration_meta = integration_meta @timestamp = Sentry.utc_now @trace_id = nil diff --git a/sentry-ruby/lib/sentry/metrics.rb b/sentry-ruby/lib/sentry/metrics.rb index 5b7e53871..217975d80 100644 --- a/sentry-ruby/lib/sentry/metrics.rb +++ b/sentry-ruby/lib/sentry/metrics.rb @@ -9,15 +9,17 @@ class << self # @param name [String] the metric name # @param value [Numeric] the value to increment by (default: 1) # @param attributes [Hash, nil] additional attributes for the metric (optional) + # @param integration [String, Symbol, nil] registered integration emitting the metric (optional) # @return [void] - def count(name, value: 1, attributes: nil) + def count(name, value: 1, attributes: nil, integration: nil) return unless Sentry.initialized? Sentry.get_current_hub.capture_metric( name: name, type: :counter, value: value, - attributes: attributes + attributes: attributes, + integration: integration ) end @@ -26,8 +28,9 @@ def count(name, value: 1, attributes: nil) # @param value [Numeric] the gauge value # @param unit [String, nil] the metric unit (optional) # @param attributes [Hash, nil] additional attributes for the metric (optional) + # @param integration [String, Symbol, nil] registered integration emitting the metric (optional) # @return [void] - def gauge(name, value, unit: nil, attributes: nil) + def gauge(name, value, unit: nil, attributes: nil, integration: nil) return unless Sentry.initialized? Sentry.get_current_hub.capture_metric( @@ -35,7 +38,8 @@ def gauge(name, value, unit: nil, attributes: nil) type: :gauge, value: value, unit: unit, - attributes: attributes + attributes: attributes, + integration: integration ) end @@ -44,8 +48,9 @@ def gauge(name, value, unit: nil, attributes: nil) # @param value [Numeric] the distribution value # @param unit [String, nil] the metric unit (optional) # @param attributes [Hash, nil] additional attributes for the metric (optional) + # @param integration [String, Symbol, nil] registered integration emitting the metric (optional) # @return [void] - def distribution(name, value, unit: nil, attributes: nil) + def distribution(name, value, unit: nil, attributes: nil, integration: nil) return unless Sentry.initialized? Sentry.get_current_hub.capture_metric( @@ -53,7 +58,8 @@ def distribution(name, value, unit: nil, attributes: nil) type: :distribution, value: value, unit: unit, - attributes: attributes + attributes: attributes, + integration: integration ) end end diff --git a/sentry-ruby/lib/sentry/scope.rb b/sentry-ruby/lib/sentry/scope.rb index e12df2154..95a8ac0fc 100644 --- a/sentry-ruby/lib/sentry/scope.rb +++ b/sentry-ruby/lib/sentry/scope.rb @@ -102,8 +102,10 @@ def apply_to_telemetry(telemetry) configuration = Sentry.configuration return telemetry unless configuration - telemetry.attributes["sentry.sdk.name"] ||= Sentry.sdk_meta["name"] - telemetry.attributes["sentry.sdk.version"] ||= Sentry.sdk_meta["version"] + sdk_meta = telemetry.respond_to?(:integration_meta) ? telemetry.integration_meta : nil + sdk_meta ||= Sentry.sdk_meta + telemetry.attributes["sentry.sdk.name"] ||= sdk_meta[:name] || sdk_meta["name"] + telemetry.attributes["sentry.sdk.version"] ||= sdk_meta[:version] || sdk_meta["version"] telemetry.attributes["sentry.environment"] ||= configuration.environment if configuration.environment telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name diff --git a/sentry-ruby/lib/sentry/telemetry_event_buffer.rb b/sentry-ruby/lib/sentry/telemetry_event_buffer.rb index 29f348af8..714957943 100644 --- a/sentry-ruby/lib/sentry/telemetry_event_buffer.rb +++ b/sentry-ruby/lib/sentry/telemetry_event_buffer.rb @@ -89,12 +89,7 @@ def clear! private def send_items - envelope = Envelope.new( - event_id: Sentry::Utils.uuid, - sent_at: Sentry.utc_now.iso8601, - dsn: @dsn, - sdk: Sentry.sdk_meta - ) + envelope = Envelope.new(sent_at: Sentry.utc_now.iso8601) discarded_count = 0 discarded_bytes = 0 diff --git a/sentry-ruby/spec/sentry/integrable_spec.rb b/sentry-ruby/spec/sentry/integrable_spec.rb index 4a6677a87..d690a0fdb 100644 --- a/sentry-ruby/spec/sentry/integrable_spec.rb +++ b/sentry-ruby/spec/sentry/integrable_spec.rb @@ -101,5 +101,21 @@ module AnotherIntegration; end event = Sentry.capture_message(message) expect(event.sdk).to eq(Sentry.sdk_meta) end + + it "generates integration-attributed metric helpers" do + expect(Sentry.metrics).to receive(:count).with( + "test.count", value: 2, attributes: { source: "test" }, integration: "fake_integration" + ) + expect(Sentry.metrics).to receive(:gauge).with( + "test.gauge", 3, unit: "item", attributes: { source: "test" }, integration: "fake_integration" + ) + expect(Sentry.metrics).to receive(:distribution).with( + "test.distribution", 4, unit: "second", attributes: { source: "test" }, integration: "fake_integration" + ) + + Sentry::FakeIntegration.count("test.count", value: 2, attributes: { source: "test" }) + Sentry::FakeIntegration.gauge("test.gauge", 3, unit: "item", attributes: { source: "test" }) + Sentry::FakeIntegration.distribution("test.distribution", 4, unit: "second", attributes: { source: "test" }) + end end end diff --git a/sentry-ruby/spec/sentry/metrics_spec.rb b/sentry-ruby/spec/sentry/metrics_spec.rb index 72353f066..970568998 100644 --- a/sentry-ruby/spec/sentry/metrics_spec.rb +++ b/sentry-ruby/spec/sentry/metrics_spec.rb @@ -229,6 +229,42 @@ expect(metric_names).to contain_exactly("test.counter1", "test.counter2", "test.gauge") end + context "with integration attribution" do + let(:integration_meta) { { name: "sentry.ruby.test", version: "1.2.3" }.freeze } + + before do + Sentry.integrations["test"] = integration_meta + end + + after do + Sentry.integrations.delete("test") + end + + it "sets metric sdk attributes from the integration" do + Sentry.metrics.count("test.counter", integration: :test) + + Sentry.get_current_client.flush + + expect(sentry_metrics.first[:attributes]["sentry.sdk.name"]).to eq( + { type: "string", value: "sentry.ruby.test" } + ) + expect(sentry_metrics.first[:attributes]["sentry.sdk.version"]).to eq( + { type: "string", value: "1.2.3" } + ) + end + + it "batches metrics with different sdk attributes into one envelope" do + Sentry.metrics.count("test.manual") + Sentry.metrics.count("test.integration", integration: "test") + + Sentry.get_current_client.flush + + expect(sentry_envelopes.count).to eq(1) + sdk_names = sentry_metrics.map { |metric| metric[:attributes]["sentry.sdk.name"][:value] } + expect(sdk_names).to contain_exactly("sentry.ruby", "sentry.ruby.test") + end + end + describe "envelope structure" do it "includes correct envelope headers" do Sentry.metrics.count("test.counter") @@ -237,10 +273,8 @@ envelope = sentry_envelopes.first headers = envelope.headers - expect(headers[:event_id]).to match(/\A[0-9a-f]{32}\z/) # UUID format + expect(headers.keys).to contain_exactly(:sent_at) expect(headers[:sent_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) # ISO8601 timestamp - expect(headers[:dsn]).to eq(Sentry.configuration.dsn) - expect(headers[:sdk]).to eq(Sentry.sdk_meta) end it "includes correct envelope item headers" do diff --git a/sentry-yabeda/lib/sentry/yabeda/adapter.rb b/sentry-yabeda/lib/sentry/yabeda/adapter.rb index a93327408..d4a47a372 100644 --- a/sentry-yabeda/lib/sentry/yabeda/adapter.rb +++ b/sentry-yabeda/lib/sentry/yabeda/adapter.rb @@ -14,7 +14,7 @@ def register_summary!(_metric); end def perform_counter_increment!(counter, tags, increment) return unless enabled? - Sentry.metrics.count( + Sentry::Yabeda.count( metric_name(counter), value: increment, attributes: attributes_for(tags) @@ -24,7 +24,7 @@ def perform_counter_increment!(counter, tags, increment) def perform_gauge_set!(gauge, tags, value) return unless enabled? - Sentry.metrics.gauge( + Sentry::Yabeda.gauge( metric_name(gauge), value, unit: unit_for(gauge), @@ -35,7 +35,7 @@ def perform_gauge_set!(gauge, tags, value) def perform_histogram_measure!(histogram, tags, value) return unless enabled? - Sentry.metrics.distribution( + Sentry::Yabeda.distribution( metric_name(histogram), value, unit: unit_for(histogram), @@ -46,7 +46,7 @@ def perform_histogram_measure!(histogram, tags, value) def perform_summary_observe!(summary, tags, value) return unless enabled? - Sentry.metrics.distribution( + Sentry::Yabeda.distribution( metric_name(summary), value, unit: unit_for(summary), diff --git a/sentry-yabeda/spec/sentry/yabeda/adapter_spec.rb b/sentry-yabeda/spec/sentry/yabeda/adapter_spec.rb index 8c6fbcdb5..fa196e639 100644 --- a/sentry-yabeda/spec/sentry/yabeda/adapter_spec.rb +++ b/sentry-yabeda/spec/sentry/yabeda/adapter_spec.rb @@ -20,7 +20,9 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup counter = build_metric(:counter, name: :orders_created, group: :myapp) - expect(Sentry.metrics).to receive(:count).with("myapp.orders_created", value: 1, attributes: nil) + expect(Sentry::Yabeda).to receive(:count).with( + "myapp.orders_created", value: 1, attributes: nil + ) adapter.perform_counter_increment!(counter, {}, 1) end @@ -29,18 +31,20 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup counter = build_metric(:counter, name: :total_requests) - expect(Sentry.metrics).to receive(:count).with("total_requests", value: 1, attributes: nil) + expect(Sentry::Yabeda).to receive(:count).with( + "total_requests", value: 1, attributes: nil + ) adapter.perform_counter_increment!(counter, {}, 1) end end describe "#perform_counter_increment!" do - it "calls Sentry.metrics.count with correct arguments" do + it "calls Sentry::Yabeda.count with correct arguments" do perform_basic_setup counter = build_metric(:counter, name: :requests, group: :rails) - expect(Sentry.metrics).to receive(:count).with( + expect(Sentry::Yabeda).to receive(:count).with( "rails.requests", value: 5, attributes: tags @@ -53,7 +57,7 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup counter = build_metric(:counter, name: :requests, group: :rails) - expect(Sentry.metrics).to receive(:count).with( + expect(Sentry::Yabeda).to receive(:count).with( "rails.requests", value: 1, attributes: nil @@ -64,11 +68,11 @@ def build_metric(type, name:, group: nil, unit: nil) end describe "#perform_gauge_set!" do - it "calls Sentry.metrics.gauge with correct arguments" do + it "calls Sentry::Yabeda.gauge with correct arguments" do perform_basic_setup gauge = build_metric(:gauge, name: :queue_depth, group: :sidekiq) - expect(Sentry.metrics).to receive(:gauge).with( + expect(Sentry::Yabeda).to receive(:gauge).with( "sidekiq.queue_depth", 42, unit: nil, @@ -82,7 +86,7 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup gauge = build_metric(:gauge, name: :memory_usage, group: :process, unit: :bytes) - expect(Sentry.metrics).to receive(:gauge).with( + expect(Sentry::Yabeda).to receive(:gauge).with( "process.memory_usage", 1024, unit: "byte", @@ -94,11 +98,11 @@ def build_metric(type, name:, group: nil, unit: nil) end describe "#perform_histogram_measure!" do - it "calls Sentry.metrics.distribution with correct arguments" do + it "calls Sentry::Yabeda.distribution with correct arguments" do perform_basic_setup histogram = build_metric(:histogram, name: :request_duration, group: :rails, unit: :milliseconds) - expect(Sentry.metrics).to receive(:distribution).with( + expect(Sentry::Yabeda).to receive(:distribution).with( "rails.request_duration", 150.5, unit: "millisecond", @@ -110,11 +114,11 @@ def build_metric(type, name:, group: nil, unit: nil) end describe "#perform_summary_observe!" do - it "calls Sentry.metrics.distribution with correct arguments" do + it "calls Sentry::Yabeda.distribution with correct arguments" do perform_basic_setup summary = build_metric(:summary, name: :response_size, group: :http, unit: :bytes) - expect(Sentry.metrics).to receive(:distribution).with( + expect(Sentry::Yabeda).to receive(:distribution).with( "http.response_size", 2048, unit: "byte", @@ -130,7 +134,7 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup histogram = build_metric(:histogram, name: :duration, group: :rails, unit: :seconds) - expect(Sentry.metrics).to receive(:distribution).with( + expect(Sentry::Yabeda).to receive(:distribution).with( "rails.duration", 1.5, unit: "second", attributes: nil ) @@ -141,7 +145,7 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup histogram = build_metric(:histogram, name: :latency, unit: :milliseconds) - expect(Sentry.metrics).to receive(:distribution).with( + expect(Sentry::Yabeda).to receive(:distribution).with( "latency", 250.0, unit: "millisecond", attributes: nil ) @@ -152,7 +156,7 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup gauge = build_metric(:gauge, name: :threads) - expect(Sentry.metrics).to receive(:gauge).with( + expect(Sentry::Yabeda).to receive(:gauge).with( "threads", 5, unit: nil, attributes: nil ) @@ -163,7 +167,7 @@ def build_metric(type, name:, group: nil, unit: nil) perform_basic_setup gauge = build_metric(:gauge, name: :uptime, unit: :second) - expect(Sentry.metrics).to receive(:gauge).with( + expect(Sentry::Yabeda).to receive(:gauge).with( "uptime", 3600, unit: "second", attributes: nil ) @@ -191,10 +195,28 @@ def build_metric(type, name:, group: nil, unit: nil) describe "guard conditions" do it "does not emit metrics when Sentry is not initialized" do - expect(Sentry.metrics).not_to receive(:count) + expect(Sentry::Yabeda).not_to receive(:count) + + counter = build_metric(:counter, name: :requests, group: :rails) + adapter.perform_counter_increment!(counter, {}, 1) + end + end + + describe "integration attribution" do + it "emits metrics with the Yabeda sdk identity" do + perform_basic_setup counter = build_metric(:counter, name: :requests, group: :rails) adapter.perform_counter_increment!(counter, {}, 1) + Sentry.get_current_client.flush + + envelope = sentry_envelopes.first + metric = envelope.items.first.payload[:items].first + + expect(envelope.headers.keys).to contain_exactly(:sent_at) + expect(metric[:attributes]["sentry.sdk.name"]).to eq( + { type: "string", value: "sentry.ruby.yabeda" } + ) end end @@ -205,7 +227,7 @@ def build_metric(type, name:, group: nil, unit: nil) complex_tags = { controller: "orders", action: "create", region: "eu-west", status: 200 } counter = build_metric(:counter, name: :requests, group: :rails) - expect(Sentry.metrics).to receive(:count).with( + expect(Sentry::Yabeda).to receive(:count).with( "rails.requests", value: 1, attributes: complex_tags