defmodule Spandex.Span do
@moduledoc """
A container for all span data and metadata.
"""
alias Spandex.Span
defstruct [
:completion_time,
:env,
:error,
:http,
:id,
:name,
:parent_id,
:private,
:resource,
:service,
:service_version,
:services,
:sql_query,
:start,
:tags,
:trace_id,
:type
]
@nested_opts [:error, :http, :sql_query]
@type t :: %Span{
completion_time: Spandex.timestamp() | nil,
env: String.t() | nil,
error: Keyword.t() | nil,
http: Keyword.t() | nil,
id: Spandex.id(),
name: String.t(),
parent_id: Spandex.id() | nil,
private: Keyword.t(),
resource: atom() | String.t(),
service: atom(),
service_version: String.t() | nil,
services: Keyword.t() | nil,
sql_query: Keyword.t() | nil,
start: Spandex.timestamp(),
tags: Keyword.t() | nil,
trace_id: Spandex.id(),
type: atom()
}
@span_opts Optimal.schema(
opts: [
completion_time: :integer,
env: :string,
error: :keyword,
http: :keyword,
id: :any,
name: :string,
parent_id: :any,
private: :keyword,
resource: [:atom, :string],
service: :atom,
service_version: :string,
services: :keyword,
sql_query: :keyword,
start: :integer,
tags: :keyword,
trace_id: :any,
type: :atom
],
defaults: [
private: [],
services: [],
tags: []
],
required: [
:id,
:name,
:service,
:start,
:trace_id
],
extra_keys?: true
)
def span_opts(), do: @span_opts
@doc """
Create a new span.
#{Optimal.Doc.document(@span_opts)}
"""
@spec new(Keyword.t()) ::
{:ok, Span.t()}
| {:error, [Optimal.error()]}
def new(opts) do
update(nil, opts, @span_opts)
end
@doc """
Update an existing span.
#{Optimal.Doc.document(Map.put(@span_opts, :required, []))}
## Special Meta
```elixir
[
http: [
url: "my_website.com?foo=bar",
status_code: "400",
method: "GET",
query_string: "foo=bar",
user_agent: "Mozilla/5.0...",
request_id: "special_id"
],
error: [
exception: ArgumentError.exception("foo"),
stacktrace: __STACKTRACE__,
error?: true # Used for specifying that a span is an error when there is no exception or stacktrace.
],
sql_query: [
rows: 100,
db: "my_database",
query: "SELECT * FROM users;"
],
# Private has the same structure as the outer meta structure, but private metadata does not
# transfer from parent span to child span.
private: [
...
]
]
```
"""
@spec update(Span.t() | nil, Keyword.t(), Optimal.Schema.t()) ::
{:ok, Span.t()}
| {:error, [Optimal.error()]}
def update(span, opts, schema \\ Map.put(@span_opts, :required, [])) do
opts_without_nils = Enum.reject(opts, fn {_key, value} -> is_nil(value) end)
starting_opts =
span
|> Kernel.||(%{})
|> Map.take(schema.opts)
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> merge_retaining_nested(opts_without_nils)
with_type =
case {starting_opts[:type], starting_opts[:services]} do
{nil, keyword} when is_list(keyword) ->
Keyword.put(starting_opts, :type, keyword[starting_opts[:service]])
_ ->
starting_opts
end
validate_and_merge(span, with_type, schema)
end
@spec merge_retaining_nested(Keyword.t(), Keyword.t()) :: Keyword.t()
defp merge_retaining_nested(left, right) do
Keyword.merge(left, right, fn key, v1, v2 ->
case key do
k when k in @nested_opts ->
left = struct_to_keyword(v1)
right = struct_to_keyword(v2)
merge_non_nils(left, right)
:tags ->
Keyword.merge(v1 || [], v2 || [])
:private ->
merge_or_choose(v1, v2)
_ ->
v2
end
end)
end
@spec merge_or_choose(Keyword.t() | nil, Keyword.t() | nil) :: Keyword.t() | nil
defp merge_or_choose(left, right) do
if left && right do
merge_retaining_nested(left, right)
else
left || right
end
end
@spec merge_non_nils(Keyword.t(), Keyword.t()) :: Keyword.t()
defp merge_non_nils(left, right) do
Keyword.merge(left, right, fn _k, v1, v2 ->
if is_nil(v2) do
v1
else
v2
end
end)
end
@spec validate_and_merge(Span.t() | nil, Keyword.t(), Optimal.schema()) ::
{:ok, Span.t()}
| {:error, [Optimal.error()]}
defp validate_and_merge(span, opts, schema) do
case Optimal.validate(opts, schema) do
{:ok, opts} ->
new_span =
if span do
struct(span, opts)
else
struct(Span, opts)
end
{:ok, new_span}
{:error, errors} ->
{:error, errors}
end
end
@spec child_of(Span.t(), String.t(), Spandex.id(), Spandex.timestamp(), Keyword.t()) ::
{:ok, Span.t()}
| {:error, [Optimal.error()]}
def child_of(parent_span, name, id, start, opts) do
child = %Span{parent_span | id: id, name: name, start: start, parent_id: parent_span.id}
update(child, opts)
end
defp struct_to_keyword(%_struct{} = struct), do: struct |> Map.from_struct() |> Enum.into([])
defp struct_to_keyword(keyword) when is_list(keyword), do: keyword
defp struct_to_keyword(nil), do: []
end