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
39 changes: 38 additions & 1 deletion lib/sentry/metric.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,22 @@ defmodule Sentry.Metric do
This adds Sentry-specific attributes like environment, release, SDK info, and server name.
Per the Sentry Metrics spec, default attributes should be attached before the
`before_send_metric` callback is applied (step 5 before step 6).

It also adds `"sentry.is_localhost"`: `true` when the calling process is serving a
request whose host is `localhost`, a `*.localhost` name, or a loopback IP address
(`127.0.0.0/8` or `::1`), and `false` otherwise, including when the process is not
serving a request. The request is the one recorded in the process's
`Sentry.Context` request context, as set by `Sentry.PlugContext`. A value of the
same name in the metric's attributes takes precedence.

*Changed in 14.0.0:* adds `"sentry.is_localhost"`.
"""
@spec attach_default_attributes(t()) :: t()
def attach_default_attributes(%__MODULE__{} = metric) do
default_attrs = %{
"sentry.sdk.name" => "sentry.elixir",
"sentry.sdk.version" => @sdk_version
"sentry.sdk.version" => @sdk_version,
"sentry.is_localhost" => serving_localhost_request?()
}

# Add optional attributes if configured
Expand Down Expand Up @@ -83,6 +93,33 @@ defmodule Sentry.Metric do
:atomics.add_get(:persistent_term.get(@sequence_key), 1, 1) - 1
end

defp serving_localhost_request? do
case request_host(Sentry.Context.get_all().request) do
host when is_binary(host) -> localhost?(String.downcase(host))
nil -> false
end
end

defp request_host(%{env: %{"SERVER_NAME" => host}}) when is_binary(host), do: host
defp request_host(%{url: url}) when is_binary(url), do: URI.parse(url).host
defp request_host(_request), do: nil

defp localhost?("localhost"), do: true

defp localhost?(host) do
String.ends_with?(host, ".localhost") or loopback_address?(host)
end

defp loopback_address?(host) do
unbracketed_host = host |> String.trim_leading("[") |> String.trim_trailing("]")

case :inet.parse_address(to_charlist(unbracketed_host)) do
{:ok, {127, _, _, _}} -> true
{:ok, {0, 0, 0, 0, 0, 0, 0, 1}} -> true
_ -> false
end
end

defp maybe_put_attr(attrs, _key, nil), do: attrs
defp maybe_put_attr(attrs, key, value), do: Map.put(attrs, key, value)

Expand Down
43 changes: 43 additions & 0 deletions test/sentry/metrics_integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,38 @@ defmodule Sentry.MetricsIntegrationTest do
end
end

describe "sentry.is_localhost attribute" do
test "is true for a metric recorded while serving a localhost request", ctx do
serve_request("http://localhost:4000/checkout")

Metrics.count("orders.placed", 1)

assert is_localhost_attribute(ctx.ref) == %{"type" => "boolean", "value" => true}
end

test "is true for a metric recorded while serving a request to a loopback address", ctx do
serve_request("http://[::1]:4000/checkout")

Metrics.count("orders.placed", 1)

assert is_localhost_attribute(ctx.ref) == %{"type" => "boolean", "value" => true}
end

test "is false for a metric recorded while serving a request to a public host", ctx do
serve_request("https://shop.example.com/checkout")

Metrics.count("orders.placed", 1)

assert is_localhost_attribute(ctx.ref) == %{"type" => "boolean", "value" => false}
end

test "is false for a metric recorded outside of a request", ctx do
Metrics.count("orders.placed", 1)

assert is_localhost_attribute(ctx.ref) == %{"type" => "boolean", "value" => false}
end
end

describe "trace context on recorded metrics" do
test "records the metric with the trace of the surrounding span", ctx do
put_test_config(traces_sample_rate: 1.0)
Expand All @@ -193,6 +225,17 @@ defmodule Sentry.MetricsIntegrationTest do
end
end

defp serve_request(url) do
:get
|> Plug.Test.conn(url)
|> Sentry.PlugContext.call(Sentry.PlugContext.init([]))
end

defp is_localhost_attribute(ref) do
[[{_header, %{"items" => [metric]}}]] = collect_envelopes(ref, 1)
metric["attributes"]["sentry.is_localhost"]
end

defp assert_metric_dropped(ctx, crashing_callback) do
put_test_config(before_send_metric: crashing_callback)

Expand Down
Loading