Skip to content

Introduction to Rhai Scripting

Learn how to write simple Rhai scripts to filter and transform log events. This tutorial bridges the gap between basic CLI usage and advanced scripting.

What You'll Learn

  • Understand the event object (e) and field access
  • Write simple filter expressions with --filter
  • Transform events with basic -e scripts
  • Use string operations and conditionals
  • Convert between types safely
  • Understand why pipeline order matters
  • Debug scripts with -F inspect and --verbose

Prerequisites

Sample Data

This tutorial uses examples/basics.jsonl - the same small JSON log file from the basics tutorial:

{"timestamp":"2024-01-15T10:00:00Z","level":"INFO","service":"api","message":"Application started","version":"1.2.3"}
{"timestamp":"2024-01-15T10:00:10Z","level":"DEBUG","service":"database","message":"Connection pool initialized","max_connections":50}
{"timestamp":"2024-01-15T10:01:00Z","level":"WARN","service":"api","message":"High memory usage detected","memory_percent":85}
{"timestamp":"2024-01-15T10:01:30Z","level":"ERROR","service":"database","message":"Query timeout","query":"SELECT * FROM users","duration_ms":5000}
{"timestamp":"2024-01-15T10:02:00Z","level":"INFO","service":"api","message":"Request received","method":"GET","path":"/api/users"}
{"timestamp":"2024-01-15T10:03:00Z","level":"ERROR","service":"auth","message":"Account locked","username":"admin","attempts":5}

Step 1: Understanding the Event Object

Every event in Kelora is represented as a map (dictionary) accessible via the variable e in your Rhai scripts.

Accessing fields:

e.level          # Direct field access
e["level"]       # Bracket notation (useful for dynamic fields)
e.status         # Access any field in the event

Let's see the structure with -F inspect:

kelora -j examples/basics.jsonl -F inspect
---
timestamp | string | "2024-01-15T10:00:00Z"
level     | string | "INFO"
message   | string | "Application started"
service   | string | "api"
version   | string | "1.2.3"
---
timestamp       | string | "2024-01-15T10:00:10Z"
level           | string | "DEBUG"
message         | string | "Connection pool initialized"
service         | string | "database"
max_connections | int    | 50
---
timestamp      | string | "2024-01-15T10:01:00Z"
level          | string | "WARN"
message        | string | "High memory usage detected"
service        | string | "api"
memory_percent | int    | 85
---
timestamp   | string | "2024-01-15T10:01:30Z"
level       | string | "ERROR"
message     | string | "Query timeout"
service     | string | "database"
query       | string | "SELECT * FROM users"
duration_ms | int    | 5000
---
timestamp | string | "2024-01-15T10:02:00Z"
level     | string | "INFO"
message   | string | "Request received"
service   | string | "api"
method    | string | "GET"
path      | string | "/api/users"
---
timestamp | string | "2024-01-15T10:03:00Z"
level     | string | "ERROR"
message   | string | "Account locked"
service   | string | "auth"
username  | string | "admin"
attempts  | int    | 5

Key insight: Field names become properties you can access in scripts.


Step 2: Simple Filter Expressions

Use --filter to keep only events where the expression returns true.

Filter by String Equality

Keep only ERROR level events:

kelora -j examples/basics.jsonl --filter 'e.level == "ERROR"'
timestamp='2024-01-15T10:01:30Z' level='ERROR' message='Query timeout' service='database'
  query='SELECT * FROM users' duration_ms=5000
timestamp='2024-01-15T10:03:00Z' level='ERROR' message='Account locked' service='auth'
  username='admin' attempts=5

What happened: Only events where e.level equals "ERROR" are kept.

Filter by Numeric Comparison

Keep only slow queries (duration > 1000ms):

kelora -j examples/basics.jsonl --filter 'e.duration_ms > 1000'
timestamp='2024-01-15T10:01:30Z' level='ERROR' message='Query timeout' service='database'
  query='SELECT * FROM users' duration_ms=5000

Important: This only keeps events that have a duration_ms field. Events without it are skipped.

Combine Conditions with Logical Operators

Find ERROR or WARN events from the database service:

kelora -j examples/basics.jsonl \
    --filter 'e.level in ["ERROR", "WARN"] && e.service == "database"'
timestamp='2024-01-15T10:01:30Z' level='ERROR' message='Query timeout' service='database'
  query='SELECT * FROM users' duration_ms=5000

Operators:

  • == - Equals
  • != - Not equals
  • >, >=, <, <= - Comparison
  • && - AND
  • || - OR
  • ! - NOT
  • in - Check membership in array (e.g., e.level in ["ERROR", "WARN"])

Step 3: Basic Transformations with --exec

Use --exec (or -e for short) to modify events or add new fields. We'll use -e in all examples below.

Add a Computed Field

Convert milliseconds to seconds:

kelora -j examples/basics.jsonl \
    -e 'e.duration_s = e.duration_ms / 1000' \
    --filter 'e.duration_s > 1.0' \
    -k timestamp,service,duration_ms,duration_s
timestamp='2024-01-15T10:01:30Z' service='database' duration_ms=5000 duration_s=5

Key insight: --exec runs before --filter, so the new field is available for filtering.

Modify Existing Fields

Normalize level to uppercase:

kelora -j examples/basics.jsonl \
    -e 'e.level = e.level.to_upper()'
timestamp='2024-01-15T10:00:00Z' level='INFO' message='Application started' service='api'
  version='1.2.3'
timestamp='2024-01-15T10:00:10Z' level='DEBUG' message='Connection pool initialized'
  service='database' max_connections=50
timestamp='2024-01-15T10:01:00Z' level='WARN' message='High memory usage detected' service='api'
  memory_percent=85
timestamp='2024-01-15T10:01:30Z' level='ERROR' message='Query timeout' service='database'
  query='SELECT * FROM users' duration_ms=5000
timestamp='2024-01-15T10:02:00Z' level='INFO' message='Request received' service='api' method='GET'
  path='/api/users'
timestamp='2024-01-15T10:03:00Z' level='ERROR' message='Account locked' service='auth'
  username='admin' attempts=5

Step 4: String Operations

Rhai provides powerful string methods for text processing.

Check if String Contains Text

Find events with "timeout" in the message:

kelora -j examples/basics.jsonl \
    --filter 'e.message.contains("timeout")'
timestamp='2024-01-15T10:01:30Z' level='ERROR' message='Query timeout' service='database'
  query='SELECT * FROM users' duration_ms=5000

Extract Parts of Strings

Extract just the error type from messages:

kelora -j examples/basics.jsonl \
    --filter 'e.level == "ERROR"' \
    -e 'e.error_type = e.message.split(" ")[0]' \
    -k timestamp,service,error_type,message
timestamp='2024-01-15T10:01:30Z' service='database' error_type='Query' message='Query timeout'
timestamp='2024-01-15T10:03:00Z' service='auth' error_type='Account' message='Account locked'

Common string methods:

  • contains(substr) - Check if string contains text
  • starts_with(prefix) - Check prefix
  • ends_with(suffix) - Check suffix
  • split(sep) - Split into array
  • to_upper() / to_lower() - Change case
  • trim() - Remove whitespace
  • len() - String length

Step 5: Conditionals and Logic

Use if/else to make decisions in your transforms.

Add Severity Classification

kelora -j examples/basics.jsonl \
    -e 'e.severity = if e.level in ["ERROR", "CRITICAL"] { "high" } else if e.level == "WARN" { "medium" } else { "low" }' \
    -k level,severity,service,message
level='INFO' severity='low' service='api' message='Application started'
level='DEBUG' severity='low' service='database' message='Connection pool initialized'
level='WARN' severity='medium' service='api' message='High memory usage detected'
level='ERROR' severity='high' service='database' message='Query timeout'
level='INFO' severity='low' service='api' message='Request received'
level='ERROR' severity='high' service='auth' message='Account locked'

Syntax:

if condition {
    // then branch
} else if another_condition {
    // else-if branch
} else {
    // else branch
}


Step 6: Type Conversions

Fields may be strings when you need numbers (or vice versa). Convert types safely.

Safe Conversion with Fallbacks

Use to_int_or() to handle conversion failures:

echo '{"id":"123","status":"200","invalid":"abc"}
{"id":"456","status":"404","invalid":"xyz"}' | \
    kelora -j \
    -e 'e.id_num = e.id.to_int_or(-1);
        e.status_num = e.status.to_int_or(0);
        e.invalid_num = e.invalid.to_int_or(999)' \
    -k id,id_num,status,status_num,invalid,invalid_num
id='123' id_num=123 status='200' status_num=200 invalid='abc' invalid_num=999
id='456' id_num=456 status='404' status_num=404 invalid='xyz' invalid_num=999

Note: Multiple statements in one -e are separated by semicolons and share the same scope. Use this when operations are related or when you need to share let variables.

Safe conversion functions:

  • to_int_or(fallback) - Convert to integer or use fallback
  • to_float_or(fallback) - Convert to float or use fallback
  • to_string() - Convert to string (always succeeds)

Step 7: Pipeline Order Matters

The order of --filter and -e flags determines execution order.

Wrong Order: Filter Before Creating Field

This won't work because duration_s doesn't exist yet:

# WRONG - will fail
kelora -j examples/basics.jsonl \
    --filter 'e.duration_s > 1.0' \
    -e 'e.duration_s = e.duration_ms / 1000'

Correct Order: Create Field Before Filtering

kelora -j examples/basics.jsonl \
    -e 'e.duration_s = e.duration_ms / 1000' \
    --filter 'e.duration_s > 1.0' \
    -k service,duration_s,message
service='database' duration_s=5 message='Query timeout'

Rule: Fields must exist before you filter on them. Scripts run in CLI order.


Step 8: Checking if Fields Exist

Not all events have the same fields. Use has() to check before accessing.

Safe Field Access

kelora -j examples/basics.jsonl \
    -e 'e.slow = if e.has("duration_ms") { e.duration_ms > 1000 } else { false }' \
    -k service,slow,message
service='api' slow=false message='Application started'
service='database' slow=false message='Connection pool initialized'
service='api' slow=false message='High memory usage detected'
service='database' slow=true message='Query timeout'
service='api' slow=false message='Request received'
service='auth' slow=false message='Account locked'

Pattern:

if e.has("field_name") {
    // Safe to access e.field_name
} else {
    // Provide default behavior
}


Step 9: Debugging Your Scripts

When scripts don't work as expected, use these techniques.

Use -F inspect to See Field Types

When a filter isn't working as expected, use -F inspect to see what fields exist and their types:

echo '{"id":"42","count":"100"}
{"id":99,"count":200}' | kelora -j -F inspect
---
id    | string | "42"
count | string | "100"
---
id    | int | 99
count | int | 200

Output shows: Field name, type (string/int/etc.), and value. Notice how id and count are strings in the first event but integers in the second - this explains why count > 50 would fail on the first event!

Use --verbose to See Errors

When scripts fail in resilient mode, use --verbose to see what went wrong:

echo '{"value":"not_a_number"}' | \
    kelora -j \
    -e 'e.num = e.value.to_int()' \
    --verbose
kelora: Executing stage 1 (exec)
value='not_a_number'

Debug workflow:

  1. Use -F inspect to check field types
  2. Use --verbose to see error messages
  3. Use --strict to fail fast on first error
  4. Add temporary fields to see intermediate values

Step 10: Multi-Stage Pipelines

Chain multiple -e and --filter stages for complex logic.

Progressive Refinement

kelora -j examples/basics.jsonl \
    -e 'e.is_error = e.level in ["ERROR", "CRITICAL"];
        e.is_slow = e.has("duration_ms") && e.duration_ms > 1000;
        e.needs_attention = e.is_error || e.is_slow' \
    --filter 'e.needs_attention' \
    -k service,level,is_error,is_slow,message
service='database' level='ERROR' is_error=true is_slow=true message='Query timeout'
service='auth' level='ERROR' is_error=true is_slow=false message='Account locked'

Pattern: Build up computed fields step-by-step, then filter on the final result.


Next Steps

For complete Rhai syntax reference, see the Rhai Cheatsheet.

For all built-in functions: kelora --help-functions


Practice Exercises

Try these on your own:

Exercise 1: Filter by Service

Filter for events from the database service:

Solution
kelora -j examples/basics.jsonl --filter 'e.service == "database"'

Exercise 2: Flag High Memory Usage

Add a high_memory field and filter for events with memory usage above 80%:

Solution
kelora -j examples/basics.jsonl \
    -e 'e.high_memory = e.has("memory_percent") && e.memory_percent > 80' \
    --filter 'e.high_memory' \
    -k service,memory_percent,message

Exercise 3: Flag Critical Security Events

Add a critical field that's true for ERROR events with failed login attempts:

Solution
kelora -j examples/basics.jsonl \
    -e 'e.critical = e.level == "ERROR" && e.has("attempts")' \
    --filter 'e.critical' \
    -k service,message,attempts

Summary

You've learned:

  • Access event fields with e.field_name
  • Filter events with --filter boolean expressions
  • Transform events with -e scripts
  • Use string methods like .contains(), .split(), .to_upper()
  • Convert types safely with to_int_or(), to_float_or()
  • Write conditionals with if/else
  • Check field existence with has()
  • Debug with -F inspect and --verbose
  • Understand pipeline order (exec before filter)
  • Build multi-stage pipelines

Next Steps

Now that you understand basic Rhai scripting, continue to:

Related guides: