defmodule OneAndDone.Telemetry do
@moduledoc """
Telemetry integration to track how long it takes to process a request.
OneAndDone emits the following metrics:
| Metric | Description | Measurements | Metadata |
| --- | --- | --- | --- |
| `[:one_and_done, :request, :start]` | When we begin processing a request. | | `conn`, `opts` |
| `[:one_and_done, :request, :stop]` | When we finish processing a request, including the duration in native units. | `duration` | `conn`, `opts` |
| `[:one_and_done, :request, :exception]` | When we finish processing a request, if an exception was raised. Includes the duration in native units. | `duration`, `exception` | `conn`, `opts` |
| `[:one_and_done, :request, :cache_hit]` | Given an idempotency key, we found a cached response. | `idempotency_key` | `conn`, `response` |
| `[:one_and_done, :request, :cache_miss]` | Given an idempotency key, we didn't find a cached response. | `idempotency_key` | `conn` |
| `[:one_and_done, :request, :idempotency_key_not_set]` | The request doesn't have an idempotency key and will not be processed further by OneAndDone. | | `conn` |
| `[:one_and_done, :request, :idempotency_key_too_long]` | The idempotency key is too long. A 400 error was returned to the client. | `key_length`, `key_length_limit` | `conn` |
| `[:one_and_done, :request, :cache_get, :start]` | When we begin checking the cache for a request. | | `conn`, `idempotency_key` |
| `[:one_and_done, :request, :cache_get, :stop]` | When we finish checking the cache for a request, including the duration in native units. | `duration` | `conn`, `idempotency_key` |
| `[:one_and_done, :request, :cache_get, :exception]` | When we finish checking the cache for a request, if an exception was raised. Includes the duration in native units. | `duration`, `exception` | `conn`, `idempotency_key` |
| `[:one_and_done, :request, :cache_put, :start]` | When we begin serializing and putting a response into the cache. | | `conn`, `idempotency_key` |
| `[:one_and_done, :request, :cache_put, :stop]` | When we finish serializing and putting a response into the cache, including the duration in native units. | `duration` | `conn`, `idempotency_key` |
| `[:one_and_done, :request, :cache_put, :exception]` | When we finish serializing and putting a response into the cache, if an exception was raised. Includes the duration in native units. | `duration`, `exception` | `conn`, `idempotency_key` |
The duration is emitted in native units. To convert to milliseconds, use `System.convert_time_unit(duration, :native, :millisecond)`.
"""
require Logger
@events [
[:one_and_done, :request, :start],
[:one_and_done, :request, :stop],
[:one_and_done, :request, :exception],
[:one_and_done, :request, :cache_hit],
[:one_and_done, :request, :cache_miss],
[:one_and_done, :request, :idempotency_key_not_set],
[:one_and_done, :request, :idempotency_key_too_long],
[:one_and_done, :request, :cache_get, :start],
[:one_and_done, :request, :cache_get, :stop],
[:one_and_done, :request, :cache_get, :exception],
[:one_and_done, :request, :cache_put, :start],
[:one_and_done, :request, :cache_put, :stop],
[:one_and_done, :request, :cache_put, :exception]
]
@doc """
Return the list of events emitted by this module.
"""
@spec events() :: list()
def events, do: @events
defmodule SpanResult do
@moduledoc """
Additional metadata to include at the end of a span.
"""
defstruct [
:status,
:result
]
@type t :: %__MODULE__{
status: :success | :error,
result: any()
}
@doc """
Create a new SpanResult struct.
"""
@spec new(any()) :: t()
def new(result) do
# Infer a success or error status from the result of the wrapped
# function. We default to calling any response a `success`.
status =
case result do
:error -> :error
{:error, _} -> :error
_ -> :success
end
%__MODULE__{status: status, result: result}
end
end
alias OneAndDone.Telemetry
@namespace :one_and_done
@doc """
Measure the duration of a function call.
"""
@spec span(atom() | list(atom()), map(), fun()) :: any()
def span(base_name, meta \\ %{}, fun) do
meta = meta_with_tags(meta)
base_name
|> build_name()
|> :telemetry.span(meta, fn ->
result = fun.()
{
result,
Map.put(meta, :result, Telemetry.SpanResult.new(result))
}
end)
end
@doc """
Emit a telemetry event.
"""
@spec event(atom() | list(atom()), map(), map()) :: :ok
def event(base_name, metrics, meta \\ %{}) do
meta = meta_with_tags(meta)
base_name
|> build_name()
|> :telemetry.execute(metrics, meta)
end
defp build_name(paths) when is_list(paths), do: [@namespace | paths]
defp build_name(path) when is_atom(path), do: [@namespace, path]
# Ensure the event metadata always has a `tags` entry
defp meta_with_tags(meta), do: Map.merge(%{tags: []}, meta)
end