Skip to main content

lib/backwork.ex

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