A command line tool for the ebean-insight /v1 API — list applications and
environments, browse captured DB query plans, and request fresh plan captures.
It is built with picocli and is designed to compile to a
single GraalVM native-image binary (insight) so it can be installed and run
without a JVM.
The CLI reaches the server one of three ways, resolved in this order:
- Static URL — pass
--url http://host:port(e.g. an ingress or a port-forward you manage yourself). When set, everything below is ignored. - A running
insight forwarddaemon — if one is active for the same target (namespace/service/port), short commands automatically reuse its tunnel (fast, no per-command startup). See Daemon mode. Use--no-sharedto bypass it. - Per-command port-forward (fallback) — otherwise the CLI starts and
supervises its own
kubectl port-forward(viaebean-insight-forwarder) for the duration of the command, reusing your existing cluster access (EKS/kubectlcredentials + RBAC) as authentication, then tears it down on exit. No API keys or OAuth required.
Connection options (shared by every subcommand):
| Option | Default | Meaning |
|---|---|---|
--url |
– | Static base URL. When set, the port-forward options below are ignored. |
--namespace |
required | Kubernetes namespace. No built-in default — set per call or persist it (see Configuration). |
--service |
required | Service to port-forward to. No built-in default — set per call or persist it. |
--target-port |
8091 |
Service port. |
--local-port |
0 |
Local port to bind; 0 picks a free ephemeral port. |
--context |
– | kubectl context to use. |
--ready-timeout |
20 |
Seconds to wait for the forward to become ready. |
--no-shared |
false |
Ignore any running insight forward daemon; start a private forward. |
--insight-key |
$INSIGHT_KEY |
API key sent as the Insight-Key header. Falls back to the INSIGHT_KEY env var. Not needed via port-forward. |
--namespace and --service are deployment-specific and have no built-in
defaults — supply them on the command line, or persist them once with
insight config (below). A port-forward command fails fast with a clear message
if neither a flag, config value, nor --url provides a target.
Persist any connection option in ~/.insight/config.properties so you don't have
to pass it every time. Explicit flags always override the stored value, which in
turn overrides the built-in default. Manage it with insight config:
insight config set namespace dev-core
insight config set service ebean-insight
insight config list # insight-key is masked
insight config get namespace
insight config unset service
insight config path # prints the file locationPersistable keys: url, namespace, service, target-port, local-port,
context, ready-timeout, insight-key, output, auth-domain,
auth-user-pool-id, auth-client-id, auth-scope, auth-redirect-port.
Resolution precedence for every option: explicit flag → config file →
built-in default (the built-in default only exists for non-identifying values
such as target-port). For example, set JSON as your default output format:
insight config set output json # every command now defaults to -o json
insight envs # JSON
insight envs -o text # flag still overrides to plain textThree independent mechanisms, depending on how the server is configured and how you reach it:
-
Port-forward (default) — auth is your Kubernetes access: the tunnel only works because your
kubectl/EKS credentials and RBAC let you forward to the Service. NoInsight-Keyis required. -
Static
--url(e.g. an ingress) — the/v1API expects anInsight-Keyheader. Provide it with--insight-key <key>or theINSIGHT_KEYenv var:export INSIGHT_KEY=... # or pass --insight-key each call insight plans --url https://insight.example.com
-
OAuth2 bearer token (
insight login) — when the server has JWT enforcement enabled (insight.auth.enabled=true, see docs/auth.md), authenticate once via the Cognito Hosted UI and the CLI sendsAuthorization: Bearer <token>on every request (alongside anyInsight-Key). This works over both port-forward and--url.
One-time setup — point the CLI at your Cognito public app client (PKCE; no
client secret). The redirect port must match a callback URL registered on the
app client (http://localhost:<port>/callback):
insight config set auth-domain https://<your>.auth.<region>.amazoncognito.com
# or derive the domain from the user pool id instead:
# insight config set auth-user-pool-id <region>_<poolId>
insight config set auth-client-id <public-app-client-id>
insight config set auth-scope insight/read # optional (default default/default)
insight config set auth-redirect-port 9876 # optional (default 9876)Then:
insight login # opens the browser; completes via a loopback redirect
insight whoami # show the cached identity + token expiry
insight logout # remove the cached token (~/.insight/token.json)insight login runs the OAuth2 Authorization-Code + PKCE flow: it starts a
short-lived loopback server on auth-redirect-port, opens your browser to the
Hosted UI, captures the redirected code, exchanges it for tokens and caches them
in ~/.insight/token.json (owner-only 0600). Subsequent commands load that
token and silently refresh it (via the refresh token) when it has expired.
When the cached access token cannot be refreshed, the server returns 401 and
you simply re-run insight login.
| Config key | Default | Meaning |
|---|---|---|
auth-domain |
– | Cognito Hosted-UI domain (e.g. https://app.auth.ap-southeast-2.amazoncognito.com). |
auth-user-pool-id |
– | Alternative to auth-domain: derive the domain from the user pool id. auth-domain wins if both are set. |
auth-client-id |
– | Public app client id (PKCE, no secret). |
auth-scope |
default/default |
Requested OAuth2 scope(s). |
auth-redirect-port |
9876 |
Loopback callback port. Must match a registered Cognito callback URL http://localhost:<port>/callback. |
Each short command otherwise pays the ~1–3s cost of establishing (and then
tearing down) a kubectl port-forward. Running a daemon keeps one supervised
tunnel open so every command becomes fast and stateless:
insight forward # holds the tunnel open; Ctrl-C to stop
# ... in another shell, these reuse it automatically (no per-command startup):
insight envs
insight plans -n 5insight forward (alias daemon):
- starts a supervised
kubectl port-forwardand keeps it alive across pod rolls / connection drops (this is where the forwarder's reconnect logic earns its keep); - advertises its stable local URL in
~/.insight/forward.propertiesso other commands discover and reuse it (skip with--no-register); - cleans up that advert and reaps the
kubectlchild on Ctrl-C / SIGTERM.
Discovery is target-scoped and self-healing: an advert is only reused when the namespace/service/port match and the daemon is both alive (pid check) and reachable (TCP probe); a stale advert is ignored and removed, falling back to a per-command forward.
| Command | Description |
|---|---|
insight forward (alias daemon) |
Hold a supervised port-forward open for other commands to reuse. --no-register to not advertise it. |
insight apps [--active-within-minutes N | --active-within-hours N] |
List known applications. |
insight envs |
List known environments. |
insight metrics --app X [--label] [--plan-capable] |
List an app's metrics (ID, NAME, HASH, LOC); full SQL with -o json. |
insight metric [<app>] [<hash>] [--app] [--hash] |
Show one metric (name, location, full SQL) by its hash. |
insight top [--app] [--env] [--by total|mean|max|count] [--since-minutes N | --since-hours N] [--plan-capable] [-n N] [--chart] [-i] |
Rank metrics by cost over a window. Omit --app to span all apps. --chart renders a horizontal Pareto bar chart; -i drives an interactive drill-down. |
insight trend [<app>] [<hash>] [--app] [--hash] [--env] [--by total|mean|max|count] [--since-minutes N | --since-hours N] |
Per-bucket trend column charts for one metric (tall top chart = --by measure, lower chart = calls). |
insight missing-plans [--app] [--by total|mean|max|count] [--since-minutes N | --since-hours N] [--older-than-minutes N | --older-than-hours N] [--capture [--yes] [--env]] [-n N] [-i] |
Plan-capable metrics with no recent plan, ranked by cost. --capture requests a plan for every listed row (capped by -n); -i drives an interactive drill-down. |
insight plans [--app] [--env] [--label] [--hash] [--since-minutes N] [--since-hours N] [-n/--limit N] |
List recently captured query plans (tabular). |
insight pending [--app] [--env] |
List in-flight plan captures — requested but not yet collected (durable; shows AGE, ages out after ~15 min). |
insight plan <planId> [--raw] |
Show one captured plan. --raw prints only the EXPLAIN plan text. |
insight capture [<app>] [<hash>...] [--app] [--hash] [--stdin] [--env] |
Request a fresh plan capture for one or more metric hashes (space or comma separated). --app/--hash are flag-form alternatives to the positionals; --stdin reads additional whitespace/comma/newline-separated hashes from standard input. |
insight config <set|get|unset|list|path> |
Manage persisted settings in ~/.insight/config.properties. |
insight login [--timeout-seconds N] |
Authenticate via Cognito (OAuth2 + PKCE) and cache the bearer token. |
insight whoami |
Show the cached login identity and token expiry. |
insight logout |
Remove the cached bearer token. |
Every command supports -h/--help, and the root supports -V/--version.
The data commands (apps, envs, top, metric, metrics, trend,
missing-plans, plans, plan, pending, capture) accept -o/--output
with text (default) or json:
insight envs -o json
insight plans -n 5 --output json | jq '.[].label'JSON is emitted compact (one line, pipe to jq to pretty-print) and an empty
result is rendered as []. For plan, -o json takes precedence over --raw.
Set insight config set output json to make JSON the default for every command.
Chart glyphs (the top --chart / interactive bars and the trend column charts)
are tinted to stand out from labels and numbers. Colour is only emitted to an
interactive terminal — piped output, redirected files and -o json stay plain.
Set NO_COLOR=1 to disable, or INSIGHT_COLOR=always to force colour on (e.g.
when piping into a pager that interprets ANSI).
insight config set namespace dev-core
insight config set service ebean-insight
insight config set context <nonprod-kube-context> # the core_nonprod EKS context
insight config set output json # optional: default to JSON
insight config listWith these persisted, every command below "just works" without connection flags.
insight apps # applications sending metrics
insight envs # environments (e.g. dev, test)insight top # all apps, by total time, last 60 min
insight top --by mean --since-hours 6 # rank by mean over a wider window
insight top --by max # worst single execution
insight top --by count # highest call volume
insight top --app myapp --env test # scope to one app / one env
insight top --plan-capable # only queries that can have a plan captured
insight top --chart # horizontal Pareto bar chart (cum% for total/count)
insight top --by mean -i # interactive drill-down (sql/plan/capture/trend)The --chart view renders dependency-free Unicode bars scaled to the largest
row; for additive measures (total, count) it also annotates a running
cumulative percentage so you can see the Pareto "vital few". -i prints a
numbered, bar-annotated list and then waits for you to pick a row and an action
— a guided drill-down without a full-screen TUI. It reads line-by-line from
stdin, so a session can be scripted via a pipe. With -o json or on a
non-interactive stream it falls back to plain output.
insight metric myapp <hash> # name, location and full SQL for one hash
insight metric myapp <hash> -o json
insight trend myapp <hash> --since-hours 6 # top chart: mean time, lower: call volume
insight trend myapp <hash> --by total # top chart: total time per bucket
insight trend myapp <hash> --by max # top chart: max (slowest) per bucket
insight trend myapp <hash> -o json | jq '.buckets'trend plots how a metric moves over time using stacked column charts: the tall
top chart plots the --by measure (total, mean (default), or max — derived
client-side from the raw per-bucket components count/total/max the server
returns), and the short lower chart plots call volume. --by count plots calls
as the headline chart on its own. The bucket resolution is chosen automatically
from the window (1m / 10m / 60m / 1d) and the chart is capped at ~60 columns wide.
insight missing-plans --by total # all apps, ranked by total time
insight missing-plans --by mean --since-hours 6 # rank by mean, wider window
insight missing-plans --app myapp # scope to one app
insight missing-plans --app myapp --older-than-hours 24 # "recent" = captured within 24h
insight missing-plans --by mean -i # interactive drill-down# one hash (--env comes right after the app: it sets which env to capture in)
insight capture myapp --env test <hash>
# several at once (space or comma separated, or repeated --hash)
insight capture myapp --env test hashA hashB hashC
insight capture myapp --env test hashA,hashB,hashC
insight capture --app myapp --env test --hash hashA --hash hashB
# pipe hashes straight from missing-plans
insight missing-plans --app myapp -n 10 -o json \
| jq -r '.[].key' | insight capture myapp --env test --stdin
# one-shot: capture every expensive-and-unplanned query (capped by -n)
insight missing-plans --app myapp --env test -n 10 --capture --yesOmitting --env requests the capture for any environment: it is delivered
to the app's next poll whichever environment it reports (shown as env * in
insight pending, with the actual env filled in once the plan is collected).
Pass --env to target a specific environment.
A capture is not instant: the app collects the bind values of the slowest execution over a ~5-minute window before the EXPLAIN plan is forwarded back. Wait ~6 minutes before checking, and the query must actually run in that window.
insight pending # captures requested but not yet collected
insight pending --app myapp --env testCapture requests are tracked durably on the server: each request is recorded
when made and cleared once its plan is ingested, so this view survives forwarder
polls and server restarts and covers the whole collection window. The AGE
column shows how long each request has been in flight; a request whose query
never executes ages out after ~15 minutes.
insight plans --app myapp --env test # recently captured plans (tabular)
insight plans --hash <hash> # all plans for one query
insight plans --since-hours 6 # captured in the last 6h
insight plan <id> # full SQL + bind values + EXPLAIN
insight plan <id> --raw # EXPLAIN plan text onlyinsight metrics --app myapp # ID, NAME, HASH, LOC
insight metrics --app myapp -o json # includes full SQL text
insight metrics --app myapp --plan-capable# any command supports -o json (compact, empty = []); pipe to jq
insight top -o json | jq '.[] | {key, label, totalMicros}'
insight plans -n 5 --output json | jq '.[].label'# talk to a server directly instead of port-forwarding
insight plans --url http://localhost:8091 --app myapp --since-hours 24
# override the persisted target for a single call
insight top --context my-eks --namespace staging-core --service ebean-insightDuring development (JVM):
mvn -pl cli,forwarder,api,client -am -DskipTests install
java -cp "cli/target/classes:$(mvn -o -q -pl cli dependency:build-classpath \
-Dmdep.outputFile=/dev/stdout | tail -1)" \
org.ebean.monitor.cli.InsightCli envsAs a native binary:
mvn -pl cli -Pnative -DskipTests package # produces cli/target/insight
./cli/target/insight envsThe native profile uses org.graalvm.buildtools:native-maven-plugin with the
picocli picocli-codegen annotation processor (which emits the reachability
metadata picocli needs under native-image). Build it with a GraalVM JDK on the
JAVA_HOME. The build passes --install-exit-handlers so the insight forward
daemon's shutdown hook (which clears its advert and reaps the kubectl child)
still runs on Ctrl-C / SIGTERM in the native binary.
InsightCli— root@Command+main.ConnectionOptions— shared--url/ port-forward mixin.ForwardRegistry— reads/writes the daemon advert (~/.insight/forward.properties); target-scoped, pid- and reachability-checked discovery.ForwardCommand— theinsight forwarddaemon (supervised tunnel + advert + clean shutdown).Insight— resolves theEndpoint(--url→ daemon advert →StaticEndpoint, else a per-commandSupervisedForwarder), builds the avajeHttpClient(JsonbBodyAdapter) and the generated typed clients (PlansApiHttpClient,AppsApiHttpClient,EnvsApiHttpClient), and owns the forwarder lifecycle (AutoCloseable).PlansCommand,PlanCommand,CaptureCommand,AppsCommand,EnvsCommand.LoginCommand/LogoutCommand/WhoamiCommand— OAuth2 (Cognito + PKCE) login, backed byAuthConfig(resolves theauth-*settings + buildsCognitoOidc),LoopbackReceiver(loopback redirect capture),BrowserLauncher(AWT-free browser open),TokenStore/TokenData(~/.insight/token.json,0600) andAuthSession(bearer + silent refresh, injected byInsight.open).
The typed API clients and DTOs come from ebean-insight-client /
ebean-insight-api (generated from api/src/main/openapi/v1.yaml). The
port-forward machinery comes from ebean-insight-forwarder.