Skip to content

Commit 0dbf638

Browse files
solnicwhatyouhide
andauthored
feat: support for Structured Logs (#969)
* Introduce Logs handler for structured logging * fix: timer race condition in LogEventBuffer flush * fix: use Task.Supervisor for async log event sending * perf: track event count in state to avoid repeated length/1 calls * refactor: rename send_log_events/1 to send_log_batch/1 * fix: remove LogEvent.t() from get_data_category/1 type spec * docs: fix version typo (11.0.0 -> 12.0.0) * docs: add version annotations to logs config options * docs: format content type with backticks * docs: link URLs properly * refactor: make buffer configurable * Update CHANGELOG.md * Update lib/sentry/log_event.ex Co-authored-by: Andrea Leopardi <an.leopardi@gmail.com> * Update lib/sentry/log_event.ex Co-authored-by: Andrea Leopardi <an.leopardi@gmail.com> * fix: remove LogEvent from envelope items * fix: enabled_logs -> enable_logs * fix: formatting * refactor: introduce a unified logger handler * fixup: update log event structure to match relay requirements * fixup: flush stale also in handle_cast --------- Co-authored-by: Andrea Leopardi <an.leopardi@gmail.com>
1 parent 19d0e04 commit 0dbf638

27 files changed

Lines changed: 2472 additions & 370 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#### Features
88

99
- Support for Distributed Tracing ([957](https://github.com/getsentry/sentry-elixir/pull/957))
10+
- Support for Structured Logs ([#969](https://github.com/getsentry/sentry-elixir/pull/969))
1011

1112
#### Various improvements
1213

lib/sentry/application.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ defmodule Sentry.Application do
3434
[]
3535
end
3636

37+
maybe_log_event_buffer =
38+
if Config.enable_logs?() do
39+
[
40+
{Task.Supervisor, name: Sentry.LogEventBuffer.TaskSupervisor},
41+
Sentry.LogEventBuffer
42+
]
43+
else
44+
[]
45+
end
46+
3747
children =
3848
[
3949
{Registry, keys: :unique, name: Sentry.Transport.SenderRegistry},
@@ -48,6 +58,7 @@ defmodule Sentry.Application do
4858
] ++
4959
maybe_http_client_spec ++
5060
maybe_span_storage ++
61+
maybe_log_event_buffer ++
5162
maybe_rate_limiter() ++
5263
[Sentry.Transport.SenderPool]
5364

lib/sentry/client.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule Sentry.Client do
1414
Envelope,
1515
Event,
1616
Interfaces,
17+
LogEvent,
1718
LoggerUtils,
1819
Options,
1920
Transaction,
@@ -134,6 +135,36 @@ defmodule Sentry.Client do
134135
end
135136
end
136137

138+
@doc """
139+
Sends a batch of log events to Sentry.
140+
141+
Log events are sent asynchronously and do not support callbacks or sampling.
142+
They are buffered and sent in batches according to the Sentry Logs Protocol.
143+
144+
Returns `{:ok, envelope_id}` on success or `{:error, reason}` on failure.
145+
"""
146+
@doc since: "12.0.0"
147+
@spec send_log_batch([LogEvent.t()]) ::
148+
{:ok, envelope_id :: String.t()} | {:error, ClientError.t()}
149+
def send_log_batch([]), do: {:ok, ""}
150+
151+
def send_log_batch(log_events) when is_list(log_events) do
152+
case Sentry.Test.maybe_collect_logs(log_events) do
153+
:collected ->
154+
{:ok, ""}
155+
156+
:not_collecting ->
157+
client = Config.client()
158+
159+
request_retries =
160+
Application.get_env(:sentry, :request_retries, Transport.default_retries())
161+
162+
log_events
163+
|> Envelope.from_log_events()
164+
|> Transport.encode_and_post_envelope(client, request_retries)
165+
end
166+
end
167+
137168
defp sample_event(sample_rate) do
138169
cond do
139170
sample_rate == 1 -> :ok

lib/sentry/config.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,28 @@ defmodule Sentry.Config do
364364
""",
365365
default: [],
366366
keys: integrations_schema
367+
],
368+
enable_logs: [
369+
type: :boolean,
370+
default: false,
371+
doc: """
372+
Whether to enable sending log events to Sentry. When enabled, the SDK will
373+
capture and send structured log events according to the
374+
[Sentry Logs Protocol](https://develop.sentry.dev/sdk/telemetry/logs/).
375+
Use `Sentry.LogsHandler` to capture log events from Erlang's `:logger`.
376+
*Available since 12.0.0*.
377+
"""
378+
],
379+
max_log_events: [
380+
type: :non_neg_integer,
381+
default: 100,
382+
doc: """
383+
The maximum number of log events to buffer before flushing to Sentry.
384+
Log events are buffered and sent in batches to reduce network overhead.
385+
When the buffer reaches this size, it will be flushed immediately.
386+
Otherwise, logs are flushed every 5 seconds. Only applies when `:enable_logs`
387+
is `true`. *Available since 12.0.0*.
388+
"""
367389
]
368390
]
369391

@@ -780,6 +802,12 @@ defmodule Sentry.Config do
780802
not is_nil(fetch!(:traces_sample_rate))) or not is_nil(get(:traces_sampler))
781803
end
782804

805+
@spec enable_logs?() :: boolean()
806+
def enable_logs?, do: fetch!(:enable_logs)
807+
808+
@spec max_log_events() :: non_neg_integer()
809+
def max_log_events, do: fetch!(:max_log_events)
810+
783811
@spec put_config(atom(), term()) :: :ok
784812
def put_config(key, value) when is_atom(key) do
785813
unless key in @valid_keys do

lib/sentry/envelope.ex

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ defmodule Sentry.Envelope do
88
ClientReport,
99
Config,
1010
Event,
11+
LogBatch,
12+
LogEvent,
1113
Transaction,
1214
UUID
1315
}
1416

1517
@type t() :: %__MODULE__{
1618
event_id: UUID.t(),
1719
items: [
18-
Attachment.t() | CheckIn.t() | ClientReport.t() | Event.t() | Transaction.t(),
20+
Attachment.t()
21+
| CheckIn.t()
22+
| ClientReport.t()
23+
| Event.t()
24+
| LogBatch.t()
25+
| Transaction.t(),
1926
...
2027
]
2128
}
@@ -68,6 +75,25 @@ defmodule Sentry.Envelope do
6875
}
6976
end
7077

78+
@doc """
79+
Creates a new envelope containing log events.
80+
81+
According to the Sentry Logs Protocol, log events are sent in batches
82+
within a single envelope item with content type `application/vnd.sentry.items.log+json`.
83+
All log events are wrapped in a single item with `{ items: [...] }`.
84+
"""
85+
@doc since: "12.0.0"
86+
@spec from_log_events([LogEvent.t()]) :: t()
87+
def from_log_events(log_events) when is_list(log_events) do
88+
# Create a single log batch item that wraps all log events
89+
log_batch = %LogBatch{log_events: log_events}
90+
91+
%__MODULE__{
92+
event_id: UUID.uuid4_hex(),
93+
items: [log_batch]
94+
}
95+
end
96+
7197
@doc """
7298
Returns the "data category" of the envelope's contents (to be used in client reports and more).
7399
"""
@@ -77,6 +103,7 @@ defmodule Sentry.Envelope do
77103
| CheckIn.t()
78104
| ClientReport.t()
79105
| Event.t()
106+
| LogBatch.t()
80107
| Transaction.t()
81108
) ::
82109
String.t()
@@ -85,6 +112,7 @@ defmodule Sentry.Envelope do
85112
def get_data_category(%CheckIn{}), do: "monitor"
86113
def get_data_category(%ClientReport{}), do: "internal"
87114
def get_data_category(%Event{}), do: "error"
115+
def get_data_category(%LogBatch{}), do: "log_item"
88116

89117
@doc """
90118
Encodes the envelope into its binary representation.
@@ -166,4 +194,24 @@ defmodule Sentry.Envelope do
166194
throw(error)
167195
end
168196
end
197+
198+
defp item_to_binary(json_library, %LogBatch{log_events: log_events}) do
199+
items = Enum.map(log_events, &LogEvent.to_map/1)
200+
payload = %{items: items}
201+
202+
case Sentry.JSON.encode(payload, json_library) do
203+
{:ok, encoded_payload} ->
204+
header = %{
205+
"type" => "log",
206+
"item_count" => length(items),
207+
"content_type" => "application/vnd.sentry.items.log+json"
208+
}
209+
210+
{:ok, encoded_header} = Sentry.JSON.encode(header, json_library)
211+
[encoded_header, ?\n, encoded_payload, ?\n]
212+
213+
{:error, _reason} = error ->
214+
throw(error)
215+
end
216+
end
169217
end

lib/sentry/log_batch.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule Sentry.LogBatch do
2+
@moduledoc """
3+
A batch of log events to be sent in a single envelope item.
4+
5+
According to the Sentry Logs Protocol, log events are sent in batches
6+
within a single envelope item with content type `application/vnd.sentry.items.log+json`.
7+
"""
8+
@moduledoc since: "12.0.0"
9+
10+
alias Sentry.LogEvent
11+
12+
@type t() :: %__MODULE__{
13+
log_events: [LogEvent.t()]
14+
}
15+
16+
@enforce_keys [:log_events]
17+
defstruct [:log_events]
18+
end

0 commit comments

Comments
 (0)