OpenTelemetry Collector deployment, instrumentation (Java/Python/Node.js/.NET/Go), and OTTL pipeline transforms for Coralogix — coralogix exporter config, Helm chart selection, Kubernetes topology, ECS/EKS/GKE deployments, SDK setup, APM transactions, and OTTL cardinality/PII/routing.
98
97%
Does it follow best practices?
Impact
99%
1.13xAverage score across 81 eval scenarios
Advisory
Suggest reviewing before use
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 | 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.
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"]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 attrsSome 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.
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 labelsconditions: BlockUse 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"])where ClauseEvery 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"] == nilOTTL 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"] != nil| 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.
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.
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
scenario-57
scenario-58
scenario-59
scenario-60
scenario-61
scenario-62
scenario-63
scenario-64
scenario-65
scenario-66
scenario-67
scenario-68
scenario-69
scenario-70
scenario-71
scenario-72
scenario-73
scenario-74
scenario-75
scenario-76
scenario-77
scenario-78
scenario-79
scenario-80
scenario-81
skills
opentelemetry
opentelemetry-collector
references
opentelemetry-instrumentation
opentelemetry-ottl