{% extends "base.html" %} {% block title %}{{ s.name }}{% endblock %} {% block content %} {% from '_limit.html' import limit, paused, locked %} {% set host = smp.get('host', {}) %}{% set cpu = smp.get('cpu', {}) %}{% set mem = smp.get('mem', {}) %}{% set sec = smp.get('security', {}) %}{% set dk = smp.get('docker', {}) %}{% set net = smp.get('net', {}) %}{% set procs = smp.get('processes', {}) %}{% set svcs = smp.get('services', {}) %}
Not enough samples in this range yet.
| {{ k }} | {{ v }} |
| {{ p.name }} | {{ p.user }} | {{ '%.1f'|format(p.cpu_pct or 0) }}% | {{ fmt_bytes(p.rss) }} |
| No data |
| By CPU | User | CPU | Mem | RSS |
|---|---|---|---|---|
| {{ p.name }} | {{ p.user }} | {{ '%.1f'|format(p.cpu_pct or 0) }}% | {{ '%.1f'|format(p.mem_pct or 0) }}% | {{ fmt_bytes(p.rss) }} |
| By memory | User | CPU | Mem | RSS |
|---|---|---|---|---|
| {{ p.name }} | {{ p.user }} | {{ '%.1f'|format(p.cpu_pct or 0) }}% | {{ '%.1f'|format(p.mem_pct or 0) }}% | {{ fmt_bytes(p.rss) }} |
| Mount | Device | FS | Used | Total | Usage | Inodes |
|---|---|---|---|---|---|---|
| {{ d.mount }} | {{ d.device }} | {{ d.fs }} | {{ fmt_bytes(d.used) }} | {{ fmt_bytes(d.total) }} | {{ '%.0f'|format(d.pct or 0) }}% | {{ '%.0f'|format(d.inodes_pct) if d.get('inodes_pct') is not none else '—' }}% |
| No disk data yet. | ||||||
| Device | Read | Write |
|---|---|---|
| {{ dev }} | {{ fmt_rate(v.get('read_bps')) }} | {{ fmt_rate(v.get('write_bps')) }} |
| Outgoing to | Process | Conns | Out / in | RTT |
|---|---|---|---|---|
| {{ o.host or o.ip }}:{{ o.port }}{% if o.host %} {{ o.ip }} {% endif %} | {{ o.process or '—' }} | {{ o.conns }} | {{ fmt_bytes(o.bytes_out) }} / {{ fmt_bytes(o.bytes_in) }} | {{ '%.0f ms'|format(o.rtt_ms) if o.rtt_ms else '—' }} |
| No outgoing connections in the last interval. | ||||
| Incoming on | Process | Conns | Clients | Top clients |
|---|---|---|---|---|
| :{{ i.port }} | {{ i.process or '—' }} | {{ i.conns }} | {{ i.clients }} | {% for cl in (i.top_clients or [])[:4] %}{{ cl.ip }} ({{ cl.conns }}){% if not loop.last %}, {% endif %}{% endfor %} |
| No incoming connections in the last interval. | ||||
| Interface | Receive | Transmit | Errors / drops |
|---|---|---|---|
| {{ name }} | {{ fmt_rate(v.get('rx_bps')) }} | {{ fmt_rate(v.get('tx_bps')) }} | {{ (v.get('rx_errors') or 0) + (v.get('tx_errors') or 0) }} / {{ v.get('rx_dropped') or 0 }} |
| No interface data yet. | |||
| Listening port | Proto | Process |
|---|---|---|
| {{ l.port }} | {{ l.proto }} | {{ l.process or '—' }} |
| No listening-port data (agent may need root). | ||
| Endpoint | Served by | Requests | Req/min | 4xx | 5xx | p50 | p95 | Max | Bytes |
|---|---|---|---|---|---|---|---|---|---|
| {{ e.method or '—' }} {{ e.path or ('port ' ~ e.port) }}{% if e.host %} {{ e.host }} {% endif %} | {{ e.process or '—' }}{% if e.port %} :{{ e.port }}{% endif %} {{ e.source }} | {{ e.count }} | {{ e.per_min }} | {{ e.errors_4xx }} | {{ e.errors_5xx }}{% if e.error_pct %} ({{ e.error_pct }}%){% endif %} | {{ '%.0f'|format(e.p50_ms) if e.p50_ms is not none else '—' }} | {{ '%.0f'|format(e.p95_ms) if e.p95_ms is not none else '—' }} | {{ '%.0f'|format(e.max_ms) if e.max_ms is not none else '—' }} | {{ fmt_bytes(e.bytes) }} |
| Destination | From process | Calls | Calls/min | 4xx | 5xx | p50 | p95 | Bytes |
|---|---|---|---|---|---|---|---|---|
| {{ e.host }}:{{ e.port }}{% if e.method %} {{ e.method }} {{ e.path }}{% else %} TLS{% endif %} | {{ e.process or '—' }} | {{ e.count }} | {{ e.per_min }} | {{ e.errors_4xx }} | {{ e.errors_5xx }} | {{ '%.0f'|format(e.p50_ms) if e.p50_ms is not none else '—' }} | {{ '%.0f'|format(e.p95_ms) if e.p95_ms is not none else '—' }} | {{ fmt_bytes(e.bytes) }} |
No HTTP traffic recorded yet in this range. Plaintext HTTP (including behind a reverse proxy on localhost) and TLS destinations are captured automatically by agent 1.1+ running as root; add the SDK for full request details behind TLS.
| When | Dir | Request | Status | Time | Process |
|---|---|---|---|---|---|
| {{ (q.at|int)|string|truncate(10, true, '') }} | {{ q.direction }} | {{ q.method }} {{ q.host }}{{ q.path }} | {{ q.status or '—' }} | {{ q.ms }} ms | {{ q.process or '—' }} |
Two lines in your app give you every request with its outgoing HTTP calls and database queries stitched into a waterfall, route names behind TLS, and error details. Spans go to the local agent (127.0.0.1:9111), never directly to the internet.
curl -fsSL {{ base_url }}/vigil-apm.py -o vigil_apm.py
import vigil_apm
vigil_apm.init(service="checkout-api", app=app) # httpx/requests/urllib, SQLAlchemy, redis auto-instrumentedcurl -fsSL {{ base_url }}/vigil-apm.js -o vigil-apm.js
require('./vigil-apm').init({ service: 'web' }) // http(s), fetch, pg, mysql2, ioredis auto-instrumentedFull SDK docs · traces propagate across services via the W3C traceparent header.
| Endpoint | Service | Calls | Errors | p50 | p95 |
|---|---|---|---|---|---|
| {{ e.name }} | {{ e.service }} | {{ e.count }} | {{ e.errors }} | {{ e.p50_ms }} | {{ e.p95_ms }} |
| Dependency | Kind | Service | Calls | Errors | p95 |
|---|---|---|---|---|---|
| {{ d.target }} | {{ d.kind }} | {{ d.service }} | {{ d.count }} | {{ d.errors }} | {{ d.p95_ms }} |
No traces in the last interval.
{% endfor %} {% endif %} {% elif tab == 'services' %}| {{ name }} | {{ state }} |
| No watched services. Add names under Alert rules (e.g. nginx, postgresql, docker). |
Docker not detected (or the agent user cannot access the socket).
{% else %}| Container | State | CPU | Memory | Restarts |
|---|---|---|---|---|
| {{ c.name }} {{ c.image }} | {{ c.state }} {{ c.status }} | {{ '%.1f'|format(c.cpu_pct) if c.get('cpu_pct') is not none else '—' }}% | {{ fmt_bytes(c.get('mem')) }} | {{ c.restarts or 0 }} |
Everything listening on this machine. Anything not behind the firewall is reachable from the internet.
| {{ l.port }}/{{ l.proto }} | {{ l.process or '—' }} | {% if l.port in (22,) %}SSH{% elif l.port in (80,443) %}web{% elif l.port in (3306,5432,6379,27017,9200) %}database — should not be public{% elif l.port in (2375,) %}Docker API unauthenticated{% endif %} |
| No data (agent may need root for process names). |
Log alert rules (pattern + threshold per window) live under Alert rules → Logs & inventory. Retention: {{ [plan.retention_days, 30]|min }} days.
{% elif tab == 'database' %} {% if not plan.databases %}{{ locked('Database monitoring (PostgreSQL, MySQL, Redis) is on Pro and Enterprise, or the Observability module.') }}{% endif %} {% include "_server_db_tab.html" %} {% elif tab == 'kubernetes' %} {% if not plan.kubernetes %}{{ locked('Kubernetes monitoring is on Pro (3 clusters) and Enterprise, or the Observability module.') }}{% endif %} {% include "_server_k8s_tab.html" %} {% elif tab == 'vulnerabilities' %} {% if not plan.vulns %}{{ locked('Vulnerability scanning against OSV is on Pro and Enterprise, or the Observability module.') }}{% endif %} {% include "_server_vulns_tab.html" %} {% elif tab == 'incidents' %}No incidents.
{% endif %}| {{ a.created_at|timeago }} | {{ a.kind }} | {{ a.title }} {% for k, v in a.delivery.items() %}{{ k.split(':')[0] }}: {{ v }}{% if not loop.last %} · {% endif %}{% endfor %} |
No alerts yet. Delivering to: {% for c in channels if c.id in (s.channel_ids or []) %}{{ c.name }} {% else %}dashboard only (pick channels under Alert rules){% endfor %}
{% endif %}Run as root on the server. Needs Python 3.8+ (installed automatically on Debian/Ubuntu/RHEL/Alpine if missing) and outbound HTTPS to {{ base_url.split('//')[-1] }}. Nothing listens; nothing inbound.
curl -fsSL {{ base_url }}/install-server-agent.sh | sh -s -- --token {{ s.token }} --name "{{ s.name }}"
Installs /usr/local/bin/vigil-server-agent, writes /etc/vigil-agent.json and starts a systemd service (launchd on macOS). Check it with systemctl status vigil-server-agent, logs with journalctl -u vigil-server-agent -f.
# manual
curl -fsSL {{ base_url }}/vigil-server-agent.py -o /usr/local/bin/vigil-server-agent
VIGIL_URL={{ base_url }} VIGIL_TOKEN={{ s.token }} python3 /usr/local/bin/vigil-server-agent --install
# try it once without installing (prints what would be sent)
VIGIL_URL={{ base_url }} VIGIL_TOKEN={{ s.token }} python3 vigil-server-agent.py --print
# Docker host monitoring (mount the host's /proc and docker socket)
docker run -d --name vigil-agent --restart unless-stopped --pid host --network host \
-v /:/host:ro -v /var/run/docker.sock:/var/run/docker.sock:ro \
-e VIGIL_URL={{ base_url }} -e VIGIL_TOKEN={{ s.token }} python:3.12-slim \
sh -c "python3 -c \"import urllib.request;open('a.py','wb').write(urllib.request.urlopen('{{ base_url }}/vigil-server-agent.py').read())\" && python3 a.py"
# uninstall
vigil-server-agent --uninstallRun the agent as a DaemonSet instead: every node reports as its own server (<cluster>/<node>) with its pods, container states and node conditions. Change cluster= to name the cluster.
kubectl apply -f "{{ k8s_manifest }}"
Creates namespace vigil, a read-only ServiceAccount (pods, nodes, nodes/stats), a Secret with this token and the DaemonSet. See Kubernetes.
The agent already captures plaintext HTTP and TLS destinations without code changes. For requests behind TLS, route names, database queries and full traces, add two lines to your app:
# Python
curl -fsSL {{ base_url }}/vigil-apm.py -o vigil_apm.py
import vigil_apm; vigil_apm.init(service="api", app=app)
# Node
curl -fsSL {{ base_url }}/vigil-apm.js -o vigil-apm.js
require('./vigil-apm').init({ service: 'web' })
Spans are sent to the agent on 127.0.0.1:9111 and appear under the Traces tab. Docs.
Edit /etc/vigil-agent.json and restart the agent. Numbers you return become chartable metrics with their own alert limits.
{
"url": "{{ base_url }}",
"token": "{{ s.token }}",
"name": "{{ s.name }}",
"watch_processes": ["nginx", "postgres"],
"watch_services": ["nginx", "postgresql"],
"commands": {
"queue_depth": "redis-cli llen jobs",
"db_connections": "psql -Atc 'select count(*) from pg_stat_activity'"
},
"log_watch": [
{"name": "nginx_5xx", "path": "/var/log/nginx/access.log", "pattern": "\" 5[0-9][0-9] "},
{"name": "app_errors", "path": "/var/log/app/app.log", "pattern": "ERROR|Traceback"}
]
}
Log counts appear as log:<name> metrics. The agent remembers file offsets and handles rotation.