# clinic-cli

`clinic` is a small Go CLI for TiDB Cloud Clinic APIs. It stores base URL + API key per environment (`prod`, `staging`, `dev`), and uses env-first runtime selection with optional env-scoped profiles for command execution.

## Quickstart (users)

Requirements:
- Go 1.23+

Install with `go install`:

```bash
go install github.com/tidbcloud/clinic-cli/cmd/clinic@latest
clinic version
```

If `clinic` is not found, add the Go bin directory to your `PATH`:

```bash
export PATH="$(go env GOPATH)/bin:$PATH"
```

Authenticate and run a first request:

```bash
clinic auth set-key --env prod
clinic auth show
clinic cluster search --query tidb --limit 5
```

## Agent and contributor workflow

Start every non-trivial change with the repository maps:

- [`AGENTS.md`](AGENTS.md) for the short execution entrypoint
- [`docs/INDEX.md`](docs/INDEX.md) for the full docs table of contents

For implementation work:
- If the change needs a plan, follow [`PLANS.md`](PLANS.md) and save the plan in `docs/`.
- Run the narrowest package tests first, then `make verify`.
- Update the smallest relevant docs when behavior changes.

Definition of done for this repo:
- tests for changed behavior are green,
- agent-facing docs are updated when needed,
- `make verify` passes locally.

## Table of contents

- [Quickstart (users)](#quickstart-users)
- [Agent and contributor workflow](#agent-and-contributor-workflow)
- [Build from source](#build-from-source)
- [Shell completion](#shell-completion)
- [Auth and environment workflow](#auth-and-environment-workflow)
- [Runtime selection and profiles](#runtime-selection-and-profiles)
- [Env-scoped profile workflow](#env-scoped-profile-workflow)
- [API command examples](#api-command-examples)
- [Notes](#notes)

## Build from source

Install from a local checkout:

```bash
go install ./cmd/clinic
```

Build and verify from the repo root:

```bash
make verify
make build
./bin/clinic --help
./bin/clinic version
```

Most command examples below use `./bin/clinic` (the repo-built binary). If you installed globally, replace it with `clinic`.

Config is stored at:
- `$XDG_CONFIG_HOME/clinic/config.json`, or
- `~/.config/clinic/config.json` when `XDG_CONFIG_HOME` is unset

On first run, `clinic` creates config entries for:
- `prod` -> `https://clinic.pingcap.com`
- `staging` -> `https://staging-clinic.pingcap.com`
- `dev` -> `https://dev-clinic.pingcap.com`

## Shell completion

Generate completion scripts:

```bash
./bin/clinic completion bash
./bin/clinic completion zsh
./bin/clinic completion fish
./bin/clinic completion powershell
```

Bash (current shell):

```bash
source <(./bin/clinic completion bash)
```

Zsh (current shell):

```zsh
source <(./bin/clinic completion zsh)
```

Zsh (persistent install):

```zsh
mkdir -p ~/.zsh/completions
./bin/clinic completion zsh > ~/.zsh/completions/_clinic
```

Add this to `~/.zshrc` (before Oh My Zsh init if you use Oh My Zsh):

```zsh
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit
compinit
```

Then reload:

```zsh
source ~/.zshrc
```

Completion includes:
- top-level command families and their subcommands
- global `--env` values
- global `--profile` values filtered by selected env
- `env use <env>` and `switch <env> <profile>` argument values

## Auth and environment workflow

Set an API key for one environment:

```bash
./bin/clinic auth set-key --env prod
```

`auth set-key` prompts for the API key interactively with terminal echo disabled.

Update the base URL for one environment:

```bash
./bin/clinic auth set-url --env staging --url https://staging-clinic.pingcap.com
```

Switch the active environment:

```bash
./bin/clinic env use staging
```

Switch active environment and profile in one step:

```bash
./bin/clinic switch staging blue
```

`./bin/clinic switch <env>` is equivalent to `./bin/clinic env use <env>`.

Show the current active environment:

```bash
./bin/clinic env current
```

List all configured environments:

```bash
./bin/clinic env list
```

Show the selected environment's URL and masked key state:

```bash
./bin/clinic auth show
```

Show auth details for a specific environment without switching:

```bash
./bin/clinic auth show --env dev
```

Use a one-off profile override in a selected environment:

```bash
./bin/clinic --env staging --profile blue auth show
```

## Runtime selection and profiles

Select a runtime environment for a command:

```bash
./bin/clinic --env staging cluster search --query tidb
```

Optionally select a profile in that environment:

```bash
./bin/clinic --env staging --profile blue metrics query --cluster-id 123456789 --query 'up'
```

If no profile is active in the selected environment, the command still runs. Profile defaults are skipped in that case, so explicit flags and `CLINIC_*` environment variables remain available.

For runtime/default debugging, add global `--verbose` (or `-v`) before the command family to print resolution and request traces to stderr:

```bash
./bin/clinic --env staging --profile blue --verbose metrics range --query 'up' --end 1712123600
```

## Env-scoped profile workflow

Profiles are isolated per environment. Each profile binds to one cluster and stores:
- required binding metadata: `cluster_id`, `cluster_name`, `org_id`, `parent_id`, `cluster_deploy_type_v2`
- optional range defaults: `range_start`, `range_end`
- response-only derived field in `profile list/current/use/set` output: `clinic_url` (`<env-url>/portal/#/orgs/<org_id>/clusters/<cluster_id>`)

List and inspect profiles:

```bash
./bin/clinic --env staging profile list
./bin/clinic --env staging profile current
```

Open a profile Clinic URL in your default browser:

```bash
./bin/clinic --env staging profile open
./bin/clinic --env staging profile open blue
```

Switch the active profile:

```bash
./bin/clinic --env staging profile use blue
```

Create or update a profile:

```bash
./bin/clinic --env staging profile set blue --cluster-id 123456789 --start 1712123000 --end 1712123600
```

Update the active profile without repeating its name:

```bash
./bin/clinic --env staging profile set current --start 1712123000 --end 1712123600
```

Delete a profile:

```bash
./bin/clinic --env staging profile delete blue
```

The `profile cluster` command subtree was removed. Cluster metadata now lives directly inside profile records.

## API command examples

Most commands print API responses as formatted JSON (via `gojq`, 2-space indentation).
`metrics range` defaults to an interactive ECharts HTML report and attempts to open it in a browser.
When writing to a terminal, JSON output is syntax-colored using gojq's color scheme.
When output is piped or redirected, color is disabled automatically.
`loki query` prints plain log lines by default and supports `-o json` (or `--output json`) for formatted JSON response output.
`loki query` uses `--limit 10` by default and enforces `--limit <= 1000` to match server limits.

### Runtime default precedence

For `cluster-id` and range-style arguments (`start/end`), runtime resolution precedence is:

1. Explicit CLI flag value
2. Environment variable fallback
3. Selected profile defaults, if a profile is active in the selected environment
4. Validation error (if still missing for a required argument)

If no profile is active in the selected environment, the profile layer is skipped entirely.

Environment keys:
- `CLINIC_CLUSTER_ID` for `--cluster-id`
- `CLINIC_RANGE_START` for `--start`
- `CLINIC_RANGE_END` for `--end`

Profile defaults:
- `selected_profile.cluster_id`
- `selected_profile.range_start`
- `selected_profile.range_end`

Example:

```bash
export CLINIC_CLUSTER_ID=123456789
export CLINIC_RANGE_START=1712123000
export CLINIC_RANGE_END=1712123600

# Uses CLINIC_CLUSTER_ID when --cluster-id is omitted
./bin/clinic metrics query --query 'up'

# Uses CLINIC_RANGE_START/CLINIC_RANGE_END and default --step=15s
./bin/clinic metrics range --query 'up'

# CLI still overrides env/profile defaults
./bin/clinic metrics range --query 'up' --start 1712122000 --end 1712122600 --step 60s
```

Verbose mode shows where resolved values came from (flag/env/profile), for example:

```text
[clinic verbose] resolved --cluster-id from env CLINIC_CLUSTER_ID: "123456789"
[clinic verbose] resolved --start from selected profile range_start: "1712123000"
[clinic verbose] resolved --end from flag --end: "1712123600"
```

Verbose mode also prints each outbound Clinic request, for example:

```text
[clinic verbose] request: GET /data-proxy/metrics/123456789/api/v1/query_range?end=1712123600&query=up&start=1712123000&step=15s
[clinic verbose] request query: end=1712123600&query=up&start=1712123000&step=15s
```

Cluster:

```bash
./bin/clinic cluster search --query tidb --limit 20 --page 1
./bin/clinic cluster search --deploy-type "premium resource pool" --limit 100
./bin/clinic cluster search-id --cluster-id 123456789
```

`cluster search` requires at least one of `--query` or `--deploy-type`. Deleted clusters are excluded by default; pass `--show-deleted` to include them. `cluster search-id` can auto-create a profile in the selected environment when the exact cluster ID is found and no profile already represents that cluster. It does not switch the active profile, and the search output remains unchanged.

`cluster search --deploy-type` maps to the dashboard `deploy_type_v2` filter and supports shell completion. The dashboard has no enumeration endpoint; known values are built into the CLI and listed in `clinic cluster search --help`, so backend-only additions may not appear.

Data proxy:

```bash
./bin/clinic data query --cluster-id 123456789 --sql 'select now()'
```

Metrics:

```bash
./bin/clinic metrics query --cluster-id 123456789 --query 'sum(rate(tidb_server_query_total[5m]))'
```

Default `metrics range` behavior writes an interactive ECharts HTML report and attempts to open it in a browser:

```bash
./bin/clinic metrics range --cluster-id 123456789 --query 'up' --start 1712123000 --end 1712123600
```

Without `--data-server`/`--ds`, `metrics range` continues to use the data-proxy
`/data-proxy/metrics/<target-id>/api/v1/query_range` path. `<target-id>` is the
cluster ID by default, or `parent_id` when `--pool` is set for eligible premium
clusters.

Use `--data-server` (or `--ds`) to opt into the data-server endpoint for
node-level/data-server metrics (for example, `node_*`). In that mode, `clinic-cli` sends `GET
/clinic/api/v1/data/metrics` with the same `query`, `start`, `end`, `step`, and
optional `timeout` query parameters, plus `X-OrgType: cloud`, `X-OrgID`, and
`X-ClusterID` headers:

```bash
./bin/clinic metrics range --cluster-id 123456789 --data-server --query 'node_cpu_seconds_total' --start 1712123000 --end 1712123600 --step 60s
```

Force JSON output when you want machine-readable output from a terminal:

```bash
./bin/clinic metrics range --cluster-id 123456789 --query 'up' --start 1712123000 --end 1712123600 --output json
```

Force the ECharts output explicitly when you want browser chart output:

```bash
./bin/clinic metrics range --cluster-id 123456789 --query 'up' --start 1712123000 --end 1712123600 --output echarts
```

Force the terminal TUI explicitly when you want the in-terminal interactive view:

```bash
./bin/clinic metrics range --cluster-id 123456789 --query 'up' --start 1712123000 --end 1712123600 --output tui
```

Use `--output tui` only when stdout is a terminal.

For premium clusters, use `--pool` (or `-p`) to query shared resource-pool metrics via `parent_id` instead of the tenant cluster ID:

```bash
./bin/clinic metrics range --cluster-id 123456789 --pool --query 'up' --start 1712123000 --end 1712123600
```

`--pool` is rejected unless the cluster deploy type is `premium` and `parent_id` is present.
`--data-server`/`--ds` is opt-in for `metrics range` only and cannot be combined with `--pool`.
`metrics query` and `metrics series` remain on their existing endpoints.

`metrics query` and `metrics range` support `--timeout <dur>` as a Prometheus
query parameter (server-side evaluation timeout). It does not set a client-side
HTTP request deadline in `clinic-cli`.

`metrics range --step` is optional. If omitted, `clinic-cli` sends `step=15s`.
`metrics range` uses `--output json`/`-o json` for formatted JSON output, `--output echarts`/`-o echarts`
for browser charts, and `--output tui`/`-o tui` for in-terminal charts. The default is `echarts`.
In ECharts output, the header shows `cluster id`, `cluster name`, `deploy type`, and the displayed time range/x-axis labels use local timezone.
In ECharts output, legend labels keep only label fields that differ across returned series (shared label fields are removed; if everything is shared, legend text is empty). All series are visible by default. Click a legend item to focus one series, click that same legend item again to restore all series, or hold Cmd/Ctrl while clicking legend items to keep/select multiple series.

Loki:

```bash
./bin/clinic loki labels --cluster-id 123456789
```

Loki query automatically scopes selectors to the selected profile/cluster by
injecting missing `cluster_env` and `namespace` label matchers, so there is no
Loki `--pool`/`-p` mode. During normal use, do not write `cluster_env` or
`namespace` in LogQL. Use `{}` for content-only searches and let `clinic`
inject the profile scope. Use `--skip-namespace-injection` when you want
`clinic` to inject only `cluster_env` and leave `namespace` unchanged.
Use `component` label filtering to distinguish shared resource-pool logs vs
cluster-unique logs within the resolved namespace scope.

Exact Starter and Essential clusters are the exception: `loki query` refreshes
their Dashboard provider, region, and deploy-type metadata, selects one
case-insensitive regional system-log datasource match, and queries that
datasource. Because a regional datasource contains multiple physical clusters,
the LogQL must contain exactly one selector with one non-empty equality matcher
such as `{cluster="physical-pool-name", component="dm-worker"}`. Omitted LogQL,
`{}`, regex or negative `cluster` matchers, and expressions with multiple
selectors are rejected before catalog or log traffic. For this route, the
injected namespace is `tidb-serverless` unless
`--skip-namespace-injection` is set. Essential V2, Essential V2 resource pool,
Premium, BYOC, Dedicated, and other deploy types retain the logical-cluster
route and existing selector behavior. This route change applies only to
`loki query`; other Loki subcommands and `diagnose` are unchanged.

Diagnose correlated evidence:

```bash
./bin/clinic diagnose "Request range exceeds bound" --cluster-id 123456789 --since 24h
./bin/clinic diagnose "Request range exceeds bound" --cluster-id 123456789 --since 24h -o json
```

`diagnose` does not infer root cause. Every output includes the profile timezone plus local and UTC query-window metadata.

Use `-o json` to emit the full `diagnose.v2` evidence bundle (`scope`, `query_plan`, `seed_matches`, `related_logs`, `events`, `topology`, `groups`, `timelines`, `summaries`, and `warnings`).

If the seed Loki query fails after retries, `diagnose` still queries independent sources such as topology and events where possible. The command writes the partial bundle with a failed `query_plan` entry and `query_error` warning, then returns the original query error.

Default table output is a compact summary derived from that same bundle: scope metadata, seed/related counts, warning types, and grouped timeline windows. Table output never prints raw log lines.

Related-log expansion controls:

- `--no-expand`: skip related-log expansion and keep only seed matches.
- `--max-seed-expand`: cap how many seed matches are eligible for expansion.
- `--max-related-logs`: cap retained related-log rows (`0` keeps none).

Loki log lines (default output):

```bash
./bin/clinic loki query \
  --cluster-id 123456789 \
  --start 1775174683000 \
  --end 1775181883000
```

Loki content search:

```bash
./bin/clinic loki query \
  --cluster-id 123456789 \
  --start 1775174683000 \
  --end 1775181883000 \
  '{} |= "Request range exceeds bound"'
```

Starter or Essential regional system-log search (use the physical `cluster`
label shown by the regional Loki datasource):

```bash
./bin/clinic loki query \
  --cluster-id 123456789 \
  --start now-30m \
  --end now \
  '{cluster="physical-pool-name", component="dm-worker"} |= "error"'
```

Loki raw JSON output:

```bash
./bin/clinic loki query \
  --cluster-id 123456789 \
  --start '2026-04-03 08:04:43' \
  --end '2026-04-03 10:04:43' \
  --limit 100 \
  -o json \
  '{} |~ "split|bucket|region"'
```

Loki instance mapping:

`loki instance-map` maps the node identifier used by data-server metrics
(`metrics range --data-server`) to the Loki stream label value.

For `node` values like `cn-hongkong.10.0.7.198`, `loki instance-map` maps to
Loki stream `instance` values such as `db-4b0d6fe-worker-tidb-u12t8g` and
derives the node IP as `10.0.7.198`.

Default table output:

```bash
./bin/clinic loki instance-map \
  --cluster-id 123456789 \
  --start 1775174683000 \
  --end 1775181883000
```

```text
NODE                        DATA_SERVER_INSTANCE        LOKI_INSTANCE                           IP
cn-hongkong.10.0.7.198      cn-hongkong.10.0.7.198      db-4b0d6fe-worker-tidb-u12t8g          10.0.7.198
```

JSON output:

```bash
./bin/clinic loki instance-map \
  --cluster-id 123456789 \
  --start 1775174683000 \
  --end 1775181883000 \
  -o json
```

```json
[
  {
    "node": "cn-hongkong.10.0.7.198",
    "data_server_instance": "cn-hongkong.10.0.7.198",
    "loki_instance": "db-4b0d6fe-worker-tidb-u12t8g",
    "ip": "10.0.7.198"
  }
]
```

Events:

```bash
./bin/clinic events list --cluster-id 123456789 --start 1712102400 --end 1712188800 --limit 50
```

Raw request:

```bash
./bin/clinic raw request \
  --method GET \
  --path /clinic/api/v1/dashboard/clusters \
  --query 'query=tidb' \
  --query 'limit=10'
```

## Notes

- Global `--env <env>` selects the runtime environment before the command family is dispatched.
- Global `--profile <name>` selects an env-local profile for that invocation only. If no active profile is set in the selected environment, commands fall back to explicit flags and `CLINIC_*` environment variables.
- Global `--verbose` / `-v` prints runtime resolution diagnostics and outbound request details to stderr.
- `auth show` masks the stored API key value; it does not print the raw key.
- If no API key is configured, API commands still build and send the request, but the Clinic service is expected to reject protected endpoints.
- Metrics `--time` / `--start` / `--end` accept datetime and epoch formats; `--start` / `--end` also accept `now` and `now-<duration>`:
  - relative now expressions: `now`, `now-30m`, `now-3h30m`, `now-1d` (`d` is a 24-hour day; minus only; plus is not supported)
  - datetime without timezone (uses local timezone): `2026-04-03 10:04:43`
  - datetime with timezone: `2026-04-03 10:04:43 +0800`
  - epoch in seconds/milliseconds/nanoseconds: `1712123000`, `1712123000000`, `1712123000000000000`
  - explicit unit suffixes: `1712123000s`, `1712123000000ms`, `1712123000000000000ns`
- Loki `--start` and `--end` accept `now` / `now-<duration>`, datetime, and epoch formats:
  - relative now expressions: `now`, `now-30m`, `now-3h30m`, `now-1d` (`d` is a 24-hour day; minus only; plus is not supported)
  - datetime without timezone (uses local timezone): `2026-04-03 10:04:43`
  - datetime with timezone: `2026-04-03 10:04:43 +0800`
  - epoch in seconds/milliseconds/nanoseconds: `1712123000`, `1712123000000`, `1712123000000000000`
  - explicit unit suffixes: `1712123000s`, `1712123000000ms`, `1712123000000000000ns`
- For required range commands, when both `--start` and `--end` are unresolved from flag/env/profile: `metrics range` and `loki query` default to `--start now-5m` and `--end now`; `events list` defaults to `--start now-7d` and `--end now` (cluster activity is low-frequency, so the wider default window matches operational review needs).
- For deploy types other than exact normalized `starter` or `essential`, Loki `query` infers a default selector when `<logql>` is omitted:
  - `cluster_env` comes from the selected environment
  - `namespace` is `tidb<clusterID>` for most cluster types
  - `namespace` is `tidb<parentID>` for `premium`/`byoc`
- For exact normalized `starter` or `essential`, omitted LogQL and `{}` are rejected. The query must contain exactly one selector and one non-empty equality `cluster="..."` matcher; the regional route injects missing `cluster_env` and `namespace="tidb-serverless"` labels.
- For other deploy types, when `<logql>` is provided and starts with a label selector, including `{}`, missing `cluster_env`/`namespace` labels are auto-filled as before.
- `loki query --skip-namespace-injection` keeps `cluster_env` auto-injection but skips `namespace` auto-injection. If `<logql>` is omitted with this flag, the default selector is `{cluster_env="<selected-env>"}`.
- Explicit `cluster_env` or `namespace` selectors are advanced raw/cross-profile or cross-namespace usage. `loki query` warns by default; pass `--allow-explicit-scope` only when that scope override is intentional.
- Loki `--limit` default is `10`; allowed range is `1..1000`.
