Impact SDK (Elixir)
OpenTelemetry-native LLM/AI tracing SDK for Elixir applications, exporting to Impact via OTLP.
This is the BEAM-side peer of impact-sdk (Python) and impact-sdk-js (JavaScript/TypeScript). The three SDKs share a canonical attribute schema so a trace produced by any one of them looks identical on the wire.
Design goals
-
Align with
impact-sdk(Python) andimpact-sdk-js(JS/TS) runtime semantics and attribute schema. - Keep the public API small, idiomatic, and predictable.
-
Be safe in BYO-OTel environments (
:auto | :bootstrap | :attach). - Never hard-fail customer apps due to optional instrumentation.
Installation
Add :impact to your mix.exs:
def deps do
[
{:impact, "~> 0.0.1"}
]
endmix deps.getConfiguration
Impact integrates in one line, from config/runtime.exs:
# config/runtime.exs
import Config
Impact.Runtime.configure!()
That's it. At boot, Impact.Runtime.configure!/1 reads IMPACT_API_KEY
(required) and IMPACT_BASE_URL (optional — derived from the key's region if
absent) and wires the OpenTelemetry OTLP/HTTP exporter to send to Impact. The
:impact OTP application auto-initialises after that — no Impact.init/1
call is required for default setups.
Why config/runtime.exs
The Erlang OpenTelemetry exporter reads its endpoint and auth headers from
Application env at supervision-tree startup. config/runtime.exs is the
standard Elixir hook that runs after compile but before any application
starts, which is the exact seam the exporter needs.
Calling Impact.init/1 from inside an already-running supervision tree (e.g.
your Application.start/2) is too late — the exporter has already booted with
empty config and won't pick up new values.
Required environment variables
| Var | Purpose |
|---|---|
IMPACT_API_KEY |
Required. Read at boot by Impact.Runtime.configure!/1. |
IMPACT_BASE_URL | Optional. Derived from the key's region if absent. |
OTEL_SERVICE_NAME |
Optional. Defaults to impact-app. |
IMPACT_DIAG_LOG_LEVEL |
Optional. none | error | warn | info | debug | verbose. |
Overrides
Pass any value as a keyword option to override env-var resolution:
Impact.Runtime.configure!(
api_key: "impact_dev_...",
endpoint: "https://api.dev.impact.ai",
service_name: "my_app",
resource_attributes: %{deployment: %{environment: "staging"}}
)Scripts and Mix tasks
config/runtime.exs is only evaluated when starting your application. For
one-off scripts (mix run path.exs), call Impact.Runtime.configure!/1 at
the top of the script and invoke Mix with --no-start so it runs before the
exporter boots:
Impact.Runtime.configure!(api_key: "impact_dev_...")
{:ok, _} = Application.ensure_all_started(:impact)mix run --no-start path/to/script.exs
See test/integration/aml_analyst_example.exs
for a complete working example.
Quick start
Impact.context(
user_id: "user_123",
interaction_id: "interaction_456",
version_id: "v1.0.0",
attributes: %{team: "growth"}
)
require Impact
Impact.trace [type: :workflow, name: "checkout"] do
do_work()
endPublic API
| Function | Purpose |
|---|---|
Impact.Runtime.configure!/1 |
Wire OTel exporter (call from config/runtime.exs) |
Impact.init/1 | Manual init / runtime override (not needed for default setup) |
Impact.context/1 | Attach context for the current execution |
Impact.trace/2 (macro) / trace/2 (fn) | Wrap an expression in a manual Impact span |
Impact.flush/1 | Force-flush pending spans |
Impact.shutdown/1 | Flush and shut down exporter |
Impact.instrumentation_results/0 | Deterministic optional-instrumentation outcomes |
Impact.Instrumentation.Bedrock.request/1 | Auto-instrument an AWS Bedrock Req call (LLM span) |
Instrumenting AWS Bedrock
For applications that call AWS Bedrock via Req, use
Impact.Instrumentation.Bedrock.request/1 in place of Req.request/1. It
emits an LLM span tagged with GenAI semantic-convention attributes
(gen_ai.system, gen_ai.request.model, token usage, tool detection) without
any call-site Impact.trace boilerplate:
req =
Req.new(
method: :post,
url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/#{arn}/converse",
json: %{"messages" => [...], "inferenceConfig" => %{...}, "toolConfig" => %{...}}
)
{:ok, response} = Impact.Instrumentation.Bedrock.request(req)
Add {:req, "~> 0.5"} to your mix.exs — Req is declared as an optional
dep of :impact, so apps that don't use this instrumentor don't pull it in.
What it captures vs what stays manual
| Span type | Source |
|---|---|
bedrock.converse | Auto via Impact.Instrumentation.Bedrock.request/1 |
| Workflow / step root |
Manual: Impact.trace [type: :workflow, name: ...] |
| Tool execution |
Manual: Impact.trace [type: :tool, name: tool_name] |
Why tool execution is manual: the Bedrock instrumentor sees the LLM
request the model made for a tool (captured as the gen_ai.tool.names attribute
on the LLM span), but the tool itself is your code — a function in your
app's tool registry. The SDK can't know which functions are tools, only which
HTTP calls are LLM calls. The same is true in the Python and JS SDKs: tools
are decorated by hand with @trace(type="tool") / impact.trace("tool_name", fn).
In the AML Analyst demo, the agent server wraps each tool execution:
Impact.trace [type: :tool, name: tool_call.name, attributes: %{tool_call_id: tool_call.id}] do
ToolRegistry.execute(tool_call.name, tool_call.input)
end
That puts each tool call in the trace tree under the parent agent loop, so you
see workflow.alert.investigate → task.agent.loop → llm.bedrock.converse → tool.search_kyc.
Canonical schema
Context attributes emitted on every span created in the current execution:
| Input (Elixir) | Attribute emitted on the wire |
|---|---|
user_id: "u_123" | impact.context.user_id |
interaction_id: "int_abc" | impact.context.interaction_id |
version_id: "v1" | impact.context.version_id |
attributes: %{team: "x"} | impact.context.team (and so on) |
Manual span attributes emitted by Impact.trace:
impact.trace.typeimpact.trace.nameimpact.trace.path(dot-separated nested span stack)impact.trace.inputimpact.trace.output
These keys are the cross-SDK invariant. They MUST stay byte-identical to what impact-sdk (Python) and impact-sdk-js (JS/TS) emit.
Runtime modes
:auto(default) — attach to an existing tracer provider if one is configured, otherwise bootstrap.:bootstrap— always (re)configure:opentelemetry+:opentelemetry_exporterwith the Impact OTLP/HTTP-protobuf exporter.:attach— never replace caller-configured providers; raise if no usable provider is found.
Endpoint resolution order:
Impact.init(endpoint: ...)IMPACT_BASE_URL-
derived from
impact_<region>_*API key (https://api.<region>.impact.ai)
Environment variables
IMPACT_BASE_URL— required if not passed toinit/1and not derivable from the keyIMPACT_API_KEY— optional if passed toinit/1IMPACT_DIAG_LOG_LEVEL—none | error | warn | info | debug | verboseOTEL_SERVICE_NAME— defaults toimpact-app
Coverage (initial scope)
BEAM ecosystem GenAI/instrumentation landscape is smaller than Python/JS, so this matrix is intentionally narrow at v0.
Status legend:
Covered:Impact.init/1can auto-enable instrumentation (best-effort) when the customer SDK is present.Model calls only: model SDK spans can be captured, but no dedicated orchestration spans.Not supported: no stable Elixir instrumentation path is integrated today.
Coverage:
| Provider / Framework | Layer | Strategy | Status |
|---|---|---|---|
| AWS Bedrock (Converse / Invoke) | Model | Impact.Instrumentation.Bedrock.request/1 (Req) | Covered |
OpenAI (openai_ex) | Model |
Impact wrapper + :telemetry bridge | Planned |
Anthropic (anthropix) | Model | :telemetry bridge | Planned |
Google Gemini (gemini_ex) | Model | Impact wrapper | Planned |
LangChain.ex (langchain) | Agent framework | :telemetry bridge | Planned |
| Phoenix | HTTP root spans | :opentelemetry_phoenix | Planned |
| Ecto | DB spans | :opentelemetry_ecto | Planned |
| Finch / Req | HTTP client | :opentelemetry_finch | Planned |
Not supported:
| Provider / Framework | Notes |
|---|---|
| CrewAI / Agno | No BEAM port; out of scope |
Validation
mix deps.get
mix format --check-formatted
mix compile --warnings-as-errors
mix testtest/contracts/ is the cross-SDK contract guard suite — it asserts that the Elixir SDK emits the same canonical attribute keys as the Python and JS SDKs.
License
Apache-2.0