Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#### Features

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

#### Various improvements

Expand Down
11 changes: 11 additions & 0 deletions lib/sentry/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ defmodule Sentry.Application do
[]
end

maybe_log_event_buffer =
if Config.enable_logs?() do
[
{Task.Supervisor, name: Sentry.LogEventBuffer.TaskSupervisor},
Sentry.LogEventBuffer
]
else
[]
end

children =
[
{Registry, keys: :unique, name: Sentry.Transport.SenderRegistry},
Expand All @@ -48,6 +58,7 @@ defmodule Sentry.Application do
] ++
maybe_http_client_spec ++
maybe_span_storage ++
maybe_log_event_buffer ++
maybe_rate_limiter() ++
[Sentry.Transport.SenderPool]

Expand Down
31 changes: 31 additions & 0 deletions lib/sentry/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Sentry.Client do
Envelope,
Event,
Interfaces,
LogEvent,
LoggerUtils,
Options,
Transaction,
Expand Down Expand Up @@ -134,6 +135,36 @@ defmodule Sentry.Client do
end
end

@doc """
Sends a batch of log events to Sentry.

Log events are sent asynchronously and do not support callbacks or sampling.
They are buffered and sent in batches according to the Sentry Logs Protocol.

Returns `{:ok, envelope_id}` on success or `{:error, reason}` on failure.
"""
@doc since: "12.0.0"
@spec send_log_batch([LogEvent.t()]) ::
{:ok, envelope_id :: String.t()} | {:error, ClientError.t()}
def send_log_batch([]), do: {:ok, ""}

def send_log_batch(log_events) when is_list(log_events) do
case Sentry.Test.maybe_collect_logs(log_events) do
:collected ->
{:ok, ""}

:not_collecting ->
client = Config.client()

request_retries =
Application.get_env(:sentry, :request_retries, Transport.default_retries())

log_events
|> Envelope.from_log_events()
|> Transport.encode_and_post_envelope(client, request_retries)
end
end

defp sample_event(sample_rate) do
cond do
sample_rate == 1 -> :ok
Expand Down
28 changes: 28 additions & 0 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,28 @@ defmodule Sentry.Config do
""",
default: [],
keys: integrations_schema
],
enable_logs: [
type: :boolean,
default: false,
doc: """
Whether to enable sending log events to Sentry. When enabled, the SDK will
capture and send structured log events according to the
[Sentry Logs Protocol](https://develop.sentry.dev/sdk/telemetry/logs/).
Use `Sentry.LogsHandler` to capture log events from Erlang's `:logger`.
*Available since 12.0.0*.
"""
],
max_log_events: [
type: :non_neg_integer,
default: 100,
doc: """
The maximum number of log events to buffer before flushing to Sentry.
Log events are buffered and sent in batches to reduce network overhead.
When the buffer reaches this size, it will be flushed immediately.
Otherwise, logs are flushed every 5 seconds. Only applies when `:enable_logs`
is `true`. *Available since 12.0.0*.
"""
]
]

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

@spec enable_logs?() :: boolean()
def enable_logs?, do: fetch!(:enable_logs)

@spec max_log_events() :: non_neg_integer()
def max_log_events, do: fetch!(:max_log_events)

@spec put_config(atom(), term()) :: :ok
def put_config(key, value) when is_atom(key) do
unless key in @valid_keys do
Expand Down
50 changes: 49 additions & 1 deletion lib/sentry/envelope.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ defmodule Sentry.Envelope do
ClientReport,
Config,
Event,
LogBatch,
LogEvent,
Transaction,
UUID
}

@type t() :: %__MODULE__{
event_id: UUID.t(),
items: [
Attachment.t() | CheckIn.t() | ClientReport.t() | Event.t() | Transaction.t(),
Attachment.t()
| CheckIn.t()
| ClientReport.t()
| Event.t()
| LogBatch.t()
| Transaction.t(),
...
]
}
Expand Down Expand Up @@ -68,6 +75,25 @@ defmodule Sentry.Envelope do
}
end

@doc """
Creates a new envelope containing log events.

According to the Sentry Logs Protocol, log events are sent in batches
within a single envelope item with content type `application/vnd.sentry.items.log+json`.
All log events are wrapped in a single item with `{ items: [...] }`.
"""
@doc since: "12.0.0"
@spec from_log_events([LogEvent.t()]) :: t()
def from_log_events(log_events) when is_list(log_events) do
# Create a single log batch item that wraps all log events
log_batch = %LogBatch{log_events: log_events}

%__MODULE__{
event_id: UUID.uuid4_hex(),
items: [log_batch]
}
end

@doc """
Returns the "data category" of the envelope's contents (to be used in client reports and more).
"""
Expand All @@ -77,6 +103,7 @@ defmodule Sentry.Envelope do
| CheckIn.t()
| ClientReport.t()
| Event.t()
| LogBatch.t()
| Transaction.t()
) ::
String.t()
Expand All @@ -85,6 +112,7 @@ defmodule Sentry.Envelope do
def get_data_category(%CheckIn{}), do: "monitor"
def get_data_category(%ClientReport{}), do: "internal"
def get_data_category(%Event{}), do: "error"
def get_data_category(%LogBatch{}), do: "log_item"

@doc """
Encodes the envelope into its binary representation.
Expand Down Expand Up @@ -166,4 +194,24 @@ defmodule Sentry.Envelope do
throw(error)
end
end

defp item_to_binary(json_library, %LogBatch{log_events: log_events}) do
items = Enum.map(log_events, &LogEvent.to_map/1)
payload = %{items: items}

case Sentry.JSON.encode(payload, json_library) do
{:ok, encoded_payload} ->
header = %{
"type" => "log",
"item_count" => length(items),
"content_type" => "application/vnd.sentry.items.log+json"
}

{:ok, encoded_header} = Sentry.JSON.encode(header, json_library)
[encoded_header, ?\n, encoded_payload, ?\n]

Comment thread
solnic marked this conversation as resolved.
{:error, _reason} = error ->
throw(error)
end
end
end
18 changes: 18 additions & 0 deletions lib/sentry/log_batch.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule Sentry.LogBatch do
@moduledoc """
A batch of log events to be sent in a single envelope item.

According to the Sentry Logs Protocol, log events are sent in batches
within a single envelope item with content type `application/vnd.sentry.items.log+json`.
"""
@moduledoc since: "12.0.0"

alias Sentry.LogEvent

@type t() :: %__MODULE__{
log_events: [LogEvent.t()]
}

@enforce_keys [:log_events]
defstruct [:log_events]
end
Loading
Loading