diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index 7c846fb11..5e02e51ae 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -12,6 +12,7 @@ require "sentry/utils/logging_helper" require "sentry/utils/callback_helper" require "sentry/utils/sample_rand" +require "sentry/utils/latency" require "sentry/configuration" require "sentry/structured_logger" require "sentry/debug_structured_logger" diff --git a/sentry-ruby/lib/sentry/job_extensions/extension.rb b/sentry-ruby/lib/sentry/job_extensions/extension.rb index 0b8e5ebf4..9fc7a3cae 100644 --- a/sentry-ruby/lib/sentry/job_extensions/extension.rb +++ b/sentry-ruby/lib/sentry/job_extensions/extension.rb @@ -132,8 +132,9 @@ def extract_trace_headers(job:, payload:) # @return [Hash] a Hash with any of the keys +:system+ (a String naming # the backend), +:message_id+ (a String), +:destination+ (a String, # such as the queue name), +:receive_latency+ (a Numeric, the - # milliseconds between enqueueing and the start of the job). Empty by - # default. + # milliseconds between enqueueing and the start of the job) and + # +:body_size+ (a non-negative Integer, the size of the message body + # in bytes). Empty by default. def messaging_data(job:) {} end diff --git a/sentry-ruby/lib/sentry/job_extensions/instrumentation.rb b/sentry-ruby/lib/sentry/job_extensions/instrumentation.rb index 71a23eee8..95f724658 100644 --- a/sentry-ruby/lib/sentry/job_extensions/instrumentation.rb +++ b/sentry-ruby/lib/sentry/job_extensions/instrumentation.rb @@ -10,7 +10,8 @@ class Instrumentation system: Span::DataConventions::MESSAGING_SYSTEM, message_id: Span::DataConventions::MESSAGING_MESSAGE_ID, destination: Span::DataConventions::MESSAGING_DESTINATION_NAME, - receive_latency: Span::DataConventions::MESSAGING_MESSAGE_RECEIVE_LATENCY + receive_latency: Span::DataConventions::MESSAGING_MESSAGE_RECEIVE_LATENCY, + body_size: Span::DataConventions::MESSAGING_MESSAGE_BODY_SIZE }.freeze attr_reader :extension diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index b53554dc4..407288366 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -50,6 +50,7 @@ module DataConventions MESSAGING_DESTINATION_NAME = "messaging.destination.name" MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" + MESSAGING_MESSAGE_BODY_SIZE = "messaging.message.body.size" # Time in ms the request spent in the server queue before processing began. HTTP_QUEUE_TIME_MS = "http.server.request.time_in_queue" diff --git a/sentry-ruby/lib/sentry/utils/latency.rb b/sentry-ruby/lib/sentry/utils/latency.rb new file mode 100644 index 000000000..702735a2f --- /dev/null +++ b/sentry-ruby/lib/sentry/utils/latency.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "time" + +module Sentry + module Utils + module Latency + module_function + + def milliseconds_since(time) + return unless time + + time = Time.parse(time) if time.is_a?(String) + (Time.now.to_f - time.to_f) * 1000.0 + end + end + end +end diff --git a/sentry-ruby/spec/sentry/job_extensions/consumer_spec.rb b/sentry-ruby/spec/sentry/job_extensions/consumer_spec.rb index e4ac37e6b..90b86380c 100644 --- a/sentry-ruby/spec/sentry/job_extensions/consumer_spec.rb +++ b/sentry-ruby/spec/sentry/job_extensions/consumer_spec.rb @@ -45,7 +45,7 @@ def capture(consumer, exception = RuntimeError.new("boom"), **options) def enriching_consumer consumer( - messaging_data: ->(job:) { { system: "backend", message_id: job.id, destination: job.queue, receive_latency: 42.5 } }, + messaging_data: ->(job:) { { system: "backend", message_id: job.id, destination: job.queue, receive_latency: 42.5, body_size: 128 } }, retry_info: ->(job:) { { count: 3, will_retry: true } }, scope_data: ->(job:) { { contexts: { backend: { "jid" => "backend-jid" } }, tags: { "backend.queue" => "low" }, extra: { size: 12 } } } ) @@ -85,6 +85,7 @@ def enriching_consumer "messaging.message.id" => "job-1", "messaging.destination.name" => "critical", "messaging.message.receive.latency" => 42.5, + "messaging.message.body.size" => 128, "messaging.message.retry.count" => 3 ) end diff --git a/sentry-ruby/spec/sentry/job_extensions/producer_spec.rb b/sentry-ruby/spec/sentry/job_extensions/producer_spec.rb index cf9f238f2..1f1a28d19 100644 --- a/sentry-ruby/spec/sentry/job_extensions/producer_spec.rb +++ b/sentry-ruby/spec/sentry/job_extensions/producer_spec.rb @@ -15,7 +15,7 @@ def producer(**hooks) def enriching_producer producer( - messaging_data: ->(job:) { { system: "backend", message_id: job.id, destination: job.queue, receive_latency: 42.5 } }, + messaging_data: ->(job:) { { system: "backend", message_id: job.id, destination: job.queue, receive_latency: 42.5, body_size: 128 } }, retry_info: ->(job:) { { count: 3, will_retry: true } } ) end @@ -59,6 +59,7 @@ def record_span(producer, &enqueue) "messaging.message.id" => "job-1", "messaging.destination.name" => "critical", "messaging.message.receive.latency" => 42.5, + "messaging.message.body.size" => 128, "messaging.message.retry.count" => 3 ) end diff --git a/sentry-ruby/spec/sentry/utils/latency_spec.rb b/sentry-ruby/spec/sentry/utils/latency_spec.rb new file mode 100644 index 000000000..9cd24aad8 --- /dev/null +++ b/sentry-ruby/spec/sentry/utils/latency_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.describe Sentry::Utils::Latency do + let(:started_at) { Time.utc(2026, 1, 1, 12, 0, 0) } + + around do |example| + Timecop.freeze(started_at + 2.5) { example.run } + end + + it "measures the milliseconds since a Time" do + expect(described_class.milliseconds_since(started_at)).to eq(2_500.0) + end + + it "measures the milliseconds since an ISO 8601 String" do + expect(described_class.milliseconds_since(started_at.iso8601(3))).to eq(2_500.0) + end + + it "measures nothing without a time" do + expect(described_class.milliseconds_since(nil)).to be_nil + end +end