RousselTMLEARNING

Article · Alloy: From Discovery to Expertise

Environment variables and OpenTelemetry

1. The key point: Alloy is not an SDK #

Who reads which variable, and why the confusion is common.

Three families of variables not to confuse #

A common misconception: “Alloy is OpenTelemetry compatible, so setting the OTEL_* variables is enough”. That is wrong. These variables are defined by the specification for SDKs, the libraries embedded in your applications. Alloy is a collector: it receives OTLP, but it is configured through its components.

Who reads which environment variable between the application, Alloy and the backend

FamilyExamplesWho reads it?
Native Alloy variablesGOMEMLIMIT, GOGC, HTTP_PROXYAlloy, automatically
Your own variablesTEMPO_ENDPOINT, CLUSTER_NAMEAlloy, only through sys.env()
OpenTelemetry variablesOTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINTThe application SDK
The exceptionOTEL_RESOURCE_ATTRIBUTESAlloy, through otelcol.processor.resourcedetection
How these results were checked: the behaviours in this article were tested on 4 November 2026 with Grafana Alloy v1.21.3, by sending real OTLP traces. The list of variables changes over time: check the documentation of your version.

2. Variables read natively by Alloy #

Memory, Go runtime, proxy and Linux packages.

Memory and Go runtime #

Alloy is written in Go, so it honours the runtime variables. Together with the proxy variables, they are the only ones the Alloy documentation lists as read automatically.

VariablePurposeDefault
GOMEMLIMITSoft memory limit of the garbage collector (900MiB, 1GiB…)Computed (see below)
AUTOMEMLIMITShare of the container limit used to compute GOMEMLIMIT (0 to 1)0.9
AUTOMEMLIMIT_EXPERIMENTsystem: base it on total system memory–
GOGCGarbage collection target percentage; off disables it100
GOMAXPROCSNumber of threads running Go code simultaneouslyNot stated
GOTRACEBACKDetail of panic outputNot stated
GODEBUGDebug options: x509usefallbackroots, netdns–
PPROF_MUTEX_PROFILING_PERCENTShare of sampled mutexes0.1%
PPROF_BLOCK_PROFILING_RATETracking rate of goroutine blocking events10000
GOMEMLIMIT is not a hard limit: the documentation says so explicitly. Keep a margin below your container limit, or the process can still be killed (OOM).

Outbound proxy #

VariablePurpose
HTTP_PROXYProxy for HTTP requests
HTTPS_PROXYProxy for HTTPS requests
NO_PROXYComma-separated exclusions: IP addresses, CIDR blocks, domains or *

Linux packages (systemd) #

With the deb and rpm packages, the service reads its settings from /etc/default/alloy (Debian, Ubuntu) or /etc/sysconfig/alloy (RHEL, Fedora, SUSE). After a change: sudo systemctl restart alloy.

VariablePurpose
CONFIG_FILEConfiguration file of the default engine
CUSTOM_ARGSExtra flags, for example --server.http.listen-addr=0.0.0.0:12345
ALLOY_OTEL_MODEEnables the OpenTelemetry engine instead of the default engine
OTEL_CONFIG_FILE, OTEL_CUSTOM_ARGSEquivalents for the OpenTelemetry engine

3. Your variables: sys.env() #

Explicit reads, default values and pitfalls.

Reading a variable in the configuration #

Any other variable must be read explicitly with sys.env(). If it does not exist, the function returns an empty string without an error, which is why it is paired with coalesce() to provide a default value.

otelcol.exporter.otlp "tempo" {
  client {
    // host:port for this gRPC exporter; default value when the variable is missing
    endpoint = coalesce(sys.env("TEMPO_ENDPOINT"), "tempo:4317")
    headers  = { "X-Scope-OrgID" = coalesce(sys.env("TEMPO_TENANT"), "anonymous") }
    tls {
      insecure = true
    }
  }
}
Pitfall: without a default, the failure happens later. With endpoint = sys.env("MISSING") on otelcol.exporter.otlphttp, alloy run stops with “at least one endpoint must be specified”. The message does not name the variable: think of it first.

An environment variable is only read when the process starts: changing it means restarting Alloy. For a value that must change at runtime, use local.file or remote.http. Secrets are covered in the Secrets Management lesson.

4. OTEL_RESOURCE_ATTRIBUTES: the variable Alloy can read #

The env detector, the override option and its pitfall.

The resourcedetection component #

The otelcol.processor.resourcedetection component has an env detector, enabled by default (detectors = ["env"]). It reads OTEL_RESOURCE_ATTRIBUTES in the key1=value1,key2=value2 format and adds these attributes to all data passing through. This is how you set the environment or cluster once, at collector level.

Measured effect of OTEL_RESOURCE_ATTRIBUTES depending on the Alloy configuration

The override pitfall #

These four results were measured with the same trace sent by an application named from-app. Three lessons:

  • Without the component, the variable has no effect. Setting it for Alloy is not enough.
  • override defaults to true: an existing attribute is replaced. If the variable contains service.name, every application going through this Alloy takes that name, and your services merge in Tempo.
  • OTEL_SERVICE_NAME is ignored by this detector, unlike an SDK where it takes precedence.
Best practice: on an Alloy receiving several applications, use override = false and limit the variable to site attributes: deployment.environment.name, k8s.cluster.name, cloud.region. Never include service.name.

5. OTEL_* variables on the application side #

Pointing an SDK at Alloy with the standard variables.

General SDK variables #

VariablePurposeDefault (specification)
OTEL_SERVICE_NAMEService name; takes precedence over service.name in OTEL_RESOURCE_ATTRIBUTES–
OTEL_RESOURCE_ATTRIBUTESResource attributes: key=value,key2=value2–
OTEL_SDK_DISABLEDDisables the SDKfalse
OTEL_LOG_LEVELSDK log levelinfo
OTEL_PROPAGATORSContext propagationtracecontext,baggage
OTEL_TRACES_SAMPLERSamplerparentbased_always_on
OTEL_TRACES_SAMPLER_ARGSampler argument (e.g. 0.1)–
OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_LOGS_EXPORTERExporter of each signalotlp

OTLP exporter variables #

Each one also exists per signal, for example OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, which then overrides the general variable.

VariablePurposeDefault
OTEL_EXPORTER_OTLP_ENDPOINTTarget URLgRPC http://localhost:4317, HTTP http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOLgrpc, http/protobuf or http/jsonhttp/protobuf
OTEL_EXPORTER_OTLP_HEADERSHeaders: key=value,key2=value2–
OTEL_EXPORTER_OTLP_TIMEOUTMaximum time for one export, retries included10s
OTEL_EXPORTER_OTLP_COMPRESSIONgzip or noneSDK choice
OTEL_EXPORTER_OTLP_CERTIFICATETrusted TLS certificate–
The path pitfall: with OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4318, the SDK appends /v1/traces. With the per-signal OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, the URL is used as is: set http://alloy:4318/v1/traces. Another frequent mistake: sending http/protobuf (the default protocol) to the gRPC port 4317.

Full example: an application and Alloy #

The application gets its standard variables; Alloy gets those about the site. Its configuration, validated with alloy validate and tested end to end, explicitly reuses the destination:

otelcol.receiver.otlp "default" {
  grpc {
    endpoint = "0.0.0.0:4317"
  }
  http {
    endpoint = "0.0.0.0:4318"
  }
  output {
    traces  = [otelcol.processor.resourcedetection.env.input]
    metrics = [otelcol.processor.resourcedetection.env.input]
    logs    = [otelcol.processor.resourcedetection.env.input]
  }
}

// Adds the OTEL_RESOURCE_ATTRIBUTES attributes without overwriting those of the applications
otelcol.processor.resourcedetection "env" {
  detectors = ["env"]
  override  = false
  output {
    traces  = [otelcol.processor.batch.default.input]
    metrics = [otelcol.processor.batch.default.input]
    logs    = [otelcol.processor.batch.default.input]
  }
}

otelcol.processor.batch "default" {
  output {
    traces  = [otelcol.exporter.otlphttp.backend.input]
    metrics = [otelcol.exporter.otlphttp.backend.input]
    logs    = [otelcol.exporter.otlphttp.backend.input]
  }
}

// Explicitly reuses the standard variable: without sys.env it would not be read
otelcol.exporter.otlphttp "backend" {
  client {
    endpoint = sys.env("OTEL_EXPORTER_OTLP_ENDPOINT")
    headers  = { "X-Scope-OrgID" = coalesce(sys.env("BACKEND_TENANT"), "anonymous") }
  }
}
services:
  alloy:
    image: grafana/alloy:latest
    command: run /etc/alloy/config.alloy --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data
    volumes:
      - ./config.alloy:/etc/alloy/config.alloy:ro
    environment:
      OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4318
      OTEL_RESOURCE_ATTRIBUTES: deployment.environment.name=prod,k8s.cluster.name=eu-west
      GOMEMLIMIT: 900MiB
    deploy:
      resources:
        limits:
          memory: 1G

  app:
    image: my-registry/orders:1.4.0
    environment:
      OTEL_SERVICE_NAME: orders
      OTEL_RESOURCE_ATTRIBUTES: service.version=1.4.0,team=payments
      OTEL_EXPORTER_OTLP_ENDPOINT: http://alloy:4318
      OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
      OTEL_TRACES_SAMPLER: parentbased_traceidratio
      OTEL_TRACES_SAMPLER_ARG: "0.1"

In our test, the trace reached /v1/traces on the backend: otelcol.exporter.otlphttp appends each signal path, like an SDK with the general variable.

Mapping: otelcol.exporter.otlphttp expects a URL (http://tempo:4318) and otelcol.exporter.otlp, over gRPC, a host:port (tempo:4317). Without endpoint, the latter is rejected at load time (“missing required attribute endpoint”), even when OTEL_EXPORTER_OTLP_ENDPOINT is set.

6. The OpenTelemetry engine: alloy otel #

The Collector ${env:VAR} syntax, in public preview.

A Collector YAML configuration #

The Alloy binary also ships an OpenTelemetry engine, started with alloy otel --config=otel.yaml. It reads a YAML file in the Collector format, which uses the ${env:VARIABLE} syntax instead of sys.env(). We checked this substitution and the env detector on Alloy v1.21.3.

receivers:
  otlp:
    protocols:
      http:
        endpoint: ${env:RECEIVER_ENDPOINT}
processors:
  resourcedetection:
    detectors: [env]
exporters:
  otlphttp:
    endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT}
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [resourcedetection]
      exporters: [otlphttp]
This engine is in public preview: the documentation warns that breaking changes are possible. For production, the default engine (alloy run) remains the stable choice.

7. Kubernetes and best practices #

Helm, Beyla, common mistakes and commands.

On Kubernetes #

# values.yaml of the grafana/alloy Helm chart
alloy:
  extraEnv:
    - name: OTEL_RESOURCE_ATTRIBUTES
      value: "deployment.environment.name=prod,k8s.cluster.name=eu-west"
    - name: GOMEMLIMIT
      value: "900MiB"
  resources:
    limits:
      memory: 1Gi

The Beyla special case: the beyla.ebpf component can pick up, through extra_resource_labels, attributes supplied by the OTEL_RESOURCE_ATTRIBUTES variable of the observed service. The application's variable is what matters, not Alloy's.

Key takeaways #

  • Setting OTEL_* for Alloy does not configure Alloy, except OTEL_RESOURCE_ATTRIBUTES with resourcedetection.
  • Read your values with sys.env() and provide defaults with coalesce().
  • Use override = false on a shared Alloy, and never service.name in its variable.
  • Keep OTEL_SERVICE_NAME and OTEL_EXPORTER_OTLP_* for the applications.
  • Set GOMEMLIMIT below the container memory limit.

Commands to know #

# Check the configuration with the variables of the current environment
TEMPO_ENDPOINT=tempo:4317 alloy validate config.alloy

# Format a file, then start it
alloy fmt config.alloy
alloy run config.alloy --server.http.listen-addr=127.0.0.1:12345

# See the variables actually received by the process
docker compose exec alloy env | grep -E 'OTEL_|GOMEM'

# Test the pipeline with a minimal trace (OTLP/HTTP)
curl -X POST -H 'Content-Type: application/json' -d @trace.json http://localhost:4318/v1/traces

# OpenTelemetry engine (public preview)
alloy otel --config=otel.yaml

Official sources #

📄 Grafana Alloy environment variables: GOMEMLIMIT, sys.env and OTEL_RESOURCE_ATTRIBUTES →

My progress

  • ⏱ Estimated time : ≈ 21 min

Sign in to track your progress and take notes. Sign in

💬 Ask a question

Course Glossary

Grafana Alloy

An open-source, vendor-neutral telemetry collector designed to replace Grafana Agent, fully compatible with OpenTelemetry (OTel).

Recommended Articles

Grafana Alloy : Collecte et Transformation de Télémétrie

Apprenez à utiliser Grafana Alloy pour collecter, transformer et acheminer logs, métriques et tra...

Référence des Composants Grafana Alloy

Un guide de référence complet sur tous les composants disponibles dans Grafana Alloy, organisé pa...

OpenTelemetry Collector, Grafana Alloy et Bindplane : comparer trois approches des pipelines de télémétrie

Comparatif détaillé de trois façons de gérer des pipelines de télémétrie : l'OpenTelemetry Collec...

Les concepts fondamentaux d'OpenTelemetry : signaux, ressources, contexte et maturité

Le vocabulaire indispensable d'OpenTelemetry : les quatre signaux, API et SDK, ressource et instr...

Grafana Alloy: Trace Management with Tempo

Dive into distributed trace processing. Learn how to ingest OTLP, Jaeger, or Zipkin traces with G...

Grafana Alloy: Understanding and exploiting the User Interface (UI)

Discover how to enable, secure, and use Grafana Alloy's built-in web interface to visualize your ...

Grafana Alloy: Introduction and Architecture

Discover the fundamental concepts of Grafana Alloy, the transition from the static Agent to Alloy...

Grafana Alloy: Log Management with Loki

Discover how to configure Grafana Alloy to read log files, journald, or network streams, process ...

Grafana Alloy: Continuous Profiling with Pyroscope

Discover how to configure continuous profiling in your environments using Grafana Alloy and Pyros...

Support us

♥ SUPPORT US

Support RousselTM Learning

Courses, articles, labs and certificates stay free with no intrusive ads. A voluntary contribution, of any amount you choose, pays for hosting, lab environments and new content.

Contribute with PayPal Any amount · card or PayPal accountLearn more
QR code PayPal
Or scan this QR code with your phone

Thank you, every contribution counts!