Custom VRL
Custom transformations in Scanner are written using VRL (Vector Remap Language). Use VRL to normalize, enrich, or restructure your log data before it's indexed.
Custom VRL transformations give you full control over your data pipeline, enabling use cases beyond what the built-in transformations provide.
Creating a Custom Transformation
Navigate to Transformations
Go to Library → Transformations to view all transformation steps.
Create a new transformation
Click the + button to create a new transformation. Enter a descriptive name (e.g., "Normalize Nginx Logs" or "Tag Internal IPs").
Write your VRL code
Use the VRL code editor to write your transformation logic. The editor provides links to VRL documentation and the VRL Playground for testing.
Use in Index Rules
Your custom transformation is now available when configuring transformation steps in your Index Rules.
Writing VRL
How Transformations Work
VRL transformations modify the log event object (.) in place. The return value of the last expression is ignored—only changes made to . affect the output.
# return exits early, but does NOT modify the log event
# "unmodified" is ignored and the log event (.) passes through unmodified
if bool(.skip_transform) ?? false {
return "unmodified"
}
# This modifies the log event - .new_field will be in the output
.new_field = "hello"You can also reassign . entirely. Scanner expects the final value to be either an object (outputting one log event) or an array of objects (outputting multiple log events).
Available Functions
Scanner supports most of the VRL standard library.
Error Handling
Important: When a VRL error occurs, Scanner stops ingestion of the entire file and requires manual intervention to fix the transformation and re-process the data. Handle errors gracefully in your VRL code to avoid ingestion failures.
Pattern 1: The ?? Operator (Fallback Values)
?? Operator (Fallback Values)Use the ?? operator to provide a fallback value when a fallible function fails:
# If .status is not a valid integer, default to 0
.status_code = to_int(.status) ?? 0
# If parsing fails, keep the original value
.parsed_message = parse_json(.message) ?? .messagePattern 2: Explicit Error Checking
For more control, capture the error and check explicitly:
parsed, err = parse_json(.message)
if err == null {
.message_parsed = parsed
.parse_status = "success"
} else {
.parse_status = "failed"
.parse_error = to_string(err)
}Handling Unwanted Events
If you encounter a log event that doesn't meet your expectations (e.g., missing required fields, invalid format), don't use assert! to reject it—that will fail ingestion of the entire file.
Instead, drop the log event conditionally by returning an empty array:
Example Transformations
Normalizing Custom Logs to ECS
Parse a custom nginx access log format and normalize fields to Elastic Common Schema (ECS).
parsed, err = parse_regex(.message, r'^(?P<client_ip>\S+) - (?P<user>\S+) \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>[^"]+)" (?P<status>\d+) (?P<bytes>\d+)')
if err == null {
."@ecs".source.ip = parsed.client_ip
."@ecs".user.name = if parsed.user != "-" { parsed.user } else { null }
."@ecs".http.request.method = parsed.method
."@ecs".url.path = parsed.path
."@ecs".http.version = parsed.protocol
."@ecs".http.response.status_code = to_int(parsed.status) ?? 0
."@ecs".http.response.body.bytes = to_int(parsed.bytes) ?? 0
}Tagging Events by IP Range
Use ip_cidr_contains() to classify events based on source IP address.
Flattening Arrays (1:N Transform)
Expand a single log event containing an array into multiple log events. This gives you more control than the built-in Unroll Array transformation.
Dropping Events Conditionally
Filter out unwanted log events by returning an empty array. This is useful for removing noise like health checks, heartbeats, or debug logs.
drop_event = false
if .url.path == "/health" || .url.path == "/ready" || .url.path == "/ping" {
drop_event = true
}
if drop_event {
. = []
return 0 # early exit; return value is ignored
}Enriching with Lookup Tables
Add contextual information from a custom lookup table. This example enriches log events with employee full name and department based on username.
First, create a lookup table (employees):
username,full_name,department
alice,Alice Smith,Engineering
bob,Bob Jones,SalesThen use it in your VRL transformation:
username = .user.name
if username == null { username = .userName }
if username != null {
employee, err = get_enrichment_table_record(
"employees",
{"username": username},
["full_name", "department"]
)
if err == null {
.user.full_name = employee.full_name
.user.department = employee.department
} else {
.user.department = "unknown"
}
}CIDR-Based Tagging with Lookup Tables
Tag log events based on IP address ranges stored in a lookup table. This is useful when you have many CIDR ranges that change frequently and you want to manage them outside of VRL code.
First, create a lookup table (cidr_tags):
cidr,tag
172.31.124.0/24,scanner_production
10.0.0.0/8,rfc1918_private
172.16.0.0/12,rfc1918_private
192.168.0.0/16,rfc1918_private
54.0.0.0/8,aws_public
0.0.0.0/0,public_internetThen use it in your VRL transformation:
records = find_enrichment_table_records!("cidr_tags", {})
src_ip, err = to_string(.sourceIPAddress)
if err == null && is_ipv4(src_ip) {
matched = false
for_each(records) -> |_index, record| {
if !matched {
result, err = ip_cidr_contains(record.cidr, src_ip)
if err == null && result == true {
.network_tag = record.tag
matched = true
}
}
}
}Note: This pattern iterates through every row of the lookup table for every log event. It works well for small to medium CSV files (hundreds of rows), but may impact performance with very large tables. Order your CSV with more specific CIDR ranges first (e.g., /24 before /8) so the first match is the most specific.
Resources
VRL Playground - Test your VRL code interactively
VRL Documentation - Complete VRL language reference
VRL Functions - Complete VRL function reference
Custom Lookup Tables - Enrichment table functions
Data Transformations - Built-in transformation options
Last updated
Was this helpful?