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"
}

What's next?