diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ead46a24..e8ddfb3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Features + +- Support for `before_send_log` ([#2634](https://github.com/getsentry/sentry-ruby/pull/2634)) + ## 5.24.0 ### Features diff --git a/sentry-ruby/lib/sentry/client.rb b/sentry-ruby/lib/sentry/client.rb index fd5db6ba8..9e1b93098 100644 --- a/sentry-ruby/lib/sentry/client.rb +++ b/sentry-ruby/lib/sentry/client.rb @@ -3,6 +3,7 @@ require "sentry/transport" require "sentry/log_event" require "sentry/log_event_buffer" +require "sentry/utils/uuid" module Sentry class Client @@ -272,6 +273,53 @@ def send_event(event, hint = nil) raise end + # Send an envelope with batched logs + # @param log_events [Array] the log events to be sent + # @api private + # @return [void] + def send_logs(log_events) + envelope = Envelope.new( + event_id: Sentry::Utils.uuid, + sent_at: Sentry.utc_now.iso8601, + dsn: configuration.dsn, + sdk: Sentry.sdk_meta + ) + + discarded_count = 0 + envelope_items = [] + + if configuration.before_send_log + log_events.each do |log_event| + processed_log_event = configuration.before_send_log.call(log_event) + + if processed_log_event + envelope_items << processed_log_event.to_hash + else + discarded_count += 1 + end + end + + envelope_items + else + envelope_items = log_events.map(&:to_hash) + end + + envelope.add_item( + { + type: "log", + item_count: envelope_items.size, + content_type: "application/vnd.sentry.items.log+json" + }, + { items: envelope_items } + ) + + send_envelope(envelope) + + unless discarded_count.zero? + transport.record_lost_event(:before_send, "log_item", num: discarded_count) + end + end + # Send an envelope directly to Sentry. # @param envelope [Envelope] the envelope to be sent. # @return [void] diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 856187dcc..0a0f7236d 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -101,6 +101,15 @@ class Configuration # @return [Proc] attr_reader :before_send_transaction + # Optional Proc, called before sending an event to the server + # @example + # config.before_send_log = lambda do |log| + # log.attributes["sentry"] = true + # log + # end + # @return [Proc] + attr_accessor :before_send_log + # An array of breadcrumbs loggers to be used. Available options are: # - :sentry_logger # - :http_logger @@ -465,6 +474,7 @@ def initialize self.before_send = nil self.before_send_transaction = nil + self.before_send_log = nil self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT self.traces_sampler = nil self.enable_tracing = nil diff --git a/sentry-ruby/lib/sentry/log_event_buffer.rb b/sentry-ruby/lib/sentry/log_event_buffer.rb index 48e3fb54f..8134da678 100644 --- a/sentry-ruby/lib/sentry/log_event_buffer.rb +++ b/sentry-ruby/lib/sentry/log_event_buffer.rb @@ -21,8 +21,6 @@ def initialize(configuration, client) @client = client @pending_events = [] @max_events = configuration.max_log_events || DEFAULT_MAX_EVENTS - @dsn = configuration.dsn - @sdk = Sentry.sdk_meta @mutex = Mutex.new log_debug("[Logging] Initialized buffer with max_events=#{@max_events}, flush_interval=#{FLUSH_INTERVAL}s") @@ -70,28 +68,8 @@ def size private def send_events - @client.send_envelope(to_envelope) + @client.send_logs(@pending_events) @pending_events.clear end - - def to_envelope - envelope = Envelope.new( - event_id: SecureRandom.uuid.delete("-"), - sent_at: Sentry.utc_now.iso8601, - dsn: @dsn, - sdk: @sdk - ) - - envelope.add_item( - { - type: "log", - item_count: size, - content_type: "application/vnd.sentry.items.log+json" - }, - { items: @pending_events.map(&:to_hash) } - ) - - envelope - 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 284b49d63..00c0d4eaf 100644 --- a/sentry-ruby/spec/sentry/log_event_buffer_spec.rb +++ b/sentry-ruby/spec/sentry/log_event_buffer_spec.rb @@ -42,13 +42,13 @@ end it "does nothing when the number of events is less than max_events " do - expect(client).to_not receive(:send_envelope) + expect(client).to_not receive(:send_logs) 2.times { log_event_buffer.add_event(log_event) } end it "auto-flushes pending events to the client when the number of events reaches max_events" do - expect(client).to receive(:send_envelope) + expect(client).to receive(:send_logs) 3.times { log_event_buffer.add_event(log_event) } @@ -60,7 +60,7 @@ let(:max_log_events) { 30 } it "thread-safely handles concurrent access" do - expect(client).to receive(:send_envelope).exactly(3).times + expect(client).to receive(:send_logs).exactly(3).times threads = 3.times.map do Thread.new do diff --git a/sentry-ruby/spec/sentry/structured_logger_spec.rb b/sentry-ruby/spec/sentry/structured_logger_spec.rb index 19d7ea3fa..ca135ea49 100644 --- a/sentry-ruby/spec/sentry/structured_logger_spec.rb +++ b/sentry-ruby/spec/sentry/structured_logger_spec.rb @@ -99,5 +99,65 @@ end end end + + describe "using config.before_send_log" do + let(:transport) do + Sentry.get_current_client.transport + end + + before do + perform_basic_setup do |config| + config.enable_logs = true + config.send_client_reports = send_client_reports + config.max_log_events = 1 + config.before_send_log = before_send_log + end + end + + context "when send_client_reports is turned off and the callback returns a log event" do + let(:send_client_reports) { false } + + let(:before_send_log) do + ->(log) { + log.attributes["hello"] = "world" + log + } + end + + it "sends processed log events" do + Sentry.logger.info("Hello World", user_id: 125, action: "create") + Sentry.logger.info("Hello World", user_id: 123, action: "create") + Sentry.logger.info("Hello World", user_id: 127, action: "create") + + expect(sentry_logs.size).to be(3) + + log_event = sentry_logs.last + + expect(log_event[:attributes]["hello"]).to eql({ value: "world", type: "string" }) + end + end + + context "when send_client_reports is turned on and the callback returns a log event" do + let(:send_client_reports) { true } + + let(:before_send_log) do + ->(log) { + if log.attributes[:user_id] == 123 + log + end + } + end + + it "records discarded events" do + Sentry.logger.info("Hello World", user_id: 125, action: "create") + Sentry.logger.info("Hello World", user_id: 123, action: "create") + Sentry.logger.info("Hello World", user_id: 127, action: "create") + + expect(sentry_logs.size).to be(1) + + expect(transport.discarded_events).to include([:before_send, "log_item"] => 2) + end + end + end end end