# Function Reference

Complete reference for all 150+ built-in Rhai functions available in Kelora. Functions are organized by category for easy lookup.

!!! tip "Function Call Syntax"
    Rhai allows two styles: `value.method(args)` or `function(value, args)`. Use whichever feels more natural.

## Quick Navigation

- [String Functions](#string-functions) - Text manipulation, parsing, encoding
- [Array Functions](#array-functions) - Array operations, sorting, filtering
- [Map/Object Functions](#mapobject-functions) - Field access, manipulation, conversion
- [DateTime Functions](#datetime-functions) - Time parsing, formatting, arithmetic
- [Math Functions](#math-functions) - Numeric operations
- [Type Conversion](#type-conversion-functions) - Safe type conversions
- [Utility Functions](#utility-functions) - Environment, files, pseudonyms
- [Tracking/Metrics](#trackingmetrics-functions) - Counters, aggregations
- [File Output](#file-output-functions) - Writing data to files
- [Event Manipulation](#event-manipulation) - Field removal, fan-out
- [Span Context](#span-context-span-close-only) - Per-span metadata & rollups

---

## String Functions

### Extraction and Searching

#### `text.extract_re(pattern [, group])`
Extract first regex match or capture group.

```rhai
e.error_code = e.message.extract_re(r"ERR-(\d+)", 1)  // "ERR-404" → "404"
e.full_match = e.line.extract_re(r"\d{3}")            // First 3-digit number
```

#### `text.extract_all_re(pattern [, group])`
Extract all regex matches as array.

```rhai
e.numbers = e.line.extract_all_re(r"\d+")             // All numbers
e.codes = e.message.extract_all_re(r"ERR-(\d+)", 1)   // All error codes
```

#### `text.extract_re_maps(pattern, field)`
Extract regex matches as array of maps for fan-out with `emit_each()`.

```rhai
// Extract all error codes with context
let errors = e.log.extract_re_maps(r"(?P<code>ERR-\d+): (?P<msg>[^\n]+)", "error");
emit_each(errors)  // Each match becomes an event with 'code' and 'msg' fields
```

#### `text.extract_ip([nth])`
Extract IP address from text (nth: 1=first, -1=last).

```rhai
e.client_ip = e.headers.extract_ip()                  // First IP
e.origin_ip = e.forwarded.extract_ip(-1)              // Last IP
```

#### `text.extract_ips()`
Extract all IP addresses as array.

```rhai
e.all_ips = e.headers.extract_ips()                   // ["192.168.1.1", "10.0.0.1"]
```

#### `text.extract_url([nth])`
Extract URL from text (nth: 1=first, -1=last).

```rhai
e.link = e.message.extract_url()                      // First URL
```

#### `text.extract_domain()`
Extract domain from URL or email address.

```rhai
e.domain = "https://api.example.com/path".extract_domain()  // "example.com"
e.mail_domain = "user@corp.example.com".extract_domain()    // "corp.example.com"
```

### String Slicing and Position

#### `text.before(delimiter [, nth])`
Text before occurrence of delimiter (nth: 1=first, -1=last).

```rhai
e.user = e.email.before("@")                          // "user@host.com" → "user"
e.path = e.url.before("?")                            // Strip query string
```

#### `text.after(delimiter [, nth])`
Text after occurrence of delimiter (nth: 1=first, -1=last).

```rhai
e.extension = e.filename.after(".")                   // "file.txt" → "txt"
e.domain = e.email.after("@")                         // "user@host.com" → "host.com"
```

#### `text.between(start, end [, nth])`
Text between start and end delimiters (nth: 1=first, -1=last).

**Note:** `text.between(left, right, nth)` is equivalent to `text.after(left, nth).before(right)`.

```rhai
e.quoted = e.line.between('"', '"')                   // Extract quoted string
"[a][b][c]".between("[", "]", 2)                      // "b" - same as .after("[", 2).before("]")
```

#### `text.starting_with(prefix [, nth])`
Return substring from prefix to end (nth: 1=first, -1=last).

```rhai
e.from_error = e.log.starting_with("ERROR:")          // "INFO: ok ERROR: bad" → "ERROR: bad"
```

#### `text.ending_with(suffix [, nth])`
Return substring from start to end of suffix (nth: 1=first, -1=last).

```rhai
e.up_to_end = e.log.ending_with(".txt")               // "file.txt more" → "file.txt"
```

#### `text.slice(spec)`
Slice text using Python notation (e.g., "1:5", ":3", "-2:").

```rhai
e.first_three = e.code.slice(":3")                    // "ABCDEF" → "ABC"
e.last_two = e.code.slice("-2:")                      // "ABCDEF" → "EF"
e.middle = e.code.slice("2:5")                        // "ABCDEF" → "CDE"
```

### Column Extraction

#### `text.col(spec [, separator])`
Extract columns by index/range/list (e.g., '1', '1,3,5', '1:4').

```rhai
e.first = e.line.col("1")                             // First column (1-indexed)
e.cols = e.line.col("1,3,5")                          // Columns 1, 3, 5
e.range = e.line.col("2:5", "\t")                     // Columns 2-5, tab-separated
```

### Parsing Functions

#### `text.parse_json()`
Parse JSON string into map/array.

```rhai
e.data = e.payload.parse_json()
e.value = e.data["key"]
```

#### `text.parse_logfmt()`
Parse logfmt line into structured fields.

```rhai
let fields = e.line.parse_logfmt()
e.level = fields["level"]
```

#### `text.parse_syslog()`
Parse syslog line into structured fields.

```rhai
let syslog = e.line.parse_syslog()
e.priority = syslog["priority"]
e.message = syslog["message"]
```

#### `text.parse_combined()`
Parse Apache/Nginx combined log line.

```rhai
let access = e.line.parse_combined()
e.ip = access["ip"]
e.status = access["status"]
```

#### `text.parse_cef()`
Parse Common Event Format line into fields.

```rhai
let cef = e.line.parse_cef()
e.severity = cef["severity"]
```

#### `text.parse_kv([sep [, kv_sep]])`
Parse key-value pairs from text. Only extracts tokens containing the key-value separator; tokens without the separator are skipped (e.g., prose words or unpaired values).

```rhai
e.params = e.query.parse_kv("&", "=")                 // "a=1&b=2" → {a: "1", b: "2"}
e.fields = e.msg.parse_kv()                           // "Payment timeout order=1234" → {order: "1234"}
```

#### `text.parse_url()`
Parse URL into structured components.

```rhai
let url = e.request.parse_url()
e.scheme = url["scheme"]
e.host = url["host"]
e.path = url["path"]
```

#### `text.parse_query_params()`
Parse URL query string into map.

```rhai
e.params = e.query_string.parse_query_params()        // "a=1&b=2" → {a: "1", b: "2"}
```

#### `text.parse_email()`
Parse email address into parts.

```rhai
let email = "User Name <user@example.com>".parse_email()
e.name = email["name"]       // "User Name"
e.address = email["address"] // "user@example.com"
```

#### `text.parse_user_agent()`
Parse common user-agent strings into components.

```rhai
let ua = e.user_agent.parse_user_agent()
e.browser = ua["browser"]
e.os = ua["os"]
```

#### `text.parse_jwt()`
Parse JWT header/payload without verification.

```rhai
let jwt = e.token.parse_jwt()
e.user_id = jwt["payload"]["sub"]
```

#### `text.parse_path()`
Parse filesystem path into components.

```rhai
let path = "/var/log/app.log".parse_path()
e.dir = path["dir"]          // "/var/log"
e.file = path["file"]        // "app.log"
```

#### `text.parse_media_type()`
Parse media type tokens and parameters.

```rhai
let mt = "text/html; charset=utf-8".parse_media_type()
e.type = mt["type"]          // "text"
e.subtype = mt["subtype"]    // "html"
```

#### `text.parse_content_disposition()`
Parse Content-Disposition header parameters.

```rhai
let cd = e.header.parse_content_disposition()
e.filename = cd["filename"]
```

### Encoding and Hashing

#### `text.encode_b64()` / `text.decode_b64()`
Base64 encoding/decoding.

```rhai
e.encoded = e.data.encode_b64()
e.decoded = e.payload.decode_b64()
```

#### `text.encode_hex()` / `text.decode_hex()`
Hexadecimal encoding/decoding.

```rhai
e.hex = e.bytes.encode_hex()
e.bytes = e.hex_string.decode_hex()
```

#### `text.encode_url()` / `text.decode_url()`
URL percent encoding/decoding.

```rhai
e.encoded = e.param.encode_url()                      // "hello world" → "hello%20world"
e.decoded = e.url_param.decode_url()
```

#### `text.escape_json()` / `text.unescape_json()`
JSON escape sequence handling.

```rhai
e.escaped = e.text.escape_json()
e.unescaped = e.json_string.unescape_json()
```

#### `text.escape_html()` / `text.unescape_html()`
HTML entity escaping/unescaping.

```rhai
e.safe = e.user_input.escape_html()                   // "<script>" → "&lt;script&gt;"
e.text = e.html_entity.unescape_html()
```

#### `text.hash([algo])`
Hash with algorithm (default: sha256, also: sha1, md5, xxh3, blake3).

```rhai
e.checksum = e.content.hash()                         // SHA-256
e.md5 = e.file.hash("md5")
e.fast = e.data.hash("xxh3")                          // Fast non-crypto hash
```

#### `text.bucket()`
Fast hash for sampling/grouping (returns INT for modulo operations).

```rhai
// Sample 10% of events
if e.user_id.bucket() % 10 == 0 {
    e.sampled = true
}
```

### IP Address Functions

#### `text.is_ipv4()` / `text.is_ipv6()`
Check if text is a valid IP address.

```rhai
if e.addr.is_ipv4() {
    e.ip_version = 4
}
```

#### `text.is_private_ip()`
Check if IP is in private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).

```rhai
if e.ip.is_private_ip() {
    e.internal = true
}
```

#### `text.is_in_cidr(cidr)`
Check if IP address is in CIDR network.

```rhai
if e.ip.is_in_cidr("10.0.0.0/8") {
    e.corp_network = true
}
```

#### `text.mask_ip([octets])`
Mask IP address (default: last octet).

```rhai
e.masked_ip = e.client_ip.mask_ip()                   // "192.168.1.100" → "192.168.1.0"
e.partial = e.ip.mask_ip(2)                           // Mask last 2 octets
```

### Pattern Normalization

#### `text.normalized([patterns])`
Replace variable patterns with placeholders (e.g., `<ipv4>`, `<email>`).

Useful for identifying unique log patterns by normalizing variable data like IP addresses, UUIDs, and email addresses to fixed placeholders.

```rhai
// Default patterns (IPs, emails, UUIDs, hashes, etc.)
e.pattern = e.message.normalized()
// "User user@test.com from 192.168.1.5" → "User <email> from <ipv4>"

// CSV-style pattern list
e.simple = e.message.normalized("ipv4,email")

// Array-style pattern list
e.custom = e.message.normalized(["uuid", "sha256", "url"])
```

**Default patterns** (when no argument provided):
`ipv4_port`, `ipv4`, `ipv6`, `email`, `url`, `fqdn`, `uuid`, `mac`, `md5`, `sha1`, `sha256`, `path`, `oauth`, `function`, `hexcolor`, `version`

**Available patterns** (opt-in):
`hexnum`, `duration`, `num`

**Common use case** - Pattern discovery:
```bash
# Recommended alias for easy pattern discovery
kelora --save-alias patterns \
  --exec 'track_unique("patterns", e.message.normalized())' \
  --metrics -q

# Usage
kelora -a patterns app.log
```

**Output with many patterns:**
```
patterns     (127 unique):
  User <email> from <ipv4>
  Request to <url> failed
  Error <uuid> occurred
  Connection <ipv4_port> established
  Processing <fqdn> with <sha256>
  [+122 more. Use --metrics-file or --end script for full list]
```

For custom analysis, access full data in `--end` scripts or `--metrics-file`.

### String Manipulation

#### `text.strip([chars])` / `text.lstrip([chars])` / `text.rstrip([chars])`
Remove whitespace or specified characters.

```rhai
e.clean = e.text.strip()                              // Remove leading/trailing whitespace
e.trimmed = e.line.lstrip("# ")                       // Remove "# " from left
e.path = e.filename.rstrip("/")                       // Remove trailing slashes
```

#### `text.clip()` / `text.lclip()` / `text.rclip()`
Remove non-alphanumeric characters from edges.

```rhai
e.word = "'hello!'".clip()                            // → "hello"
e.left = "...start".lclip()                           // → "start"
e.right = "end...".rclip()                            // → "end"
```

#### `text.upper()` / `text.lower()`
Case conversion.

```rhai
e.normalized = e.country_code.upper()                 // "us" → "US"
e.lowercase = e.name.lower()
```

#### `text.replace(pattern, replacement)`
Replace all occurrences of pattern.

```rhai
e.cleaned = e.text.replace("ERROR", "WARN")
```

#### `text.split(separator)` / `text.split_re(pattern)`
Split string into array.

```rhai
e.parts = e.path.split("/")
e.tokens = e.line.split_re(r"\s+")                    // Split on whitespace
```

### String Testing

#### `text.contains(pattern)`
Check if text contains pattern.

```rhai
if e.message.contains("timeout") {
    e.timeout_error = true
}
```

#### `text.like(pattern)`
Glob match (anchored) with `*` and `?`.

```rhai
if e.message.like("ERROR * timeout") {
    e.timeout_error = true
}
```

#### `text.ilike(pattern)`
Case-insensitive glob match with Unicode folding.

```rhai
if e.message.ilike("*straße*") {
    e.locale = "de"
}
```

#### `text.matches(pattern)`
Regex search with cached compilation. Invalid patterns raise errors.

```rhai
if e.path.matches(r"^/api/[^/]+/details$") {
    e.route = "details"
}
```

#### `text.has_matches(pattern)`
Legacy regex helper (invalid patterns return `false`).

```rhai
if e.code.has_matches(r"^ERR-\d+$") {
    e.valid_error_code = true
}
```

#### `text.matches` vs `text.has_matches`

| Function | Anchored | Errors on invalid pattern | Case handling | Use case |
|----------|----------|---------------------------|---------------|----------|
| `like()` | Yes      | N/A (glob syntax)         | Exact         | Simple wildcard matching |
| `ilike()`| Yes      | N/A                       | Unicode fold  | Case-insensitive glob |
| `matches()` | No   | Yes                       | Regex-driven  | Full regex search with caching |
| `has_matches()` | No | No (returns `false`)    | Regex-driven  | Backwards-compatible regex check |

> ⚠️ Regex performance tips: avoid nested quantifiers like `(.*)*`, prefer anchored patterns when possible, and reuse patterns to benefit from the per-thread cache.

#### `text.is_digit()`
Check if text contains only digits.

```rhai
if e.status.is_digit() {
    e.status_code = e.status.to_int()
}
```

#### `text.count(pattern)`
Count occurrences of pattern in text.

```rhai
e.error_count = e.log.count("ERROR")
```

#### `text.edit_distance(other)`
Compute Levenshtein edit distance between two strings.

```rhai
if e.message.edit_distance("connection reset") <= 3 {
    e.is_connection_issue = true
}
```

#### `text.index_of(pattern)`
Find position of substring (-1 if not found).

```rhai
e.at_pos = e.url.index_of("?")
```

---

## Array Functions

### Sorting and Filtering

#### `array.sorted()`
Return new sorted array (numeric/lexicographic).

```rhai
e.sorted_scores = sorted(e.scores)                    // [3, 1, 2] → [1, 2, 3]
e.sorted_names = sorted(e.names)                      // Alphabetical
```

#### `array.sorted_by(field)`
Sort array of objects by field name.

```rhai
let sorted_users = sorted_by(e.users, "age")
e.oldest = sorted_users[-1]
```

#### `array.reversed()`
Return new array in reverse order.

```rhai
e.reversed = reversed(e.items)
```

#### `array.slice(spec)`
Slice array using Python notation (e.g., `"1:5"`, `":3"`, `"-2:"`).

```rhai
e.top_three = e.values.slice(":3")                   // [9, 8, 7, 6] → [9, 8, 7]
e.tail = e.values.slice("-2:")                       // [9, 8, 7, 6] → [7, 6]
e.every_other = e.values.slice("0::2")               // [9, 8, 7, 6] → [9, 7]
```

#### `array.unique()`
Remove all duplicate elements (preserves first occurrence).

```rhai
e.unique_tags = unique(e.tags)                        // [1, 2, 1, 3] → [1, 2, 3]
```

#### `array.filter(|item| condition)`
Keep elements matching condition.

```rhai
e.errors = e.logs.filter(|log| log.level == "ERROR")
```

### Aggregation

#### `array.max()` / `array.min()`
Find maximum/minimum value in array.

```rhai
e.max_score = e.scores.max()
e.min_time = e.times.min()
```

#### `array.percentile(pct)`
Calculate percentile of numeric array.

```rhai
e.p95 = e.latencies.percentile(95)
e.median = e.values.percentile(50)
```

#### `array.reduce(|acc, item| expr, init)`
Aggregate array into single value.

```rhai
e.total = e.amounts.reduce(|sum, x| sum + x, 0)
```

### Transformation

#### `array.map(|item| expression)`
Transform each element.

```rhai
e.doubled = e.numbers.map(|n| n * 2)
e.names = e.users.map(|u| u.name)
```

#### `array.flattened([style [, max_depth]])`
Flatten nested arrays/objects.

```rhai
e.flat = [[1, 2], [3, 4]].flattened()                 // Returns flat map
e.fields = e.nested.flattened("dot", 2)               // Flatten to dot notation
```

### Testing

#### `array.contains(value)`
Check if array contains value.

```rhai
if e.roles.contains("admin") {
    e.is_admin = true
}
```

#### `array.contains_any(search_array)`
Check if array contains any search values.

```rhai
if e.tags.contains_any(["error", "critical"]) {
    e.alert = true
}
```

#### `array.starts_with_any(search_array)`
Check if array starts with any search values.

```rhai
if e.path_parts.starts_with_any(["/api", "/v1"]) {
    e.api_call = true
}
```

#### `array.all(|item| condition)` / `array.some(|item| condition)`
Check if all/any elements match condition.

```rhai
e.all_valid = e.scores.all(|s| s >= 0)
e.has_errors = e.logs.some(|l| l.level == "ERROR")
```

### Other Operations

#### `array.join(separator)`
Join array elements with separator.

```rhai
e.path = e.parts.join("/")
e.csv = e.values.join(",")
```

#### `array.push(item)` / `array.pop()`
Add/remove items from array.

```rhai
e.tags.push("new_tag")
let last = e.items.pop()
```

---

## Map/Object Functions

### Field Access

#### `map.get_path("field.path" [, default])`
Safe nested field access with fallback.

```rhai
e.user_name = e.get_path("user.profile.name", "unknown")
e.score = e.get_path("stats.score", 0)
```

#### `map.has_path("field.path")`
Check if nested field path exists.

```rhai
if e.has_path("error.details.code") {
    e.detailed_error = true
}
```

#### `map.path_equals("path", value)`
Safe nested field comparison.

```rhai
if path_equals(e, "user.role", "admin") {
    e.elevated = true
}
```

#### `map.has("key")`
Check if map contains key with non-unit value.

```rhai
if e.has("error_code") {
    // Field exists and has a value
}
```

### Field Manipulation

#### `map.rename_field("old", "new")`
Rename a field, returns true if successful.

```rhai
e.rename_field("old_name", "new_name")
```

#### `map.merge(other_map)`
Merge another map into this one (overwrites existing keys).

```rhai
e.merge(#{status: "ok", timestamp: now()})
```

#### `map.enrich(other_map)`
Merge another map, inserting only missing keys (does not overwrite).

```rhai
e.enrich(#{user: "default", level: "info"})  // Only adds if keys don't exist
```

#### `map.flattened([style [, max_depth]])`
Flatten nested object to dot notation.

```rhai
let flat = e.nested.flattened("dot")                  // {a: {b: 1}} → {"a.b": 1}
let flat = e.nested.flattened("dot", 2)               // With max depth
```

#### `map.flatten_field("field_name")`
Flatten just one specific field from the map.

```rhai
let flat = e.flatten_field("metadata")                // Flattens only e.metadata
```

#### `map.unflatten([separator])`
Reconstruct nested object from flat keys.

```rhai
let nested = e.flat.unflatten(".")                    // {"a.b": 1} → {a: {b: 1}}
```

### Format Conversion

#### `map.to_json([pretty])`
Convert map to JSON string.

```rhai
e.payload = e.data.to_json()
e.readable = e.data.to_json(true)                     // Pretty-printed
```

#### `map.to_logfmt()`
Convert map to logfmt format string.

```rhai
e.formatted = e.fields.to_logfmt()                    // {a: 1, b: 2} → "a=1 b=2"
```

#### `map.to_kv([sep [, kv_sep]])`
Convert map to key-value string with separators.

```rhai
e.query = e.params.to_kv("&", "=")                    // {a: 1, b: 2} → "a=1&b=2"
```

#### `map.to_syslog()` / `map.to_cef()` / `map.to_combined()`
Convert map to specific log format.

```rhai
e.syslog_line = e.fields.to_syslog()
e.cef_line = e.security_event.to_cef()
e.access_log = e.request.to_combined()
```

---

## DateTime Functions

### Creation

#### `now()`
Current timestamp (UTC).

```rhai
e.timestamp = now()
```

#### `to_datetime(text [, fmt [, tz]])`
Convert string into datetime value with optional hints.

```rhai
e.parsed = to_datetime("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S", "UTC")
e.auto = to_datetime("2024-01-15T10:30:00Z")          // Auto-detect format
```

#### `to_duration("1h30m")`
Convert duration string into duration value.

```rhai
let timeout = to_duration("5m")
e.deadline = now() + timeout
```

#### `duration_from_seconds(n)`, `duration_from_minutes(n)`, etc.
Create duration from specific units.

```rhai
let hour = duration_from_hours(1)
let day = duration_from_days(1)
```

### Formatting

#### `dt.to_iso()`
Convert datetime to ISO 8601 string.

```rhai
e.iso_timestamp = e.timestamp.to_iso()                // "2024-01-15T10:30:00Z"
```

#### `dt.format("format_string")`
Format datetime using custom format string (see `--help-time`).

```rhai
e.date = e.timestamp.format("%Y-%m-%d")               // "2024-01-15"
e.time = e.timestamp.format("%H:%M:%S")               // "10:30:00"
```

### Component Extraction

#### `dt.year()`, `dt.month()`, `dt.day()`
Extract date components.

```rhai
e.year = e.timestamp.year()
e.month = e.timestamp.month()
e.day = e.timestamp.day()
```

#### `dt.hour()`, `dt.minute()`, `dt.second()`
Extract time components.

```rhai
e.hour = e.timestamp.hour()
```

### Timezone Conversion

#### `dt.to_utc()` / `dt.to_local()`
Convert timezone.

```rhai
e.utc_time = e.local_timestamp.to_utc()
e.local_time = e.utc_timestamp.to_local()
```

#### `dt.to_timezone("tz_name")`
Convert to named timezone.

```rhai
e.ny_time = e.timestamp.to_timezone("America/New_York")
```

#### `dt.timezone_name()`
Get timezone name as string.

```rhai
e.tz = e.timestamp.timezone_name()                    // "UTC"
```

### Arithmetic and Comparison

#### `dt + duration`, `dt - duration`
Add/subtract duration from datetime.

```rhai
e.future = now() + duration_from_hours(1)
e.past = now() - duration_from_days(7)
```

#### `dt1 - dt2`
Get duration between datetimes.

```rhai
let elapsed = now() - e.start_time
e.duration_ms = elapsed.as_milliseconds()
```

#### `dt1 == dt2`, `dt1 > dt2`, etc.
Compare datetimes.

```rhai
if e.timestamp > to_datetime("2024-01-01") {
    e.this_year = true
}
```

### Duration Operations

#### `duration.as_seconds()`, `duration.as_milliseconds()`, etc.
Convert duration to specific units.

```rhai
e.seconds = duration.as_seconds()
e.ms = duration.as_milliseconds()
e.hours = duration.as_hours()
```

#### `duration.to_string()` / `humanize_duration(ms)`
Format duration as human-readable string.

```rhai
e.readable = duration.to_string()                     // "1h 30m"
e.humanized = humanize_duration(5400000)              // "1h 30m"
```

---

## Math Functions

#### `abs(x)`
Absolute value of number.

```rhai
e.magnitude = abs(e.value)
```

#### `clamp(value, min, max)`
Constrain value to be within min/max range.

```rhai
e.bounded = clamp(e.score, 0, 100)
```

#### `floor(x)` / `round(x)`
Rounding operations.

```rhai
e.floored = floor(e.value)
e.rounded = round(e.value)
```

#### `mod(a, b)` / `a % b`
Modulo operation with division-by-zero protection.

```rhai
e.bucket = e.id % 10
```

#### `rand()` / `rand_int(min, max)`
Random number generation.

```rhai
if rand() < 0.1 {                                     // 10% sampling
    e.sampled = true
}
e.random_id = rand_int(1000, 9999)
```

---

## Type Conversion Functions

#### `to_int(value)` / `to_float(value)` / `to_bool(value)`
Convert value to type (returns `()` on error).

```rhai
e.status = to_int(e.status_string)
e.score = to_float(e.score_string)
```

#### `to_int_or(value, default)` / `to_float_or(value, default)` / `to_bool_or(value, default)`
Convert value to type with fallback.

```rhai
e.status = to_int_or(e.status_string, 0)
e.score = to_float_or(e.score_string, 0.0)
```

#### `value.or_empty()`
Convert empty values to Unit `()` for removal/filtering.

Converts conceptually "empty" values to Unit, which:
- Removes the field when assigned (e.g., `e.field = value.or_empty()`)
- Gets skipped by `track_*()` functions
- Works with missing fields (passes Unit through unchanged)

**Supported empty values:**
- Empty string: `""` → `()`
- Empty array: `[]` → `()`
- Empty map: `#{}` → `()`
- Unit itself: `()` → `()` (pass-through)

**String extraction:**
```rhai
// Extract only when prefix exists, otherwise remove field
e.name = e.message.after("prefix:").or_empty()

// Track only non-empty values
track_unique("names", e.extracted.or_empty())
```

**Array filtering:**
```rhai
// Only assign tags if array is non-empty
e.tags = e.tags.or_empty()  // [] becomes (), field removed

// Track only events with items
track_bucket("item_count", e.items.len())
if e.items.len() == 0 {
    e.items = e.items.or_empty()  // Remove empty array
}
```

**Map filtering:**
```rhai
// Only keep non-empty metadata
e.metadata = e.parse_json().or_empty()  // {} becomes (), field removed

// Safe chaining with missing fields
e.optional = e.maybe_field.or_empty()  // Works even if maybe_field is ()
```

**Common pattern - conditional extraction and tracking:**
```rhai
e.extracted = e.message.after("User:").or_empty()
track_unique("users", e.extracted)  // Only tracks when extraction succeeds

// Filter events with no data
e.results = e.search_results.or_empty()
track_unique("result_sets", e.results)  // Skips empty arrays and ()
```

---

## Utility Functions

#### `get_env(var [, default])`
Get environment variable with optional default.

```rhai
e.branch = get_env("CI_BRANCH", "main")
e.build_id = get_env("BUILD_ID")
```

#### `pseudonym(value, domain)`
Generate domain-separated pseudonym (requires `KELORA_SECRET`).

```rhai
e.user_alias = pseudonym(e.username, "users")
e.ip_alias = pseudonym(e.client_ip, "ips")
```

#### `read_file(path)` / `read_lines(path)`
Read file contents.

```rhai
e.config = read_file("config.json")
e.lines = read_lines("data.txt")
```

#### `print(message)` / `eprint(message)`
Print to stdout/stderr (suppressed with `--no-script-output`/`--silent`).

```rhai
print("Processing event: " + e.id)
eprint("Warning: " + e.error)
```

#### `exit(code)`
Exit kelora with given exit code.

```rhai
if e.critical {
    exit(1)
}
```

#### `type_of(value)`
Get type name as string.

```rhai
e.value_type = type_of(e.value)                       // "string", "int", "array", etc.
```

#### `window.pluck(field)` / `window.pluck_as_nums(field)`
Extract field values from the current window array (requires `--window`).

```rhai
let recent_statuses = window.pluck("status")
let recent_times = window.pluck_as_nums("response_time")
e.avg_time = recent_times.reduce(|s, x| s + x, 0) / recent_times.len()
```

---

## Tracking/Metrics Functions

All tracking functions require the `--metrics` flag.

!!! tip "Unit Value Handling"
    All `track_*()` functions that accept values silently skip Unit `()` values. This enables safe tracking of optional or extracted fields without needing conditional checks.

### Tracking Functions {#tracking-functions}

#### `track_count(key)`
Increment counter for key by 1.

```rhai
track_count(e.service)                                // Count by service
track_count("total")                                  // Global counter
```

#### `track_sum(key, value)`
Accumulate numeric values for key. Skips Unit `()` values.

```rhai
track_sum("total_bytes", e.bytes)
track_sum(e.endpoint, e.response_time)

// Safe with conversions that may fail
let score = e.score_str.to_int()  // Returns () on error
track_sum("total_score", score)   // Skips () values
```

#### `track_min(key, value)` / `track_max(key, value)`
Track minimum/maximum value for key. Skips Unit `()` values.

```rhai
track_min("fastest", e.response_time)
track_max("slowest", e.response_time)
```

#### `track_unique(key, value)`
Track unique values for key. Skips Unit `()` values.

```rhai
track_unique("users", e.user_id)
track_unique("ips", e.client_ip)

// Combined with .or_empty() for conditional tracking
track_unique("names", e.message.after("User:").or_empty())
```

#### `track_bucket(key, bucket)`
Track values in buckets for histograms. Skips Unit `()` values.

```rhai
let bucket = floor(e.response_time / 100) * 100
track_bucket("latency", bucket)

// Safe with optional fields
track_bucket("user_types", e.user_type.or_empty())  // Skips empty/missing
```

---

## File Output Functions

All file output functions require the `--allow-fs-writes` flag.

#### `append_file(path, text_or_array)`
Append line(s) to file; arrays append one line per element.

```rhai
append_file("errors.log", e.message)
append_file("batch.log", [e.line1, e.line2, e.line3])
```

#### `truncate_file(path)`
Create or zero-length a file for fresh output.

```rhai
truncate_file("output.log")
```

#### `mkdir(path [, recursive])`
Create directory (set recursive=true to create parents).

```rhai
mkdir("logs")
mkdir("deep/nested/path", true)
```

---

## Event Manipulation

#### `emit_each(array [, base_map])`
Fan out array elements as separate events (returns emitted count).

```rhai
emit_each(e.users)                                    // Each user becomes an event
emit_each(e.items, #{batch_id: e.batch_id})           // Add batch_id to each

// Use return value to track emission count
let count = emit_each(e.batch_items, #{batch_id: e.id})
track_sum("items_emitted", count)
```

#### `e = ()`
Clear entire event (remove all fields).

```rhai
if e.should_drop {
    e = ()  // Event is filtered out
}
```

#### `e.field = ()`
Remove individual field from event.

```rhai
e.password = ()                                       // Remove sensitive field
e.temp_data = ()                                      // Clean up temporary field
```

#### `e.absorb_kv(field [, options])`
Parse inline `key=value` tokens from a string field, merge the pairs into the event, and get a status report back. Returns a map with `status`, `data`, `written`, `remainder`, `removed_source`, and `error` so scripts can branch without guessing.

```rhai
let res = e.absorb_kv("msg", #{ sep: ",", kv_sep: "=", keep_source: true });
if res.status == "applied" {
    e.cleaned_msg = res.remainder ?? "";
    // Parsed keys now live on the event; res.data mirrors the inserted pairs
}
```

Options:

- `sep`: string or `()` (default whitespace) – token separator; `()` normalizes whitespace.
- `kv_sep`: string (default `"="`) – separator between key and value.
- `keep_source`: bool (default `false`) – leave the original field untouched; use `remainder` for cleaned text.
- `overwrite`: bool (default `true`) – allow parsed keys to overwrite existing event fields; set `false` to skip conflicts.

Unknown option keys set `status = "invalid_option"`; in `--strict` mode this aborts the pipeline.

#### `e.absorb_json(field [, options])`
Parse a JSON object from a string field, merge its keys into the event, and return the same status map as `absorb_kv()`. On success the source field is deleted unless `keep_source` is true, and `remainder` is always `()`.

```rhai
let res = e.absorb_json("payload");
if res.status == "applied" {
    e.actor = e.actor ?? e.user;      // merged from payload
} else if res.status == "parse_error" {
    warn(`bad payload: ${res.error}`);
}
```

Options:

- `keep_source`: bool (default `false`) – keep the original JSON string instead of deleting the field.
- `overwrite`: bool (default `true`) – allow parsed keys to replace existing event fields (`false` skips conflicts).

Other absorb options (like `sep`) are accepted for consistency but ignored. JSON parsing is all-or-nothing: invalid JSON or non-object payloads set `status = "parse_error"` and leave the event untouched.


## Span Context – `--span-close` Only

A read-only `span` object is injected into scope whenever a `--span-close` script runs. Use it to emit per-span rollups after Kelora closes a count- or time-based window.

### Span Identity

`span.id` returns the current span identifier. Count-based spans use `#<index>` (zero-based). Time-based spans use `ISO_START/DURATION` (e.g. `2024-05-19T12:00:00Z/5m`).

```rhai
let id = span.id;  // "#0" or "2024-05-19T12:05:00Z/5m"
```

### Span Boundaries

`span.start` and `span.end` expose the half-open window bounds as `DateTime` values. Count-based spans return `()` for both fields.

```rhai
if span.start != () {
    print(`Window: ${span.start} → ${span.end}`);
}
```

### Span Size and Events

`span.size` reports how many events survived filters and were buffered in the span. `span.events` returns those events in arrival order. Each map includes span metadata fields (`span_status`, `span_id`, `span_start`, `span_end`) alongside the original event data.

```rhai
let included = span.events
    .filter(|evt| evt.span_status == "included")
    .len();
```

### Metrics Snapshot

`span.metrics` contains per-span deltas from `track_*` calls. Values reset automatically after each span closes, so you can emit per-span summaries without manual bookkeeping.

```rhai
let metrics = span.metrics;
let hits = metrics["events"];          // from track_count("events")
let failures = metrics["failures"];    // from track_count("failures")
let ratio = if hits > 0 { failures * 100 / hits } else { 0 };
print(span.id + ": " + ratio.to_string() + "% failure rate");
```

---

## Quick Reference by Use Case

**Error Extraction:**
```rhai
e.error_code = e.message.extract_re(r"ERR-(\d+)", 1)
```

**IP Anonymization:**
```rhai
e.masked_ip = e.client_ip.mask_ip()
e.ip_alias = pseudonym(e.client_ip, "ips")
```

**Time Filtering:**
```rhai
if e.timestamp > to_datetime("2024-01-01") {
    // Process recent events
}
```

**Metrics Tracking:**
```rhai
track_count(e.service)
track_sum("bytes", e.response_size)
track_unique("users", e.user_id)
```

**Array Fan-Out:**
```rhai
emit_each(e.users, #{batch_id: e.batch_id})
```

**Safe Field Access:**
```rhai
e.user_name = e.get_path("user.profile.name", "unknown")
if e.has_path("error.details.code") {
    e.detailed = true
}
```

---

## See Also

- [CLI Reference](cli-reference.md) - Command-line flags and options
- [Rhai Cheatsheet](rhai-cheatsheet.md) - Rhai language syntax
- [Advanced Scripting Tutorial](../tutorials/advanced-scripting.md) - Learn advanced scripting
- [How-To: Sanitize Logs Before Sharing](../how-to/extract-and-mask-sensitive-data.md) - Practical examples

For more details, run:
```bash
kelora --help-functions    # This reference in CLI form
kelora --help-rhai         # Rhai language guide
kelora --help-examples     # Common usage patterns
```
