Current section
Files
Jump to
Current section
Files
lib/impact/runtime.ex
defmodule Impact.Runtime do
@moduledoc """
One-call runtime configuration. Designed to be invoked from `config/runtime.exs`
in a Phoenix/Mix application — BEFORE any application starts — so the
`:opentelemetry` and `:opentelemetry_exporter` supervision trees boot with
Impact's OTLP endpoint and auth header already wired in.
## Why this exists
The Erlang OpenTelemetry exporter reads its endpoint and headers from
Application env *at supervision-tree startup*. Calling `Impact.init/1` from
inside a running application is too late — the exporter has already booted
with empty config and will not pick up new values without being restarted.
`config/runtime.exs` is the standard Elixir hook that runs **after compile
but before any application starts**. It is the correct seam for this kind
of dependency-time configuration.
## Usage in a Phoenix / Mix app
# config/runtime.exs
import Config
Impact.Runtime.configure!()
That single line is the full integration. `:impact` reads
`IMPACT_API_KEY` (required) and `IMPACT_BASE_URL` (optional — derived from
the key's region if absent) at boot. No `Impact.init/1` call is required;
the `:impact` OTP application picks the config up on its own start.
## Usage in a one-off script
Mix scripts (`mix run path.exs`) do **not** evaluate `config/runtime.exs`,
so scripts must call `Impact.Runtime.configure!/1` at the top **with
`--no-start`**, then start apps explicitly:
Impact.Runtime.configure!(api_key: "impact_dev_...")
{:ok, _} = Application.ensure_all_started(:impact)
## Overrides
All values can be passed as keyword options, taking precedence over env vars:
Impact.Runtime.configure!(
api_key: "impact_dev_...",
endpoint: "https://api.dev.impact.ai",
service_name: "my_app",
resource_attributes: %{deployment: %{environment: "staging"}}
)
"""
@spec configure!(keyword()) :: :ok
def configure!(opts \\ []) do
api_key = opts[:api_key] || fetch_env!("IMPACT_API_KEY")
endpoint = opts[:endpoint] || System.get_env("IMPACT_BASE_URL") || derive_endpoint!(api_key)
service_name = opts[:service_name] || System.get_env("OTEL_SERVICE_NAME") || "impact-app"
extra_resource = opts[:resource_attributes] || %{}
resource = deep_merge(%{service: %{name: service_name}}, extra_resource)
# OpenTelemetry OTLP/HTTP-protobuf exporter aimed at the Impact endpoint.
# The exporter reads these at supervision-tree start.
Application.put_env(:opentelemetry_exporter, :otlp_protocol, :http_protobuf)
Application.put_env(:opentelemetry_exporter, :otlp_endpoint, endpoint)
Application.put_env(:opentelemetry_exporter, :otlp_headers, [
{"authorization", "Bearer " <> api_key}
])
# OpenTelemetry SDK: resource + processor pipeline. The :processors list
# (NOT the older :span_processor + :traces_exporter keys) is what modern
# `:opentelemetry` (>= 1.1) reads to wire processors to exporters.
Application.put_env(:opentelemetry, :resource, resource)
Application.put_env(:opentelemetry, :processors, [
{:otel_batch_processor, %{exporter: {:opentelemetry_exporter, %{}}}}
])
# Stash the resolved values so `Impact.Application.start/2` can populate
# `Impact.Config.Store` without a separate `Impact.init/1` call.
Application.put_env(:impact, :runtime_configured, true)
Application.put_env(:impact, :api_key, api_key)
Application.put_env(:impact, :endpoint, endpoint)
Application.put_env(:impact, :service_name, service_name)
:ok
end
@doc false
def runtime_configured?, do: Application.get_env(:impact, :runtime_configured, false)
# Recursive map merge — required because OTel resource attributes are nested
# (`service.name`, `service.namespace`, `deployment.environment`, ...) and a
# shallow Map.merge would clobber whole subtrees.
defp deep_merge(left, right) when is_map(left) and is_map(right) do
Map.merge(left, right, fn _k, l, r -> deep_merge(l, r) end)
end
defp deep_merge(_left, right), do: right
defp fetch_env!(var) do
case System.get_env(var) do
nil ->
raise "Impact.Runtime.configure!/1: env var #{var} is required (or pass it as an option)"
"" ->
raise "Impact.Runtime.configure!/1: env var #{var} is empty"
v ->
v
end
end
defp derive_endpoint!("impact_" <> rest) do
case String.split(rest, "_", parts: 2) do
[region, _] when byte_size(region) > 0 ->
"https://api.#{region}.impact.ai"
_ ->
raise "Impact.Runtime.configure!/1: cannot derive endpoint from API key; pass :endpoint or set IMPACT_BASE_URL"
end
end
defp derive_endpoint!(_) do
raise "Impact.Runtime.configure!/1: API key does not match expected format; pass :endpoint or set IMPACT_BASE_URL"
end
end