OpenTelemetry Transformation Language (OTTL) for OTel Collector pipelines. Use when writing or debugging OTTL statements in the transform processor, filter processor, or routing connector in any collector configuration — context selection, path expression mistakes (`attributes` vs `resource.attributes`), `error_mode`, cardinality reduction with `keep_keys`, JSON body handling with `ParseJSON`, PII redaction with `SHA256` / `replace_pattern`, and span naming. Not for receiver/exporter connectivity, DNS, pipeline wiring, or telemetry that never reaches the processor — those are infra problems, not OTTL problems.
79
100%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
OpenTelemetry Transformation Language (OTTL) transforms, filters, and routes telemetry inside an OTel Collector pipeline without modifying application code. Load this skill when writing or debugging OTTL statements in the transform processor, filter processor, or routing connector.
| Use case | What to do |
|---|---|
| Change values or fields conditionally | transform processor with the correct context |
| Drop telemetry entirely (match = drop) | filter processor |
| Set static resource attributes everywhere | resource processor — simpler than OTTL |
| Copy a resource field down to spans or logs | transform with the correct context |
| Route telemetry to different pipelines | routing connector |
| Reduce metric or trace cardinality | transform with keep_keys / delete_matching_keys → references/cardinality.md |
| Redact or pseudonymize PII | transform with SHA256 / replace_all_patterns → references/redaction.md |
| Debug no data, DNS, receiver/exporter issues, or pipeline wiring | Not an OTTL problem — say so before going further |
Reference material for each topic lives under references/ and is listed in the References footer at the bottom of this file. When the question involves a specific processor, context, or function you have not recently reviewed — or when the user pastes a Collector error — consult the matching reference file before answering. One or two targeted reads beat guessing from memory.
context: determines what attributes means. In context: resource, attributes["k"] is a resource attribute. In context: log or context: span, attributes["k"] is the record-level attribute — use resource.attributes["k"] to reach the resource. Wrong context = silent nil, no error. → references/contexts.md
| Signal | Valid contexts |
|---|---|
| logs | resource, scope, log |
| traces | resource, scope, span, spanevent |
| metrics | resource, scope, metric, datapoint |
Metric-level edits (name, description, unit) belong in context: metric; per-series label/attribute edits belong in context: datapoint. Mixing the two in one block means one of them silently no-ops. → references/contexts.md
| Mode | Behavior |
|---|---|
propagate (default) | Any OTTL runtime error halts the pipeline — causes data loss |
ignore | Log the error, skip the statement, continue the pipeline — use in production |
silent | Skip the statement and suppress error logging |
Set error_mode explicitly on every transform and filter processor — the default
(propagate) halts the whole pipeline on the first runtime error (missing optional
field, wrong type, indexing a nil), silently dropping every subsequent record. The fix
for a pipeline that "goes silent after one failure" is almost always the missing
error_mode key:
processors:
transform:
error_mode: ignore # log, skip the statement, continue the pipeline
log_statements:
- context: log
statements:
- set(attributes["env"], resource.attributes["deployment.environment"])Map the Collector message to a root cause before proposing a fix:
| Collector message | Root cause | First action |
|---|---|---|
INVALID_ARGUMENT | Type mismatch, invalid function input, or invalid path for the active context | Add nil/type guards; confirm active context |
... cannot be indexed | Indexing a non-map value — string or empty body | Add IsMap(body) guard before body indexing |
segment "..." is not a valid path | Wrong context or field not available in the chosen context | Switch to the correct context; check path reference |
one or more paths were modified to include their context prefix | Bare attributes[...] where explicit prefixes are required | Rewrite with resource.attributes, datapoint.attributes, etc. |
statement has invalid syntax: ... invalid quoted string | YAML + OTTL quoting collision — the string was consumed by the YAML parser before reaching OTTL | Use YAML single quotes outside and OTTL double quotes inside → references/processors.md |
| Statement loads but has no visible effect | Condition never matches, wrong signal block, or processor in the wrong pipeline stage | Surface debug attributes to prove matching; verify pipeline placement |
Full annotated example of a transform + filter pipeline (log/trace/metric
statements, error_mode, conditions: [IsMap(body)], filter-before-transform
ordering) lives in references/processors.md.
IsMap, IsString, != nil).attributes vs resource.attributes).conditions: semantics or tail sampling policy ordering.When the body arrives as a raw JSON string (IsMap(body) is false), guard with
IsString(body) and parse with ParseJSON. Prefer IsString(body) over
not IsMap(body) — it is the affirmative check and avoids matching empty/nil
bodies.
- context: log
conditions:
- IsString(body) # body is a JSON string (affirmative guard)
statements:
# Promote every top-level JSON field into attributes
- merge_maps(attributes, ParseJSON(body), "insert")
# Or lift specific fields only:
- set(attributes["user_id"], ParseJSON(body)["user_id"]) where ParseJSON(body)["user_id"] != nil
- set(attributes["request_id"], ParseJSON(body)["request_id"]) where ParseJSON(body)["request_id"] != nilParseJSON is a Converter — it returns a value but has no side effect of its own,
so it must be wrapped in an Editor (set, merge_maps). A standalone
ParseJSON(body) line loads without errors and does nothing. → references/transformations.md
- context: datapoint
statements:
- keep_keys(attributes, ["service.name", "http.route", "http.response.status_code"])
- delete_matching_keys(attributes, "^k8s\\.pod\\.uid$")Prefer keep_keys (allowlist) over many delete_key calls (blocklist). → references/cardinality.md
Exporters that pick a destination from attributes — most notably the Coralogix exporter's
application_name_attributes and subsystem_name_attributes — read from resource
attributes, not log-record or span attributes. If the source value lives on the record,
copy it up to the resource from context: log (or context: span) with
set(resource.attributes[...], attributes[...]). Don't rename the field in the
application and don't change the exporter config.
Full pattern (both attribute pairs, error_mode, and pipeline ordering) is in
→ references/contexts.md.
- context: log
statements:
- set(attributes["user.id"], SHA256(attributes["user.id"])) where attributes["user.id"] != nil
- replace_all_patterns(attributes, "value", "(?i)bearer\\s+[a-z0-9._-]+", "bearer ***")SHA256 preserves correlation without exposing the raw identifier. replace_all_patterns redacts across every attribute value without listing each key. → references/redaction.md
error_mode: ignore explicitly. The default propagate causes data loss on any runtime error.conditions: to scope a block. Statements run when any listed condition matches (OR semantics) — cheaper than a where clause on every statement. For strict AND, combine with a and b or use per-statement where.IsMap(body) before map access, IsString(body)
before string operations, where attributes["x"] != nil before reading
optional fields. An unguarded indexing into the wrong type raises
INVALID_ARGUMENT and (with the default error_mode) halts the pipeline.conditions: is OR, not AND. For strict AND, combine into one boolean
(a and b) or add where on each statement.keep_keys over many delete_key calls. An allowlist is shorter and self-documenting.OTTL is not the fix for: receiver connectivity, exporter connectivity, DNS or Kubernetes service discovery, load balancing, gateway reachability, pipeline wiring mistakes, or telemetry that never reaches the processor. Say so before going further.
Scope fences:
The OTTL function list evolves each Collector release. This skill covers patterns and common misuses — for current function signatures, consult the upstream docs in the References footer below.
transform, filter, and routing connector configurationParseJSON, fallback chainskeep_keys, delete_matching_keys, replace_patternSHA256 pseudonymization, token/card/auth header redactionUpstream:
6b2e359
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.