From ee2f270abc7e6ae4e7c2b7531ee491f8a24c6661 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 23 Aug 2026 21:39:17 +1000 Subject: [PATCH] Drop chcat/chtail/chgrep symlink entrypoints; ship chlog subcommands only The RPM's /usr/bin/chcat conflicts with SELinux's policycoreutils-python-utils on Fedora. Per Ben: remove the symlink entrypoints entirely and ship only the chlog binary with cat/tail/grep subcommands. - Remove chcat/chtail/chgrep symlinks and their completions from nfpm.yaml, build-rpm.sh and the Makefile - Remove the argv[0] dispatch in main.go; subcommands are unchanged - Update the completion test to cover chlog only - Update README usage to chlog cat|tail|grep --- .gitignore | 3 --- .woodpecker/release.yaml | 2 +- Makefile | 13 ++++------- README.md | 36 ++++++++++++++++------------- main.go | 35 +++++++--------------------- main_test.go | 49 +++++++++++++++++----------------------- packaging/nfpm.yaml | 47 -------------------------------------- scripts/build-rpm.sh | 20 +++++----------- 8 files changed, 60 insertions(+), 145 deletions(-) diff --git a/.gitignore b/.gitignore index 48c6e5b..49a1714 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ # built binaries (repo root only) /chlog -/chcat -/chtail -/chgrep # cross-compiled release artifacts (e.g. chlog-linux-amd64) /chlog-* dist/ diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml index 729b942..49bd1b3 100644 --- a/.woodpecker/release.yaml +++ b/.woodpecker/release.yaml @@ -44,7 +44,7 @@ steps: memory: 2Gi cpu: 2 - # Package the built binary + symlinks + shell completions into an RPM. + # Package the built binary + shell completions into an RPM. - name: package image: git.unkin.net/unkin/almalinux9-rpmbuilder:latest commands: diff --git a/Makefile b/Makefile index 50ff968..9c27eef 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,4 @@ BINARY := chlog -# chcat/chtail/chgrep are argv[0]-dispatched symlinks to the single chlog binary. -LINKS := chcat chtail chgrep DIST := dist VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) GOFLAGS := -ldflags="-s -w -X main.version=$(VERSION)" @@ -13,7 +11,6 @@ all: build build: CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build $(GOFLAGS) -o $(DIST)/$(BINARY) . - @for l in $(LINKS); do ln -sf $(BINARY) $(DIST)/$$l; done test: go test -v -race ./... @@ -30,14 +27,12 @@ clean: install: go install $(GOFLAGS) . -# Generate bash/zsh/fish completions for chlog and each symlink entrypoint. +# Generate bash/zsh/fish completions for chlog. completions: build @mkdir -p $(DIST)/completions - @for b in $(BINARY) $(LINKS); do \ - $(DIST)/$$b completion bash > $(DIST)/completions/$$b.bash; \ - $(DIST)/$$b completion zsh > $(DIST)/completions/_$$b; \ - $(DIST)/$$b completion fish > $(DIST)/completions/$$b.fish; \ - done + @$(DIST)/$(BINARY) completion bash > $(DIST)/completions/$(BINARY).bash + @$(DIST)/$(BINARY) completion zsh > $(DIST)/completions/_$(BINARY) + @$(DIST)/$(BINARY) completion fish > $(DIST)/completions/$(BINARY).fish rpm: build rpm-package diff --git a/README.md b/README.md index a4e163d..afad100 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,24 @@ # clickhouse-tools CLI tools for the ClickHouse log store (`logs.raw`): one binary, `chlog`, with -three entrypoints installed as symlinks: +three subcommands: -| Command | Also as | Does | -|----------|---------------|------| -| `chcat` | `chlog cat` | Print logs oldest-first over a bounded time range | -| `chtail` | `chlog tail` | Follow logs live (2s poll, overlap + dedupe so nothing is lost or repeated) | -| `chgrep` | `chlog grep` | Search log messages (substring, `-i`, `--regex`) | +| Command | Does | +|--------------|------| +| `chlog cat` | Print logs oldest-first over a bounded time range | +| `chlog tail` | Follow logs live (2s poll, overlap + dedupe so nothing is lost or repeated) | +| `chlog grep` | Search log messages (substring, `-i`, `--regex`) | + +(Earlier releases also shipped `chcat`/`chtail`/`chgrep` symlinks; they were +dropped because `/usr/bin/chcat` conflicts with SELinux's +`policycoreutils-python-utils` package.) ## Why time bounds everywhere `logs.raw` has no text index and holds ~281M rows/day (3-day TTL). An unbounded message scan takes ~1 minute and the server kills queries at 120s. Every query these tools issue is therefore time-bounded — the default range is the last -hour (`--since 1h`) — and `chgrep` refuses a search wider than 6h with no +hour (`--since 1h`) — and `chlog grep` refuses a search wider than 6h with no `--namespace`/`--host`/`--app` filter unless you pass `--force`. All user input travels as ClickHouse HTTP `{name:Type}` parameters; nothing is @@ -23,12 +27,12 @@ ever interpolated into SQL text. ## Usage ```sh -chcat -n logging --since 30m -chcat --host web01 --since 2h --until 1h --format logfmt -chtail -n media --app jellyfin -chgrep -n kube-system -i "connection refused" --since 4h -chgrep --app vector --regex 'timed? ?out' --since 1d -chgrep --fields req_id=42 -n api "payment" +chlog cat -n logging --since 30m +chlog cat --host web01 --since 2h --until 1h --format logfmt +chlog tail -n media --app jellyfin +chlog grep -n kube-system -i "connection refused" --since 4h +chlog grep --app vector --regex 'timed? ?out' --since 1d +chlog grep --fields req_id=42 -n api "payment" ``` ### Common flags @@ -41,7 +45,7 @@ chgrep --fields req_id=42 -n api "payment" - `--format text|json|logfmt` — text is `ts ns/pod msg` (host for vm rows), colored only on a TTY (`NO_COLOR` respected) -### chgrep extras +### chlog grep extras - pattern is a substring by default; `-i` case-insensitive; `--regex` RE2 (`match()`) - `--fields key=value` (repeatable) filters the structured `fields` map @@ -58,8 +62,8 @@ chgrep --fields req_id=42 -n api "payment" ## Build and release ```sh -make build # dist/chlog + symlinks +make build # dist/chlog make test # go test -race ./... -make rpm # nfpm RPM with binary, symlinks, bash/zsh/fish completions +make rpm # nfpm RPM with binary + bash/zsh/fish completions make patch # tag + push next vX.Y.Z → CI releases RPM to artifactapi rpm-internal ``` diff --git a/main.go b/main.go index 821b98c..abde0c8 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "os/signal" - "path/filepath" "strings" "syscall" "time" @@ -88,10 +87,10 @@ func (cf *commonFlags) formatter() (chlog.Formatter, error) { return chlog.NewFormatter(cf.format, color) } -func newCatCmd(use string) *cobra.Command { +func newCatCmd() *cobra.Command { cf := &commonFlags{} cmd := &cobra.Command{ - Use: use, + Use: "cat", Short: "Print logs from the ClickHouse log store, oldest first", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -114,10 +113,10 @@ func newCatCmd(use string) *cobra.Command { return cmd } -func newTailCmd(use string) *cobra.Command { +func newTailCmd() *cobra.Command { cf := &commonFlags{} cmd := &cobra.Command{ - Use: use, + Use: "tail", Short: "Follow logs from the ClickHouse log store", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -146,7 +145,7 @@ func newTailCmd(use string) *cobra.Command { return cmd } -func newGrepCmd(use string) *cobra.Command { +func newGrepCmd() *cobra.Command { cf := &commonFlags{} var ( regex bool @@ -155,7 +154,7 @@ func newGrepCmd(use string) *cobra.Command { force bool ) cmd := &cobra.Command{ - Use: use + " ", + Use: "grep ", Short: "Search log messages in the ClickHouse log store", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -217,32 +216,14 @@ func newRootCmd() *cobra.Command { SilenceUsage: true, SilenceErrors: true, } - root.AddCommand(newCatCmd("cat"), newTailCmd("tail"), newGrepCmd("grep")) + root.AddCommand(newCatCmd(), newTailCmd(), newGrepCmd()) return root } -func entrypoint() *cobra.Command { - var cmd *cobra.Command - switch filepath.Base(os.Args[0]) { - case "chcat": - cmd = newCatCmd("chcat") - case "chtail": - cmd = newTailCmd("chtail") - case "chgrep": - cmd = newGrepCmd("chgrep") - default: - return newRootCmd() - } - cmd.Version = version - cmd.SilenceUsage = true - cmd.SilenceErrors = true - return cmd -} - func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - if err := entrypoint().ExecuteContext(ctx); err != nil { + if err := newRootCmd().ExecuteContext(ctx); err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } diff --git a/main_test.go b/main_test.go index 6895ce1..305aeee 100644 --- a/main_test.go +++ b/main_test.go @@ -1,7 +1,6 @@ package main import ( - "os" "strings" "testing" "time" @@ -41,7 +40,7 @@ func TestCommonFlagsFilterDefaults(t *testing.T) { } func TestGrepGuardBlocksWideUnfilteredSearch(t *testing.T) { - cmd := newGrepCmd("chgrep") + cmd := newGrepCmd() cmd.SetArgs([]string{"--since", "24h", "needle"}) var out strings.Builder cmd.SetOut(&out) @@ -56,7 +55,7 @@ func TestGrepGuardAllowsFilteredSearch(t *testing.T) { // A namespace filter disables the guard; the query then fails on the // unreachable server rather than the guard, proving the guard passed. t.Setenv("CH_URL", "http://127.0.0.1:1") - cmd := newGrepCmd("chgrep") + cmd := newGrepCmd() cmd.SetArgs([]string{"--since", "24h", "--namespace", "logging", "needle"}) var out strings.Builder cmd.SetOut(&out) @@ -72,7 +71,7 @@ func TestGrepGuardAllowsFilteredSearch(t *testing.T) { func TestGrepGuardAllowsShortWindow(t *testing.T) { t.Setenv("CH_URL", "http://127.0.0.1:1") - cmd := newGrepCmd("chgrep") + cmd := newGrepCmd() cmd.SetArgs([]string{"--since", "1h", "needle"}) err := cmd.Execute() if err == nil { @@ -83,33 +82,27 @@ func TestGrepGuardAllowsShortWindow(t *testing.T) { } } -func TestCompletionAllEntrypointsAndShells(t *testing.T) { - // scripts/build-rpm.sh runs " completion " for every - // entrypoint; guard that each argv[0]-dispatched command tree exposes a - // working completion subcommand for all three packaged shells. - for _, name := range []string{"chlog", "chcat", "chtail", "chgrep"} { - for _, shell := range []string{"bash", "zsh", "fish"} { - t.Run(name+"/"+shell, func(t *testing.T) { - orig := os.Args - os.Args = []string{name} - defer func() { os.Args = orig }() - cmd := entrypoint() - var out strings.Builder - cmd.SetOut(&out) - cmd.SetErr(&out) - cmd.SetArgs([]string{"completion", shell}) - if err := cmd.Execute(); err != nil { - t.Fatalf("%s completion %s: %v", name, shell, err) - } - if !strings.Contains(out.String(), name) { - t.Fatalf("%s completion %s output does not mention %q", name, shell, name) - } - }) - } +func TestCompletionAllShells(t *testing.T) { + // scripts/build-rpm.sh runs "chlog completion "; guard that a + // working completion subcommand exists for all three packaged shells. + for _, shell := range []string{"bash", "zsh", "fish"} { + t.Run(shell, func(t *testing.T) { + cmd := newRootCmd() + var out strings.Builder + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"completion", shell}) + if err := cmd.Execute(); err != nil { + t.Fatalf("chlog completion %s: %v", shell, err) + } + if !strings.Contains(out.String(), "chlog") { + t.Fatalf("chlog completion %s output does not mention chlog", shell) + } + }) } } -func TestEntrypointDispatch(t *testing.T) { +func TestRootSubcommands(t *testing.T) { root := newRootCmd() names := map[string]bool{} for _, c := range root.Commands() { diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml index ab44a09..7729a47 100644 --- a/packaging/nfpm.yaml +++ b/packaging/nfpm.yaml @@ -24,17 +24,6 @@ contents: owner: root group: root - # chcat/chtail/chgrep dispatch on argv[0] inside the chlog binary. - - src: /usr/bin/chlog - dst: /usr/bin/chcat - type: symlink - - src: /usr/bin/chlog - dst: /usr/bin/chtail - type: symlink - - src: /usr/bin/chlog - dst: /usr/bin/chgrep - type: symlink - # Shell completions (generated by scripts/build-rpm.sh before packaging). - src: dist/completions/chlog.bash dst: /usr/share/bash-completion/completions/chlog @@ -48,39 +37,3 @@ contents: dst: /usr/share/fish/vendor_completions.d/chlog.fish file_info: mode: 0644 - - src: dist/completions/chcat.bash - dst: /usr/share/bash-completion/completions/chcat - file_info: - mode: 0644 - - src: dist/completions/_chcat - dst: /usr/share/zsh/site-functions/_chcat - file_info: - mode: 0644 - - src: dist/completions/chcat.fish - dst: /usr/share/fish/vendor_completions.d/chcat.fish - file_info: - mode: 0644 - - src: dist/completions/chtail.bash - dst: /usr/share/bash-completion/completions/chtail - file_info: - mode: 0644 - - src: dist/completions/_chtail - dst: /usr/share/zsh/site-functions/_chtail - file_info: - mode: 0644 - - src: dist/completions/chtail.fish - dst: /usr/share/fish/vendor_completions.d/chtail.fish - file_info: - mode: 0644 - - src: dist/completions/chgrep.bash - dst: /usr/share/bash-completion/completions/chgrep - file_info: - mode: 0644 - - src: dist/completions/_chgrep - dst: /usr/share/zsh/site-functions/_chgrep - file_info: - mode: 0644 - - src: dist/completions/chgrep.fish - dst: /usr/share/fish/vendor_completions.d/chgrep.fish - file_info: - mode: 0644 diff --git a/scripts/build-rpm.sh b/scripts/build-rpm.sh index 2db597c..4e9d258 100755 --- a/scripts/build-rpm.sh +++ b/scripts/build-rpm.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # Package the (already built) chlog binary into an RPM with nfpm, bundling -# chcat/chtail/chgrep symlinks and generated bash/zsh/fish shell completions. +# generated bash/zsh/fish shell completions. # Usage: scripts/build-rpm.sh [version] (version defaults to $CI_COMMIT_TAG) # set -euo pipefail @@ -12,7 +12,6 @@ cd "${ROOT_DIR}" VERSION="${1:-${CI_COMMIT_TAG:-0.0.0-dev}}" VERSION="${VERSION#v}" # strip a leading v BINARY="chlog" -NAMES=(chlog chcat chtail chgrep) DIST="dist" if [ ! -f "${DIST}/${BINARY}" ]; then @@ -20,26 +19,19 @@ if [ ! -f "${DIST}/${BINARY}" ]; then exit 1 fi -for l in chcat chtail chgrep; do - ln -sf "${BINARY}" "${DIST}/${l}" -done - -# Generate shell completions per entrypoint name so they always match the -# shipped flags/subcommands (each name dispatches to its own command tree). +# Generate shell completions so they always match the shipped flags/subcommands. COMP_DIR="${DIST}/completions" mkdir -p "${COMP_DIR}" -for b in "${NAMES[@]}"; do - "./${DIST}/${b}" completion bash >"${COMP_DIR}/${b}.bash" - "./${DIST}/${b}" completion zsh >"${COMP_DIR}/_${b}" - "./${DIST}/${b}" completion fish >"${COMP_DIR}/${b}.fish" -done +"./${DIST}/${BINARY}" completion bash >"${COMP_DIR}/${BINARY}.bash" +"./${DIST}/${BINARY}" completion zsh >"${COMP_DIR}/_${BINARY}" +"./${DIST}/${BINARY}" completion fish >"${COMP_DIR}/${BINARY}.fish" export PACKAGE_NAME="clickhouse-tools" export PACKAGE_VERSION="${VERSION}" export PACKAGE_RELEASE="1" export PACKAGE_ARCH="amd64" export PACKAGE_PLATFORM="linux" -export PACKAGE_DESCRIPTION="CLI tools for the ClickHouse log store: chlog with chcat (print), chtail (follow) and chgrep (search) entrypoints" +export PACKAGE_DESCRIPTION="CLI tools for the ClickHouse log store: chlog with cat (print), tail (follow) and grep (search) subcommands" export PACKAGE_MAINTAINER="Ben Vincent " export PACKAGE_HOMEPAGE="https://git.unkin.net/unkin/clickhouse-tools" export PACKAGE_LICENSE="MIT"