Skip to content

Commit 45ef9c4

Browse files
committed
feat: support for log_byte and trace_metric_byte
1 parent de6fa74 commit 45ef9c4

15 files changed

Lines changed: 594 additions & 30 deletions

File tree

lib/sentry/client_report/sender.ex

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@ defmodule Sentry.ClientReport.Sender do
66

77
use GenServer
88

9-
alias Sentry.{Client, ClientReport, Config, Envelope, Transaction}
9+
alias Sentry.{
10+
Client,
11+
ClientReport,
12+
Config,
13+
Envelope,
14+
LogBatch,
15+
LogEvent,
16+
Metric,
17+
MetricBatch,
18+
Transaction
19+
}
20+
21+
alias Sentry.Telemetry.Category
1022

1123
@send_interval 30_000
1224

@@ -39,6 +51,10 @@ defmodule Sentry.ClientReport.Sender do
3951
| Sentry.CheckIn.t()
4052
| ClientReport.t()
4153
| Sentry.Event.t()
54+
| LogBatch.t()
55+
| LogEvent.t()
56+
| Metric.t()
57+
| MetricBatch.t()
4258
| Sentry.Transaction.t()
4359
def record_discarded_events(reason, event_items, genserver)
4460
when is_list(event_items) do
@@ -65,6 +81,38 @@ defmodule Sentry.ClientReport.Sender do
6581
[{Envelope.get_data_category(transaction), 1}, {"span", span_count}]
6682
end
6783

84+
defp data_categories(%LogEvent{} = log_event) do
85+
[
86+
{Category.data_category(:log), 1},
87+
{Category.byte_data_category(:log), Envelope.log_event_byte_size(log_event)}
88+
]
89+
end
90+
91+
defp data_categories(%Metric{} = metric) do
92+
[
93+
{Category.data_category(:metric), 1},
94+
{Category.byte_data_category(:metric), Envelope.metric_byte_size(metric)}
95+
]
96+
end
97+
98+
defp data_categories(%LogBatch{log_events: log_events}) do
99+
bytes = Enum.reduce(log_events, 0, &(Envelope.log_event_byte_size(&1) + &2))
100+
101+
[
102+
{Category.data_category(:log), length(log_events)},
103+
{Category.byte_data_category(:log), bytes}
104+
]
105+
end
106+
107+
defp data_categories(%MetricBatch{metrics: metrics}) do
108+
bytes = Enum.reduce(metrics, 0, &(Envelope.metric_byte_size(&1) + &2))
109+
110+
[
111+
{Category.data_category(:metric), length(metrics)},
112+
{Category.byte_data_category(:metric), bytes}
113+
]
114+
end
115+
68116
defp data_categories(item) do
69117
[{Envelope.get_data_category(item), 1}]
70118
end

lib/sentry/envelope.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,46 @@ defmodule Sentry.Envelope do
138138
def get_data_category(%LogBatch{}), do: "log_item"
139139
def get_data_category(%MetricBatch{}), do: "trace_metric"
140140

141+
@doc """
142+
Approximates the serialized byte size of a single log event.
143+
144+
The size is computed by encoding the log event with the same serialization used
145+
when the event is placed in an envelope item (see `Sentry.LogEvent.to_map/1`),
146+
then measuring the byte size of the resulting JSON. Per the client report spec,
147+
a serialized-size approximation like this is acceptable for `log_byte` outcomes.
148+
149+
Returns `0` if the log event cannot be encoded.
150+
"""
151+
@doc since: "13.4.0"
152+
@spec log_event_byte_size(LogEvent.t()) :: non_neg_integer()
153+
def log_event_byte_size(%LogEvent{} = log_event) do
154+
log_event |> LogEvent.to_map() |> item_byte_size()
155+
end
156+
157+
@doc """
158+
Approximates the serialized byte size of a single metric.
159+
160+
The size is computed by encoding the metric with the same serialization used
161+
when the metric is placed in an envelope item (see `Sentry.Metric.to_map/1`),
162+
then measuring the byte size of the resulting JSON. Per the client report spec,
163+
a serialized-size approximation like this is acceptable for `trace_metric_byte`
164+
outcomes.
165+
166+
Returns `0` if the metric cannot be encoded.
167+
"""
168+
@doc since: "13.4.0"
169+
@spec metric_byte_size(Metric.t()) :: non_neg_integer()
170+
def metric_byte_size(%Metric{} = metric) do
171+
metric |> Metric.to_map() |> item_byte_size()
172+
end
173+
174+
defp item_byte_size(map) do
175+
case Sentry.JSON.encode(map, Config.json_library()) do
176+
{:ok, encoded} -> byte_size(encoded)
177+
{:error, _reason} -> 0
178+
end
179+
end
180+
141181
@doc """
142182
Returns the total number of payload items in the envelope.
143183

lib/sentry/logger_handler/logs_backend.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ defmodule Sentry.LoggerHandler.LogsBackend do
3737
log_event_struct = LogEvent.from_logger_event(log_event, attributes, parameters)
3838

3939
case TelemetryProcessor.add(log_event_struct) do
40-
{:ok, {:rate_limited, data_category}} ->
41-
Sentry.ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)
40+
{:ok, {:rate_limited, _data_category}} ->
41+
Sentry.ClientReport.Sender.record_discarded_events(:ratelimit_backoff, [log_event_struct])
4242

4343
:ok ->
4444
:ok

lib/sentry/metrics.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ defmodule Sentry.Metrics do
135135
metric = Metric.attach_default_attributes(metric)
136136

137137
case TelemetryProcessor.add(metric) do
138-
{:ok, {:rate_limited, data_category}} ->
139-
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)
138+
{:ok, {:rate_limited, _data_category}} ->
139+
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, [metric])
140140

141141
:ok ->
142142
:ok

lib/sentry/telemetry/buffer.ex

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule Sentry.Telemetry.Buffer do
2424

2525
alias Sentry.ClientReport
2626
alias Sentry.Telemetry.Category
27+
alias Sentry.{LogEvent, Metric}
2728

2829
@enforce_keys [:category, :capacity, :batch_size]
2930
defstruct [
@@ -178,12 +179,9 @@ defmodule Sentry.Telemetry.Buffer do
178179

179180
defp offer(%Buffer{size: size, capacity: capacity} = state, item)
180181
when size >= capacity do
181-
{{:value, _dropped}, items} = :queue.out(state.items)
182+
{{:value, dropped}, items} = :queue.out(state.items)
182183

183-
ClientReport.Sender.record_discarded_events(
184-
:cache_overflow,
185-
Category.data_category(state.category)
186-
)
184+
record_overflow_discard(state.category, dropped)
187185

188186
%{state | items: :queue.in(item, items)}
189187
end
@@ -192,6 +190,24 @@ defmodule Sentry.Telemetry.Buffer do
192190
%{state | items: :queue.in(item, state.items), size: state.size + 1}
193191
end
194192

193+
# Log and metric structs go through the item-based recorder for their byte
194+
# outcome; other categories fall back to the category string because the
195+
# buffer is generic and the item-based recorder raises on unknown items.
196+
defp record_overflow_discard(:log, %LogEvent{} = dropped) do
197+
ClientReport.Sender.record_discarded_events(:cache_overflow, [dropped])
198+
end
199+
200+
defp record_overflow_discard(:metric, %Metric{} = dropped) do
201+
ClientReport.Sender.record_discarded_events(:cache_overflow, [dropped])
202+
end
203+
204+
defp record_overflow_discard(category, _dropped) do
205+
ClientReport.Sender.record_discarded_events(
206+
:cache_overflow,
207+
Category.data_category(category)
208+
)
209+
end
210+
195211
defp poll_batch(state, count), do: poll_batch(state, count, [])
196212
defp poll_batch(state, 0, acc), do: {Enum.reverse(acc), state}
197213
defp poll_batch(%{size: 0} = state, _count, acc), do: {Enum.reverse(acc), state}

lib/sentry/telemetry/category.ex

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,75 @@ defmodule Sentry.Telemetry.Category do
186186
def data_category(:transaction), do: "transaction"
187187
def data_category(:log), do: "log_item"
188188
def data_category(:metric), do: "trace_metric"
189+
190+
@doc """
191+
Returns the byte-based Sentry data category string for a given telemetry category.
192+
193+
Some data categories have a companion "byte" category used to report the total
194+
serialized size of dropped items in client reports and to honor byte-based rate
195+
limits. Only `:log` and `:metric` currently have such companion categories.
196+
197+
These strings are used in client reports and rate limiting alongside the
198+
count-based category returned by `data_category/1`.
199+
200+
## Examples
201+
202+
iex> Sentry.Telemetry.Category.byte_data_category(:log)
203+
"log_byte"
204+
205+
iex> Sentry.Telemetry.Category.byte_data_category(:metric)
206+
"trace_metric_byte"
207+
208+
"""
209+
@spec byte_data_category(:log | :metric) :: String.t()
210+
def byte_data_category(:log), do: "log_byte"
211+
def byte_data_category(:metric), do: "trace_metric_byte"
212+
213+
@doc """
214+
Returns every rate-limit data category that gates the given count-based data
215+
category, including any companion byte category.
216+
217+
Logs and metrics have a companion byte category (`log_byte` /
218+
`trace_metric_byte`) that Sentry can rate-limit independently, so an active
219+
limit on either the count or the byte category must suppress sending. All
220+
other categories gate on themselves only.
221+
222+
These strings are matched against the limits stored from the
223+
`X-Sentry-Rate-Limits` response header.
224+
225+
## Examples
226+
227+
iex> Sentry.Telemetry.Category.rate_limit_categories("log_item")
228+
["log_item", "log_byte"]
229+
230+
iex> Sentry.Telemetry.Category.rate_limit_categories("trace_metric")
231+
["trace_metric", "trace_metric_byte"]
232+
233+
iex> Sentry.Telemetry.Category.rate_limit_categories("error")
234+
["error"]
235+
236+
"""
237+
@spec rate_limit_categories(String.t()) :: [String.t(), ...]
238+
def rate_limit_categories("log_item"), do: ["log_item", "log_byte"]
239+
def rate_limit_categories("trace_metric"), do: ["trace_metric", "trace_metric_byte"]
240+
def rate_limit_categories(category) when is_binary(category), do: [category]
241+
242+
@doc """
243+
Returns all Sentry data category strings recognized by the SDK.
244+
245+
This includes the count-based categories returned by `data_category/1` as well
246+
as the byte-based categories returned by `byte_data_category/1`. Any category
247+
outside this set is unknown to the SDK.
248+
249+
## Examples
250+
251+
iex> categories = Sentry.Telemetry.Category.data_categories()
252+
iex> "log_byte" in categories and "trace_metric_byte" in categories
253+
true
254+
255+
"""
256+
@spec data_categories() :: [String.t(), ...]
257+
def data_categories do
258+
Enum.map(@categories, &data_category/1) ++ ["log_byte", "trace_metric_byte"]
259+
end
189260
end

lib/sentry/telemetry/scheduler.ex

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,9 @@ defmodule Sentry.Telemetry.Scheduler do
531531
items == [] ->
532532
:ok
533533

534-
# Transactions carry spans, so pass the actual structs through the
535-
# list-based recorder to also record the discarded "span" outcomes.
536-
category == :transaction ->
534+
# These categories expand to paired outcomes (span / log_byte /
535+
# trace_metric_byte) that only the struct-based recorder emits.
536+
category in [:transaction, :log, :metric] ->
537537
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, items)
538538

539539
true ->
@@ -551,8 +551,10 @@ defmodule Sentry.Telemetry.Scheduler do
551551
defp category_rate_limited?(%{on_envelope: cb}, _category) when is_function(cb, 1), do: false
552552

553553
defp category_rate_limited?(_state, category) do
554-
data_category = Category.data_category(category)
555-
RateLimiter.rate_limited?(data_category)
554+
category
555+
|> Category.data_category()
556+
|> Category.rate_limit_categories()
557+
|> Enum.any?(&RateLimiter.rate_limited?/1)
556558
end
557559

558560
defp default_weights do

lib/sentry/telemetry_processor.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ defmodule Sentry.TelemetryProcessor do
253253
defp add_to_buffer(processor, category, item) when is_atom(processor) do
254254
data_category = Category.data_category(category)
255255

256-
if RateLimiter.rate_limited?(data_category) do
256+
if data_category_rate_limited?(data_category) do
257257
{:ok, {:rate_limited, data_category}}
258258
else
259259
Buffer.add(buffer_name(processor, category), item)
@@ -265,7 +265,7 @@ defmodule Sentry.TelemetryProcessor do
265265
defp add_to_buffer(processor, category, item) do
266266
data_category = Category.data_category(category)
267267

268-
if RateLimiter.rate_limited?(data_category) do
268+
if data_category_rate_limited?(data_category) do
269269
{:ok, {:rate_limited, data_category}}
270270
else
271271
buffer = get_buffer(processor, category)
@@ -276,6 +276,12 @@ defmodule Sentry.TelemetryProcessor do
276276
end
277277
end
278278

279+
defp data_category_rate_limited?(data_category) do
280+
data_category
281+
|> Category.rate_limit_categories()
282+
|> Enum.any?(&RateLimiter.rate_limited?/1)
283+
end
284+
279285
defp safe_get_buffer(processor, category) when is_atom(processor) do
280286
try do
281287
{:ok,

lib/sentry/transport.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule Sentry.Transport do
44
# This module is exclusively responsible for encoding and POSTing envelopes to Sentry.
55

66
alias Sentry.{ClientError, ClientReport, Config, Envelope, LoggerUtils}
7+
alias Sentry.Telemetry.Category
78
alias Sentry.Transport.RateLimiter
89

910
@default_retries [1000, 2000, 4000, 8000]
@@ -86,8 +87,10 @@ defmodule Sentry.Transport do
8687
defp check_rate_limited(envelope_items) do
8788
rate_limited? =
8889
Enum.any?(envelope_items, fn item ->
89-
category = Envelope.get_data_category(item)
90-
RateLimiter.rate_limited?(category)
90+
item
91+
|> Envelope.get_data_category()
92+
|> Category.rate_limit_categories()
93+
|> Enum.any?(&RateLimiter.rate_limited?/1)
9194
end)
9295

9396
if rate_limited?, do: {:error, :rate_limited}, else: :ok

test/envelope_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,45 @@ defmodule Sentry.EnvelopeTest do
336336
assert Envelope.get_data_category(metric_batch) == "trace_metric"
337337
end
338338
end
339+
340+
describe "log_event_byte_size/1" do
341+
test "approximates the serialized size of a single log event" do
342+
log_event = %LogEvent{
343+
timestamp: 1_588_601_261.535_386,
344+
level: :info,
345+
body: "something happened"
346+
}
347+
348+
assert Envelope.log_event_byte_size(log_event) > 0
349+
end
350+
351+
test "grows with the size of the log body" do
352+
base = %LogEvent{timestamp: 1_588_601_261.535_386, level: :info, body: "hi"}
353+
large = %LogEvent{base | body: String.duplicate("x", 1_000)}
354+
355+
assert Envelope.log_event_byte_size(large) >
356+
Envelope.log_event_byte_size(base) + 900
357+
end
358+
end
359+
360+
describe "metric_byte_size/1" do
361+
test "approximates the serialized size of a single metric" do
362+
metric = %Metric{
363+
type: :counter,
364+
name: "test.counter",
365+
value: 1,
366+
timestamp: 1_588_601_261.535_386
367+
}
368+
369+
assert Envelope.metric_byte_size(metric) > 0
370+
end
371+
372+
test "grows with the size of the metric name" do
373+
base = %Metric{type: :counter, name: "m", value: 1, timestamp: 1_588_601_261.535_386}
374+
large = %Metric{base | name: String.duplicate("n", 1_000)}
375+
376+
assert Envelope.metric_byte_size(large) >
377+
Envelope.metric_byte_size(base) + 900
378+
end
379+
end
339380
end

0 commit comments

Comments
 (0)