diff --git a/CHANGELOG.md b/CHANGELOG.md index b71c760af..13b9b626e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,18 @@ ## Unreleased +### Features + +- Support for `:origin` attribute in log events ([#2712](https://github.com/getsentry/sentry-ruby/pull/2712)) + ### Bug Fixes - Skip including `sentry.message.template` in the log event attributes if there are no interpolation parameters provided ([#2700](https://github.com/getsentry/sentry-ruby/pull/2700)) - Respect `log_level` when logging via `:std_lib_logger` patch ([#2709](https://github.com/getsentry/sentry-ruby/pull/2709)) +- Add `sentry.origin` attribute to log events ([#2712](https://github.com/getsentry/sentry-ruby/pull/2712)) ## 5.27.0 -### Feature +### Features - Propagated sampling rates as specified in [Traces](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value) docs ([#2671](https://github.com/getsentry/sentry-ruby/pull/2671)) - Support for Rails ActiveSupport log subscribers ([#2690](https://github.com/getsentry/sentry-ruby/pull/2690)) diff --git a/sentry-rails/lib/sentry/rails/log_subscriber.rb b/sentry-rails/lib/sentry/rails/log_subscriber.rb index 5de6cdf6e..678c0b1ad 100644 --- a/sentry-rails/lib/sentry/rails/log_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/log_subscriber.rb @@ -27,6 +27,8 @@ module Rails # end # end class LogSubscriber < ActiveSupport::LogSubscriber + ORIGIN = "auto.logger.rails.log_subscriber" + class << self if ::Rails.version.to_f < 6.0 # Rails 5.x does not provide detach_from @@ -51,8 +53,9 @@ def detach_from(namespace, notifications = ActiveSupport::Notifications) # @param message [String] The log message # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal) # @param attributes [Hash] Additional structured attributes to include - def log_structured_event(message:, level: :info, attributes: {}) - Sentry.logger.public_send(level, message, **attributes) + # @param origin [String] The origin of the log event + def log_structured_event(message:, level: :info, attributes: {}, origin: ORIGIN) + Sentry.logger.public_send(level, message, **attributes, origin: origin) rescue => e # Silently handle any errors in logging to avoid breaking the application Sentry.configuration.sdk_logger.debug("Failed to log structured event: #{e.message}") diff --git a/sentry-rails/spec/sentry/rails/log_subscribers/action_controller_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/log_subscribers/action_controller_subscriber_spec.rb index 98dd588cb..69e44992a 100644 --- a/sentry-rails/spec/sentry/rails/log_subscribers/action_controller_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/log_subscribers/action_controller_subscriber_spec.rb @@ -35,6 +35,7 @@ expect(log_event[:attributes][:method][:value]).to eq("GET") expect(log_event[:attributes][:path][:value]).to eq("/world") expect(log_event[:attributes][:format][:value]).to eq(:html) + expect(log_event[:attributes]["sentry.origin"][:value]).to eq("auto.logger.rails.log_subscriber") end it "logs bad requests appropriately" do diff --git a/sentry-rails/spec/sentry/rails/log_subscribers/action_mailer_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/log_subscribers/action_mailer_subscriber_spec.rb index df55048c4..cea1158cc 100644 --- a/sentry-rails/spec/sentry/rails/log_subscribers/action_mailer_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/log_subscribers/action_mailer_subscriber_spec.rb @@ -38,6 +38,7 @@ expect(log_event[:attributes][:duration_ms][:value]).to be > 0 expect(log_event[:attributes][:perform_deliveries][:value]).to be true expect(log_event[:attributes][:delivery_method][:value]).to eq(:test) + expect(log_event[:attributes]["sentry.origin"][:value]).to eq("auto.logger.rails.log_subscriber") expect(log_event[:attributes][:date]).to be_present end diff --git a/sentry-rails/spec/sentry/rails/log_subscribers/active_job_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/log_subscribers/active_job_subscriber_spec.rb index 49a4573c5..3d24aa69f 100644 --- a/sentry-rails/spec/sentry/rails/log_subscribers/active_job_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/log_subscribers/active_job_subscriber_spec.rb @@ -26,6 +26,7 @@ expect(log_event[:level]).to eq("info") expect(log_event[:attributes][:job_class][:value]).to eq("NormalJob") expect(log_event[:attributes][:duration_ms][:value]).to be > 0 + expect(log_event[:attributes]["sentry.origin"][:value]).to eq("auto.logger.rails.log_subscriber") end it "logs job enqueue events when jobs are enqueued" do diff --git a/sentry-rails/spec/sentry/rails/log_subscribers/active_record_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/log_subscribers/active_record_subscriber_spec.rb index 316a4c74d..b8e575083 100644 --- a/sentry-rails/spec/sentry/rails/log_subscribers/active_record_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/log_subscribers/active_record_subscriber_spec.rb @@ -25,6 +25,7 @@ expect(log_event[:level]).to eq("info") expect(log_event[:attributes][:sql][:value]).to include("INSERT INTO") expect(log_event[:attributes][:duration_ms][:value]).to be > 0 + expect(log_event[:attributes]["sentry.origin"][:value]).to eq("auto.logger.rails.log_subscriber") end it "logs SELECT queries with proper attributes" do diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index 48b44e9f2..c9283641c 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -500,6 +500,7 @@ def capture_check_in(slug, status, **options) # @param [Hash] options Extra log event options # @option options [Symbol] level The log level (:trace, :debug, :info, :warn, :error, :fatal) # @option options [Integer] severity The severity number according to the Sentry Logs Protocol + # @option options [String] origin The origin of the log event (e.g., "auto.db.rails", "manual") # @option options [Hash] Additional attributes to include with the log # # @example Direct usage (prefer using Sentry.logger instead) diff --git a/sentry-ruby/lib/sentry/client.rb b/sentry-ruby/lib/sentry/client.rb index cf8bb8c7d..d41056aa5 100644 --- a/sentry-ruby/lib/sentry/client.rb +++ b/sentry-ruby/lib/sentry/client.rb @@ -195,9 +195,10 @@ def event_from_check_in( def event_from_log(message, level:, **options) return unless configuration.sending_allowed? - attributes = options.reject { |k, _| k == :level || k == :severity } + attributes = options.reject { |k, _| k == :level || k == :severity || k == :origin } + origin = options[:origin] - LogEvent.new(level: level, body: message, attributes: attributes) + LogEvent.new(level: level, body: message, attributes: attributes, origin: origin) end # Initializes an Event object with the given Transaction object. diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 31eb20374..438fb85c2 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -29,7 +29,8 @@ class LogEvent "sentry.address" => :server_name, "sentry.sdk.name" => :sdk_name, "sentry.sdk.version" => :sdk_version, - "sentry.message.template" => :template + "sentry.message.template" => :template, + "sentry.origin" => :origin } PARAMETER_PREFIX = "sentry.message.parameter" @@ -42,7 +43,7 @@ class LogEvent LEVELS = %i[trace debug info warn error fatal].freeze - attr_accessor :level, :body, :template, :attributes, :user + attr_accessor :level, :body, :template, :attributes, :user, :origin attr_reader :configuration, *(SERIALIZEABLE_ATTRIBUTES - %i[level body attributes]) @@ -82,6 +83,7 @@ def initialize(configuration: Sentry.configuration, **options) @template = @body if is_template? @attributes = options[:attributes] || DEFAULT_ATTRIBUTES @user = options[:user] || {} + @origin = options[:origin] @contexts = {} end diff --git a/sentry-ruby/lib/sentry/std_lib_logger.rb b/sentry-ruby/lib/sentry/std_lib_logger.rb index ba4e3f71f..6d0697ada 100644 --- a/sentry-ruby/lib/sentry/std_lib_logger.rb +++ b/sentry-ruby/lib/sentry/std_lib_logger.rb @@ -12,6 +12,8 @@ module StdLibLogger 4 => :fatal }.freeze + ORIGIN = "auto.logger.ruby.std_logger" + def add(severity, message = nil, progname = nil, &block) result = super @@ -35,7 +37,7 @@ def add(severity, message = nil, progname = nil, &block) message = message.to_s.strip if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity] - Sentry.logger.send(method, message) + Sentry.logger.send(method, message, origin: ORIGIN) end end diff --git a/sentry-ruby/spec/isolated/std_lib_logger_spec.rb b/sentry-ruby/spec/isolated/std_lib_logger_spec.rb index 700657a65..0428c4daf 100644 --- a/sentry-ruby/spec/isolated/std_lib_logger_spec.rb +++ b/sentry-ruby/spec/isolated/std_lib_logger_spec.rb @@ -57,6 +57,7 @@ expect(log_event[:level]).to eql(level) expect(log_event[:body]).to eql("Hello World") + expect(log_event[:attributes]["sentry.origin"][:value]).to eq("auto.logger.ruby.std_logger") end end end diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index 73b46096e..f2580c495 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -20,6 +20,17 @@ expect(event.body).to eq("User John has logged in!") end + it "accepts origin parameter" do + event = described_class.new( + configuration: configuration, + level: :info, + body: "Database query executed", + origin: "auto.db.rails" + ) + + expect(event.origin).to eq("auto.db.rails") + end + it "accepts attributes" do attributes = { "sentry.message.template" => "User %s has logged in!", @@ -172,5 +183,30 @@ expect(hash[:attributes]["user.name"]).to eq("john_doe") expect(hash[:attributes]["user.email"]).to eq("john@example.com") end + + it "includes sentry.origin attribute when origin is set" do + event = described_class.new( + configuration: configuration, + level: :info, + body: "Database query executed", + origin: "auto.db.rails" + ) + + hash = event.to_hash + + expect(hash[:attributes]["sentry.origin"]).to eq({ value: "auto.db.rails", type: "string" }) + end + + it "does not include sentry.origin attribute when origin is nil" do + event = described_class.new( + configuration: configuration, + level: :info, + body: "Manual log message" + ) + + hash = event.to_hash + + expect(hash[:attributes]).not_to have_key("sentry.origin") + end end end diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 1e4bbd0c6..a2a94c43e 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -399,6 +399,39 @@ expect(log_event[:trace_id]).to_not be(nil) expect(log_event[:attributes]).to have_key("sentry.trace.parent_span_id") end + + it "includes sentry.origin attribute when origin is provided" do + expect do + described_class.capture_log("Database query executed", level: :info, origin: "auto.logger.rails.log_subscriber") + end.to_not change { sentry_events.count } + + Sentry.get_current_client.flush + + expect(sentry_envelopes.count).to eq(1) + + log_event = sentry_logs.first + + expect(log_event[:level]).to eq("info") + expect(log_event[:body]).to eq("Database query executed") + expect(log_event[:attributes]).to have_key("sentry.origin") + expect(log_event[:attributes]["sentry.origin"]).to eq({ value: "auto.logger.rails.log_subscriber", type: "string" }) + end + + it "does not include sentry.origin attribute when origin is not provided" do + expect do + described_class.capture_log("Manual log message", level: :info) + end.to_not change { sentry_events.count } + + Sentry.get_current_client.flush + + expect(sentry_envelopes.count).to eq(1) + + log_event = sentry_logs.first + + expect(log_event[:level]).to eq("info") + expect(log_event[:body]).to eq("Manual log message") + expect(log_event[:attributes]).not_to have_key("sentry.origin") + end end describe ".start_transaction" do