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
-escripts - Use string operations and conditionals
- Convert between types safely
- Understand why pipeline order matters
- Debug scripts with
-F inspectand--verbose
Prerequisites¶
- Basics: Input, Display & Filtering - Basic CLI usage
- Time: ~20 minutes
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:
---
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:
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):
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!- NOTin- 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
Key insight: --exec runs before --filter, so the new field is available for filtering.
Modify Existing Fields¶
Normalize level to uppercase:
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:
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 textstarts_with(prefix)- Check prefixends_with(suffix)- Check suffixsplit(sep)- Split into arrayto_upper()/to_lower()- Change casetrim()- Remove whitespacelen()- 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 fallbackto_float_or(fallback)- Convert to float or use fallbackto_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
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:
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:
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:
Debug workflow:
- Use
-F inspectto check field types - Use
--verboseto see error messages - Use
--strictto fail fast on first error - 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:
Exercise 2: Flag High Memory Usage¶
Add a high_memory field and filter for events with memory usage above 80%:
Solution
Exercise 3: Flag Critical Security Events¶
Add a critical field that's true for ERROR events with failed login attempts:
Solution
Summary¶
You've learned:
- Access event fields with
e.field_name - Filter events with
--filterboolean expressions - Transform events with
-escripts - 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 inspectand--verbose - Understand pipeline order (exec before filter)
- Build multi-stage pipelines
Next Steps¶
Now that you understand basic Rhai scripting, continue to:
- Working with Time - Time filtering and timezone handling
- Metrics and Tracking - Aggregate data with
track_*()functions - Advanced Scripting - Advanced patterns and techniques
Related guides:
- Function Reference - Complete function catalog
- Rhai Cheatsheet - Quick syntax reference
- How-To: Triage Production Errors - Practical examples