☸ Collect cluster diagnostics into one archive
Repo: github.com/hrodrig/groot · Releases: GitHub Releases
Badges: release = latest GitHub Release (published notes/assets). version = latest git tag on the repo. They can differ if a tag was pushed without publishing a Release yet—install artifacts are still on the tags / release page for that tag. The VERSION file in the default branch is the source of truth for the next release number.
GROOT is a Go CLI (Cobra + Viper) that collects broad Kubernetes diagnostics, including worker/node details, control plane logs, namespace resources, pod logs, and events.
- Features
- Requirements
- Install or update
- Quick start
- First run
- Usage examples
- Config
- Resolution and precedence
- Output naming
- Console output modes
- Typical collected data
- Notifications
- Rootless container
- Security note
- Get involved
- Cobra CLI with
collectcommand - Viper YAML config + environment variable override
- Concurrent
kubectlexecution for faster collection - Worker/node and control plane oriented log gathering
- Output folder +
.tar.gzarchive generation - Optional notifications (Slack, Discord, Teams, PagerDuty, Telegram, generic webhooks)
- Rootless container image support
kubectlconfigured against the target cluster- RBAC permissions to read logs/resources
- Go 1.26+ only if you build from source (
make build)
Pre-built .deb, .rpm, .tar.gz (and .zip on Windows) are on GitHub Releases and latest release. The release badge at the top of this README shows the current tag at a glance.
Why not a single latest URL for every file? GitHub’s …/releases/latest/download/<file> only works if the asset filename is identical on every release. GoReleaser puts the semver without v in filenames (for example groot_0.1.8_amd64.deb), while the download URL path uses the git tag with v (…/download/v0.1.8/…). Do not use groot_${TAG}_… with TAG=v0.1.8 in the filename—that causes 404. Options: pick names from the release page, use the snippet below, or use the badge.
# Latest published release tag (python3 or jq). Asset basename has NO "v" — see VER below.
TAG="$(curl -fsSL https://api.github.com/repos/hrodrig/groot/releases/latest | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')"
# Alternative: TAG="$(curl -fsSL https://api.github.com/repos/hrodrig/groot/releases/latest | jq -r .tag_name)"
[ -n "$TAG" ] || { echo "Could not resolve tag (empty). Install python3 or jq, or set TAG manually from the Releases page." >&2; exit 1; }
VER="${TAG#v}" # e.g. v0.1.8 -> 0.1.8 (matches GoReleaser .deb filename)
DEB="groot_${VER}_amd64.deb"
URL="https://github.com/hrodrig/groot/releases/download/${TAG}/${DEB}"
TMP="/tmp/${DEB}"
# Download to /tmp so user _apt can read the file (apt often cannot read ~/.deb when $HOME is mode 700).
if ! curl -fsSL "$URL" -o "$TMP"; then
echo "Download failed (curl exit $?). Check URL: $URL" >&2
exit 1
fi
if [ ! -f "$TMP" ]; then
echo "Expected $TMP after download — not found." >&2
exit 1
fi
sudo apt install "$TMP"Paste the block as a whole, or chain with &&, so apt does not run after a failed curl. curl -f exits non‑zero on HTTP errors (404, etc.).
apt + _apt / “Permission denied” under $HOME: if you curl the .deb into ~ and run sudo apt install ./groot_….deb, Debian/Ubuntu may warn that _apt cannot read the file (home directory not world-executable). Use /tmp as above, or sudo cp "$DEB" /tmp/ then sudo apt install "/tmp/$DEB".
404 on groot_v0.1.6_amd64.deb: the file on GitHub is groot_0.1.6_amd64.deb (no v in the basename). Empty TAG: if jq/python3 failed, you get .../download//groot__amd64.deb and ./groot__amd64.deb from apt.
groot is installed to /usr/bin. The package drops a sample at /etc/groot/groot.yml.sample (from configs/groot.yml.sample in the repo). With no --config, discovery is ./groot.yml, then ~/.groot/groot.yml, then /etc/groot/groot.yml, then /etc/groot/groot.yml.sample. Use a per-user copy under ~/.groot/, sudo cp /etc/groot/groot.yml.sample /etc/groot/groot.yml for a machine-wide config, or --config /path/to/file.yaml. Use arm64 in the download filename on ARM64.
| Format | Example (tag v0.1.8 in the URL path; artifact basename uses 0.1.8 without v) |
|---|---|
.deb |
curl -fsSL -o /tmp/groot_0.1.8_amd64.deb https://github.com/hrodrig/groot/releases/download/v0.1.8/groot_0.1.8_amd64.deb then sudo apt install /tmp/groot_0.1.8_amd64.deb (use /tmp so _apt can read the file if $HOME is 700) |
.rpm |
curl -fsSLO https://github.com/hrodrig/groot/releases/download/v0.1.8/groot_0.1.8_amd64.rpm then sudo rpm -Uvh groot_0.1.8_amd64.rpm or sudo dnf install ./groot_0.1.8_amd64.rpm |
.tar.gz |
curl -fsSLO https://github.com/hrodrig/groot/releases/download/v0.1.8/groot_0.1.8_linux_amd64.tar.gz then tar xzf groot_0.1.8_linux_amd64.tar.gz and run ./groot inside the extracted directory |
Update: download a newer release and run the same install command again (rpm -Uvh, apt install over the .deb, or replace the tarball tree).
Windows: use the .zip asset for your arch, unpack, run groot.exe where kubectl is available.
Then configure and run groot collect (or groot --print-sample-config > groot.yml first).
Build from a clone of this repository:
make build
./bin/groot --print-sample-config > groot.yml
# Edit groot.yml: replace sample values with your cluster settings (namespaces, targets,
# kubeconfig, output paths, optional notify webhooks/tokens) before collecting.
./bin/groot collectIf you installed from a release package, use groot on your PATH instead of ./bin/groot.
Useful runtime flags (global or with collect):
--versionprints version, commit, branch, and build date--test-connectionvalidates Kubernetes connectivity and exits--verboseshows each executed command asCMD, plusOK/ERRresults--quietsuppresses normal console output (INFO/WARN/CMD/OK) and only prints errors; notify integrations still run (Slack, Discord, Teams, PagerDuty, Telegram, generic) unless you disable them in config or use--no-notify--no-notifyskips all notifications after a successful collect (useful for cron when you only want the archive). Same effect as envGROOT_NO_NOTIFY=1(ortrue/yes, case-insensitive)--no-colordisables ANSI colors--message "label text"appends a sanitized suffix to archive and capture-related output names--kubeconfig /path/to/configoverrides kubeconfig from file/env
If you do not have a config file yet, print a sample and save it:
./bin/groot --print-sample-config > groot.ymlThe generated file is a template only. Open groot.yml and set your own values for your environment—for example kubeconfig (if not using the default), collection.namespaces, workloads under collection.targets (deployments, StatefulSets, DaemonSets, Helm releases), output_dir / file_prefix, and any notify.* URLs or secrets. Until you do, the sample names and disabled notification blocks will not match a real cluster.
Then run:
./bin/groot collectDefault config discovery order (when --config is not provided). The first existing file wins; if none exist, built-in defaults apply, then GROOT_* environment variables override where applicable:
./groot.yml~/.groot/groot.yml/etc/groot/groot.yml/etc/groot/groot.yml.sample(sample from the.deb/.rpmpackage)
You can always override file discovery with --config (see Usage examples).
Paths below use ./bin/groot after make build; if you installed from Releases or make install, use groot on your PATH the same way (for example groot collect ...).
Paths under ./, ~/.groot/, and /etc/groot/ (groot.yml, groot.yml.sample) are discovered automatically (see First run). Any other path must be passed explicitly:
./bin/groot collect --config /path/to/my-groot.yml
./bin/groot collect --config ./groot-mi-test.ymlFrom the repository root, after editing your copy:
./bin/groot collect --config groot-mi-test.yml./bin/groot --config ./groot-mi-test.yml --test-connection
./bin/groot collect --config ./groot-mi-test.yml --test-connectionConsole only (Slack/Discord/etc. still run if enabled in YAML):
./bin/groot collect --config /path/to/groot.yml --quietSkip all notify channels for this run (archive still created); same as env GROOT_NO_NOTIFY=1 / true / yes:
./bin/groot collect --config /path/to/groot.yml --quiet --no-notify
0 * * * * GROOT_NO_NOTIFY=1 /usr/local/bin/groot collect --config /home/you/.groot/prod.yml --quiet./bin/groot collect --config groot.yml --message "staging-network-audit-2026-04-28"./bin/groot collect --config groot.yml --kubeconfig /path/to/other-kubeconfigEdit groot.yml (or any file passed with --config) and align every section with your cluster and operational needs. Do not rely on the shipped sample as a drop-in configuration.
Sample config (same as groot --print-sample-config and configs/groot.yml.sample in the repo):
kubeconfig: ""
output_dir: "./out"
file_prefix: "groot-capture"
collection:
timeout: 20m
worker_concurrency: 6
namespaces:
- kube-system
- default
targets:
default:
deployments:
- api
statefulsets:
- redis
daemonsets:
- node-agent
helm_releases:
- my-release
include_pod_logs: true
include_previous_logs: true
pod_log_tail_lines: 1500
include_node_details: true
extra_kubectl:
- "get componentstatuses"
- "get csr"
notify:
slack:
enabled: false
# One URL, or several separated by ';' (e.g. team A; team B webhooks)
webhook_url: ""
discord:
enabled: false
# Discord server Settings → Integrations → Webhooks (same ';' for multiple URLs)
webhook_url: ""
teams:
enabled: false
# Same ';' convention as Slack for multiple Teams incoming webhooks
webhook_url: ""
pagerduty:
enabled: false
# Events API v2 integration key(s); multiple keys separated by ';'
routing_key: ""
severity: "warning"
source: "groot"
telegram:
enabled: false
token: ""
# One chat id, or several (group/user) ids separated by ';' with the same bot
chat_id: ""
generic:
enabled: false
# POST JSON with one root string field only: {"<json_key>":"<summary>"} (see README → Notifications).
webhook_url: ""
json_key: "text"
headers: {}Environment variables use the GROOT_ prefix (Viper). Nested YAML keys map to env names by replacing . with _ (for example collection.timeout → GROOT_COLLECTION_TIMEOUT). kubeconfig in YAML still loses to the process KUBECONFIG env when that is set (see Resolution and precedence).
Common examples:
GROOT_OUTPUT_DIR,GROOT_FILE_PREFIXGROOT_COLLECTION_TIMEOUT,GROOT_COLLECTION_WORKER_CONCURRENCY,GROOT_COLLECTION_INCLUDE_POD_LOGS(boolean),GROOT_COLLECTION_POD_LOG_TAIL_LINES, …- Notify secrets (also read when
enabled: trueand the YAML field is empty):GROOT_NOTIFY_SLACK_WEBHOOK_URL,GROOT_NOTIFY_DISCORD_WEBHOOK_URL,GROOT_NOTIFY_TEAMS_WEBHOOK_URL,GROOT_NOTIFY_TELEGRAM_TOKEN,GROOT_NOTIFY_TELEGRAM_CHAT_ID,GROOT_NOTIFY_GENERIC_WEBHOOK_URL,GROOT_NOTIFY_PAGERDUTY_ROUTING_KEY GROOT_NO_NOTIFY=1(ortrue/yes): same as--no-notifyfor a run
collection.extra_kubectl: Each string is split on whitespace and passed as additional kubectl arguments (no shell). At load time, Groot only accepts read-oriented subcommands: get, describe, explain, top, logs, api-resources, api-versions, version, cluster-info, wait, plus config view … and auth can-i …. Anything else fails collect immediately with a configuration error so a typo or copy-paste cannot turn extras into destructive verbs (delete, exec, apply, etc.).
When a notification channel is enabled and required credentials are missing, groot fails fast with a clear configuration error.
Configuration file precedence:
--configexplicit path./groot.yml~/.groot/groot.yml/etc/groot/groot.yml/etc/groot/groot.yml.sample- defaults
kubeconfig precedence:
--kubeconfig /path/to/configKUBECONFIGkubeconfigvalue in YAML- if all empty,
kubectldefault behavior
Workload filter behavior (collection.targets):
- per namespace, you can define
deployments,statefulsets,daemonsets, andhelm_releases - if a namespace has targets, pod logs for that namespace are limited to those workloads
- if a namespace has no targets, pod logs keep the default broad behavior
helm_releasesmatchesapp.kubernetes.io/instance
pod_log_tail_lines behavior:
0: collect full logs (no--tail; use when you need the entire log stream)>0: collect only the last N lines per pod- applies to both current and
--previouspod logs
include_previous_logs behavior:
true: also collectskubectl logs --previousper pod into*.previous.logfalse: collects only current pod logs
output_dir path expansion:
- supports
~(home directory), for example~/tmp/groot-out - supports environment variables, for example
${HOME}/tmp/groot-out
Capture output names are:
- directory:
<timestamp> - archive:
<timestamp>-<cluster>[-<message>].tar.gz
--message is sanitized before use:
- lowercase
- trims leading/trailing spaces
- removes accents/diacritics
- converts spaces and
_to- - removes unsupported filesystem characters
- collapses repeated dashes
Example:
- input:
--message "network routing issue" - suffix:
network-routing-issue - output:
20260428-123200-my-cluster-network-routing-issue.tar.gz
Directory layout:
nodes/extras/- one directory per configured namespace (for example
kube-system/,default/) - pod log files:
<pod>__<node>.log(and.previous.logwhen enabled), same pattern for control-plane pods underkube-system/ - after archive creation, the timestamp directory is automatically removed
Inside the .tar.gz, every path is prefixed with the capture folder name (<timestamp>/…, for example 20260502-174207/kube-system/…). Extracting into a shared directory (for example ~/tmp/groot-out) keeps each run under its own subdirectory instead of mixing kube-system/, cloudbridge/, etc. at the extraction root. Archives produced by older Groot versions may still have a flat layout at the tar root.
- default: summary
INFOlines --verbose: adds per-commandCMD/OK/ERR--quiet: suppresses normal console output, prints only errors; does not disable webhooks/API notifications--no-notify: skips every notify channel for this run (config can still haveenabled: true; use from cron when you want silence to external systems). Env equivalent:GROOT_NO_NOTIFY=1--no-color: disables ANSI colors
kubectl cluster-infokubectl get nodes -o widekubectl get pods -A -o widekubectl get events -Akubectl describe node <node>kubectl top node <node>kubectl logs -n <ns> <pod> --all-containers→ files named<pod>__<node>.logunder each namespace directory (pending/unscheduled pods useunknown-node)- Control plane pod logs in
kube-system(tier=control-plane, when available) use the same<pod>__<node>.logpattern extras/kubeconfig.txtderived from kubeconfig (context,cluster,user,server)
Enable each channel in config:
- Slack Incoming Webhook (
notify.slack.webhook_urlorGROOT_NOTIFY_SLACK_WEBHOOK_URL). For multiple channels, put several full webhook URLs on the same value separated by;(spaces optional); Groot notifies each URL in order and reports combined errors if any request fails. - Discord Incoming Webhook (
notify.discord.webhook_urlorGROOT_NOTIFY_DISCORD_WEBHOOK_URL): same;-separated URL list. Payload is{"content":"<summary>"}per Discord webhook API. Messages longer than 2000 characters are truncated with...so the request stays valid. - Teams Incoming Webhook — same
;-separated list fornotify.teams.webhook_url/GROOT_NOTIFY_TEAMS_WEBHOOK_URL. - PagerDuty Events API v2 (
notify.pagerdutyorGROOT_NOTIFY_PAGERDUTY_ROUTING_KEY):routing_keyis the Events v2 integration key (several keys separated by;each gets its owntriggerevent).severitymust becritical,error,warning, orinfo(defaultwarning).sourcedefaults togroot. The eventpayload.summaryis the same line as other channels;payload.custom_detailsincludestotal,success,failed,duration,output_dir, andarchive_path. Successful delivery expects HTTP 202 from PagerDuty. - Telegram Bot API (
notify.telegram.token+chat_id, orGROOT_NOTIFY_TELEGRAM_TOKEN/GROOT_NOTIFY_TELEGRAM_CHAT_ID). One bot token; multiple destinations use several chat ids in onechat_idstring separated by;(same message to each chat). - Generic HTTP webhook (
notify.genericorGROOT_NOTIFY_GENERIC_WEBHOOK_URL):POSTwithContent-Type: application/jsonand body{"<json_key>":"<summary text>"}. Defaultjson_keyistext. For Discord, usenotify.discordinstead (correctcontentfield and length limit). Optionalheaders(YAML map) are sent on every request. Multiple endpoints: separate full URLs with;inwebhook_url.
Generic webhook — scope (read this before relying on it):
- What it sends: exactly one JSON object at the root, with one string field whose name you set via
json_key. The value is always Groot’s single-line collection summary (same text as other channels). Example:{"text":"GROOT finished. total=…"}. - What it does not do: no arbitrary body templates (you cannot place the summary in several fields, wrap it in nested objects, or mix fixed keys beyond that single pair). No non-JSON bodies (no raw text,
application/x-www-form-urlencoded, XML). If an integration needs extra fields, signing (HMAC), or a custom layout, use a small proxy service or extend Groot later.
Implemented channels: Slack, Discord, Teams, PagerDuty (Events v2), Telegram, and generic JSON webhooks as above. There is no built-in email, etc.
make docker-build
make docker-buildx
make scan
docker run --rm \
-v "$HOME/.kube:/home/nonroot/.kube:ro" \
-v "$(pwd)/out:/app/out" \
groot:localFor strict rootless runtime, use Podman:
podman build -t groot:local .
podman run --rm \
-v "$HOME/.kube:/home/nonroot/.kube:ro" \
-v "$(pwd)/out:/app/out" \
groot:localCollected logs may contain sensitive data. Handle archives according to your security policy.
Found Groot useful? We'd love your help to make it better. You can:
- Report bugs or suggest features — open an issue
- Contribute code — see CONTRIBUTING.md for how to submit a pull request
- Star the repo — it helps others discover Groot
Thanks for using Groot.
