diff --git a/CHANGELOG.md b/CHANGELOG.md index 933a8cc71..87f26a396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,47 @@ 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 + ``` + + 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 diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index e3fff2a62..8d3f0578e 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 @@ -94,11 +95,6 @@ def exception_locals_tp # @return [Metrics::Aggregator, nil] attr_reader :metrics_aggregator - # @!attribute [r] logger - # @return [Logger] - # @!visibility private - attr_reader :sdk_logger - ##### Patch Registration ##### # @!visibility private @@ -244,9 +240,6 @@ def init(&block) config = Configuration.new yield(config) if block_given? - # Internal SDK logger - @sdk_logger = config.sdk_logger - config.detect_release apply_patches(config) config.validate @@ -499,12 +492,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) @@ -614,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 @@ -634,6 +661,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..fd5db6ba8 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 @@ -179,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/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 02df33604..856187dcc 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 @@ -275,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. @@ -463,6 +468,7 @@ def initialize self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT self.traces_sampler = nil self.enable_tracing = nil + self.enable_logs = false self.profiler_class = Sentry::Profiler diff --git a/sentry-ruby/lib/sentry/log_event.rb b/sentry-ruby/lib/sentry/log_event.rb index 52160a1a6..a66828531 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 @@ -21,20 +25,23 @@ 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 def initialize(configuration: Sentry.configuration, **options) super(configuration: configuration) + @type = TYPE @level = options.fetch(:level) @body = options[:body] - @attributes = options[:attributes] || {} - @contexts = {} + @template = @body if is_template? + @attributes = options[:attributes] || DEFAULT_ATTRIBUTES + @contexts = DEFAULT_CONTEXT end def to_hash @@ -72,15 +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 + elsif parameters.is_a?(Hash) + body % parameters + else + 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 @@ -109,5 +126,34 @@ def value_type(value) "string" end end + + def parameters + @parameters ||= begin + return DEFAULT_PARAMETERS unless template + + parameters = template_tokens.empty? ? + attributes.fetch(:parameters, DEFAULT_PARAMETERS) : attributes.slice(*template_tokens) + + if parameters.is_a?(Hash) + parameters.each do |key, value| + attributes["sentry.message.parameter.#{key}"] = value + end + else + parameters.each_with_index do |param, index| + attributes["sentry.message.parameter.#{index}"] = param + end + end + end + end + + TOKEN_REGEXP = /%\{(\w+)\}/ + + def template_tokens + @template_tokens ||= body.scan(TOKEN_REGEXP).flatten.map(&:to_sym) + end + + def is_template? + body.include?("%s") || TOKEN_REGEXP.match?(body) + end end end diff --git a/sentry-ruby/lib/sentry/log_event_buffer.rb b/sentry-ruby/lib/sentry/log_event_buffer.rb index 6864a6662..48e3fb54f 100644 --- a/sentry-ruby/lib/sentry/log_event_buffer.rb +++ b/sentry-ruby/lib/sentry/log_event_buffer.rb @@ -3,10 +3,18 @@ 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 + # @!visibility private + attr_reader :pending_events + def initialize(configuration, client) super(configuration.sdk_logger, FLUSH_INTERVAL) @@ -27,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") @@ -47,6 +53,7 @@ def add_event(event) @mutex.synchronize do @pending_events << event + send_events if size >= @max_events end self @@ -62,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/lib/sentry/structured_logger.rb b/sentry-ruby/lib/sentry/structured_logger.rb new file mode 100644 index 000000000..79ca8d005 --- /dev/null +++ b/sentry-ruby/lib/sentry/structured_logger.rb @@ -0,0 +1,138 @@ +# 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" + # ) + # + # @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 + # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number + LEVELS = { + 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 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) + end + + # Logs a message at DEBUG level + # + # @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) + end + + # Logs a message at INFO level + # + # @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) + end + + # Logs a message at WARN level + # + # @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) + end + + # Logs a message at ERROR level + # + # @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) + end + + # Logs a message at FATAL level + # + # @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) + 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 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) + 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 +end 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/log_event_spec.rb b/sentry-ruby/spec/sentry/log_event_spec.rb index fdc436abb..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( @@ -46,21 +46,35 @@ configuration.server_name = "server-123" end - it "includes all required fields" do - attributes = { - "sentry.message.template" => "User %s has logged in!", - "sentry.message.parameters.0" => "John" - } + it "formats message with hash-based parameters" do + attributes = { name: "John", day: "Monday" } event = described_class.new( configuration: configuration, level: :info, - body: "User John has logged in!", + 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.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 + event = described_class.new( + configuration: configuration, + level: :info, + body: "User John has logged in!" + ) + + hash = event.to_hash + expect(hash[:level]).to eq("info") expect(hash[:body]).to eq("User John has logged in!") expect(hash[:timestamp]).to be_a(Float) @@ -68,15 +82,22 @@ 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 + 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", 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..19d7ea3fa --- /dev/null +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Sentry::StructuredLogger do + context "when enable_logs is set to false" do + before do + perform_basic_setup do |config| + config.enable_logs = false + end + end + + it "configures default SDK logger" do + expect(Sentry.logger).to be(Sentry.configuration.sdk_logger) + end + end + + context "when log events are enabled" do + before do + perform_basic_setup do |config| + config.max_log_events = 1 + config.enable_logs = true + end + end + + ["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(sentry_logs).to_not be_empty + + log_event = sentry_logs.last + + 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(sentry_logs).to_not be_empty + + log_event = sentry_logs.last + + 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.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 + Sentry.logger.public_send(level, "Hello %s it is %s", ["Jane", "Monday"], extra: 312) + + expect(sentry_logs).to_not be_empty + + log_event = sentry_logs.last + + 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.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 + Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", name: "Jane", day: "Monday") + + expect(sentry_logs).to_not be_empty + + log_event = sentry_logs.last + + 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.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 + Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", name: "Jane", day: "Monday", user_id: 123) + + expect(sentry_logs).to_not be_empty + + log_event = sentry_logs.last + + 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.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 + end +end diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 558963f3c..2b4810deb 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 @@ -298,11 +294,8 @@ 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.message.parameter.0" => { "value" => "John", "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 @@ -584,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"