Packages
sentry
13.3.0
13.3.0
13.2.0
13.1.0
13.0.1
13.0.0
12.0.3
12.0.2
12.0.1
12.0.0
11.0.4
11.0.3
11.0.2
11.0.1
11.0.0
10.10.0
10.9.0
10.8.1
10.8.0
10.7.1
10.7.0
10.6.2
10.6.1
10.6.0
10.5.0
10.4.0
10.3.0
10.2.1
10.2.0
10.2.0-rc.2
10.2.0-rc.1
10.1.0
10.0.3
10.0.2
10.0.1
10.0.0
9.1.0
9.0.0
8.1.0
8.0.6
8.0.5
8.0.4
8.0.3
8.0.2
8.0.1
8.0.0
8.0.0-rc.2
8.0.0-rc.1
8.0.0-rc.0
retired
7.2.5
7.2.4
7.2.3
7.2.2
7.2.1
7.2.0
7.1.0
7.0.6
7.0.5
7.0.4
7.0.3
7.0.2
7.0.1
7.0.0
6.4.2
6.4.1
6.4.0
6.3.0
6.2.1
6.2.0
6.1.0
6.0.5
6.0.4
6.0.3
6.0.2
6.0.1
6.0.0
5.0.1
5.0.0
4.0.3
4.0.2
4.0.1
4.0.0
3.0.0
2.2.0
2.1.0
2.0.2
2.0.1
2.0.0
1.1.2
1.1.1
1.1.0
1.0.0
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
The Official Elixir client for Sentry
Current section
Files
Jump to
Current section
Files
lib/sentry/opentelemetry/span_storage.ex
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
defmodule Sentry.OpenTelemetry.SpanStorage do
@moduledoc false
use GenServer
defstruct [:cleanup_interval, :table_name]
alias Sentry.OpenTelemetry.SpanRecord
@cleanup_interval :timer.minutes(5)
@span_ttl 30 * 60
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) when is_list(opts) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
@impl true
def init(opts) do
table_name = Keyword.get(opts, :table_name, default_table_name())
cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval)
_ = :ets.new(table_name, [:named_table, :public, :ordered_set])
schedule_cleanup(cleanup_interval)
{:ok, %__MODULE__{cleanup_interval: cleanup_interval, table_name: table_name}}
end
@impl true
def handle_info(:cleanup_stale_spans, state) do
cleanup_stale_spans(state.table_name)
schedule_cleanup(state.cleanup_interval)
{:noreply, state}
end
@spec span_exists?(String.t(), keyword()) :: boolean()
def span_exists?(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
case :ets.lookup(table_name, {:root_span, span_id}) do
[{{:root_span, ^span_id}, _span, _stored_at}] ->
true
[] ->
case :ets.match_object(table_name, {{:child_span, :_, span_id}, :_, :_}) do
[] -> false
_ -> true
end
end
end
@doc """
Retrieves a span by its ID, regardless of whether it's a root or child span.
Returns nil if the span is not found.
"""
@spec get_span(String.t(), keyword()) :: SpanRecord.t() | nil
def get_span(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
case :ets.lookup(table_name, {:root_span, span_id}) do
[{{:root_span, ^span_id}, span, _stored_at}] ->
span
[] ->
case :ets.match_object(table_name, {{:child_span, :_, span_id}, :_, :_}) do
[{_key, span, _stored_at} | _] -> span
[] -> nil
end
end
end
@spec store_span(SpanRecord.t(), keyword()) :: true
def store_span(span_data, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
stored_at = System.system_time(:second)
if span_data.parent_span_id == nil do
:ets.insert(table_name, {{:root_span, span_data.span_id}, span_data, stored_at})
else
key = {:child_span, span_data.parent_span_id, span_data.span_id}
:ets.insert(table_name, {key, span_data, stored_at})
end
end
@spec get_root_span(String.t(), keyword()) :: SpanRecord.t() | nil
def get_root_span(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
case :ets.lookup(table_name, {:root_span, span_id}) do
[{{:root_span, ^span_id}, span, _stored_at}] -> span
[] -> nil
end
end
@spec get_child_spans(String.t(), keyword()) :: [SpanRecord.t()]
def get_child_spans(parent_span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
get_all_descendants(parent_span_id, table_name)
end
defp get_all_descendants(parent_span_id, table_name) do
direct_children =
:ets.match_object(table_name, {{:child_span, parent_span_id, :_}, :_, :_})
|> Enum.map(fn {_key, span_data, _stored_at} -> span_data end)
nested_descendants =
Enum.flat_map(direct_children, fn child ->
get_all_descendants(child.span_id, table_name)
end)
(direct_children ++ nested_descendants)
|> Enum.sort_by(& &1.start_time)
end
@spec update_span(SpanRecord.t(), keyword()) :: :ok
def update_span(%{parent_span_id: parent_span_id} = span_data, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
stored_at = System.system_time(:second)
key =
if parent_span_id == nil do
{:root_span, span_data.span_id}
else
{:child_span, parent_span_id, span_data.span_id}
end
:ets.update_element(table_name, key, [{2, span_data}, {3, stored_at}])
:ok
end
@spec remove_root_span(String.t(), keyword()) :: :ok
def remove_root_span(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
key = {:root_span, span_id}
:ets.select_delete(table_name, [{{key, :_, :_}, [], [true]}])
remove_child_spans(span_id, table_name: table_name)
:ok
end
@spec remove_transaction_root_span(String.t(), String.t() | nil, keyword()) :: :ok
def remove_transaction_root_span(span_id, parent_span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
_key =
if parent_span_id == nil do
:ets.select_delete(table_name, [{{{:root_span, span_id}, :_, :_}, [], [true]}])
else
:ets.delete(table_name, {:child_span, parent_span_id, span_id})
end
remove_child_spans(span_id, table_name: table_name)
:ok
end
@spec remove_child_spans(String.t(), keyword()) :: :ok
def remove_child_spans(parent_span_id, opts) do
table_name = Keyword.get(opts, :table_name, default_table_name())
:ets.match_object(table_name, {{:child_span, parent_span_id, :_}, :_, :_})
|> Enum.each(fn {key, span_data, _stored_at} ->
if span_data.end_time != nil do
:ets.delete(table_name, key)
end
end)
:ok
end
@spec remove_child_span(String.t(), String.t(), keyword()) :: :ok
def remove_child_span(parent_span_id, span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
key = {:child_span, parent_span_id, span_id}
:ets.delete(table_name, key)
:ok
end
defp schedule_cleanup(interval) do
Process.send_after(self(), :cleanup_stale_spans, interval)
end
defp cleanup_stale_spans(table_name) do
now = System.system_time(:second)
cutoff_time = now - @span_ttl
root_match_spec = [
{{{:root_span, :"$1"}, :_, :"$2"}, [{:<, :"$2", cutoff_time}], [:"$1"]}
]
expired_root_spans = :ets.select(table_name, root_match_spec)
Enum.each(expired_root_spans, fn span_id ->
remove_root_span(span_id, table_name: table_name)
end)
child_match_spec = [
{{{:child_span, :_, :_}, :_, :"$1"}, [{:<, :"$1", cutoff_time}], [true]}
]
:ets.select_delete(table_name, child_match_spec)
end
defp default_table_name do
Module.concat(__MODULE__, ETSTable)
end
end
end