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 --exec scripts
  • Use string operations and conditionals
  • Convert between types safely
  • Understand why pipeline order matters
  • Debug scripts with -F inspect and --verbose
  • Avoid common mistakes

Prerequisites

Sample Data

This tutorial uses examples/simple_json.jsonl - application logs with various services and events.

Preview the data:

kelora -j examples/simple_json.jsonl --take 5
kelora -j examples/simple_json.jsonl --take 5
timestamp='2024-01-15T10:00:00Z' level='INFO' message='Application started' service='api'
  version='1.2.3'
timestamp='2024-01-15T10:00:05Z' level='DEBUG' message='Loading configuration' service='api'
  config_file='/etc/app/config.yml'
timestamp='2024-01-15T10:00:10Z' level='INFO' 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

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/simple_json.jsonl -F inspect --take 1
kelora -j examples/simple_json.jsonl -F inspect --take 1
---
timestamp | string | "2024-01-15T10:00:00Z"
level     | string | "INFO"
message   | string | "Application started"
service   | string | "api"
version   | string | "1.2.3"

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/simple_json.jsonl --filter 'e.level == "ERROR"'
kelora -j examples/simple_json.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:30Z' level='ERROR' message='Account locked' service='auth'
  username='admin' attempts=5
timestamp='2024-01-15T10:16:00Z' level='ERROR' message='Service unavailable' service='api'
  reason='disk space'

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

Filter by Numeric Comparison

Keep only slow queries (duration > 1000ms):

kelora -j examples/simple_json.jsonl --filter 'e.duration_ms > 1000'
kelora -j examples/simple_json.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
timestamp='2024-01-15T10:10:00Z' level='INFO' message='Backup completed' service='scheduler'
  size_mb=1024 duration_ms=300000

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/simple_json.jsonl \
    --filter '(e.level == "ERROR" || e.level == "WARN") && e.service == "database"'
kelora -j examples/simple_json.jsonl \
    --filter '(e.level == "ERROR" || e.level == "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


Step 3: Basic Transformations with --exec

Use --exec to modify events or add new fields.

Add a Computed Field

Convert milliseconds to seconds:

kelora -j examples/simple_json.jsonl \
    --exec 'e.duration_s = e.duration_ms / 1000' \
    --filter 'e.duration_s > 1.0' \
    -k timestamp,service,duration_ms,duration_s
kelora -j examples/simple_json.jsonl \
    --exec '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
timestamp='2024-01-15T10:10:00Z' service='scheduler' duration_ms=300000 duration_s=300

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

Modify Existing Fields

Normalize level to uppercase:

kelora -j examples/simple_json.jsonl \
    --exec 'e.level = e.level.to_upper()' \
    --take 3
kelora -j examples/simple_json.jsonl \
    --exec 'e.level = e.level.to_upper()' \
    --take 3
timestamp='2024-01-15T10:00:00Z' level='INFO' message='Application started' service='api'
  version='1.2.3'
timestamp='2024-01-15T10:00:05Z' level='DEBUG' message='Loading configuration' service='api'
  config_file='/etc/app/config.yml'
timestamp='2024-01-15T10:00:10Z' level='INFO' message='Connection pool initialized'
  service='database' max_connections=50

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/simple_json.jsonl \
    --filter 'e.message.contains("timeout")'
kelora -j examples/simple_json.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/simple_json.jsonl \
    --filter 'e.level == "ERROR"' \
    --exec 'e.error_type = e.message.split(" ")[0]' \
    -k timestamp,service,error_type,message
kelora -j examples/simple_json.jsonl \
    --filter 'e.level == "ERROR"' \
    --exec '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:30Z' service='auth' error_type='Account' message='Account locked'
timestamp='2024-01-15T10:16:00Z' service='api' error_type='Service' message='Service unavailable'

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/simple_json.jsonl \
    --exec 'e.severity = if e.level == "ERROR" || e.level == "CRITICAL" {
                "high"
            } else if e.level == "WARN" {
                "medium"
            } else {
                "low"
            }' \
    -k level,severity,service,message \
    --take 5
kelora -j examples/simple_json.jsonl \
    --exec 'e.severity = if e.level == "ERROR" || e.level == "CRITICAL" { "high" } else if e.level == "WARN" { "medium" } else { "low" }' \
    -k level,severity,service,message \
    --take 5
level='INFO' severity='low' service='api' message='Application started'
level='DEBUG' severity='low' service='api' message='Loading configuration'
level='INFO' 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'

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

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 --exec 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/simple_json.jsonl \
    --filter 'e.duration_s > 1.0' \
    --exec 'e.duration_s = e.duration_ms / 1000'

✅ Correct Order: Create Field Before Filtering

kelora -j examples/simple_json.jsonl \
    --exec 'e.duration_s = e.duration_ms / 1000' \
    --filter 'e.duration_s > 1.0' \
    -k service,duration_s,message
kelora -j examples/simple_json.jsonl \
    --exec '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'
service='scheduler' duration_s=300 message='Backup completed'

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_field() to check before accessing.

Safe Field Access

kelora -j examples/simple_json.jsonl \
    --exec 'e.has_duration = e.has_field("duration_ms")' \
    --exec 'if e.has_field("duration_ms") {
                e.slow = e.duration_ms > 1000
            } else {
                e.slow = false
            }' \
    -k service,has_duration,slow,message \
    --take 5
kelora -j examples/simple_json.jsonl \
    --exec 'e.has_duration = e.has_field("duration_ms")' \
    --exec 'if e.has_field("duration_ms") { e.slow = e.duration_ms > 1000 } else { e.slow = false }' \
    -k service,has_duration,slow,message \
    --take 5
service='api' has_duration=false slow=false message='Application started'
service='api' has_duration=false slow=false message='Loading configuration'
service='database' has_duration=false slow=false message='Connection pool initialized'
service='api' has_duration=false slow=false message='High memory usage detected'
service='database' has_duration=true slow=true message='Query timeout'

Pattern:

if e.has_field("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 Types

kelora -j examples/simple_json.jsonl \
    --exec 'e.status = 200' \
    --exec 'e.computed = e.status * 2' \
    -F inspect --take 1
kelora -j examples/simple_json.jsonl \
    --exec 'e.status = 200' \
    --exec 'e.computed = e.status * 2' \
    -F inspect --take 1
---
timestamp | string | "2024-01-15T10:00:00Z"
level     | string | "INFO"
message   | string | "Application started"
service   | string | "api"
version   | string | "1.2.3"
status    | int    | 200
computed  | int    | 400

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 \
    --exec 'e.num = e.value.to_int()' \
    --verbose
echo '{"value":"not_a_number"}' | \
    kelora -j \
    --exec 'e.num = e.value.to_int()' \
    --verbose
🔹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 --exec and --filter stages for complex logic.

Progressive Refinement

kelora -j examples/simple_json.jsonl \
    --exec 'e.is_error = e.level == "ERROR" || e.level == "CRITICAL"' \
    --exec 'e.is_slow = e.has_field("duration_ms") && e.duration_ms > 1000' \
    --exec 'e.needs_attention = e.is_error || e.is_slow' \
    --filter 'e.needs_attention' \
    -k service,level,is_error,is_slow,message
kelora -j examples/simple_json.jsonl \
    --exec 'e.is_error = e.level == "ERROR" || e.level == "CRITICAL"' \
    --exec 'e.is_slow = e.has_field("duration_ms") && e.duration_ms > 1000' \
    --exec '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'
service='scheduler' level='INFO' is_error=false is_slow=true message='Backup completed'
service='disk' level='CRITICAL' is_error=true is_slow=false message='Disk space critical'
service='api' level='ERROR' is_error=true is_slow=false message='Service unavailable'

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


Common Mistakes and Solutions

❌ Mistake 1: Accessing Missing Fields

Problem:

kelora -j app.log --filter 'e.status >= 400'  # Fails if status doesn't exist

Solution:

kelora -j app.log --filter 'e.has_field("status") && e.status >= 400'


❌ Mistake 2: String vs Number Comparison

Problem:

# If status is string "200", this won't match
kelora -j app.log --filter 'e.status == 200'

Solution:

# Convert to int first
kelora -j app.log --filter 'e.status.to_int_or(0) == 200'


❌ Mistake 3: Wrong Pipeline Order

Problem:

# Field doesn't exist yet!
kelora -j app.log --filter 'e.is_slow' --exec 'e.is_slow = e.duration > 1000'

Solution:

# Create field first, then filter
kelora -j app.log --exec 'e.is_slow = e.duration > 1000' --filter 'e.is_slow'


❌ Mistake 4: Forgetting Quotes

Problem:

# Shell interprets && as command separator
kelora -j app.log --filter e.level == ERROR && e.service == api

Solution:

# Quote the entire expression
kelora -j app.log --filter 'e.level == "ERROR" && e.service == "api"'


Quick Reference

Accessing Fields

e.field_name              # Direct access
e["field_name"]           # Bracket notation
e.has_field("name")       # Check existence
e.get("name", default)    # Get with fallback

Filter Operators

==  !=                    # Equality
<  <=  >  >=              # Comparison
&&  ||  !                 # Logical AND, OR, NOT

String Methods

.contains("text")         # Check substring
.starts_with("pre")       # Check prefix
.ends_with("suf")         # Check suffix
.to_upper()  .to_lower()  # Change case
.split(" ")               # Split into array
.trim()                   # Remove whitespace
.len()                    # String length

Type Conversions

.to_int_or(fallback)      # String → Int
.to_float_or(fallback)    # String → Float
.to_string()              # Any → String

Conditionals

if condition {
    // code
} else if other {
    // code
} else {
    // code
}

Practice Exercises

Try these on your own:

Exercise 1: Find High-Memory Warnings

Filter for WARN events where memory_percent > 80:

Solution
kelora -j examples/simple_json.jsonl \
    --filter 'e.level == "WARN" && e.has_field("memory_percent") && e.memory_percent > 80'

Exercise 2: Classify Request Speeds

Add a speed field: "fast" if duration < 100ms, "normal" if < 1000ms, else "slow":

Solution
kelora -j examples/simple_json.jsonl \
    --exec 'if e.has_field("duration_ms") {
                e.speed = if e.duration_ms < 100 { "fast" }
                         else if e.duration_ms < 1000 { "normal" }
                         else { "slow" }
            } else {
                e.speed = "unknown"
            }' \
    -k service,duration_ms,speed,message

Exercise 3: Extract HTTP Method

For events with a method field, add is_safe_method (true for GET/HEAD):

Solution
kelora -j examples/simple_json.jsonl \
    --exec 'if e.has_field("method") {
                e.is_safe_method = e.method == "GET" || e.method == "HEAD"
            }' \
    --filter 'e.has_field("is_safe_method")' \
    -k method,is_safe_method,path

Summary

You've learned:

  • ✅ Access event fields with e.field_name
  • ✅ Filter events with --filter boolean expressions
  • ✅ Transform events with --exec 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_field()
  • ✅ 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: - Function Reference - Complete function catalog - Rhai Cheatsheet - Quick syntax reference - How-To: Triage Production Errors - Practical examples