Transform plugin

July 31, 2026 · View on GitHub

It transforms events with programs written in a small expression language. A single transform action can rename, reshape, parse and delete fields — work that otherwise takes a chain of single-purpose actions.

Example:

pipelines:
  example_pipeline:
    ...
    actions:
    - type: transform
      source: |
        # parse lines like "INFO 2025-05-25 11:11:11,222 [shard 1] compaction - done"
        m = capture(.log, r'^(?P<level>\S+)\s+(?P<time>\S+ \S+)\s+\[(?P<shard>[^\]]+)\]\s+(?P<operation>\S+)\s+-\s+(?P<message>.+)$')
        if m != null {
          .level = m.level
          .time = m.time
          .shard = m.shard
          .operation = m.operation
          .message = m.message
          del .log
        }
    ...

The event {"log":"INFO 2025-05-25 11:11:11,222 [shard 1] compaction - done"} becomes:

{
  "level": "INFO",
  "time": "2025-05-25 11:11:11,222",
  "shard": "shard 1",
  "operation": "compaction",
  "message": "done"
}

The program is compiled once at pipeline start; an invalid program fails fast at startup. A runtime error (e.g. a type error on a particular event) stops the program for that event only: the error is logged and the event continues down the pipeline, keeping the fields that were set before the error.

Config params

source string

The transformation program executed for every event. See the language reference and the list of built-in functions below.


Language

A program is a list of expressions executed top to bottom for every event. Expressions are separated by newlines, or by ; when written on one line. Comments start with # and last to the end of the line.

Event fields

Paths address fields of the current event and always start with a dot:

.level                    # top-level field
.user.name                # nested field
."key with spaces"        # quoted field name
.items[0]                 # array element
.items[-1]                # array element, counted from the end
.items[i]                 # index from a variable

Reading a missing path returns null — it is never an error, so checks like if .user.name == null { ... } are safe on any event.

Assigning to a path writes the field and creates missing parent objects on the way:

.a.b.c = 1                # {} -> {"a":{"b":{"c":1}}}

del removes a field (no-op when the field does not exist):

del .user.password

Path dots must be written tightly: .a.b is one path, while .a .b is a compile error.

Values and literals

typeliterals
integer42, 0
float3.14, 1e10, 1.5e-3
string"hello" with \n, \t, \" escapes
raw strings'C:\new\path' — backslashes kept as is
regexr'\d+' — compiled at startup
timestampt'2024-01-15T10:30:00Z' — RFC3339, 2006-01-02T15:04:05 or 2006-01-02
booltrue, false
nullnull
array[1, "two", true]
object{level: "info", "other key": 2}

In conditions null and false are falsy; every other value is truthy.

Variables

Variables hold intermediate values and live for one event:

name = .user.name             # read a field into a variable
parts = capture(.log, r'...') # keep a function result
.out = name                   # write it back to the event

Fields of object values are accessed with a dot or an index; both forms are assignable, and arrays grow with nulls when assigned past their end:

parts.level                   # same as parts["level"]
arr[0] = 1
obj.key = "value"

Member access dots follow the same rule as paths: m.level is member access, m .level is a compile error.

Operators

In order of increasing precedence:

operatorsmeaning
=assignment, right-associative: a = b = 1
||logical or, short-circuit
&&logical and, short-circuit
== !=equality (integers and floats compare numerically)
< <= > >=comparison of numbers, strings or timestamps
+ -addition, subtraction; + also concatenates strings
* / %multiplication, division, modulo; division by zero is a runtime error
! - (unary)negation
f() a[i] a.bcall, index, member access

+ concatenates only strings with strings — convert other values first: "code " + string(.code).

Control flow

if .status >= 500 {
  .severity = "crit"
} else if .status >= 400 {
  .severity = "warn"
} else {
  .severity = "ok"
}

for i, item in .items {      # iterate an array; use _ to skip a variable
  .items[i] = item
}

if .level == "DEBUG" {
  abort                      # stop the program for this event
}

abort only stops the transform program — the event itself continues down the pipeline.

Functions

Functions are called with positional and named arguments. Positional arguments are required; named arguments are optional and fall back to their defaults: fn(value, option: "x").

  • upcase(value) — returns the string in upper case.

    .level = upcase(.level)    # "info" -> "INFO"
    
  • string(value) — converts any value to its string representation; null becomes an empty string. Use it to build strings from non-string fields:

    .msg = "code is " + string(.code)
    
  • capture(value, pattern) — matches the string against a regular expression and returns an object of its named groups (?P<name>...), or null when the value does not match (unnamed groups are ignored):

    m = capture(.log, r'^(?P<level>\S+)\s+(?P<message>.+)$')
    if m != null {
      .level = m.level
      .message = m.message
    }
    
  • after(value, separator) — returns everything after the first occurrence of separator; the value is returned unchanged when the separator is not found.

    .message = after(.log, " - ")
    
  • before(value, separator) — returns everything before the first occurrence of separator; unchanged when not found.

    .level = before(.log, " ")
    
  • between(value, open, close) — returns the text between the first open and the following close; unchanged when either delimiter is not found.

    .shard = between(.log, "[", "]")
    


Generated using insane-doc