LinkMesh

Search docs, blog and changelog

ENDE

Snippets · Processors

Set resource attributes the sender left out

Dashboards and alerts group by deployment.environment.name and service.namespace, and not every SDK or source sets them. A collector that serves one environment can stamp them on, in the resource context of the transform processor, as a default for whatever arrives without one.

Signals
LogsMetricsTraces
Category
Collection
Source
OTLP
Components
otlpbatchmemory_limitertransformotlphttp
Destination
OTLP/HTTP endpoint

OpenTelemetry Collector

OpenTelemetry Collector YAML
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25
  # Stamps every record with the environment and namespace it came from, but
  # only where the sender did not set them already.
  transform/set-resource:
    error_mode: ignore
    log_statements:
      - context: resource
        statements:
          - set(attributes["deployment.environment.name"], "production") where attributes["deployment.environment.name"] == nil
          - set(attributes["service.namespace"], "shop") where attributes["service.namespace"] == nil
    trace_statements:
      - context: resource
        statements:
          - set(attributes["deployment.environment.name"], "production") where attributes["deployment.environment.name"] == nil
          - set(attributes["service.namespace"], "shop") where attributes["service.namespace"] == nil
    metric_statements:
      - context: resource
        statements:
          - set(attributes["deployment.environment.name"], "production") where attributes["deployment.environment.name"] == nil
          - set(attributes["service.namespace"], "shop") where attributes["service.namespace"] == nil
  batch:
    timeout: 5s

exporters:
  otlphttp:
    endpoint: https://your-backend:4318

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [memory_limiter, transform/set-resource, batch]
      exporters: [otlphttp]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, transform/set-resource, batch]
      exporters: [otlphttp]
    traces:
      receivers: [otlp]
      processors: [memory_limiter, transform/set-resource, batch]
      exporters: [otlphttp]

LinkMesh

LinkMesh has no built-in template for this job. Add it as a custom processor in raw mode, with transform as the component type and this body - the same lines as under transform/set-resource in the collector config:

Custom processor body (transform)
error_mode: ignore
log_statements:
  - context: resource
    statements:
      - set(attributes["deployment.environment.name"], "production") where attributes["deployment.environment.name"] == nil
      - set(attributes["service.namespace"], "shop") where attributes["service.namespace"] == nil
trace_statements:
  - context: resource
    statements:
      - set(attributes["deployment.environment.name"], "production") where attributes["deployment.environment.name"] == nil
      - set(attributes["service.namespace"], "shop") where attributes["service.namespace"] == nil
metric_statements:
  - context: resource
    statements:
      - set(attributes["deployment.environment.name"], "production") where attributes["deployment.environment.name"] == nil
      - set(attributes["service.namespace"], "shop") where attributes["service.namespace"] == nil

How custom components are validated and rolled out: create a custom processor.

Download config.yamlValidated with otelcol-contrib v0.161.0 on 2026-10-26

Complete config files, each checked with the collector's own validate command (otelcol-contrib v0.161.0). Replace https://your-backend:4318 with your own OTLP endpoint before you run it.

Before and after

Example telemetry in, and what this exact config sent out when we ran it in otelcol-contrib v0.161.0. CI re-runs it on every change and fails if the output differs. Changed values are highlighted.

2 log records in, 2 log records out

  1. Before

    body
    nothing set by the SDK
    resource.service.name
    checkout

    After

    body
    nothing set by the SDK
    resource.service.name
    checkout
    resource.deployment.environment.name
    production
    resource.service.namespace
    shop
  2. Before

    body
    SDK already says staging
    resource.service.name
    checkout
    resource.deployment.environment.name
    staging

    After

    body
    SDK already says staging
    resource.service.name
    checkout
    resource.deployment.environment.name
    staging
    resource.service.namespace
    shop

The gotcha

Without the where clause, set() overwrites. In our test the guarded config left a record's staging environment alone; the same statements without where turned it into production. Guard every default with == nil, or the collector silently rewrites what the application told you.

Manage this across a fleet

LinkMesh manages collector configuration across a fleet from one self-hosted control plane, so a change like this is made once rather than host by host. The first 25 Collectors are free after a no-card registration in the OpenSight Customer Portal (5 without one).

Doing this in LinkMesh: Processors