Skip to content

Commit f5f5b91

Browse files
authored
feat(client-reports): Record log_byte and trace_metric_byte outcomes (#2997)
1 parent 664eeb1 commit f5f5b91

10 files changed

Lines changed: 185 additions & 10 deletions

File tree

sentry-ruby/lib/sentry/client.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,13 @@ def send_envelope(envelope)
304304
rescue => e
305305
log_error("Envelope sending failed", e, debug: configuration.debug)
306306

307-
envelope.items.map(&:data_category).each do |data_category|
308-
transport.record_lost_event(:network_error, data_category)
307+
envelope.items.each do |item|
308+
transport.record_lost_event(
309+
:network_error,
310+
item.data_category,
311+
num: item.item_count,
312+
num_bytes: item.lost_event_byte_size
313+
)
309314
end
310315

311316
raise

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def self.data_category(type)
2525
end
2626
end
2727

28+
def self.byte_data_category(data_category)
29+
case data_category
30+
when "log_item" then "log_byte"
31+
when "trace_metric" then "trace_metric_byte"
32+
end
33+
end
34+
2835
def initialize(headers, payload)
2936
@headers = headers
3037
@payload = payload
@@ -33,6 +40,20 @@ def initialize(headers, payload)
3340
@size_limit = SIZE_LIMITS[type]
3441
end
3542

43+
def byte_data_category
44+
self.class.byte_data_category(data_category)
45+
end
46+
47+
def item_count
48+
headers[:item_count] || 1
49+
end
50+
51+
def lost_event_byte_size
52+
return unless byte_data_category
53+
54+
(payload.is_a?(String) ? payload : JSON.generate(payload)).bytesize
55+
end
56+
3657
def to_s
3758
[JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
3859
end

sentry-ruby/lib/sentry/telemetry_event_buffer.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require "json"
34
require "sentry/threaded_periodic_worker"
45
require "sentry/envelope"
56

@@ -58,7 +59,11 @@ def add_item(item)
5859

5960
if size >= @max_items_before_drop
6061
log_debug("[#{self.class}] exceeded max capacity, dropping event")
61-
@client.transport.record_lost_event(:queue_overflow, @data_category)
62+
@client.transport.record_lost_event(
63+
:queue_overflow,
64+
@data_category,
65+
num_bytes: JSON.generate(item.to_h).bytesize
66+
)
6267
else
6368
@pending_items << item
6469
end
@@ -92,6 +97,7 @@ def send_items
9297
)
9398

9499
discarded_count = 0
100+
discarded_bytes = 0
95101
envelope_items = []
96102

97103
if @before_send
@@ -102,14 +108,15 @@ def send_items
102108
envelope_items << processed_item.to_h
103109
else
104110
discarded_count += 1
111+
discarded_bytes += JSON.generate(item.to_h).bytesize
105112
end
106113
end
107114
else
108115
envelope_items = @pending_items.map(&:to_h)
109116
end
110117

111118
unless discarded_count.zero?
112-
@client.transport.record_lost_event(:before_send, @data_category, num: discarded_count)
119+
@client.transport.record_lost_event(:before_send, @data_category, num: discarded_count, num_bytes: discarded_bytes)
113120
end
114121

115122
return if envelope_items.empty?

sentry-ruby/lib/sentry/transport.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def send_envelope(envelope)
6464
end
6565
rescue Sentry::SizeExceededError
6666
serialized_items&.each do |item|
67-
record_lost_event(:send_error, item.data_category)
67+
record_lost_event(:send_error, item.data_category, num: item.item_count, num_bytes: item.lost_event_byte_size)
6868
end
6969
end
7070

@@ -167,11 +167,16 @@ def envelope_from_event(event)
167167
envelope
168168
end
169169

170-
def record_lost_event(reason, data_category, num: 1)
170+
def record_lost_event(reason, data_category, num: 1, num_bytes: nil)
171171
return unless @send_client_reports
172172
return unless CLIENT_REPORT_REASONS.include?(reason)
173173

174174
@discarded_events[[reason, data_category]] += num
175+
176+
return unless num_bytes
177+
178+
byte_category = Envelope::Item.byte_data_category(data_category)
179+
@discarded_events[[reason, byte_category]] += num_bytes if byte_category
175180
end
176181

177182
def flush
@@ -211,7 +216,13 @@ def reject_rate_limited_items(envelope)
211216
envelope.items.reject! do |item|
212217
if is_rate_limited?(item.data_category)
213218
log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
214-
record_lost_event(:ratelimit_backoff, item.data_category)
219+
220+
record_lost_event(
221+
:ratelimit_backoff,
222+
item.data_category,
223+
num: item.item_count,
224+
num_bytes: item.lost_event_byte_size
225+
)
215226

216227
true
217228
else

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,50 @@
2121
end
2222
end
2323
end
24+
25+
describe '.byte_data_category' do
26+
[
27+
['log_item', 'log_byte'],
28+
['trace_metric', 'trace_metric_byte'],
29+
['error', nil],
30+
['transaction', nil],
31+
['default', nil]
32+
].each do |data_category, byte_category|
33+
it "maps data category #{data_category} to byte category #{byte_category.inspect}" do
34+
expect(described_class.byte_data_category(data_category)).to eq(byte_category)
35+
end
36+
end
37+
end
38+
39+
describe '#item_count' do
40+
it "returns the item_count header when present" do
41+
item = described_class.new({ type: "log", item_count: 5 }, { items: [] })
42+
expect(item.item_count).to eq(5)
43+
end
44+
45+
it "defaults to 1 when the header is absent" do
46+
item = described_class.new({ type: "event" }, {})
47+
expect(item.item_count).to eq(1)
48+
end
49+
end
50+
51+
describe '#lost_event_byte_size' do
52+
it "returns the serialized payload byte size for byte-tracked items" do
53+
payload = { items: [{ body: "hello" }] }
54+
item = described_class.new({ type: "log", item_count: 1 }, payload)
55+
expect(item.lost_event_byte_size).to eq(JSON.generate(payload).bytesize)
56+
expect(item.lost_event_byte_size).to be > 0
57+
end
58+
59+
it "uses the payload as-is when it is already serialized" do
60+
payload = JSON.generate({ items: [{ body: "hello" }] })
61+
item = described_class.new({ type: "log", item_count: 1 }, payload)
62+
expect(item.lost_event_byte_size).to eq(payload.bytesize)
63+
end
64+
65+
it "returns nil for items without a byte category" do
66+
item = described_class.new({ type: "event" }, { foo: "bar" })
67+
expect(item.lost_event_byte_size).to be_nil
68+
end
69+
end
2470
end

sentry-ruby/spec/sentry/metrics_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@
313313

314314
expect(sentry_metrics.count).to eq(1)
315315
expect(sentry_metrics.first[:name]).to eq("test.allowed")
316-
expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:before_send, 'trace_metric', num: 2)
316+
expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:before_send, 'trace_metric', num: 2, num_bytes: a_value > 0)
317317
end
318318
end
319319
end

sentry-ruby/spec/sentry/structured_logger_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
expect(sentry_logs.size).to be(1)
242242

243243
expect(transport.discarded_events).to include([:before_send, "log_item"] => 2)
244+
expect(transport.discarded_events[[:before_send, "log_byte"]]).to be > 0
244245
end
245246
end
246247
end

sentry-ruby/spec/sentry/transport_spec.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,83 @@
214214
end
215215
end
216216

217+
context "byte-count client outcomes" do
218+
let(:log_events) do
219+
3.times.map do
220+
Sentry::LogEvent.new(level: :info, body: "User has logged in!")
221+
end
222+
end
223+
224+
let(:metric_events) do
225+
3.times.map do
226+
Sentry::MetricEvent.new(name: "my.metric", type: "counter", value: 1)
227+
end
228+
end
229+
230+
let(:log_envelope) do
231+
envelope = Sentry::Envelope.new
232+
envelope.add_item(
233+
{ type: "log", item_count: log_events.size, content_type: "application/vnd.sentry.items.log+json" },
234+
{ items: log_events.map(&:to_h) }
235+
)
236+
envelope
237+
end
238+
239+
let(:metric_envelope) do
240+
envelope = Sentry::Envelope.new
241+
envelope.add_item(
242+
{ type: "trace_metric", item_count: metric_events.size, content_type: "application/vnd.sentry.items.trace-metric+json" },
243+
{ items: metric_events.map(&:to_h) }
244+
)
245+
envelope
246+
end
247+
248+
describe "#record_lost_event" do
249+
it "fans out into the paired byte category" do
250+
subject.record_lost_event(:ratelimit_backoff, "log_item", num: 3, num_bytes: 1243)
251+
252+
expect(subject.discarded_events[[:ratelimit_backoff, "log_item"]]).to eq(3)
253+
expect(subject.discarded_events[[:ratelimit_backoff, "log_byte"]]).to eq(1243)
254+
end
255+
256+
it "does not record a byte category for non-byte-tracked categories" do
257+
subject.record_lost_event(:ratelimit_backoff, "error", num: 1, num_bytes: 1243)
258+
259+
expect(subject.discarded_events.keys).to contain_exactly([:ratelimit_backoff, "error"])
260+
end
261+
262+
it "does not record bytes when num_bytes is nil" do
263+
subject.record_lost_event(:ratelimit_backoff, "log_item")
264+
265+
expect(subject.discarded_events.keys).to contain_exactly([:ratelimit_backoff, "log_item"])
266+
end
267+
end
268+
269+
context "when a batched log item is rate limited" do
270+
before { subject.rate_limits.merge!("log_item" => Time.now + 60) }
271+
272+
it "records log_item count and log_byte size" do
273+
log_item = log_envelope.items.first
274+
subject.send_envelope(log_envelope)
275+
276+
expect(subject.discarded_events[[:ratelimit_backoff, "log_item"]]).to eq(3)
277+
expect(subject.discarded_events[[:ratelimit_backoff, "log_byte"]]).to eq(JSON.generate(log_item.payload).bytesize)
278+
end
279+
end
280+
281+
context "when a batched trace metric item is rate limited" do
282+
before { subject.rate_limits.merge!("trace_metric" => Time.now + 60) }
283+
284+
it "records trace_metric count and trace_metric_byte size" do
285+
metric_item = metric_envelope.items.first
286+
subject.send_envelope(metric_envelope)
287+
288+
expect(subject.discarded_events[[:ratelimit_backoff, "trace_metric"]]).to eq(3)
289+
expect(subject.discarded_events[[:ratelimit_backoff, "trace_metric_byte"]]).to eq(JSON.generate(metric_item.payload).bytesize)
290+
end
291+
end
292+
end
293+
217294
context "log events" do
218295
let(:log_events) do
219296
5.times.map do |i|

sentry-ruby/spec/spec_helper.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,16 @@
9393
reset_sentry_globals!
9494
end
9595

96-
RSpec::Matchers.define :have_recorded_lost_event do |reason, data_category, num: 1|
96+
RSpec::Matchers.define :have_recorded_lost_event do |reason, data_category, num: 1, num_bytes: nil|
9797
match do |transport|
9898
expect(transport.discarded_events[[reason, data_category]]).to eq(num)
99+
100+
next true unless num_bytes
101+
102+
byte_category = Sentry::Envelope::Item.byte_data_category(data_category)
103+
expect(transport.discarded_events[[reason, byte_category]]).to match(num_bytes)
104+
105+
true
99106
end
100107
end
101108
end

sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
it "records lost event when dropping due to queue overflow" do
110110
max_items_before_drop.times { subject.add_item(event) }
111111

112-
expect(client.transport).to receive(:record_lost_event).with(:queue_overflow, subject.data_category)
112+
expect(client.transport).to receive(:record_lost_event).with(:queue_overflow, subject.data_category, num_bytes: a_value > 0)
113113

114114
subject.add_item(event)
115115
end

0 commit comments

Comments
 (0)