defmodule Backwork do
@moduledoc """
One-call OpenTelemetry tracing for [backwork.dev](https://backwork.dev).
There are two pieces — one boot-time, one runtime:
1. **Exporter config** (boot-time) in `config/runtime.exs`:
config :opentelemetry, traces_exporter: :otlp
config :opentelemetry_exporter, Backwork.exporter_config()
2. **Instrumentation** (runtime) in your `Application.start/2`, before the
supervisor children start:
Backwork.setup(phoenix_adapter: :bandit, ecto: [[:my_app, :repo]])
Set `BACKWORK_TOKEN` to your project's ingest token, and optionally
`OTEL_RESOURCE_ATTRIBUTES=service.name=my-app` so it's named in backwork.
Logs and host/container metrics come from the backwork agent — this package is
only for distributed traces / APM.
"""
require Logger
@default_endpoint "https://backwork.dev/otlp"
@doc """
Returns the keyword config for `:opentelemetry_exporter`. Reads `BACKWORK_TOKEN`
and `BACKWORK_ENDPOINT` from the environment; override with `:token` / `:endpoint`.
"""
@spec exporter_config(keyword) :: keyword
def exporter_config(opts \\ []) do
token = opts[:token] || System.get_env("BACKWORK_TOKEN") || ""
endpoint = opts[:endpoint] || System.get_env("BACKWORK_ENDPOINT") || @default_endpoint
if token == "", do: Logger.warning("[backwork] BACKWORK_TOKEN is empty — traces will be rejected (401).")
[
otlp_protocol: :http_protobuf,
otlp_endpoint: endpoint,
otlp_headers: [{"authorization", "Bearer " <> token}]
]
end
@doc """
Attach the OpenTelemetry instrumentation for whichever libraries are present.
Call once from `Application.start/2`.
Options:
* `:phoenix_adapter` — `:bandit` (default) or `:cowboy2`
* `:ecto` — list of telemetry prefixes, e.g. `[[:my_app, :repo]]`
"""
@spec setup(keyword) :: :ok
def setup(opts \\ []) do
maybe(OpentelemetryPhoenix, :setup, [[adapter: opts[:phoenix_adapter] || :bandit]])
maybe(OpentelemetryBandit, :setup, [])
maybe(OpentelemetryCowboy, :setup, [])
for prefix <- List.wrap(opts[:ecto]), do: maybe(OpentelemetryEcto, :setup, [prefix])
:ok
end
defp maybe(mod, fun, args) do
if Code.ensure_loaded?(mod) and function_exported?(mod, fun, length(args)) do
apply(mod, fun, args)
end
end
end