Repository navigation
v2.0.0
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-diagnosticsshortcut 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. postgresbuilt-in format - Parses PostgreSQL server logs written with the defaultlog_line_prefix = '%m [%p] 'intots,log_tz,pid,level, andmsg. The timestamp is naive and resolved through--input-tz(default UTC); the logged zone abbreviation is kept inlog_tzbut 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 indentto fold multi-lineERROR:/STATEMENT:records. A customized prefix won't auto-detect — use-f regex:. (Adapted from lnav, BSD-3-Clause.)--helpnow documents the exit-code model - A new "Exit Codes" section in the full--helpreference spells out the resilient-by-default contract:0is success even when some lines/transforms failed (recovered),1covers genuine failures (unopenable input, failed--assert, a gate that never succeeded,--stricterrors),2is invalid usage, plus the signal/panic codes. The one-screenkelora -his unchanged.criformat for Kubernetes container logs - The CRI/containerd on-disk layout<RFC3339Nano> <stream> <tag> <message>(the shape of/var/log/pods/*/*.logandkubectl logs --timestamps) is now a built-in format, parsed intots,stream,tag, andmsg. Selectable with-f criand usable in cascades. Because a CRI message is often itself JSON or logfmt,crihas 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), andiso8601-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 thelinefallback, so nothing already detected changes. Year-less timestamps (glog,redis) resolve to the current year;haproxyis syslog-wrapped, so use-f haproxyfor the structured fields. (Adapted from lnav, BSD-3-Clause; seeTHIRD_PARTY_LICENSES.md.) - Composable
cols:/regex:cascades via repeated-f--fis 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 customtimestamp LEVEL messagetext 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 -
levelmapandkeymapnow 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), matchingtailmap. New--legend/--no-legendflags control all three map formats: by default the legend shows only on an interactive terminal, so piped output stays clean. -dshortcut and richer output for--discover- Field discovery, the documented starting point for unknown files, gets a short flag:kelora app.log -d(-d=jsonfor the machine-readable form).--discover-depth=0fully flattens deeply nested JSON, removing the previous 3-level cap. See Changed for the expanded discover footer.-Dshortcut for--discover-final, plus a discoverability hint - The post-pipeline counterpart to-dgets its own short flag (-D,-D=json). To close the "I added--execbut my computed field isn't in--discover" confusion (--discoverprofiles parsed input, before scripts run), the plain--discoverfooter now prints a tip pointing at--discover-finalwhenever the pipeline filters or transforms events. A bare probe stays uncluttered, and JSON output stays machine-clean.e.get()map accessor -e.get("key")ande.get("key", default)are now registered for events, mirroringget_pathfor 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 iplists the IP helpers. The=KEYWORDform works too; bare--help-functionsis unchanged. - Keyword search in
--help- The full CLI reference is searchable the same way:kelora --help KEYWORDprints 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 -jfinds only-j(never the-jinside--multiline-join); a bare word is a case-insensitive substring search across names and descriptions. The=KEYWORDform works; bare--helpis unchanged. - No-script shortcuts for the most common aggregations:
--freq,--describe,--card- The most common tracking operations are now plain flags.--freq FIELDbuilds a frequency table (track_freq),--describe FIELDprints a numeric summary (track_stats— count/min/max/avg/p50/p95/p99), and--card FIELDestimates 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/--bottomflag —--freqalready sorts by count descending, so ranking is left to the shell (--freq url | head). For grouping, custom names, or score-based ranking, write thetrack_*call in--exec. - Pipe-friendly
tsvmetrics output, auto-selected when not on a terminal - Metrics now have a tab-separated form alongside the human table and JSON: each metric emitsmetric<TAB>key<TAB>valuerows (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 thelsconvention it auto-selects — human table on a terminal,tsvstream when piped or redirected — so--freq url | head,| sort,| awk,| wccompose freely. Escape hatches:--metrics=fullforces the table through a pipe,--metrics=tsvforces the stream to a terminal. (Behavior change:kelora -m … > filenow yieldstsvrather than the table; add--metrics=fullfor 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. -Pshort flag for--parallel- The performance toggle now has a short form, following thexargs/GNUparallelconvention.- "No input" hint instead of silent exit - A bare
kelorareading 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/--helptext now states that interactive mode requires a terminal. -l/--levelswarns on a vocabulary mismatch instead of returning a silent empty result - When-ldrops every event because the level field uses a different dialect than the filter (glog logsI/W/E/F; syslog usesCRITnotCRITICAL), kelora now lists the levels present (-l ERROR matched none of the levels present: E,I) and points at a--filterworkaround. The filter stays purely lexical. Goes to stderr, exit0, 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--filtercompares a seen field for equality against a quoted numeric-looking literal and nothing matched, kelora now suggests dropping the quotes and points at-sto check the type. Goes to stderr, exit0, 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/--spanboundaries 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, noTZ), 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 likeabsorb_json()— a malformed token setsstatus = "parse_error"and leaves the event untouched. Supportskeep_source/overwrite. Signatures are not verified (debugging / trusted tokens only); for datetime-typedexp/iat/nbfuseparse_jwt().parse_jwt()decodes the standard time claims into datetimes - The JWT parser now exposes the NumericDate claimsexp,iat, andnbfas datetimes underexpires_at,issued_at, andnot_before(alongside the raw integers inclaims), 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 ofabsorb_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-splitterabsorb_kv()(which keeps quotes and splits inside quoted values, manglingerr="connection refused"), it is quote-aware and infers numeric/boolean types. All-or-nothing likeabsorb_json(). Supportskeep_source/overwrite. Theparse_kv()/absorb_kv()docs now flag their lack of quote-awareness and point here. (Relatedly, strict-mode option errors fromabsorb_json/absorb_regexnow name the actual function instead of always reportingabsorb_kv:.)--max-line-bytesper-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--strictit is a hard error (exit 1). Accepts a byte count or IEC/SI suffix (--max-line-bytes 1MiB,1GiB,1048576);0/off/unlimiteddisables 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 like42.zipwere 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/--paralleland--window/-B/-Cconflict notices, the over-large-span notice, and the "parsing mostly failed" notice are now warnings (🔸); the "writing to a file namedjson; 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--execerror 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/--verboseto 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/--levelson unstructured input, no timestamps for--since/--until, a typo'd filter field, a quoted numeric comparison). -
Removed: undocumented
KELORA_NO_TIPSenv var - The single-purpose, never-documentedKELORA_NO_TIPS(it only ever gated the format-detection tip) is gone. UseKELORA_NO_HINTSfor all hints, orKELORA_NO_WARNINGSfor 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 andtrack_top/track_bottom[_by]rankings) now print a two-column table with a header row and right-aligned numbers, so a row like200 3reads unambiguously as value200, count3. Columns are named for context (value/countfortrack_freq,item/countoritem/scorefor rankings), the header's count noun agrees with the left column, and the redundant#1/#2rank prefix is dropped (rows are already in rank order). Display-only: the stored numbers,tsvstream, 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-argumenttrack_count(value)andtrack_bucket(key, bucket). Values are stringified automatically (noto_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 fortrack_sum(name, 1)(the two merge identically). For weighted accumulation usetrack_sum(name, value).- Score-based ranking moved from the 4-argument
track_top(key, item, n, value)totrack_top_by(name, item, score [, n])/track_bottom_by; the old form errors with a migration hint.nnow 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--diagnosticshint 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", …)thentrack_min("x", …)) is now a call-time error (previously silently blended into garbage under parallel merging). In--parallel, a name reused across--beginvs the event stages is caught at merge time as a warning. - Float values keep their 1.x
track_bucketlabels (200.0→"200"), so JSON consumers keyed on the old names keep working. track_uniquewarns once past 100,000 stored values, pointing attrack_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
--filterstage — must work: if a gate never once succeeds (no line parses, or a filter errors on every event it sees) the run exits1; a gate erroring on only some records is recovered (exit0), 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--execrolls back to the original event and emits it, so exec errors never fail the run on their own. Structural failures and--assertstill fail in any mode;--strictescalates any single error. Two changes from 1.x: (1) a--filterthat errors on every event now exits1instead of0— a totally broken filter used to pass monitoring checks silently (#241); (2) a partial parse failure now exits0instead of1(only all-lines-fail still exits1). 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 asdefualts =or[alias]was silently ignored. Onlydefaults(root) and the[aliases]section are recognized; a rejected file exits2(was1). -
Breaking: invalid
--input-tzis rejected - An unrecognized--input-tz(e.g.Europe/Berln) now fails fast with exit code2instead of silently falling back to local time, which could shift every timestamp. Uselocal,UTC, or a valid IANA timezone name. -
Breaking: failed type annotations yield
()instead of a string - For:int/:float/:boolannotations 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.--strictstill aborts. This unifies all four typed parsers (fixing a bug wherecolsignored--strictfor conversions). For tolerant coercion with a fallback, drop the annotation and useto_int_orin a script stage. -
Breaking: ragged CSV/TSV rows are kept, and
--strictrejects 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.--strictnow 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), andinf/nan/Infinityturned into floats. A value is now coerced only when it is a valid JSON number (no leading zeros, no leading+, noinf/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=007compared 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.--wrapforces wrapping through a pipe,--no-wrapdisables it everywhere; both settable viadefaultsin.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
--parallelinconsistencies:track_avg/track_statsfinalize 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::Valuetree that dominated worker-thread CPU. Output is byte-identical; ~17–31% faster wall-clock on JSON inputs, with the largest gains on wide objects. -
--metricsrenderstrack_freqmaps as an aligned, sorted list - The human--metricsview 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=fullor the map has ≤10 categories. Display-only:--metrics-fileand JSON keep the full map. -
Consistent count label across grouped metric outputs - The human
--metricsview now labels atrack_freq/--freqtable(N items):to match the ranked (track_top/track_bottom) and--drainoutputs;track_unique's(N unique):label is unchanged. Display-only. -
--stats/--metricsformat flags are more discoverable ---helpnow notes that the format attaches with=(so-s jsonreadsjsonas a filename); a failed open of a "file" named exactlyjson/table/short/fullappends a hint pointing at the--stats=jsonform; 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. -
--metricstext view rounds floats consistently - Float metrics in the human view are now rounded to 6 significant figures, so atrack_statsblock no longer mixes clean percentiles with noisy raw floats. Display-only: stored values and JSON keep full precision. -
--discoverfooter expanded - The discover table now identifies the primary timestamp field (timestamp: ts,timestamp: ts (60% parsed), ortimestamp: 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=jsongains matchingtimestampandformat/format_countsobjects. 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-rhaiwith the safe idioms (e.has/e.get/??). Referencing a field without thee.prefix (e.g.statusinstead ofe.status) now suggestse.<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 asline, the hint now suggests-f 'cols:ts(2) level *msg'(or aregex:) and cascading a mixed file with repeated-f. When-f autolocks 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). -
--includeno longer double-executes before the first stage - An--includeplaced before the first--filter/--execstage 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/--endis 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 waygrep/ripgrepdo: invalid byte sequences are replaced withU+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 stays0. Clean logs are byte-for-byte unchanged. Pass--strict-utf8to restore the old hard-failure behavior.
Fixed
emit_each()in--begin/--endis 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/--endemissions 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(exit1), mirroring howstate/drain_templatereject--parallel;emit_each()in--exec/--filteris unchanged. Breaking: move any--begin 'emit_each(…)'to a per-event--execstage, 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 jsonsilently wrote a file namedjson, printed nothing to stdout, and exited0. 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 likeout.jsonor/tmp/jsonnever trigger it.- Blank CSV header columns get positional
cNnames 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,,,cover1,2,3,4lost the2). Empty keys are also unrepresentable inlogfmt. Blank cells now take the positionalcNname used for headerless and ragged-overflow columns, so every column stays addressable and the output round-trips. Thelogfmt/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}}becamea=1(losingb). The single-entry branch now keeps itskey=value/key:valueshape 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/nullbehavior changed. array.percentile()accepts an integer percentile, matching its own docs - Rhai doesn't coerce an integer literal tof64, so the documentedarr.percentile(95)failed withFunction 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 mapform - The--help-rhaiguide, cheatsheet, and span-aggregation cookbook illustrated map iteration withfor (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 readspan.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 throughto_string()/to_debug()emitted the Rust type path rather than the timestamp, because onlyDurationWrapperhad registered those functions. Both are now registered forDateTimeWrapper, so`${span.start}`yields2024-07-01T09:00:00+00:00. Most visible in--span-closehooks;.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 indefault/logfmt/json/csv/inspect. All serialization paths now route these wrappers through theirDisplay, so a datetime renders as2026-01-02T15:04:05+00:00and a duration as1m 30s, and--discoverreports their type asdatetime/duration. - No spurious "rerun with -m" metrics hint when
--endconsumes the metrics - The nudge printed after a run that recordstrack_*metrics without a display option also fired when an--endstage was present, even though--endis the idiomatic way to consume metrics into a report. The hint now treats an--endstage (alongside-m/--metrics-file) as the metrics already being handled; the bare track-and-forgot--mcase still gets the nudge. clamp()with an inverted range reports an error instead of aborting the process - Bothclamp()overloads forwarded tostd'sclamp, which panics whenmin > max— soclamp(50, 100, 10)aborted the whole run with exit134underpanic = "abort". Both now return a runtime error naming the bounds, so the run continues. In-range clamping is unchanged.- Dividing a
durationby zero reports an error instead of aborting the process - The Rhaiduration / noperator forwarded to chrono'sDuration / i32, which panics on a zero divisor (including ani64value that truncates to zero), aborting the run with exit134. The operator now rejects a zero (narrowed) divisor with a runtime error. Valid divisors are unchanged. - A large
--spantime duration no longer aborts the process - A span duration that fit ini64milliseconds but exceeded chrono's datetime range (e.g.--span 1000000000d) hit an unchecked.unwrap()and panicked, aborting with exit134. The window-boundary conversion now clamps to chrono's representable min/max, so an oversized span degrades to a single all-encompassing window and exits0. ts_nanos()reports an error out of range instead of silently returning0-ts_nanos()is backed by chrono'stimestamp_nanos_opt(),Noneoutside ~1677..2262; the previous.unwrap_or(0)mapped every out-of-range timestamp (including9999-12-31) to the Unix epoch, silently corrupting downstream arithmetic. It now returns a runtime error naming the supported range, matchinground_to/ceil_to. In-range conversions are unchanged.--stats=jsonemits JSON instead of silently falling back to the table ---stats=jsonwas 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, plusragged_rows/decode_warnings/assertion_failures/fileswhen relevant). Bare-sand--stats=tableare unchanged.- Minute-precision timestamps (
2024-01-15 12:00) now parse ---helpadvertised journalctl-style'2024-01-15 12:00'(date plusHH:MM, no seconds), but the parser had no matching format, so the documented value failed with exit2and 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
-mof--freq/--describe- Thetrack_*skipped-missing-value hint — the signal that catches a typo'd field name — was hidden by the diagnostics suppression that--freq/--describe/--metrics/--drainimply, sokelora app.log --freq stautsreported a bareNo metrics trackedwith no clue. The hint now survives a data-only mode's implicit suppression (stderr, exit0), while an explicit--no-diagnostics/--silentstill suppress it. --merge-sortedon stdin no longer silently skips its validation - Fed from stdin (no file arguments),--merge-sortedpassed the stream straight through — disordered events came out in original order with exit0and 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/--keysand-K/--exclude-keysnow trim whitespace around comma-separated entries --k 'ts, level, msg'silently selected onlytsand droppedlevel/msg, because clap split on commas without trimming (--exclude-keys ' secret 'likewise failed to drop the field). Keys now trim like--levelsand 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-relativenow±bound standing alone never reached the anchor parser — all independent bounds now parse throughparse_anchored_timestamp; (2)--since -30mwas rejected as an unknown argument, forcing the=form —--since/--untilnow accept leading-hyphen values. Garbage like--since now-bogusstill exits2. --discoverSeen/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=jsonfollows suit:seen/missingare per-event, with a newobservationsfield for the raw per-element count.-k/--keyshint guides nested paths toget_pathinstead 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-keysact on top-level fields and points at--exec 'e.val = e.get_path("api.queries")'(or-k tagsfortags[]).--helpdocuments 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/--parallela split record is reported as a clearUnterminated quoted fielderror pointing at sequential mode, and a new parser guard rejects any record that ends inside an open quoted field. -k/--keysno longer falsely warns about fields created in--exec- The "never present in the input" typo hint compared-k/--exclude-keysnames 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
--statsno 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, solevel:"WARN"+severity:"high"madehighshow 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 exits1, 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 checkedlines_read(only incremented under-s/--stats). The guard now also checkslines_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_byno 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 (liketrack_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_*/mergemutations 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 explicite.field = …assignment — breaking the documentede.absorb_kv("msg")workflow. Kelora now detects these mutators rooted ateand runs the write-back; read-only methods stay on their fast path. span.metricsno longer silently drops non-additive aggregators - Inside--span-close,span.metricswas computed by diffing the global tracker against the span baseline, which only works for additive aggregators — sotrack_avg/track_percentilesnever appeared andtrack_max/track_minreported the global extreme, not the window's.track_avgnow reports the true per-window average (Δsum / Δcount), joiningtrack_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 thespan.eventsworkaround (suppressed by--no-diagnostics/--silent).- Filter and exec error counts no longer undercounted - A
--filterthat 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
confread-only error fixed - Readingconfin an--execstage and then filtering on the derived field in a separate--filterthat doesn't nameconfraised a false "conf map is read-only outside --begin" error. The immutability check is now gated on whether the stage actually referencesconf; genuineconfmutations 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-diagnosticsand 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 exited0, contradicting the documented contract. Parse errors are now reported on stderr and exit1, matching normal mode. - Zero-result hint for level/time filters on the wrong input - Running
-l/--levelsagainst unstructured input with no level field, or--since/--untilagainst 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/regexor 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/--keysand--exclude-keyson names never present - Naming a field that never appears anywhere in the stream was silent:-k timestampagainstts-keyed logs emptied every event, and--exclude-keys passwrdquietly 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. Exit0, honors--silent/--no-diagnostics. track_statsmetrics now usable in--endandspan_close- Percentile, average, and cardinality sketches were exposed as raw blobs, makingmetrics["foo_p95"]unusable. They are now properly finalized to scalar values.track_freqaverage false positive - Atrack_freqmap whose values are literally namedsumandcountno longer renders as a bogus average; finalization keys off the recorded operation instead of sniffing the value's shape.--discoverHLL 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.--discoverempty 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_newnovelty 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/--keysand--exclude-keysby @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 mapiteration 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