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
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
# built binaries (repo root only)
|
||||
/chlog
|
||||
/chcat
|
||||
/chtail
|
||||
/chgrep
|
||||
# cross-compiled release artifacts (e.g. chlog-linux-amd64)
|
||||
/chlog-*
|
||||
dist/
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 + " <pattern>",
|
||||
Use: "grep <pattern>",
|
||||
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)
|
||||
}
|
||||
|
||||
+21
-28
@@ -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 "<name> completion <shell>" 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 <shell>"; 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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-14
@@ -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 <ben@unkin.net>"
|
||||
export PACKAGE_HOMEPAGE="https://git.unkin.net/unkin/clickhouse-tools"
|
||||
export PACKAGE_LICENSE="MIT"
|
||||
|
||||
Reference in New Issue
Block a user