{% 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', {}) %}
Servers / {{ s.name }}

{% if s.online %}{% elif s.online is sameas false %}{% else %}{% endif %}{{ s.name }} {% if s.online is sameas false %}offline{% elif s.online %}online{% else %}not connected{% endif %}{% if muted %}muted{% endif %}{% for t in s.tags or [] %}{{ t }}{% endfor %}

{{ s.hostname or '—' }} · {{ s.os or '—' }} · {{ s.kernel or '' }} {{ s.arch or '' }} · {{ s.cpu_count or '?' }} CPU · {{ fmt_bytes(s.mem_total) }} RAM{% if host.get('virtualization') and host.get('virtualization') not in ('none','unknown') %} · {{ host.virtualization }}{% endif %}{% if s.public_ip %} · {{ s.public_ip }}{% endif %} · up {{ fmt_uptime(host.get('uptime_s')) }} · agent v{{ s.agent_version or '?' }} · last report {{ s.last_seen_at|timeago }}
{% if user.can_edit %}
{% if muted %}{% else %} {% endif %}
{% endif %}
{% for i in open_inc %}
{{ i.title }} · {{ i.started_at|timeago }}{% if i.cause %}
{{ i.cause[:200] }}
{% endif %}
Details
{% endfor %} {% if s.online is none %}
No data yet. Install the agent on the machine: see the Install tab.
{% else %} {% set cpu_v = cpu.get('pct') %}{% set mem_v = mem.get('pct') %}{% set disks = smp.get('disks', []) %}{% set disk_v = (disks|map(attribute='pct')|max) if disks else none %}
CPU
{{ '%.0f'|format(cpu_v) if cpu_v is not none else '—' }}%
load {{ cpu.get('load1', '—') }} · {{ cpu.get('load5', '—') }} · {{ cpu.get('load15', '—') }}{% if cpu.get('iowait') %} · iowait {{ '%.1f'|format(cpu.iowait) }}%{% endif %}{% if cpu.get('steal') %} · steal {{ '%.1f'|format(cpu.steal) }}%{% endif %}
Memory
{{ '%.0f'|format(mem_v) if mem_v is not none else '—' }}%
{{ fmt_bytes(mem.get('used')) }} used · {{ fmt_bytes(mem.get('available')) }} free{% if mem.get('swap_total') %} · swap {{ '%.0f'|format(mem.get('swap_pct') or 0) }}%{% endif %}
Disk (worst mount)
{{ '%.0f'|format(disk_v) if disk_v is not none else '—' }}%
{{ disks|length }} mount{{ 's' if disks|length != 1 }} · r {{ fmt_rate(smp.get('disk_io', {}).get('read_bps')) }} · w {{ fmt_rate(smp.get('disk_io', {}).get('write_bps')) }}
Network
↓ {{ fmt_rate(net.get('rx_bps')) }}
↑ {{ fmt_rate(net.get('tx_bps')) }} · {{ net.get('tcp_established', '—') }} conns · {{ procs.get('total', '—') }} procs
{% endif %}
Overview Processes Disks Network APIs & traffic{% set http_ = smp.get('http', {}) %}{% if http_.get('incoming') or http_.get('outgoing') %} {{ (http_.get('incoming') or [])|length + (http_.get('outgoing') or [])|length }}{% endif %} Traces{% if smp.get('traces', {}).get('sdk_services') %} SDK{% endif %} Services & containers{% if svcs.get('failed') %} {{ svcs.failed|length }}{% endif %} Security{% if sec.get('reboot_required') %} reboot{% elif sec.get('updates_pending') %} {{ sec.updates_pending }}{% endif %} Logs{% if smp.get('logs_ship_dropped') %} {{ smp.logs_ship_dropped }} dropped{% endif %} Databases{% set dbs_ = smp.get('database') or [] %}{% if dbs_|selectattr('ok', 'sameas', false)|list %} down{% elif dbs_ %} {{ dbs_|length }}{% endif %} {% set k8s_ = smp.get('kubernetes') or {} %}{% if k8s_.get('detected') %}Kubernetes {{ (k8s_.get('pods') or [])|length }} pods{% else %}Kubernetes{% endif %} Vulnerabilities Incidents & alerts Alert rules Install
{% if tab == 'overview' %}

Metrics

{% for r in ['1h','6h','24h','7d','30d'] %}{{ r }}{% endfor %}
{% if series.points|length < 2 %}

Not enough samples in this range yet.

{% else %}
{% for key, title, unit, maxv in [('cpu','CPU %','%',100), ('mem','Memory %','%',100), ('load','Load (1 min)','',none), ('io','I/O wait %','%',100), ('disk','Disk usage % (worst mount)','%',100), ('net','Network (rx / tx)','B/s',none), ('dio','Disk I/O (read / write)','B/s',none)] %}
{{ title }}
{% endfor %} {% if latest_custom %}
Custom metrics & log matches (latest)
{% for k, v in latest_custom.items() %}{% endfor %}
{{ k }}{{ v }}
{% endif %}
{% endif %}
{% include "_server_forecast_panel.html" %}

Top processes by CPU

{% for p in (procs.get('top_cpu') or [])[:8] %}{% else %}{% endfor %}
{{ p.name }}{{ p.user }}{{ '%.1f'|format(p.cpu_pct or 0) }}%{{ fmt_bytes(p.rss) }}
No data

Host

Hostname
{{ host.get('fqdn') or s.hostname or '—' }}
OS
{{ s.os or '—' }}
Kernel
{{ s.kernel or '—' }} {{ s.arch or '' }}
CPU
{{ s.cpu_count or '?' }}× {{ host.get('cpu_model') or '' }}
Memory
{{ fmt_bytes(s.mem_total) }}
Booted
{{ s.boot_time|dt }}
Timezone
{{ host.get('timezone') or '—' }}
Agent
v{{ s.agent_version or '?' }} · every {{ s.interval }} s · {{ s.public_ip or '' }}
{% elif tab == 'processes' %}
{% for p in procs.get('top_cpu') or [] %}{% endfor %}
By CPUUserCPUMemRSS
{{ p.name }}
{{ p.cmd }}
{{ p.user }}{{ '%.1f'|format(p.cpu_pct or 0) }}%{{ '%.1f'|format(p.mem_pct or 0) }}%{{ fmt_bytes(p.rss) }}
{% for p in procs.get('top_mem') or [] %}{% endfor %}
By memoryUserCPUMemRSS
{{ p.name }}
{{ p.cmd }}
{{ p.user }}{{ '%.1f'|format(p.cpu_pct or 0) }}%{{ '%.1f'|format(p.mem_pct or 0) }}%{{ fmt_bytes(p.rss) }}
{{ procs.get('total', '—') }} processes · {{ procs.get('running', '—') }} running · {{ procs.get('zombie', 0) }} zombie · watched: {% for p in rules.watch_processes %}{{ p }}{% else %}none (set in Alert rules){% endfor %}
{% elif tab == 'disks' %}
{% for d in smp.get('disks') or [] %}{% else %}{% endfor %}
MountDeviceFSUsedTotalUsageInodes
{{ 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.
{% set dio = smp.get('disk_io', {}) %}
Read
{{ fmt_rate(dio.get('read_bps')) }}
{{ dio.get('read_iops', '—') }} IOPS
Write
{{ fmt_rate(dio.get('write_bps')) }}
{{ dio.get('write_iops', '—') }} IOPS
Utilisation
{{ '%.0f'|format(dio.util_pct) if dio.get('util_pct') is not none else '—' }}%
I/O wait
{{ '%.1f'|format(cpu.iowait) if cpu.get('iowait') is not none else '—' }}%
{% if dio.get('per_device') %}
{% for dev, v in dio.per_device.items() %}{% endfor %}
DeviceReadWrite
{{ dev }}{{ fmt_rate(v.get('read_bps')) }}{{ fmt_rate(v.get('write_bps')) }}
{% endif %} {% elif tab == 'network' %} {% set flows = smp.get('network', {}) %} {% if flows.get('outgoing') or flows.get('incoming') %}

Service map

{{ flows.get('totals', {}).get('incoming_conns', 0) }} incoming · {{ flows.get('totals', {}).get('outgoing_conns', 0) }} outgoing connections · {{ flows.get('totals', {}).get('unique_remote_hosts', 0) }} remote hosts
{% set ins = (flows.get('incoming') or [])[:8] %}{% set outs = (flows.get('outgoing') or [])[:10] %} {% set mid = ([ins|length, outs|length, 3]|max * 44 + 20) / 2 %} {% for i in ins %}{% set y = 30 + loop.index0 * 44 %}:{{ i.port }} {{ i.process or '' }}{{ i.clients }} clients · {{ i.conns }} conns{% endfor %} {{ s.name }}{{ s.hostname or '' }} {% for o in outs %}{% set y = 30 + loop.index0 * 44 %}{{ (o.host or o.ip)[:22] }}:{{ o.port }}{{ o.process or '?' }} · {{ o.conns }} conns{% if o.rtt_ms %} · {{ '%.0f'|format(o.rtt_ms) }} ms{% endif %}{% endfor %}
{% for o in flows.get('outgoing') or [] %}{% else %}{% endfor %}
Outgoing toProcessConnsOut / inRTT
{{ 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.
{% for i in flows.get('incoming') or [] %}{% else %}{% endfor %}
Incoming onProcessConnsClientsTop 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.
{% elif s.online %}
Connection flows need agent 1.1+ (`ss` on Linux). Reinstall with the command on the Install tab to upgrade.
{% endif %}
{% for name, v in (net.get('per_iface') or {}).items() %}{% else %}{% endfor %}
InterfaceReceiveTransmitErrors / 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.
TCP: {{ net.get('tcp_established', '—') }} established · {{ net.get('tcp_time_wait', '—') }} time-wait
{% for l in (net.get('listening') or [])|sort(attribute='port') %}{% else %}{% endfor %}
Listening portProtoProcess
{{ l.port }}{{ l.proto }}{{ l.process or '—' }}
No listening-port data (agent may need root).
{% elif tab == 'apis' %} {% set http_ = smp.get('http', {}) %}
Capture: {{ http_.get('capture', 'agent 1.1+ required') }}{% if http_.get('packets') %} · {{ http_.get('packets') }} packets/interval{% endif %}{% for l in http_.get('logs') or [] %} · log {{ l.format }} ({{ l.lines }} lines){% endfor %} · {{ known_dest }} known destinations
{% for r in ['6h','24h','7d'] %}{{ r }}{% endfor %}
{% if user.can_edit %}
{% endif %}
{% if http_.get('capture') == 'unavailable' %}
Packet capture needs the agent to run as root on Linux. Access-log parsing and the SDK still work.
{% endif %} {% if api_hist and (api_hist.incoming or api_hist.outgoing) %}
Incoming requests · {{ api_hist.hours }}h
{{ api_hist.total_in }}
Outgoing calls · {{ api_hist.hours }}h
{{ api_hist.total_out }}
Endpoints
{{ api_hist.incoming|length }}
External APIs
{{ api_hist.outgoing|length }}

Incoming endpoints ({{ api_hist.hours }}h)

{% for e in api_hist.incoming %}{% endfor %}
EndpointServed byRequestsReq/min4xx5xxp50p95MaxBytes
{{ 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) }}

Outgoing APIs & dependencies ({{ api_hist.hours }}h)

{% for e in api_hist.outgoing %}{% endfor %}
DestinationFrom processCallsCalls/min4xx5xxp50p95Bytes
{{ 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) }}
{% else %}

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.

{% endif %} {% if http_.get('slow') %}

Slowest & failed requests (last interval)

{% for q in http_.slow %}{% endfor %}
WhenDirRequestStatusTimeProcess
{{ (q.at|int)|string|truncate(10, true, '') }}{{ q.direction }}{{ q.method }} {{ q.host }}{{ q.path }}{{ q.status or '—' }}{{ q.ms }} ms{{ q.process or '—' }}
{% endif %} {% elif tab == 'traces' %} {% set tr = smp.get('traces', {}) %} {% if not tr.get('sdk_services') %}

Add the SDK for full traces

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.

Python (FastAPI, Starlette, Flask, Django, any WSGI/ASGI)
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-instrumented
Node.js (Express, Fastify, plain http, fetch)
curl -fsSL {{ base_url }}/vigil-apm.js -o vigil-apm.js

require('./vigil-apm').init({ service: 'web' })   // http(s), fetch, pg, mysql2, ioredis auto-instrumented

Full SDK docs · traces propagate across services via the W3C traceparent header.

{% else %}
Services
{{ tr.sdk_services|join(', ') }}
Spans / interval
{{ tr.get('spans', 0) }}
Endpoints
{{ (tr.get('endpoints') or [])|length }}
Dependencies
{{ (tr.get('dependencies') or [])|length }}
{% for e in tr.get('endpoints') or [] %}{% endfor %}
EndpointServiceCallsErrorsp50p95
{{ e.name }}{{ e.service }}{{ e.count }}{{ e.errors }}{{ e.p50_ms }}{{ e.p95_ms }}
{% for d in tr.get('dependencies') or [] %}{% endfor %}
DependencyKindServiceCallsErrorsp95
{{ d.target }}{{ d.kind }}{{ d.service }}{{ d.count }}{{ d.errors }}{{ d.p95_ms }}

Slowest and failed traces (last interval)

{% for t in tr.get('traces') or [] %}
{{ t.root.name }} {{ t.root.status or '—' }} {{ t.root.ms }} ms · {{ t.service }} · {{ (t.spans or [])|length }} spans · {{ t.trace_id[:16] }} {% set total = [t.root.ms, 1]|max %}
{% for sp in t.spans or [] %}
{{ sp.name }}{{ sp.kind }}
{{ sp.ms }} ms
{% endfor %}
{% else %}

No traces in the last interval.

{% endfor %} {% endif %} {% elif tab == 'services' %}

Services ({{ svcs.get('manager') or 'unknown' }})

{% if svcs.get('failed') %}
Failed: {{ svcs.failed|join(', ') }}
{% else %}
No failed units.
{% endif %} {% for name, state in (svcs.get('watched') or {}).items() %}{% else %}{% endfor %}
{{ name }}{{ state }}
No watched services. Add names under Alert rules (e.g. nginx, postgresql, docker).

Docker containers

{% if not dk.get('available') %}

Docker not detected (or the agent user cannot access the socket).

{% else %}
{% for c in dk.get('containers') or [] %}{% endfor %}
ContainerStateCPUMemoryRestarts
{{ 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 }}
{% endif %}
{% elif tab == 'security' %}

Posture

Pending updates
{% if sec.get('updates_pending') is not none %}{{ sec.updates_pending }}{% if sec.get('security_updates') %} {{ sec.security_updates }} security{% endif %}{% else %}unknown{% endif %}
Reboot required
{% if sec.get('reboot_required') %}yes{% elif sec.get('reboot_required') is sameas false %}no{% else %}unknown{% endif %}
Firewall
{% set fw = sec.get('firewall') or 'unknown' %}{{ fw }}
SSH password auth
{% if sec.get('ssh_password_auth') is sameas true %}enabled prefer keys only{% elif sec.get('ssh_password_auth') is sameas false %}disabled{% else %}unknown{% endif %}
SSH root login
{% set rl = sec.get('ssh_root_login') %}{% if rl == 'yes' %}yes{% elif rl in ('no','prohibit-password') %}{{ rl }}{% else %}unknown{% endif %}
Failed logins (24h)
{{ sec.get('failed_logins_24h') if sec.get('failed_logins_24h') is not none else '—' }}
Users logged in
{{ sec.get('users_logged_in') if sec.get('users_logged_in') is not none else '—' }}

Exposed ports

Everything listening on this machine. Anything not behind the firewall is reachable from the internet.

{% for l in (net.get('listening') or [])|sort(attribute='port') %}{% else %}{% endfor %}
{{ 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).
{% elif tab == 'logs' %} {% if plan.log_files < 10 %}{{ locked('The ' ~ plan.name ~ ' plan ships up to ' ~ plan.log_files ~ ' log files per server and keeps ' ~ (plan.logs_lines // 1000) ~ 'k lines. Pro keeps 1M lines from 50 files.') }}{% endif %}

Recent log lines

Last 100 lines from this server (7 days). {% if smp.get('logs_ship_dropped') %}{{ smp.logs_ship_dropped }} lines dropped by the agent's per-interval cap (raise it under Alert rules).{% endif %}
Search & live tailExport CSV
{% if not (rules.get('log_ship') or rules.get('journal_units')) and not log_lines %}
This server does not ship logs yet. Add file paths or journal units under Alert rules → Logs & inventory; the agent (1.2+) picks them up on its next report and lines appear here within a minute.
{% endif %}
{% with rows = log_lines|reverse|list %}{% include "_log_rows.html" %}{% endwith %} {% if not log_lines %}
No log lines in the last 7 days.
{% endif %}

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' %}

Incidents

{% if incidents %}{% else %}

No incidents.

{% endif %}

Alerts sent

{% if alerts %}
{% for a in alerts %}{% endfor %}
{{ 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 %}
{% else %}

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 %}
{% elif tab == 'rules' %}

Thresholds

Alert when a value stays at or above the limit for the sustain window. Blank = default, 0 = off.

0 = alert on the first sample over the limit.
Custom metrics come from commands and log_watch in /etc/vigil-agent.json on the server (see Install tab).

Watch & notify

Alert immediately when none of that name is found. Prefix match on the process name.
Pushed to the agent automatically; alert when a unit is failed, inactive or missing.

APIs & traffic

Logs & inventory

The agent tails these (new files start at the end) and ships every line to Logs. Stack traces are joined into one line.
50–5000. Lines beyond the cap are counted as dropped.
Alert when the 7-day trend says the disk fills within this many days.
Each rule opens an incident log:<name> when at least threshold shipped lines match the regex pattern (level any|error|warn|info|debug) within window_min minutes, and resolves when the count drops below.
{% for c in channels %}{% else %}
Add a channel first; until then alerts show only in Vigil.
{% endfor %}
{% if user.can_edit %}{% endif %}
{% elif tab == 'install' %}

Install the agent

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 / Docker / uninstall
# 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 --uninstall
{% if user.can_edit %}
{% endif %}

Kubernetes cluster optional

Run 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.

Application tracing SDK optional

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.

Custom metrics & log watching

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.

{% endif %} {% endblock %} {% block scripts %} {% if tab == 'overview' and series.points|length >= 2 %} {% endif %} {% endblock %}