Monitor Analytics is a fleet-wide view that rolls up every monitor and check type in your organization into a single page — where the per-monitor metrics graph on a monitor’s detail page answers “how is this one monitor doing,” Monitor Analytics answers “how is my whole fleet doing.” Find it under Monitors → Analytics in the sidebar.
What counts as “healthy”
Every number here is based on whether the target was healthy, not just whether the job executed — a Completed job can still represent a failing check (an HTTP 500, an expired certificate, a DNSBL listing). See Job Statuses for the full explanation. Concretely, a job counts as healthy when it’s Completed and its summary reports zero failed checks.
Summary Tiles
At the top of the page: total monitor count, overall uptime over the last 7 days, monitors currently failing, and active alert count — a fleet-wide snapshot at a glance.
MQL Query Console
Below the summary tiles is the MQL Analytics Console — a Splunk-style query language for ad-hoc analysis across every job and check type in your fleet, instead of hunting for the right fixed chart.
There’s no autocomplete yet, so this page is the field reference until there is — keep it open in a tab while you write queries. The 1D / 7D / 2W / 30D / 90D buttons in the console header control the time window for whatever query is currently loaded; queries re-run automatically when you change the range. Results render as a sortable table, or for timechart queries, a line chart.
Query Anatomy
An MQL query is a search clause, optionally followed by one or more pipe commands:
<search clause> | <pipe 1> | <pipe 2> | ...
The search clause sets the data source and filters:
source=jobs status=failed type=http duration_ms>2000
Filters are implicitly ANDed together — there’s no explicit AND keyword, no OR, and no parentheses for grouping. A bare word with no operator (e.g. just typing timeout) is shorthand for error_message LIKE '%timeout%'.
Pipes transform or shape the filtered rows, left to right, same as Splunk/SPL:
source=jobs status=failed | stats count() by error_message | sort count desc | limit 10
Data Sources
source= |
Backing data | Granularity | Use it for |
|---|---|---|---|
jobs (default) |
The jobs table directly |
One row per check execution | Raw investigation — exact error messages, exact result payloads, exact timestamps |
metrics |
The fleet_job_metrics_15m continuous aggregate |
Pre-aggregated in 5-minute buckets | Trends over longer windows (uptime %, avg duration) without scanning every raw job |
source=metrics is much cheaper for wide time ranges (30D/90D) since it’s already bucketed, but it only has the aggregate fields listed below — it can’t reach error_message or any results.* field. type=dmarc jobs are excluded from source=metrics; DMARC has its own reporting pages and API (see the DMARC docs).
Standard Fields
These work for both filtering (in the search clause) and in table/stats ... by/timechart ... by/sort.
source=jobs
| Field | Type | Notes |
|---|---|---|
status |
string | pending, running, completed, failed, cancelled (job execution state — not the same as target health, see above) |
type (alias check_type) |
string | http, dns, ssl, ssh, dnsbl, tcp, udp, ehlo, email_auth, pageload, broken_links, domain_expiry, tls_audit, nmap, page_snapshot |
duration_ms (alias duration) |
number | Job execution time in milliseconds |
error_message (alias error) |
string | Free-text error, when the job or check failed |
created_at |
timestamp | When the job was created/queued |
completed_at |
timestamp | When the job finished |
schedule_id |
uuid | The monitor’s ID |
name (alias schedule_name) |
string | The monitor’s display name |
source=metrics
| Field | Type | Notes |
|---|---|---|
type (alias check_type) |
string | Same values as above |
bucket |
timestamp | Start of the 5-minute aggregation bucket |
total_jobs |
number | Job count in the bucket |
successful_jobs |
number | Jobs where the target passed (see healthy) |
failed_jobs |
number | Jobs where the target failed, or the job itself errored |
avg_duration_ms |
number | Average job duration in the bucket |
schedule_id |
uuid | The monitor’s ID |
name (alias schedule_name) |
string | The monitor’s display name |
Reaching Into Check Results (results.*, source=jobs only)
Every check type’s raw output is stored one level below the top-level results column, nested under a type-specific key. Address a nested field with dot-separated path segments: results.<type_key>.<field>. Comparisons against numeric fields (>, >=, <, <=) are cast automatically.
type= |
results. key |
Useful fields |
|---|---|---|
http |
http_results |
status_code, response_time_ms, success, body, headers.<header-name> |
dns |
dns_results.<record_type>[i] |
Array per record type (A, MX, TXT, …); best queried via table and read in the results view rather than filtered |
ssl |
ssl_result |
is_valid, is_self_signed, expiry, issuer, cipher_suite, protocol_version |
ssh |
ssh_result |
connected, banner, banner_match, version |
dnsbl |
dnsbl_results.<server> |
Map keyed by DNSBL server hostname, e.g. results.dnsbl_results.zen\.spamhaus\.org.listed |
tcp |
tcp_result |
connected, response_time_ms |
udp |
udp_result |
reachable, response_time_ms |
ehlo |
ehlo_results |
connected, ehlo_response, extensions |
email_auth |
email_auth_results |
spf_valid, dmarc_valid, dkim_valid, spf_record, dmarc_record |
pageload |
page_load_result |
ttfb, page_load, fcp, lcp, cls, http_status_code, resource_count |
broken_links |
broken_links_result |
total_links, broken_count, target_url |
domain_expiry |
domain_expiry_result |
days_until_expiry, is_expired, expiry_date, registrar |
page_snapshot |
page_snapshot_result |
http_status_code, size_bytes |
nmap |
nmap_result |
total_scanned, unexpected_open |
tls_audit |
tls_audit_result |
has_weak_ciphers, has_weak_versions, accepted_versions, weak_ciphers |
| (any type) | summary |
total_checks, successful_checks, failed_checks, execution_time_ms |
The dnsbl and dns keys are maps/arrays, not fixed objects — the server hostname or record type is a dynamic map key rather than something you can enumerate here. For those, table results.dnsbl_results | limit 5 and reading the raw JSON is usually faster than trying to build a precise filter.
Getting the path wrong doesn’t error — an incorrect path just extracts nothing (NULL), so a query like results.status_code>=500 (missing the http_results segment) silently matches zero rows instead of failing loudly. If a results.* filter isn’t matching what you expect, double-check the path against the table above first.
Operators
| Operator | Meaning |
|---|---|
= |
Equals |
!= |
Not equals |
>, >=, <, <= |
Numeric/time comparison |
LIKE |
SQL LIKE pattern match — use % as a wildcard, e.g. error_message LIKE '%timeout%' |
NOT LIKE |
Inverse of LIKE |
String and quoted values can use single or double quotes: error_message="expected status 200" or error_message='expected status 200'.
Aggregate Functions
Used inside stats and timechart.
| Function | Meaning |
|---|---|
count() |
Row count. Default aggregate if you don’t specify one. |
avg(field) |
Average |
sum(field) |
Sum |
min(field) |
Minimum |
max(field) |
Maximum |
uptime() |
(healthy jobs / total jobs) * 100 — works with both source=jobs and source=metrics |
Give an aggregate a custom output name with AS: avg(duration_ms) AS avg_latency.
Pipe Commands
| Pipe | Syntax | Notes |
|---|---|---|
stats |
stats <agg>(), <agg>(field) by field1, field2 |
by is optional — omit it to aggregate over the whole result set into a single row |
timechart |
timechart span=15m <agg>() by field |
span accepts <N>m, <N>h, or <N>d (e.g. span=5m, span=1h, span=1d); defaults to 15m if omitted. by is optional. |
table |
table field1, field2, field3 |
Explicit column selection. Omit entirely for a sensible default column set. |
sort |
sort field, sort field desc, sort field asc, sort -field (desc shorthand), sort +field (asc shorthand) |
Defaults to created_at desc / bucket desc if you don’t add a sort pipe |
limit |
limit 50 |
Default 100, capped at 1000 |
Worked Examples
Most common failure messages fleet-wide, worst first:
source=jobs status=failed | stats count() by error_message | sort count desc
HTTP 5xx responses in the last window, with context:
source=jobs type=http results.http_results.status_code>=500 | table schedule_id, duration_ms, error_message
Certificates expiring within 14 days:
source=jobs type=ssl results.ssl_result.expiry<="2026-09-15T00:00:00Z" | table schedule_id, results.ssl_result.expiry, results.ssl_result.issuer
(SSL results store an absolute expiry timestamp, not a precomputed days-remaining, so filter against a date rather than a day count.)
Slowest monitors right now:
source=jobs duration_ms>2000 | table schedule_id, duration_ms, type | sort duration_ms desc | limit 20
Fleet-wide uptime trend, HTTP only, one point per 15 minutes:
source=metrics type=http | timechart span=15m uptime() by name
Average latency per monitor over the last window:
source=jobs | stats avg(duration_ms) AS avg_latency by name | sort avg_latency desc
DMARC/SPF/DKIM auth failures:
source=jobs type=email_auth results.email_auth_results.dmarc_valid=false | table schedule_id, error_message
Anything with “timeout” in the error, excluding a known-flaky monitor:
source=jobs error_message LIKE '%timeout%' name NOT LIKE '%staging%'
Jobs that failed to even execute (runner-level, not target-level):
source=jobs status=failed | table schedule_id, type, error_message, created_at | sort created_at desc
Weak TLS ciphers detected, across the fleet:
source=jobs type=tls_audit results.tls_audit_result.has_weak_ciphers=true | table schedule_id, results.tls_audit_result.weak_ciphers
API Access
The console is backed by GET/POST /v1/monitor-analytics/query — see the API Reference if you want to run MQL queries programmatically or build your own dashboard on top of the same data.