{"id":51524792,"url":"https://github.com/ahrefs/riplog","last_synced_at":"2026-07-08T20:01:15.835Z","repository":{"id":358715401,"uuid":"1229937242","full_name":"ahrefs/riplog","owner":"ahrefs","description":"[experimental] ripgrep-like tool for `logfmt` logs","archived":false,"fork":false,"pushed_at":"2026-05-29T19:04:15.000Z","size":569,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T22:31:12.894Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ahrefs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-05T14:20:41.000Z","updated_at":"2026-05-29T19:04:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ahrefs/riplog","commit_stats":null,"previous_names":["ahrefs/riplog"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ahrefs/riplog","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahrefs%2Friplog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahrefs%2Friplog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahrefs%2Friplog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahrefs%2Friplog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ahrefs","download_url":"https://codeload.github.com/ahrefs/riplog/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahrefs%2Friplog/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35276781,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-08T02:00:06.796Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-08T20:01:14.984Z","updated_at":"2026-07-08T20:01:15.829Z","avatar_url":"https://github.com/ahrefs.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# riplog\n\nSlice and filter logfmt streams. Reads one or more files (or stdin),\nbisects on timestamp when given a time window, and applies boolean\nfilters per line.\n\n```\nriplog [OPTIONS] [FILE]...\n```\n\nMultiple files are processed in order; aggregated output (`--count`,\n`--group-by`, `--list-keys`, `--list-values-for`) is emitted once at\nthe end and reflects the union of all files. `--from`/`--to` are\napplied per file (each file is bisected independently), so a time\nrange that straddles a log rotation works as expected. `-f`/`-F` is\nattached to the *last* file — `riplog foo.log.1 foo.log -F` reads the\nrotated log, then the current log, then keeps tailing it.\n\nUse `-` as a file argument to read from stdin in position — e.g.\n`riplog rotated.log.1 rotated.log - --if 'level=error'` streams the\ntwo rotated logs, then stdin. `-` may appear at most once, and is\nincompatible with `-f`, `-F`, and `--time-range`. With `-` in the\nlist, symbolic `--from`/`--to` anchors and `--n-buckets` resolve\nagainst the real files' span; the resolved window is then applied\nas a per-line filter to stdin too. `--bucket=DURATION` switches to\nstreaming output (per-bucket rows emit as they close).\n\n## Install\n\nClone this and `cargo install --path=.` from inside the repo should do it.\nMake sure `~/.cargo/bin` is in your path.\n\n## Time slicing\n\n- `--from \u003cT\u003e`, `--to \u003cT\u003e`: bounds. Forms: RFC 3339\n  (`2026-04-24T18:09:03Z`), date (`2026-04-24`), time-of-day (`18:00`,\n  anchored to the file's first/last timestamp), or symbolic (`start`,\n  `start+1h`, `end-30m`). Duration units accept word and plural forms\n  with optional whitespace: `start+5 min`, `end-2 days`,\n  `start+1 hour`, `end - 30 seconds`. Units: `s/sec/seconds`,\n  `m/min/minutes`, `h/hour/hours`, `d/day/days`.\n- `--window-secs \u003cN\u003e`: reorder tolerance for the bisect. Default `10`.\n- `--time-range`: print the first and last timestamps. With multiple\n  files, reports the span across all of them (min of per-file firsts,\n  max of per-file lasts).\n- `--tz \u003cZ\u003e`: display timezone. `utc`, `local`, IANA name, or `+02:00`.\n\n## Filtering\n\n- `--if \u003cEXPR\u003e` (alias `--where`): boolean expression. Repeatable;\n  multiple `--if` are AND-ed.\n  - leaf predicates: `\u003ckey\u003e \u003cop\u003e \u003cvalue\u003e` with op in\n    `=`, `!=`, `\u003c`, `\u003c=`, `\u003e`, `\u003e=`, `=~`\n  - existence: `exists \u003ckey\u003e`\n  - logical: `and`, `or`, `not`, parentheses\n  - examples: `level\u003e=warn`,\n    `level=error and (facil=net or facil=db)`,\n    `not msg =~ \"noisy.*timeout\"`,\n    `exists trace_id and level\u003e=warn`\n\n## Sampling\n\n- `--sample-rate \u003cRATE\u003e`: keep each matched line with probability `RATE`\n  in `[0, 1]`. Applied after `--if` / time filters; counters reflect the\n  post-sampling set.\n- `--sample-if \u003cEXPR\u003e`: restrict the dice roll to lines matching this\n  expression (same syntax as `--if`); other lines pass through.\n\n## Output\n\n- `-o, --output \u003cFILE\u003e`: write to file instead of stdout.\n- `--color \u003cauto|always|never\u003e`: colorize. `auto` is on for terminal\n  stdout without `-o`.\n- `-n, --limit \u003cN\u003e`: stop after `N` matched lines.\n- `--json`: emit JSONL (one JSON value per line) instead of logfmt.\n  Matched lines become JSON objects with keys in logfmt parse order;\n  all values stay JSON strings (no number/bool coercion). Aggregation\n  rows (`--count` + `--group-by`/`--bucket`/`--n-buckets`) become JSON\n  objects with flat keys (`count` is a number, `key.\u003ck\u003e`, `bucket.start`,\n  `bucket.end`, `time.start`, `time.end` are strings). `--count` alone\n  emits `{\"count\": N}`. `--list-keys` and `--list-values-for` emit one\n  JSON array (multi-key `--list-values-for` emits an array of\n  `{\"key\": K, \"value\": V}` objects). Conflicts with `--raw-key` and\n  `--color=always`.\n\n## Aggregation (suppresses line output)\n\n- `--count`: print the matched-line count. Alone, emits a bare number.\n  Combined with `--group-by`, `--bucket`, and/or `--n-buckets`, emits\n  one logfmt row per group instead.\n- `--group-by \u003cKEY[,KEY…]\u003e`: group matched lines by key value (requires\n  `--count`). One logfmt row per tuple of values:\n  `count=N key.\u003ck1\u003e=… key.\u003ck2\u003e=… time.start=… time.end=…`. User keys\n  are prefixed with `key.` to disambiguate from synthetic columns;\n  `time.start`/`time.end` are the min/max observed timestamps in the\n  group. Repeatable, and a single flag may carry a comma-separated\n  list — `--group-by level,facil` and\n  `--group-by level --group-by facil` are equivalent.\n- `--bucket \u003cDURATION\u003e`: with `--count`, partition the time range into\n  fixed-size, epoch-aligned buckets; each row carries\n  `bucket.start=\u003cRFC 3339\u003e` and `bucket.end=\u003cRFC 3339\u003e` boundaries.\n  Same duration syntax as `--from start+\u003cdur\u003e`: `5m`, `30s`, `2 hours`.\n  Lines without a parseable timestamp are dropped (they can't be\n  bucketed). Composes with `--group-by` to bucket per\n  (group keys, time slice). Mutually exclusive with `--n-buckets`.\n- `--n-buckets \u003cN\u003e`: with `--count`, divide the active time window into\n  `N` equal-width buckets aligned to the window start. Width is\n  `(end − start) / N`, taking `start`/`end` from `--from`/`--to`\n  when set, otherwise from the file's first/last timestamps. Useful\n  for graphing — gives a fixed number of points across the time range.\n  Requires a file argument. Mutually exclusive with `--bucket`.\n- `--list-keys`: print every distinct key seen on matched lines.\n- `--list-values-for \u003cKEY[,KEY…]\u003e`: print every distinct value seen for\n  `KEY`. Repeatable / comma-separated like `--group-by`.\n- `--raw-key \u003cKEY\u003e`: emit only the unquoted, unescaped value of `KEY`\n  for each matched line (one per line; lines without the key are\n  skipped). Handy for piping a single field downstream, e.g.\n  `riplog app.log --if 'level=error' --raw-key msg | sort | uniq -c`.\n\nExample — error rate per service in 5-minute slices:\n\n```\nriplog --count --group-by=facil --bucket=5m --if 'level\u003e=error' \\\n       --from start --to end app.log\n```\n\nemits rows like:\n\n```\ncount=42 key.facil=db bucket.start=2026-05-06T12:00:00Z bucket.end=2026-05-06T12:05:00Z time.start=2026-05-06T12:00:01Z time.end=2026-05-06T12:04:58Z\ncount=17 key.facil=net bucket.start=2026-05-06T12:05:00Z bucket.end=2026-05-06T12:10:00Z time.start=2026-05-06T12:05:03Z time.end=2026-05-06T12:09:51Z\n```\n\nFor graphing, `--n-buckets` is often more convenient than `--bucket`:\n\n```\nriplog --count --n-buckets=60 --if 'level\u003e=error' --from start --to end app.log\n```\n\nalways emits 60 rows — one per equal-width slice across the range.\n\n### Streaming under `-f` / `-F` (and stdin)\n\nWhen `--bucket=DURATION` is combined with `-f`, `-F`, or stdin input,\noutput switches to streaming mode: each bucket's row is emitted as soon as\nthe bucket closes, in time-ascending order. A bucket B closes once\n`max_ts_seen \u003e B.end + window_secs` (the existing reorder-tolerance flag\ndoubles as the close grace). On `Ctrl-C` / EOF, any still-open buckets are\nflushed in time order. This makes\n`riplog -F app.log --count --group-by=svc --bucket=1m` a live histogram\npipe suitable for graph tooling, and lets stdin act as a one-shot pipe:\n`gen | riplog --count --group-by=svc --bucket=10s | dashboard`.\n\n`-f` / `-F` rejects two combinations: `--n-buckets` (the upper bound is\nunknown in follow mode) and `--group-by` without `--bucket` (no completion\nsignal — nothing would print until you `Ctrl-C`).\n\n## Follow\n\n- `-f, --follow`: like `tail -f`; stays on the same inode.\n- `-F, --follow-reopen`: like `tail -F`; reopens on rotation/truncation.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahrefs%2Friplog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fahrefs%2Friplog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahrefs%2Friplog/lists"}