The OpenTelemetry Transformation Language (OTTL) is a powerful language for transforming, filtering, and enriching telemetry data inside the OpenTelemetry Collector. This guide covers everything you need to know to write effective OTTL expressions for production observability.
OTTL allows you to manipulate telemetry data without changing application code. You can:
OTTL expressions can be used in multiple OpenTelemetry Collector components:
Navigate telemetry data using dot notation:
span.name
span.attributes["http.method"]
resource.attributes["service.name"]
log.body["message"]
metric.name
The first segment in a path expression defines the context:
| Context | Description | Use With |
|---|---|---|
resource |
Resource-level attributes | All signal types |
scope |
Instrumentation scope | All signal types |
span |
Span data | Traces |
spanevent |
Span events | Traces |
metric |
Metric metadata | Metrics |
datapoint |
Metric data points | Metrics |
log |
Log records | Logs |
| Category | Operators | Example |
|---|---|---|
| Assignment | = |
set(span.attributes["env"], "prod") |
| Comparison | ==, !=, >, <, >=, <= |
span.status.code == STATUS_CODE_ERROR |
| Logical | and, or, not |
IsMatch(metric.name, "^http") and resource.attributes["env"] == "prod" |
Use where clauses to apply transformations conditionally:
set(span.attributes["region"], "us-east-1") where resource.attributes["cloud.provider"] == "aws"
Always check for nil to avoid creating attributes that don’t exist:
span.attributes["user.id"] != nil
| Function | Purpose | Example |
|---|---|---|
set |
Set a field to a value | set(span.attributes["env"], "production") |
delete_key |
Remove a key from a map | delete_key(span.attributes, "internal.key") |
delete_matching_keys |
Remove keys matching a pattern | delete_matching_keys(span.attributes, "^temp\\.") |
replace_pattern |
Replace text matching regex | replace_pattern(log.body["string"], "\\d{4}-\\d{4}-\\d{4}-\\d{4}", "****-****-****-****") |
merge_maps |
Merge two maps | merge_maps(span.attributes, resource.attributes, "upsert") |
flatten |
Flatten nested structures | flatten(span.attributes, "nested.", 2) |
limit |
Limit map size | limit(span.attributes, 50, ["user.id", "trace.id"]) |
truncate_all |
Truncate string values | truncate_all(span.attributes, 1024) |
| Function | Purpose | Example |
|———-|———|———|
| IsString | Check if value is string | IsString(span.attributes["user.id"]) |
| IsInt | Check if value is integer | IsInt(span.attributes["http.status_code"]) |
| IsMap | Check if value is map | IsMap(span.attributes["metadata"]) |
| IsMatch | Check if string matches regex | IsMatch(metric.name, "^k8s\\.") |
| Function | Purpose | Example |
|———-|———|———|
| String | Convert to string | String(span.attributes["http.status_code"]) |
| Int | Convert to integer | Int(span.attributes["duration_ms"]) |
| Double | Convert to float | Double(span.attributes["cpu_usage"]) |
| Bool | Convert to boolean | Bool(span.attributes["is_error"]) |
| Function | Purpose | Example |
|———-|———|———|
| Concat | Join strings | Concat([resource.attributes["service.name"], span.name], ".") |
| Substring | Extract substring | Substring(log.body["string"], 0, 100) |
| ToLowerCase | Convert to lowercase | ToLowerCase(span.attributes["http.method"]) |
| Split | Split string into array | Split(span.attributes["tags"], ",") |
| Trim | Remove whitespace | Trim(log.body["message"]) |
| Function | Purpose | Example |
|———-|———|———|
| ParseJSON | Parse JSON string | ParseJSON(log.body["json_data"]) |
| ExtractPatterns | Extract regex groups | ExtractPatterns(log.body["string"], "user=(?P<user>\\w+)") |
| ParseKeyValue | Parse key-value pairs | ParseKeyValue(log.body["kv_data"], "=", " ") |
| Function | Purpose | Example |
|———-|———|———|
| SHA256 | SHA256 hash | SHA256(span.attributes["user.email"]) |
| MD5 | MD5 hash | MD5(span.attributes["session.id"]) |
| Function | Purpose | Example |
|———-|———|———|
| Now | Current time | Now() |
| UnixNano | Convert to nanoseconds | UnixNano(Now()) |
| FormatTime | Format time string | FormatTime(log.time, "2006-01-02") |
# Set static value
set(resource.attributes["deployment.environment"], "production")
# Set based on condition
set(span.status.code, STATUS_CODE_ERROR) where span.attributes["http.status_code"] >= 500
# Copy from another field
set(span.attributes["region"], resource.attributes["cloud.region"])
# Redact authorization headers
set(span.attributes["http.request.header.authorization"], "REDACTED") where span.attributes["http.request.header.authorization"] != nil
# Mask credit card numbers
replace_pattern(log.body["string"], "\\b(\\d{4})\\d{8,12}(\\d{4})\\b", "$1****$2")
# Hash email addresses
set(span.attributes["user.email"], SHA256(span.attributes["user.email"])) where span.attributes["user.email"] != nil
# Add cluster information
set(resource.attributes["k8s.cluster.name"], "prod-cluster-us-west-2")
# Extract namespace from pod name
set(resource.attributes["k8s.namespace.name"], Split(resource.attributes["k8s.pod.name"], "-")[0])
# Set environment based on namespace
set(resource.attributes["deployment.environment"], "production") where IsMatch(resource.attributes["k8s.namespace.name"], "^prod-")
set(resource.attributes["deployment.environment"], "staging") where IsMatch(resource.attributes["k8s.namespace.name"], "^staging-")
# Limit attribute count to prevent cardinality explosion
limit(span.attributes, 20, ["service.name", "http.method", "http.status_code"])
# Delete high-cardinality attributes
delete_matching_keys(span.attributes, "^user\\.(id|email)")
# Truncate long strings
truncate_all(span.attributes, 256)
# Route errors to separate pipeline
span.status.code == STATUS_CODE_ERROR
# Route specific services
resource.attributes["service.name"] == "critical-service"
# Route by metric name pattern
IsMatch(metric.name, "^system\\.")
For complex parsing operations, use a temporary map to store intermediate results:
# Parse complex log message into temp map
set(cache["parsed"], ParseJSON(log.body["string"]))
# Extract specific fields
set(span.attributes["user.id"], cache["parsed"]["userId"])
set(span.attributes["request.id"], cache["parsed"]["requestId"])
# Clean up temp map
delete_key(cache, "parsed")
# Step 1: Parse URL
set(cache["url"], URL(span.attributes["http.url"]))
# Step 2: Extract host and path
set(span.attributes["http.host"], cache["url"]["host"])
set(span.attributes["http.route"], cache["url"]["path"])
# Step 3: Clean query parameters
set(span.attributes["http.url"], Concat([cache["url"]["scheme"], "://", cache["url"]["host"], cache["url"]["path"]], ""))
# Step 4: Clean up
delete_key(cache, "url")
# Safe parsing with error handling
set(span.attributes["parsed_data"], ParseJSON(span.attributes["json_string"])) where IsString(span.attributes["json_string"])
# Defensive attribute access
set(span.attributes["method"], span.attributes["http.request.method"]) where span.attributes["http.request.method"] != nil
processors:
transform/redact-pii:
error_mode: ignore
trace_statements:
- context: span
statements:
# Redact authorization headers
- set(span.attributes["http.request.header.authorization"], "REDACTED") where span.attributes["http.request.header.authorization"] != nil
- set(span.attributes["http.request.header.cookie"], "REDACTED") where span.attributes["http.request.header.cookie"] != nil
# Hash user identifiers
- set(span.attributes["user.email"], SHA256(span.attributes["user.email"])) where span.attributes["user.email"] != nil
log_statements:
- context: log
statements:
# Mask credit card numbers in log bodies
- replace_pattern(log.body["string"], "\\b(\\d{4})[\\d\\s-]{8,12}(\\d{4})\\b", "$1-****-****-$2")
# Remove social security numbers
- replace_pattern(log.body["string"], "\\b\\d{3}-\\d{2}-\\d{4}\\b", "***-**-****")
processors:
transform/k8s-enrichment:
error_mode: ignore
trace_statements:
- context: resource
statements:
# Add cluster information
- set(resource.attributes["k8s.cluster.name"], "prod-us-west-2")
# Set environment based on namespace
- set(resource.attributes["deployment.environment"], "production") where IsMatch(resource.attributes["k8s.namespace.name"], "^prod-")
- set(resource.attributes["deployment.environment"], "staging") where IsMatch(resource.attributes["k8s.namespace.name"], "^(staging|test)-")
- set(resource.attributes["deployment.environment"], "development") where IsMatch(resource.attributes["k8s.namespace.name"], "^dev-")
processors:
transform/http-enhancement:
error_mode: ignore
trace_statements:
- context: span
statements:
# Normalize HTTP methods
- set(span.attributes["http.method"], ToUpperCase(span.attributes["http.method"]))
# Set span status based on HTTP status code
- set(span.status.code, STATUS_CODE_ERROR) where span.attributes["http.status_code"] >= 400
# Extract route pattern from URL
- set(span.attributes["http.route"], replace_pattern(span.attributes["http.target"], "/\\d+", "/{id}"))
processors:
transform/db-sanitization:
error_mode: ignore
trace_statements:
- context: span
statements:
# Sanitize SQL queries by removing literal values
- replace_pattern(span.attributes["db.statement"], "'[^']*'", "'?'")
- replace_pattern(span.attributes["db.statement"], "\\b\\d+\\b", "?")
# Limit query length
- set(span.attributes["db.statement"], Substring(span.attributes["db.statement"], 0, 1000))
Always set error_mode explicitly in your processor configuration:
processors:
transform:
error_mode: ignore # Recommended for production
trace_statements:
- context: span
statements:
- set(span.attributes["env"], "production")
| Mode | Behavior | When to Use |
|---|---|---|
propagate |
Stops processing on error | Development and testing |
ignore |
Logs error, continues processing | Production (recommended) |
silent |
Ignores errors without logging | High-volume pipelines |
log_level: debug in your collector configuration# BAD: comparing string to int
span.attributes["status_code"] == 200
# GOOD: convert to int first
Int(span.attributes["status_code"]) == 200
# BAD: expensive regex in inner loop
IsMatch(span.attributes["trace_id"], "^[a-f0-9]{32}$")
# GOOD: use simpler checks when possible
Len(span.attributes["trace_id"]) == 32
When using OTTL with Dynatrace, consider these patterns:
Enrich spans with metadata that makes DQL queries more effective:
# Add query-friendly tags
set(span.attributes["dt.service_tier"], "frontend") where IsMatch(span.name, "^(http|web|ui)")
set(span.attributes["dt.service_tier"], "backend") where IsMatch(span.name, "^(db|sql|mongo)")
set(span.attributes["dt.error_type"], "timeout") where IsMatch(span.status.message, "(?i)timeout")
# Standardize resource attributes for Dynatrace dashboards
set(resource.attributes["dt.entity.host"], resource.attributes["host.name"])
set(resource.attributes["dt.entity.service"], resource.attributes["service.name"])
For more examples and patterns, see the BMAD OTTL Examples directory.