defmodule Impact do
@moduledoc """
Impact is an OpenTelemetry-native SDK for capturing LLM/AI traces from Elixir
applications and exporting them to Impact via OTLP.
The Elixir SDK is the BEAM-side peer of `impact-sdk` (Python) and `impact-sdk-js`
(JavaScript/TypeScript). The three SDKs share a canonical attribute schema:
* `impact.context.<key>` for app-supplied request context
* `impact.trace.{type,name,path,input,output}` for manual spans
## Quick start
Impact.init(
api_key: System.fetch_env!("IMPACT_API_KEY"),
endpoint: System.get_env("IMPACT_BASE_URL"),
mode: :auto
)
Impact.context(
user_id: "u_123",
interaction_id: "int_abc",
attributes: %{team: "growth"}
)
Impact.trace [type: :workflow, name: "checkout"] do
do_work()
end
## Runtime modes
Mirrors the Python and JS SDKs:
* `:auto` (default) — attach to an existing tracer provider if one is configured,
otherwise bootstrap.
* `:bootstrap` — always (re)configure `:opentelemetry` exporter + batch processor.
* `:attach` — never replace caller-configured providers; raises if no usable
provider is found.
"""
alias Impact.{Config, Context, Otel}
@type init_opt ::
{:api_key, String.t() | nil}
| {:endpoint, String.t() | nil}
| {:service_name, String.t() | nil}
| {:mode, :auto | :bootstrap | :attach}
| {:capture_content, boolean()}
| {:diag_log_level, :none | :error | :warn | :info | :debug | :verbose}
@doc """
Initialize Impact. Idempotent — calling twice replays the last configuration.
"""
@spec init([init_opt()]) :: :ok | {:error, term()}
def init(opts \\ []) do
with {:ok, cfg} <- Config.build(opts) do
Otel.setup(cfg)
end
end
@doc """
Attach context to the current execution. Keys are emitted as `impact.context.<key>`
span attributes for every span created under this process / OTel context.
Reserved keys are flattened from `:attributes`. The canonical reserved keys are:
`:user_id`, `:interaction_id`, `:version_id`.
"""
@spec context(keyword() | map()) :: :ok
def context(ctx), do: Context.put(ctx)
@doc """
Wrap an expression in a manual Impact span.
Both block and function forms are supported. Block form is a macro so the
block body is evaluated lazily inside the span:
require Impact
Impact.trace [type: :task, name: "payment.authorize"] do
authorize(order_id)
end
Function form for non-block use:
Impact.trace([type: :task, name: "payment.authorize"], fn ->
authorize(order_id)
end)
"""
defmacro trace(opts, do: block) do
quote do
Impact.Trace.with_span(unquote(opts), fn -> unquote(block) end)
end
end
defmacro trace(opts, fun) do
quote do
Impact.Trace.with_span(unquote(opts), unquote(fun))
end
end
@doc """
Add attributes to the currently active span. Useful for response-side
attributes that are only known after an operation completes.
Impact.trace [type: :llm, name: "bedrock_call", attributes: request_attrs] do
result = call_llm()
Impact.set_attributes(%{"gen_ai.usage.input_tokens" => result.tokens})
result
end
Thin macro over `OpenTelemetry.Tracer.set_attributes/1` — re-exported so
apps don't need to add `:opentelemetry_api` to their deps just to set
mid-span attributes.
"""
defmacro set_attributes(attrs) do
quote do
require OpenTelemetry.Tracer
OpenTelemetry.Tracer.set_attributes(unquote(attrs))
end
end
@doc """
Set the status of the currently active span. `code` is `:ok` | `:error` |
`:unset`. `message` is an optional string (typically required for `:error`).
Impact.trace [type: :task, name: "do_thing"] do
case do_thing() do
{:ok, _} = ok -> ok
{:error, reason} = err ->
Impact.set_status(:error, inspect(reason))
err
end
end
"""
defmacro set_status(code, message \\ "") do
quote do
require OpenTelemetry.Tracer
OpenTelemetry.Tracer.set_status(unquote(code), unquote(message))
end
end
@doc """
Add a timestamped event to the currently active span. Useful for marking
notable points within a long-running span (state transitions, retries,
cache hits, etc).
Impact.add_event("cache_miss", %{key: cache_key})
"""
defmacro add_event(name, attrs \\ %{}) do
quote do
require OpenTelemetry.Tracer
OpenTelemetry.Tracer.add_event(unquote(name), unquote(attrs))
end
end
@doc """
Flush pending spans synchronously. Useful in scripts, Lambda-style runtimes,
and tests.
"""
@spec flush(timeout :: pos_integer()) :: :ok
def flush(timeout \\ 5_000), do: Otel.force_flush(timeout)
@doc """
Flush and shut down Impact's exporter / batch processor. Safe to call from
`Application.stop/1`.
"""
@spec shutdown(timeout :: pos_integer()) :: :ok
def shutdown(timeout \\ 5_000), do: Otel.shutdown(timeout)
@doc """
Returns the deterministic outcome of optional instrumentation activation,
aligned with `impact.instrumentationResults` in the JS SDK.
"""
@spec instrumentation_results() :: %{optional(atom()) => :ok | {:error, term()}}
def instrumentation_results, do: Otel.instrumentation_results()
end