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

  1. Align with impact-sdk (Python) and impact-sdk-js (JS/TS) runtime semantics and attribute schema.
  2. Keep the public API small, idiomatic, and predictable.
  3. Be safe in BYO-OTel environments (:auto | :bootstrap | :attach).
  4. Never hard-fail customer apps due to optional instrumentation.

Installation

Add :impact to your mix.exs:

def deps do
  [
    {:impact, "~> 0.0.1"}
  ]
end
mix deps.get

Configuration

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()
end

Public 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.exsReq 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.converseAuto 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:

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

  1. :auto (default) — attach to an existing tracer provider if one is configured, otherwise bootstrap.
  2. :bootstrap — always (re)configure :opentelemetry + :opentelemetry_exporter with the Impact OTLP/HTTP-protobuf exporter.
  3. :attach — never replace caller-configured providers; raise if no usable provider is found.

Endpoint resolution order:

  1. Impact.init(endpoint: ...)
  2. IMPACT_BASE_URL
  3. derived from impact_<region>_* API key (https://api.<region>.impact.ai)

Environment variables

Coverage (initial scope)

BEAM ecosystem GenAI/instrumentation landscape is smaller than Python/JS, so this matrix is intentionally narrow at v0.

Status legend:

  1. Covered: Impact.init/1 can auto-enable instrumentation (best-effort) when the customer SDK is present.
  2. Model calls only: model SDK spans can be captured, but no dedicated orchestration spans.
  3. 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 test

test/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