Skip to content

Commit 6da79bd

Browse files
authored
Introduce Sentry.capture_log (#2606)
* Introduce LogEvent * Support for LogEvent in Transport * Introduce `log` data category in events * Introduce Sentry.capture_log * Update CHANGELOG * Rework LogEvent serialization * Add more info to serialized log events * Use each_with_object for consistency * Grab trace_id from contexts[:trace] * Add parent_span_id * Add parent_span_id and nest sentry info under attributes * Set SDK info under attributes correctly * rubocop * server_name -> address
1 parent 7265f14 commit 6da79bd

File tree

13 files changed

+444
-6
lines changed

13 files changed

+444
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
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/2606))
1617

1718
### Bug Fixes
1819

sentry-ruby/lib/sentry-ruby.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,18 @@ def capture_check_in(slug, status, **options)
489489
get_current_hub.capture_check_in(slug, status, **options)
490490
end
491491

492+
# Captures a log event and sends it to Sentry via the currently active hub.
493+
#
494+
# @param message [String] the log message
495+
# @param [Hash] options Extra log event options
496+
# @option options [Symbol] level The log level
497+
#
498+
# @return [LogEvent, nil]
499+
def capture_log(message, **options)
500+
return unless initialized?
501+
get_current_hub.capture_log_event(message, **options)
502+
end
503+
492504
# Takes or initializes a new Sentry::Transaction and makes a sampling decision for it.
493505
#
494506
# @return [Transaction, nil]

sentry-ruby/lib/sentry/client.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "sentry/transport"
4+
require "sentry/log_event"
45

56
module Sentry
67
class Client
@@ -167,6 +168,20 @@ def event_from_check_in(
167168
)
168169
end
169170

171+
# Initializes a LogEvent object with the given message and options
172+
def event_from_log(message, level:, **options)
173+
return unless configuration.sending_allowed?
174+
175+
attributes = options.reject { |k, _| k == :level }
176+
177+
LogEvent.new(
178+
level: level,
179+
body: message,
180+
timestamp: Time.now.to_f,
181+
attributes: attributes
182+
)
183+
end
184+
170185
# Initializes an Event object with the given Transaction object.
171186
# @param transaction [Transaction] the transaction to be recorded.
172187
# @return [TransactionEvent]

sentry-ruby/lib/sentry/envelope/item.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Envelope::Item
1515
# rate limits and client reports use the data_category rather than envelope item type
1616
def self.data_category(type)
1717
case type
18-
when "session", "attachment", "transaction", "profile", "span" then type
18+
when "session", "attachment", "transaction", "profile", "span", "log" then type
1919
when "sessions" then "session"
2020
when "check_in" then "monitor"
2121
when "statsd", "metric_meta" then "metric_bucket"

sentry-ruby/lib/sentry/hub.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ def capture_check_in(slug, status, **options)
216216
event.check_in_id
217217
end
218218

219+
def capture_log_event(message, **options)
220+
return unless current_client
221+
222+
event = current_client.event_from_log(message, **options)
223+
224+
return unless event
225+
226+
capture_event(event, **options)
227+
end
228+
219229
def capture_event(event, **options, &block)
220230
check_argument_type!(event, Sentry::Event)
221231

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
# Event type that represents a log entry with its attributes
5+
#
6+
# @see https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload
7+
class LogEvent < Event
8+
TYPE = "log"
9+
10+
SERIALIZEABLE_ATTRIBUTES = %i[
11+
level
12+
body
13+
timestamp
14+
trace_id
15+
attributes
16+
]
17+
18+
SENTRY_ATTRIBUTES = {
19+
"sentry.trace.parent_span_id" => :parent_span_id,
20+
"sentry.environment" => :environment,
21+
"sentry.release" => :release,
22+
"sentry.address" => :server_name,
23+
"sentry.sdk.name" => :sdk_name,
24+
"sentry.sdk.version" => :sdk_version
25+
}
26+
27+
LEVELS = %i[trace debug info warn error fatal].freeze
28+
29+
attr_accessor :level, :body, :attributes, :trace_id
30+
31+
def initialize(configuration: Sentry.configuration, **options)
32+
super(configuration: configuration)
33+
@type = TYPE
34+
@level = options.fetch(:level)
35+
@body = options[:body]
36+
@attributes = options[:attributes] || {}
37+
@contexts = {}
38+
end
39+
40+
def to_hash
41+
SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |name, memo|
42+
memo[name] = serialize(name)
43+
end
44+
end
45+
46+
private
47+
48+
def serialize(name)
49+
serializer = :"serialize_#{name}"
50+
51+
if respond_to?(serializer, true)
52+
__send__(serializer)
53+
else
54+
public_send(name)
55+
end
56+
end
57+
58+
def serialize_level
59+
level.to_s
60+
end
61+
62+
def serialize_sdk_name
63+
Sentry.sdk_meta["name"]
64+
end
65+
66+
def serialize_sdk_version
67+
Sentry.sdk_meta["version"]
68+
end
69+
70+
def serialize_timestamp
71+
Time.parse(timestamp).to_f
72+
end
73+
74+
def serialize_trace_id
75+
@contexts.dig(:trace, :trace_id)
76+
end
77+
78+
def serialize_parent_span_id
79+
@contexts.dig(:trace, :parent_span_id)
80+
end
81+
82+
def serialize_attributes
83+
hash = @attributes.each_with_object({}) do |(key, value), memo|
84+
memo[key] = attribute_hash(value)
85+
end
86+
87+
SENTRY_ATTRIBUTES.each do |key, name|
88+
if (value = serialize(name))
89+
hash[key] = attribute_hash(value)
90+
end
91+
end
92+
93+
hash
94+
end
95+
96+
def attribute_hash(value)
97+
{ value: value, type: value_type(value) }
98+
end
99+
100+
def value_type(value)
101+
case value
102+
when Integer
103+
"integer"
104+
when TrueClass, FalseClass
105+
"boolean"
106+
when Float
107+
"double"
108+
else
109+
"string"
110+
end
111+
end
112+
end
113+
end

sentry-ruby/lib/sentry/scope.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def apply_to_event(event, hint = nil)
5454
event.transaction = transaction_name if transaction_name
5555
event.transaction_info = { source: transaction_source } if transaction_source
5656
event.fingerprint = fingerprint
57-
event.level = level
57+
event.level = level unless event.is_a?(LogEvent)
5858
event.breadcrumbs = breadcrumbs
5959
event.rack_env = rack_env if rack_env
6060
event.attachments = attachments

sentry-ruby/lib/sentry/transport.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,21 @@ def envelope_from_event(event)
133133

134134
envelope = Envelope.new(envelope_headers)
135135

136-
envelope.add_item(
137-
{ type: item_type, content_type: "application/json" },
138-
event_payload
139-
)
136+
if event.is_a?(LogEvent)
137+
envelope.add_item(
138+
{
139+
type: "log",
140+
item_count: 1,
141+
content_type: "application/vnd.sentry.items.log+json"
142+
},
143+
{ items: [event_payload] }
144+
)
145+
else
146+
envelope.add_item(
147+
{ type: item_type, content_type: "application/json" },
148+
event_payload
149+
)
150+
end
140151

141152
if event.is_a?(TransactionEvent) && event.profile
142153
envelope.add_item(

sentry-ruby/spec/sentry/client_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def sentry_context
103103
envelope.add_item({ type: 'event' }, { payload: 'test' })
104104
envelope.add_item({ type: 'statsd' }, { payload: 'test2' })
105105
envelope.add_item({ type: 'transaction' }, { payload: 'test3' })
106+
envelope.add_item({ type: 'log' }, { level: 'info', message: 'test4' })
106107
envelope
107108
end
108109

@@ -119,6 +120,15 @@ def sentry_context
119120
subject.send_envelope(envelope)
120121
end
121122

123+
it 'includes log item in the envelope' do
124+
log_item = envelope.items.find { |item| item.type == 'log' }
125+
126+
expect(log_item).not_to be_nil
127+
expect(log_item.payload[:level]).to eq('info')
128+
expect(log_item.payload[:message]).to eq('test4')
129+
expect(log_item.data_category).to eq('log')
130+
end
131+
122132
it 'sends envelope with spotlight transport if enabled' do
123133
configuration.spotlight = true
124134

@@ -153,6 +163,7 @@ def sentry_context
153163
expect(subject.transport).to have_recorded_lost_event(:network_error, 'error')
154164
expect(subject.transport).to have_recorded_lost_event(:network_error, 'metric_bucket')
155165
expect(subject.transport).to have_recorded_lost_event(:network_error, 'transaction')
166+
expect(subject.transport).to have_recorded_lost_event(:network_error, 'log')
156167
end
157168
end
158169
end

sentry-ruby/spec/sentry/envelope/item_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
['transaction', 'transaction'],
1212
['span', 'span'],
1313
['profile', 'profile'],
14+
['log', 'log'],
1415
['check_in', 'monitor'],
1516
['statsd', 'metric_bucket'],
1617
['metric_meta', 'metric_bucket'],

0 commit comments

Comments
 (0)