Resource Detectors in OpenTelemetry Elixir/Erlang

Resource detectors automatically detect and collect information about the environment in which your Elixir/Erlang application is running. This information is attached to telemetry data (traces, metrics, and logs) to provide additional context for monitoring and debugging.

OpenTelemetry Erlang/Elixir provides several built-in resource detectors, and you can create custom detectors to gather application-specific information.

By default, OpenTelemetry uses the OS environment variable OTEL_RESOURCE_ATTRIBUTES and the opentelemetry OTP application environment variable resource:

elixir
# config/runtime.exs
config :opentelemetry,
  resource: %{
    service: %{
      name: "user-api",
      version: "1.0.0"
    },
    deployment: %{
      environment: "production"
    }
  }

Configuration

Environment Variables

You can configure resource detectors through application configuration or environment variables:

elixir
# config/runtime.exs
config :opentelemetry,
  resource_detectors: [:otel_resource_env_var, :otel_resource_app_env]

Or using environment variables:

bash
export OTEL_RESOURCE_DETECTORS="otel_resource_env_var,otel_resource_app_env"
export OTEL_RESOURCE_ATTRIBUTES="service.name=user-api,service.version=1.2.0,deployment.environment=production"
export OTEL_SERVICE_NAME="user-api"

Timeout Configuration

All resource detectors are protected with a timeout, in milliseconds, after which they return an empty value. The default is 5000 milliseconds:

elixir
# config/runtime.exs
config :opentelemetry, otel_resource_detector_timeout: 3000

Or via environment variable:

bash
export OTEL_RESOURCE_DETECTOR_TIMEOUT=3000

Built-in Detectors

:otel_resource_env_var

Detects resource attributes from the OTEL_RESOURCE_ATTRIBUTES environment variable:

bash
export OTEL_RESOURCE_ATTRIBUTES="service.name=user-api,service.version=1.2.0,deployment.environment=production"

:otel_resource_app_env

Uses the resource configuration from the opentelemetry OTP application environment:

elixir
# config/runtime.exs
config :opentelemetry,
  resource: %{
    service: %{
      name: "user-api",
      version: "1.2.0",
      instance_id: "instance-1"
    },
    host: %{
      name: System.get_env("HOSTNAME", "localhost")
    },
    deployment: %{
      environment: "production"
    }
  }

Resource attributes in the resource OTP application environment variable are flattened and combined with ., so %{deployment: %{environment: "development"}} becomes "deployment.environment" => "development".

Custom Resource Detectors

Custom resource detectors can be created by implementing the otel_resource_detector behaviour which contains a single callback get_resource/1 that returns an otel_resource.

Basic Detector

elixir
defmodule MyApp.CustomResourceDetector do
  @moduledoc """
  Custom resource detector that adds application-specific metadata.
  """

  @behaviour :otel_resource_detector

  def get_resource(_config) do
    attributes = %{
      "service.name" => Application.get_env(:my_app, :service_name, "my-app"),
      "service.version" => Application.spec(:my_app, :vsn) |> to_string(),
      "service.namespace" => "ecommerce",
      "deployment.environment" => System.get_env("MIX_ENV", "development"),
      "host.name" => System.get_env("HOSTNAME", "localhost"),
      "process.pid" => System.get_env("HOSTNAME") |> String.to_integer() |> to_string()
    }

    :otel_resource.create(attributes, [])
  end
end

Advanced Detector

elixir
defmodule MyApp.MetadataResourceDetector do
  @behaviour :otel_resource_detector

  def get_resource(_config) do
    attributes =
      %{}
      |> maybe_add_metadata_from_file("/etc/app/metadata.properties")
      |> maybe_add_system_attributes()

    :otel_resource.create(attributes, [])
  end

  defp maybe_add_metadata_from_file(attributes, file_path) do
    try do
      file_path
      |> File.read!()
      |> String.split("\n", trim: true)
      |> Enum.reduce(attributes, fn line, acc ->
        case String.split(line, "=", parts: 2) do
          [key, value] when key != "" -> Map.put(acc, String.trim(key), String.trim(value))
          _ -> acc
        end
      end)
    rescue
      File.Error -> attributes
    end
  end

  defp maybe_add_system_attributes(attributes) do
    Map.merge(attributes, %{
      "host.name" => System.get_env("HOSTNAME", "unknown"),
      "process.runtime.name" => "beam",
      "process.runtime.version" => System.version()
    })
  end
end

Cloud Detector

elixir
defmodule MyApp.CloudResourceDetector do
  @behaviour :otel_resource_detector

  def get_resource(_config) do
    attributes =
      %{}
      |> maybe_add_aws_metadata()
      |> maybe_add_kubernetes_metadata()

    :otel_resource.create(attributes, [])
  end

  defp maybe_add_aws_metadata(attributes) do
    case System.get_env("AWS_REGION") do
      nil -> attributes
      region ->
        Map.merge(attributes, %{
          "cloud.provider" => "aws",
          "cloud.region" => region
        })
    end
  end

  defp maybe_add_kubernetes_metadata(attributes) do
    case System.get_env("KUBERNETES_SERVICE_HOST") do
      nil -> attributes
      _host ->
        Map.merge(attributes, %{
          "k8s.namespace.name" => System.get_env("POD_NAMESPACE"),
          "k8s.pod.name" => System.get_env("POD_NAME")
        })
    end
  end
end

Registration

Add your custom detectors to the configuration:

elixir
# config/runtime.exs
config :opentelemetry,
  resource_detectors: [
    :otel_resource_env_var,
    :otel_resource_app_env,
    MyApp.CustomResourceDetector,
    MyApp.MetadataResourceDetector,
    MyApp.CloudResourceDetector
  ]

Validation and Debugging

elixir
defmodule MyApp.ResourceDebugger do
  require Logger

  def print_resource_attributes do
    resource = :otel_resource_detector.get_resource()
    attributes = :otel_resource.attributes(resource)

    Logger.info("Resource attributes:")
    Enum.each(attributes, fn {key, value} ->
      Logger.info("  #{key} = #{inspect(value)}")
    end)
  end

  def validate_service_name do
    resource = :otel_resource_detector.get_resource()
    attributes = :otel_resource.attributes(resource)

    case Map.get(attributes, "service.name") do
      nil -> raise "Service name is required"
      "" -> raise "Service name cannot be empty"
      _name -> :ok
    end
  end
end

Common Attributes

Service Attributes

elixir
%{
  "service.name" => "user-api",                    # Required
  "service.version" => "1.2.3",                   # Recommended
  "service.namespace" => "ecommerce",              # Optional
  "service.instance.id" => "instance-12345"       # Optional, must be unique
}

Deployment & Host

elixir
%{
  "deployment.environment" => "production",        # staging, development, etc.
  "host.name" => "web-server-01",
  "process.runtime.name" => "beam",
  "process.runtime.version" => "14.2.2"
}

OpenTelemetry APM

Uptrace is a OpenTelemetry backend that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.

Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.

In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.

What's next?