From 6d0093962cf28228d6c32fc7f396f5d05b783c13 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 7 May 2025 08:14:34 +0000 Subject: [PATCH 01/21] Add StructuredLogger and re-purpose Sentry.logger --- sentry-ruby/lib/sentry-ruby.rb | 17 ++++--- sentry-ruby/lib/sentry/client.rb | 3 ++ sentry-ruby/lib/sentry/configuration.rb | 1 + sentry-ruby/lib/sentry/log_event_buffer.rb | 3 ++ sentry-ruby/lib/sentry/logging/device.rb | 48 ++++++++++++++++++ sentry-ruby/lib/sentry/structured_logger.rb | 49 +++++++++++++++++++ .../spec/sentry/structured_logger_spec.rb | 48 ++++++++++++++++++ 7 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 sentry-ruby/lib/sentry/logging/device.rb create mode 100644 sentry-ruby/lib/sentry/structured_logger.rb create mode 100644 sentry-ruby/spec/sentry/structured_logger_spec.rb diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index e3fff2a62..600bc5b5f 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -11,7 +11,7 @@ require "sentry/utils/encoding_helper" require "sentry/utils/logging_helper" require "sentry/configuration" -require "sentry/logger" +require "sentry/structured_logger" require "sentry/event" require "sentry/error_event" require "sentry/transaction_event" @@ -54,6 +54,7 @@ module Sentry GLOBALS = %i[ main_hub + logger session_flusher backpressure_monitor metrics_aggregator @@ -95,9 +96,8 @@ def exception_locals_tp attr_reader :metrics_aggregator # @!attribute [r] logger - # @return [Logger] - # @!visibility private - attr_reader :sdk_logger + # @return [Logging::Device] + attr_reader :logger ##### Patch Registration ##### @@ -244,8 +244,8 @@ def init(&block) config = Configuration.new yield(config) if block_given? - # Internal SDK logger - @sdk_logger = config.sdk_logger + # Public-facing Structured Logger + @logger = StructuredLogger.new(config) if config._experiments[:enable_logs] config.detect_release apply_patches(config) @@ -634,6 +634,11 @@ def sys_command(command) result.strip end + # @!visibility private + def sdk_logger + configuration.sdk_logger + end + # @!visibility private def sdk_meta META diff --git a/sentry-ruby/lib/sentry/client.rb b/sentry-ruby/lib/sentry/client.rb index 23d8c0e32..63fd08465 100644 --- a/sentry-ruby/lib/sentry/client.rb +++ b/sentry-ruby/lib/sentry/client.rb @@ -16,6 +16,9 @@ class Client # @return [SpotlightTransport, nil] attr_reader :spotlight_transport + # @!visibility private + attr_reader :log_event_buffer + # @!macro configuration attr_reader :configuration diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 02df33604..171577cd3 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -12,6 +12,7 @@ require "sentry/metrics/configuration" require "sentry/linecache" require "sentry/interfaces/stacktrace_builder" +require "sentry/logger" require "sentry/log_event_buffer" module Sentry diff --git a/sentry-ruby/lib/sentry/log_event_buffer.rb b/sentry-ruby/lib/sentry/log_event_buffer.rb index 6864a6662..983bff429 100644 --- a/sentry-ruby/lib/sentry/log_event_buffer.rb +++ b/sentry-ruby/lib/sentry/log_event_buffer.rb @@ -7,6 +7,9 @@ class LogEventBuffer < ThreadedPeriodicWorker FLUSH_INTERVAL = 5 # seconds DEFAULT_MAX_EVENTS = 100 + # @!visibility private + attr_reader :pending_events + def initialize(configuration, client) super(configuration.sdk_logger, FLUSH_INTERVAL) diff --git a/sentry-ruby/lib/sentry/logging/device.rb b/sentry-ruby/lib/sentry/logging/device.rb new file mode 100644 index 000000000..bab0f7116 --- /dev/null +++ b/sentry-ruby/lib/sentry/logging/device.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Sentry + module Logging + class Device + attr_reader :handlers + + def initialize(options) + @handlers = options.fetch(:handlers) + end + + def trace(message, payload = {}) + log(:trace, message, payload) + end + + def debug(message, payload = {}) + log(:debug, message, payload) + end + + def info(message, payload = {}) + log(:info, message, payload) + end + + def warn(message, payload = {}) + log(:warn, message, payload) + end + + def error(message, payload = {}) + log(:error, message, payload) + end + + def fatal(message, payload = {}) + log(:fatal, message, payload) + end + + def log(level, message, payload) + handlers.each do |handler| + case handler + when Sentry::Logger + handler.public_send(level, message) + else + handler.public_send(level, message, payload) + end + end + end + end + end +end diff --git a/sentry-ruby/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb new file mode 100644 index 000000000..d2ce19ec5 --- /dev/null +++ b/sentry-ruby/lib/sentry/structured_logger.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Sentry + class StructuredLogger + # https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number + LEVELS = { + "trace" => 1, + "debug" => 5, + "info" => 9, + "warn" => 13, + "error" => 17, + "fatal" => 21 + }.freeze + + attr_reader :config + + def initialize(config) + @config = config + end + + def trace(message, payload = {}) + log(:trace, message, payload) + end + + def debug(message, payload = {}) + log(:debug, message, payload) + end + + def info(message, payload = {}) + log(:info, message, payload) + end + + def warn(message, payload = {}) + log(:warn, message, payload) + end + + def error(message, payload = {}) + log(:error, message, payload) + end + + def fatal(message, payload = {}) + log(:fatal, message, payload) + end + + def log(level, message, payload) + Sentry.capture_log(message, level: level, severity: LEVELS[level], **payload) + end + end +end diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb new file mode 100644 index 000000000..05f294226 --- /dev/null +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Sentry::StructuredLogger do + context "when log events are not enabled" do + before do + perform_basic_setup + end + + it "logger is not set up" do + expect(Sentry.logger).to be_nil + end + end + + context "when log events are enabled" do + before do + perform_basic_setup do |config| + config.max_log_events = 1 + config._experiments = { enable_logs: true } + end + end + + let(:logs) do + Sentry.get_current_client.log_event_buffer.pending_events + end + + # TODO: At the moment the Sentry::Logger enforces info - is that intentional? + ["info", "warn", "error", "fatal"].each do |level| + describe "##{level}" do + it "logs using default logger and LogEvent logger with extra attributes" do + payload = { user_id: 123, action: "create" } + + Sentry.logger.public_send(level, "Hello World", payload) + + expect(logs).to_not be_empty + + log_event = logs.last + + expect(log_event.type).to eql("log") + expect(log_event.level).to eql(level.to_sym) + expect(log_event.body).to eql("Hello World") + expect(log_event.attributes).to include(payload) + end + end + end + end +end From 570b449ca644b86e8ea5caf94020088c421b10b5 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 7 May 2025 11:46:41 +0000 Subject: [PATCH 02/21] YARD docs for structured logger --- sentry-ruby/lib/sentry-ruby.rb | 34 +++++++++++-- sentry-ruby/lib/sentry/structured_logger.rb | 53 ++++++++++++++++++++- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index 600bc5b5f..a2aefe6f0 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -96,7 +96,24 @@ def exception_locals_tp attr_reader :metrics_aggregator # @!attribute [r] logger - # @return [Logging::Device] + # Returns the structured logger instance that implements Sentry's SDK telemetry logs protocol. + # This logger is only available when logs are enabled in the configuration. + # + # @example Enable logs in configuration + # Sentry.init do |config| + # config.dsn = "YOUR_DSN" + # config._experiments = { enable_logs: true } + # end + # + # @example Basic usage + # Sentry.logger.info("User logged in successfully", user_id: 123) + # Sentry.logger.error("Failed to process payment", + # transaction_id: "tx_123", + # error_code: "PAYMENT_FAILED" + # ) + # + # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol + # @return [StructuredLogger, nil] The structured logger instance or nil if logs are disabled attr_reader :logger ##### Patch Registration ##### @@ -244,7 +261,9 @@ def init(&block) config = Configuration.new yield(config) if block_given? - # Public-facing Structured Logger + # Initialize the public-facing Structured Logger if logs are enabled + # This creates a StructuredLogger instance that implements Sentry's SDK telemetry logs protocol + # @see https://develop.sentry.dev/sdk/telemetry/logs/ @logger = StructuredLogger.new(config) if config._experiments[:enable_logs] config.detect_release @@ -499,12 +518,19 @@ def capture_check_in(slug, status, **options) end # Captures a log event and sends it to Sentry via the currently active hub. + # This is the underlying method used by the StructuredLogger class. # # @param message [String] the log message # @param [Hash] options Extra log event options - # @option options [Symbol] level The log level + # @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 [Hash] Additional attributes to include with the log # - # @return [LogEvent, nil] + # @example Direct usage (prefer using Sentry.logger instead) + # Sentry.capture_log("User logged in", level: :info, user_id: 123) + # + # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol + # @return [LogEvent, nil] The created log event or nil if logging is disabled def capture_log(message, **options) return unless initialized? get_current_hub.capture_log_event(message, **options) diff --git a/sentry-ruby/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb index d2ce19ec5..ef7377e5d 100644 --- a/sentry-ruby/lib/sentry/structured_logger.rb +++ b/sentry-ruby/lib/sentry/structured_logger.rb @@ -1,8 +1,27 @@ # frozen_string_literal: true module Sentry + # The StructuredLogger class implements Sentry's SDK telemetry logs protocol. + # It provides methods for logging messages at different severity levels and + # sending them to Sentry with structured data. + # + # This class follows the Sentry Logs Protocol as defined in: + # https://develop.sentry.dev/sdk/telemetry/logs/ + # + # @example Basic usage + # Sentry.logger.info("User logged in", user_id: 123) + # + # @example With structured data + # Sentry.logger.warn("API request failed", + # status_code: 404, + # endpoint: "/api/users", + # request_id: "abc-123" + # ) + # + # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol class StructuredLogger - # https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number + # Severity number mapping for log levels according to the Sentry Logs Protocol + # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number LEVELS = { "trace" => 1, "debug" => 5, @@ -12,36 +31,68 @@ class StructuredLogger "fatal" => 21 }.freeze + # @return [Configuration] The Sentry configuration attr_reader :config + # Initializes a new StructuredLogger instance + # @param config [Configuration] The Sentry configuration def initialize(config) @config = config end + # Logs a message at TRACE level + # @param message [String] The log message + # @param payload [Hash] Additional attributes to include with the log + # @return [LogEvent, nil] The created log event or nil if logging is disabled def trace(message, payload = {}) log(:trace, message, payload) end + # Logs a message at DEBUG level + # @param message [String] The log message + # @param payload [Hash] Additional attributes to include with the log + # @return [LogEvent, nil] The created log event or nil if logging is disabled def debug(message, payload = {}) log(:debug, message, payload) end + # Logs a message at INFO level + # @param message [String] The log message + # @param payload [Hash] Additional attributes to include with the log + # @return [LogEvent, nil] The created log event or nil if logging is disabled def info(message, payload = {}) log(:info, message, payload) end + # Logs a message at WARN level + # @param message [String] The log message + # @param payload [Hash] Additional attributes to include with the log + # @return [LogEvent, nil] The created log event or nil if logging is disabled def warn(message, payload = {}) log(:warn, message, payload) end + # Logs a message at ERROR level + # @param message [String] The log message + # @param payload [Hash] Additional attributes to include with the log + # @return [LogEvent, nil] The created log event or nil if logging is disabled def error(message, payload = {}) log(:error, message, payload) end + # Logs a message at FATAL level + # @param message [String] The log message + # @param payload [Hash] Additional attributes to include with the log + # @return [LogEvent, nil] The created log event or nil if logging is disabled def fatal(message, payload = {}) log(:fatal, message, payload) end + # Logs a message at the specified level + # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal) + # @param message [String] The log message + # @param payload [Hash] Additional attributes to include with the log + # @return [LogEvent, nil] The created log event or nil if logging is disabled def log(level, message, payload) Sentry.capture_log(message, level: level, severity: LEVELS[level], **payload) end From 4d808cc6c99c0b2cde932a64eb59a3623d54b536 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 7 May 2025 12:04:39 +0000 Subject: [PATCH 03/21] Remove obsolete comment --- sentry-ruby/spec/sentry/structured_logger_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index 05f294226..e2c509477 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -25,7 +25,6 @@ Sentry.get_current_client.log_event_buffer.pending_events end - # TODO: At the moment the Sentry::Logger enforces info - is that intentional? ["info", "warn", "error", "fatal"].each do |level| describe "##{level}" do it "logs using default logger and LogEvent logger with extra attributes" do From 8ce961eef7e79156b8152e3dc641421b62763795 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 8 May 2025 13:34:52 +0000 Subject: [PATCH 04/21] Add support for message templates --- sentry-ruby/lib/sentry/client.rb | 9 ++- sentry-ruby/lib/sentry/log_event.rb | 29 +++++++- sentry-ruby/lib/sentry/structured_logger.rb | 74 ++++++++++++------- sentry-ruby/spec/sentry/log_event_spec.rb | 13 +--- .../spec/sentry/structured_logger_spec.rb | 43 +++++++++++ sentry-ruby/spec/sentry/transport_spec.rb | 15 +--- 6 files changed, 130 insertions(+), 53 deletions(-) diff --git a/sentry-ruby/lib/sentry/client.rb b/sentry-ruby/lib/sentry/client.rb index 63fd08465..fd5db6ba8 100644 --- a/sentry-ruby/lib/sentry/client.rb +++ b/sentry-ruby/lib/sentry/client.rb @@ -182,10 +182,17 @@ def event_from_check_in( end # Initializes a LogEvent object with the given message and options + # + # @param message [String] the log message + # @param level [Symbol] the log level (:trace, :debug, :info, :warn, :error, :fatal) + # @param options [Hash] additional options + # @option options [Array] :parameters Array of values to replace template tokens in the message + # + # @return [LogEvent] the created log event def event_from_log(message, level:, **options) return unless configuration.sending_allowed? - attributes = options.reject { |k, _| k == :level } + attributes = options.reject { |k, _| k == :level || k == :severity } LogEvent.new( level: level, diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 52160a1a6..50aa12549 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -21,19 +21,32 @@ class LogEvent < Event "sentry.release" => :release, "sentry.address" => :server_name, "sentry.sdk.name" => :sdk_name, - "sentry.sdk.version" => :sdk_version + "sentry.sdk.version" => :sdk_version, + "sentry.message.template" => :template } LEVELS = %i[trace debug info warn error fatal].freeze - attr_accessor :level, :body, :attributes, :trace_id + attr_accessor :level, :body, :template, :attributes, :trace_id, :parameters def initialize(configuration: Sentry.configuration, **options) super(configuration: configuration) + @type = TYPE @level = options.fetch(:level) @body = options[:body] + @template = @body @attributes = options[:attributes] || {} + @parameters = @attributes[:parameters] || [] + + if has_template_parameters? + @attributes["sentry.message.template"] = @body + + @parameters.each_with_index do |param, index| + @attributes["sentry.message.parameters.#{index}"] = param + end + end + @contexts = {} end @@ -79,6 +92,14 @@ def serialize_parent_span_id @contexts.dig(:trace, :parent_span_id) end + def serialize_body + if @parameters.empty? + @body + else + sprintf(@body, *@parameters) + end + end + def serialize_attributes hash = @attributes.each_with_object({}) do |(key, value), memo| memo[key] = attribute_hash(value) @@ -109,5 +130,9 @@ def value_type(value) "string" end end + + def has_template_parameters? + @parameters.is_a?(Array) && !@parameters.empty? && @body.is_a?(String) && @body.include?("%") + end end end diff --git a/sentry-ruby/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb index ef7377e5d..f396f6ccf 100644 --- a/sentry-ruby/lib/sentry/structured_logger.rb +++ b/sentry-ruby/lib/sentry/structured_logger.rb @@ -23,78 +23,98 @@ class StructuredLogger # Severity number mapping for log levels according to the Sentry Logs Protocol # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number LEVELS = { - "trace" => 1, - "debug" => 5, - "info" => 9, - "warn" => 13, - "error" => 17, - "fatal" => 21 + trace: 1, + debug: 5, + info: 9, + warn: 13, + error: 17, + fatal: 21 }.freeze # @return [Configuration] The Sentry configuration + # @!visibility private attr_reader :config # Initializes a new StructuredLogger instance + # # @param config [Configuration] The Sentry configuration def initialize(config) @config = config end # Logs a message at TRACE level + # # @param message [String] The log message - # @param payload [Hash] Additional attributes to include with the log + # @param parameters [Array] Array of values to replace template parameters in the message + # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled - def trace(message, payload = {}) - log(:trace, message, payload) + def trace(message, parameters = [], **attributes) + log(__method__, message, parameters: parameters, **attributes) end # Logs a message at DEBUG level + # # @param message [String] The log message - # @param payload [Hash] Additional attributes to include with the log + # @param parameters [Array] Array of values to replace template parameters in the message + # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled - def debug(message, payload = {}) - log(:debug, message, payload) + def debug(message, parameters = [], **attributes) + log(__method__, message, parameters: parameters, **attributes) end # Logs a message at INFO level + # # @param message [String] The log message - # @param payload [Hash] Additional attributes to include with the log + # @param parameters [Array] Array of values to replace template parameters in the message + # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled - def info(message, payload = {}) - log(:info, message, payload) + def info(message, parameters = [], **attributes) + log(__method__, message, parameters: parameters, **attributes) end # Logs a message at WARN level + # # @param message [String] The log message - # @param payload [Hash] Additional attributes to include with the log + # @param parameters [Array] Array of values to replace template parameters in the message + # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled - def warn(message, payload = {}) - log(:warn, message, payload) + def warn(message, parameters = [], **attributes) + log(__method__, message, parameters: parameters, **attributes) end # Logs a message at ERROR level + # # @param message [String] The log message - # @param payload [Hash] Additional attributes to include with the log + # @param parameters [Array] Array of values to replace template parameters in the message + # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled - def error(message, payload = {}) - log(:error, message, payload) + def error(message, parameters = [], **attributes) + log(__method__, message, parameters: parameters, **attributes) end # Logs a message at FATAL level + # # @param message [String] The log message - # @param payload [Hash] Additional attributes to include with the log + # @param parameters [Array] Array of values to replace template parameters in the message + # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled - def fatal(message, payload = {}) - log(:fatal, message, payload) + def fatal(message, parameters = [], **attributes) + log(__method__, message, parameters: parameters, **attributes) end # Logs a message at the specified level + # # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal) # @param message [String] The log message - # @param payload [Hash] Additional attributes to include with the log + # @param parameters [Array] Array of values to replace template parameters in the message + # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled - def log(level, message, payload) - Sentry.capture_log(message, level: level, severity: LEVELS[level], **payload) + def log(level, message, parameters:, **attributes) + if parameters.is_a?(Hash) && attributes.empty? + Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: [], **parameters) + else + Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes) + end end end end diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index fdc436abb..913f4341e 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -47,16 +47,10 @@ end it "includes all required fields" 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 + body: "User John has logged in!" ) hash = event.to_hash @@ -68,11 +62,6 @@ 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.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/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index e2c509477..784e4f56e 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -41,6 +41,49 @@ expect(log_event.body).to eql("Hello World") expect(log_event.attributes).to include(payload) end + + it "logs with template parameters" do + Sentry.logger.public_send(level, "Hello %s it is %s", ["Jane", "Monday"]) + + expect(logs).to_not be_empty + + log_event = logs.last + log_hash = log_event.to_hash + + expect(log_event.type).to eql("log") + expect(log_event.level).to eql(level.to_sym) + expect(log_event.body).to eql("Hello %s it is %s") + + expect(log_hash[:body]).to eql("Hello Jane it is Monday") + + attributes = log_hash[:attributes] + + expect(attributes["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) + expect(attributes["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) + expect(attributes["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) + end + + it "logs with template parameters and extra attributres" do + Sentry.logger.public_send(level, "Hello %s it is %s", ["Jane", "Monday"], extra: 312) + + expect(logs).to_not be_empty + + log_event = logs.last + log_hash = log_event.to_hash + + expect(log_event.type).to eql("log") + expect(log_event.level).to eql(level.to_sym) + expect(log_event.body).to eql("Hello %s it is %s") + + expect(log_hash[:body]).to eql("Hello Jane it is Monday") + + attributes = log_hash[:attributes] + + expect(attributes[:extra]).to eql({ value: 312, type: "integer" }) + expect(attributes["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) + expect(attributes["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) + expect(attributes["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) + end end end end diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 558963f3c..2ea17d559 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -243,15 +243,11 @@ Sentry::LogEvent.new( configuration: configuration, level: :info, - body: "User John has logged in!", + body: "User %s 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" + parameters: ["John"] } ) end @@ -299,10 +295,7 @@ 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" => "development", "type" => "string" }, - "sentry.release" => { "value" => "1.0.0", "type" => "string" }, - "sentry.trace.parent_span_id" => { "value" => "b0e6f15b45c36b12", "type" => "string" }, - "sentry.address" => { "value" => matching(/\w+/), "type" => "string" } + "sentry.environment" => { "value" => "development", "type" => "string" } ) end end @@ -318,7 +311,7 @@ it "gracefully removes bad encoding breadcrumb message" do expect do - serialized_result = JSON.generate(event.to_hash) + JSON.generate(event.to_hash) end.not_to raise_error end end From b8ef85ef06d5cd4e1034401e840b8202268bd9bf Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:00:01 +0000 Subject: [PATCH 05/21] Support Hash-based log templates too --- sentry-ruby/lib/sentry/log_event.rb | 36 ++++++++++----- sentry-ruby/lib/sentry/structured_logger.rb | 6 +-- sentry-ruby/spec/sentry/log_event_spec.rb | 22 +++++++++ .../spec/sentry/structured_logger_spec.rb | 45 +++++++++++++++++++ 4 files changed, 94 insertions(+), 15 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 50aa12549..cd985210d 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -37,17 +37,9 @@ def initialize(configuration: Sentry.configuration, **options) @body = options[:body] @template = @body @attributes = options[:attributes] || {} - @parameters = @attributes[:parameters] || [] - - if has_template_parameters? - @attributes["sentry.message.template"] = @body - - @parameters.each_with_index do |param, index| - @attributes["sentry.message.parameters.#{index}"] = param - end - end - @contexts = {} + + initialize_parameters end def to_hash @@ -95,6 +87,8 @@ def serialize_parent_span_id def serialize_body if @parameters.empty? @body + elsif @parameters.is_a?(Hash) + @body % @parameters else sprintf(@body, *@parameters) end @@ -131,8 +125,26 @@ def value_type(value) end end - def has_template_parameters? - @parameters.is_a?(Array) && !@parameters.empty? && @body.is_a?(String) && @body.include?("%") + def initialize_parameters + @parameters = @attributes[:parameters] || [] + + return unless is_template? + + attributes["sentry.message.template"] = body + + if @parameters.is_a?(Hash) + @parameters.each do |key, value| + @attributes["sentry.message.parameters.#{key}"] = value + end + else + @parameters.each_with_index do |param, index| + @attributes["sentry.message.parameters.#{index}"] = param + end + end + end + + def is_template? + @body.is_a?(String) && @body.include?("%") end end end diff --git a/sentry-ruby/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb index f396f6ccf..5600ced66 100644 --- a/sentry-ruby/lib/sentry/structured_logger.rb +++ b/sentry-ruby/lib/sentry/structured_logger.rb @@ -106,12 +106,12 @@ def fatal(message, parameters = [], **attributes) # # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal) # @param message [String] The log message - # @param parameters [Array] Array of values to replace template parameters in the message + # @param parameters [Array, Hash] Array or Hash of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # @return [LogEvent, nil] The created log event or nil if logging is disabled def log(level, message, parameters:, **attributes) - if parameters.is_a?(Hash) && attributes.empty? - Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: [], **parameters) + if parameters.is_a?(Hash) && attributes.empty? && !message.include?("%") + Sentry.capture_log(message, level: level, severity: LEVELS[level], **parameters) else Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes) end diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index 913f4341e..e29d5ce79 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -46,6 +46,28 @@ configuration.server_name = "server-123" end + it "formats message with hash-based parameters" do + attributes = { + parameters: { name: "John", day: "Monday" } + } + + event = described_class.new( + configuration: configuration, + level: :info, + body: "Hello %{name}, today is %{day}", + attributes: attributes + ) + + hash = event.to_hash + + expect(hash[:body]).to eq("Hello John, today is Monday") + + attributes = hash[:attributes] + expect(attributes["sentry.message.template"]).to eq({ value: "Hello %{name}, today is %{day}", type: "string" }) + expect(attributes["sentry.message.parameters.name"]).to eq({ value: "John", type: "string" }) + expect(attributes["sentry.message.parameters.day"]).to eq({ value: "Monday", type: "string" }) + end + it "includes all required fields" do event = described_class.new( configuration: configuration, diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index 784e4f56e..8c7de274b 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -84,6 +84,51 @@ expect(attributes["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) expect(attributes["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) end + + it "logs with hash-based template parameters" do + hash_params = { name: "Jane", day: "Monday" } + Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", hash_params) + + expect(logs).to_not be_empty + + log_event = logs.last + log_hash = log_event.to_hash + + expect(log_event.type).to eql("log") + expect(log_event.level).to eql(level.to_sym) + expect(log_event.body).to eql("Hello %{name}, it is %{day}") + + expect(log_hash[:body]).to eql("Hello Jane, it is Monday") + + attributes = log_hash[:attributes] + + expect(attributes["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) + expect(attributes["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) + expect(attributes["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) + end + + it "logs with hash-based template parameters and extra attributes" do + hash_params = { name: "Jane", day: "Monday" } + Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", hash_params, user_id: 123) + + expect(logs).to_not be_empty + + log_event = logs.last + log_hash = log_event.to_hash + + expect(log_event.type).to eql("log") + expect(log_event.level).to eql(level.to_sym) + expect(log_event.body).to eql("Hello %{name}, it is %{day}") + + expect(log_hash[:body]).to eql("Hello Jane, it is Monday") + + attributes = log_hash[:attributes] + + expect(attributes[:user_id]).to eql({ value: 123, type: "integer" }) + expect(attributes["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) + expect(attributes["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) + expect(attributes["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) + end end end end From 3aba1f50e5fb179976333fb7d82410fc0b2629da Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:01:20 +0000 Subject: [PATCH 06/21] Remove redundant assignment --- sentry-ruby/lib/sentry/log_event.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index cd985210d..9d82439c2 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -130,8 +130,6 @@ def initialize_parameters return unless is_template? - attributes["sentry.message.template"] = body - if @parameters.is_a?(Hash) @parameters.each do |key, value| @attributes["sentry.message.parameters.#{key}"] = value From 4d83fac0c3c01252ed31045f43c390051d8422a3 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:08:10 +0000 Subject: [PATCH 07/21] Ensure we don't set message.template when body is not a template --- sentry-ruby/lib/sentry/log_event.rb | 4 ++-- sentry-ruby/spec/sentry/log_event_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 9d82439c2..eb9d033e9 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -35,7 +35,7 @@ def initialize(configuration: Sentry.configuration, **options) @type = TYPE @level = options.fetch(:level) @body = options[:body] - @template = @body + @template = @body if is_template? @attributes = options[:attributes] || {} @contexts = {} @@ -128,7 +128,7 @@ def value_type(value) def initialize_parameters @parameters = @attributes[:parameters] || [] - return unless is_template? + return unless template if @parameters.is_a?(Hash) @parameters.each do |key, value| diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index e29d5ce79..1cd58f935 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -88,6 +88,18 @@ expect(attributes["sentry.sdk.version"]).to eq({ value: Sentry::VERSION, type: "string" }) end + it "doesn't set message.template when the body is not a template" do + event = described_class.new( + configuration: configuration, + level: :info, + body: "User John has logged in!" + ) + + hash = event.to_hash + + expect(hash[:attributes]).not_to have_key("sentry.message.template") + end + it "serializes different attribute types correctly" do attributes = { "string_attr" => "string value", From 5645fb484fd2508b74acdc7120abeb42f4515cf1 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:13:40 +0000 Subject: [PATCH 08/21] Lazy-load parameters No need to do this upfront because it is needed only when a log event actually gets to the envelope --- sentry-ruby/lib/sentry/log_event.rb | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index eb9d033e9..a72c6f7f3 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -27,7 +27,7 @@ class LogEvent < Event LEVELS = %i[trace debug info warn error fatal].freeze - attr_accessor :level, :body, :template, :attributes, :trace_id, :parameters + attr_accessor :level, :body, :template, :attributes, :trace_id def initialize(configuration: Sentry.configuration, **options) super(configuration: configuration) @@ -38,8 +38,6 @@ def initialize(configuration: Sentry.configuration, **options) @template = @body if is_template? @attributes = options[:attributes] || {} @contexts = {} - - initialize_parameters end def to_hash @@ -85,12 +83,12 @@ def serialize_parent_span_id end def serialize_body - if @parameters.empty? + if parameters.empty? @body - elsif @parameters.is_a?(Hash) - @body % @parameters + elsif parameters.is_a?(Hash) + @body % parameters else - sprintf(@body, *@parameters) + sprintf(@body, *parameters) end end @@ -125,18 +123,20 @@ def value_type(value) end end - def initialize_parameters - @parameters = @attributes[:parameters] || [] + def parameters + @parameters ||= begin + return [] unless template - return unless template + parameters = attributes[:parameters] || [] - if @parameters.is_a?(Hash) - @parameters.each do |key, value| - @attributes["sentry.message.parameters.#{key}"] = value - end - else - @parameters.each_with_index do |param, index| - @attributes["sentry.message.parameters.#{index}"] = param + if parameters.is_a?(Hash) + parameters.each do |key, value| + attributes["sentry.message.parameters.#{key}"] = value + end + else + parameters.each_with_index do |param, index| + attributes["sentry.message.parameters.#{index}"] = param + end end end end From 4d5571e08dba4ee6dfe932d6544a629ddf2396ba Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:16:40 +0000 Subject: [PATCH 09/21] Reduce object allocations --- sentry-ruby/lib/sentry/log_event.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index a72c6f7f3..358b73b51 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -7,6 +7,10 @@ module Sentry class LogEvent < Event TYPE = "log" + DEFAULT_PARAMETERS = [].freeze + DEFAULT_ATTRIBUTES = {}.freeze + DEFAULT_CONTEXT = {}.freeze + SERIALIZEABLE_ATTRIBUTES = %i[ level body @@ -36,8 +40,8 @@ def initialize(configuration: Sentry.configuration, **options) @level = options.fetch(:level) @body = options[:body] @template = @body if is_template? - @attributes = options[:attributes] || {} - @contexts = {} + @attributes = options[:attributes] || DEFAULT_ATTRIBUTES + @contexts = DEFAULT_CONTEXT end def to_hash @@ -125,9 +129,9 @@ def value_type(value) def parameters @parameters ||= begin - return [] unless template + return DEFAULT_PARAMETERS unless template - parameters = attributes[:parameters] || [] + parameters = attributes[:parameters] || DEFAULT_PARAMETERS if parameters.is_a?(Hash) parameters.each do |key, value| From 4c7c762f0a8838d1ba5a738ff5b2979d2fea8fbf Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:18:12 +0000 Subject: [PATCH 10/21] Use attr readers consistently --- sentry-ruby/lib/sentry/log_event.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 358b73b51..cf3ad2066 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -79,25 +79,25 @@ def serialize_timestamp end def serialize_trace_id - @contexts.dig(:trace, :trace_id) + contexts.dig(:trace, :trace_id) end def serialize_parent_span_id - @contexts.dig(:trace, :parent_span_id) + contexts.dig(:trace, :parent_span_id) end def serialize_body if parameters.empty? - @body + body elsif parameters.is_a?(Hash) - @body % parameters + body % parameters else - sprintf(@body, *parameters) + sprintf(body, *parameters) end end def serialize_attributes - hash = @attributes.each_with_object({}) do |(key, value), memo| + hash = attributes.each_with_object({}) do |(key, value), memo| memo[key] = attribute_hash(value) end @@ -146,7 +146,7 @@ def parameters end def is_template? - @body.is_a?(String) && @body.include?("%") + body.is_a?(String) && body.include?("%") end end end From 02da067924e3f07a3c215c2f2158cad3b430c409 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:18:36 +0000 Subject: [PATCH 11/21] Remove unused reader --- sentry-ruby/lib/sentry/log_event.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index cf3ad2066..78c0f69f6 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -31,7 +31,7 @@ class LogEvent < Event LEVELS = %i[trace debug info warn error fatal].freeze - attr_accessor :level, :body, :template, :attributes, :trace_id + attr_accessor :level, :body, :template, :attributes def initialize(configuration: Sentry.configuration, **options) super(configuration: configuration) From 8bc6add98ad6f365c8ce31a3d1e1f23f33581b23 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 12:33:49 +0000 Subject: [PATCH 12/21] Update docs --- sentry-ruby/lib/sentry/structured_logger.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sentry-ruby/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb index 5600ced66..9d77d7274 100644 --- a/sentry-ruby/lib/sentry/structured_logger.rb +++ b/sentry-ruby/lib/sentry/structured_logger.rb @@ -18,6 +18,16 @@ module Sentry # request_id: "abc-123" # ) # + # @example With a message template + # # Using positional parameters + # Sentry.logger.info("User %s logged in", ["Jane Doe"]) + # + # # Using hash parameters + # Sentry.logger.info("User %{name} logged in", name: "Jane Doe") + # + # # Using hash parameters and extra attributes + # Sentry.logger.info("User %{name} logged in", name: "Jane Doe", user_id: 312) + # # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol class StructuredLogger # Severity number mapping for log levels according to the Sentry Logs Protocol @@ -47,6 +57,7 @@ def initialize(config) # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log + # # @return [LogEvent, nil] The created log event or nil if logging is disabled def trace(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) @@ -57,6 +68,7 @@ def trace(message, parameters = [], **attributes) # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log + # # @return [LogEvent, nil] The created log event or nil if logging is disabled def debug(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) @@ -67,6 +79,7 @@ def debug(message, parameters = [], **attributes) # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log + # # @return [LogEvent, nil] The created log event or nil if logging is disabled def info(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) @@ -77,6 +90,7 @@ def info(message, parameters = [], **attributes) # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log + # # @return [LogEvent, nil] The created log event or nil if logging is disabled def warn(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) @@ -87,6 +101,7 @@ def warn(message, parameters = [], **attributes) # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log + # # @return [LogEvent, nil] The created log event or nil if logging is disabled def error(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) @@ -97,6 +112,7 @@ def error(message, parameters = [], **attributes) # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log + # # @return [LogEvent, nil] The created log event or nil if logging is disabled def fatal(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) @@ -108,6 +124,7 @@ def fatal(message, parameters = [], **attributes) # @param message [String] The log message # @param parameters [Array, Hash] Array or Hash of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log + # # @return [LogEvent, nil] The created log event or nil if logging is disabled def log(level, message, parameters:, **attributes) if parameters.is_a?(Hash) && attributes.empty? && !message.include?("%") From 9658adfff366b6e7193964860878e331d4caadad Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 13:08:20 +0000 Subject: [PATCH 13/21] Fix issues with parameters handling and extra attributes --- sentry-ruby/lib/sentry/log_event.rb | 11 +++++++++-- sentry-ruby/lib/sentry/structured_logger.rb | 7 ++++--- sentry-ruby/spec/sentry/structured_logger_spec.rb | 6 ++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 78c0f69f6..799d90c12 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -131,7 +131,8 @@ def parameters @parameters ||= begin return DEFAULT_PARAMETERS unless template - parameters = attributes[:parameters] || DEFAULT_PARAMETERS + parameters = template_tokens.empty? ? + attributes.fetch(:parameters, DEFAULT_PARAMETERS) : attributes.slice(*template_tokens) if parameters.is_a?(Hash) parameters.each do |key, value| @@ -145,8 +146,14 @@ def parameters end end + TOKEN_REGEXP = /%\{(\w+)\}/ + + def template_tokens + @template_tokens ||= body.scan(TOKEN_REGEXP).flatten.map(&:to_sym) + end + def is_template? - body.is_a?(String) && body.include?("%") + body.include?("%s") || TOKEN_REGEXP.match?(body) end end end diff --git a/sentry-ruby/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb index 9d77d7274..79ca8d005 100644 --- a/sentry-ruby/lib/sentry/structured_logger.rb +++ b/sentry-ruby/lib/sentry/structured_logger.rb @@ -127,10 +127,11 @@ def fatal(message, parameters = [], **attributes) # # @return [LogEvent, nil] The created log event or nil if logging is disabled def log(level, message, parameters:, **attributes) - if parameters.is_a?(Hash) && attributes.empty? && !message.include?("%") - Sentry.capture_log(message, level: level, severity: LEVELS[level], **parameters) - else + case parameters + when Array then Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes) + else + Sentry.capture_log(message, level: level, severity: LEVELS[level], **parameters) end end end diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index 8c7de274b..b07d963bf 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -86,8 +86,7 @@ end it "logs with hash-based template parameters" do - hash_params = { name: "Jane", day: "Monday" } - Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", hash_params) + Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", name: "Jane", day: "Monday") expect(logs).to_not be_empty @@ -108,8 +107,7 @@ end it "logs with hash-based template parameters and extra attributes" do - hash_params = { name: "Jane", day: "Monday" } - Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", hash_params, user_id: 123) + Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", name: "Jane", day: "Monday", user_id: 123) expect(logs).to_not be_empty From 633a135cb18ee67ac014ccb29dc78ddff800bbca Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 9 May 2025 13:27:25 +0000 Subject: [PATCH 14/21] Fix spec --- sentry-ruby/spec/sentry/log_event_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index 1cd58f935..78cf931bd 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -47,9 +47,7 @@ end it "formats message with hash-based parameters" do - attributes = { - parameters: { name: "John", day: "Monday" } - } + attributes = { name: "John", day: "Monday" } event = described_class.new( configuration: configuration, From d074fac62ec6e6064042e660e08725af5a90d7bd Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 12 May 2025 08:45:35 +0000 Subject: [PATCH 15/21] Auto-flush log events when adding and fix flush --- sentry-ruby/lib/sentry/log_event_buffer.rb | 17 ++- .../spec/sentry/log_event_buffer_spec.rb | 51 ++++----- .../spec/sentry/structured_logger_spec.rb | 104 ++++++------------ 3 files changed, 73 insertions(+), 99 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event_buffer.rb b/sentry-ruby/lib/sentry/log_event_buffer.rb index 983bff429..48e3fb54f 100644 --- a/sentry-ruby/lib/sentry/log_event_buffer.rb +++ b/sentry-ruby/lib/sentry/log_event_buffer.rb @@ -3,6 +3,11 @@ require "sentry/threaded_periodic_worker" module Sentry + # LogEventBuffer buffers log events and sends them to Sentry in a single envelope. + # + # This is used internally by the `Sentry::Client`. + # + # @!visibility private class LogEventBuffer < ThreadedPeriodicWorker FLUSH_INTERVAL = 5 # seconds DEFAULT_MAX_EVENTS = 100 @@ -30,13 +35,11 @@ def start def flush @mutex.synchronize do - return unless size >= @max_events + return if empty? log_debug("[LogEventBuffer] flushing #{size} log events") - @client.send_envelope(to_envelope) - - @pending_events.clear + send_events end log_debug("[LogEventBuffer] flushed #{size} log events") @@ -50,6 +53,7 @@ def add_event(event) @mutex.synchronize do @pending_events << event + send_events if size >= @max_events end self @@ -65,6 +69,11 @@ def size private + def send_events + @client.send_envelope(to_envelope) + @pending_events.clear + end + def to_envelope envelope = Envelope.new( event_id: SecureRandom.uuid.delete("-"), diff --git a/sentry-ruby/spec/sentry/log_event_buffer_spec.rb b/sentry-ruby/spec/sentry/log_event_buffer_spec.rb index dacf02899..284b49d63 100644 --- a/sentry-ruby/spec/sentry/log_event_buffer_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_buffer_spec.rb @@ -7,12 +7,20 @@ let(:string_io) { StringIO.new } let(:logger) { ::Logger.new(string_io) } + let(:client) { double(Sentry::Client) } + let(:log_event) do + Sentry::LogEvent.new( + configuration: Sentry.configuration, + level: :info, + body: "Test message" + ) + end before do perform_basic_setup do |config| config.sdk_logger = logger config.background_worker_threads = 0 - config.max_log_events = 3 + config.max_log_events = max_log_events end Sentry.background_worker = Sentry::BackgroundWorker.new(Sentry.configuration) @@ -22,17 +30,8 @@ Sentry.background_worker = Class.new { def shutdown; end; }.new end - let(:client) { Sentry.get_current_client } - let(:transport) { client.transport } - describe "#add_event" do - let(:log_event) do - Sentry::LogEvent.new( - configuration: Sentry.configuration, - level: :info, - body: "Test message" - ) - end + let(:max_log_events) { 3 } it "does nothing when there are no pending events" do expect(client).not_to receive(:capture_envelope) @@ -43,38 +42,36 @@ end it "does nothing when the number of events is less than max_events " do - 2.times { log_event_buffer.add_event(log_event) } + expect(client).to_not receive(:send_envelope) - log_event_buffer.flush - - expect(sentry_envelopes.size).to be(0) + 2.times { log_event_buffer.add_event(log_event) } end - it "sends pending events to the client" do - 3.times { log_event_buffer.add_event(log_event) } - - log_event_buffer.flush + it "auto-flushes pending events to the client when the number of events reaches max_events" do + expect(client).to receive(:send_envelope) - expect(sentry_envelopes.size).to be(1) + 3.times { log_event_buffer.add_event(log_event) } expect(log_event_buffer).to be_empty end + end - it "thread-safely handles concurrent access", skip: RUBY_ENGINE == "jruby" do - expect(client).to receive(:send_envelope) do |_envelope| - sleep 0.1 - end + describe "multi-threaded access" do + let(:max_log_events) { 30 } - threads = 100.times.map do - (1..50).to_a.sample.times { log_event_buffer.add_event(log_event) } + it "thread-safely handles concurrent access" do + expect(client).to receive(:send_envelope).exactly(3).times + threads = 3.times.map do Thread.new do - log_event_buffer.flush + (20..30).to_a.sample.times { log_event_buffer.add_event(log_event) } end end threads.each(&:join) + log_event_buffer.flush + expect(log_event_buffer).to be_empty end end diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index b07d963bf..70bfce532 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -21,10 +21,6 @@ end end - let(:logs) do - Sentry.get_current_client.log_event_buffer.pending_events - end - ["info", "warn", "error", "fatal"].each do |level| describe "##{level}" do it "logs using default logger and LogEvent logger with extra attributes" do @@ -32,100 +28,72 @@ Sentry.logger.public_send(level, "Hello World", payload) - expect(logs).to_not be_empty + expect(sentry_logs).to_not be_empty - log_event = logs.last + log_event = sentry_logs.last - expect(log_event.type).to eql("log") - expect(log_event.level).to eql(level.to_sym) - expect(log_event.body).to eql("Hello World") - expect(log_event.attributes).to include(payload) + expect(log_event[:level]).to eql(level) + expect(log_event[:body]).to eql("Hello World") + expect(log_event[:attributes]).to include({ user_id: { value: 123, type: "integer" } }) + expect(log_event[:attributes]).to include({ action: { value: "create", type: "string" } }) end it "logs with template parameters" do Sentry.logger.public_send(level, "Hello %s it is %s", ["Jane", "Monday"]) - expect(logs).to_not be_empty - - log_event = logs.last - log_hash = log_event.to_hash - - expect(log_event.type).to eql("log") - expect(log_event.level).to eql(level.to_sym) - expect(log_event.body).to eql("Hello %s it is %s") + expect(sentry_logs).to_not be_empty - expect(log_hash[:body]).to eql("Hello Jane it is Monday") + log_event = sentry_logs.last - attributes = log_hash[:attributes] - - expect(attributes["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) - expect(attributes["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) - expect(attributes["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:level]).to eql(level) + expect(log_event[:body]).to eql("Hello Jane it is Monday") + expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) end it "logs with template parameters and extra attributres" do Sentry.logger.public_send(level, "Hello %s it is %s", ["Jane", "Monday"], extra: 312) - expect(logs).to_not be_empty - - log_event = logs.last - log_hash = log_event.to_hash - - expect(log_event.type).to eql("log") - expect(log_event.level).to eql(level.to_sym) - expect(log_event.body).to eql("Hello %s it is %s") + expect(sentry_logs).to_not be_empty - expect(log_hash[:body]).to eql("Hello Jane it is Monday") + log_event = sentry_logs.last - attributes = log_hash[:attributes] - - expect(attributes[:extra]).to eql({ value: 312, type: "integer" }) - expect(attributes["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) - expect(attributes["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) - expect(attributes["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:level]).to eql(level) + expect(log_event[:body]).to eql("Hello Jane it is Monday") + expect(log_event[:attributes][:extra]).to eql({ value: 312, type: "integer" }) + expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) end it "logs with hash-based template parameters" do Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", name: "Jane", day: "Monday") - expect(logs).to_not be_empty - - log_event = logs.last - log_hash = log_event.to_hash - - expect(log_event.type).to eql("log") - expect(log_event.level).to eql(level.to_sym) - expect(log_event.body).to eql("Hello %{name}, it is %{day}") - - expect(log_hash[:body]).to eql("Hello Jane, it is Monday") + expect(sentry_logs).to_not be_empty - attributes = log_hash[:attributes] + log_event = sentry_logs.last - expect(attributes["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) - expect(attributes["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) - expect(attributes["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:level]).to eql(level) + expect(log_event[:body]).to eql("Hello Jane, it is Monday") + expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) end it "logs with hash-based template parameters and extra attributes" do Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", name: "Jane", day: "Monday", user_id: 123) - expect(logs).to_not be_empty - - log_event = logs.last - log_hash = log_event.to_hash - - expect(log_event.type).to eql("log") - expect(log_event.level).to eql(level.to_sym) - expect(log_event.body).to eql("Hello %{name}, it is %{day}") - - expect(log_hash[:body]).to eql("Hello Jane, it is Monday") + expect(sentry_logs).to_not be_empty - attributes = log_hash[:attributes] + log_event = sentry_logs.last - expect(attributes[:user_id]).to eql({ value: 123, type: "integer" }) - expect(attributes["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) - expect(attributes["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) - expect(attributes["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:level]).to eql(level) + expect(log_event[:body]).to eql("Hello Jane, it is Monday") + expect(log_event[:attributes][:user_id]).to eql({ value: 123, type: "integer" }) + expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) end end end From bdf604dccf774c56044fba9e148504381c5fdc34 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 12 May 2025 14:09:31 +0000 Subject: [PATCH 16/21] Add enabled_logs toplevel config --- sentry-ruby/lib/sentry-ruby.rb | 4 ++-- sentry-ruby/lib/sentry/configuration.rb | 5 +++++ sentry-ruby/spec/sentry/structured_logger_spec.rb | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index a2aefe6f0..bec2de4af 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -102,7 +102,7 @@ def exception_locals_tp # @example Enable logs in configuration # Sentry.init do |config| # config.dsn = "YOUR_DSN" - # config._experiments = { enable_logs: true } + # config.enable_logs = true # end # # @example Basic usage @@ -264,7 +264,7 @@ def init(&block) # Initialize the public-facing Structured Logger if logs are enabled # This creates a StructuredLogger instance that implements Sentry's SDK telemetry logs protocol # @see https://develop.sentry.dev/sdk/telemetry/logs/ - @logger = StructuredLogger.new(config) if config._experiments[:enable_logs] + @logger = StructuredLogger.new(config) if config.enable_logs config.detect_release apply_patches(config) diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 171577cd3..710a39562 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -276,6 +276,10 @@ def logger # @return [Proc] attr_accessor :traces_sampler + # Enable Structured Logging + # @return [Boolean] + attr_accessor :enable_logs + # Easier way to use performance tracing # If set to true, will set traces_sample_rate to 1.0 # @deprecated It will be removed in the next major release. @@ -464,6 +468,7 @@ def initialize self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT self.traces_sampler = nil self.enable_tracing = nil + self.enable_logs = true self.profiler_class = Sentry::Profiler diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index 70bfce532..c5bbcd3a6 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -17,7 +17,7 @@ before do perform_basic_setup do |config| config.max_log_events = 1 - config._experiments = { enable_logs: true } + config.enable_logs = true end end From 13863bce7ffb95adfb935124acd8342781608504 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 13 May 2025 09:56:27 +0000 Subject: [PATCH 17/21] Rework logger init to work with enabled_logs config --- sentry-ruby/lib/sentry-ruby.rb | 71 ++++++++++--------- sentry-ruby/lib/sentry/configuration.rb | 2 +- .../spec/sentry/structured_logger_spec.rb | 10 +-- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index bec2de4af..8d3f0578e 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -95,27 +95,6 @@ def exception_locals_tp # @return [Metrics::Aggregator, nil] attr_reader :metrics_aggregator - # @!attribute [r] logger - # Returns the structured logger instance that implements Sentry's SDK telemetry logs protocol. - # This logger is only available when logs are enabled in the configuration. - # - # @example Enable logs in configuration - # Sentry.init do |config| - # config.dsn = "YOUR_DSN" - # config.enable_logs = true - # end - # - # @example Basic usage - # Sentry.logger.info("User logged in successfully", user_id: 123) - # Sentry.logger.error("Failed to process payment", - # transaction_id: "tx_123", - # error_code: "PAYMENT_FAILED" - # ) - # - # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol - # @return [StructuredLogger, nil] The structured logger instance or nil if logs are disabled - attr_reader :logger - ##### Patch Registration ##### # @!visibility private @@ -261,11 +240,6 @@ def init(&block) config = Configuration.new yield(config) if block_given? - # Initialize the public-facing Structured Logger if logs are enabled - # This creates a StructuredLogger instance that implements Sentry's SDK telemetry logs protocol - # @see https://develop.sentry.dev/sdk/telemetry/logs/ - @logger = StructuredLogger.new(config) if config.enable_logs - config.detect_release apply_patches(config) config.validate @@ -640,18 +614,45 @@ def continue_trace(env, **options) get_current_hub.continue_trace(env, **options) end - ##### Helpers ##### - - # @!visibility private + # Returns the structured logger instance that implements Sentry's SDK telemetry logs protocol. + # + # This logger is only available when logs are enabled in the configuration. + # + # @example Enable logs in configuration + # Sentry.init do |config| + # config.dsn = "YOUR_DSN" + # config.enable_logs = true + # end + # + # @example Basic usage + # Sentry.logger.info("User logged in successfully", user_id: 123) + # Sentry.logger.error("Failed to process payment", + # transaction_id: "tx_123", + # error_code: "PAYMENT_FAILED" + # ) + # + # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol + # + # @return [StructuredLogger, nil] The structured logger instance or nil if logs are disabled def logger - warn <<~STR - [sentry] `Sentry.logger` will no longer be used as internal SDK logger when `enable_logs` feature is turned on. - Use Sentry.configuration.sdk_logger for SDK-specific logging needs." - STR - - configuration.sdk_logger + @logger ||= + if configuration.enable_logs + # Initialize the public-facing Structured Logger if logs are enabled + # This creates a StructuredLogger instance that implements Sentry's SDK telemetry logs protocol + # @see https://develop.sentry.dev/sdk/telemetry/logs/ + StructuredLogger.new(configuration) + else + warn <<~STR + [sentry] `Sentry.logger` will no longer be used as internal SDK logger when `enable_logs` feature is turned on. + Use Sentry.configuration.sdk_logger for SDK-specific logging needs." + STR + + configuration.sdk_logger + end end + ##### Helpers ##### + # @!visibility private def sys_command(command) result = `#{command} 2>&1` rescue nil diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 710a39562..856187dcc 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -468,7 +468,7 @@ def initialize self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT self.traces_sampler = nil self.enable_tracing = nil - self.enable_logs = true + self.enable_logs = false self.profiler_class = Sentry::Profiler diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index c5bbcd3a6..a3a2a9af0 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -3,13 +3,15 @@ require "spec_helper" RSpec.describe Sentry::StructuredLogger do - context "when log events are not enabled" do + context "when enable_logs is set to false" do before do - perform_basic_setup + perform_basic_setup do |config| + config.enable_logs = false + end end - it "logger is not set up" do - expect(Sentry.logger).to be_nil + it "configures default SDK logger" do + expect(Sentry.logger).to be(Sentry.configuration.sdk_logger) end end From d879a8b00e1a627f01692f2b6399ea8939b4020f Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 13 May 2025 10:00:37 +0000 Subject: [PATCH 18/21] Update CHANGELOG --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 933a8cc71..bddb6b68e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,18 @@ 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/2617)) +- Add new `Sentry.logger` for [Structured Logging](https://develop.sentry.dev/sdk/telemetry/logs/) feature ([#2620](https://github.com/getsentry/sentry-ruby/pull/2620)). + + To enable structured logging you need to turn on the `enable_logs` configuration option: + ```ruby + Sentry.init do |config| + # ... your setup ... + config.enable_logs = true + end + ``` + :warning: When this is enabled, previous `Sentry.logger` should no longer be used for internal SDK + logging - it was replaced by `Sentry.configuration.sdk_logger` and should be used only by the SDK + itself and its extensions. - New configuration option called `active_job_report_on_retry_error` which enables reporting errors on each retry error ([#2617](https://github.com/getsentry/sentry-ruby/pull/2617)) ### Bug Fixes From 7a96c56d500bb2c1d9b269fb1ed59114f9f1a334 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 13 May 2025 12:53:34 +0000 Subject: [PATCH 19/21] Remove unused Device logging class --- sentry-ruby/lib/sentry/logging/device.rb | 48 ------------------------ 1 file changed, 48 deletions(-) delete mode 100644 sentry-ruby/lib/sentry/logging/device.rb diff --git a/sentry-ruby/lib/sentry/logging/device.rb b/sentry-ruby/lib/sentry/logging/device.rb deleted file mode 100644 index bab0f7116..000000000 --- a/sentry-ruby/lib/sentry/logging/device.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -module Sentry - module Logging - class Device - attr_reader :handlers - - def initialize(options) - @handlers = options.fetch(:handlers) - end - - def trace(message, payload = {}) - log(:trace, message, payload) - end - - def debug(message, payload = {}) - log(:debug, message, payload) - end - - def info(message, payload = {}) - log(:info, message, payload) - end - - def warn(message, payload = {}) - log(:warn, message, payload) - end - - def error(message, payload = {}) - log(:error, message, payload) - end - - def fatal(message, payload = {}) - log(:fatal, message, payload) - end - - def log(level, message, payload) - handlers.each do |handler| - case handler - when Sentry::Logger - handler.public_send(level, message) - else - handler.public_send(level, message, payload) - end - end - end - end - end -end From c41de52a4d9f244daed9ad44d72c6fdbcdbbcc1f Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 13 May 2025 13:07:35 +0000 Subject: [PATCH 20/21] More examples in CHANGELOG --- CHANGELOG.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bddb6b68e..87f26a396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,38 @@ config.enable_logs = true end ``` - :warning: When this is enabled, previous `Sentry.logger` should no longer be used for internal SDK + + Once you configured structured logging, you get access to a new `Sentry.logger` object that can be + used as a regular logger with additional structured data support: + + ```ruby + Sentry.logger.info("User logged in", user_id: 123) + + Sentry.logger.error("Failed to process payment", + transaction_id: "tx_123", + error_code: "PAYMENT_FAILED" + ) + ``` + + You can also use message templates with positional or hash parameters: + + ```ruby + Sentry.logger.info("User %{name} logged in", name: "Jane Doe") + + Sentry.logger.info("User %s logged in", ["Jane Doe"]) + ``` + + Any other arbitrary attributes will be sent as part of the log event payload: + + ```ruby + # Here `user_id` and `action` will be sent as extra attributes that Sentry Logs UI displays + Sentry.logger.info("User %{user} logged in", user: "Jane", user_id: 123, action: "create") + ``` + + :warning: When `enable_logs` is `true`, previous `Sentry.logger` should no longer be used for internal SDK logging - it was replaced by `Sentry.configuration.sdk_logger` and should be used only by the SDK itself and its extensions. + - New configuration option called `active_job_report_on_retry_error` which enables reporting errors on each retry error ([#2617](https://github.com/getsentry/sentry-ruby/pull/2617)) ### Bug Fixes From 8514a252f1dcb747cf00c05f094a203ec02c0ed3 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 13 May 2025 14:12:21 +0000 Subject: [PATCH 21/21] Fix parameter naming in log event attributes --- sentry-ruby/lib/sentry/log_event.rb | 4 ++-- sentry-ruby/spec/sentry/log_event_spec.rb | 6 +++--- .../spec/sentry/structured_logger_spec.rb | 16 ++++++++-------- sentry-ruby/spec/sentry/transport_spec.rb | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 799d90c12..a66828531 100644 --- a/sentry-ruby/lib/sentry/log_event.rb +++ b/sentry-ruby/lib/sentry/log_event.rb @@ -136,11 +136,11 @@ def parameters if parameters.is_a?(Hash) parameters.each do |key, value| - attributes["sentry.message.parameters.#{key}"] = value + attributes["sentry.message.parameter.#{key}"] = value end else parameters.each_with_index do |param, index| - attributes["sentry.message.parameters.#{index}"] = param + attributes["sentry.message.parameter.#{index}"] = param end end end diff --git a/sentry-ruby/spec/sentry/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index 78cf931bd..b818835b9 100644 --- a/sentry-ruby/spec/sentry/log_event_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_spec.rb @@ -25,7 +25,7 @@ it "accepts attributes" do attributes = { "sentry.message.template" => "User %s has logged in!", - "sentry.message.parameters.0" => "John" + "sentry.message.parameter.0" => "John" } event = described_class.new( @@ -62,8 +62,8 @@ attributes = hash[:attributes] expect(attributes["sentry.message.template"]).to eq({ value: "Hello %{name}, today is %{day}", type: "string" }) - expect(attributes["sentry.message.parameters.name"]).to eq({ value: "John", type: "string" }) - expect(attributes["sentry.message.parameters.day"]).to eq({ value: "Monday", type: "string" }) + expect(attributes["sentry.message.parameter.name"]).to eq({ value: "John", type: "string" }) + expect(attributes["sentry.message.parameter.day"]).to eq({ value: "Monday", type: "string" }) end it "includes all required fields" do diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index a3a2a9af0..19d7ea3fa 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -50,8 +50,8 @@ expect(log_event[:level]).to eql(level) expect(log_event[:body]).to eql("Hello Jane it is Monday") expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.0"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.1"]).to eql({ value: "Monday", type: "string" }) end it "logs with template parameters and extra attributres" do @@ -65,8 +65,8 @@ expect(log_event[:body]).to eql("Hello Jane it is Monday") expect(log_event[:attributes][:extra]).to eql({ value: 312, type: "integer" }) expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %s it is %s", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.0"]).to eql({ value: "Jane", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.1"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.0"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.1"]).to eql({ value: "Monday", type: "string" }) end it "logs with hash-based template parameters" do @@ -79,8 +79,8 @@ expect(log_event[:level]).to eql(level) expect(log_event[:body]).to eql("Hello Jane, it is Monday") expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.name"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.day"]).to eql({ value: "Monday", type: "string" }) end it "logs with hash-based template parameters and extra attributes" do @@ -94,8 +94,8 @@ expect(log_event[:body]).to eql("Hello Jane, it is Monday") expect(log_event[:attributes][:user_id]).to eql({ value: 123, type: "integer" }) expect(log_event[:attributes]["sentry.message.template"]).to eql({ value: "Hello %{name}, it is %{day}", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.name"]).to eql({ value: "Jane", type: "string" }) - expect(log_event[:attributes]["sentry.message.parameters.day"]).to eql({ value: "Monday", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.name"]).to eql({ value: "Jane", type: "string" }) + expect(log_event[:attributes]["sentry.message.parameter.day"]).to eql({ value: "Monday", type: "string" }) end end end diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 2ea17d559..2b4810deb 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -294,7 +294,7 @@ 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.message.parameter.0" => { "value" => "John", "type" => "string" }, "sentry.environment" => { "value" => "development", "type" => "string" } ) end @@ -577,7 +577,7 @@ timestamp: 1544719860.0, attributes: { "sentry.message.template" => "User %s has logged in!", - "sentry.message.parameters.0" => "John", + "sentry.message.parameter.0" => "John", "sentry.environment" => "production", "sentry.release" => "1.0.0", "sentry.trace.parent_span_id" => "b0e6f15b45c36b12"