Skip to content
Draft
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
11 changes: 11 additions & 0 deletions lib/sentry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,17 @@ defmodule Sentry do
(Sentry.LogEvent.t() -> as_boolean(Sentry.LogEvent.t()))
| {module(), function_name :: atom()}

@typedoc """
A callback to use with the `:before_send_metric` configuration option.

If this is `{module, function_name}`, then `module.function_name(metric)` will
be called, where `metric` is of type `t:Sentry.Metric.t/0`.
"""
@typedoc since: "13.0.0"
@type before_send_metric_callback() ::
(Sentry.Metric.t() -> as_boolean(Sentry.Metric.t()))
| {module(), function_name :: atom()}

@typedoc """
The strategy to use when sending an event to Sentry.
"""
Expand Down
81 changes: 81 additions & 0 deletions lib/sentry/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Sentry.Client do
Event,
Interfaces,
LoggerUtils,
Metric,
Options,
TelemetryProcessor,
Transaction,
Expand Down Expand Up @@ -144,6 +145,86 @@ defmodule Sentry.Client do
end
end

@spec send_metric(Metric.t()) :: :ok
def send_metric(%Metric{} = metric) do
result_type = Config.send_result()

case result_type do
:sync ->
case apply_before_send_metric(metric) do
[%Metric{} = filtered_metric] ->
case Sentry.Test.maybe_collect_metrics([filtered_metric]) do
:collected ->
:ok

:not_collecting ->
_result =
if Config.dsn() do
client = Config.client()

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

filtered_metric
|> List.wrap()
|> Envelope.from_metric_events()
|> Transport.encode_and_post_envelope(client, request_retries)
end

:ok
end

[] ->
:ok
end

:none ->
# Buffered mode (production): add to TelemetryProcessor
if Config.telemetry_processor_category?(:metric) do
case TelemetryProcessor.add(metric) do
{:ok, {:rate_limited, data_category}} ->
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)

:ok ->
:ok
end
end

:ok
end
end

defp apply_before_send_metric(metric) do
callback = Config.before_send_metric()

if callback do
case call_before_send_metric(metric, callback) do
%Metric{} = modified -> [modified]
_ -> []
end
else
[metric]
end
end

defp call_before_send_metric(metric, function) when is_function(function, 1) do
function.(metric)
rescue
error ->
require Logger
Logger.warning("before_send_metric callback failed: #{inspect(error)}")
metric
end

defp call_before_send_metric(metric, {mod, fun}) do
apply(mod, fun, [metric])
rescue
error ->
require Logger
Logger.warning("before_send_metric callback failed: #{inspect(error)}")
metric
end

defp sample_event(sample_rate) do
cond do
sample_rate == 1 -> :ok
Expand Down
34 changes: 32 additions & 2 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,17 @@ defmodule Sentry.Config do
*Available since 12.0.0*.
"""
],
enable_metrics: [
type: :boolean,
default: true,
doc: """
Whether to enable sending metric events to Sentry. When enabled, the SDK will
capture and send metrics (counters, gauges, distributions) according to the
[Sentry Metrics Protocol](https://develop.sentry.dev/sdk/telemetry/metrics/).
Use `Sentry.Metrics` functions to record metrics.
*Available since 13.0.0*.
"""
],
logs: [
type: :keyword_list,
default: [],
Expand Down Expand Up @@ -434,8 +445,8 @@ defmodule Sentry.Config do
]
],
telemetry_processor_categories: [
type: {:list, {:in, [:error, :check_in, :transaction, :log]}},
default: [:log],
type: {:list, {:in, [:error, :check_in, :transaction, :log, :metric]}},
default: [:log, :metric],
doc: """
List of event categories that should be processed through the TelemetryProcessor.
Categories in this list use the TelemetryProcessor's ring buffer and weighted
Expand All @@ -447,6 +458,7 @@ defmodule Sentry.Config do
* `:check_in` - Cron check-ins (high priority, batch_size=1)
* `:transaction` - Performance transactions (medium priority, batch_size=1)
* `:log` - Log entries (low priority, batch_size=100, 5s timeout)
* `:metric` - Metric events (low priority, batch_size=100, 5s timeout)

*Available since 12.0.0*.
"""
Expand Down Expand Up @@ -702,6 +714,17 @@ defmodule Sentry.Config do
(potentially-updated) `Sentry.LogEvent`, then the updated log event is used instead.
*Available since v12.0.0*.
"""
],
before_send_metric: [
type: {:or, [nil, {:fun, 1}, {:tuple, [:atom, :atom]}]},
type_doc: "`t:before_send_metric_callback/0`",
doc: """
Allows performing operations on a metric *before* it is sent, as
well as filtering out the metric altogether.
If the callback returns `nil` or `false`, the metric is not reported. If it returns a
(potentially-updated) `Sentry.Metric`, then the updated metric is used instead.
*Available since v13.0.0*.
"""
]
]

Expand Down Expand Up @@ -905,6 +928,9 @@ defmodule Sentry.Config do
@spec enable_logs?() :: boolean()
def enable_logs?, do: fetch!(:enable_logs)

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

@spec logs() :: keyword()
def logs, do: fetch!(:logs)

Expand Down Expand Up @@ -937,6 +963,10 @@ defmodule Sentry.Config do
(Sentry.LogEvent.t() -> Sentry.LogEvent.t() | nil | false) | {module(), atom()} | nil
def before_send_log, do: get(:before_send_log)

@spec before_send_metric() ::
(Sentry.Metric.t() -> Sentry.Metric.t() | nil | false) | {module(), atom()} | nil
def before_send_metric, do: get(:before_send_metric)

@spec put_config(atom(), term()) :: :ok
def put_config(key, value) when is_atom(key) do
unless key in @valid_keys do
Expand Down
47 changes: 46 additions & 1 deletion lib/sentry/envelope.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defmodule Sentry.Envelope do
Event,
LogBatch,
LogEvent,
Metric,
MetricBatch,
Transaction,
UUID
}
Expand All @@ -22,6 +24,7 @@ defmodule Sentry.Envelope do
| ClientReport.t()
| Event.t()
| LogBatch.t()
| MetricBatch.t()
| Transaction.t(),
...
]
Expand Down Expand Up @@ -94,6 +97,25 @@ defmodule Sentry.Envelope do
}
end

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

According to the Sentry Metrics Protocol, metrics are sent in batches
within a single envelope item with content type `application/vnd.sentry.items.trace-metric+json`.
All metric events are wrapped in a single item with `{ items: [...] }`.
"""
@doc since: "13.0.0"
@spec from_metric_events([Metric.t()]) :: t()
def from_metric_events(metrics) when is_list(metrics) do
# Create a single metric batch item that wraps all metrics
metric_batch = %MetricBatch{metrics: metrics}

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

@doc """
Returns the "data category" of the envelope's contents (to be used in client reports and more).
"""
Expand All @@ -104,6 +126,7 @@ defmodule Sentry.Envelope do
| ClientReport.t()
| Event.t()
| LogBatch.t()
| MetricBatch.t()
| Transaction.t()
) ::
String.t()
Expand All @@ -113,17 +136,19 @@ defmodule Sentry.Envelope do
def get_data_category(%ClientReport{}), do: "internal"
def get_data_category(%Event{}), do: "error"
def get_data_category(%LogBatch{}), do: "log_item"
def get_data_category(%MetricBatch{}), do: "trace_metric"

@doc """
Returns the total number of payload items in the envelope.

For log envelopes, this counts individual log events within the LogBatch.
For log and metric envelopes, this counts individual items within the batch.
For other envelope types, each item counts as 1.
"""
@spec item_count(t()) :: non_neg_integer()
def item_count(%__MODULE__{items: items}) do
Enum.reduce(items, 0, fn
%LogBatch{log_events: log_events}, acc -> acc + length(log_events)
%MetricBatch{metrics: metrics}, acc -> acc + length(metrics)
_other, acc -> acc + 1
end)
end
Expand Down Expand Up @@ -228,4 +253,24 @@ defmodule Sentry.Envelope do
throw(error)
end
end

defp item_to_binary(json_library, %MetricBatch{metrics: metrics}) do
items = Enum.map(metrics, &Metric.to_map/1)
payload = %{items: items}

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

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

{:error, _reason} = error ->
throw(error)
end
end
end
Loading
Loading