Add chlog CLI with chcat/chtail/chgrep entrypoints #1
Reference in New Issue
Block a user
Delete Branch "benvin/initial"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
The ClickHouse log store (
logs.raw, ~281M rows/day, 3-day TTL, no text index) is currently only queryable by hand-writing HTTP queries — easy to get wrong and easy to accidentally fire an unbounded scan that runs into the server's 120s kill. This adds a purpose-built CLI so reading, following and searching logs is one command, with the cost guardrails baked in.What
chlogbinary withcat/tail/grepsubcommands;chcat/chtail/chgrepinstalled as symlinks and dispatched on argv[0], each with its own shell completions--since 1h) and fully parameterized via ClickHouse HTTP{name:Type}params — no user input ever interpolated into SQLchcatpages large ranges as bounded keyset queries with boundary-timestamp dedupechtailfollows with a 2s poll, overlap re-query and dedupe so late-arriving rows surface exactly oncechgrepdoes substring/-i/--regexmessage search plus--fields k=v, and refuses unfiltered searches wider than 6h without--force-n/--namespace,--host,--pod,--container,--app,--severity,--stream,--limit; outputtext(colored, no-TTY safe),json,logfmtCH_URL/CH_USER(defaultlogreader)/CH_PASSWORDpatch/minor/major, nfpm RPM (binary + symlinks + bash/zsh/fish completions), woodpeckerpre-commit/test/buildPR workflows and a tag release pipeline publishing to artifactapirpm-internalgo test -race, vet and gofmt cleanVERDICT: ISSUES
Reviewed diff + body only, at head
415bf0c. Shallow-clonedbenvin/initial, rango test -race ./...,go vet ./...,gofmt -l .— all clean locally. One blocking issue found (CI's own lint gate), everything else checks out.Blocking:
ci/woodpecker/pr/testis red (lint step)test.yaml'slintstep runsgolangci-lint run ./.... Reproduced locally with golangci-lint v2.13.1 (matching thegolangci-lint:latestimage tag the pipeline pulls) — defaulterrchecklinter flags 6 unchecked error returns:internal/chlog/client.go:45—io.WriteString(h, s)inRow.Key()internal/chlog/client.go:84—defer resp.Body.Close()internal/chlog/client_test.go:58—r.Body.Read(body)internal/chlog/client_test.go:83—enc.Encode(row)main.go:144—cmd.Flags().MarkHidden("until")main.go:145—cmd.Flags().MarkHidden("limit")None are functionally dangerous, but they're why the
testjob is failing (confirmed via commit-status:test= failure,build= success,pre-commit= pending). Please fix and let the pipeline go green.Everything else looks solid
SQL injection safety — confirmed clean.
Build()(internal/chlog/query.go) routes every user-controlled value (namespace/host/pod/container/app/stream/source/severity/pattern/--fieldskeys and values) through theParamsmap as{name:Type}placeholders — including field map keys, which is easy to get wrong and isn't here.Client.Run(client.go) sends the SQL as static body text and every value as a separateparam_*HTTP form value, never string-built.TestBuildNoUserInputInSQLasserts injection payloads (incl. aDROP TABLEattempt and a fake{p:String}placeholder) never land in the SQL string.Bounded-query discipline —
Build()rejects missing/inverted since/until;--sincedefaults to1h;chgrep's guard innewGrepCmdcorrectly blocks unfiltered windows >6h unless--forceorFilter.Selective()(namespace/host/app) — tested for block/allow-filtered/allow-short-window.Page()'s keyset pagination + boundary dedupe is well tested, including a same-millisecond-burst edge case (TestPageSingleMillisecondBurst) that proves forward progress even when a page is saturated with same-timestamp rows.Tail()'s overlap+dedupe with a pruned seen-set is tested against concurrent late-arriving rows (TestTailDedupesAcrossPolls).CLI conventions vs node-lookup — matches exactly: Makefile
patch/minor/majortag-bump targets, nfpm RPM bundling the binary + 3 symlinks + bash/zsh/fish completions for all 4 entrypoint names, no-TTY-safe color (stdoutIsTTY()+NO_COLORrespected), 4-file woodpecker split (pre-commit/test/buildonpull_request,releaseon tag) withbackend_options.kubernetes.resourcesset on every step, tag release publishes the RPM to artifactapirpm-internal. (serviceAccountName: defaultmatches node-lookup's own existing convention — not a regression introduced here.)Tests — meaningful, not rubber-stamped: query builder bounds/params/injection, time parsing (durations, RFC3339, invalid input), pager/tail dedupe against a mocked ClickHouse HTTP server, formatting (text/json/logfmt, color vs no-color), grep guard behavior. Good coverage of the exact edge cases called out in the PR description.
CI state:
ci/woodpecker/pr/buildsuccess,ci/woodpecker/pr/testfailure,ci/woodpecker/pr/pre-commitpending.VERDICT: CLEAN
Delta re-review of fix commit
4d78ed5(on top of415bf0c), scoped to its diff only againstinternal/chlog/client.go,internal/chlog/client_test.go,main.go.All 6 errcheck findings from the prior review (comment 10483) are resolved, and each fix is genuinely cosmetic/unfailable — verified individually, not rubber-stamped:
internal/chlog/client.go:45—io.WriteString(h, s)inRow.Key()→_, _ = io.WriteString(...).hisfnv.New64a();hash.Hash.Writeis documented to never return an error. Safe to discard.internal/chlog/client.go:84—defer resp.Body.Close()→defer func() { _ = resp.Body.Close() }(). Standard idiom for discarding a Close error on a response body already read to completion; matches the stated node-lookup convention.internal/chlog/client_test.go:58—r.Body.Read(body)→body, _ := io.ReadAll(r.Body). This is actually a correctness improvement, not just a lint fix: the old singleRead()call into a pre-sized buffer wasn't guaranteed to fill it. Ignoring theReadAllerror in a test fake is fine — a real failure here would surface as a body/SQL mismatch downstream in the same test.internal/chlog/client_test.go:83—enc.Encode(row)→_ = enc.Encode(row). Test fake writing JSON to the response; an encode failure would show up as a client-side decode/assertion failure. Cosmetic.main.go:144-145—cmd.Flags().MarkHidden("until")/MarkHidden("limit")→ blank-assigned. Both flag names are hardcoded literals just registered viacf.register(cmd, 0)a few lines above, soMarkHidden's only error case (flag not found) is unreachable here.No
//nolintdirectives added anywhere — all findings genuinely fixed rather than suppressed. Diff is scoped exactly to the 6 flagged lines (git diff 415bf0c 4d78ed5— 3 files, 7/-7 lines, nothing else touched).Commit-status on
4d78ed5:ci/woodpecker/pr/build,ci/woodpecker/pr/test,ci/woodpecker/pr/pre-commitallsuccess.No action needed from my side beyond this confirmation — merge remains Ben's call.