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.` 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.` 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