The monoscope CLI provides terminal access to your observability platform — search logs and traces, query metrics, manage monitors and dashboards, triage issues, and more.
curl https://monoscope.tech/install.sh | shDetects your platform (Linux/macOS, x86_64/arm64), downloads the latest release, verifies the SHA256 checksum, and installs monoscope to ~/.local/bin.
Specific version:
curl https://monoscope.tech/install.sh | sh -s v1.2.3Custom install directory:
MONOSCOPE_INSTALL_DIR=/usr/local/bin curl https://monoscope.tech/install.sh | shUpgrade to latest (re-running the install script upgrades automatically if a newer version exists):
curl https://monoscope.tech/install.sh | shIf the installed version is already current, the script exits with "already up to date". To force reinstall the same version, pass --force:
curl https://monoscope.tech/install.sh | sh -s -- --forceBrowser-based login (interactive — opens browser automatically):
monoscope auth loginToken login (non-interactive / CI):
monoscope auth login --token <your-api-key>Check auth state:
monoscope auth statusLog out:
monoscope auth logout| Variable | Description |
|---|---|
MONOSCOPE_API_KEY |
API key — takes precedence over stored token |
MONOSCOPE_PROJECT |
Default project UUID |
MONOSCOPE_API_URL |
API base URL (default: https://api.monoscope.tech) |
MONOSCOPE_FORCE_COLOR |
Set to 1 to keep colored table output when stdout is piped (otherwise auto-detect switches to JSON) |
MONOSCOPE_DEBUG |
Set to 1 to print every outgoing request to stderr (same as --debug) |
Config is resolved in this order (later values win):
~/.config/monoscope/config.yaml— global config.monoscope.yaml— project-local config (searched up from cwd)- Environment variables
Interactive setup wizard:
monoscope config initSet/get individual values:
monoscope config set project <uuid>
monoscope config set api_url https://your-instance.example.com
monoscope config get # show all
monoscope config get project # show one valueValid config keys: api_url, project, api_key.
Every command accepts these flags:
| Flag | Description |
|---|---|
--project/-p <uuid> |
Override the project for this invocation |
--json |
Emit JSON (forced anyway when stdout is piped) |
--yaml |
Emit YAML |
--table |
Force pretty-printed table (overrides pipe → JSON auto-detect) |
--debug |
Print every outgoing request URL to stderr |
When more than one output flag is set, precedence is --json > --yaml > --table.
table— default when stdout is a TTY. Unicode box-drawing renderer with severity colouring, terminal-width truncation, and a pagination cue under list output. SetMONOSCOPE_FORCE_COLOR=1to keep colours when piping.json— default when stdout is a pipe. Pretty-printed JSON envelope, stable shape forjqand agent consumption.yaml— config-friendly. Round-trips cleanly throughapplyfor monitors and dashboards.
Logs and traces share the same event storage. logs and traces are aliases for events with a kind filter pre-applied.
The positional QUERY argument is KQL (the same query language used in the web UI). Equality uses == with double-quoted strings; combine with and/or/parens. Bare strings (no KQL operators) are auto-rewritten to body has "X" or summary has "X", so monoscope logs search POISON_ROW_DROPPED just works.
# Bare-string full-text search (rewritten to body has "..." or summary has "...")
monoscope logs search POISON_ROW_DROPPED --since 24h
# All events from a service in the last hour (default --since=1H, matching the UI)
monoscope logs search --service checkout-api
# Only error-level logs in the last 30 minutes (--level normalises to upper-case)
monoscope logs search --level error --since 30m --limit 50
# Free-text search inside the log body
monoscope logs search 'body contains "payment failed"' --since 1h
# Filter by attribute (any field from `monoscope schema`)
monoscope events search 'attributes.http.response.status_code >= 500' --since 1h
# Absolute time range
monoscope traces search --service auth \
--from 2026-04-01T00:00:00Z --to 2026-04-02T00:00:00ZOptions:
| Flag | Description |
|---|---|
QUERY |
KQL search query (positional). Run monoscope schema to see available fields. |
--since <duration> |
Relative lookback (default: 1H). Examples: 10s, 30m, 2h, 7d |
--from <timestamp> |
Start time (ISO 8601) — pairs with --to, overrides --since |
--to <timestamp> |
End time (ISO 8601) |
--kind log|trace |
Filter by event kind (mapped to the source query param) |
--service <name> |
Shorthand for resource.service.name=="<name>". Repeatable — --service a --service b expands to in (a, b) |
--level <level> |
Shorthand for severity.text=="<LEVEL>" (auto-uppercased) |
--limit/-n <N> |
Max results to return |
--fields <f1,f2> |
Comma-separated columns to keep in JSON / table output |
--cursor <value> |
Pagination: pass the cursor field from a previous response |
--first |
Return only the first matching event (the JSON envelope still carries count/has_more) |
--id-only |
Print just the first event's id to stdout — natural input for events get/share-link. Implies --first |
--with-children |
Also return descendants of each matched span (default: predicate hits only) |
--chunk-hours <H> |
Hours per internal fetch slice for wide --since windows (default 1; 0 = single request) |
The JSON output envelope is stable: {events: [...], count, has_more, cursor}. Use cursor to drive pagination loops, count for the total matching, and has_more to terminate. Windows wider than --chunk-hours are fetched in slices internally (sidestepping gateway timeouts on multi-day queries) and merged client-side — the output is always that single envelope, with events deduplicated at slice boundaries and --limit applied across the whole window.
# Single event by id (default --since=24H)
monoscope events get <event-id>
# Project a specific field out of the event JSON (repeatable, no jq needed)
monoscope events get <event-id> --field body --field summary
# Common case: just the message text
monoscope events get <event-id> --show-body
# All spans of a trace
monoscope traces get <trace-id> --treeOptions for events get:
| Flag | Description |
|---|---|
--tree |
Render as a trace tree (only relevant for span IDs) |
--at <timestamp> |
ISO-8601 hint for a fast point-in-time lookup (skips the 90d range scan) |
--field <name> |
Project a single field out of the JSON. Repeatable |
--show-body |
Shorthand for --field body --field summary |
Stream events in real-time (polls every 2 seconds):
monoscope events tail
monoscope logs tail --service api --level error
monoscope logs tail --grep "timeout" # client-side substring filterShow events around a specific timestamp:
monoscope events context --at 2026-04-15T12:34:56Z --service api --window 10m
# Per-trace summary alongside raw events (recommended for incident triage)
monoscope events context --at 2026-04-15T12:34:56Z --window 10m --summaryWith --summary, the JSON envelope is augmented with a traces array:
{
"events": [...],
"count": 42,
"has_more": false,
"traces": [
{ "trace_id": "abc...", "services": ["checkout", "payments"], "span_count": 12, "error_count": 1 }
]
}The metrics endpoint runs KQL queries against the same store as the web UI.
Use summarize for aggregations and by for grouping.
# Event count grouped by service (last 1H, default)
monoscope metrics query 'summarize count() by resource.service.name'
# p99 latency per service over the last 30 minutes
monoscope metrics query 'summarize percentile(duration, 99) by resource.service.name' --since 30m
# Assertion (CI-friendly: exits non-zero if the numeric result fails the condition)
monoscope metrics query 'summarize count()' --since 30m --assert '< 1000'Options: --since (default 1H), --from, --to, --assert <condition>.
monoscope metrics chart 'summarize count() by bin(timestamp, 1m)' --since 2h
monoscope metrics chart 'summarize avg(duration) by bin(timestamp, 1m)' --watch 30smonoscope services list # services active in last 24h
monoscope services list --since 7dmonoscope monitors list
monoscope monitors get <id>
# Create / update from file (YAML or JSON)
monoscope monitors create monitor.yaml
monoscope monitors update <id> monitor.yaml # full replace (PUT)
monoscope monitors patch <id> patch.yaml # partial update (PATCH)
# Apply a file or directory (idempotent upsert keyed by name)
monoscope monitors apply monitors/
# Lifecycle
monoscope monitors mute <id> --for 30 # mute for 30 minutes
monoscope monitors unmute <id>
monoscope monitors resolve <id>
monoscope monitors toggle-active <id>
monoscope monitors delete <id>
# Bulk actions: delete, activate, deactivate, mute, unmute, resolve
monoscope monitors bulk mute --ids id1,id2,id3 --duration 60
monoscope monitors bulk resolve --ids id1,id2monoscope dashboards list
monoscope dashboards get <id>
monoscope dashboards create dashboard.yaml
monoscope dashboards update <id> dashboard.yaml
monoscope dashboards patch <id> patch.yaml
monoscope dashboards delete <id>
monoscope dashboards star <id>
monoscope dashboards unstar <id>
monoscope dashboards duplicate <id>
# Dump as YAML (for editing and re-applying)
monoscope dashboards yaml <id> > dashboard.yaml
# Idempotent upsert from file or directory
monoscope dashboards apply dashboards/
# Bulk: delete
monoscope dashboards bulk delete --ids id1,id2
# Widget operations
monoscope dashboards widget upsert <dashboard-id> widget.yaml
monoscope dashboards widget delete <dashboard-id> <widget-id>
monoscope dashboards widget reorder <dashboard-id> --tab overview positions.jsonpositions.json shape:
{
"widget-id-1": { "x": 0, "y": 0, "w": 6, "h": 4 },
"widget-id-2": { "x": 6, "y": 0, "w": 6, "h": 4 }
}monoscope api-keys list
monoscope api-keys get <id>
monoscope api-keys create "my-key-name" # prints plaintext key once
monoscope api-keys activate <id>
monoscope api-keys deactivate <id> # soft disable
monoscope api-keys delete <id> # permanent removalRotating a key:
monoscope api-keys create "ops-2026-05" -o json | jq -r .key > ~/.monoscope-key-new
export MONOSCOPE_API_KEY=$(cat ~/.monoscope-key-new)
monoscope api-keys deactivate <old-key-id>Issues are the user-facing representation of detected problems.
monoscope issues list # open issues by default
monoscope issues list --status acknowledged # open | acknowledged | archived | all
monoscope issues list --type runtime_exception # filter by issue type
monoscope issues list --service checkout-api # filter by service
monoscope issues get <id>
monoscope issues ack <id>
monoscope issues unack <id>
monoscope issues archive <id>
monoscope issues unarchive <id>
# Bulk: acknowledge, unack, archive, unarchive
monoscope issues bulk acknowledge --ids id1,id2,id3monoscope endpoints list
monoscope endpoints list --search '/v1/' --per-page 100
monoscope endpoints list --outgoing # only outgoing calls
monoscope endpoints get <id>monoscope log-patterns list --per-page 50
monoscope log-patterns get <id>
monoscope log-patterns ack <id>
# Bulk: ack, ignore
monoscope log-patterns bulk ack --ids 1,2,3
monoscope log-patterns bulk ignore --ids 4,5monoscope teams list
monoscope teams get <id>
monoscope teams create team.yaml # { name, handle, description, notify_emails, slack_channels }
monoscope teams update <id> team.yaml # full replace (PUT)
monoscope teams patch <id> patch.yaml # partial update (PATCH)
monoscope teams delete <id>
monoscope teams bulk delete --ids id1,id2The everyone handle is reserved — the built-in everyone team cannot be updated or deleted.
monoscope members list
monoscope members get <user-id>
# Add by email (creates a stub user if unknown) or by existing user ID
monoscope members add --email teammate@example.com --permission view
monoscope members add --user-id <uuid> --permission edit
# Permissions: view | edit | admin
monoscope members patch <user-id> admin
monoscope members remove <user-id>monoscope me # show current project identity
monoscope project get
monoscope project patch patch.yaml # PATCH any subset of: title, description, time_zone,
# daily_notif, weekly_notif, endpoint_alerts, error_alertsCreate a 48-hour share link for a stored event:
monoscope share-link create \
--event-id <uuid> \
--created-at 2026-04-15T00:00:00Z \
--type log # request | log | spanReturns { id, url }.
Fetch the telemetry field schema for the current project. The full payload
is large; use --search/--limit so an LLM agent doesn't have to load
hundreds of field names into context to answer a single question.
monoscope schema # all fields
monoscope schema --search service # only fields whose name contains "service"
monoscope schema --search http --limit 20 # http-related fields, capped at 20
monoscope schema -o json | jq '.fields | keys[]' # bare field namesThe response shape is {fields: { <field_name>: { field_type, description, ... }, ... }, ...}.
Note fields is an object keyed by field name, not an array — jq '.fields[]' works,
jq '.fields[0]' does not.
facets returns precomputed top-N values per field — the platform tracks
this for every faceted column (service name, severity, HTTP method, status
code, etc.) so an agent can answer "what's actually in this project?"
without paying for an aggregation query each time.
# Dump every faceted field with its top values
monoscope facets
# Drill into a single field
monoscope facets resource.service.name
monoscope facets severity.text --top 5
# Widen the lookback (default 24h)
monoscope facets --since 7d
# Pipe into a search
SVC=$(monoscope facets resource.service.name --top 1 \
| jq -r '.["resource.service.name"][0].value')
monoscope events search '' --service "$SVC" --since 1hResponse shape:
{
"resource.service.name": [
{ "value": "checkout-api", "count": 1284 },
{ "value": "payments", "count": 812 },
...
],
"severity.text": [...],
"attributes.http.response.status_code": [...]
}Each field's value list is sorted by count descending. With --top N,
the CLI trims each field's list to N entries (server returns the full
top-N already, the flag is a client-side cap when you want fewer).
When facets haven't been generated yet (new project, or the background
job hasn't run), the response is {} — not 404. Agents can rely on the
shape regardless.
auth status emits structured output whenever stdout isn't a TTY, or with an
explicit --json / --yaml:
monoscope --json auth status
# {
# "authenticated": true,
# "method": "token", # "token" | "env" | null
# "api_url": "https://api.monoscope.tech",
# "project": "00000000-..."
# }In non-interactive mode auth login (without --token) refuses to start the
interactive device-code flow and exits non-zero — pass --token <key> from
your secret store instead. CI=1 and CLAUDE_CODE=1 are still honoured as
auto-detect hints for non-interactivity.
- Gateway errors (
502/503/504, Cloudflare HTML) are collapsed into a single-line message with a hint to narrow--since; the CLI retries the request once with jittered backoff before giving up. - Structured errors on stderr in non-table mode —
printErroremits{"error": {"code", "message", "field?", "suggestion?", "details?"}}as NDJSON on stderr. ANSI is suppressed automatically whenever stdout isn't a TTY (override withMONOSCOPE_FORCE_COLOR=1). --debugsets anX-Debug: 1header so the server includes the raw SQL / Hasql exception text undererror.details. Combine withMONOSCOPE_DEBUG=1for outgoing-request tracing.
monoscope completion bash >> ~/.bashrc
monoscope completion zsh >> ~/.zshrc
monoscope completion fish >> ~/.config/fish/completions/monoscope.fishUnknown shells exit non-zero with a clear error rather than silently emitting a bash script.
In agent / pipe mode the CLI emits stable, documented JSON. For event-shaped commands (events search, logs search, traces search, events get, events context) the response is:
{
"events": [{ "id": "...", "timestamp": "...", "service": "...", "summary": "...", "trace_id": "...", "kind": "...", ... }],
"count": 501,
"has_more": true,
"cursor": "2026-05-02T17:34:00.053333Z"
}Each event is a flat object — no colIdxMap / logsData indirection. Use --fields a,b,c to project a subset.
For services list:
{ "services": [{ "name": "checkout-api", "events": 1284 }], "count": 12 }For every other list command (monitors list, dashboards list, api-keys list, teams list, members list, issues list, endpoints list, log-patterns list) the CLI normalises to a single shape:
{
"data": [...],
"pagination": {
"has_more": false,
"total": 42, // null if the server doesn't report total_count
"cursor": null,
"page": 0, // null if the server uses cursor-only pagination
"per_page": 20
}
}This normalisation happens client-side — the underlying API still returns its native shape (some endpoints emit a Paged envelope {items, page, per_page, total_count, has_more}, others a bare array). jq '.data[] | .id' works for every list command, regardless of which the server returned.
Migration note: the previous shapes (
{items, total_count, ...}for issues/endpoints/log-patterns; bare arrays for monitors/dashboards/api-keys/teams/members) are no longer emitted. Pipelines that read.itemsshould switch to.data. Theevents searchenvelope ({events, count, has_more, cursor}) is unchanged — those commands remain on their event-specific shape because they expose--cursor/--first/--id-only.
A typical incident investigation, all in JSON, suitable for piping into jq or feeding to an LLM agent:
# 1. What's on fire? List open issues, sorted by recency.
monoscope issues list --status open -o json | jq '.data[] | {id,title,service,severity}'
# 2. Which services are active? (and which are quiet)
monoscope services list -o json | jq '.services'
# 3. Drill in: errors from the suspect service in the last 30 minutes
monoscope logs search --service checkout-api --level error --since 30m -o json \
| jq '.events[] | {timestamp, summary, trace_id}'
# 4. Pick a trace and pull the full tree
monoscope traces get <trace-id> --tree
# 5. Find similar errors via log patterns
monoscope log-patterns list --per-page 20 -o json \
| jq '.data[] | select(.state=="open") | {id, occurrence_count, service_name}'
# 6. Check a specific symptom: which endpoints serve 5xx?
monoscope events search 'attributes.http.response.status_code >= 500' --since 1h -o json \
| jq -r '.events[] | "\(.timestamp) \(.service) \(.summary)"'
# 7. Acknowledge, and keep moving
monoscope issues ack <issue-id>Every command honours --debug (or MONOSCOPE_DEBUG=1) to print the outgoing request URL — invaluable when an agent gets a 4xx and needs to inspect what it actually sent. Server validation errors (e.g. invalid KQL) are forwarded verbatim from the response body, including line/column markers for parse failures.
Export these in CI environments:
export MONOSCOPE_API_KEY=<your-api-key>
export MONOSCOPE_PROJECT=<project-uuid>JSON is the default when stdout is not a TTY or CI is set. Use jq for scripting:
# Error-rate gate in CI (count of error-level events in the last 30 min)
monoscope metrics query 'summarize count() | where severity.text=="error"' --since 30m --assert '< 100'
# Get open issue count
monoscope issues list --status open -o json | jq '.data | length'
# Apply all monitors from a directory (idempotent)
monoscope monitors apply .monoscope/monitors/The CLI ships with three layers of tests; pick the one that matches the change you're making.
cli/CLI/Validate.hs carries doctests for validateDuration, validateUuid,
validateKind, and normalizeKind. They run as part of the lib's doctest
suite once exposed; until then they document expected behavior in-source.
Add a doctest for any new pure helper — they are the cheapest regression
guard you have.
test/integration/CLI/CLIBinarySpec.hs runs monoscope --help for every
subcommand and asserts exit code 0. This catches optparse parser
construction errors before they reach a user. Add an entry to helpCases
when you add a new command "..." in cli/Main.hs — the test catches
copy-paste typos in metavar / parser wiring instantly.
test/integration/CLI/CLIE2ESpec.hs runs the actual monoscope binary
against an HTTP server. This is what catches wire-format regressions
(KQL operator changes, JSON envelope drift, missing query params, broken
auth headers) — the bug class that motivated the audit.
The suite reuses the same env vars the CLI itself reads (MONOSCOPE_API_URL /
MONOSCOPE_API_KEY / MONOSCOPE_PROJECT) — so if you've configured your shell
to drive monoscope against any server, the e2e tests automatically pick
that up.
Defaults: when the env vars are unset, the suite targets the public
demo project on prod — MONOSCOPE_API_URL=https://api.monoscope.tech,
MONOSCOPE_PROJECT=00000000-0000-0000-0000-000000000000. Only
MONOSCOPE_API_KEY has no safe default; set it to any read-only key
minted against the demo project to enable the suite. Without a key the
tests still mark themselves pending (with a message naming the missing
variable) rather than fail.
# Run against the prod demo project (default — only the key is needed):
MONOSCOPE_API_KEY=<read-only demo key> \
USE_EXTERNAL_DB=true cabal test integration-tests \
--test-options='--match "CLI binary E2E"'
# Or point at your local dev server:
MONOSCOPE_API_URL=http://localhost:8080 \
MONOSCOPE_API_KEY=$(monoscope --json api-keys create "e2e-tests" | jq -r .key) \
MONOSCOPE_PROJECT=<your project uuid> \
USE_EXTERNAL_DB=true cabal test integration-tests \
--test-options='--match "CLI binary E2E"'CI on main/PRs reads the same MONOSCOPE_API_KEY from the
MONOSCOPE_DEMO_API_KEY repo secret, so every push runs the e2e suite
against prod. PRs from forks (where secrets aren't exposed) gracefully
skip the suite — they still get the CLIBinarySpec smoke tests.
Each spec checks a single audit finding (KQL operator, kind→source mapping, error-body surfacing, list envelope, agent-mode JSON, validation messages, etc.) — adding a new feature? Add the regression test alongside.
- Add the parser entry in
cli/Main.hs(the binary smoke test catches wiring errors automatically). - Implement in
cli/CLI/Commands.hsorcli/CLI/Resource.hsand any pure helper goes incli/CLI/Validate.hs(with doctests). - Add an
it "<feature>" $ withReachableServer $ \cfg -> ...intest/integration/CLI/CLIE2ESpec.hsthat asserts the wire-level behaviour — the JSON envelope shape, the exit code, the error message. - Run the e2e suite locally before merging if the change touches the request URL, the query params, or the response shape.