
Opentelemetry Ottl
- 21 installs
- 4 repo stars
- Updated June 9, 2026
- coralogix/cx-skills
Write and debug OpenTelemetry Transformation Language (OTTL) statements in Collector transform, filter, and routing pipelines.
About
Reference for authoring OTTL statements in the OTel Collector, covering context selection, path expressions, cardinality reduction, JSON body handling, and PII redaction. A developer uses it when writing or debugging transform, filter, or routing processor config.
- Fixes common attributes vs resource.attributes path mistakes
- PII redaction with SHA256/replace_pattern and keep_keys cardinality control
Opentelemetry Ottl by the numbers
- 21 all-time installs (skills.sh)
- Ranked #910 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/coralogix/cx-skills --skill opentelemetry-ottlAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 4 |
| Last updated | June 9, 2026 |
| Repository | coralogix/cx-skills ↗ |
What it does
Write and debug OpenTelemetry Transformation Language (OTTL) statements in Collector transform, filter, and routing pipelines.
Files
OTTL
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.
When to Use This Skill
| 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 |
| Extract histogram _sum/_count into standalone metrics, then drop the histogram | transform with extract_sum_metric / extract_count_metric in context: metric, then filter to drop the original |
| 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.
Key Concepts
Contexts and path expressions
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
Log event timestamps can be changed in context: log by setting time with Time(...) or time_unix_nano with an integer nanosecond value. Span status checks belong in context: span; prefer STATUS_CODE_ERROR, STATUS_CODE_OK, and STATUS_CODE_UNSET over raw numeric comparisons. → references/transformations.md
Error modes
| 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"])Error patterns
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 |
Canonical pipeline shape
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.
Common Workflows
1. Debug an OTTL statement
1. Confirm the problem is actually OTTL — not component choice, pipeline wiring, or infrastructure. 2. Identify the signal (logs / traces / metrics) and the specific context. 3. Match the exact error text against the Error patterns table above. 4. Check for missing nil or type guards (IsMap, IsString, != nil). 5. Check for the wrong context prefix (attributes vs resource.attributes). 6. Check conditions: semantics or tail sampling policy ordering. 7. Only then propose the corrected statement and minimal YAML.
2. Promote a JSON log body to attributes
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
3. Reduce metric cardinality
- 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
4. Feed an exporter that reads resource attributes
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.
5. Extract histogram aggregations and drop the source metric
To keep _sum and _count for average latency calculations while dropping raw bucket data:
processors:
transform:
error_mode: ignore
metric_statements:
- context: metric
conditions:
- type == METRIC_DATA_TYPE_HISTOGRAM
statements:
- extract_sum_metric(true) # creates <name>_sum as a new Sum metric; true = monotonic
- extract_count_metric(true) # creates <name>_count as a new Counter metric
filter:
error_mode: ignore
metrics:
metric:
- 'type == METRIC_DATA_TYPE_HISTOGRAM and name == "http.server.request.duration"'extract_sum_metric(monotonic) and extract_count_metric(monotonic) are OTTL Editor functions that run in context: metric and append new standalone metrics to the pipeline output — the original histogram is still present until the filter processor drops it. The `transform` processor must come before `filter` in the pipeline so the new metrics exist before the histogram is removed. Pass true for cumulative/monotonic counters, false for delta.
6. Redact PII across attributes
- 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
Best Practices
Pipeline and ordering
1. Filter before transform. Don't spend CPU transforming records that will be dropped. 2. Set `error_mode: ignore` explicitly. The default propagate causes data loss on any runtime error. 3. Use `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.
Defensive OTTL
1. Guard before indexing. 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. 2. Convert numeric-looking strings before comparing. If an attribute can be "5" or 5, branch with IsString / IsInt and use Int(...) or Double(...) before > / < comparisons. 3. `conditions:` is OR, not AND. For strict AND, combine into one boolean (a and b) or add where on each statement.
Authoring style
1. Identify signal and context first. Keep traces, metrics, and logs separate; use the context that matches the referenced fields. 2. Prefer several short statements over one dense expression. Show exact path prefixes when clarity matters. 3. Prefer `keep_keys` over many `delete_key` calls. An allowlist is shorter and self-documenting.
Limitations
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:
- OTTL can only use fields exposed in the active signal and context model.
- The Collector sees telemetry payloads, not raw incoming requests — upstream HTTP headers are usually not available after receiver ingest.
- If a field is not in the active context, OTTL cannot infer or reconstruct it.
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.
References
- [references/contexts.md](references/contexts.md) — OTTL contexts, path expressions, and the most common context mistakes
- [references/processors.md](references/processors.md) —
transform,filter, androutingconnector configuration - [references/filtering.md](references/filtering.md) — Dropping logs, metrics, and spans with the filter processor
- [references/transformations.md](references/transformations.md) — Span naming, semconv migration, body operations,
ParseJSON, fallback chains - [references/cardinality.md](references/cardinality.md) — Reducing metric and trace cardinality with
keep_keys,delete_matching_keys,replace_pattern - [references/redaction.md](references/redaction.md) — PII masking,
SHA256pseudonymization, token/card/auth header redaction
Upstream:
metric-vs-datapoint-context
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI need to drop only datapoints where attributes[\"tenant\"] is missing, but keep the metric itself when other datapoints are valid. Should this be metric context or datapoint context?",
"type": "weighted_checklist",
"checklist": [
{
"name": "context-datapoint",
"description": "The response matches the pattern: (?i)(context:? ?datapoint|datapoint context)",
"max_score": 3,
"category": "INTENT"
},
{
"name": "explains-that-per-series-per-point-filtering",
"description": "Explains that per-series/per-point filtering belongs in datapoint context, not metric context, and avoids dropping the entire metric unintentionally.",
"max_score": 2,
"category": "RUBRIC"
}
]
}
datapoint-filter-missing-tenant
You are a Coralogix support expert. A user has asked the following question:
---
I need to drop only datapoints where attributes["tenant"] is missing, but keep the metric itself when other datapoints are valid. Should this be metric context or datapoint context?
---
cardinality-reduction
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI want to delete attributes where the key starts with \"http.request.header.\" from spans before export. What's the OTTL way to remove all matching keys?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-delete-matching-keys",
"description": "The response contains \"delete_matching_keys\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "context-span",
"description": "The response matches the pattern: context:? ?span",
"max_score": 3,
"category": "INTENT"
}
]
}
delete-matching-keys-header-prefix
You are a Coralogix support expert. A user has asked the following question:
---
I want to delete attributes where the key starts with "http.request.header." from spans before export. What's the OTTL way to remove all matching keys?
---
error-mode-config
{
"context": "Evaluating a Coralogix support response for this user question:\n\nMy collector pipeline stops processing logs entirely whenever it encounters a log with a missing field. The transform processor is running set() on an optional field that not every log has, and after one failure the whole pipeline goes silent. How do I make it skip errors instead of halting?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-error-mode",
"description": "The response contains \"error_mode\" (case-insensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "recommends-setting-error-mode-to-ignore-or-si",
"description": "Recommends setting error_mode to ignore (or silent) explicitly and warns that the default propagate causes pipeline halts / data loss. Includes a YAML snippet showing the error_mode key in a processor block.",
"max_score": 2,
"category": "RUBRIC"
}
]
}
error-mode-propagate-vs-ignore
You are a Coralogix support expert. A user has asked the following question:
---
My collector pipeline stops processing logs entirely whenever it encounters a log with a missing field. The transform processor is running set() on an optional field that not every log has, and after one failure the whole pipeline goes silent. How do I make it skip errors instead of halting?
---
filter-vs-transform
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI want to drop all health check spans from my traces. Should I use the transform processor with a delete or set statement, or should I use the filter processor? What's the difference?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-filter",
"description": "The response contains \"filter\" (case-insensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-suggests-implementing-the-drop-via-transfo",
"description": "Explains that the filter processor drops records when its condition matches (match = drop), and that transform is for mutating telemetry rather than removing it. A response that suggests implementing the drop via transform (delete statements, conditional set, error_mode tricks) is a FAIL.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
filter-vs-transform-dropping
You are a Coralogix support expert. A user has asked the following question:
---
I want to drop all health check spans from my traces. Should I use the transform processor with a delete or set statement, or should I use the filter processor? What's the difference?
---
nil-safety-guards
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI want to set attributes[\"db.namespace\"] to whichever of db.name, server.address, or net.peer.name is populated on the span \u2014 whichever one appears first wins. I tried jamming it all into one set() with a big conditional and the statement is unreadable. How does OTTL express a first-non-nil-wins fallback chain cleanly?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-set",
"description": "The response contains \"set(\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "nil-null",
"description": "The response matches the pattern: (!= nil|!= null)",
"max_score": 3,
"category": "INTENT"
}
]
}
first-non-nil-fallback-chain
You are a Coralogix support expert. A user has asked the following question:
---
I want to set attributes["db.namespace"] to whichever of db.name, server.address, or net.peer.name is populated on the span — whichever one appears first wins. I tried jamming it all into one set() with a big conditional and the statement is unreadable. How does OTTL express a first-non-nil-wins fallback chain cleanly?
---
histogram-metric-operations
{
"context": "Evaluating a Coralogix support response for this user question:\n\nWe have a high-volume histogram metric http.server.request.duration with many bucket boundaries. We want to keep the _sum and _count aggregations for average latency calculations but drop the raw histogram buckets entirely to reduce ingestion cost. Can OTTL do this? What's the right sequence to extract the aggregations and then drop the source histogram?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-extract-sum-metric",
"description": "The response contains \"extract_sum_metric\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "mentions-extract-count-metric",
"description": "The response contains \"extract_count_metric\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-says-ottl-cannot-do-this-at-all-without-of",
"description": "Describes a two-step approach: first extract the aggregated series (_sum and _count) from the histogram into standalone metrics using extract_sum_metric and extract_count_metric in context: metric, then drop the original histogram metric with the filter processor. The extraction step must produce new metrics before the histogram is deleted \u2014 suggesting to drop the histogram first and then extract is a FAIL. A response that tries to use keep_keys or delete_key to selectively strip bucket series from within the histogram datapoints (rather than extracting to new metrics) is a FAIL. A response that says OTTL cannot do this at all, without offering any collector-side approach, is a FAIL.",
"max_score": 2,
"category": "MUST_NOT"
},
{
"name": "filter-drop-metric",
"description": "The response matches the pattern: (?i)(filter|drop.*metric|delete.*metric)",
"max_score": 3,
"category": "INTENT"
}
]
}
histogram-bucket-extraction-drop
You are a Coralogix support expert. A user has asked the following question:
---
We have a high-volume histogram metric http.server.request.duration with many bucket boundaries. We want to keep the _sum and _count aggregations for average latency calculations but drop the raw histogram buckets entirely to reduce ingestion cost. Can OTTL do this? What's the right sequence to extract the aggregations and then drop the source histogram?
---
json-body-extraction
{
"context": "Evaluating a Coralogix support response for this user question:\n\nMy logs come in as JSON strings in the body field. I want to extract a field out of that JSON and promote it to a log attribute. I tried body[\"my_field\"] but it throws an INVALID_ARGUMENT error. What's the right approach?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-parsejson",
"description": "The response contains \"ParseJSON\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-shows-a-bare-parsejson-body-line-without-a",
"description": "Wraps ParseJSON inside an Editor function \u2014 specifically set or merge_maps. A response that shows a bare ParseJSON(body) line without an enclosing Editor is a FAIL.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
json-body-parsejson-extraction
You are a Coralogix support expert. A user has asked the following question:
---
My logs come in as JSON strings in the body field. I want to extract a field out of that JSON and promote it to a log attribute. I tried body["my_field"] but it throws an INVALID_ARGUMENT error. What's the right approach?
---
ismap-guard
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI added a transform processor with keep_keys(body, [\"message\", \"level\"]) in context: log, but the collector is now logging INVALID_ARGUMENT errors and some logs are being dropped. The bodies look like plain strings in some of our log sources. How do I fix this so the statement only runs when the body is actually a map?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-ismap",
"description": "The response contains \"IsMap\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
}
]
}
keep-keys-ismap-guard
You are a Coralogix support expert. A user has asked the following question:
---
I added a transform processor with keep_keys(body, ["message", "level"]) in context: log, but the collector is now logging INVALID_ARGUMENT errors and some logs are being dropped. The bodies look like plain strings in some of our log sources. How do I fix this so the statement only runs when the body is actually a map?
---
resource-attribute-routing
{
"context": "Evaluating a Coralogix support response for this user question:\n\nWe route logs to a Coralogix subsystem using the coralogix exporter with subsystem_name_attributes: [\"subsystem\"]. Our application writes subsystem into the log-record attributes (attributes[\"log.file.subsystem\"]), but the subsystem always comes back blank \u2014 the exporter can't see it. We don't want to rename the field in the application. How do I use OTTL so the exporter picks it up?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-resource-attributes",
"description": "The response contains \"resource.attributes\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "mentions-set",
"description": "The response contains \"set(\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-tell-the-customer-to-rename-the-field-in-t",
"description": "Identifies the root cause: the coralogix exporter reads application_name_attributes / subsystem_name_attributes from the RESOURCE attributes, but the customer's field lives on the log-record attributes. Proposes an OTTL transform that copies the value into resource.attributes (e.g. set(resource.attributes[\"subsystem\"], attributes[\"log.file.subsystem\"])) inside context: log. Does NOT tell the customer to rename the field in their application or to change the exporter configuration.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
log-attr-to-resource-for-exporter
You are a Coralogix support expert. A user has asked the following question:
---
We route logs to a Coralogix subsystem using the coralogix exporter with subsystem_name_attributes: ["subsystem"]. Our application writes subsystem into the log-record attributes (attributes["log.file.subsystem"]), but the subsystem always comes back blank — the exporter can't see it. We don't want to rename the field in the application. How do I use OTTL so the exporter picks it up?
---
context-path-expressions
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI need to parse a timestamp string from attributes[\"event_time\"] and set the log timestamp from it. Can OTTL change the event timestamp, and what should I watch out for?",
"type": "weighted_checklist",
"checklist": [
{
"name": "time-timestamp",
"description": "The response matches the pattern: (?i)(time|timestamp|Time)",
"max_score": 3,
"category": "INTENT"
},
{
"name": "explains-whether-timestamp-mutation-is-suppor",
"description": "Explains whether timestamp mutation is supported in the relevant log context, shows a concrete parsing approach, and warns about format/timezone mismatch or invalid values.",
"max_score": 2,
"category": "RUBRIC"
}
]
}
log-timestamp-mutation
You are a Coralogix support expert. A user has asked the following question:
---
I need to parse a timestamp string from attributes["event_time"] and set the log timestamp from it. Can OTTL change the event timestamp, and what should I watch out for?
---
metric-vs-datapoint-context
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI'm trying to rename a metric and also change one of its per-series label values at the same time. I put both statements in a context: metric block but the label change never takes effect. What am I doing wrong?",
"type": "weighted_checklist",
"checklist": [
{
"name": "context-datapoint",
"description": "The response matches the pattern: (?i)(context:? ?datapoint|datapoint context)",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-generic-try-a-different-context-advice-wit",
"description": "Diagnoses the root cause: metric-level edits (name, description, unit) belong in context: metric, but per-series label/attribute edits belong in context: datapoint \u2014 and putting a datapoint-level edit in a metric block causes it to silently no-op (no error). The fix is a second block with context: datapoint for the label change. Generic \"try a different context\" advice without naming metric vs datapoint explicitly is a FAIL.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
metric-vs-datapoint-context
You are a Coralogix support expert. A user has asked the following question:
---
I'm trying to rename a metric and also change one of its per-series label values at the same time. I put both statements in a context: metric block but the label change never takes effect. What am I doing wrong?
---
nil-safety-guards
{
"context": "Evaluating a Coralogix support response for this user question:\n\nMy transform works fine in dev but in production it crashes on certain spans with errors about invalid path segments. The spans that fail are ones where some optional attributes aren't present. How do I write OTTL that is safe when optional fields might be missing?",
"type": "weighted_checklist",
"checklist": [
{
"name": "nil-null-not",
"description": "The response matches the pattern: (!= nil|!= null|not nil|where.*nil|nil.*guard)",
"max_score": 3,
"category": "INTENT"
},
{
"name": "prescribes-a-concrete-guard-pattern-where-att",
"description": "Prescribes a concrete guard pattern \u2014 where attributes[...] != nil, IsMap, IsString, or an equivalent \u2014 rather than generic advice like \"handle errors appropriately\" or \"add validation\". Must show or describe the guard as part of the OTTL statement itself.",
"max_score": 2,
"category": "RUBRIC"
}
]
}
nil-guard-optional-attributes
You are a Coralogix support expert. A user has asked the following question:
---
My transform works fine in dev but in production it crashes on certain spans with errors about invalid path segments. The spans that fail are ones where some optional attributes aren't present. How do I write OTTL that is safe when optional fields might be missing?
---
scope-deflection
{
"context": "Evaluating a Coralogix support response for this user question:\n\nNone of my logs are reaching Coralogix. I've tried adding a transform processor with set(attributes[\"forwarded\"], true) to force something through, but it makes no difference. Maybe OTTL is broken in our version? How do I fix the transform so the logs get exported?",
"type": "weighted_checklist",
"checklist": [
{
"name": "receiver-exporter-pipeline",
"description": "The response matches the pattern: (?i)(receiver|exporter|pipeline|connectivity|DNS|wiring)",
"max_score": 3,
"category": "INTENT"
},
{
"name": "not-ottl-transform",
"description": "The response matches the pattern: (?i)(\\bnot\\b.*(OTTL|transform)|OTTL.*(\\bnot\\b|\\bisn't\\b)|\\bisn't\\b.*(OTTL|transform))",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-response-prescribes-ottl-fixes-transform-t",
"description": "Declines to treat this as an OTTL problem and redirects to infrastructure troubleshooting \u2014 receiver connectivity, exporter endpoint/auth, pipeline wiring, or Coralogix endpoint configuration \u2014 before offering any OTTL YAML. If the response prescribes OTTL fixes (transform tweaks, different statements) as the primary answer, that is a FAIL.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
no-logs-infra-deflection
You are a Coralogix support expert. A user has asked the following question:
---
None of my logs are reaching Coralogix. I've tried adding a transform processor with set(attributes["forwarded"], true) to force something through, but it makes no difference. Maybe OTTL is broken in our version? How do I fix the transform so the logs get exported?
---
json-body-extraction
{
"context": "Evaluating a Coralogix support response for this user question:\n\nMy logs arrive as raw JSON strings in the body \u2014 IsMap(body) returns false. I need to extract the \"user_id\" and \"request_id\" fields from that JSON string and promote them to log attributes. What is the correct OTTL approach?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-parsejson",
"description": "The response contains \"ParseJSON\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "mentions-isstring",
"description": "The response contains \"IsString\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
}
]
}
parsejson-isstring-guard
You are a Coralogix support expert. A user has asked the following question:
---
My logs arrive as raw JSON strings in the body — IsMap(body) returns false. I need to extract the "user_id" and "request_id" fields from that JSON string and promote them to log attributes. What is the correct OTTL approach?
---
json-body-extraction
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI added ParseJSON(body) as a standalone statement in my transform processor. It loads without errors but nothing happens \u2014 no attributes show up from the JSON. Why doesn't this work when used by itself, and what's the right way to get JSON fields out of the body into attributes?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-converter",
"description": "The response contains \"converter\" (case-insensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "merge-maps-set",
"description": "The response matches the pattern: (merge_maps|set\\()",
"max_score": 3,
"category": "INTENT"
}
]
}
parsejson-standalone-converter
You are a Coralogix support expert. A user has asked the following question:
---
I added ParseJSON(body) as a standalone statement in my transform processor. It loads without errors but nothing happens — no attributes show up from the JSON. Why doesn't this work when used by itself, and what's the right way to get JSON fields out of the body into attributes?
---
pii-redaction
{
"context": "Evaluating a Coralogix support response for this user question:\n\nWe need to mask PII in our logs before they leave the collector. We want to hash user IDs so we can still correlate without exposing them, mask credit card numbers in the log message field, and redact Authorization header values in span attributes. How do I do each of these with OTTL?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-sha256",
"description": "The response contains \"SHA256\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "mentions-replace-pattern",
"description": "The response contains \"replace_pattern\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-suggest-deleting-the-fields-outright-when",
"description": "Maps each PII requirement to the right OTTL primitive: SHA256 for the user ID (correlation-preserving hashing), replace_pattern or replace_all_patterns for card numbers in the body, and replace_pattern for the Authorization header value in span attributes. Does not suggest deleting the fields outright when the user asked for masking/hashing.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
pii-masking-sha256-replace-pattern
You are a Coralogix support expert. A user has asked the following question:
---
We need to mask PII in our logs before they leave the collector. We want to hash user IDs so we can still correlate without exposing them, mask credit card numbers in the log message field, and redact Authorization header values in span attributes. How do I do each of these with OTTL?
---
cardinality-reduction
{
"context": "Evaluating a Coralogix support response for this user question:\n\nOur metrics backend is being overwhelmed \u2014 we have hundreds of thousands of unique time series. The main culprits seem to be resource attributes like process IDs, hostnames, and SDK version strings that create a unique combination per process instance. How do I use OTTL to reduce this?",
"type": "weighted_checklist",
"checklist": [
{
"name": "context-resource",
"description": "The response matches the pattern: context:? ?resource",
"max_score": 3,
"category": "INTENT"
},
{
"name": "keep-keys-delete-matching-keys-delete-key",
"description": "The response matches the pattern: (keep_keys|delete_matching_keys|delete_key)",
"max_score": 3,
"category": "INTENT"
}
]
}
resource-attr-cardinality-reduction
You are a Coralogix support expert. A user has asked the following question:
---
Our metrics backend is being overwhelmed — we have hundreds of thousands of unique time series. The main culprits seem to be resource attributes like process IDs, hostnames, and SDK version strings that create a unique combination per process instance. How do I use OTTL to reduce this?
---
context-path-expressions
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI'm trying to copy the k8s namespace name from the resource down into each log record as an attribute so I can filter on it later. I wrote this in my transform processor under context: log \u2014 set(attributes[\"namespace\"], attributes[\"k8s.namespace.name\"]) \u2014 but the attribute always comes out nil. What am I doing wrong?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-resource-attributes",
"description": "The response contains \"resource.attributes\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "explains-that-under-context-log-the-bare-attr",
"description": "Explains that under context: log the bare attributes[...] refers to the log-record attributes, not the resource attributes, and that the fix is to use resource.attributes[...] \u2014 i.e. diagnoses the wrong-context cause, not just prescribes a path rewrite.",
"max_score": 2,
"category": "RUBRIC"
}
]
}
resource-to-log-attr-context
You are a Coralogix support expert. A user has asked the following question:
---
I'm trying to copy the k8s namespace name from the resource down into each log record as an attribute so I can filter on it later. I wrote this in my transform processor under context: log — set(attributes["namespace"], attributes["k8s.namespace.name"]) — but the attribute always comes out nil. What am I doing wrong?
---
routing-connector
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI need to send error spans and slow spans to a high-priority pipeline with more aggressive sampling, and send everything else to a standard pipeline. Can I do this with OTTL? Which component handles pipeline splitting?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-routing",
"description": "The response contains \"routing\" (case-insensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-response-tells-the-user-to-achieve-the-spl",
"description": "Names the routing connector (not the transform or filter processor, and not a second receiver) as the component that splits telemetry across pipelines based on an OTTL condition. Describes the routing mechanism concretely \u2014 either via a table: of statements keyed by OTTL condition (route() statements, or context + condition + pipelines entries) or an equivalent OTTL-conditioned split. Mentioning an explicit route() call or a table: block both pass. FAIL if the response tells the user to achieve the split with the transform processor, filter processor, tail_sampling processor, or by forking receivers.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
routing-connector-error-spans
You are a Coralogix support expert. A user has asked the following question:
---
I need to send error spans and slow spans to a high-priority pipeline with more aggressive sampling, and send everything else to a standard pipeline. Can I do this with OTTL? Which component handles pipeline splitting?
---
semconv-migration
{
"context": "Evaluating a Coralogix support response for this user question:\n\nOur fleet has a mix of old and new OpenTelemetry instrumentation libraries. Some spans come in with http.method and http.status_code (v1 semconv), others with http.request.method and http.response.status_code (v2). Our dashboards expect v2 attribute names. How do I write OTTL that normalizes the old v1 names to v2 without overwriting spans that already have v2?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-http-request-method",
"description": "The response contains \"http.request.method\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "mentions-set",
"description": "The response contains \"set(\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-copies-v1-into-v2-unconditionally-or-that",
"description": "Guards each set() with a where clause that checks the v2 destination is nil \u2014 e.g. where attributes[\"http.request.method\"] == nil \u2014 so spans that already have v2 attributes are not overwritten. A response that copies v1 into v2 unconditionally, or that only guards on the v1 source being present without also checking the v2 destination is empty, is a FAIL.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
semconv-v1-to-v2-normalization
You are a Coralogix support expert. A user has asked the following question:
---
Our fleet has a mix of old and new OpenTelemetry instrumentation libraries. Some spans come in with http.method and http.status_code (v1 semconv), others with http.request.method and http.response.status_code (v2). Our dashboards expect v2 attribute names. How do I write OTTL that normalizes the old v1 names to v2 without overwriting spans that already have v2?
---
span-naming
{
"context": "Evaluating a Coralogix support response for this user question:\n\nAll my spans from our API service come in with a generic name like \"http.request\" regardless of which endpoint was called. I want to rename them to something like \"GET /users/:id\" using the HTTP method and route attributes. How do I do this with OTTL?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-concat",
"description": "The response contains \"Concat\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-only-if-concat-is-in-the-wrong-context-or",
"description": "PASS if the response uses context: span AND sets name via Concat([...], ...) built from the HTTP method and route attributes \u2014 either http.request.method + http.route (v2 semconv) or the v1 fallback http.method + http.route. The attributes are assumed to be populated by the upstream instrumentation; do NOT penalize the response for not explaining how to derive http.route from other fields, for not handling missing-route fallbacks, or for not stripping query strings. FAIL only if Concat is in the wrong context, or the method+route attribute pair is replaced with unrelated fields (e.g. http.url without any route attribute).",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
span-name-concat-http-route
You are a Coralogix support expert. A user has asked the following question:
---
All my spans from our API service come in with a generic name like "http.request" regardless of which endpoint was called. I want to rename them to something like "GET /users/:id" using the HTTP method and route attributes. How do I do this with OTTL?
---
context-path-expressions
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI tried set(attributes[\"is_error\"], status.code == 2) on spans, but my condition behaves strangely. How should I check span status code in OTTL?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-status-code-error",
"description": "The response contains \"STATUS_CODE_ERROR\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "status-code",
"description": "The response matches the pattern: status\\.code",
"max_score": 3,
"category": "INTENT"
},
{
"name": "context-span",
"description": "The response matches the pattern: context:? ?span",
"max_score": 3,
"category": "INTENT"
}
]
}
span-status-code-comparison
You are a Coralogix support expert. A user has asked the following question:
---
I tried set(attributes["is_error"], status.code == 2) on spans, but my condition behaves strangely. How should I check span status code in OTTL?
---
metric-name-transformation
{
"context": "Evaluating a Coralogix support response for this user question:\n\nWe're scraping Prometheus metrics and every counter comes into our backend with a _total suffix on the name (http_requests_total, grpc_calls_total, etc). Our dashboards and alerts don't want that suffix. What's the OTTL way to strip _total from metric names?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-replace-pattern",
"description": "The response contains \"replace_pattern\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "context-metric",
"description": "The response matches the pattern: context:? ?metric",
"max_score": 3,
"category": "INTENT"
}
]
}
strip-total-suffix-metric-names
You are a Coralogix support expert. A user has asked the following question:
---
We're scraping Prometheus metrics and every counter comes into our backend with a _total suffix on the name (http_requests_total, grpc_calls_total, etc). Our dashboards and alerts don't want that suffix. What's the OTTL way to strip _total from metric names?
---
nil-safety-guards
{
"context": "Evaluating a Coralogix support response for this user question:\n\nMy OTTL comparison attributes[\"retries\"] > 3 fails because retries is sometimes a string like \"5\". How do I safely compare numeric-looking attributes?",
"type": "weighted_checklist",
"checklist": [
{
"name": "convert-int-double",
"description": "The response matches the pattern: (?i)(convert|int|double|string|IsString|IsInt|IsDouble)",
"max_score": 3,
"category": "INTENT"
},
{
"name": "recommends-type-guards-or-conversion-before-c",
"description": "Recommends type guards or conversion before comparison and avoids assuming all attribute values are already numeric.",
"max_score": 2,
"category": "RUBRIC"
}
]
}
type-coercion-numeric-comparison
You are a Coralogix support expert. A user has asked the following question:
---
My OTTL comparison attributes["retries"] > 3 fails because retries is sometimes a string like "5". How do I safely compare numeric-looking attributes?
---
conditions-vs-where
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI'm confused about when to use the \"where\" clause on individual statements versus the \"conditions:\" block on a context. Are they interchangeable? Does it matter which one I use?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-conditions",
"description": "The response contains \"conditions\" (case-insensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "mentions-where",
"description": "The response contains \"where\" (case-insensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "no-says-the-two-are-interchangeable-or-that-f",
"description": "Differentiates the two: conditions: applies at the block level and gates every statement under the context, while where applies per individual statement. A response that says the two are interchangeable, or that fails to note that multiple entries under conditions: are OR'd (so strict AND requires combining into a single boolean or using per-statement where), is a FAIL.",
"max_score": 2,
"category": "MUST_NOT"
}
]
}
where-vs-conditions-scope
You are a Coralogix support expert. A user has asked the following question:
---
I'm confused about when to use the "where" clause on individual statements versus the "conditions:" block on a context. Are they interchangeable? Does it matter which one I use?
---
yaml-ottl-escaping
{
"context": "Evaluating a Coralogix support response for this user question:\n\nI'm trying to clean up span names with a transform processor's replace_pattern, but the collector refuses to start and logs: \"statement has invalid syntax: 1:28: invalid quoted string '\\\"cycle-(manager|rpa-manager)\\\\\\\\.[0-9a-f]{8}-[0-9a-f]{4}-...\\\"': invalid syntax\". My regex works fine in a regex tester. What's wrong with how I'm writing it in the YAML?",
"type": "weighted_checklist",
"checklist": [
{
"name": "mentions-replace-pattern",
"description": "The response contains \"replace_pattern\" (case-sensitive).",
"max_score": 3,
"category": "INTENT"
},
{
"name": "escap-quot-backslash",
"description": "The response matches the pattern: (?i)(escap|quot|backslash|yaml)",
"max_score": 3,
"category": "INTENT"
},
{
"name": "explains-that-the-error-is-not-an-ottl-bug-or",
"description": "Explains that the error is NOT an OTTL bug or a bad regex, but a YAML+OTTL quoting collision: the regex has to be a valid OTTL string literal AND a valid YAML scalar. Suggests a concrete fix \u2014 most commonly using single quotes on the outside (YAML) with double quotes as the OTTL string delimiter, or using a YAML block scalar (|- or >-), or doubling the backslashes to survive both parsing layers. Avoids vague \"check your escaping\" without a working example.",
"max_score": 2,
"category": "RUBRIC"
}
]
}
yaml-ottl-quoting-escape
You are a Coralogix support expert. A user has asked the following question:
---
I'm trying to clean up span names with a transform processor's replace_pattern, but the collector refuses to start and logs: "statement has invalid syntax: 1:28: invalid quoted string '\"cycle-(manager|rpa-manager)\\\\.[0-9a-f]{8}-[0-9a-f]{4}-...\"': invalid syntax". My regex works fine in a regex tester. What's wrong with how I'm writing it in the YAML?
---
Cardinality Reduction with OTTL
High cardinality is the most common reason customers add OTTL processors to their pipelines. These patterns are drawn from real configurations.
---
Metric cardinality: keep_keys (allowlist)
keep_keys removes every attribute NOT in the list. Safer than many delete_key calls when you know exactly what you need.
processors:
transform:
error_mode: ignore
metric_statements:
- context: resource
statements:
# Keep only the labels needed for dashboards/alerts; drop everything else
- keep_keys(attributes, ["service.name", "k8s.namespace.name", "k8s.deployment.name"])
- context: datapoint
statements:
# Scope to specific metrics using where — don't trim labels on all metrics
- keep_keys(attributes, ["span.name", "service.name", "span.kind", "status_code", "http.method", "le"]) where name == "calls" or name == "duration"---
Metric cardinality: delete_matching_keys (bulk denylist)
Use when you want to remove a category of labels (e.g. all process or OS attributes) without listing every individual key.
processors:
transform:
error_mode: ignore
metric_statements:
- context: resource
statements:
# Remove all process, OS, host, telemetry SDK metadata — common cardinality sources
- delete_matching_keys(attributes, "(?i)^(process|os|host|telemetry|container)\\.")
- delete_matching_keys(attributes, "(?i)^(aws|azure|gcp)\\.")
# Remove specific high-cardinality keys
- delete_key(attributes, "service.instance.id")
- delete_key(attributes, "service.version")
- delete_key(attributes, "process.command_args")
- delete_key(attributes, "process.command")
- context: datapoint
statements:
# Datapoint attributes may have different keys than resource
- delete_key(attributes, "process.command_args")
- delete_key(attributes, "url.scheme")
- delete_key(attributes, "network.protocol.version")---
Span/trace cardinality: normalize dynamic IDs in URLs and span names
Dynamic path segments (UUIDs, numeric IDs, MongoDB ObjectIds) cause unbounded cardinality in span names and http.url / http.target attributes.
processors:
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
# Replace UUIDs, MongoDB ObjectIds, and numeric IDs with a placeholder
- replace_pattern(attributes["http.url"], "/([0-9a-fA-F]{24,32}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9]+)(/|$|\\?)", "/:id$2")
# Strip query strings from URLs entirely
- replace_pattern(attributes["http.url"], "\\?.*$", "")
# Replace numeric IDs in span name path segments
- replace_pattern(name, "/[0-9]+(/|$)", "/:id$1")Delete span attributes by key prefix
Use delete_matching_keys when a whole family of span attributes should be removed, such as captured request headers. Run it in context: span so attributes means span attributes, not resource or metric datapoint attributes.
processors:
transform/remove-span-headers:
error_mode: ignore
trace_statements:
- context: span
statements:
- delete_matching_keys(attributes, "^http\\.request\\.header\\.")Example: GraphQL resolver cardinality
GraphQL spans produce names like graphql.resolve User.profile.name, graphql.resolve User.posts[0]. Truncate to the first field access:
trace_statements:
- context: span
statements:
- replace_pattern(name, "^(graphql\\.resolve\\s+[a-zA-Z\\.]+).*$", "$1")When configured via the otel-integration Helm preset
If the customer is setting these patterns through opentelemetry-agent.presets.spanMetrics.spanNameReplacePattern in values.yaml rather than writing the transform processor directly, the same two escape rules that apply to any collector config apply here:
1. Single-quote the regex in YAML (or use a block scalar) so backslashes aren't consumed by YAML's double-quoted-string parser — Rule 1 above. 2. Write `$1` / `$2` backreferences as `$$1` / `$$2`. The collector's envprovider expands $... references at startup; $$ is the literal-$ escape. This is a collector rule, not a Helm rule — Helm passes $ through unchanged.
Symptom when Rule 1 is wrong: statement has invalid syntax: 1:28: invalid quoted string ... invalid syntax on helm upgrade, even though the regex is valid in a tester. Symptom when Rule 2 is wrong: no startup error, but replacements produce empty output.
Verify with helm template -f values.yaml | grep -A 20 transform/span_name before upgrading. Worked example: skills/opentelemetry/opentelemetry-collector/references/setup-kubernetes.md — "Escape layers in collector config YAML".
---
Span/trace cardinality: normalize db.query.text
Raw database query strings have near-infinite cardinality. Reduce to query type:
processors:
transform:
error_mode: silent # attribute-missing errors are expected here
trace_statements:
- context: span
conditions:
- attributes["db.query.text"] != nil
statements:
- set(attributes["db.query.text"], "INSERT") where IsMatch(attributes["db.query.text"], "(?i)^insert\\s")
- set(attributes["db.query.text"], "UPDATE") where IsMatch(attributes["db.query.text"], "(?i)^update\\s")
- set(attributes["db.query.text"], "DELETE") where IsMatch(attributes["db.query.text"], "(?i)^delete\\s")
- set(attributes["db.query.text"], "SELECT_AGGREGATE") where IsMatch(attributes["db.query.text"], "(?i)select.*(count|sum|max|min|exists)\\s*\\(")
- set(attributes["db.query.text"], "SELECT_JOIN") where IsMatch(attributes["db.query.text"], "(?i)select.*\\s+join\\s+")
- set(attributes["db.query.text"], "SELECT_DISTINCT") where IsMatch(attributes["db.query.text"], "(?i)^select\\s+distinct\\s+")
- set(attributes["db.query.text"], "SELECT") where IsMatch(attributes["db.query.text"], "(?i)^select\\s")---
Log body cardinality: keep only required fields
When log bodies are structured maps (e.g. JSON-parsed k8s events), keep only the fields you need. Always guard with `IsMap(body)` — indexing a non-map body causes INVALID_ARGUMENT.
processors:
transform:
error_mode: ignore
log_statements:
- context: log
conditions:
- IsMap(body)
statements:
- keep_keys(body, ["type", "action", "reason", "note", "metadata", "regarding", "eventTime"])---
Span metrics: limit dimensions with spanmetrics connector
OTTL keep_keys reduces attributes before the spanmetrics connector sees them. Apply in the pipeline that feeds into spanmetrics, not after:
processors:
transform/pre-spanmetrics:
error_mode: ignore
trace_statements:
- context: resource
statements:
- keep_keys(attributes, ["service.name", "k8s.deployment.name", "k8s.namespace.name"])
- context: span
statements:
- keep_keys(attributes, ["http.method", "http.route", "http.response.status_code", "db.system", "span.kind", "status_code"])
service:
pipelines:
traces/spanmetrics:
receivers: [forward/spans]
processors: [transform/pre-spanmetrics]
exporters: [spanmetrics]You can also configure the spanmetrics connector's dimensions: directly to limit which attributes become metric labels — that is the preferred approach when the connector supports it.
OTTL Contexts
Each OTTL statement runs in a context that defines which telemetry object attributes and other path expressions refer to. Choosing the wrong context is the most common OTTL mistake.
Context Reference
| Context | Available in | What attributes means |
|---|---|---|
resource | transform (log/trace/metric) | Resource-level attributes |
scope | transform (log/trace/metric) | Instrumentation scope attributes |
log | transform (log), filter (logs) | Log record attributes |
span | transform (trace), filter (traces) | Span attributes |
spanevent | transform (trace) | Span event attributes |
metric | transform (metric), filter (metrics) | Metric-level fields (name, description, unit) |
datapoint | transform (metric), filter (metrics) | Individual datapoint attributes |
For the full field list available in each context, see the OTTL context documentation.
Path Expressions
Within a context, paths navigate the telemetry structure using dot notation and bracket notation:
attributes["key"] # record-level attributes (log/span/datapoint-level)
resource.attributes["key"] # resource-level attributes (accessible from any context)
instrumentation_scope.name # scope name
body # log body (log context only)
time # log event timestamp as time.Time (log context only)
time_unix_nano # log event timestamp as epoch nanoseconds (log context only)
severity_number # log severity number (log context only)
name # span name (span context) or metric name (metric context)
status.code # span status code (span context)
duration # span duration in nanoseconds (span context)Access nested maps with chained brackets:
attributes["http.request.headers"]["content-type"]
body["event"]["metadata"]["user_id"]The Number-One Mistake: Wrong Context for Attribute Access
In context: log or context: span, attributes["key"] refers to the record-level attribute. Resource attributes live on the resource, not the record. Always use resource.attributes["key"] to reach resource scope from a log or span context.
# WRONG — in context: log, k8s.namespace.name lives on the resource, not the log record
# This silently sets environment to nil with no error
log_statements:
- context: log
statements:
- set(attributes["environment"], attributes["k8s.namespace.name"])
# CORRECT — reach resource attributes from log context
log_statements:
- context: log
statements:
- set(attributes["environment"], resource.attributes["k8s.namespace.name"])
# ALSO CORRECT — use context: resource to mutate resource attributes directly
log_statements:
- context: resource
statements:
- set(attributes["environment"], attributes["k8s.namespace.name"]) # both are resource attrsExporters that read from resource attributes
Some exporters pick their destination from attributes and read those keys from the resource, not the log record or span. The most common example is the Coralogix exporter:
exporters:
coralogix:
application_name_attributes: ["application"] # read from resource.attributes
subsystem_name_attributes: ["subsystem"] # read from resource.attributesIf the application emits these fields on the log record (attributes["subsystem"], attributes["log.file.subsystem"], …), the exporter won't see them and the application / subsystem arrive blank. The fix is not to rename the field in the application or to change the exporter config — it's to copy the value up to resource scope with OTTL:
# Application sets attributes["log.file.subsystem"]; exporter expects resource.attributes["subsystem"]
processors:
transform:
error_mode: ignore
log_statements:
- context: log
statements:
- set(resource.attributes["subsystem"], attributes["log.file.subsystem"]) where attributes["log.file.subsystem"] != nil
# same pattern for application_name_attributes
- set(resource.attributes["application"], attributes["service.namespace"]) where attributes["service.namespace"] != nilOrder matters: place the transform processor before the coralogix exporter in the pipeline. The same pattern applies to any exporter that reads routing/destination keys from resource attributes.
Metric Context: metric vs datapoint vs resource
This is the second most common mistake. Metric attributes live at different levels:
| What you want to access/change | Context to use |
|---|---|
| Metric name, description, unit | metric |
| Per-datapoint attributes (labels) | datapoint |
| Resource attributes on the metric | resource |
metric_statements:
- context: metric
statements:
- replace_pattern(name, "_total$", "") # strip Prometheus suffix from name
- context: resource
statements:
- keep_keys(attributes, ["service.name", "k8s.namespace.name"]) # trim resource labels
- context: datapoint
statements:
- delete_key(attributes, "process.command_args") # trim per-datapoint labelsThe conditions: Block
Use conditions: to scope an entire statement block. More efficient than adding where to every individual statement.
Multiple entries under `conditions:` are OR'd — statements run if any listed condition is true. This is the documented behavior in both the transform and filter processors.
# Single condition — statements run only when body is a map.
log_statements:
- context: log
conditions:
- IsMap(body)
statements:
- keep_keys(body, ["message", "level", "trace_id"])
- set(attributes["log.level"], body["level"])
# Two conditions — statements run if EITHER matches (OR). For strict AND,
# combine into a single boolean instead.
log_statements:
- context: log
conditions:
- IsMap(body) and attributes["source"] == "application"
statements:
- keep_keys(body, ["message", "level", "trace_id"])The where Clause
Every OTTL statement can be individually guarded with a where condition:
statements:
- set(attributes["env"], "production") where resource.attributes["k8s.namespace.name"] == "prod"
- set(attributes["env"], "staging") where resource.attributes["k8s.namespace.name"] == "staging"
- set(attributes["env"], "unknown") where attributes["env"] == nilNil Safety
OTTL returns nil (not an error) when a path does not exist. A nil value silently propagates. Guard before using a value:
# Safe: set only if source exists
- set(attributes["db.namespace"], attributes["db.name"]) where attributes["db.name"] != nil
# Safe: fallback chain — first non-nil value wins
- set(attributes["db.namespace"], attributes["db.name"]) where attributes["db.name"] != nil
- set(attributes["db.namespace"], attributes["server.address"]) where attributes["db.namespace"] == nil and attributes["server.address"] != nil
- set(attributes["db.namespace"], attributes["net.peer.name"]) where attributes["db.namespace"] == nil and attributes["net.peer.name"] != nilOperators
| Operator | Example |
|---|---|
| Equality | attributes["env"] == "prod" |
| Inequality | attributes["env"] != "dev" |
| Comparison | severity_number >= SEVERITY_NUMBER_WARN |
| Nil check | attributes["key"] == nil |
| Logical and | attributes["a"] == "x" and attributes["b"] == "y" |
| Logical or | attributes["env"] == "prod" or attributes["env"] == "staging" |
| Logical not | not IsMatch(body, "health.*") |
| Pattern match | IsMatch(attributes["url"], "^/api/v[0-9]+/.*") |
| Type check | IsMap(body), IsString(attributes["retries"]), IsInt(attributes["retries"]) |
| Span status enum | status.code == STATUS_CODE_ERROR |
The full operator and literal reference is in the OTTL grammar.
Body Type Guards
IsMap(body) is required before map indexing. IsString(body) is required before string operations on the body. Log bodies can be maps, strings, or empty — never assume the type.
log_statements:
- context: log
statements:
# Map indexing — guard with IsMap
- keep_keys(body, ["message", "level", "trace_id"]) where IsMap(body)
# String operations — guard with IsString
- replace_pattern(body, "token=[^&]+", "token=REDACTED") where IsString(body)
# Debugging: surface body type to verify what you're actually receiving
- set(attributes["debug.body_type"], "map") where IsMap(body)
- set(attributes["debug.body_type"], "string") where IsString(body)When IsMap(body) is false but the body is a JSON string, use ParseJSON() to convert it to a map before indexing. See transformations for the full pattern.
Filtering with OTTL
The filter processor drops records that match the given condition. A match = drop.
Place filter before `transform` in the pipeline. If you need enrichment attributes to make the filter decision (e.g., k8s labels), place the enrichment processor before filter.
---
Log filtering
processors:
filter:
error_mode: ignore
logs:
log_record:
# Drop DEBUG and TRACE — reduces volume by 20-40% in many environments
- severity_number < SEVERITY_NUMBER_INFO
# Drop health check hits by URL pattern in body
- IsMatch(body, "GET /(healthz?|readyz?|ping|livez?) HTTP/[0-9\\.]+ 200")
# Drop by service name
- resource.attributes["service.name"] == "noisy-sidecar"
# Drop by service name pattern
- IsMatch(resource.attributes["service.name"], "^(fluent-bit|prometheus-agent)$")
# Drop by severity + service combo (drop high-volume low-severity from specific services)
- 'IsMatch(resource.attributes["service.name"], "(metrics-proxy|telemetry-agent)") and severity_number <= 9'Drop by namespace (denylist)
filter:
error_mode: ignore
logs:
log_record:
- resource.attributes["k8s.namespace.name"] == "kube-system"
- resource.attributes["k8s.namespace.name"] == "cert-manager"
- resource.attributes["k8s.namespace.name"] == "monitoring"Keep only specific namespaces (allowlist)
The filter processor drops records that match — to keep only specific namespaces, drop everything else:
filter:
error_mode: ignore
logs:
log_record:
- not (resource.attributes["k8s.namespace.name"] == "prod" or resource.attributes["k8s.namespace.name"] == "payments")---
Metric filtering
processors:
filter:
error_mode: ignore
metrics:
metric:
# Drop by exact name
- name == "go.goroutines"
# Drop all Go runtime metrics
- IsMatch(name, "^go\\..*")
# Drop by prefix
- IsMatch(name, "^runtime\\..*")Drop metrics that lack a required attribute
filter:
error_mode: ignore
metrics:
datapoint:
# Drop datapoints with no service.name (unidentifiable, noisy)
- resource.attributes["service.name"] == nil
# Drop only datapoints missing a per-series label; keep the metric and
# any sibling datapoints that still match the backend contract.
- attributes["tenant"] == nilUse metrics.datapoint for per-series/per-point filtering. A metrics.metric condition evaluates the whole metric and can remove every datapoint under that metric, which is not what you want when only some datapoints are invalid.
---
Trace filtering
processors:
filter:
error_mode: ignore
traces:
span:
# Drop health check endpoints
- attributes["http.target"] == "/healthz"
- attributes["http.target"] == "/readyz"
- IsMatch(attributes["http.url"], ".*/(healthz?|readyz?|ping)$")
# Drop very short spans (sub-millisecond — typically noise)
- duration < 1000000 # nanoseconds
# Drop spans from a specific service
- resource.attributes["service.name"] == "k6-load-test"
# Drop non-DB spans in a DB-specific pipeline
- attributes["db.system"] == nil
# Drop spans where service is not the one you care about
- resource.attributes["service.name"] != "api-gateway"---
Tail sampling with OTTL conditions
Tail sampling policies support OTTL via ottl_condition. This runs at the span level, not the trace level — combine with other policy types (status_code, probabilistic, rate_limiting) using and policies.
processors:
tail_sampling:
policies:
# Force sample traces that have a custom "force_sample" attribute set
- name: force-sample
type: ottl_condition
ottl_condition:
error_mode: ignore
span:
- 'attributes["force_sample"] == "true" or attributes["http.request.header.x-debug"] == "true"'
# Sample error spans from specific operations at 5%
- name: sample-noisy-errors
type: and
and:
and_sub_policy:
- name: is-error
type: status_code
status_code:
status_codes: [ERROR]
- name: is-noisy-span
type: ottl_condition
ottl_condition:
error_mode: ignore
span:
- 'IsMatch(name, "^(exec_binder|getContentDetail|getContentRating)$")'
- name: probabilistic
type: probabilistic
probabilistic:
sampling_percentage: 5
# Sample all other errors at 25%
- name: sample-other-errors
type: and
and:
and_sub_policy:
- name: is-error
type: status_code
status_code:
status_codes: [ERROR]
- name: exclude-noisy
type: ottl_condition
ottl_condition:
error_mode: ignore
span:
- 'not IsMatch(name, "^(exec_binder|getContentDetail|getContentRating)$")'
- name: probabilistic
type: probabilistic
probabilistic:
sampling_percentage: 25
# Sample 5xx HTTP errors with a rate limit
- name: http-5xx-errors
type: and
and:
and_sub_policy:
- name: is-5xx
type: ottl_condition
ottl_condition:
error_mode: ignore
span:
- 'IsMatch(attributes["http.status_code"], "5..")'
- name: probabilistic
type: probabilistic
probabilistic:
sampling_percentage: 10.0
- name: rate-limit
type: rate_limiting
rate_limiting:
spans_per_second: 400
# Decentralized sampling control: services set sampling.rule resource attribute
- name: service-defined-full-sample
type: ottl_condition
ottl_condition:
error_mode: ignore
span:
- 'IsMatch(resource.attributes["sampling.rule"], ".*-100-sampling$")'Important limitation: OTTL conditions in tail sampling evaluate at span level, but sampling decisions are made at trace level. A condition that matches only some spans in a trace does not selectively drop those spans — it influences whether the whole trace is sampled.
OTTL Processors
Three Collector components accept OTTL expressions: the transform processor (mutate), the filter processor (drop), and the routing connector (split pipelines).
---
transform processor
The transform processor mutates telemetry in-place. Statements are grouped by signal type (log_statements, trace_statements, metric_statements) and context.
Always set `error_mode: ignore` or `error_mode: silent`. The default propagate halts the pipeline on any runtime error, dropping all subsequent records.
ignore— logs the error and continuessilent— suppresses the error and continues (use when attribute-missing errors are expected)
processors:
transform:
error_mode: ignore
log_statements:
- context: resource
statements:
- set(attributes["environment"], attributes["deployment.environment"])
- context: log
conditions:
- IsMap(body)
statements:
- keep_keys(body, ["message", "level", "timestamp", "trace_id", "span_id"])
trace_statements:
- context: span
conditions:
- attributes["http.route"] != nil
statements:
- set(name, Concat([attributes["http.request.method"], attributes["http.route"]], " "))
metric_statements:
- context: metric
statements:
- replace_pattern(name, "_total$", "")
- context: resource
statements:
- keep_keys(attributes, ["service.name", "k8s.namespace.name", "k8s.deployment.name"])
- context: datapoint
statements:
- delete_key(attributes, "process.command_args")
- delete_key(attributes, "process.command")Editor vs Converter functions
OTTL functions split into two categories with different usage rules:
- Editors (lowercase first letter):
set(),delete_key(),keep_keys(),replace_pattern()— modify telemetry in place, used as statements - Converters (uppercase first letter):
IsMatch(),SHA256(),ParseJSON(),Concat(),Split(),ExtractPatterns()— return a value, used as expressions inside statements or conditions
This means ParseJSON(body) alone on a statement line is invalid — it returns a map but does nothing with it. Wrap it in an editor: merge_maps(attributes, ParseJSON(body), "insert").
Commonly used transform functions
For the full, always-current list see ottlfuncs README:
| Function | What it does |
|---|---|
set(target, value) | Set a field to a value |
delete_key(map, key) | Remove a single key from a map |
delete_matching_keys(map, pattern) | Remove all keys matching a regex |
keep_keys(map, [keys]) | Remove all keys not in the list (allowlist) |
replace_pattern(target, regex, replacement) | Replace regex matches in a string field |
replace_all_patterns(map, "value", regex, replacement) | Replace pattern in all map values |
merge_maps(target, source, strategy) | Merge two attribute maps |
truncate_all(map, limit) | Truncate all string values to a max length |
limit(map, limit, [keys]) | Cap the number of keys in a map |
IsMatch(value, regex) | Boolean — does value match regex |
IsMap(value) | Boolean — is value a map (use to guard body access) |
IsString(value) | Boolean — is value a string |
ParseJSON(value) | Parse a JSON string into a map (Converter — use inside merge_maps or indexing) |
Concat(values, separator) | Concatenate a list of strings |
Split(value, delimiter) | Split string, returns list (index with [0], [1], ...) |
Substring(value, start, length) | Extract a substring by position |
ExtractPatterns(value, regex) | Extract named capture groups, returns a map |
SHA256(value) | SHA-256 hash of a string (useful for pseudonymizing PII) |
Int(value) | Convert to integer |
Double(value) | Convert to floating-point number |
String(value) | Convert to string |
Time(value, format, [location], [locale]) | Parse a string into a timestamp |
IsInt(value) / IsDouble(value) / IsString(value) | Guard type-specific comparisons and conversions |
---
filter processor
The filter processor drops records that match the given OTTL condition. A match means drop.
processors:
filter:
error_mode: ignore
logs:
log_record:
- severity_number < SEVERITY_NUMBER_INFO # drop DEBUG and TRACE
- IsMatch(body, "^.*GET /healthz.*200.*$") # drop health check hits
- resource.attributes["service.name"] == "noisy-exporter" # drop from a specific service
- 'IsMatch(resource.attributes["service.name"], ".(ccrecognition|monitoring).") and severity_number > 9'
metrics:
metric:
- name == "go.goroutines" # drop by exact metric name
- IsMatch(name, "^go\\..*") # drop all Go runtime metrics
traces:
span:
- attributes["http.target"] == "/healthz" # drop health check spans
- attributes["db.system"] != nil # drop all DB spans
- duration < 1000000 # drop spans under 1ms (nanoseconds)Severity number constants (use these instead of integers):
| Constant | Severity |
|---|---|
SEVERITY_NUMBER_TRACE | 1 |
SEVERITY_NUMBER_DEBUG | 5 |
SEVERITY_NUMBER_INFO | 9 |
SEVERITY_NUMBER_WARN | 13 |
SEVERITY_NUMBER_ERROR | 17 |
SEVERITY_NUMBER_FATAL | 21 |
severity_number < SEVERITY_NUMBER_INFO drops both TRACE and DEBUG levels.
For current constants and the full field list, see the filter processor documentation.
---
routing connector
The routing connector splits a pipeline into multiple downstream pipelines based on OTTL conditions. Use this when different data needs different processors or exporters.
connectors:
routing:
error_mode: ignore
default_pipelines: [traces/sampled]
table:
- statement: route() where attributes["force_sample"] == "true"
pipelines: [traces/full]
- statement: route() where resource.attributes["k8s.namespace.name"] == "prod"
pipelines: [traces/prod]
service:
pipelines:
traces/ingress:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [routing]
traces/full:
receivers: [routing]
exporters: [otlp/backend]
traces/prod:
receivers: [routing]
processors: [tail_sampling]
exporters: [otlp/backend]
traces/sampled:
receivers: [routing]
processors: [tail_sampling]
exporters: [otlp/backend]---
Pipeline ordering
service:
pipelines:
logs:
processors: [filter, transform, batch]- filter first — drop unwanted records before spending CPU transforming them
- transform after filter — only process records that will actually be exported
- Exception: if you need to enrich attributes before filtering on them (e.g., add
k8sattributesbefore a filter that usesk8s.namespace.name), put enrichment processors beforefilter - batch last — always the final processor
---
When to use attributes processor instead
For simple attribute operations (add, update, delete) without conditions, the attributes processor is simpler than OTTL:
processors:
attributes:
actions:
- key: sensitive_field
action: delete
- key: env
value: production
action: insertUse transform when you need: where conditions, cross-context access (resource.attributes from a span context), functions like replace_pattern/keep_keys, or conditions: blocks.
---
YAML + OTTL quoting collision
OTTL statements embedded in YAML have to be valid on two parsing layers: the YAML parser strips its own quoting and escapes first, then the OTTL parser reads what is left as a string literal. A replace_pattern regex like "cycle-(manager|rpa-manager)\\.[0-9a-f]{8}-..." that works in a standalone regex tester can fail at Collector startup with:
statement has invalid syntax: 1:28: invalid quoted stringThis is a syntax error, not an OTTL bug or a bad regex — the backslashes and quotes were consumed twice. Three concrete fixes, in order of readability:
processors:
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
# 1. YAML single quotes outside, OTTL double quotes inside — cleanest
- 'replace_pattern(name, "cycle-(manager|rpa-manager)\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "cleaned-span-name")'
# 2. YAML block scalar — backslashes survive untouched, good for very long expressions
- >-
replace_pattern(name,
"cycle-(manager|rpa-manager)\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
"cleaned-span-name")
# 3. YAML double quotes outside — must double every backslash to survive YAML
- "replace_pattern(name, \"cycle-(manager|rpa-manager)\\\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\", \"cleaned-span-name\")"Rule of thumb: YAML single quotes outside, OTTL double quotes inside. The OTTL string literal keeps its normal double quotes and you only escape for OTTL, not for YAML.
OTTL Data Redaction and PII Masking
Patterns for redacting, masking, and pseudonymizing sensitive data before telemetry leaves the collector. All patterns use the transform processor.
---
Pseudonymize with SHA256
Replace a sensitive value with its hash rather than dropping it. Preserves correlation (same input always hashes to the same output) without exposing raw identifiers.
log_statements:
- context: log
statements:
- set(attributes["user.id"], SHA256(attributes["user.id"])) where attributes["user.id"] != nil
trace_statements:
- context: span
statements:
- set(attributes["user.id"], SHA256(attributes["user.id"])) where attributes["user.id"] != nil---
Mask credit card numbers
log_statements:
- context: log
statements:
# Mask PAN in a structured body field
- replace_pattern(body["message"], "[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}", "[CARD_REDACTED]") where IsMap(body) and body["message"] != nil
# Mask PAN in plain-string body
- replace_pattern(body, "[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}", "[CARD_REDACTED]") where IsString(body)---
Redact Authorization headers in spans
replace_all_patterns iterates over all values in a map and applies the replacement where the pattern matches — avoids listing every possible header key.
trace_statements:
- context: span
statements:
- replace_all_patterns(attributes, "value", "(?i)^(bearer|basic)\\s+.+$", "$1 [REDACTED]")---
Drop an attribute matching a PII pattern
Use when the attribute itself is the PII and should not appear at all:
log_statements:
- context: log
statements:
- delete_key(attributes, "user.email") where IsMatch(attributes["user.email"], ".+@.+\\..+")---
Redact tokens from URL query strings
log_statements:
- context: log
statements:
- replace_pattern(attributes["http.url"], "([?&])(token|api_key|secret|password)=[^&]+", "$1$2=[REDACTED]") where attributes["http.url"] != nil
trace_statements:
- context: span
statements:
- replace_pattern(attributes["url.full"], "([?&])(token|api_key|secret|password)=[^&]+", "$1$2=[REDACTED]") where attributes["url.full"] != nil---
Redact from plain-string body
log_statements:
- context: log
statements:
- replace_pattern(body, "([?&])(token|api_key|secret|password)=[^&]+", "$1$2=[REDACTED]") where IsString(body)
- replace_pattern(body, "[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}", "[CARD_REDACTED]") where IsString(body)---
Truncate long attribute values
Prevents large blobs (stack traces, request bodies) from inflating storage:
log_statements:
- context: log
statements:
- truncate_all(attributes, 512) # cap all attribute values at 512 charsCommon OTTL Transformations
Patterns drawn from real customer configurations for span naming, semantic convention migration, attribute extraction, and log body manipulation.
---
Span naming from HTTP method + route
Generic span names like HTTP GET or http.request lose HTTP context. Reconstruct from method + route. Use Split to strip query strings from http.target:
processors:
transform:
error_mode: ignore
trace_statements:
- context: span
conditions:
- attributes["span.kind"] == "server" or attributes["span.kind"] == "SPAN_KIND_SERVER"
statements:
# Prefer explicit http.route if present
- set(name, Concat([attributes["http.request.method"], attributes["http.route"]], " ")) where attributes["http.route"] != nil and attributes["http.request.method"] != nil
# Fallback: extract path from http.target (strips ?query=string)
- set(attributes["http.route"], Split(attributes["http.target"], "?")[0]) where attributes["http.route"] == nil and attributes["http.target"] != nil
- set(name, Concat([attributes["http.request.method"], attributes["http.route"]], " ")) where attributes["http.route"] != nil and attributes["http.request.method"] != nil
# Old convention fallback (http.method / http.url)
- set(name, Concat([attributes["http.method"], attributes["http.route"]], " ")) where attributes["http.route"] != nil and attributes["http.method"] != nil
# Guard: set a meaningful fallback if name is still generic
- set(name, attributes["http.route"]) where name == "http.request" or name == "HTTP GET" or name == "HTTP POST"---
Span name from database query summary
Replace raw database span names with a readable query summary attribute when available:
trace_statements:
- context: span
conditions:
- attributes["db.query.summary"] != nil
statements:
- set(name, attributes["db.query.summary"])---
Database attribute normalization (fallback chain)
Database spans from different systems (SQL, MongoDB, Redis, Cassandra, DynamoDB) use different attribute names. Map them to db.namespace and db.collection.name:
processors:
transform:
error_mode: silent
trace_statements:
- context: span
conditions:
- attributes["db.system"] != nil
statements:
# db.namespace: pick the first available source
- set(attributes["db.namespace"], attributes["db.name"]) where attributes["db.namespace"] == nil and attributes["db.name"] != nil
- set(attributes["db.namespace"], attributes["server.address"]) where attributes["db.namespace"] == nil and attributes["server.address"] != nil
- set(attributes["db.namespace"], attributes["net.peer.name"]) where attributes["db.namespace"] == nil and attributes["net.peer.name"] != nil
- set(attributes["db.namespace"], attributes["db.system"]) where attributes["db.namespace"] == nil
# db.collection.name: varies by database system
- set(attributes["db.collection.name"], attributes["db.sql.table"]) where attributes["db.collection.name"] == nil and attributes["db.sql.table"] != nil
- set(attributes["db.collection.name"], attributes["db.mongodb.collection"]) where attributes["db.collection.name"] == nil and attributes["db.mongodb.collection"] != nil
- set(attributes["db.collection.name"], attributes["db.cassandra.table"]) where attributes["db.collection.name"] == nil and attributes["db.cassandra.table"] != nil
- set(attributes["db.collection.name"], attributes["db.elasticsearch.path_parts.index"]) where attributes["db.collection.name"] == nil
- set(attributes["db.collection.name"], attributes["db.cosmosdb.container"]) where attributes["db.collection.name"] == nil
- set(attributes["db.collection.name"], attributes["aws.dynamodb.table_names"]) where attributes["db.collection.name"] == nil
- set(attributes["db.collection.name"], attributes["db.namespace"]) where attributes["db.collection.name"] == nil and attributes["db.system"] == "redis"---
Extract table name from raw SQL statement
When db.sql.table is not populated, extract it from the raw SQL string using named capture groups:
trace_statements:
- context: span
conditions:
- attributes["db.sql.table"] == nil
- attributes["db.statement"] != nil
statements:
- set(attributes["db.sql.table"], ExtractPatterns(attributes["db.statement"], "(?i)(?:update|insert\\s+into|delete\\s+from|select[\\s\\S]+?from)\\s+['\"]?(?P<table>[a-zA-Z_][a-zA-Z0-9_]*)['\"]?")["table"])---
HTTP semantic convention migration (v1 → v2)
OpenTelemetry HTTP semantic conventions changed in v2. Instrumentation libraries may emit either version. Use transform as a compatibility shim:
processors:
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
# Method: http.method → http.request.method
- set(attributes["http.request.method"], attributes["http.method"]) where attributes["http.request.method"] == nil and attributes["http.method"] != nil
# Status code: http.status_code → http.response.status_code
- set(attributes["http.response.status_code"], attributes["http.status_code"]) where attributes["http.response.status_code"] == nil and attributes["http.status_code"] != nil
# URL: http.url → url.full
- set(attributes["url.full"], attributes["http.url"]) where attributes["url.full"] == nil and attributes["http.url"] != nil
# Auto-set span status from HTTP response code
- set(status.code, STATUS_CODE_ERROR) where attributes["http.response.status_code"] != nil and attributes["http.response.status_code"] > 399---
Prometheus metric name normalization
Remove the _total suffix that Prometheus adds — it becomes redundant after ingestion in most backends:
metric_statements:
- context: metric
statements:
- replace_pattern(name, "_total$", "")---
Log body operations
Always guard with `IsMap(body)` before indexing into log body fields. A non-map body (plain string, empty) causes INVALID_ARGUMENT without the guard.
log_statements:
- context: log
conditions:
- IsMap(body)
statements:
# Keep only fields needed — discard the rest
- keep_keys(body, ["message", "level", "timestamp", "trace_id", "span_id", "error"])
# Promote a body field to a log attribute
- set(attributes["log.level"], body["level"]) where body["level"] != nil
# Mask sensitive values within the body map
- replace_pattern(body["user_email"], "^(.+)$", "[REDACTED]") where body["user_email"] != nil---
Parsing a JSON string body with ParseJSON
When the log body arrives as a JSON string (not an already-parsed map), IsMap(body) returns false and body indexing fails. Use ParseJSON() to convert the string to a map first.
Key distinction: IsMap(body) guards map indexing on a pre-parsed map body. IsString(body) + ParseJSON() handles a raw JSON string that the receiver has not yet parsed. Both patterns are needed depending on what your log receiver produces.
log_statements:
- context: log
conditions:
- IsString(body)
statements:
# Merge all JSON fields from the body string into log attributes
- merge_maps(attributes, ParseJSON(body), "insert")
- context: log
conditions:
- IsMap(body)
statements:
# Body is already a parsed map — index directly
- set(attributes["trace_id"], body["trace_id"]) where body["trace_id"] != nilTo extract a single field inline without merging the whole body:
log_statements:
- context: log
statements:
- set(attributes["trace_id"], ParseJSON(body)["trace_id"]) where IsString(body) and ParseJSON(body)["trace_id"] != nilmerge_maps strategies: "insert" (add only, don't overwrite), "update" (overwrite only existing), "upsert" (add and overwrite).
---
Parsing and setting log timestamps
OTTL can set a log record's event timestamp from context: log. The destination is time for a time.Time value or time_unix_nano for an epoch-nanosecond integer. Do not use a made-up timestamp path.
processors:
transform/log-time:
error_mode: ignore
log_statements:
- context: log
statements:
- set(time, Time(attributes["event_time"], "%Y-%m-%dT%H:%M:%S%z")) where IsString(attributes["event_time"])Time(value, format, optional_location, optional_locale) parses a string using the format you provide. Guard with IsString, set error_mode: ignore or silent, and make the format/time zone explicit. A mismatch between the incoming timestamp string and the format, missing time-zone data, or invalid input values will produce transform errors.
---
Type-safe numeric comparisons
Attribute values can arrive as strings, integers, or doubles depending on the receiver and source library. Do not compare attributes["retries"] > 3 until you know the value is numeric or have converted it.
trace_statements:
- context: span
statements:
- set(attributes["retry_bucket"], "high") where IsInt(attributes["retries"]) and attributes["retries"] > 3
- set(attributes["retry_bucket"], "high") where IsString(attributes["retries"]) and Int(attributes["retries"]) > 3Use Double(...) instead of Int(...) for decimal values, and keep error_mode: ignore or a stricter where guard around conversions when malformed strings are possible.
---
Building dynamic values with Concat and Split
trace_statements:
- context: span
statements:
# Build span name from method + route
- set(name, Concat([attributes["http.request.method"], " ", attributes["http.route"]], ""))
# Build a compound key
- set(attributes["db.operation.full"], Concat([attributes["db.operation.name"], ".", attributes["db.collection.name"]], ""))
log_statements:
- context: log
statements:
# Extract the path component from a full URL (strip query string)
- set(attributes["http.path"], Split(attributes["http.url"], "?")[0]) where attributes["http.url"] != nil
# Extract first segment of a log source path
- set(attributes["log.source"], Split(attributes["log.file.path"], "/")[1]) where attributes["log.file.path"] != nil---
Setting span status from attributes
trace_statements:
- context: span
statements:
# Mark as error if HTTP status >= 400
- set(status.code, STATUS_CODE_ERROR) where attributes["otel.status_code"] == nil and attributes["http.response.status_code"] != nil and attributes["http.response.status_code"] > 399
# Mark as ok if status < 400
- set(status.code, STATUS_CODE_OK) where attributes["otel.status_code"] == nil and attributes["http.response.status_code"] != nil and attributes["http.response.status_code"] < 400Prefer the span-context enum constants STATUS_CODE_UNSET, STATUS_CODE_OK, and STATUS_CODE_ERROR over raw numbers. For checks, write conditions like status.code == STATUS_CODE_ERROR in context: span.
---
Injecting environment variables as resource attributes
Use the resource processor (not transform) for static values from environment variables:
processors:
resource/add-env:
attributes:
- key: deployment.environment
value: "${env:ENVIRONMENT}"
action: insert
- key: collector.instance.id
value: "${env:HOSTNAME}"
action: insertFor dynamic values that require conditions or functions, use transform/context: resource instead.