Files
coralogix__cx-cli/example_config.toml
cx-saga[bot] 78cbd9e25c Print "View in Coralogix" console links after dashboard/alert/case mutations (#176)
## Summary

- After a successful command on any entity in the coverage table below,
`cx` prints a single `View in Coralogix: <url>` line to stderr per
profile. The link is **stderr-only** - `-o json`/`-o agents` output
never carries a `consoleUrl` field or any other trace of it, so
scripting against structured output never needs to account for it.
- Console URL resolution order: (1) explicit `console_url` in the
profile TOML - pure config, no API call; (2) a fresh cached `team_url`
in the profile TOML, written back the first time step 3 resolves it and
reused for 7 days so repeated invocations skip the round-trip; (3) `GET
/identity/whoami`'s `team_url` field, resolved live and then cached per
step 2 - the default on a cold cache, no config needed; (4) no link. A
failed/unusable `/identity/whoami` response, or an
expired-and-unrefreshable cache, fails quietly - no stderr hint, just no
link - and the command still succeeds normally. See
`docs/configuration.md` § "Console links".
- Console links can be suppressed entirely with `--no-console-link`
(single invocation), `CX_NO_CONSOLE_LINK` (env var / CI job), or
`no_console_link = true` in `~/.cx/config.toml` (all invocations) -
narrowest to broadest scope. Suppressing also skips the
`/identity/whoami` lookup on a cold cache.
- What the single stderr line points at differs by command shape, but it
is always exactly one link per profile - never a per-row link: entity
commands (`get`/`create`/`update`/...) point at that entity's own page;
list/search commands that return many rows (`alerts list`, `iam api-keys
list`, `tco list`, `dashboards check`, etc.) point at the shared
list/settings page instead, printed once per profile regardless of row
count.
- Empty results skip link printing entirely, so an empty list never
advertises a page link.
- 132 of 252 subcommands across 30 command groups print a link; the
Command coverage table below documents every subcommand's ✅/✖ status and
why.

## Command coverage

Every `cx` command group and **every individual subcommand** in it
(enumerated from `cx schema`, not hand-curated), each explicitly marked
whether it prints a "View in Coralogix" console link (✅) or not (✖) -
including read-only subcommands. 252 subcommands total across 30 command
groups; **132 print a link**.

**Every single ✅/✖ has its own stated reason below** - not just one
blanket note per command group. Three distinct kinds of "✖" show up, and
each row says which kind applies:
- **(a) no page exists at all** - genuinely nothing to link to,
confirmed against `cx-web-workspace` routing source or the command's own
read-only/local nature.
- **(b) a route exists but reads weren't wired for that group** - the
group is a *per-entity* group (an `:id` path segment or `?id=`-style
query param) where only some subcommand(s) are linked; the remaining
backlog after this round is listed below the table.
- **(c) nothing left to point at** - `delete`/`bulk-delete` subcommands,
where the entity no longer exists afterward.

| Command group | Every subcommand (✅ = prints a console link, ✖ = does
not) | Notes |
|---|---|---|
| `profiles` | `✖ list`, `✖ add`, `✖ delete`, `✖ set-default` | (a)
Local command: reads/writes `~/.cx/profiles/*.toml` on disk only - there
is no Coralogix console concept of a "profile", so none of the four has
a page |
| `completions` | `✖ generate`, `✖ install`, `✖ refresh` | (a) Local
command: generates/installs/refreshes shell completion scripts on disk;
no console counterpart for any of the three |
| `cleanup` | `✖ cleanup` | (a) Local command: deletes stale temp files
under `~/.cx`; purely local filesystem operation |
| `logs` | `✖ logs` | (a) Read-only DataPrime query; Explore's ad-hoc
query screen isn't a bookmarkable per-query URL (only *saved views* are,
already covered by the `views` link) |
| `metrics` | `✖ query`, `✖ query-range`, `✖ search`, `✖ get-labels` |
(a) Read-only PromQL queries/label lookups; same reasoning as `logs` -
no addressable per-query page |
| `spans` | `✖ spans` | (a) Read-only DataPrime query over spans; same
reasoning as `logs` |
| `dashboards` | `✖ catalog`, `✅ get`, `✅ create`, `✅ replace`, `✅
check`, `✖ delete`, `✖ search`, `✖ query-search`, `✖ folders/list`, `✖
folders/create`, `✖ folders/delete` | `create`/`replace`/`get` ✅ →
`{base}/dashboards/{id}`, a real per-id route confirmed in
`routing-utils.ts`. `check` ✅ when checking a *stored* dashboard by id
(`cx dashboards check <id>`) - same route as `get`; prints no link when
checking `--from-file` since there's no persisted entity.
`catalog`/`search`/`query-search` ✖ (a) - list/search operations return
many dashboards, no single one to point at. `delete` ✖ (c) - dashboard
no longer exists afterward. `folders/*` ✖ (a) - `folderId` lives only in
the data model, confirmed never written to the URL |
| `alerts` | `✅ list`, `✅ get`, `✅ create`, `✖ delete`, `✅ enable`, `✅
disable`, `✖ events`, `✖ event-stats`, `✖ suppression-rules/create`, `✖
suppression-rules/update`, `✖ suppression-rules/list`, `✖
suppression-rules/get`, `✖ suppression-rules/delete` | `create`/`get` ✅
→ `{base}/alerts/{id}`. `list` ✅ - same as the static-page groups: the
alerts-list page (`{base}/alerts`) link is printed once per profile on
stderr, not a per-row link. `delete` ✖ (c, alert gone afterward).
`enable`/`disable` ✅ - status toggle on an existing alert with a known
id, same route as `create`/`get`. `events`/`event-stats` ✖ (a) -
aggregate event history/stats across time, not a single alert's own
page. The whole `suppression-rules` group is ✖ here. The route
`{base}/suppression-rules?edit={id}` (top-level, not nested under
`/alerts`) is confirmed, but the console resolves `?edit=` against the
rule's `uniqueIdentifier`, and the CLI's response model only parses the
API's `id` field - which is the rule *version* id - so the link silently
fell back to the bare list page. Fixing it needs a response-model
change, not a link change, so `create`/`update` were pulled back out of
this PR (same treatment as `parsing-rules` below); the fix plus links
for `create`/`update`/`get`/`list` land together in FORGE-710. `delete`
✖ (c) regardless |
| `cases` | `✅ get`, `✅ update`, `✅ comment`, `✅ assign`, `✅ unassign`,
`✅ acknowledge`, `✅ unacknowledge`, `✅ resolve`, `✅ close`, `✅
set-priority`, `✅ clear-priority`, `✖ events/list`, `✖ events/get`, `✖
notifications` | `get` and all ten lifecycle-mutation subcommands ✅ →
`{base}/cases?id={id}` (confirmed via `SELECTED_CASE_QUERY_PARAM`) -
each acts on one already-known case id. No `create` (cases are
pipeline-generated, never created via this CLI).
`events/list`/`events/get` ✖ (a) - a case's audit/event history is a
distinct sub-resource with no page of its own. `notifications` ✖ (a) -
reads a case's notification-routing settings, same distinct-sub-resource
reasoning |
| `notifications` | `✅ connectors/create`, `✅ connectors/update`, `✖
connectors/list`, `✅ connectors/get`, `✖ connectors/delete`, `✖
connectors/types`, `✖ connectors/entity-types`, `✖
connectors/entity-subtypes`, `✅ routers/create`, `✅ routers/update`, `✖
routers/list`, `✖ routers/get`, `✖ routers/delete`, `✖
routers/validate-matcher`, `✖ presets/list`, `✖ presets/get`, `✖
presets/create`, `✖ presets/update`, `✖ presets/delete`, `✖
presets/set-default`, `✖ test/connector`, `✖ test/destination`, `✖
test/preset`, `✖ test/routing-condition`, `✖ test/template-render` |
`connectors create`/`update`/`get` ✅ → `?id=` auto-open route.
`connectors/list` ✖ (a, enumerates many) and `connectors/delete` ✖ (c,
gone afterward). `routers create`/`update` ✅ → same `?id=` pattern; its
`list` ✖ (a), `get` ✖ (b, not wired yet), and `delete` ✖ (c) follow the
same reasoning. `connectors/types`/`entity-types`/`entity-subtypes` ✖
(a) - list the *taxonomy* of connector kinds (string enum values), not
any actual connector instance. `routers/validate-matcher` ✖ (a) - a
stateless dry-run check, nothing persisted. `presets/*` ✖ (a) - a real
route exists (`/presets/:entityType?id=`) but requires resolving
`entityType` from the payload first, a materially bigger change than the
query-param groups above; tracked as backlog. `test/*` ✖ (a) - fires a
one-off test notification; nothing is created or persisted |
| `usage` | `✅ summary`, `✅ daily`, `✅ logs-count`, `✅ spans-count`, `✅
capabilities`, `✅ query`, `✅ export-status` | All seven ✅ →
`{base}/settings/datausage`. `usage` is 100% read-only with no mutation
to gate on at all, so every subcommand links to the one page that exists
regardless of which of the seven you ran |
| `tco` | `✅ list`, `✅ get`, `✅ create`, `✅ update`, `✅ delete`, `✅
reorder`, `✅ test`, `✅ settings`, `✅ settings-update` | All nine ✅ →
`{base}/tco-policies`. TCO is a single settings page per team regardless
of subcommand - list/get/test/settings (read-only) and
create/update/delete/reorder/settings-update (mutations) all point at
the same page, and even `delete` still links here since the *page* (not
a per-policy route) is what's being pointed at |
| `retentions` | `✖ list`, `✖ update`, `✖ activate`, `✖ status` | (a) A
global per-team setting; no per-entity or per-feature route for it could
be confirmed in `cx-web-workspace` frontend source at all - genuinely no
page found |
| `e2m` | `✅ create`, `✅ update`, `✖ list`, `✖ get`, `✖ delete`, `✖
labels-cardinality`, `✖ limits` | `create`/`update` ✅ →
`{base}/tco/metrics/{id}` (`:metricId` path-segment route). `list` ✖ (a,
enumerates many). `get` ✖ (b, known-id read not wired). `delete` ✖ (c,
gone afterward). `labels-cardinality`/`limits` ✖ (a) - team-wide
diagnostic/quota reporting across *all* E2M definitions, not any single
one |
| `recording-rules` | `✅ list`, `✅ get`, `✅ create`, `✅ update`, `✅
delete` | All five ✅ → `{base}/recording-rules`. Single settings page
per team regardless of which rule group is being listed/read/edited |
| `parsing-rules` | `✖ create`, `✖ update`, `✖ list`, `✖ get`, `✖
delete`, `✖ bulk-delete`, `✖ usage-limits` | Not wired in this PR -
`create`/`update` had a confirmed `{base}/rules/group/{id}` route
(`:themeId` path-segment) but were pulled out of this PR's scope;
tracked as backlog alongside `get` ✖ (b). `list` ✖ (a, enumerates many).
`delete`/`bulk-delete` ✖ (c, gone afterward). `usage-limits` ✖ (a) -
team-wide quota reporting, not a single rule group |
| `enrichments` | `✅ list`, `✅ add`, `✅ remove`, `✅ overwrite`, `✅
limit`, `✅ settings`, `✅ custom/list`, `✅ custom/get`, `✅
custom/create`, `✅ custom/update`, `✅ custom/delete`, `✅ custom/search`
| All twelve ✅ → `{base}/enrichments` (shared by enrichment rules and
custom enrichment tables - tabs on the same page). Single settings page
per team regardless of subcommand, so even
`custom/delete`/`custom/search` still point at the page rather than a
per-entity route that doesn't exist |
| `integrations` | `✅ list`, `✅ get`, `✅ definition`, `✅ deployed`, `✅
create`, `✅ update`, `✅ delete`, `✅ test`, `✅ template`, `✅
extensions/list`, `✅ extensions/get`, `✅ extensions/deployed`, `✅
extensions/deploy`, `✅ extensions/update`, `✅ extensions/undeploy`, `✅
contextual-data/list`, `✅ contextual-data/get`, `✅
contextual-data/create`, `✅ contextual-data/update`, `✅
contextual-data/delete`, `✅ contextual-data/definition`, `✅
contextual-data/test` | All 22 ✅ → `{base}/extensions/integrations`,
shared across integrations/extensions/contextual-data (facets of the
same catalog, itself a static per-type page, not per-instance) |
| `webhooks` | `✅ list`, `✅ get`, `✅ create`, `✅ update`, `✅ delete`, `✅
test`, `✅ types`, `✖ actions/list`, `✖ actions/get`, `✖ actions/create`,
`✖ actions/update`, `✖ actions/delete`, `✖ actions/batch`, `✖
actions/reorder` | `webhooks` subcommands: all seven ✅ →
`{base}/extensions/outbound-webhooks`, one settings page regardless of
subcommand. `actions/*` all seven ✖ (a) - `actions` renders as an in-app
slide-in panel, confirmed to have no route at all in frontend source;
this applies uniformly to every `actions` subcommand |
| `views` | `✖ list`, `✖ get`, `✅ create`, `✅ update`, `✖ delete`, `✖
folders/list`, `✖ folders/get`, `✖ folders/create`, `✖ folders/update`,
`✖ folders/delete` | `create`/`update` ✅ → `{base}/explore?viewId={id}`
(confirmed via `viewIdParam()`). `list` ✖ (a, enumerates many). `get` ✖
(b, known-id read not wired). `delete` ✖ (c, gone afterward).
`folders/*` ✖ (a) - `folderId` confirmed to exist only in the data
model, never written to the URL |
| `iam` | `✅ roles/create`, `✅ roles/update`, `✖ roles/list`, `✖
roles/get`, `✖ roles/delete`, `✖ roles/system`, `✅ scopes/create`, `✅
scopes/update`, `✖ scopes/list`, `✖ scopes/get`, `✖ scopes/delete`, `✅
groups/create`, `✅ groups/update`, `✖ groups/list`, `✖ groups/get`, `✖
groups/get-by-name`, `✖ groups/users`, `✖ groups/delete`, `✅
api-keys/list`, `✅ api-keys/get`, `✅ api-keys/create`, `✅
api-keys/update`, `✅ api-keys/delete`, `✅ api-keys/send-data-keys`, `✅
api-keys/admin/delete`, `✅ api-keys/admin/set-status`, `✅ users/search`,
`✅ users/get`, `✅ users/create`, `✅ users/update`, `✅ users/set-status`,
`✅ ip-access/get`, `✅ ip-access/create`, `✅ ip-access/update`, `✅
ip-access/delete` | `roles`/`scopes`/`groups create`/`update` ✅ →
`?selected*Id=` auto-open routes (per-entity). Their `list` ✖ (a,
enumerates many), `get`/`get-by-name` ✖ (b, known-id read not wired),
`delete` ✖ (c, gone afterward). `roles/system` ✖ (a) - lists built-in
system roles which have no editable console entry at all. `groups/users`
✖ (a) - reads a group's *membership list*, a distinct sub-resource from
the group's own settings row. **`api-keys`, `users`, `ip-access`**: all
17 ✅ → their respective static settings pages
(`{base}/settings/api-keys`, `{base}/settings/team/members`,
`{base}/settings/login-access-policies`) - flat pages where create/edit
is a dialog, so list/get/read subcommands point at the same page as
mutations |
| `archive` | `✖ metrics/get`, `✖ metrics/create`, `✖ metrics/update`,
`✖ metrics/enable`, `✖ metrics/disable`, `✖ metrics/validate`, `✖
logs/get`, `✖ logs/set` | Not wired in this PR - all eight had a
confirmed `{base}/physical-locations` route (shared by both the metrics-
and logs-archive subtrees, since they configure the same underlying
storage locations) but were pulled out of this PR's scope; tracked as
backlog |
| `ai-center` | `✅ applications/list`, `✅ applications/get`, `✅
evaluations/list`, `✅ evaluations/get`, `✅ evaluations/create`, `✅
evaluations/update`, `✅ evaluations/delete`, `✅
custom-evaluations/list`, `✅ custom-evaluations/list-for-application`,
`✅ custom-evaluations/create`, `✅ custom-evaluations/update`, `✅
custom-evaluations/add`, `✅ custom-evaluations/remove`, `✖ coverage`, `✖
model-pricing/get`, `✖ model-pricing/set` | `applications/list`+`get` ✅
→ `{base}/ai-center/overview/application-catalog` - this CLI has no
`create`/`update`/`delete` for applications at all, so `list`/`get` are
the *only* entry points and were linked from the start.
`evaluations/*`+`custom-evaluations/*` (13 total) ✅ →
`{base}/ai-center/overview/eval-catalog`, one settings page regardless
of subcommand. `coverage` ✖ (a) - a team-wide aggregate report; no
dedicated route could be confirmed in frontend source at all.
`model-pricing/get`+`set` ✖ (a) - confirmed dialog-only, no route |
| `slos` | `✅ create`, `✅ update`, `✖ list`, `✖ get`, `✖ delete` |
`create`/`update` ✅ → `{base}/slo/{id}/overview` (`:sloId` path-segment
route). `list` ✖ (a, enumerates many). `get` ✖ (b, known-id read not
wired). `delete` ✖ (c, gone afterward) |
| `infra` | `✖ resources/types`, `✖ resources/list`, `✖
resources/health-history`, `✖ resources/raw-data` | (a) Read-only
infrastructure inventory/health queries; no per-resource console route
could be confirmed in `cx-web-workspace` frontend source for any of the
four |
| `search-fields` | `✖ search-fields` | (a) Read-only field-metadata
search; no console page indexes/searches fields this way at all |
| `dataprime` | `✖ list`, `✖ show`, `✖ query` | (a) `list`/`show` are
local reference lookups (DataPrime language docs bundled in the binary);
`query` is a raw read-only query, same reasoning as `logs` |
| `docs` | `✖ search`, `✖ fetch` | (a) Read-only documentation
search/fetch against Coralogix's public docs site, which isn't part of
the web console at all |
| `schema` | `✖ schema` | (a) Local introspection - dumps this CLI's own
command tree as JSON for agent consumption; no console counterpart |
| `olly` | `✅ ask`, `✖ artifacts/list`, `✖ artifacts/get` | `ask` ✅ →
`{base}/olly/chat/{chatId}`, the chat the command just
created/continued. **Caveat:** only the parent `{base}/olly` route is
confirmed in `cx-web-workspace` frontend source; the `/chat/{chatId}`
child segment is not, and no generated link has been opened yet — it
needs one manual click before merge, or the link should fall back to
`{base}/olly`. `artifacts/list`+`get` ✖ (a) - these retrieve a
previously-generated Olly *output* (a stored answer object), a distinct
resource with no route of its own confirmed in frontend source |

Category (b) rows (`notifications routers/get`, `e2m get`,
`parsing-rules get`, `views get`, `iam roles/scopes/groups
get`/`get-by-name`, `slos get`) remain a coherent backlog: each has a
confirmed route (the same one its `create`/`update` sibling uses), it's
just that wiring that particular read wasn't part of this round.
Category (a) rows have no route to wire at all, confirmed against
`cx-web-workspace` frontend source. Category (c) rows
(`delete`/`bulk-delete`) are permanent - there will never be a page to
link to once the entity is gone. `parsing-rules create`/`update` are a
special case of (b): they were wired earlier in this PR's development
but pulled back out to keep this round scoped to a smaller diff, so the
whole group is backlog for now, not just `get`. `archive` is the same
case: all eight of its subcommands were wired earlier in this PR's
development against the confirmed `{base}/physical-locations` route, but
the whole group was pulled back out to keep this round scoped to a
smaller diff. `alerts suppression-rules` was pulled back out for a
different reason - its link was wired and *wrong* (version id instead of
`uniqueIdentifier`, see the `alerts` row) - and is tracked in FORGE-710,
which stacks on this branch.

## Live verification

The scripts added in `verification/forge-586-console-links/` were run
against a real Coralogix team and their output captured into an
interactive terminal-replay report: **[cx CLI — console-link commands,
terminal output (PR
#176)](https://claude.ai/code/artifact/6fbae5df-d0b6-41d0-a79e-4e2ef72ab2e8)**.

The report replays the recorded runs across the 15 command groups that
have at least one linked subcommand - both the ✅ and the ✖ subcommands,
so the "no link is printed" cases are evidenced too - as they appeared
in a real terminal (stderr progress lines then stdout result). Each card
shows the exact command that was run, exit code, duration, and
scrollable terminal output; commands with multiple output formats (`-o
text`, `-o json`, `-o agents`) have tabs to switch between them. Totals:
**244 pass / 39 fail / 33 skip across 322 invocations (76%)** — the
fails are concentrated in groups whose live team data hit empty-list or
backend-permission edge cases, not link-wiring regressions. `archive`
was pulled out of this PR's scope (see Command coverage above), so its
recorded runs were dropped from this report.

## Test plan
- [x] `cargo test --lib` (557 tests, 9 ignored -
filesystem/keyring-dependent)
- [x] `cargo test --test console_urls` (58/58, full-binary integration
suite - includes regression tests for the empty-list console-link fix
and the `/identity/whoami`-based `team_url` resolution)
- [x] `cargo test --test <name>` for every touched command module's
integration suite (`alerts`, `api_keys`, `cases`, `connectors`,
`contextual_data`, `custom_enrichments`, `data_archive`, `data_usage`,
`e2m`, `enrichments`, `extensions`, `integrations`, `ip_access`, `olly`,
`parsing_rules`, `recording_rules`, `roles`, `routers`, `scopes`,
`slos`, `tco_policies`, `team_groups`, `users`, `views`, `webhooks`,
`dashboards`, `ai_center`) - all green
- [x] `cargo fmt` clean
- [x] `cargo clippy --locked -- -D warnings` clean
- [x] `docs/configuration.md` "Console links" section kept in sync with
current resolution order and behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: cx-saga[bot] <294118815+cx-saga[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Binyamin Levine <binyamin.levine@coralogix.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 17:53:38 +03:00

36 lines
1.5 KiB
TOML

# Example global cx config.
# Save as ~/.cx/config.toml
# Profile name used when --profile is not provided.
default_profile = "default"
# Default output mode when --output is not provided.
# Valid values: "text", "json", "agents"
default_output_format = "text"
# Maximum serialized byte size of a non-aggregated Dataprime response that can
# be printed directly to stdout in "agents" output mode. If the payload exceeds
# this limit the results are written to a temp file instead, and a short summary
# (result count + file path) is printed to stdout. Default: 102400 (100 KiB).
# Set to -1 to disable the limit and always print results directly.
# max_dataprime_direct_output_size = 102400
# Directory where temp result files are stored when the payload exceeds
# max_dataprime_direct_output_size. Files are prefixed with "cx_results" so
# they can be identified and removed with `cx cleanup`. Default: "/tmp/".
# temp_dir = "/tmp/"
# Credentials live in separate profile files under ~/.cx/profiles/.
# Example: ~/.cx/profiles/default.toml
#
# api_key = "cxp_your_api_key_here"
# region = "eu1"
# label = "production"
#
# Optional: override the web console base URL used to build "View in
# Coralogix" links printed after dashboard/alert/case mutations. By default
# this is resolved automatically via GET /identity/whoami; only needed when
# that lookup isn't reachable (e.g. a custom, self-hosted environment).
# See docs/configuration.md#console-links.
# console_url = "https://<your-team>.<console-domain>"