Skip to content

Commit b9fc537

Browse files
authored
Add new Sentry.logger (#2620)
* Add StructuredLogger and re-purpose Sentry.logger * YARD docs for structured logger * Remove obsolete comment * Add support for message templates * Support Hash-based log templates too * Remove redundant assignment * Ensure we don't set message.template when body is not a template * Lazy-load parameters No need to do this upfront because it is needed only when a log event actually gets to the envelope * Reduce object allocations * Use attr readers consistently * Remove unused reader * Update docs * Fix issues with parameters handling and extra attributes * Fix spec * Auto-flush log events when adding and fix flush * Add enabled_logs toplevel config * Rework logger init to work with enabled_logs config * Update CHANGELOG * Remove unused Device logging class * More examples in CHANGELOG * Fix parameter naming in log event attributes
1 parent 63b5162 commit b9fc537

File tree

11 files changed

+483
-85
lines changed

11 files changed

+483
-85
lines changed

CHANGELOG.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,47 @@
1313
config.sidekiq.propagate_traces = false unless Rails.const_defined?('Server')
1414
```
1515
- Only expose `active_storage` keys on span data if `send_default_pii` is on ([#2589](https://github.com/getsentry/sentry-ruby/pull/2589))
16-
- Add `Sentry.capture_log` ([#2606](https://github.com/getsentry/sentry-ruby/pull/2617))
16+
- Add new `Sentry.logger` for [Structured Logging](https://develop.sentry.dev/sdk/telemetry/logs/) feature ([#2620](https://github.com/getsentry/sentry-ruby/pull/2620)).
17+
18+
To enable structured logging you need to turn on the `enable_logs` configuration option:
19+
```ruby
20+
Sentry.init do |config|
21+
# ... your setup ...
22+
config.enable_logs = true
23+
end
24+
```
25+
26+
Once you configured structured logging, you get access to a new `Sentry.logger` object that can be
27+
used as a regular logger with additional structured data support:
28+
29+
```ruby
30+
Sentry.logger.info("User logged in", user_id: 123)
31+
32+
Sentry.logger.error("Failed to process payment",
33+
transaction_id: "tx_123",
34+
error_code: "PAYMENT_FAILED"
35+
)
36+
```
37+
38+
You can also use message templates with positional or hash parameters:
39+
40+
```ruby
41+
Sentry.logger.info("User %{name} logged in", name: "Jane Doe")
42+
43+
Sentry.logger.info("User %s logged in", ["Jane Doe"])
44+
```
45+
46+
Any other arbitrary attributes will be sent as part of the log event payload:
47+
48+
```ruby
49+
# Here `user_id` and `action` will be sent as extra attributes that Sentry Logs UI displays
50+
Sentry.logger.info("User %{user} logged in", user: "Jane", user_id: 123, action: "create")
51+
```
52+
53+
:warning: When `enable_logs` is `true`, previous `Sentry.logger` should no longer be used for internal SDK
54+
logging - it was replaced by `Sentry.configuration.sdk_logger` and should be used only by the SDK
55+
itself and its extensions.
56+
1757
- 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))
1858

1959
### Bug Fixes

sentry-ruby/lib/sentry-ruby.rb

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
require "sentry/utils/encoding_helper"
1212
require "sentry/utils/logging_helper"
1313
require "sentry/configuration"
14-
require "sentry/logger"
14+
require "sentry/structured_logger"
1515
require "sentry/event"
1616
require "sentry/error_event"
1717
require "sentry/transaction_event"
@@ -54,6 +54,7 @@ module Sentry
5454

5555
GLOBALS = %i[
5656
main_hub
57+
logger
5758
session_flusher
5859
backpressure_monitor
5960
metrics_aggregator
@@ -94,11 +95,6 @@ def exception_locals_tp
9495
# @return [Metrics::Aggregator, nil]
9596
attr_reader :metrics_aggregator
9697

97-
# @!attribute [r] logger
98-
# @return [Logger]
99-
# @!visibility private
100-
attr_reader :sdk_logger
101-
10298
##### Patch Registration #####
10399

104100
# @!visibility private
@@ -244,9 +240,6 @@ def init(&block)
244240
config = Configuration.new
245241
yield(config) if block_given?
246242

247-
# Internal SDK logger
248-
@sdk_logger = config.sdk_logger
249-
250243
config.detect_release
251244
apply_patches(config)
252245
config.validate
@@ -499,12 +492,19 @@ def capture_check_in(slug, status, **options)
499492
end
500493

501494
# Captures a log event and sends it to Sentry via the currently active hub.
495+
# This is the underlying method used by the StructuredLogger class.
502496
#
503497
# @param message [String] the log message
504498
# @param [Hash] options Extra log event options
505-
# @option options [Symbol] level The log level
499+
# @option options [Symbol] level The log level (:trace, :debug, :info, :warn, :error, :fatal)
500+
# @option options [Integer] severity The severity number according to the Sentry Logs Protocol
501+
# @option options [Hash] Additional attributes to include with the log
506502
#
507-
# @return [LogEvent, nil]
503+
# @example Direct usage (prefer using Sentry.logger instead)
504+
# Sentry.capture_log("User logged in", level: :info, user_id: 123)
505+
#
506+
# @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol
507+
# @return [LogEvent, nil] The created log event or nil if logging is disabled
508508
def capture_log(message, **options)
509509
return unless initialized?
510510
get_current_hub.capture_log_event(message, **options)
@@ -614,18 +614,45 @@ def continue_trace(env, **options)
614614
get_current_hub.continue_trace(env, **options)
615615
end
616616

617-
##### Helpers #####
618-
619-
# @!visibility private
617+
# Returns the structured logger instance that implements Sentry's SDK telemetry logs protocol.
618+
#
619+
# This logger is only available when logs are enabled in the configuration.
620+
#
621+
# @example Enable logs in configuration
622+
# Sentry.init do |config|
623+
# config.dsn = "YOUR_DSN"
624+
# config.enable_logs = true
625+
# end
626+
#
627+
# @example Basic usage
628+
# Sentry.logger.info("User logged in successfully", user_id: 123)
629+
# Sentry.logger.error("Failed to process payment",
630+
# transaction_id: "tx_123",
631+
# error_code: "PAYMENT_FAILED"
632+
# )
633+
#
634+
# @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol
635+
#
636+
# @return [StructuredLogger, nil] The structured logger instance or nil if logs are disabled
620637
def logger
621-
warn <<~STR
622-
[sentry] `Sentry.logger` will no longer be used as internal SDK logger when `enable_logs` feature is turned on.
623-
Use Sentry.configuration.sdk_logger for SDK-specific logging needs."
624-
STR
625-
626-
configuration.sdk_logger
638+
@logger ||=
639+
if configuration.enable_logs
640+
# Initialize the public-facing Structured Logger if logs are enabled
641+
# This creates a StructuredLogger instance that implements Sentry's SDK telemetry logs protocol
642+
# @see https://develop.sentry.dev/sdk/telemetry/logs/
643+
StructuredLogger.new(configuration)
644+
else
645+
warn <<~STR
646+
[sentry] `Sentry.logger` will no longer be used as internal SDK logger when `enable_logs` feature is turned on.
647+
Use Sentry.configuration.sdk_logger for SDK-specific logging needs."
648+
STR
649+
650+
configuration.sdk_logger
651+
end
627652
end
628653

654+
##### Helpers #####
655+
629656
# @!visibility private
630657
def sys_command(command)
631658
result = `#{command} 2>&1` rescue nil
@@ -634,6 +661,11 @@ def sys_command(command)
634661
result.strip
635662
end
636663

664+
# @!visibility private
665+
def sdk_logger
666+
configuration.sdk_logger
667+
end
668+
637669
# @!visibility private
638670
def sdk_meta
639671
META

sentry-ruby/lib/sentry/client.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class Client
1616
# @return [SpotlightTransport, nil]
1717
attr_reader :spotlight_transport
1818

19+
# @!visibility private
20+
attr_reader :log_event_buffer
21+
1922
# @!macro configuration
2023
attr_reader :configuration
2124

@@ -179,10 +182,17 @@ def event_from_check_in(
179182
end
180183

181184
# Initializes a LogEvent object with the given message and options
185+
#
186+
# @param message [String] the log message
187+
# @param level [Symbol] the log level (:trace, :debug, :info, :warn, :error, :fatal)
188+
# @param options [Hash] additional options
189+
# @option options [Array] :parameters Array of values to replace template tokens in the message
190+
#
191+
# @return [LogEvent] the created log event
182192
def event_from_log(message, level:, **options)
183193
return unless configuration.sending_allowed?
184194

185-
attributes = options.reject { |k, _| k == :level }
195+
attributes = options.reject { |k, _| k == :level || k == :severity }
186196

187197
LogEvent.new(
188198
level: level,

sentry-ruby/lib/sentry/configuration.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require "sentry/metrics/configuration"
1313
require "sentry/linecache"
1414
require "sentry/interfaces/stacktrace_builder"
15+
require "sentry/logger"
1516
require "sentry/log_event_buffer"
1617

1718
module Sentry
@@ -275,6 +276,10 @@ def logger
275276
# @return [Proc]
276277
attr_accessor :traces_sampler
277278

279+
# Enable Structured Logging
280+
# @return [Boolean]
281+
attr_accessor :enable_logs
282+
278283
# Easier way to use performance tracing
279284
# If set to true, will set traces_sample_rate to 1.0
280285
# @deprecated It will be removed in the next major release.
@@ -463,6 +468,7 @@ def initialize
463468
self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
464469
self.traces_sampler = nil
465470
self.enable_tracing = nil
471+
self.enable_logs = false
466472

467473
self.profiler_class = Sentry::Profiler
468474

sentry-ruby/lib/sentry/log_event.rb

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ module Sentry
77
class LogEvent < Event
88
TYPE = "log"
99

10+
DEFAULT_PARAMETERS = [].freeze
11+
DEFAULT_ATTRIBUTES = {}.freeze
12+
DEFAULT_CONTEXT = {}.freeze
13+
1014
SERIALIZEABLE_ATTRIBUTES = %i[
1115
level
1216
body
@@ -21,20 +25,23 @@ class LogEvent < Event
2125
"sentry.release" => :release,
2226
"sentry.address" => :server_name,
2327
"sentry.sdk.name" => :sdk_name,
24-
"sentry.sdk.version" => :sdk_version
28+
"sentry.sdk.version" => :sdk_version,
29+
"sentry.message.template" => :template
2530
}
2631

2732
LEVELS = %i[trace debug info warn error fatal].freeze
2833

29-
attr_accessor :level, :body, :attributes, :trace_id
34+
attr_accessor :level, :body, :template, :attributes
3035

3136
def initialize(configuration: Sentry.configuration, **options)
3237
super(configuration: configuration)
38+
3339
@type = TYPE
3440
@level = options.fetch(:level)
3541
@body = options[:body]
36-
@attributes = options[:attributes] || {}
37-
@contexts = {}
42+
@template = @body if is_template?
43+
@attributes = options[:attributes] || DEFAULT_ATTRIBUTES
44+
@contexts = DEFAULT_CONTEXT
3845
end
3946

4047
def to_hash
@@ -72,15 +79,25 @@ def serialize_timestamp
7279
end
7380

7481
def serialize_trace_id
75-
@contexts.dig(:trace, :trace_id)
82+
contexts.dig(:trace, :trace_id)
7683
end
7784

7885
def serialize_parent_span_id
79-
@contexts.dig(:trace, :parent_span_id)
86+
contexts.dig(:trace, :parent_span_id)
87+
end
88+
89+
def serialize_body
90+
if parameters.empty?
91+
body
92+
elsif parameters.is_a?(Hash)
93+
body % parameters
94+
else
95+
sprintf(body, *parameters)
96+
end
8097
end
8198

8299
def serialize_attributes
83-
hash = @attributes.each_with_object({}) do |(key, value), memo|
100+
hash = attributes.each_with_object({}) do |(key, value), memo|
84101
memo[key] = attribute_hash(value)
85102
end
86103

@@ -109,5 +126,34 @@ def value_type(value)
109126
"string"
110127
end
111128
end
129+
130+
def parameters
131+
@parameters ||= begin
132+
return DEFAULT_PARAMETERS unless template
133+
134+
parameters = template_tokens.empty? ?
135+
attributes.fetch(:parameters, DEFAULT_PARAMETERS) : attributes.slice(*template_tokens)
136+
137+
if parameters.is_a?(Hash)
138+
parameters.each do |key, value|
139+
attributes["sentry.message.parameter.#{key}"] = value
140+
end
141+
else
142+
parameters.each_with_index do |param, index|
143+
attributes["sentry.message.parameter.#{index}"] = param
144+
end
145+
end
146+
end
147+
end
148+
149+
TOKEN_REGEXP = /%\{(\w+)\}/
150+
151+
def template_tokens
152+
@template_tokens ||= body.scan(TOKEN_REGEXP).flatten.map(&:to_sym)
153+
end
154+
155+
def is_template?
156+
body.include?("%s") || TOKEN_REGEXP.match?(body)
157+
end
112158
end
113159
end

sentry-ruby/lib/sentry/log_event_buffer.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@
33
require "sentry/threaded_periodic_worker"
44

55
module Sentry
6+
# LogEventBuffer buffers log events and sends them to Sentry in a single envelope.
7+
#
8+
# This is used internally by the `Sentry::Client`.
9+
#
10+
# @!visibility private
611
class LogEventBuffer < ThreadedPeriodicWorker
712
FLUSH_INTERVAL = 5 # seconds
813
DEFAULT_MAX_EVENTS = 100
914

15+
# @!visibility private
16+
attr_reader :pending_events
17+
1018
def initialize(configuration, client)
1119
super(configuration.sdk_logger, FLUSH_INTERVAL)
1220

@@ -27,13 +35,11 @@ def start
2735

2836
def flush
2937
@mutex.synchronize do
30-
return unless size >= @max_events
38+
return if empty?
3139

3240
log_debug("[LogEventBuffer] flushing #{size} log events")
3341

34-
@client.send_envelope(to_envelope)
35-
36-
@pending_events.clear
42+
send_events
3743
end
3844

3945
log_debug("[LogEventBuffer] flushed #{size} log events")
@@ -47,6 +53,7 @@ def add_event(event)
4753

4854
@mutex.synchronize do
4955
@pending_events << event
56+
send_events if size >= @max_events
5057
end
5158

5259
self
@@ -62,6 +69,11 @@ def size
6269

6370
private
6471

72+
def send_events
73+
@client.send_envelope(to_envelope)
74+
@pending_events.clear
75+
end
76+
6577
def to_envelope
6678
envelope = Envelope.new(
6779
event_id: SecureRandom.uuid.delete("-"),

0 commit comments

Comments
 (0)