System Health & Prometheus Metrics
The Athena server counts what it is doing: agent connections, heartbeat and database writes,
REST API latency, commands, deployments, agent updates, CPU and memory. It shows those numbers in
two places that read the same counters: the live Settings → System Health tab in
the web console, and a Prometheus /metrics endpoint that Prometheus,
Grafana or any other tool that reads the Prometheus text format can scrape. Because both read the
same numbers, the console and your dashboards always agree.
/metrics is open by default
Out of the box the metrics endpoint listens on all network interfaces, TCP port
9464, over plain HTTP, with no sign-in. This is how Prometheus usually
scrapes a service. The data is limited to counts, durations, versions and days until expiry,
but if you do not want it reachable from your network, restrict it. See
Restricting who can read /metrics.
This page covers the server's own performance counters. For the up/down health checks
(/health, database, PKI, audit and disk status), see
Server Health & Monitoring. For CPU, memory and disk on
managed endpoints, see Resource Metrics & Performance.
Who can see it#
| To | You need | Built-in roles |
|---|---|---|
| Open Settings → System Health | settings.health.view and settings.manage (the Settings page itself requires it) | Admin |
Read the same report over REST (GET /api/metrics/summary) | settings.health.view | Admin, Operator |
| Generate, rotate or revoke the scrape token | settings.manage | Admin |
Scrape /metrics | No Athena account. Access is controlled only by the listener settings and the optional scrape token. | n/a |
Operators hold settings.health.view but cannot open Settings, so they
read System Health over REST. See Permission Model & Role
Bundles.
Opening System Health#
- Open Settings.
- Click the System Health tab, or go straight to
https://<server>:8443/settings/system-health.
While the tab is open, the server sends it new numbers every 10 seconds. The page does not poll. A badge at the top right reads Live with the time of the last update. If the browser loses its live connection, the badge turns amber and reads Live updates paused. The tab keeps the last numbers and reconnects by itself.
Use the 15m / 1h range selector to choose how much history the
charts draw. The server keeps history in memory for the last 60 minutes, at one
point every 10 seconds. Restarting the server clears that history and resets every counter. For
long-term trends, scrape /metrics with Prometheus.
Key indicators#
The tiles at the top summarise the last 5 minutes. A tile turns amber when it crosses the threshold below. p95 means 95 % of the operations finished faster than the value shown.
| Tile | What it shows | Turns amber when |
|---|---|---|
| Agents online | Agents with a live connection, out of the agents registered, plus the licence agent limit and how many are left. | n/a |
| Agent connect p95 | How long agent connections took to finish their start-up work (status, configuration and scan definitions sent). Shows the peak of the last hour. | Above 2 s |
| REST p95 | Response time of the slowest 5 % of API requests. | Above 500 ms |
| Error rate | Share of API requests that failed with a 5xx server error. | Above 1 % |
| Heartbeat batch p95 | How long the database took to write a batch of agent heartbeats, with the current queue length and the number of heartbeats dropped. | Above 1 s |
| CPU | Server process CPU, as a share of all cores. | Above 85 % |
| Memory | Process working set and .NET heap. | n/a |
| Agent registrations | New agent enrolments. | n/a |
| Certificate expiry | Whichever certificate expires first: the web certificate (port 8443), the agent gRPC certificate (port 8444) or the Athena Root CA. | Under 14 days |
| Licence | Days until the installed licence expires. | Under 14 days |
Below the tiles, four charts plot Agents connected (against registered), REST latency, Heartbeat batch duration and CPU & memory over the selected range.
Database, Work and Process panels#
- Database: heartbeat queue, batch p95 and the last batch, heartbeats dropped, write-lock wait p95, inventory writes in flight, inventory write p95, the Agents list query p95, and database size. On the built-in SQLite database every write goes through a single writer, so a growing Heartbeat queue or Write-lock wait means writes are waiting for each other. A Writer behind marker appears in that case. This is most likely when a whole fleet reconnects at once. For fleets like that, use PostgreSQL.
- Work: commands, deployments, agent updates (automatic and pushed, listed separately) and monitor evaluations since the server started, each with OK / Failed / Active counts. It also shows alerts fired and firing now, agent-update failures by reason, and each scheduled job with its last run.
- Process: uptime, GC heap and pause time, thread pool, open console sessions and live-update connections, logins since start (with failures and rate-limit bans), and the days left on each certificate.
- Prometheus endpoint: the listener's address and exposure (public - no auth, loopback only or disabled), whether a scrape token is required, the last scrape time and the number of series. If the listener is not running, the box shows why. This box also has the Copy and token buttons.
Scraping /metrics with Prometheus#
By default the server serves Prometheus text on its own port:
The appliance firewall allows only TCP 8443 and 8444, so port 9464 is closed on the appliance and Prometheus cannot scrape it from outside today.
http://<server>:9464/metrics
This port serves /metrics and nothing else. It is not the web
console port. Do not point Prometheus at 8443/metrics, which only returns the web
page. A minimal scrape configuration:
scrape_configs:
- job_name: athena
scrape_interval: 15s
static_configs:
- targets: ['athena.example.local:9464']
The Copy Prometheus scrape config button in the Prometheus endpoint box on the System Health tab copies a configuration like this for your server.
Counters reset when the server restarts. In Grafana, use rate() or
increase() on any metric whose name ends in _total. Two useful
queries:
# REST p95 over 5 minutes, excluding the long-lived agent stream
histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_seconds_bucket{http_route!~".*Connect"}[5m])))
# Heartbeat batch p95 - the database write bottleneck
histogram_quantile(0.95, sum by (le) (rate(athena_heartbeat_batch_duration_seconds_bucket[5m])))
Useful metrics#
Athena's own metrics start with athena_. Durations end in _seconds, sizes
in _bytes, and counters in _total. Durations are histograms, so they also
have _bucket, _sum and _count series. The endpoint also
carries the standard ASP.NET Core HTTP, Kestrel and SignalR metrics (for example
http_server_request_duration_seconds). On PostgreSQL it adds the Npgsql
connection-pool metrics. A selection:
| Metric | Labels | What it measures |
|---|---|---|
athena_agents_connected | Agents with a live connection now. | |
athena_agents_registered | Agents registered. | |
athena_agents_status | status | Agents by status. |
athena_agent_connects_total | result | Agent connections accepted or refused. |
athena_agent_disconnects_total | reason | Agent connections that ended. |
athena_agent_connect_duration_seconds | Connection accepted to start-up work done. | |
athena_agent_registrations_total | result | Agent enrolment calls. |
athena_heartbeat_batch_duration_seconds | result | Heartbeat batch write time (one transaction per batch). |
athena_heartbeat_queue_length | Heartbeats waiting to be written. | |
athena_heartbeats_dropped_total | Heartbeats dropped because the queue was full. | |
athena_db_write_lock_wait_seconds | Time spent waiting for the database write lock. | |
athena_db_size_bytes | file | Database size on disk. |
athena_logins_total | method, result | REST login attempts. |
athena_commands_total | status | Commands that reached a final status. |
athena_deployments_total | status | Deployment executions that reached a final status. |
athena_agent_updates_total | trigger, result | Agent update pushes, automatic or manual. |
athena_alerts_fired_total | severity | Alerts raised. |
athena_alerts_firing | Alerts firing now. | |
athena_scheduled_job_runs_total | job_type, result | Scheduled job executions. |
athena_process_cpu_seconds_total | Process CPU time. rate() divided by the core count gives CPU %. | |
athena_process_working_set_bytes | Process working set. | |
athena_uptime_seconds | Seconds since the server started. Every counter resets with it. | |
athena_build_info | version, db_provider | Always 1. The labels carry the server version and database provider. |
athena_licence_days_remaining | Days until the licence expires. Absent when no licence is installed. | |
athena_licence_agents_limit | The licence agent limit. | |
athena_certificate_expiry_days | cert = ca, server_web, server_grpc | Days until each certificate expires. |
athena_metrics_scrapes_total | result = ok, unauthorized, forbidden | Requests to /metrics. Refused scrapes show up here. |
For alerting, athena_certificate_expiry_days and
athena_licence_days_remaining let you warn well before an expiry, and a rising
athena_heartbeat_queue_length or athena_heartbeats_dropped_total warns
that the database cannot keep up.
Restricting who can read /metrics#
/metrics exposes only counts, durations, versions, days until expiry and licence
limits. It never contains host names, user names, agent IDs, IP addresses, the
licence customer name or ID, certificate subjects or any secret. Every label comes from a fixed
list, and any other label is dropped before it is published.
To lock it down further, use any combination of the following. All of them are off by default.
| To | Do this |
|---|---|
| Require a token | Generate a scrape token on the System Health tab (see below). From then on every scrape must send it. Scrapes without it get HTTP 401. |
| Allow only some addresses | Set Metrics:AllowedIps to a list of IP addresses or CIDR ranges, for example 10.20.0.0/16. A single address is treated as a /32 (IPv4) or /128 (IPv6). Other addresses get HTTP 403. |
| Serve it over HTTPS | Set Metrics:UseHttps to true. The listener uses the server's own web certificate with TLS 1.2/1.3, so server TLS must be enabled (see Server TLS Certificate). |
| Allow only this machine | Set Metrics:ListenAddress to 127.0.0.1. The default 0.0.0.0 means all interfaces. |
| Change the port | Set Metrics:Port (1–65535). Default 9464. |
| Turn it off | Set Metrics:Enabled to false. |
| Block it at the network | Close TCP 9464 in the host or network firewall. See Network Ports & Firewall. |
When both an allow-list and a token are set, the address is checked first (403), then the token (401).
Setting the Metrics options#
The Metrics:* options go in appsettings.local.json in the server's
data directory (see Data Directories & Backup), or in
environment variables with a double underscore, such as Metrics__ListenAddress. They
take effect after a server restart.
{
"Metrics": {
"Enabled": true,
"ListenAddress": "0.0.0.0",
"Port": 9464,
"UseHttps": true,
"AllowedIps": [ "10.20.0.15", "10.30.0.0/24" ]
}
}
# The same as environment variables (double underscore for the colon)
Metrics__UseHttps=true
Metrics__AllowedIps__0=10.20.0.15
Metrics__AllowedIps__1=10.30.0.0/24
If a Metrics:* value cannot be used (an address that is not an IP address, a
port out of range, an invalid allow-list entry, UseHttps without server TLS, or a
port already in use), only the metrics listener stays down. The rest of Athena starts normally.
The reason is written to the server log and shown in the Prometheus endpoint
box.
Scrape token#
- Open Settings → System Health and find the Prometheus endpoint box.
- Click the Generate / rotate scrape token button.
- Copy the token right away. It is shown once and cannot be displayed again. The server keeps only a hash of it.
- Save the token in a file on the Prometheus host and reference it from the scrape configuration. Every scrape must now send it as a Bearer token.
scrape_configs:
- job_name: athena
scrape_interval: 15s
scheme: https # only if Metrics:UseHttps is true
tls_config:
ca_file: /etc/prometheus/athena-ca.crt # CA that issued the server's web certificate
authorization:
type: Bearer
credentials_file: /etc/prometheus/athena-scrape-token
static_configs:
- targets: ['athena.example.local:9464']
- Takes effect immediately, with no restart. Rotating a token makes the old one stop working at once, so update the Prometheus credentials file and reload Prometheus right after.
- Revoke: click Revoke scrape token next to it.
/metricsis then open again.Metrics:AllowedIpsstill applies. - Per server: the token is not copied to other servers by Settings Sync. Each server has its own token.
REST API#
| Endpoint | Permission | Does |
|---|---|---|
GET /api/metrics/summary | settings.health.view | Returns the System Health report as JSON (tiles, Database, Work and Process panels, and the Prometheus listener state). Optional ?historyMinutes=15 or 60 adds the 10-second chart points. Any other value returns HTTP 400. |
POST /api/metrics/scrape-token | settings.manage | Generates or rotates the scrape token. The response is the only time the token is returned. |
DELETE /api/metrics/scrape-token | settings.manage | Revokes the scrape token. |
Invoke-RestMethod "https://athena.contoso.com:8443/api/metrics/summary?historyMinutes=15" `
-Headers @{ Authorization = "Bearer $token" } -SkipCertificateCheck |
Select-Object -ExpandProperty data
See API Reference for signing in and getting a token.
Audit trail#
| Event type | Recorded when |
|---|---|
SystemHealthViewed | Someone opens Settings → System Health, or reads /api/metrics/summary. |
MetricsScrapeTokenGenerated | A scrape token is generated or rotated. |
MetricsScrapeTokenRevoked | The scrape token is revoked. |
Individual scrapes are counted (athena_metrics_scrapes_total), not audited. See
Audit & SIEM.
Troubleshooting#
| Symptom | What to check |
|---|---|
| Prometheus reports the target as down | At startup the server log shows Metrics: Prometheus endpoint http://…:9464/metrics when the listener is up. If it shows /metrics listener NOT started, the reason follows, usually a port already in use or an invalid Metrics:* setting. Also check the host firewall. See Server Logging. |
| Prometheus gets HTTP 401 | A scrape token is required and the scrape sent none or an old one. Rotate the token, put the new value in the credentials file and reload Prometheus. |
| Prometheus gets HTTP 403 | The Prometheus host is not in Metrics:AllowedIps. |
| Prometheus gets an HTML page | It is scraping the web console port (8443). Use port 9464. |
The log warns refused a /metrics scrape | A scraper is being turned away (wrong address or token). The warning appears at most once a minute. Further refusals are only counted. |
| The tab shows Live updates paused | The browser lost its live connection. The tab reconnects by itself. If it stays paused, reload the page. |
| Charts are empty after a restart | Expected. History is kept in memory only and starts again when the server restarts. |