Skip to content

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 18 Jun 16:02
· 227 commits to main since this release

The 2.0 line. The headline changes are a redesigned tracking-function family, a set of built-in application-log formats, composable parser cascades, and a much more capable --discover mode. Breaking changes are flagged below — most affect tracking scripts and a few error/validation behaviors. See What's New in 2.0 for migration guidance with old → new examples.

Added

  • Three-tier diagnostic model with granular --no-warnings / --no-hints - Advisory output is now organized into three tiers, each silenced independently: errors (⚠️) show unless --silent; warnings (🔸) flag a recovered problem and are hidden by --no-warnings / KELORA_NO_WARNINGS / --silent; hints (💡) are teaching suggestions, hidden by --no-hints / KELORA_NO_HINTS / --silent. Positive counterparts (--warnings/--hints) and the combined --diagnostics/--no-diagnostics shortcut override an env var or config default for a single run; precedence is explicit flag > env var > config default. The same classification drives both a message's emoji and the flag that silences it, so the two can't drift apart. See Error Handling.
  • postgres built-in format - Parses PostgreSQL server logs written with the default log_line_prefix = '%m [%p] ' into ts, log_tz, pid, level, and msg. The timestamp is naive and resolved through --input-tz (default UTC); the logged zone abbreviation is kept in log_tz but not applied (abbreviations can't encode an offset/DST), so UTC-logged servers are correct by default and a non-UTC server needs --input-tz <IANA>. Pair with -M indent to fold multi-line ERROR:/STATEMENT: records. A customized prefix won't auto-detect — use -f regex:. (Adapted from lnav, BSD-3-Clause.)
  • --help now documents the exit-code model - A new "Exit Codes" section in the full --help reference spells out the resilient-by-default contract: 0 is success even when some lines/transforms failed (recovered), 1 covers genuine failures (unopenable input, failed --assert, a gate that never succeeded, --strict errors), 2 is invalid usage, plus the signal/panic codes. The one-screen kelora -h is unchanged.
  • cri format for Kubernetes container logs - The CRI/containerd on-disk layout <RFC3339Nano> <stream> <tag> <message> (the shape of /var/log/pods/*/*.log and kubectl logs --timestamps) is now a built-in format, parsed into ts, stream, tag, and msg. Selectable with -f cri and usable in cascades. Because a CRI message is often itself JSON or logfmt, cri has a dedicated detector that runs before the logfmt/CSV steps, so auto-detection works regardless of the payload. The message is kept verbatim for an optional second-stage parse (--exec 'e.absorb_json("msg")'). Kelora-original, not adapted from lnav.
  • Built-in application-log formats - A curated set of common layouts — glog (Go/klog), nginx-error, apache-error, log4j/Java, python-logging, redis, s3 (AWS S3 access log), haproxy (http/tcp), and iso8601-level — are now parsed into structured fields (ts, level, msg, plus extras) via the regex engine. They are first-class: selectable with -f <name>, usable in cascades, shown in the auto-detect notice, --stats, and --help-formats. They are tried only as the last step before the line fallback, so nothing already detected changes. Year-less timestamps (glog, redis) resolve to the current year; haproxy is syslog-wrapped, so use -f haproxy for the structured fields. (Adapted from lnav, BSD-3-Clause; see THIRD_PARTY_LICENSES.md.)
  • Composable cols:/regex: cascades via repeated -f - -f is now repeatable, building a cascade from each spec in order: kelora -f json -f 'cols:ts(2) level *msg' app.log. This is the only way to put spec-based parsers (cols:, regex:) into a cascade — a comma list can't, since a regex may contain commas — and closes the "JSON lines mixed with custom timestamp LEVEL message text in one file" case. Catch-alls (line, raw, cols:) must come last; regex: is selective and may sit earlier. A single -f (including a comma list) is unchanged.
  • Data-driven legends for map outputs - levelmap and keymap now append a one-line legend decoding their glyphs, built from the data seen (E = ERROR | I = INFO | W = WARN; 2 = 200,204 | 4 = 404 | 5 = 500,503), matching tailmap. New --legend/--no-legend flags control all three map formats: by default the legend shows only on an interactive terminal, so piped output stays clean.
  • -d shortcut and richer output for --discover - Field discovery, the documented starting point for unknown files, gets a short flag: kelora app.log -d (-d=json for the machine-readable form). --discover-depth=0 fully flattens deeply nested JSON, removing the previous 3-level cap. See Changed for the expanded discover footer.
  • -D shortcut for --discover-final, plus a discoverability hint - The post-pipeline counterpart to -d gets its own short flag (-D, -D=json). To close the "I added --exec but my computed field isn't in --discover" confusion (--discover profiles parsed input, before scripts run), the plain --discover footer now prints a tip pointing at --discover-final whenever the pipeline filters or transforms events. A bare probe stays uncluttered, and JSON output stays machine-clean.
  • e.get() map accessor - e.get("key") and e.get("key", default) are now registered for events, mirroring get_path for top-level keys. Part of a clearer missing-field model (see Changed).
  • Keyword search in --help-functions - --help-functions [KEYWORD] filters the 150+ function catalogue by a case-insensitive substring match (name, description, or section) instead of forcing a scroll: --help-functions ip lists the IP helpers. The =KEYWORD form works too; bare --help-functions is unchanged.
  • Keyword search in --help - The full CLI reference is searchable the same way: kelora --help KEYWORD prints only matching options under their section headings. A keyword beginning with a dash is a flag query — a case-sensitive, whole-token match, so --help -j finds only -j (never the -j inside --multiline-join); a bare word is a case-insensitive substring search across names and descriptions. The =KEYWORD form works; bare --help is unchanged.
  • No-script shortcuts for the most common aggregations: --freq, --describe, --card - The most common tracking operations are now plain flags. --freq FIELD builds a frequency table (track_freq), --describe FIELD prints a numeric summary (track_stats — count/min/max/avg/p50/p95/p99), and --card FIELD estimates distinct values (track_cardinality — HyperLogLog, ~1% error in constant memory). All are repeatable, accept dotted paths (--freq user.id), and run as the last per-event stage (post-filter/transform, like --discover-final). They imply -m, with output via the usual --metrics=…/--metrics-file. There is deliberately no --top/--bottom flag — --freq already sorts by count descending, so ranking is left to the shell (--freq url | head). For grouping, custom names, or score-based ranking, write the track_* call in --exec.
  • Pipe-friendly tsv metrics output, auto-selected when not on a terminal - Metrics now have a tab-separated form alongside the human table and JSON: each metric emits metric<TAB>key<TAB>value rows (lists sorted by count/score descending, scalars as a single row with an empty key column), with full float precision and embedded tabs/newlines flattened to spaces. Following the ls convention it auto-selects — human table on a terminal, tsv stream when piped or redirected — so --freq url | head, | sort, | awk, | wc compose freely. Escape hatches: --metrics=full forces the table through a pipe, --metrics=tsv forces the stream to a terminal. (Behavior change: kelora -m … > file now yields tsv rather than the table; add --metrics=full for the old rendering.)
  • Intent-based hints for unknown flags borrowed from other tools - For a curated set of habit flags, kelora now points to the kelora idiom instead of clap's edit-distance guess: --where/--grep/--match → --filter; --sort/--rank/--top-n → track_top_by; --count/--group-by/--uniq → --freq/track_freq. These stay unknown (exit 2), so no namespace is reserved. Genuine near-miss typos (--filer) still get clap's suggestion.
  • -P short flag for --parallel - The performance toggle now has a short form, following the xargs/GNU parallel convention.
  • "No input" hint instead of silent exit - A bare kelora reading from an empty non-TTY source (kelora < /dev/null, an empty pipe) previously exited 0 in silence. Kelora now prints a one-line stderr hint when no files are given, stdin is not a terminal, and nothing was read, suppressed by --no-input/--no-diagnostics/--silent. The -h/--help text now states that interactive mode requires a terminal.
  • -l/--levels warns on a vocabulary mismatch instead of returning a silent empty result - When -l drops every event because the level field uses a different dialect than the filter (glog logs I/W/E/F; syslog uses CRIT not CRITICAL), kelora now lists the levels present (-l ERROR matched none of the levels present: E,I) and points at a --filter workaround. The filter stays purely lexical. Goes to stderr, exit 0, suppressed by --no-diagnostics/-q/--silent. A present-but-unmatched level stays quiet.
  • Zero-result hint for the number-vs-quoted-string filter mistake - On typed input, --filter 'e.status == "404"' is always false (in Rhai a number never equals a string), silently emptying the result. When a --filter compares a seen field for equality against a quoted numeric-looking literal and nothing matched, kelora now suggests dropping the quotes and points at -s to check the type. Goes to stderr, exit 0, suppressed by --no-diagnostics/-q/--silent.
  • Naive-timestamp UTC-assumption hint instead of a silent shift - Timestamps without a zone offset (syslog, log4j, postgres, …, and ambiguous abbreviations like CEST/PST) resolve through --input-tz, default UTC; for a source logging local time this silently shifts --since/--until/--span boundaries and, under --normalize-ts, bakes the wrong offset into output. Kelora now prints a one-time stderr hint when timestamps are naive, no zone was chosen (no --input-tz, no TZ), and the run depends on the assumption (a time filter, --span, or --normalize-ts), pointing at --input-tz <zone>. It stays silent for the common UTC case, an explicit zone, and offset-bearing timestamps. This surfaces the existing timestamp policy (now consolidated in --help-time), not a change to it. Suppressed by --no-diagnostics/--silent.
  • absorb_jwt() - flatten JWT claims onto the event - A new whole-event mutator joining the absorb family: it parses a JWT from a string field and merges its claims into the event (header and signature ignored), returning the usual status map. All-or-nothing like absorb_json() — a malformed token sets status = "parse_error" and leaves the event untouched. Supports keep_source/overwrite. Signatures are not verified (debugging / trusted tokens only); for datetime-typed exp/iat/nbf use parse_jwt().
  • parse_jwt() decodes the standard time claims into datetimes - The JWT parser now exposes the NumericDate claims exp, iat, and nbf as datetimes under expires_at, issued_at, and not_before (alongside the raw integers in claims), so they compose with kelora's time machinery: --filter 'e.token.parse_jwt().expires_at < now()', (jwt.expires_at - jwt.issued_at), jwt.expires_at.to_iso(). Claims are read as whole seconds; a missing/invalid claim omits its field rather than erroring. Still does not verify signatures.
  • absorb_logfmt() - quote-aware sibling of absorb_kv() - A new whole-event mutator that parses a logfmt string field and merges its keys, returning the usual absorb status map. Unlike the plain-splitter absorb_kv() (which keeps quotes and splits inside quoted values, mangling err="connection refused"), it is quote-aware and infers numeric/boolean types. All-or-nothing like absorb_json(). Supports keep_source/overwrite. The parse_kv()/absorb_kv() docs now flag their lack of quote-awareness and point here. (Relatedly, strict-mode option errors from absorb_json/absorb_regex now name the actual function instead of always reporting absorb_kv:.)
  • --max-line-bytes per-line memory circuit breaker - A new safety cap on the bytes a single input line may consume in memory (default 64 MiB), guarding against runaway RAM from a newline-free stream — e.g. a tiny gzip/zstd payload that decompresses into one enormous line. Reading is streamed, so large multi-line files are unaffected; only a single over-long line trips it. An over-limit line is truncated to the cap with a warning (🔸, exit 0); under --strict it is a hard error (exit 1). Accepts a byte count or IEC/SI suffix (--max-line-bytes 1MiB, 1GiB, 1048576); 0/off/unlimited disables it. The default is sized for ~zero false positives on real logs (Docker/CRI split lines at 16 KB; fat JSON tops out in low single-digit MB). See SECURITY.md. (Recursive ZIP bombs like 42.zip were never a risk — ZIP input is rejected; only gzip/zstd are supported.) See Changed for the breaking default.

Changed

  • Diagnostic re-tiering and data-only-mode warning behavior (part of the three-tier model above) - Several advisory messages were reclassified so a message's appearance matches the flag that silences it: the --span/--parallel and --window/-B/-C conflict notices, the over-large-span notice, and the "parsing mostly failed" notice are now warnings (🔸); the "writing to a file named json; did you mean -F?" notice moved from warning to hint (💡). Data-only modes (-m/--drain/--discover) now hush only hints, not warnings, so a recovered --exec error still surfaces under -m. The detection notices lost an inconsistent terminal-only gate and now follow only their tier flags, reaching a stuck user even through redirected/CI stderr. Going further, a successful run is now silent (the Unix "rule of silence"): the neutral 🔹 status notices ("Auto-detected format: …", config/defaults/alias expansion) no longer print by default — run -v/--verbose to see them, with no exception for an explicit --config-file — while a config that fails to load is still a loud error. The generic zero-match notice is gone (an empty result after your own filter is self-evident); the specific zero-result hints stay, since each names a concrete footgun (-l/--levels on unstructured input, no timestamps for --since/--until, a typo'd filter field, a quoted numeric comparison).

  • Removed: undocumented KELORA_NO_TIPS env var - The single-purpose, never-documented KELORA_NO_TIPS (it only ever gated the format-detection tip) is gone. Use KELORA_NO_HINTS for all hints, or KELORA_NO_WARNINGS for warnings — the env vars now mirror the flags. No deprecation alias is kept.

  • Human metrics table gets labeled, aligned columns - In --metrics=full, list-valued metrics (frequency tables and track_top/track_bottom[_by] rankings) now print a two-column table with a header row and right-aligned numbers, so a row like 200 3 reads unambiguously as value 200, count 3. Columns are named for context (value/count for track_freq, item/count or item/score for rankings), the header's count noun agrees with the left column, and the redundant #1/#2 rank prefix is dropped (rows are already in rank order). Display-only: the stored numbers, tsv stream, JSON, and scalar metrics are untouched.

  • Breaking: tracking-function redesign (track_freq, track_inc, track_top_by) - The tracking family is consolidated around one convention: track_fn(name, args...). See What's New for old → new examples.

    • track_freq(name, value) is the frequency table, replacing both the old one-argument track_count(value) and track_bucket(key, bucket). Values are stringified automatically (no to_string() needed), and counts land in separate per-name sub-maps so different metrics can no longer collide. Both old names error with a migration hint.
    • track_inc(name) increments a running counter by 1 — sugar for track_sum(name, 1) (the two merge identically). For weighted accumulation use track_sum(name, value).
    • Score-based ranking moved from the 4-argument track_top(key, item, n, value) to track_top_by(name, item, score [, n]) / track_bottom_by; the old form errors with a migration hint. n now defaults to 10 in all four ranking functions.
    • All track_* functions now skip Unit () (missing-field) values instead of erroring. Skips are counted per metric, and a --diagnostics hint surfaces a likely field-name typo — but only when a metric recorded a value on no event, so a field present in some events and absent in others no longer triggers it.
    • Reusing one metric name across different track functions (e.g. track_sum("x", …) then track_min("x", …)) is now a call-time error (previously silently blended into garbage under parallel merging). In --parallel, a name reused across --begin vs the event stages is caught at merge time as a warning.
    • Float values keep their 1.x track_bucket labels (200.0 → "200"), so JSON consumers keyed on the old names keep working.
    • track_unique warns once past 100,000 stored values, pointing at track_cardinality() for unique counts; honors --silent/--no-diagnostics.
  • Breaking: gate-vs-transform exit-code model - Kelora now exits non-zero only when it couldn't do the job, not because the data was messy. Gates — parse and each individual --filter stage — must work: if a gate never once succeeds (no line parses, or a filter errors on every event it sees) the run exits 1; a gate erroring on only some records is recovered (exit 0), and filters are gated per stage so a working first filter can't mask a broken later one. Transforms — exec — are best-effort: a failing --exec rolls back to the original event and emits it, so exec errors never fail the run on their own. Structural failures and --assert still fail in any mode; --strict escalates any single error. Two changes from 1.x: (1) a --filter that errors on every event now exits 1 instead of 0 — a totally broken filter used to pass monitoring checks silently (#241); (2) a partial parse failure now exits 0 instead of 1 (only all-lines-fail still exits 1). The full model with a scenario table lives in Error Handling.

  • Breaking: config files are validated strictly - .kelora.ini (and --config-file) now reject unknown root keys, unknown sections, and malformed lines, naming the file and line (with a "did you mean" hint for case mismatches). Previously a typo such as defualts = or [alias] was silently ignored. Only defaults (root) and the [aliases] section are recognized; a rejected file exits 2 (was 1).

  • Breaking: invalid --input-tz is rejected - An unrecognized --input-tz (e.g. Europe/Berln) now fails fast with exit code 2 instead of silently falling back to local time, which could shift every timestamp. Use local, UTC, or a valid IANA timezone name.

  • Breaking: failed type annotations yield () instead of a string - For :int/:float/:bool annotations in csv/tsv/cols/regex, a value that can't satisfy the declared type now becomes () (explicitly absent) in resilient mode instead of keeping the original string; the rest of the row is preserved. --strict still aborts. This unifies all four typed parsers (fixing a bug where cols ignored --strict for conversions). For tolerant coercion with a fallback, drop the annotation and use to_int_or in a script stage.

  • Breaking: ragged CSV/TSV rows are kept, and --strict rejects them - Rows with more columns than the header previously lost the extras silently. Overflow columns are now kept under positional names (c5, c6, …), short rows keep trailing fields absent, and both are counted in --stats. --strict now treats a ragged row as a parse error (previously it only governed type conversion), naming where the expected width came from ((from header) / (from first line)).

  • Breaking: logfmt/CEF numeric inference no longer mangles zero-padded or signed values - The type-inferring parsers (logfmt, cef) used to coerce any Rust-parseable number, silently rewriting data: leading zeros dropped (zip=02134 → 2134), a leading + stripped (phone=+15551234), and inf/nan/Infinity turned into floats. A value is now coerced only when it is a valid JSON number (no leading zeros, no leading +, no inf/nan); everything else stays a string. Genuine numbers are unaffected (status=500, dur=1.5, n=-5, sci=1e3), and the same token now resolves to the same type whether it arrives via JSON or logfmt/CEF — which matters for mixed-format cascades. csv/tsv/cols/regex are unchanged. Migration: a field that relied on the old coercion (code=007 compared with == 7) now compares as a string (== "007"); coerce with --exec 'e.code = to_int_or(e.code, 0)' if needed.

  • Breaking: default-format word-wrapping is now TTY-aware - The default output format used to wrap wide events onto continuation lines even when piped or redirected, so wc -l, head, and other line-oriented consumers over-counted. Wrapping now follows the same human-vs-machine rule as color and emoji: auto by default (on for a terminal, off for a pipe), so each event stays on one line downstream. --wrap forces wrapping through a pipe, --no-wrap disables it everywhere; both settable via defaults in .kelora.ini. Interactive output is unchanged.

  • Breaking: input lines are capped at 64 MiB by default (--max-line-bytes) - To bound memory against a newline-free stream (see Added), a single input line may now use at most 64 MiB by default; a longer line is truncated to the cap with a warning (exit 0), or errors under --strict (exit 1). No real log line approaches this, so normal use is unaffected, but a workflow that genuinely processed >64 MiB single lines must raise or disable the cap: --max-line-bytes 0 (unlimited) or a higher value like --max-line-bytes 256MiB.

  • Parallel mode now delivers worker tracking metadata reliably - Worker internal tracking state (operation metadata, error counters) is now delivered exactly once per worker, per-batch user deltas are cleared after each send, and the multiline (event-batch) path attaches operation metadata like the line path. This fixes several --parallel inconsistencies: track_avg/track_stats finalize correctly inside --end (previously a raw {sum, count} map), the skipped-missing-value and exec-error diagnostics now appear as in sequential mode, and multiline runs merge averages correctly.

  • Faster JSON parsing - The JSON line parser now deserializes straight into the event's field map via a custom serde visitor, skipping the intermediate serde_json::Value tree that dominated worker-thread CPU. Output is byte-identical; ~17–31% faster wall-clock on JSON inputs, with the largest gains on wide objects.

  • --metrics renders track_freq maps as an aligned, sorted list - The human --metrics view previously dumped raw Rhai map syntax (status = #{"500": 67, "404": 12}). It now prints an aligned list sorted by count descending, truncated to 5 entries (with a "+N more" hint) unless --metrics=full or the map has ≤10 categories. Display-only: --metrics-file and JSON keep the full map.

  • Consistent count label across grouped metric outputs - The human --metrics view now labels a track_freq/--freq table (N items): to match the ranked (track_top/track_bottom) and --drain outputs; track_unique's (N unique): label is unchanged. Display-only.

  • --stats/--metrics format flags are more discoverable - --help now notes that the format attaches with = (so -s json reads json as a filename); a failed open of a "file" named exactly json/table/short/full appends a hint pointing at the --stats=json form; and the hidden config-override negations (--no-strict, --no-parallel, --no-stats, --no-metrics, --no-silent) are now mentioned in their positive flag's help.

  • --metrics text view rounds floats consistently - Float metrics in the human view are now rounded to 6 significant figures, so a track_stats block no longer mixes clean percentiles with noisy raw floats. Display-only: stored values and JSON keep full precision.

  • --discover footer expanded - The discover table now identifies the primary timestamp field (timestamp: ts, timestamp: ts (60% parsed), or timestamp: when (--ts-field) for an override), names the input parser (format: cef (auto-detected), or per-format counts for cascades), and moves the scanned-event count to a footer line. -d=json gains matching timestamp and format/format_counts objects. String examples are now quoted and escaped to match -F inspect, and the examples column grows to the full terminal width.

  • Clearer missing-field model and hints in Rhai scripts - A missing field is () and access never throws by itself; this is now documented in --help-rhai with the safe idioms (e.has/e.get/??). Referencing a field without the e. prefix (e.g. status instead of e.status) now suggests e.<field> when the bare name matches a real field, instead of offering wrong-scope variables.

  • line-fallback and mixed-format hints point at field extraction - When auto-detection keeps whole lines as line, the hint now suggests -f 'cols:ts(2) level *msg' (or a regex:) and cascading a mixed file with repeated -f. When -f auto locks onto one format but the input is mixed, the parse-failure warning re-detects the failing line and prints a copy-pasteable cascade (Detected mixed formats (json + line). Try: -f json,line).

  • --include no longer double-executes before the first stage - An --include placed before the first --filter/--exec stage was loaded into both that stage and a synthesized begin stage, running any top-level statements twice. Includes now form a begin/end script only when an explicit --begin/--end is present. Helper-function-only includes (the documented use) are unaffected.

  • Parse error summaries include filenames - Parse error messages now show which file the error came from, making multi-file runs easier to debug.

  • Non-UTF-8 input is decoded losslessly instead of aborting the stream - A single invalid UTF-8 byte previously tore down the whole run (rest of file silently dropped, exit 1, no opt-out). Kelora now tolerates non-UTF-8 input the way grep/ripgrep do: invalid byte sequences are replaced with U+FFFD (�), so a stray Latin-1/Windows-1252 byte or embedded binary no longer truncates a multi-GB file. Recovery is visible — a diagnostic reports how many lines were affected (also in --stats) — and the exit code stays 0. Clean logs are byte-for-byte unchanged. Pass --strict-utf8 to restore the old hard-failure behavior.

Fixed

  • emit_each() in --begin/--end is now a clear error instead of silently misbehaving - emit_each() defers its events to a buffer drained only by the per-event loop, so --begin/--end emissions were silently dropped (under --no-input), interleaved as a side effect of the first event, or lost entirely (--end) — and absent under --parallel. It now raises a runtime error from --begin/--end (exit 1), mirroring how state/drain_template reject --parallel; emit_each() in --exec/--filter is unchanged. Breaking: move any --begin 'emit_each(…)' to a per-event --exec stage, or pipe a counter (seq 1 N | kelora -f line --exec 'emit_each(…)').
  • -o <format> (meant for -F <format>) now warns instead of silently writing a mystery file - kelora … -o json silently wrote a file named json, printed nothing to stdout, and exited 0. When --output-file's value is a bare name (no separator, no extension) matching a known format keyword, kelora now prints a stderr warning (did you mean -F json (--output-format)?) while still writing the file. Honors --silent; real filenames like out.json or /tmp/json never trigger it.
  • Blank CSV header columns get positional cN names instead of an empty key (which silently dropped data) - A blank header cell produced an empty-string key, and multiple blanks collided into one "" field, silently dropping data (a,,,c over 1,2,3,4 lost the 2). Empty keys are also unrepresentable in logfmt. Blank cells now take the positional cN name used for headerless and ragged-overflow columns, so every column stays addressable and the output round-trips. The logfmt/to_logfmt() key sanitizer also maps an empty key to _ as defense-in-depth.
  • A single-key nested object no longer drops its key when flattened to logfmt/csv/to_logfmt() - The compact flattening had a special case that, when a nested structure flattened to exactly one entry, emitted only the value and discarded the key — so {"a":{"b":1}} became a=1 (losing b). The single-entry branch now keeps its key=value/key:value shape like the multi-entry case (a="b=1"), and single-element arrays keep their index (a="0=42"). Empty maps/arrays still render as an empty value, so no empty/null behavior changed.
  • array.percentile() accepts an integer percentile, matching its own docs - Rhai doesn't coerce an integer literal to f64, so the documented arr.percentile(95) failed with Function not found — only .percentile(95.0) worked. An integer overload is now registered alongside the float one, so both forms work.
  • Rhai docs no longer show the unsupported for (key, value) in map form - The --help-rhai guide, cheatsheet, and span-aggregation cookbook illustrated map iteration with for (key, value) in map, which Rhai doesn't support (object maps aren't directly iterable), so a new user copying it hit an immediate error. All spots now use the working idiom — for key in e.keys() { … e[key] … }. The cookbook's per-span CSV example is corrected to read span.metrics["service"] and iterate its keys.
  • Datetime values render as RFC3339 in scripts instead of an internal type name - Interpolating a datetime in a string template (`${span.start}`), printing it, or letting it fall through to_string()/to_debug() emitted the Rust type path rather than the timestamp, because only DurationWrapper had registered those functions. Both are now registered for DateTimeWrapper, so `${span.start}` yields 2024-07-01T09:00:00+00:00. Most visible in --span-close hooks; .to_iso()/.format() are unchanged.
  • Datetime/duration values stored in a field render correctly in output instead of leaking an internal type name - The companion to the fix above: assigning a datetime/duration to an event field and emitting it (--exec 'e.t = meta.parsed_ts') printed the Rust type path in default/logfmt/json/csv/inspect. All serialization paths now route these wrappers through their Display, so a datetime renders as 2026-01-02T15:04:05+00:00 and a duration as 1m 30s, and --discover reports their type as datetime/duration.
  • No spurious "rerun with -m" metrics hint when --end consumes the metrics - The nudge printed after a run that records track_* metrics without a display option also fired when an --end stage was present, even though --end is the idiomatic way to consume metrics into a report. The hint now treats an --end stage (alongside -m/--metrics-file) as the metrics already being handled; the bare track-and-forgot--m case still gets the nudge.
  • clamp() with an inverted range reports an error instead of aborting the process - Both clamp() overloads forwarded to std's clamp, which panics when min > max — so clamp(50, 100, 10) aborted the whole run with exit 134 under panic = "abort". Both now return a runtime error naming the bounds, so the run continues. In-range clamping is unchanged.
  • Dividing a duration by zero reports an error instead of aborting the process - The Rhai duration / n operator forwarded to chrono's Duration / i32, which panics on a zero divisor (including an i64 value that truncates to zero), aborting the run with exit 134. The operator now rejects a zero (narrowed) divisor with a runtime error. Valid divisors are unchanged.
  • A large --span time duration no longer aborts the process - A span duration that fit in i64 milliseconds but exceeded chrono's datetime range (e.g. --span 1000000000d) hit an unchecked .unwrap() and panicked, aborting with exit 134. The window-boundary conversion now clamps to chrono's representable min/max, so an oversized span degrades to a single all-encompassing window and exits 0.
  • ts_nanos() reports an error out of range instead of silently returning 0 - ts_nanos() is backed by chrono's timestamp_nanos_opt(), None outside ~1677..2262; the previous .unwrap_or(0) mapped every out-of-range timestamp (including 9999-12-31) to the Unix epoch, silently corrupting downstream arithmetic. It now returns a runtime error naming the supported range, matching round_to/ceil_to. In-range conversions are unchanged.
  • --stats=json emits JSON instead of silently falling back to the table - --stats=json was documented but never wired to a renderer, so it printed the human table. It now produces a JSON object whose groups mirror the table view (format, lines, events, throughput, timestamp, time_span, levels, keys, plus ragged_rows/decode_warnings/assertion_failures/files when relevant). Bare -s and --stats=table are unchanged.
  • Minute-precision timestamps (2024-01-15 12:00) now parse - --help advertised journalctl-style '2024-01-15 12:00' (date plus HH:MM, no seconds), but the parser had no matching format, so the documented value failed with exit 2 and minute-precision event timestamps were silently dropped. The parser now accepts minute precision in both separators and with an optional zone marker (2024-01-15T12:00Z, …+00:00), for both bounds and event fields. Seconds-bearing forms are unaffected.
  • Missing-field typo hint now survives the implied -m of --freq/--describe - The track_* skipped-missing-value hint — the signal that catches a typo'd field name — was hidden by the diagnostics suppression that --freq/--describe/--metrics/--drain imply, so kelora app.log --freq stauts reported a bare No metrics tracked with no clue. The hint now survives a data-only mode's implicit suppression (stderr, exit 0), while an explicit --no-diagnostics/--silent still suppress it.
  • --merge-sorted on stdin no longer silently skips its validation - Fed from stdin (no file arguments), --merge-sorted passed the stream straight through — disordered events came out in original order with exit 0 and the timestamp/disorder checks were dropped, voiding the flag's contract on the one input you can't pre-sort. Stdin is now rejected with an error pointing at the files form (kelora --merge-sorted app-*.log); file inputs are unchanged.
  • -k/--keys and -K/--exclude-keys now trim whitespace around comma-separated entries - -k 'ts, level, msg' silently selected only ts and dropped level/ msg, because clap split on commas without trimming (--exclude-keys ' secret ' likewise failed to drop the field). Keys now trim like --levels and field specs already did, and empty entries (-k 'a,,b') are dropped.
  • Self-relative now+/now- time bounds now work, and space-separated negative durations parse - Two documented forms were broken: (1) --since now-15m (from --help-time) failed because a self-relative now± bound standing alone never reached the anchor parser — all independent bounds now parse through parse_anchored_timestamp; (2) --since -30m was rejected as an unknown argument, forcing the = form — --since/--until now accept leading-hyphen values. Garbage like --since now-bogus still exits 2.
  • --discover Seen/Miss are now per-event for array and nested fields - A flattened array-element row (tags[]) counted one observation per element but divided Miss% by the event count, so an array present in every event read as mostly-missing and disagreed with its container row. Seen and Miss% now count the number of events that contained the path; type breakdown, Uniq, and samples stay element-scoped. --discover=json follows suit: seen/missing are per-event, with a new observations field for the raw per-element count.
  • -k/--keys hint guides nested paths to get_path instead of guessing the parent - Pasting a nested name from --discover (-k api.queries) failed, and the "never present" hint just suggested the bare parent (Did you mean 'api'?). When an unseen key looks like a flattened path whose leading segment is a real field, the hint now explains that -k/--exclude-keys act on top-level fields and points at --exec 'e.val = e.get_path("api.queries")' (or -k tags for tags[]). --help documents the top-level-only behavior.
  • CSV/TSV quoted fields with embedded newlines are reassembled instead of corrupting rows - A valid RFC 4180 record whose quoted value contained a newline was split on the physical newline before parsing, so one logical row became two — reported only as a soft "ragged rows" hint with exit 0, and Kelora's own CSV output round-tripped into corruption. The reader now uses a quote-aware chunker for the CSV/TSV family that tracks quote parity across physical lines and hands the parser one complete record. Reassembly is sequential-only; under -P/--parallel a split record is reported as a clear Unterminated quoted field error pointing at sequential mode, and a new parser guard rejects any record that ends inside an open quoted field.
  • -k/--keys no longer falsely warns about fields created in --exec - The "never present in the input" typo hint compared -k/--exclude-keys names against fields discovered in the input, before script stages ran, so a transform-created-then-selected field (--exec 'e.total = …' -k total) was flagged as missing even while printing on every line. The check now counts fields produced by scripts too (the union of input and output keys); real typos are still flagged.
  • Level hints and --stats no longer advertise non-level values as levels - The level filter reads the first present field from the level-name list (level, lvl, severity, …) and ignores the rest, but the stats collector recorded values from every such field, so level:"WARN" + severity:"high" made high show up as a level the filter could never match. Both collectors now stop at the first present level field, exactly as the filter does.
  • Duplicate error when no input file could be opened - When auto-detection couldn't open any input file, Kelora printed the specific per-file reason and a redundant generic Pipeline error: Failed to open any input files for detection. The generic line is now suppressed (via a typed marker error) in both sequential and parallel detection; the run still exits 1, and genuine pipeline errors still print normally.
  • Misleading "stdin is empty" hint on unparseable input - Piping content that produced zero events (e.g. plain text fed with -j) printed a "Parse errors" report and a contradictory "No input: stdin is empty…" nudge, because the hint's guard checked lines_read (only incremented under -s/--stats). The guard now also checks lines_errors, so input that arrived is no longer reported as empty. Genuinely empty input still gets the nudge.
  • track_top/track_bottom/track_top_by/track_bottom_by no longer drop heavy hitters - All four ranking functions truncated their list to N after every event, so a frequent item first seen once the N slots were full re-entered at count 1 and was evicted before it could accumulate — silently returning the first N distinct items rather than the most frequent. Each now retains every distinct item (like track_freq) and ranks/truncates only when metrics are emitted, and the parallel merge keeps all items. Results are now exact; memory is proportional to the number of distinct items.
  • In-place absorb_*/merge mutations no longer dropped from events - Whole-event mutating calls (absorb_kv/absorb_logfmt/absorb_json/absorb_regex/merge/enrich/rename_field) and in-place collection mutators on a nested field (e.tags.push(x), …) were visible within the same script but silently discarded from the emitted event unless the script also had an explicit e.field = … assignment — breaking the documented e.absorb_kv("msg") workflow. Kelora now detects these mutators rooted at e and runs the write-back; read-only methods stay on their fast path.
  • span.metrics no longer silently drops non-additive aggregators - Inside --span-close, span.metrics was computed by diffing the global tracker against the span baseline, which only works for additive aggregators — so track_avg/track_percentiles never appeared and track_max/track_min reported the global extreme, not the window's. track_avg now reports the true per-window average (Δsum / Δcount), joining track_freq/track_sum/track_unique. Genuinely non-additive aggregators (track_min/track_max/track_percentiles/track_cardinality/ranking) are omitted with a one-time warning per metric pointing at the span.events workaround (suppressed by --no-diagnostics/--silent).
  • Filter and exec error counts no longer undercounted - A --filter that errored on every line reported "Filter errors: 1 total" because the error paths skipped the thread-local→context sync the success path performs. Both filter error branches now persist their counts, matching the exec path; exec error counts also now survive the stage's atomic-rollback path.
  • Spurious conf read-only error fixed - Reading conf in an --exec stage and then filtering on the derived field in a separate --filter that doesn't name conf raised a false "conf map is read-only outside --begin" error. The immutability check is now gated on whether the stage actually references conf; genuine conf mutations are still rejected.
  • Script-error scope restored in --metrics/--drain - The "affecting every event" total-failure indicator in the script-error summary is derived from event counts, which data-only modes disabled — dropping the most useful part of the summary right where a stuck user lands. The scope now surfaces in these modes; the advisory follow-up honors --no-diagnostics and the suppression implied by data-only modes, and is re-enabled with --diagnostics.
  • Parse errors no longer swallowed in --metrics/--drain - These modes disabled stats collection, so parse failures produced no summary and exited 0, contradicting the documented contract. Parse errors are now reported on stderr and exit 1, matching normal mode.
  • Zero-result hint for level/time filters on the wrong input - Running -l/--levels against unstructured input with no level field, or --since/--until against input with no parseable timestamp, silently dropped every event. Kelora now prints a hint naming the structural cause and a workaround (parse levels with -f cols/regex or match text with --filter; set --ts-field/--ts-format). A genuine value mismatch is still treated as a legitimate empty result.
  • Typo hint for -k/--keys and --exclude-keys on names never present - Naming a field that never appears anywhere in the stream was silent: -k timestamp against ts-keyed logs emptied every event, and --exclude-keys passwrd quietly left scrubbed-meant data in the output. Kelora now prints a hint naming the unseen key, with a "Did you mean ''?" suggestion or a list of present fields, and the exclude variant states that nothing was removed. It fires per key but only on names absent from the entire stream, so heterogeneous logs are never flagged. Exit 0, honors --silent/--no-diagnostics.
  • track_stats metrics now usable in --end and span_close - Percentile, average, and cardinality sketches were exposed as raw blobs, making metrics["foo_p95"] unusable. They are now properly finalized to scalar values.
  • track_freq average false positive - A track_freq map whose values are literally named sum and count no longer renders as a bogus average; finalization keys off the recorded operation instead of sniffing the value's shape.
  • --discover HLL cardinality clamped to observation count - The HyperLogLog estimate could exceed the number of times a field was seen; it is now clamped so "Uniq > Seen" rows no longer appear.
  • --discover empty strings render as "" - Previously showed a bare gap that looked like a stray separator.
  • Grok alias lookup order - Fixed unordered alias lookup when building match names.

What's Changed

  • Add design specs for Bloom is_new novelty detection and inter-record delta/lag functions by @dloss in #159
  • Revise bloom filter spec to clarify error asymmetry and add exact variant by @dloss in #160
  • spec(inter-record): lock v1 scope and refine prev/lag/delta/ewma semantics by @dloss in #161
  • Add sequential inter-record helpers (prev/lag/delta/ewma) with docs and tests by @dloss in #165
  • Revert inter-record functions (prev/lag/delta/ewma) by @dloss in #167
  • dev/: consolidate feature and quality backlogs, remove completed checklists by @dloss in #168
  • Revise vision-and-design doc: simplify scope, clarify principles, and define pipeline model by @dloss in #169
  • dev/: add incremental refactoring plan by @dloss in #171
  • dev/: correct R1/R2 line counts and rescope R4 in refactoring plan by @dloss in #172
  • dev/: add klogg-inspired feature ideas to backlog by @dloss in #175
  • fix: 1.5.0 papercuts found during dogfooding by @dloss in #176
  • feat: format --discover examples in inspect style by @dloss in #177
  • feat: --discover-depth=0 means unlimited nested flattening by @dloss in #179
  • fix: clamp --discover HLL cardinality to scalar observation count by @dloss in #180
  • build(deps): bump rand from 0.9.2 to 0.9.4 by @dependabot[bot] in #178
  • docs: update Rhai help and function docs (snapshotting, bar/sparkline, styling) by @dloss in #181
  • Add Core Features doc and update homepage/nav by @dloss in #170
  • build(deps): bump thin-vec from 0.2.15 to 0.2.16 by @dependabot[bot] in #182
  • Include filename in first parse error summary by @dloss in #183
  • Large refactoring to improve maintainability by @dloss in #184
  • Deterministic capture name ordering and name-to-index lookup for grok matches by @dloss in #187
  • fix(grok): make alias-to-capture mapping deterministic by @dloss in #188
  • Add syslog error fixture for drain demo by @dloss in #186
  • Rewrite README and docs/index.md opener around drain demo by @dloss in #189
  • docs: simplify performance guidance and centralize benchmark snapshots by @dloss in #190
  • build(deps): bump thin-vec from 0.2.14 to 0.2.18 in /fuzz by @dependabot[bot] in #191
  • Align extract_json indexing with sibling extractors by @dloss in #192
  • Fix to_local() to use the system local timezone by @dloss in #193
  • Serialize stats tests to fix flaky recoverable-error assertion by @dloss in #195
  • Reject invalid --input-tz at config time by @dloss in #194
  • Add simple-cases throughput benchmark suite by @dloss in #196
  • Fix spurious conf immutability error in non-conf stages by @dloss in #197
  • docs: rewrite landing-page hero around discover → mixed-format → drain by @dloss in #198
  • docs: constrain README logo size by @dloss in #199
  • Add -d shortcut for --discover field profiling by @dloss in #201
  • Format metrics output with significant figures rounding by @dloss in #200
  • Improve error hints for missing e. prefix on event fields by @dloss in #202
  • Add structural hints for zero-result filters on mismatched input by @dloss in #203
  • Fix in-place mutating calls not persisting to event by @dloss in #204
  • Surface error scope in data-only modes; report parse errors in --metrics by @dloss in #205
  • Add built-in named log formats (adapted from lnav) by @dloss in #207
  • Make -f repeatable so cols:/regex: compose in cascades by @dloss in #206
  • Fix span.metrics silently dropping non-additive track_* aggregators by @dloss in #209
  • Add test coverage for the bare-field e. suggestion hint by @dloss in #210
  • Preserve ragged CSV/TSV rows instead of silently dropping columns by @dloss in #211
  • Redesign tracking functions for 2.0: track_count(name, category) et al by @dloss in #212
  • Add curated "did you mean" hints for common flag synonyms by @dloss in #213
  • Format track_count maps as aligned, sorted lists in --metrics output by @dloss in #214
  • Add data-driven legends to map formatters (levelmap/keymap/tailmap) by @dloss in #215
  • Add map.get() methods for safe field access with defaults by @dloss in #216
  • Detect and suggest mixed format cascades in parse failure warnings by @dloss in #217
  • Add timestamp respacing for Apache log generation by @dloss in #219
  • Add levelmap example to quick help by @dloss in #220
  • Fix filter error tracking to count all failures, not just the last by @dloss in #221
  • Register all track_* functions in function catalog for error hints by @dloss in #222
  • docs: Add quick-start guide for running examples by @dloss in #223
  • Add keyword filtering to --help-functions by @dloss in #218
  • Add timestamp field detection and display to discover output by @dloss in #224
  • 2.0 release: tracking redesign, named formats, cascades, discover enhancements by @dloss in #225
  • Clarify JWT parsing documentation and update payload reference by @dloss in #228
  • Add WrapMode enum for intelligent TTY-aware word wrapping by @dloss in #226
  • Add typo detection hints for -k/--keys and --exclude-keys by @dloss in #227
  • Support bracketed timestamps in iso8601-level log format by @dloss in #229
  • Update README example output to show parsed field breakdown by @dloss in #230
  • Warn when pseudonyms use ephemeral key at default verbosity by @dloss in #231
  • Surface meta.parsed_ts and other event metadata in inspect formatter by @dloss in #233
  • Add hint for empty stdin when no files given by @dloss in #235
  • Distinguish level field absence from vocabulary mismatch in hints by @dloss in #234
  • Deduplicate log level color mapping logic by @dloss in #232
  • docs(faq): add entry on level-severity filtering by @dloss in #237
  • Fix track_top/bottom to retain all items until format time by @dloss in #236
  • docs: add What's New in 2.0 migration guide by @dloss in #238
  • docs: clarify track_top_by/track_bottom_by rank distinct items by @dloss in #240
  • Fix --help-time anchored timestamp examples to use current names by @dloss in #243
  • Decode non-UTF-8 input losslessly instead of aborting the stream by @dloss in #244
  • Make exit code track "did the job", not "any error" (#241) by @dloss in #245
  • Gate filters per stage; persist gate counters across engine calls (#241) by @dloss in #246
  • Fix three first-time-user papercuts: empty-stdin hint, exit-code docs, missing-file error by @dloss in #247
  • Add debug tripwire for stale gate-success flags by @dloss in #248
  • Fix broken now± and space-separated relative --since/--until bounds by @dloss in #249
  • Fix CSV multiline corruption, false -k hint, and level over-collection by @dloss in #250
  • docs: update README --discover footer to include meta.parsed_ts hint by @dloss in #253
  • Add hints for silent key-selection drops and number-vs-string filters by @dloss in #255
  • Reassemble multi-line CSV/TSV records in parallel mode by @dloss in #254
  • Make randomness reproducible via KELORA_SEED; de-flake sampling test by @dloss in #256
  • Parse minute-precision timestamps (2024-01-15 12:00) as documented by @dloss in #252
  • docs: slim the power-user page into a discovery gallery; split out a state guide by @dloss in #257
  • Fix two out-of-range datetime bugs: --span panic and silent ts_nanos zero by @dloss in #258
  • Rename track_count to track_freq; add track_inc counter by @dloss in #259
  • Hint at --discover-final from --discover when pipeline reshapes fields by @dloss in #261
  • feat(metrics): --count / --describe / --top CLI sugar for common aggregations by @dloss in #262
  • Fix two process-aborting panics reachable from Rhai scripts by @dloss in #260
  • docs: trim duplicated cols mechanics in --help-regex by @dloss in #263
  • Harmonize --help-formats: describe all input formats by @dloss in #264
  • fix(multiline): make --multiline-join=space insert a separator by @dloss in #266
  • Rename metrics-sugar --count to --freq; redirect --count by @dloss in #267
  • Render datetimes as RFC3339 in scripts; drop redundant metrics hint with --end by @dloss in #268
  • Fix five new-user UX/consistency issues by @dloss in #269
  • Fix five new-user UX/consistency issues by @dloss in #270
  • docs(rhai): fix unsupported for (key, value) in map iteration examples by @dloss in #271
  • docs(functions): note PII patterns are opt-in for normalized() by @dloss in #272
  • docs(functions): fix misattributed PII filters in Drain help by @dloss in #274
  • docs(tutorials): pin -f line in multiline stacktrace examples by @dloss in #273
  • Make first-line basis of auto-detection clearer by @dloss in #275
  • Honor --no-emoji in emit_each and stage-help diagnostics by @dloss in #265
  • Surface missing-field typo hint in --freq/--describe/--top by @dloss in #277
  • Add -b/--brief to the -h quick help screen by @dloss in #278
  • docs(security): clarify Rhai capability model vs. sandbox by @dloss in #279
  • Bump version to 1.99.0 (2.0 preview) by @dloss in #280
  • fix(rhai): accept integer percentile in array.percentile() by @dloss in #276
  • fix(logfmt): quote flattened nested values so output round-trips by @dloss in #282
  • feat(formats): add cri format for Kubernetes container logs by @dloss in #284
  • Add quote-aware absorb_logfmt and clarify parse_kv/absorb_kv limits by @dloss in #285
  • fix(output): keep key when flattening single-key nested values by @dloss in #289
  • Warn when naive timestamps are silently assumed UTC (#287) by @dloss in #288
  • fix(logfmt,cef): only coerce values that are valid JSON numbers by @dloss in #290
  • fix(logfmt): sanitize empty field key so output round-trips by @dloss in #291
  • docs: clarify that --stats shows stats instead of events in quickstart by @dloss in #292
  • docs: fix Rhai calling-style guidance and a Quickstart wording slip by @dloss in #294
  • fix(cli): warn on -o footgun and remove stray output artifacts by @dloss in #293
  • Add postgres and alb named log formats by @dloss in #286
  • Force sequential mode for --window and -B/-C context (issue #281) by @dloss in #283
  • docs(changelog): merge duplicate Fixed sections under [Unreleased] by @dloss in #295
  • Reject emit_each() in --begin/--end instead of silently dropping events by @dloss in #297
  • feat(jwt): expose exp/iat/nbf claims as datetimes in parse_jwt by @dloss in #298
  • feat: smartcase matching for --help-functions search by @dloss in #299
  • Add state.get(key, default) overload for Map parity by @dloss in #296
  • Add keyword search to --help by @dloss in #300
  • feat(metrics): pipe-friendly tsv output, drop --top for shell ranking by @dloss in #301
  • Gate the missing-field skip hint to never-recorded metrics by @dloss in #302
  • Label metrics table number column as count vs score by @dloss in #303
  • Add --card flag as shorthand for track_cardinality() by @dloss in #304
  • Fix exit-code ordering in --help output by @dloss in #306
  • Add three-tier diagnostic model with --no-warnings / --no-hints by @dloss in #308
  • Adopt rule of silence: status is verbose-only, drop generic zero-match nag by @dloss in #309
  • Drop the alb named format; document postgres multi-line handling by @dloss in #310
  • Render datetime/duration field values instead of leaking the type name by @dloss in #311
  • Exit 2 (not 1) on a malformed config file by @dloss in #312
  • docs: tighten 2.0 changelog and what's-new for release by @dloss in #313
  • docs: make v2.0 notice less prominent by @dloss in #314
  • Rename user-facing "named application-log formats" to "built-in application-log formats" by @dloss in #315
  • docs: surface What's New page in nav and home page by @dloss in #316
  • docs: move Installation to a dedicated Getting Started page by @dloss in #317
  • docs: add from-source install option to installation.md by @dloss in #318
  • docs: update log-analysis SKILL.md for v2.0 by @dloss in #319
  • docs(dev): add baseline novelty detection design note (logjuicer-inspired) by @dloss in #321
  • docs: rewrite dos-protection spec around the input pipeline by @dloss in #320
  • Complete --help 'See also' line with all help topics by @dloss in #305
  • Shorten year-less timestamp warning to two lines by @dloss in #307
  • docs: remove redundant Installation section from Quickstart by @dloss in #322

Full Changelog: v1.5.0...v2.0.0