From e045cc6151100dedb6c3065d71e262e59169aeb7 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 25 Apr 2025 10:33:35 +0000 Subject: [PATCH 01/14] Introduce LogEvent --- sentry-ruby/lib/sentry/log_event.rb | 59 ++++++++++++ sentry-ruby/spec/sentry/log_event_spec.rb | 105 ++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 sentry-ruby/lib/sentry/log_event.rb create mode 100644 sentry-ruby/spec/sentry/log_event_spec.rb diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb new file mode 100644 index 000000000..d871f8f4e --- /dev/null +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Sentry + class LogEvent < Event + TYPE = "log" + + LEVELS = %i[trace debug info warn error fatal].freeze + + attr_accessor :level, :body, :attributes, :trace_id + + def initialize(configuration: Sentry.configuration, **options) + super(configuration: configuration) + @type = TYPE + @level = options.fetch(:level) + @body = options[:body] + @trace_id = options[:trace_id] || SecureRandom.hex(16) + @attributes = options[:attributes] || {} + end + + # https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload + def to_hash + data = {} + data[:level] = @level.to_s + data[:body] = @body + data[:timestamp] = Time.parse(@timestamp).to_f + data[:trace_id] = @trace_id + data[:attributes] = serialize_attributes + data + end + + private + + def serialize_attributes + result = {} + + @attributes.each do |key, value| + result[key] = { + value: value, + type: value_type(value) + } + end + + result + end + + def value_type(value) + case value + when Integer + "integer" + when TrueClass, FalseClass + "boolean" + when Float + "double" + else + "string" + end + end + end +end diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb new file mode 100644 index 000000000..447c00724 --- /dev/null +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Sentry::LogEvent do + let(:configuration) do + Sentry::Configuration.new.tap do |config| + config.dsn = Sentry::TestHelper::DUMMY_DSN + end + end + + describe "#initialize" do + it "initializes with required attributes" do + event = described_class.new( + configuration: configuration, + level: :info, + body: "User John has logged in!" + ) + + expect(event).to be_a(described_class) + expect(event.level).to eq(:info) + expect(event.body).to eq("User John has logged in!") + expect(event.trace_id).to_not be(nil) + end + + it "accepts trace_id" do + trace_id = "5b8efff798038103d269b633813fc60c" + event = described_class.new( + configuration: configuration, + level: :info, + body: "User John has logged in!", + trace_id: trace_id + ) + + expect(event.trace_id).to eq(trace_id) + end + + it "accepts attributes" do + attributes = { + "sentry.message.template" => "User %s has logged in!", + "sentry.message.parameters.0" => "John" + } + + event = described_class.new( + configuration: configuration, + level: :info, + body: "User John has logged in!", + attributes: attributes + ) + + expect(event.attributes).to eq(attributes) + end + end + + describe "#to_hash" do + it "includes all required fields" do + trace_id = "5b8efff798038103d269b633813fc60c" + attributes = { + "sentry.message.template" => "User %s has logged in!", + "sentry.message.parameters.0" => "John" + } + + event = described_class.new( + configuration: configuration, + level: :info, + body: "User John has logged in!", + trace_id: trace_id, + attributes: attributes + ) + + hash = event.to_hash + + expect(hash[:level]).to eq("info") + expect(hash[:body]).to eq("User John has logged in!") + expect(hash[:trace_id]).to eq(trace_id) + expect(hash[:timestamp]).to be_a(Float) + expect(hash[:attributes]).to be_a(Hash) + expect(hash[:attributes]["sentry.message.template"]).to eq({ value: "User %s has logged in!", type: "string" }) + expect(hash[:attributes]["sentry.message.parameters.0"]).to eq({ value: "John", type: "string" }) + end + + it "serializes different attribute types correctly" do + attributes = { + "string_attr" => "string value", + "integer_attr" => 42, + "boolean_attr" => true, + "float_attr" => 3.14 + } + + event = described_class.new( + configuration: configuration, + level: :info, + body: "Test message", + attributes: attributes + ) + + hash = event.to_hash + + expect(hash[:attributes]["string_attr"]).to eq({ value: "string value", type: "string" }) + expect(hash[:attributes]["integer_attr"]).to eq({ value: 42, type: "integer" }) + expect(hash[:attributes]["boolean_attr"]).to eq({ value: true, type: "boolean" }) + expect(hash[:attributes]["float_attr"]).to eq({ value: 3.14, type: "double" }) + end + end +end From 855048d7926bc762e122976812ea011c88a82711 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 25 Apr 2025 10:35:44 +0000 Subject: [PATCH 02/14] Support for LogEvent in Transport --- sentry-ruby/lib/sentry/transport.rb | 19 +++- sentry-ruby/spec/sentry/transport_spec.rb | 115 ++++++++++++++++++++++ 2 files changed, 130 insertions(+), 4 deletions(-) diff --git a/sentry-ruby/lib/sentry/transport.rb b/sentry-ruby/lib/sentry/transport.rb index d446387f2..40c313a12 100644 --- a/sentry-ruby/lib/sentry/transport.rb +++ b/sentry-ruby/lib/sentry/transport.rb @@ -133,10 +133,21 @@ def envelope_from_event(event) envelope = Envelope.new(envelope_headers) - envelope.add_item( - { type: item_type, content_type: "application/json" }, - event_payload - ) + if event.is_a?(LogEvent) + envelope.add_item( + { + type: "log", + item_count: 1, + content_type: "application/vnd.sentry.items.log+json" + }, + { items: [event_payload] } + ) + else + envelope.add_item( + { type: item_type, content_type: "application/json" }, + event_payload + ) + end if event.is_a?(TransactionEvent) && event.profile envelope.add_item( diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 372fbf636..45d86e50c 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -237,6 +237,76 @@ end end + context "log events" do + let(:log_events) do + 5.times.map do |i| + Sentry::LogEvent.new( + configuration: configuration, + level: :info, + body: "User John has logged in!", + trace_id: "5b8efff798038103d269b633813fc60c", + timestamp: 1544719860.0, + attributes: { + "sentry.message.template" => "User %s has logged in!", + "sentry.message.parameters.0" => "John", + "sentry.environment" => "production", + "sentry.release" => "1.0.0", + "sentry.trace.parent_span_id" => "b0e6f15b45c36b12" + } + ) + end + end + + let(:envelope) do + envelope = Sentry::Envelope.new + + envelope.add_item( + { + type: "log", + item_count: log_events.size, + content_type: "application/vnd.sentry.items.log+json" + }, + { items: log_events.map(&:to_hash) } + ) + + envelope + end + + it "generates correct envelope content for log events" do + result, _ = subject.serialize_envelope(envelope) + + envelope_header, item_header, item_payload = result.split("\n") + + envelope_header_parsed = JSON.parse(envelope_header) + expect(envelope_header_parsed).to be_a(Hash) + + item_header_parsed = JSON.parse(item_header) + expect(item_header_parsed).to eq({ + "type" => "log", + "item_count" => 5, + "content_type" => "application/vnd.sentry.items.log+json" + }) + + item_payload_parsed = JSON.parse(item_payload) + expect(item_payload_parsed).to have_key("items") + expect(item_payload_parsed["items"].size).to eq(5) + + log_event = item_payload_parsed["items"].first + expect(log_event["level"]).to eq("info") + expect(log_event["body"]).to eq("User John has logged in!") + expect(log_event["trace_id"]).to eq("5b8efff798038103d269b633813fc60c") + expect(log_event["timestamp"]).to be_a(Float) + + expect(log_event["attributes"]).to include( + "sentry.message.template" => { "value" => "User %s has logged in!", "type" => "string" }, + "sentry.message.parameters.0" => { "value" => "John", "type" => "string" }, + "sentry.environment" => { "value" => "production", "type" => "string" }, + "sentry.release" => { "value" => "1.0.0", "type" => "string" }, + "sentry.trace.parent_span_id" => { "value" => "b0e6f15b45c36b12", "type" => "string" } + ) + end + end + context "malformed breadcrumb" do let(:event) { client.event_from_message("foo") } @@ -502,6 +572,51 @@ expect(io.string).to match(/Sending envelope with items \[event, attachment, attachment\]/) end end + + context "log events" do + let(:log_events) do + 5.times.map do |i| + Sentry::LogEvent.new( + configuration: configuration, + level: :info, + body: "User John has logged in!", + trace_id: "5b8efff798038103d269b633813fc60c", + timestamp: 1544719860.0, + attributes: { + "sentry.message.template" => "User %s has logged in!", + "sentry.message.parameters.0" => "John", + "sentry.environment" => "production", + "sentry.release" => "1.0.0", + "sentry.trace.parent_span_id" => "b0e6f15b45c36b12" + } + ) + end + end + + let(:envelope) do + envelope = Sentry::Envelope.new + + # Add log item header + envelope.add_item( + { + type: "log", + item_count: log_events.size, + content_type: "application/vnd.sentry.items.log+json" + }, + { items: log_events.map(&:to_hash) } + ) + + envelope + end + + it "sends the log events and logs the action" do + expect(subject).to receive(:send_data) + + subject.send_envelope(envelope) + + expect(io.string).to match(/Sending envelope with items \[log\]/) + end + end end describe "#send_event" do From 3cbc33a4aed277cae73fdc20176bc81f9f4c9ebe Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 25 Apr 2025 10:37:31 +0000 Subject: [PATCH 03/14] Introduce `log` data category in events --- sentry-ruby/lib/sentry/envelope/item.rb | 2 +- sentry-ruby/spec/sentry/client_spec.rb | 11 +++++++++++ sentry-ruby/spec/sentry/envelope/item_spec.rb | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/sentry-ruby/lib/sentry/envelope/item.rb b/sentry-ruby/lib/sentry/envelope/item.rb index e1539bf6c..864ad0239 100644 --- a/sentry-ruby/lib/sentry/envelope/item.rb +++ b/sentry-ruby/lib/sentry/envelope/item.rb @@ -15,7 +15,7 @@ class Envelope::Item # rate limits and client reports use the data_category rather than envelope item type def self.data_category(type) case type - when "session", "attachment", "transaction", "profile", "span" then type + when "session", "attachment", "transaction", "profile", "span", "log" then type when "sessions" then "session" when "check_in" then "monitor" when "statsd", "metric_meta" then "metric_bucket" diff --git a/sentry-ruby/spec/sentry/client_spec.rb b/sentry-ruby/spec/sentry/client_spec.rb index e069ccc5b..51e493023 100644 --- a/sentry-ruby/spec/sentry/client_spec.rb +++ b/sentry-ruby/spec/sentry/client_spec.rb @@ -103,6 +103,7 @@ def sentry_context envelope.add_item({ type: 'event' }, { payload: 'test' }) envelope.add_item({ type: 'statsd' }, { payload: 'test2' }) envelope.add_item({ type: 'transaction' }, { payload: 'test3' }) + envelope.add_item({ type: 'log' }, { level: 'info', message: 'test4' }) envelope end @@ -119,6 +120,15 @@ def sentry_context subject.send_envelope(envelope) end + it 'includes log item in the envelope' do + log_item = envelope.items.find { |item| item.type == 'log' } + + expect(log_item).not_to be_nil + expect(log_item.payload[:level]).to eq('info') + expect(log_item.payload[:message]).to eq('test4') + expect(log_item.data_category).to eq('log') + end + it 'sends envelope with spotlight transport if enabled' do configuration.spotlight = true @@ -153,6 +163,7 @@ def sentry_context expect(subject.transport).to have_recorded_lost_event(:network_error, 'error') expect(subject.transport).to have_recorded_lost_event(:network_error, 'metric_bucket') expect(subject.transport).to have_recorded_lost_event(:network_error, 'transaction') + expect(subject.transport).to have_recorded_lost_event(:network_error, 'log') end end end diff --git a/sentry-ruby/spec/sentry/envelope/item_spec.rb b/sentry-ruby/spec/sentry/envelope/item_spec.rb index ea9cf1019..f21fb11b7 100644 --- a/sentry-ruby/spec/sentry/envelope/item_spec.rb +++ b/sentry-ruby/spec/sentry/envelope/item_spec.rb @@ -11,6 +11,7 @@ ['transaction', 'transaction'], ['span', 'span'], ['profile', 'profile'], + ['log', 'log'], ['check_in', 'monitor'], ['statsd', 'metric_bucket'], ['metric_meta', 'metric_bucket'], From cf1296bfe94e0ba57a4d4f23b81180c98223f2ce Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 25 Apr 2025 10:53:24 +0000 Subject: [PATCH 04/14] Introduce Sentry.capture_log --- sentry-ruby/lib/sentry-ruby.rb | 12 ++++++++++++ sentry-ruby/lib/sentry/client.rb | 15 +++++++++++++++ sentry-ruby/lib/sentry/hub.rb | 10 ++++++++++ sentry-ruby/lib/sentry/scope.rb | 2 +- sentry-ruby/spec/sentry_spec.rb | 13 +++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index fea912c0a..f2c81ae3a 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -489,6 +489,18 @@ def capture_check_in(slug, status, **options) get_current_hub.capture_check_in(slug, status, **options) end + # Captures a log event and sends it to Sentry via the currently active hub. + # + # @param message [String] the log message + # @param [Hash] options Extra log event options + # @option options [Symbol] level The log level + # + # @return [LogEvent, nil] + def capture_log(message, **options) + return unless initialized? + get_current_hub.capture_log_event(message, **options) + end + # Takes or initializes a new Sentry::Transaction and makes a sampling decision for it. # # @return [Transaction, nil] diff --git a/sentry-ruby/lib/sentry/client.rb b/sentry-ruby/lib/sentry/client.rb index 5435302e0..30ebc963c 100644 --- a/sentry-ruby/lib/sentry/client.rb +++ b/sentry-ruby/lib/sentry/client.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "sentry/transport" +require "sentry/log_event" module Sentry class Client @@ -167,6 +168,20 @@ def event_from_check_in( ) end + # Initializes a LogEvent object with the given message and options + def event_from_log(message, level:, **options) + return unless configuration.sending_allowed? + + attributes = options.reject { |k, _| k == :level } + + LogEvent.new( + level: level, + body: message, + timestamp: Time.now.to_f, + attributes: attributes + ) + end + # Initializes an Event object with the given Transaction object. # @param transaction [Transaction] the transaction to be recorded. # @return [TransactionEvent] diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb index 5e233a665..bc02de65d 100644 --- a/sentry-ruby/lib/sentry/hub.rb +++ b/sentry-ruby/lib/sentry/hub.rb @@ -216,6 +216,16 @@ def capture_check_in(slug, status, **options) event.check_in_id end + def capture_log_event(message, **options) + return unless current_client + + event = current_client.event_from_log(message, **options) + + return unless event + + capture_event(event, **options) + end + def capture_event(event, **options, &block) check_argument_type!(event, Sentry::Event) diff --git a/sentry-ruby/lib/sentry/scope.rb b/sentry-ruby/lib/sentry/scope.rb index 4feb6ecdd..68dfc079c 100644 --- a/sentry-ruby/lib/sentry/scope.rb +++ b/sentry-ruby/lib/sentry/scope.rb @@ -54,7 +54,7 @@ def apply_to_event(event, hint = nil) event.transaction = transaction_name if transaction_name event.transaction_info = { source: transaction_source } if transaction_source event.fingerprint = fingerprint - event.level = level + event.level = level unless event.is_a?(LogEvent) event.breadcrumbs = breadcrumbs event.rack_env = rack_env if rack_env event.attachments = attachments diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index b09afaef4..1dcfe7bc7 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -352,6 +352,19 @@ end end + describe ".capture_log" do + it "sends a log event via current hub" do + expect do + described_class.capture_log("Test", level: :info, tags: { foo: "baz" }) + end.to change { sentry_events.count }.by(1) + + log_event = sentry_events.first + + expect(log_event.type).to eql("log") + expect(log_event.level).to eq(:info) + expect(log_event.attributes).to eql({ tags: { foo: "baz" } }) + end + end describe ".start_transaction" do describe "sampler example" do From be910c41ac6a95a6fbbc01fba6146bc5a3964e25 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 25 Apr 2025 11:48:27 +0000 Subject: [PATCH 05/14] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b0bcb716..eb3a96a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ config.sidekiq.propagate_traces = false unless Rails.const_defined?('Server') ``` - Only expose `active_storage` keys on span data if `send_default_pii` is on ([#2589](https://github.com/getsentry/sentry-ruby/pull/2589)) +- Add `Sentry.capture_log` ([#2606](https://github.com/getsentry/sentry-ruby/pull/2606)) ### Bug Fixes From 4ac7e75f6db3b9f2ea106df90a4c330529d8afe7 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 09:55:20 +0000 Subject: [PATCH 06/14] Rework LogEvent serialization --- sentry-ruby/lib/sentry/log_event.rb | 36 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index d871f8f4e..2a6ab511d 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -1,9 +1,16 @@ # frozen_string_literal: true module Sentry + # Event type that represents a log entry with its attributes + # + # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload class LogEvent < Event TYPE = "log" + SERIALIZEABLE_ATTRIBUTES = %i[ + level body timestamp trace_id attributes + ] + LEVELS = %i[trace debug info warn error fatal].freeze attr_accessor :level, :body, :attributes, :trace_id @@ -17,19 +24,32 @@ def initialize(configuration: Sentry.configuration, **options) @attributes = options[:attributes] || {} end - # https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload def to_hash - data = {} - data[:level] = @level.to_s - data[:body] = @body - data[:timestamp] = Time.parse(@timestamp).to_f - data[:trace_id] = @trace_id - data[:attributes] = serialize_attributes - data + SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |name, memo| + memo[name] = serialize(name) + end end private + def serialize(name) + serializer = :"serialize_#{name}" + + if respond_to?(serializer, true) + __send__(serializer) + else + public_send(name) + end + end + + def serialize_level + level.to_s + end + + def serialize_timestamp + Time.parse(timestamp).to_f + end + def serialize_attributes result = {} From 43de0dc06cacad77834fa2c4a21ba8ead11486a8 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 09:57:10 +0000 Subject: [PATCH 07/14] Add more info to serialized log events --- sentry-ruby/lib/sentry/log_event.rb | 11 ++++++++++- sentry-ruby/spec/sentry/log_event_spec.rb | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 2a6ab511d..8dafb157d 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -8,7 +8,16 @@ class LogEvent < Event TYPE = "log" SERIALIZEABLE_ATTRIBUTES = %i[ - level body timestamp trace_id attributes + level + body + timestamp + trace_id + attributes + release + sdk + platform + environment + server_name ] LEVELS = %i[trace debug info warn error fatal].freeze diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index 447c00724..e3df08b7c 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -77,6 +77,12 @@ expect(hash[:attributes]).to be_a(Hash) expect(hash[:attributes]["sentry.message.template"]).to eq({ value: "User %s has logged in!", type: "string" }) expect(hash[:attributes]["sentry.message.parameters.0"]).to eq({ value: "John", type: "string" }) + + expect(hash[:sdk]).to eq(Sentry.sdk_meta) + expect(hash[:platform]).to eq(:ruby) + expect(hash[:environment]).to eq("development") + expect(hash).to have_key(:release) + expect(hash).to have_key(:server_name) end it "serializes different attribute types correctly" do From a601ba03bbab8fdbc5b28b8eccf960d4b307b8cf Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 10:08:44 +0000 Subject: [PATCH 08/14] Use each_with_object for consistency --- sentry-ruby/lib/sentry/log_event.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 8dafb157d..df8c1bc1e 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -60,16 +60,12 @@ def serialize_timestamp end def serialize_attributes - result = {} - - @attributes.each do |key, value| - result[key] = { + @attributes.each_with_object({}) do |(key, value), memo| + memo[key] = { value: value, type: value_type(value) } end - - result end def value_type(value) From dcf75e73db270563dc1640231dd3ed225a061bf8 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 10:36:05 +0000 Subject: [PATCH 09/14] Grab trace_id from contexts[:trace] --- sentry-ruby/lib/sentry/log_event.rb | 7 ++++++- sentry-ruby/spec/sentry/log_event_spec.rb | 16 ---------------- sentry-ruby/spec/sentry/transport_spec.rb | 1 - sentry-ruby/spec/sentry_spec.rb | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index df8c1bc1e..88ab0f143 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -18,6 +18,7 @@ class LogEvent < Event platform environment server_name + trace_id ] LEVELS = %i[trace debug info warn error fatal].freeze @@ -29,8 +30,8 @@ def initialize(configuration: Sentry.configuration, **options) @type = TYPE @level = options.fetch(:level) @body = options[:body] - @trace_id = options[:trace_id] || SecureRandom.hex(16) @attributes = options[:attributes] || {} + @contexts = {} end def to_hash @@ -59,6 +60,10 @@ def serialize_timestamp Time.parse(timestamp).to_f end + def serialize_trace_id + @contexts.dig(:trace, :trace_id) + end + def serialize_attributes @attributes.each_with_object({}) do |(key, value), memo| memo[key] = { diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index e3df08b7c..70bd47f95 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -20,19 +20,6 @@ expect(event).to be_a(described_class) expect(event.level).to eq(:info) expect(event.body).to eq("User John has logged in!") - expect(event.trace_id).to_not be(nil) - end - - it "accepts trace_id" do - trace_id = "5b8efff798038103d269b633813fc60c" - event = described_class.new( - configuration: configuration, - level: :info, - body: "User John has logged in!", - trace_id: trace_id - ) - - expect(event.trace_id).to eq(trace_id) end it "accepts attributes" do @@ -54,7 +41,6 @@ describe "#to_hash" do it "includes all required fields" do - trace_id = "5b8efff798038103d269b633813fc60c" attributes = { "sentry.message.template" => "User %s has logged in!", "sentry.message.parameters.0" => "John" @@ -64,7 +50,6 @@ configuration: configuration, level: :info, body: "User John has logged in!", - trace_id: trace_id, attributes: attributes ) @@ -72,7 +57,6 @@ expect(hash[:level]).to eq("info") expect(hash[:body]).to eq("User John has logged in!") - expect(hash[:trace_id]).to eq(trace_id) expect(hash[:timestamp]).to be_a(Float) expect(hash[:attributes]).to be_a(Hash) expect(hash[:attributes]["sentry.message.template"]).to eq({ value: "User %s has logged in!", type: "string" }) diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 45d86e50c..f7cd41d10 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -294,7 +294,6 @@ log_event = item_payload_parsed["items"].first expect(log_event["level"]).to eq("info") expect(log_event["body"]).to eq("User John has logged in!") - expect(log_event["trace_id"]).to eq("5b8efff798038103d269b633813fc60c") expect(log_event["timestamp"]).to be_a(Float) expect(log_event["attributes"]).to include( diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 1dcfe7bc7..566219e87 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -364,6 +364,25 @@ expect(log_event.level).to eq(:info) expect(log_event.attributes).to eql({ tags: { foo: "baz" } }) end + + it "sends a log event with trace and span info" do + expect do + Sentry.with_scope do |scope| + Sentry.with_child_span(op: "log_testing") do |_span| + described_class.capture_log("Test", level: :info, tags: { foo: "baz" }) + end + end + end.to change { sentry_events.count }.by(1) + + log_event = sentry_events.first + + expect(log_event.type).to eql("log") + expect(log_event.level).to eq(:info) + expect(log_event.attributes).to eql({ tags: { foo: "baz" } }) + + hash = log_event.to_hash + expect(hash[:trace_id]).to_not be(nil) + end end describe ".start_transaction" do From dd2d44b2445a2d2d2f6f423cd22cccea762cdd6c Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 11:50:59 +0000 Subject: [PATCH 10/14] Add parent_span_id --- sentry-ruby/lib/sentry/log_event.rb | 18 ++++++++++++++- sentry-ruby/spec/sentry_spec.rb | 34 ++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 88ab0f143..7ca972361 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -21,6 +21,10 @@ class LogEvent < Event trace_id ] + SENTRY_ATTRIBUTES = { + "sentry.trace.parent_span_id" => :parent_span_id + } + LEVELS = %i[trace debug info warn error fatal].freeze attr_accessor :level, :body, :attributes, :trace_id @@ -64,13 +68,25 @@ def serialize_trace_id @contexts.dig(:trace, :trace_id) end + def serialize_parent_span_id + @contexts.dig(:trace, :parent_span_id) + end + def serialize_attributes - @attributes.each_with_object({}) do |(key, value), memo| + hash = @attributes.each_with_object({}) do |(key, value), memo| memo[key] = { value: value, type: value_type(value) } end + + SENTRY_ATTRIBUTES.each do |key, name| + if (value = serialize(name)) + hash[key] = value + end + end + + hash end def value_type(value) diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 566219e87..4aa3a4e56 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -353,9 +353,17 @@ end describe ".capture_log" do - it "sends a log event via current hub" do + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + end + end + + it "sends a log event with trace_id" do expect do - described_class.capture_log("Test", level: :info, tags: { foo: "baz" }) + Sentry.with_scope do |scope| + described_class.capture_log("Test", level: :info, tags: { foo: "baz" }) + end end.to change { sentry_events.count }.by(1) log_event = sentry_events.first @@ -363,16 +371,21 @@ expect(log_event.type).to eql("log") expect(log_event.level).to eq(:info) expect(log_event.attributes).to eql({ tags: { foo: "baz" } }) + + hash = log_event.to_hash + expect(hash[:trace_id]).to_not be(nil) + expect(hash[:attributes]).to_not have_key("sentry.trace.parent_span_id") end - it "sends a log event with trace and span info" do - expect do - Sentry.with_scope do |scope| - Sentry.with_child_span(op: "log_testing") do |_span| - described_class.capture_log("Test", level: :info, tags: { foo: "baz" }) - end - end - end.to change { sentry_events.count }.by(1) + it "sends a log event with parent_span_id" do + transaction = Sentry.start_transaction(name: "test_transaction", op: "test.op") + span = transaction.start_child(op: "child span") + + Sentry.get_current_scope.set_span(span) + + described_class.capture_log("Test", level: :info, tags: { foo: "baz" }) + + transaction.finish log_event = sentry_events.first @@ -382,6 +395,7 @@ hash = log_event.to_hash expect(hash[:trace_id]).to_not be(nil) + expect(hash[:attributes]["sentry.trace.parent_span_id"]).to eql(transaction.span_id) end end From 97543a0eac2d420f1e667956add62728362440b0 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 11:57:09 +0000 Subject: [PATCH 11/14] Add parent_span_id and nest sentry info under attributes --- sentry-ruby/lib/sentry/log_event.rb | 32 ++++++++++++++--------- sentry-ruby/spec/sentry/log_event_spec.rb | 24 ++++++++++------- sentry-ruby/spec/sentry/transport_spec.rb | 5 ++-- sentry-ruby/spec/sentry_spec.rb | 4 +-- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 7ca972361..287235c18 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -13,16 +13,15 @@ class LogEvent < Event timestamp trace_id attributes - release - sdk - platform - environment - server_name - trace_id ] SENTRY_ATTRIBUTES = { - "sentry.trace.parent_span_id" => :parent_span_id + "sentry.trace.parent_span_id" => :parent_span_id, + "sentry.environment" => :environment, + "sentry.release" => :release, + "sentry.server_name" => :server_name, + "sentry.sdk.name" => :sdk_name, + "sentry.sdk.version" => :sdk_version } LEVELS = %i[trace debug info warn error fatal].freeze @@ -60,6 +59,14 @@ def serialize_level level.to_s end + def serialize_sdk_name + Sentry.sdk_meta[:name] + end + + def serialize_sdk_version + Sentry.sdk_meta[:version] + end + def serialize_timestamp Time.parse(timestamp).to_f end @@ -74,21 +81,22 @@ def serialize_parent_span_id def serialize_attributes hash = @attributes.each_with_object({}) do |(key, value), memo| - memo[key] = { - value: value, - type: value_type(value) - } + memo[key] = attribute_hash(value) end SENTRY_ATTRIBUTES.each do |key, name| if (value = serialize(name)) - hash[key] = value + hash[key] = attribute_hash(value) end end hash end + def attribute_hash(value) + { value: value, type: value_type(value) } + end + def value_type(value) case value when Integer diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index 70bd47f95..a61b6baf1 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -40,6 +40,12 @@ end describe "#to_hash" do + before do + configuration.release = "1.2.3" + configuration.environment = "test" + configuration.server_name = "server-123" + end + it "includes all required fields" do attributes = { "sentry.message.template" => "User %s has logged in!", @@ -58,15 +64,15 @@ expect(hash[:level]).to eq("info") expect(hash[:body]).to eq("User John has logged in!") expect(hash[:timestamp]).to be_a(Float) - expect(hash[:attributes]).to be_a(Hash) - expect(hash[:attributes]["sentry.message.template"]).to eq({ value: "User %s has logged in!", type: "string" }) - expect(hash[:attributes]["sentry.message.parameters.0"]).to eq({ value: "John", type: "string" }) - - expect(hash[:sdk]).to eq(Sentry.sdk_meta) - expect(hash[:platform]).to eq(:ruby) - expect(hash[:environment]).to eq("development") - expect(hash).to have_key(:release) - expect(hash).to have_key(:server_name) + + attributes = hash[:attributes] + + expect(attributes).to be_a(Hash) + expect(attributes["sentry.message.template"]).to eq({ value: "User %s has logged in!", type: "string" }) + expect(attributes["sentry.message.parameters.0"]).to eq({ value: "John", type: "string" }) + expect(attributes["sentry.environment"]).to eq({value: "test", type: "string"}) + expect(attributes["sentry.release"]).to eq({value: "1.2.3", type: "string"}) + expect(attributes["sentry.server_name"]).to eq({value: "server-123", type: "string"}) end it "serializes different attribute types correctly" do diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index f7cd41d10..09711cc61 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -299,9 +299,10 @@ expect(log_event["attributes"]).to include( "sentry.message.template" => { "value" => "User %s has logged in!", "type" => "string" }, "sentry.message.parameters.0" => { "value" => "John", "type" => "string" }, - "sentry.environment" => { "value" => "production", "type" => "string" }, + "sentry.environment" => { "value" => "development", "type" => "string" }, "sentry.release" => { "value" => "1.0.0", "type" => "string" }, - "sentry.trace.parent_span_id" => { "value" => "b0e6f15b45c36b12", "type" => "string" } + "sentry.trace.parent_span_id" => { "value" => "b0e6f15b45c36b12", "type" => "string" }, + "sentry.server_name" => { "value" => matching(/\w+/), "type" => "string" } ) end end diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 4aa3a4e56..bc9e6937c 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -394,8 +394,8 @@ expect(log_event.attributes).to eql({ tags: { foo: "baz" } }) hash = log_event.to_hash - expect(hash[:trace_id]).to_not be(nil) - expect(hash[:attributes]["sentry.trace.parent_span_id"]).to eql(transaction.span_id) + expect(hash[:trace_id]).to eq(transaction.trace_id) + expect(hash[:attributes]["sentry.trace.parent_span_id"]).to eql({ value: transaction.span_id, type: "string" }) end end From 167c5ef25c784dad15e659731a41a5be9a880e40 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 12:10:54 +0000 Subject: [PATCH 12/14] Set SDK info under attributes correctly --- sentry-ruby/lib/sentry/log_event.rb | 4 ++-- sentry-ruby/spec/sentry/log_event_spec.rb | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 287235c18..81807932e 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -60,11 +60,11 @@ def serialize_level end def serialize_sdk_name - Sentry.sdk_meta[:name] + Sentry.sdk_meta["name"] end def serialize_sdk_version - Sentry.sdk_meta[:version] + Sentry.sdk_meta["version"] end def serialize_timestamp diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index a61b6baf1..8c2b13c94 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -73,6 +73,8 @@ expect(attributes["sentry.environment"]).to eq({value: "test", type: "string"}) expect(attributes["sentry.release"]).to eq({value: "1.2.3", type: "string"}) expect(attributes["sentry.server_name"]).to eq({value: "server-123", type: "string"}) + expect(attributes["sentry.sdk.name"]).to eq({ value: "sentry.ruby", type: "string" }) + expect(attributes["sentry.sdk.version"]).to eq({ value: Sentry::VERSION, type: "string" }) end it "serializes different attribute types correctly" do From b05901488e19865390f385d88eede4b396cfced3 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 1 May 2025 12:14:20 +0000 Subject: [PATCH 13/14] rubocop --- sentry-ruby/spec/sentry/log_event_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index 8c2b13c94..db4efaf73 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -70,9 +70,9 @@ expect(attributes).to be_a(Hash) expect(attributes["sentry.message.template"]).to eq({ value: "User %s has logged in!", type: "string" }) expect(attributes["sentry.message.parameters.0"]).to eq({ value: "John", type: "string" }) - expect(attributes["sentry.environment"]).to eq({value: "test", type: "string"}) - expect(attributes["sentry.release"]).to eq({value: "1.2.3", type: "string"}) - expect(attributes["sentry.server_name"]).to eq({value: "server-123", type: "string"}) + expect(attributes["sentry.environment"]).to eq({ value: "test", type: "string" }) + expect(attributes["sentry.release"]).to eq({ value: "1.2.3", type: "string" }) + expect(attributes["sentry.server_name"]).to eq({ value: "server-123", type: "string" }) expect(attributes["sentry.sdk.name"]).to eq({ value: "sentry.ruby", type: "string" }) expect(attributes["sentry.sdk.version"]).to eq({ value: Sentry::VERSION, type: "string" }) end From 6d7777c6b3e7c3a624fe1cc11da5e1fcb3316e93 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 2 May 2025 08:55:10 +0000 Subject: [PATCH 14/14] server_name -> address --- sentry-ruby/lib/sentry/log_event.rb | 2 +- sentry-ruby/spec/sentry/log_event_spec.rb | 2 +- sentry-ruby/spec/sentry/transport_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 81807932e..52160a1a6 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -19,7 +19,7 @@ class LogEvent < Event "sentry.trace.parent_span_id" => :parent_span_id, "sentry.environment" => :environment, "sentry.release" => :release, - "sentry.server_name" => :server_name, + "sentry.address" => :server_name, "sentry.sdk.name" => :sdk_name, "sentry.sdk.version" => :sdk_version } diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index db4efaf73..fdc436abb 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -72,7 +72,7 @@ expect(attributes["sentry.message.parameters.0"]).to eq({ value: "John", type: "string" }) expect(attributes["sentry.environment"]).to eq({ value: "test", type: "string" }) expect(attributes["sentry.release"]).to eq({ value: "1.2.3", type: "string" }) - expect(attributes["sentry.server_name"]).to eq({ value: "server-123", type: "string" }) + expect(attributes["sentry.address"]).to eq({ value: "server-123", type: "string" }) expect(attributes["sentry.sdk.name"]).to eq({ value: "sentry.ruby", type: "string" }) expect(attributes["sentry.sdk.version"]).to eq({ value: Sentry::VERSION, type: "string" }) end diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 09711cc61..5ffc0e24c 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -302,7 +302,7 @@ "sentry.environment" => { "value" => "development", "type" => "string" }, "sentry.release" => { "value" => "1.0.0", "type" => "string" }, "sentry.trace.parent_span_id" => { "value" => "b0e6f15b45c36b12", "type" => "string" }, - "sentry.server_name" => { "value" => matching(/\w+/), "type" => "string" } + "sentry.address" => { "value" => matching(/\w+/), "type" => "string" } ) end end