feat: Claude Code Monitor — lanes, pipelines and a merged workspace

Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 57dc91585d
783 changed files with 221743 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
.data/
.bin/
node_modules/
package-lock.json
+272
View File
@@ -0,0 +1,272 @@
# CCAM monitoring stack (Prometheus + Grafana)
![Prometheus](https://img.shields.io/badge/Prometheus-2.x-E6522C?style=flat-square&logo=prometheus&logoColor=white)
![Grafana](https://img.shields.io/badge/Grafana-10.x-F46800?style=flat-square&logo=grafana&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-optional-2496ED?style=flat-square&logo=docker&logoColor=white)
![Node.js](https://img.shields.io/badge/npm-managed-339933?style=flat-square&logo=nodedotjs&logoColor=white)
A turnkey [Prometheus](https://prometheus.io/) + [Grafana](https://grafana.com/)
stack that scrapes the dashboard's [`GET /api/metrics`](../docs/API.md#metrics)
endpoint and renders **four auto-provisioned Grafana dashboards** (default home:
**CCAM — Overview**). Use it to watch live
sessions, agent states, event throughput, and token burn from the same
observability stack as the rest of your infra.
**No Homebrew, apt, or global installs required.** There is no official `prometheus` or
`grafana` server package on npm (only client libraries and UI components) — the
stack uses npm's own install lifecycle instead: `npm run monitoring:install`
runs `postinstall` in this folder and pulls official release binaries into
`monitoring/.bin/` (same pattern as Playwright browsers or Electron).
### Supported platforms (npm path)
| OS | Architectures |
| --- | --- |
| macOS | Apple Silicon (`arm64`), Intel (`x64`) |
| Linux | `arm64`, `amd64` (`x64`) |
| Windows | `x64` |
Node.js 20+ is the only prerequisite. On Windows, run the npm commands from
PowerShell or Command Prompt in the repo root (same as the rest of CCAM).
## Grafana login (local default)
Open <http://localhost:3000> after `monitoring:up` or `monitoring:docker:up`.
The admin account is **auto-created on first start** — no manual signup:
| Field | Value |
| --- | --- |
| Username | `admin` |
| Password | `admin` |
Credentials are defined in [`grafana.defaults.env`](./grafana.defaults.env) and
applied by both the npm and Docker paths. **CCAM — Overview** is the default
home dashboard (`GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_UID=ccam-overview`) with
PromQL queries against your live `/api/metrics` scrape — no sample or synthetic
data. The Prometheus datasource is pre-provisioned, so you land straight in the
UI after login.
> If login fails after an earlier Grafana run, reset local state:
> `rm -rf monitoring/.data/grafana` (npm) or recreate the `grafana-data` Docker
> volume, then start the stack again.
```
monitoring/
├── package.json # npm install downloads binaries (postinstall)
├── grafana.defaults.env # default admin / admin credentials
├── scripts/ # lifecycle helpers (setup / up / down)
├── prometheus/
│ ├── prometheus-native.yml # scrape config for npm-managed stack
│ ├── prometheus.yml # scrape config for Docker stack
│ ├── prometheus-docker.yml # all-Docker stack (docker-compose.full.yml)
│ ├── ccam-rules.yml # recording rules (derived from live scrapes)
│ └── consoles/index.html # Prometheus CCAM console (pre-built graphs)
├── grafana/
│ ├── provisioning/… # Docker datasource + dashboard provider
│ ├── provisioning-native/… # npm-managed datasource template
│ └── dashboards/ # ccam-overview + 3 focused boards
└── docker-compose.yml # optional Docker path
```
## Quick start (npm — no Docker)
1. **Start the dashboard** on loopback (default `npm start` on port 4820). No
`DASHBOARD_ALLOWED_HOSTS` override is needed — Prometheus scrapes
`127.0.0.1:4820`, which the server's Host guard already accepts.
```bash
npm start
```
2. **One-time install** — uses npm's `postinstall` to download official
Prometheus + Grafana OSS binaries into `monitoring/.bin/` (~150 MB total):
```bash
npm run monitoring:install
```
(`monitoring:setup` is an alias for the same command.)
3. **Bring up the stack:**
```bash
npm run monitoring:up
```
4. **Open Grafana** at <http://localhost:3000> (login `admin` / `admin`). The
**CCAM — Overview** dashboard is already there — no import step. For
Prometheus, open the pre-built **CCAM console** at
<http://localhost:9090/consoles/index.html> (live graphs + tables), or
<http://localhost:9090> → **Consoles** → `index.html` (`Status → Targets`:
`ccam` should be **UP**).
Stop with `npm run monitoring:down`. For a foreground session with logs on the
terminal, use `npm run monitoring:start` instead (Ctrl+C stops both).
## Quick start (Docker / Podman)
Use this when you prefer containers or when the dashboard itself runs in Docker.
1. **Start the dashboard** so the container can scrape it. The server's
DNS-rebinding guard only accepts loopback `Host` headers, and Prometheus (in
Docker) reaches the host as `host.docker.internal` — so allow that Host:
```bash
DASHBOARD_ALLOWED_HOSTS=host.docker.internal npm start
```
When the dashboard runs **inside Docker** (`docker compose up` at the repo
root), set the same variable on the `agent-monitor` service:
```bash
DASHBOARD_ALLOWED_HOSTS=host.docker.internal docker compose up -d
```
> Without this you'll see the Prometheus target stuck **DOWN** with
> `403 EBADHOST`. If you also set `DASHBOARD_TOKEN`, see [Auth](#auth) below.
2. **Bring up the monitoring stack:**
```bash
npm run monitoring:docker:up
```
3. **Open Grafana** at <http://localhost:3000> (login `admin` / `admin`).
Tear down with `npm run monitoring:docker:down`.
### All-in-one Docker (app + monitoring)
Runs the dashboard, Prometheus, and Grafana on one network — no
`host.docker.internal` wiring required:
```bash
npm run docker:full:up
npm run monitoring:verify
```
Stop with `npm run docker:full:down`.
## Deployment options
| Dashboard | Monitoring | Commands |
| --- | --- | --- |
| **npm** (`npm start`) | **npm** | `monitoring:install` → `monitoring:up` |
| **npm** | **Docker** | `DASHBOARD_ALLOWED_HOSTS=host.docker.internal npm start` → `monitoring:docker:up` |
| **Docker** (`docker:up`) | **Docker** | `DASHBOARD_ALLOWED_HOSTS=host.docker.internal docker compose up -d` → `monitoring:docker:up` |
| **Docker** | **npm** | `docker:up` + `monitoring:up` (scrapes host `127.0.0.1:4820`) |
| **Docker full stack** | **included** | `docker:full:up` |
Verify any running stack: `npm run monitoring:verify`.
## npm scripts
| Command | Description |
| --- | --- |
| `npm run monitoring:install` | `npm install` in `monitoring/` — downloads binaries via `postinstall` |
| `npm run monitoring:setup` | Alias for `monitoring:install` |
| `npm run monitoring:up` | Start Prometheus (:9090) + Grafana (:3000) in the background |
| `npm run monitoring:down` | Stop the npm-managed stack |
| `npm run monitoring:start` | Foreground start (Ctrl+C stops both) |
| `npm run monitoring:docker:up` | `docker compose` Prometheus + Grafana |
| `npm run monitoring:docker:down` | Tear down the Docker monitoring stack |
| `npm run monitoring:verify` | Health-check dashboard + Prometheus + Grafana + scrape target |
| `npm run docker:up` | Start the dashboard container only |
| `npm run docker:down` | Stop the dashboard container |
| `npm run docker:full:up` | Dashboard + Prometheus + Grafana (all Docker) |
| `npm run docker:full:down` | Tear down the full Docker stack |
## Bundled Grafana dashboards
Four dashboards are auto-provisioned from `grafana/dashboards/` — all query live
`/api/metrics` data (no sample or synthetic series):
| Dashboard | UID | Focus |
| --- | --- | --- |
| **CCAM — Overview** | `ccam-overview` | Default home — fleet snapshot, totals, breakdowns, rates |
| **CCAM — Sessions & Agents** | `ccam-sessions-agents` | Session lifecycle, agent states, WebSocket clients |
| **CCAM — Tokens & Events** | `ccam-tokens-events` | Cumulative tokens/events, throughput rates, cache efficiency |
| **CCAM — Platform Health** | `ccam-platform` | Scrape/API uptime, process memory, remote sources, build info |
Each board links to the others in the header. After `monitoring:up`, open
http://localhost:3000/dashboards or land on **Overview** as the home dashboard.
## What's on the Overview dashboard
The bundled **CCAM — Overview** board uses only metrics from your database.
**Cumulative stat panels** (total sessions, events, tokens) show substantial
numbers on the first scrape; **rate panels** need a few minutes of scrape
history before lines appear.
| Section | Example queries |
| --- | --- |
| Live fleet | `ccam_sessions{status="active"}`, `ccam_agents{status="working"}`, `ccam_websocket_clients`, `sum(ccam_sessions)` |
| Database totals | `ccam_sessions{status="completed"}`, `ccam_events_total`, `ccam_tokens_total`, `sum(ccam_tokens_total)` |
| Breakdown | `ccam_sessions` (pie), `ccam_tokens_total` (bar gauge) |
| Over time | `ccam_sessions`, `ccam_events_total`, `ccam_tokens_total` |
| Rates | `rate(ccam_events_total[5m])`, `rate(ccam_tokens_total[5m])` |
| Process | `ccam_process_uptime_seconds`, `ccam_process_resident_memory_bytes`, `ccam_build_info` |
See [`docs/API.md` → Metrics](../docs/API.md#metrics) for the full metric list.
## Prometheus quick start
**Open the CCAM console first** — static HTML that queries Prometheus directly
(Prometheus 3.x compatible; no deprecated console template libraries):
**http://localhost:9090/consoles/index.html**
Also reachable from the Prometheus UI menu: **Consoles → index.html**.
The console runs real PromQL against your scraped metrics: session totals,
cumulative events/tokens, working agents, and drill-down links into the Graph UI.
If the page warns about a missing scrape target, start CCAM on port 4820 and
wait ~15s for the first poll.
### Graph tab bookmarks
Open [http://localhost:9090](http://localhost:9090) and paste any of these into
the **Graph** tab (all read live scraped data — never seeded):
| Query | What it shows |
| --- | --- |
| `sum(ccam_sessions)` | Total sessions across all statuses |
| `ccam_events_total` | Cumulative hook events in the database |
| `sum(ccam_tokens_total)` | All token kinds combined |
| `ccam:sessions:total` | Same as above via [recording rule](./prometheus/ccam-rules.yml) |
| `ccam:events:rate5m` | Events per second (5m window) |
Pre-filled graph links (bookmark these):
- [Total sessions](http://localhost:9090/graph?g0.expr=sum(ccam_sessions)&g0.tab=0)
- [Total events](http://localhost:9090/graph?g0.expr=ccam_events_total&g0.tab=0)
- [Token totals by kind](http://localhost:9090/graph?g0.expr=ccam_tokens_total&g0.tab=0)
## Configuration
- **npm-managed stack.** Scrape target lives in
[`prometheus/prometheus-native.yml`](./prometheus/prometheus-native.yml)
(default `127.0.0.1:4820`). Binary versions are pinned in
[`scripts/paths.js`](./scripts/paths.js).
- **Docker stack.** Host-native dashboard: [`prometheus/prometheus.yml`](./prometheus/prometheus.yml)
(`host.docker.internal:4820`). All-Docker stack: [`prometheus/prometheus-docker.yml`](./prometheus/prometheus-docker.yml)
(`agent-monitor:4820`, used by `docker-compose.full.yml`).
- **<a id="auth"></a>Auth (`DASHBOARD_TOKEN`).** If the server requires a token,
uncomment the `authorization` block in the relevant prometheus config and set
`credentials` to your `DASHBOARD_TOKEN`. (The `DASHBOARD_ALLOWED_HOSTS` step is
still required for Docker scrapes — the Host guard runs independently of the
token.)
- **Scrape interval** lives in the prometheus config (`global.scrape_interval`).
- **Grafana admin password** — defaults to `admin` / `admin` via
[`grafana.defaults.env`](./grafana.defaults.env) (Docker) and `grafanaAdminEnv()`
in [`scripts/paths.js`](./scripts/paths.js) (npm). Applied on **first start**
only; wipe `monitoring/.data/grafana` or the `grafana-data` volume to re-seed.
## Security note
`/api/metrics` exposes aggregate operational counts (session/agent tallies, event
and token totals, uptime) — no prompts, transcripts, costs, or secrets. It sits
behind the same loopback/Host guard and optional `DASHBOARD_TOKEN` as the rest of
the API; scraping is only possible once you explicitly allow the scraper's Host
(and token, if set). Keep Grafana/Prometheus on a trusted network or behind your
own reverse proxy.
+48
View File
@@ -0,0 +1,48 @@
# Turnkey Prometheus + Grafana stack for the CCAM `/api/metrics` endpoint.
#
# cd monitoring && docker compose up -d
# open http://localhost:3000 (admin / admin)
#
# The "CCAM — Overview" dashboard and the Prometheus datasource are
# pre-provisioned, so there is nothing to click after start. Prometheus scrapes
# the dashboard running on the HOST (a plain `npm start` on :4820) via
# host.docker.internal — see ./prometheus/prometheus.yml to change the target
# or add a bearer token when DASHBOARD_TOKEN is set. See ./README.md.
services:
prometheus:
image: prom/prometheus:latest
container_name: ccam-prometheus
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.enable-lifecycle
- --web.console.templates=/etc/prometheus/consoles
volumes:
- ./prometheus:/etc/prometheus:ro
- prometheus-data:/prometheus
ports:
- "127.0.0.1:9090:9090"
# Lets the container reach a CCAM server running on the host (Linux needs the
# explicit host-gateway mapping; Docker Desktop resolves it natively).
extra_hosts:
- "host.docker.internal:host-gateway"
grafana:
image: grafana/grafana:latest
container_name: ccam-grafana
restart: unless-stopped
depends_on:
- prometheus
env_file:
- ./grafana.defaults.env
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
ports:
- "127.0.0.1:3000:3000"
volumes:
prometheus-data:
grafana-data:
+6
View File
@@ -0,0 +1,6 @@
# Default local Grafana credentials — applied on first start (npm or Docker).
# Shared by monitoring/docker-compose.yml, docker-compose.full.yml, and start.js.
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=admin
GF_USERS_ALLOW_SIGN_UP=false
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_UID=ccam-overview
@@ -0,0 +1,855 @@
{
"uid": "ccam-overview",
"title": "CCAM — Overview",
"description": "Live metrics scraped from GET /api/metrics on each 15s interval. Every value is read from your SQLite database at scrape time — no sample or synthetic data.",
"tags": ["ccam", "claude-code"],
"timezone": "browser",
"schemaVersion": 39,
"version": 2,
"editable": true,
"refresh": "15s",
"time": { "from": "now-24h", "to": "now" },
"links": [
{
"title": "Sessions & Agents",
"type": "link",
"url": "/d/ccam-sessions-agents/ccam-sessions-agents",
"keepTime": true
},
{
"title": "Tokens & Events",
"type": "link",
"url": "/d/ccam-tokens-events/ccam-tokens-events",
"keepTime": true
},
{
"title": "Platform Health",
"type": "link",
"url": "/d/ccam-platform/ccam-platform",
"keepTime": true
},
{
"title": "Prometheus — total sessions",
"type": "link",
"icon": "external link",
"url": "http://localhost:9090/graph?g0.expr=sum(ccam_sessions)&g0.tab=0",
"targetBlank": true,
"keepTime": true
},
{
"title": "Prometheus — event total",
"type": "link",
"icon": "external link",
"url": "http://localhost:9090/graph?g0.expr=ccam_events_total&g0.tab=0",
"targetBlank": true,
"keepTime": true
}
],
"templating": { "list": [] },
"annotations": { "list": [] },
"panels": [
{
"id": 100,
"type": "row",
"title": "Live fleet",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }
},
{
"id": 1,
"type": "stat",
"title": "Active sessions",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_sessions{status=\"active\"}",
"legendFormat": "active"
}
]
},
{
"id": 2,
"type": "stat",
"title": "Working agents",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_agents{status=\"working\"}",
"legendFormat": "working"
}
]
},
{
"id": 3,
"type": "stat",
"title": "Realtime clients",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_websocket_clients",
"legendFormat": "clients"
}
]
},
{
"id": 4,
"type": "stat",
"title": "Enabled remote sources",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "none",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_remote_sources{enabled=\"true\"}",
"legendFormat": "enabled"
}
]
},
{
"id": 12,
"type": "stat",
"title": "Scrape healthy",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "background",
"graphMode": "none",
"textMode": "value",
"justifyMode": "center"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"type": "value",
"options": {
"0": { "text": "DOWN", "color": "red" },
"1": { "text": "UP", "color": "green" }
}
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_up",
"legendFormat": "up"
}
]
},
{
"id": 13,
"type": "stat",
"title": "Total sessions",
"description": "Sum of all session statuses in the database (active + completed + error + abandoned).",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "orange", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "sum(ccam_sessions)",
"legendFormat": "total"
}
]
},
{
"id": 101,
"type": "row",
"title": "Database totals (cumulative — from your SQLite history)",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }
},
{
"id": 14,
"type": "stat",
"title": "Completed sessions",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 6 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_sessions{status=\"completed\"}",
"legendFormat": "completed"
}
]
},
{
"id": 15,
"type": "stat",
"title": "Error sessions",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 6 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_sessions{status=\"error\"}",
"legendFormat": "error"
}
]
},
{
"id": 16,
"type": "stat",
"title": "Total events",
"description": "Monotonic counter since the database was created.",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 6 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_events_total",
"legendFormat": "events"
}
]
},
{
"id": 17,
"type": "stat",
"title": "Input tokens",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 6 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "yellow", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_tokens_total{kind=\"input\"}",
"legendFormat": "input"
}
]
},
{
"id": 18,
"type": "stat",
"title": "Output tokens",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 6 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "orange", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_tokens_total{kind=\"output\"}",
"legendFormat": "output"
}
]
},
{
"id": 19,
"type": "stat",
"title": "All tokens (sum)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 6 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "sum(ccam_tokens_total)",
"legendFormat": "tokens"
}
]
},
{
"id": 102,
"type": "row",
"title": "Breakdown",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 10 }
},
{
"id": 20,
"type": "piechart",
"title": "Sessions by status",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 11 },
"options": {
"legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] },
"pieType": "donut",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"tooltip": { "mode": "single" }
},
"fieldConfig": {
"defaults": { "unit": "none", "decimals": 0 },
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_sessions",
"legendFormat": "{{status}}",
"instant": true
}
]
},
{
"id": 21,
"type": "bargauge",
"title": "Token totals by kind",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 11 },
"options": {
"orientation": "horizontal",
"displayMode": "gradient",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"showUnfilled": true
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"color": { "mode": "palette-classic" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_tokens_total",
"legendFormat": "{{kind}}",
"instant": true
}
]
},
{
"id": 22,
"type": "bargauge",
"title": "Agents by status",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 11 },
"options": {
"orientation": "horizontal",
"displayMode": "gradient",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"showUnfilled": true
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0,
"color": { "mode": "palette-classic" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_agents",
"legendFormat": "{{status}}",
"instant": true
}
]
},
{
"id": 103,
"type": "row",
"title": "Over time",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 19 }
},
{
"id": 5,
"type": "timeseries",
"title": "Sessions by status",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [] },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"fieldConfig": {
"defaults": {
"unit": "none",
"color": { "mode": "palette-classic" },
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10,
"showPoints": "never",
"stacking": { "mode": "none", "group": "A" }
}
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_sessions",
"legendFormat": "{{status}}"
}
]
},
{
"id": 6,
"type": "timeseries",
"title": "Agents by status",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [] },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"fieldConfig": {
"defaults": {
"unit": "none",
"color": { "mode": "palette-classic" },
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10,
"showPoints": "never",
"stacking": { "mode": "none", "group": "A" }
}
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_agents",
"legendFormat": "{{status}}"
}
]
},
{
"id": 23,
"type": "timeseries",
"title": "Cumulative events (database total)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 28 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] },
"tooltip": { "mode": "single", "sort": "none" }
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0,
"color": { "mode": "palette-classic" },
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 20,
"showPoints": "never",
"stacking": { "mode": "none", "group": "A" }
}
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_events_total",
"legendFormat": "events"
}
]
},
{
"id": 24,
"type": "timeseries",
"title": "Cumulative tokens by kind",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 28 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"fieldConfig": {
"defaults": {
"unit": "short",
"color": { "mode": "palette-classic" },
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10,
"showPoints": "never",
"stacking": { "mode": "none", "group": "A" }
}
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_tokens_total",
"legendFormat": "{{kind}}"
}
]
},
{
"id": 104,
"type": "row",
"title": "Rates (need a few scrape intervals)",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 36 }
},
{
"id": 7,
"type": "timeseries",
"title": "Event throughput (events/sec)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 37 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [] },
"tooltip": { "mode": "single", "sort": "none" }
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"color": { "mode": "palette-classic" },
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 15,
"showPoints": "never"
}
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "rate(ccam_events_total[5m])",
"legendFormat": "events/s"
}
]
},
{
"id": 8,
"type": "timeseries",
"title": "Token usage rate by kind (tokens/sec)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 37 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [] },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"fieldConfig": {
"defaults": {
"unit": "none",
"color": { "mode": "palette-classic" },
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "rate(ccam_tokens_total[5m])",
"legendFormat": "{{kind}}"
}
]
},
{
"id": 105,
"type": "row",
"title": "Process",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 45 }
},
{
"id": 9,
"type": "stat",
"title": "Server uptime",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 6, "w": 6, "x": 0, "y": 46 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "none",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "s",
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_process_uptime_seconds",
"legendFormat": "uptime"
}
]
},
{
"id": 10,
"type": "stat",
"title": "Server memory (RSS)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 6, "w": 6, "x": 6, "y": 46 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"colorMode": "value",
"graphMode": "area",
"textMode": "auto",
"justifyMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] }
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_process_resident_memory_bytes",
"legendFormat": "rss"
}
]
},
{
"id": 11,
"type": "table",
"title": "Build info",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 6, "w": 12, "x": 12, "y": 46 },
"options": { "showHeader": true },
"fieldConfig": {
"defaults": {},
"overrides": [
{
"matcher": { "id": "byName", "options": "Value" },
"properties": [{ "id": "custom.hidden", "value": true }]
},
{
"matcher": { "id": "byName", "options": "Time" },
"properties": [{ "id": "custom.hidden", "value": true }]
},
{
"matcher": { "id": "byName", "options": "job" },
"properties": [{ "id": "custom.hidden", "value": true }]
}
]
},
"targets": [
{
"refId": "A",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"expr": "ccam_build_info",
"format": "table",
"instant": true
}
]
}
]
}
@@ -0,0 +1,401 @@
{
"uid": "ccam-platform",
"title": "CCAM — Platform Health",
"description": "Scrape health, process resources, remote sources, and build info from live metrics.",
"tags": ["ccam", "claude-code", "platform", "health"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"editable": true,
"refresh": "15s",
"time": { "from": "now-24h", "to": "now" },
"links": [
{
"title": "Overview",
"type": "link",
"url": "/d/ccam-overview/ccam-overview",
"keepTime": true
},
{
"title": "Sessions & Agents",
"type": "link",
"url": "/d/ccam-sessions-agents/ccam-sessions-agents",
"keepTime": true
},
{
"title": "Tokens & Events",
"type": "link",
"url": "/d/ccam-tokens-events/ccam-tokens-events",
"keepTime": true
}
],
"templating": { "list": [] },
"annotations": { "list": [] },
"panels": [
{
"id": 100,
"type": "row",
"title": "Availability",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }
},
{
"id": 1,
"type": "stat",
"title": "Prometheus scrape",
"description": "up{job=\"ccam\"} — 1 when Prometheus successfully scrapes /api/metrics.",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "background",
"graphMode": "none",
"textMode": "value"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"type": "value",
"options": {
"0": { "text": "DOWN", "color": "red" },
"1": { "text": "UP", "color": "green" }
}
}
],
"thresholds": {
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"targets": [{ "refId": "A", "expr": "up{job=\"ccam\"}", "legendFormat": "scrape" }]
},
{
"id": 2,
"type": "stat",
"title": "CCAM API",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "background",
"graphMode": "none",
"textMode": "value"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"type": "value",
"options": {
"0": { "text": "DOWN", "color": "red" },
"1": { "text": "UP", "color": "green" }
}
}
],
"thresholds": {
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"targets": [{ "refId": "A", "expr": "ccam_up", "legendFormat": "api" }]
},
{
"id": 3,
"type": "stat",
"title": "Server uptime",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "s",
"thresholds": { "steps": [{ "color": "green", "value": null }] }
}
},
"targets": [{ "refId": "A", "expr": "ccam_process_uptime_seconds", "legendFormat": "uptime" }]
},
{
"id": 4,
"type": "stat",
"title": "Memory (RSS)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"thresholds": { "steps": [{ "color": "blue", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_process_resident_memory_bytes", "legendFormat": "rss" }
]
},
{
"id": 5,
"type": "timeseries",
"title": "Scrape health over time",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 7, "w": 12, "x": 0, "y": 6 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"min": 0,
"max": 1,
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 30,
"showPoints": "never",
"spanNulls": false
}
},
"overrides": [
{
"matcher": { "id": "byName", "options": "scrape" },
"properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }]
},
{
"matcher": { "id": "byName", "options": "api" },
"properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }]
}
]
},
"targets": [
{ "refId": "A", "expr": "up{job=\"ccam\"}", "legendFormat": "scrape" },
{ "refId": "B", "expr": "ccam_up", "legendFormat": "api" }
]
},
{
"id": 6,
"type": "timeseries",
"title": "Process memory (RSS)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 7, "w": 12, "x": 12, "y": 6 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"unit": "bytes",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 20,
"showPoints": "never"
},
"color": { "mode": "fixed", "fixedColor": "blue" }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_process_resident_memory_bytes", "legendFormat": "rss" }
]
},
{
"id": 101,
"type": "row",
"title": "Realtime & federation",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 }
},
{
"id": 7,
"type": "stat",
"title": "WebSocket clients",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 14 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"thresholds": { "steps": [{ "color": "purple", "value": null }] }
}
},
"targets": [{ "refId": "A", "expr": "ccam_websocket_clients", "legendFormat": "clients" }]
},
{
"id": 8,
"type": "stat",
"title": "Remote sources (enabled)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 14 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "none"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"thresholds": { "steps": [{ "color": "green", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_remote_sources{enabled=\"true\"}", "legendFormat": "enabled" }
]
},
{
"id": 9,
"type": "stat",
"title": "Remote sources (disabled)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 14 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "none"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"thresholds": { "steps": [{ "color": "orange", "value": null }] }
}
},
"targets": [
{
"refId": "A",
"expr": "ccam_remote_sources{enabled=\"false\"}",
"legendFormat": "disabled"
}
]
},
{
"id": 10,
"type": "stat",
"title": "Hook activity (events/s)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 14 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"decimals": 3,
"thresholds": { "steps": [{ "color": "blue", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "rate(ccam_events_total[5m])", "legendFormat": "events/s" }
]
},
{
"id": 11,
"type": "timeseries",
"title": "WebSocket clients over time",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 7, "w": 12, "x": 0, "y": 18 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 25,
"showPoints": "never"
},
"color": { "mode": "fixed", "fixedColor": "purple" }
}
},
"targets": [{ "refId": "A", "expr": "ccam_websocket_clients", "legendFormat": "clients" }]
},
{
"id": 12,
"type": "timeseries",
"title": "Remote data sources",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 7, "w": 12, "x": 12, "y": 18 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 15,
"showPoints": "never"
},
"color": { "mode": "palette-classic" }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_remote_sources", "legendFormat": "enabled={{enabled}}" }
]
},
{
"id": 102,
"type": "row",
"title": "Build",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 25 }
},
{
"id": 13,
"type": "table",
"title": "CCAM build info",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 6, "w": 12, "x": 0, "y": 26 },
"options": { "showHeader": true },
"fieldConfig": {
"defaults": {},
"overrides": [
{
"matcher": { "id": "byName", "options": "Value" },
"properties": [{ "id": "custom.hidden", "value": true }]
},
{
"matcher": { "id": "byName", "options": "Time" },
"properties": [{ "id": "custom.hidden", "value": true }]
},
{
"matcher": { "id": "byName", "options": "job" },
"properties": [{ "id": "custom.hidden", "value": true }]
},
{
"matcher": { "id": "byName", "options": "instance" },
"properties": [{ "id": "custom.hidden", "value": true }]
}
]
},
"targets": [{ "refId": "A", "expr": "ccam_build_info", "format": "table", "instant": true }]
},
{
"id": 14,
"type": "timeseries",
"title": "Server uptime",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 6, "w": 12, "x": 12, "y": 26 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10,
"showPoints": "never"
}
}
},
"targets": [{ "refId": "A", "expr": "ccam_process_uptime_seconds", "legendFormat": "uptime" }]
}
]
}
@@ -0,0 +1,371 @@
{
"uid": "ccam-sessions-agents",
"title": "CCAM — Sessions & Agents",
"description": "Session lifecycle and agent fleet metrics from live /api/metrics scrapes.",
"tags": ["ccam", "claude-code", "sessions", "agents"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"editable": true,
"refresh": "15s",
"time": { "from": "now-24h", "to": "now" },
"links": [
{
"title": "Overview",
"type": "link",
"url": "/d/ccam-overview/ccam-overview",
"keepTime": true
},
{
"title": "Tokens & Events",
"type": "link",
"url": "/d/ccam-tokens-events/ccam-tokens-events",
"keepTime": true
},
{
"title": "Platform Health",
"type": "link",
"url": "/d/ccam-platform/ccam-platform",
"keepTime": true
}
],
"templating": { "list": [] },
"annotations": { "list": [] },
"panels": [
{
"id": 100,
"type": "row",
"title": "Sessions",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }
},
{
"id": 1,
"type": "stat",
"title": "Active",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "steps": [{ "color": "green", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_sessions{status=\"active\"}", "legendFormat": "active" }
]
},
{
"id": 2,
"type": "stat",
"title": "Completed",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "steps": [{ "color": "blue", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_sessions{status=\"completed\"}", "legendFormat": "completed" }
]
},
{
"id": 3,
"type": "stat",
"title": "Error",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "steps": [{ "color": "red", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_sessions{status=\"error\"}", "legendFormat": "error" }
]
},
{
"id": 4,
"type": "stat",
"title": "Abandoned",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"color": { "mode": "thresholds" },
"thresholds": { "steps": [{ "color": "orange", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_sessions{status=\"abandoned\"}", "legendFormat": "abandoned" }
]
},
{
"id": 5,
"type": "stat",
"title": "Total sessions",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none",
"color": { "mode": "thresholds" },
"thresholds": { "steps": [{ "color": "purple", "value": null }] }
}
},
"targets": [{ "refId": "A", "expr": "sum(ccam_sessions)", "legendFormat": "total" }]
},
{
"id": 6,
"type": "stat",
"title": "Active % of total",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "none"
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"decimals": 1,
"min": 0,
"max": 1,
"color": { "mode": "thresholds" },
"thresholds": { "steps": [{ "color": "green", "value": null }] }
}
},
"targets": [
{
"refId": "A",
"expr": "ccam_sessions{status=\"active\"} / clamp_min(sum(ccam_sessions), 1)",
"legendFormat": "active ratio"
}
]
},
{
"id": 7,
"type": "piechart",
"title": "Sessions by status",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 9, "w": 8, "x": 0, "y": 5 },
"options": {
"legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] },
"pieType": "donut",
"reduceOptions": { "calcs": ["lastNotNull"] }
},
"fieldConfig": { "defaults": { "decimals": 0 } },
"targets": [
{ "refId": "A", "expr": "ccam_sessions", "legendFormat": "{{status}}", "instant": true }
]
},
{
"id": 8,
"type": "timeseries",
"title": "Sessions over time",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 9, "w": 16, "x": 8, "y": 5 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom" },
"tooltip": { "mode": "multi" }
},
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 12,
"showPoints": "never"
},
"color": { "mode": "palette-classic" }
}
},
"targets": [{ "refId": "A", "expr": "ccam_sessions", "legendFormat": "{{status}}" }]
},
{
"id": 101,
"type": "row",
"title": "Agents",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 }
},
{
"id": 9,
"type": "stat",
"title": "Working",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 15 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"thresholds": { "steps": [{ "color": "green", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_agents{status=\"working\"}", "legendFormat": "working" }
]
},
{
"id": 10,
"type": "stat",
"title": "Waiting",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 15 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"thresholds": { "steps": [{ "color": "yellow", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_agents{status=\"waiting\"}", "legendFormat": "waiting" }
]
},
{
"id": 11,
"type": "stat",
"title": "Completed",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 15 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"thresholds": { "steps": [{ "color": "blue", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_agents{status=\"completed\"}", "legendFormat": "completed" }
]
},
{
"id": 12,
"type": "stat",
"title": "Error",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 15 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"thresholds": { "steps": [{ "color": "red", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_agents{status=\"error\"}", "legendFormat": "error" }
]
},
{
"id": 13,
"type": "bargauge",
"title": "Agents by status",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 19 },
"options": {
"orientation": "horizontal",
"displayMode": "gradient",
"reduceOptions": { "calcs": ["lastNotNull"] }
},
"fieldConfig": { "defaults": { "decimals": 0, "color": { "mode": "palette-classic" } } },
"targets": [
{ "refId": "A", "expr": "ccam_agents", "legendFormat": "{{status}}", "instant": true }
]
},
{
"id": 14,
"type": "timeseries",
"title": "Agents over time",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 16, "x": 8, "y": 19 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom" },
"tooltip": { "mode": "multi" }
},
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10,
"showPoints": "never"
},
"color": { "mode": "palette-classic" }
}
},
"targets": [{ "refId": "A", "expr": "ccam_agents", "legendFormat": "{{status}}" }]
},
{
"id": 15,
"type": "timeseries",
"title": "Realtime dashboard clients",
"description": "WebSocket clients connected to the CCAM UI.",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 7, "w": 24, "x": 0, "y": 27 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 20,
"showPoints": "never"
},
"color": { "mode": "fixed", "fixedColor": "purple" }
}
},
"targets": [{ "refId": "A", "expr": "ccam_websocket_clients", "legendFormat": "clients" }]
}
]
}
@@ -0,0 +1,350 @@
{
"uid": "ccam-tokens-events",
"title": "CCAM — Tokens & Events",
"description": "Cumulative and rate views of hook events and token usage from live /api/metrics scrapes.",
"tags": ["ccam", "claude-code", "tokens", "events"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"editable": true,
"refresh": "15s",
"time": { "from": "now-24h", "to": "now" },
"links": [
{
"title": "Overview",
"type": "link",
"url": "/d/ccam-overview/ccam-overview",
"keepTime": true
},
{
"title": "Sessions & Agents",
"type": "link",
"url": "/d/ccam-sessions-agents/ccam-sessions-agents",
"keepTime": true
},
{
"title": "Platform Health",
"type": "link",
"url": "/d/ccam-platform/ccam-platform",
"keepTime": true
}
],
"templating": { "list": [] },
"annotations": { "list": [] },
"panels": [
{
"id": 100,
"type": "row",
"title": "Cumulative totals",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }
},
{
"id": 1,
"type": "stat",
"title": "Total events",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0,
"thresholds": { "steps": [{ "color": "blue", "value": null }] }
}
},
"targets": [{ "refId": "A", "expr": "ccam_events_total", "legendFormat": "events" }]
},
{
"id": 2,
"type": "stat",
"title": "Input tokens",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 5, "x": 4, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"thresholds": { "steps": [{ "color": "yellow", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_tokens_total{kind=\"input\"}", "legendFormat": "input" }
]
},
{
"id": 3,
"type": "stat",
"title": "Output tokens",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 5, "x": 9, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"thresholds": { "steps": [{ "color": "orange", "value": null }] }
}
},
"targets": [
{ "refId": "A", "expr": "ccam_tokens_total{kind=\"output\"}", "legendFormat": "output" }
]
},
{
"id": 4,
"type": "stat",
"title": "Cache read",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 5, "x": 14, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"thresholds": { "steps": [{ "color": "green", "value": null }] }
}
},
"targets": [
{
"refId": "A",
"expr": "ccam_tokens_total{kind=\"cache_read\"}",
"legendFormat": "cache_read"
}
]
},
{
"id": 5,
"type": "stat",
"title": "Cache write",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 4, "w": 5, "x": 19, "y": 1 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"thresholds": { "steps": [{ "color": "teal", "value": null }] }
}
},
"targets": [
{
"refId": "A",
"expr": "ccam_tokens_total{kind=\"cache_write\"}",
"legendFormat": "cache_write"
}
]
},
{
"id": 6,
"type": "piechart",
"title": "Token share by kind",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 9, "w": 8, "x": 0, "y": 5 },
"options": {
"legend": { "displayMode": "table", "placement": "right", "values": ["value", "percent"] },
"pieType": "pie",
"reduceOptions": { "calcs": ["lastNotNull"] }
},
"fieldConfig": { "defaults": { "unit": "short" } },
"targets": [
{ "refId": "A", "expr": "ccam_tokens_total", "legendFormat": "{{kind}}", "instant": true }
]
},
{
"id": 7,
"type": "timeseries",
"title": "Cumulative tokens by kind",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 9, "w": 16, "x": 8, "y": 5 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom" },
"tooltip": { "mode": "multi" }
},
"fieldConfig": {
"defaults": {
"unit": "short",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 15,
"showPoints": "never"
},
"color": { "mode": "palette-classic" }
}
},
"targets": [{ "refId": "A", "expr": "ccam_tokens_total", "legendFormat": "{{kind}}" }]
},
{
"id": 8,
"type": "timeseries",
"title": "Cumulative events",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 14 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] }
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0,
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 25,
"showPoints": "never"
},
"color": { "mode": "fixed", "fixedColor": "blue" }
}
},
"targets": [{ "refId": "A", "expr": "ccam_events_total", "legendFormat": "events" }]
},
{
"id": 101,
"type": "row",
"title": "Throughput rates",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 }
},
{
"id": 9,
"type": "timeseries",
"title": "Events per second (5m rate)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 23 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 20,
"showPoints": "never"
},
"color": { "mode": "fixed", "fixedColor": "blue" }
}
},
"targets": [
{ "refId": "A", "expr": "rate(ccam_events_total[5m])", "legendFormat": "events/s" },
{ "refId": "B", "expr": "ccam:events:rate5m", "legendFormat": "recording rule" }
]
},
{
"id": 10,
"type": "timeseries",
"title": "Tokens per second by kind (5m rate)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 23 },
"options": {
"legend": { "displayMode": "list", "placement": "bottom" },
"tooltip": { "mode": "multi" }
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 12,
"showPoints": "never"
},
"color": { "mode": "palette-classic" }
}
},
"targets": [
{ "refId": "A", "expr": "rate(ccam_tokens_total[5m])", "legendFormat": "{{kind}}" }
]
},
{
"id": 102,
"type": "row",
"title": "Cache efficiency",
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 31 }
},
{
"id": 11,
"type": "stat",
"title": "Cache read share of prompt tokens",
"description": "cache_read / (input + cache_read) — higher means more prompt served from cache.",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 5, "w": 8, "x": 0, "y": 32 },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"decimals": 1,
"min": 0,
"max": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "yellow", "value": 0.2 },
{ "color": "green", "value": 0.5 }
]
}
}
},
"targets": [
{
"refId": "A",
"expr": "ccam_tokens_total{kind=\"cache_read\"} / clamp_min(ccam_tokens_total{kind=\"input\"} + ccam_tokens_total{kind=\"cache_read\"}, 1)",
"legendFormat": "cache hit ratio"
}
]
},
{
"id": 12,
"type": "timeseries",
"title": "Input vs cache read tokens (cumulative)",
"datasource": { "type": "prometheus", "uid": "ccam-prometheus" },
"gridPos": { "h": 5, "w": 16, "x": 8, "y": 32 },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"fieldConfig": {
"defaults": {
"unit": "short",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10,
"showPoints": "never"
}
}
},
"targets": [
{ "refId": "A", "expr": "ccam_tokens_total{kind=\"input\"}", "legendFormat": "input" },
{
"refId": "B",
"expr": "ccam_tokens_total{kind=\"cache_read\"}",
"legendFormat": "cache_read"
}
]
}
]
}
@@ -0,0 +1,15 @@
# Dashboard provider for the native stack. start-native.sh writes the absolute
# dashboards path into .data/grafana-provisioning/dashboards/provider.runtime.yml
# because Grafana requires an absolute path in file providers.
apiVersion: 1
providers:
- name: CCAM
orgId: 1
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: false
@@ -0,0 +1,12 @@
# Auto-provisions the Prometheus datasource for a NATIVE (non-Docker) stack.
# Prometheus is expected on the host loopback interface (see start-native.sh).
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: ccam-prometheus
access: proxy
url: http://127.0.0.1:9090
isDefault: true
editable: true
@@ -0,0 +1,14 @@
# Loads every dashboard JSON under /var/lib/grafana/dashboards on startup — four
# bundled CCAM boards (Overview, Sessions & Agents, Tokens & Events, Platform Health).
apiVersion: 1
providers:
- name: CCAM
orgId: 1
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: false
@@ -0,0 +1,12 @@
# Auto-provisions the Prometheus datasource the CCAM dashboard queries. The uid
# is fixed so the bundled dashboard JSON can reference it deterministically.
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: ccam-prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@ccam/monitoring",
"version": "1.0.0",
"private": true,
"description": "CCAM Prometheus + Grafana stack (npm-managed binaries, no Docker required)",
"scripts": {
"postinstall": "node scripts/ensure-binaries.js",
"start": "node scripts/start.js --detach",
"start:foreground": "node scripts/start.js --foreground",
"stop": "node scripts/stop.js",
"verify": "node scripts/verify.js"
},
"dependencies": {
"adm-zip": "^0.5.16",
"tar": "^7.4.3"
},
"engines": {
"node": ">=20.0.0"
}
}
+21
View File
@@ -0,0 +1,21 @@
# Recording rules derived from live CCAM scrapes — no synthetic data.
# Pre-aggregates common queries so Prometheus Graph/Explore autocomplete
# surfaces useful series immediately after the ccam target comes up.
groups:
- name: ccam_recording
interval: 15s
rules:
- record: ccam:sessions:total
expr: sum(ccam_sessions)
- record: ccam:sessions:active
expr: ccam_sessions{status="active"}
- record: ccam:sessions:completed
expr: ccam_sessions{status="completed"}
- record: ccam:agents:working
expr: ccam_agents{status="working"}
- record: ccam:events:rate5m
expr: rate(ccam_events_total[5m])
- record: ccam:tokens:rate5m
expr: sum by (kind) (rate(ccam_tokens_total[5m]))
- record: ccam:tokens:total
expr: sum(ccam_tokens_total)
+294
View File
@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CCAM — Prometheus Console</title>
<style>
:root {
color-scheme: light dark;
--bg: #0b0f14;
--card: #141a22;
--text: #e8edf4;
--muted: #9aa7b5;
--accent: #e6522c;
--ok: #3dd68c;
--warn: #f5a524;
--border: #243041;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f4f6f8;
--card: #fff;
--text: #1a1f26;
--muted: #5c6b7a;
--border: #d8dee6;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
}
a { color: var(--accent); }
.wrap { max-width: 1100px; margin: 0 auto; padding: 24px 20px 48px; }
h1 { margin: 0 0 8px; font-size: 1.6rem; }
.sub { color: var(--muted); margin: 0 0 24px; }
.banner {
display: none;
padding: 12px 14px;
border-radius: 8px;
border: 1px solid var(--warn);
background: color-mix(in srgb, var(--warn) 12%, transparent);
margin-bottom: 20px;
}
.banner.show { display: block; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 28px;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
}
.card .label { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.card .value { font-size: 1.5rem; font-weight: 600; margin-top: 4px; }
.card .value.ok { color: var(--ok); }
.card .value.bad { color: #f87171; }
.card a.graph { font-size: 0.8rem; }
h2 { font-size: 1.1rem; margin: 0 0 12px; }
table {
width: 100%;
border-collapse: collapse;
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
margin-bottom: 28px;
}
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border); }
th { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; }
tr:last-child td { border-bottom: none; }
ul { margin: 0; padding-left: 20px; }
li { margin: 6px 0; }
code { font-size: 0.9em; }
.footer { color: var(--muted); font-size: 0.85rem; margin-top: 24px; }
</style>
</head>
<body>
<div class="wrap">
<h1>CCAM — Claude Code Agent Monitor</h1>
<p class="sub">
Live metrics from <code>GET /api/metrics</code> via Prometheus — your SQLite
database on each scrape. No synthetic data.
</p>
<div id="warn" class="banner" role="alert">
<strong>No <code>ccam</code> scrape target yet.</strong>
Start the dashboard (<code>npm start</code> on port 4820), wait ~15s, then
<a href="">refresh</a>.
</div>
<div id="cards" class="grid" aria-live="polite"></div>
<h2>Sessions by status</h2>
<table>
<thead><tr><th>Status</th><th>Count</th><th>Graph</th></tr></thead>
<tbody id="sessions-body"><tr><td colspan="3">Loading…</td></tr></tbody>
</table>
<h2>Tokens by kind</h2>
<table>
<thead><tr><th>Kind</th><th>Total</th><th>Graph</th></tr></thead>
<tbody id="tokens-body"><tr><td colspan="3">Loading…</td></tr></tbody>
</table>
<h2>Starter expressions</h2>
<ul id="starters"></ul>
<p class="footer">
Grafana:
<a href="http://localhost:3000/d/ccam-overview/ccam-overview" target="_blank" rel="noopener">Overview</a>
&middot;
<a href="http://localhost:3000/dashboards" target="_blank" rel="noopener">All dashboards</a>
&middot;
<a href="/graph">Prometheus Graph</a>
&middot;
<a href="/targets">Targets</a>
</p>
</div>
<script>
const STARTERS = [
["sum(ccam_sessions)", "Total sessions"],
["ccam_sessions{status=\"completed\"}", "Completed sessions"],
["ccam_events_total", "Cumulative events"],
["sum(ccam_tokens_total)", "All tokens"],
["rate(ccam_events_total[5m])", "Event throughput (/s)"],
["sum(rate(ccam_tokens_total[5m]))", "Token throughput (/s)"],
["ccam:sessions:total", "Recording rule — total sessions"],
["ccam_websocket_clients", "WebSocket clients"],
];
function graphUrl(expr) {
return "/graph?g0.expr=" + encodeURIComponent(expr) + "&g0.tab=0";
}
function humanize(n) {
const x = Number(n);
if (!Number.isFinite(x)) return "—";
if (Math.abs(x) >= 1e9) return (x / 1e9).toFixed(2) + "B";
if (Math.abs(x) >= 1e6) return (x / 1e6).toFixed(2) + "M";
if (Math.abs(x) >= 1e4) return (x / 1e3).toFixed(1) + "K";
if (Number.isInteger(x)) return String(x);
return x.toFixed(2);
}
function bytes(n) {
const x = Number(n);
if (!Number.isFinite(x)) return "—";
if (x >= 1073741824) return (x / 1073741824).toFixed(2) + " GiB";
if (x >= 1048576) return (x / 1048576).toFixed(1) + " MiB";
if (x >= 1024) return (x / 1024).toFixed(1) + " KiB";
return x + " B";
}
async function promql(query) {
const res = await fetch("/api/v1/query?" + new URLSearchParams({ query }));
if (!res.ok) throw new Error("HTTP " + res.status);
const body = await res.json();
if (body.status !== "success") throw new Error(body.error || "query failed");
return body.data.result;
}
function scalar(results) {
if (!results || !results.length) return null;
return results[0].value[1];
}
function card(label, value, opts) {
const el = document.createElement("div");
el.className = "card";
const cls = opts && opts.cls ? " value " + opts.cls : " value";
el.innerHTML =
'<div class="label">' + label + "</div>" +
'<div class="' + cls.trim() + '">' + value + "</div>" +
(opts && opts.expr
? ' <div><a class="graph" href="' + graphUrl(opts.expr) + '">Graph →</a></div>'
: "");
return el;
}
function tableRows(container, rows, emptyText) {
container.innerHTML = "";
if (!rows.length) {
const tr = document.createElement("tr");
tr.innerHTML = '<td colspan="3">' + emptyText + "</td>";
container.appendChild(tr);
return;
}
for (const row of rows) {
const tr = document.createElement("tr");
tr.innerHTML =
"<td><code>" + row.label + "</code></td>" +
"<td>" + row.value + "</td>" +
'<td><a href="' + graphUrl(row.expr) + '">Open</a></td>';
container.appendChild(tr);
}
}
async function main() {
const warn = document.getElementById("warn");
const cards = document.getElementById("cards");
const sessionsBody = document.getElementById("sessions-body");
const tokensBody = document.getElementById("tokens-body");
const starters = document.getElementById("starters");
for (const [expr, label] of STARTERS) {
const li = document.createElement("li");
li.innerHTML = label + ': <a href="' + graphUrl(expr) + '"><code>' + expr + "</code></a>";
starters.appendChild(li);
}
try {
const [
scrapeUp,
apiUp,
totalSessions,
activeSessions,
totalEvents,
allTokens,
workingAgents,
rss,
sessionVec,
tokenVec,
] = await Promise.all([
promql('up{job="ccam"}'),
promql("ccam_up"),
promql("sum(ccam_sessions)"),
promql('ccam_sessions{status="active"}'),
promql("ccam_events_total"),
promql("sum(ccam_tokens_total)"),
promql('ccam_agents{status="working"}'),
promql("ccam_process_resident_memory_bytes"),
promql("ccam_sessions"),
promql("ccam_tokens_total"),
]);
const scrapeVal = scalar(scrapeUp);
if (scrapeVal !== "1") warn.classList.add("show");
const upCls = (v) => (v === "1" ? "ok" : "bad");
const upText = (v) => (v === "1" ? "UP" : v == null ? "—" : "DOWN");
cards.appendChild(card("Scrape", upText(scrapeVal), { cls: upCls(scrapeVal), expr: 'up{job="ccam"}' }));
cards.appendChild(card("API", upText(scalar(apiUp)), { cls: upCls(scalar(apiUp)), expr: "ccam_up" }));
cards.appendChild(card("Total sessions", humanize(scalar(totalSessions)), { expr: "sum(ccam_sessions)" }));
cards.appendChild(card("Active sessions", humanize(scalar(activeSessions)), { expr: 'ccam_sessions{status="active"}' }));
cards.appendChild(card("Total events", humanize(scalar(totalEvents)), { expr: "ccam_events_total" }));
cards.appendChild(card("All tokens", humanize(scalar(allTokens)), { expr: "sum(ccam_tokens_total)" }));
cards.appendChild(card("Working agents", humanize(scalar(workingAgents)), { expr: 'ccam_agents{status="working"}' }));
cards.appendChild(card("Memory (RSS)", bytes(scalar(rss)), { expr: "ccam_process_resident_memory_bytes" }));
tableRows(
sessionsBody,
sessionVec.map((s) => ({
label: s.metric.status || "?",
value: humanize(s.value[1]),
expr: 'ccam_sessions{status="' + (s.metric.status || "") + '"}',
})),
"No session series yet."
);
tableRows(
tokensBody,
tokenVec.map((s) => ({
label: s.metric.kind || "?",
value: humanize(s.value[1]),
expr: 'ccam_tokens_total{kind="' + (s.metric.kind || "") + '"}',
})),
"No token series yet."
);
} catch (err) {
warn.classList.add("show");
warn.innerHTML =
"<strong>Could not query Prometheus.</strong> " +
(err && err.message ? err.message : err) +
' — is Prometheus running on this host?';
cards.innerHTML = "";
sessionsBody.innerHTML = '<tr><td colspan="3">Query failed.</td></tr>';
tokensBody.innerHTML = '<tr><td colspan="3">Query failed.</td></tr>';
}
}
main();
</script>
</body>
</html>
@@ -0,0 +1,19 @@
# Prometheus scrape config when BOTH CCAM and Prometheus run in Docker on the
# shared `ccam` network (docker-compose.full.yml). The Host guard on the
# dashboard must allow `agent-monitor` — set via DASHBOARD_ALLOWED_HOSTS there.
global:
scrape_interval: 15s
scrape_timeout: 10s
rule_files:
- ccam-rules.yml
scrape_configs:
- job_name: ccam
metrics_path: /api/metrics
static_configs:
- targets: ["agent-monitor:4820"]
# If the server sets DASHBOARD_TOKEN (LAN deployments), uncomment and fill in:
# authorization:
# type: Bearer
# credentials: "<DASHBOARD_TOKEN>"
@@ -0,0 +1,26 @@
# Prometheus scrape config for a NATIVE (non-Docker) CCAM + monitoring stack.
#
# Use this when Prometheus runs directly on the host (Homebrew, apt, a release
# tarball, etc.) and the dashboard is a plain `npm start` on loopback :4820.
# No DASHBOARD_ALLOWED_HOSTS override is required — 127.0.0.1 is already
# accepted by the server's Host guard.
#
# Started by: monitoring/scripts/start-native.sh (or npm run monitoring:native)
# See ../README.md.
global:
scrape_interval: 15s
scrape_timeout: 10s
rule_files:
- ccam-rules.yml
scrape_configs:
- job_name: ccam
metrics_path: /api/metrics
static_configs:
- targets: ["127.0.0.1:4820"]
# If the server sets DASHBOARD_TOKEN (LAN deployments), uncomment and fill in
# so the scrape passes the token guard:
# authorization:
# type: Bearer
# credentials: "<DASHBOARD_TOKEN>"
+29
View File
@@ -0,0 +1,29 @@
# Prometheus scrape config for the CCAM dashboard's /api/metrics endpoint.
#
# CCAM normally runs on the HOST (a plain `npm start` on port 4820), so from
# inside the Prometheus container it is reached via host.docker.internal. If the
# dashboard runs elsewhere or on a different port, edit the `targets` below.
#
# IMPORTANT: the CCAM server's DNS-rebinding guard rejects any Host header that
# is not loopback, so a container scrape gets 403 EBADHOST until you start the
# server allowing this Host:
# DASHBOARD_ALLOWED_HOSTS=host.docker.internal npm start
# (and, if DASHBOARD_TOKEN is set, also uncomment the authorization block below).
# See ../README.md.
global:
scrape_interval: 15s
scrape_timeout: 10s
rule_files:
- ccam-rules.yml
scrape_configs:
- job_name: ccam
metrics_path: /api/metrics
static_configs:
- targets: ["host.docker.internal:4820"]
# If the server sets DASHBOARD_TOKEN (LAN deployments), uncomment and fill in
# so the scrape passes the same-origin/token guard:
# authorization:
# type: Bearer
# credentials: "<DASHBOARD_TOKEN>"
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* @file Downloads official Prometheus + Grafana OSS binaries into monitoring/.bin/.
* Supports macOS (arm64/Intel), Linux (arm64/amd64), and Windows (x64).
* Run via `npm run monitoring:setup` — no Homebrew, apt, or global install needed.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const {
BIN_ROOT,
DATA_ROOT,
VERSIONS,
SUPPORTED_TARGETS,
prometheusUrl,
grafanaUrl,
prometheusArchiveName,
grafanaArchiveName,
prometheusArchiveExt,
grafanaArchiveExt,
prometheusHome,
grafanaHome,
prometheusBinary,
grafanaBinary,
prometheusPlatform,
binariesReady,
} = require("./paths");
const {
downloadFile,
extractArchive,
findExtractedRoot,
replaceDir,
prometheusContainsFile,
grafanaContainsFile,
} = require("./install-utils");
async function installPrometheus(tmpDir) {
const archive = path.join(tmpDir, `${prometheusArchiveName()}.${prometheusArchiveExt()}`);
const extractRoot = path.join(tmpDir, "prometheus-extract");
console.log(`→ Prometheus ${VERSIONS.prometheus} (${prometheusPlatform()})`);
await downloadFile(prometheusUrl(), archive);
await fs.promises.rm(extractRoot, { recursive: true, force: true });
await extractArchive(archive, extractRoot);
const extracted = await findExtractedRoot(
extractRoot,
[prometheusArchiveName(), `prometheus-${VERSIONS.prometheus}`],
prometheusContainsFile
);
await replaceDir(prometheusHome(), extracted);
}
async function installGrafana(tmpDir) {
const archive = path.join(tmpDir, `${grafanaArchiveName()}.${grafanaArchiveExt()}`);
const extractRoot = path.join(tmpDir, "grafana-extract");
console.log(`→ Grafana OSS ${VERSIONS.grafana} (${prometheusPlatform()})`);
await downloadFile(grafanaUrl(), archive);
await fs.promises.rm(extractRoot, { recursive: true, force: true });
await extractArchive(archive, extractRoot);
const extracted = await findExtractedRoot(
extractRoot,
[grafanaArchiveName(), `grafana-${VERSIONS.grafana}`, `grafana-v${VERSIONS.grafana}`],
grafanaContainsFile
);
await replaceDir(grafanaHome(), extracted);
}
async function main() {
if (binariesReady()) {
console.log("Monitoring binaries already present:");
console.log(` Prometheus: ${prometheusBinary()}`);
console.log(` Grafana: ${grafanaBinary()}`);
return;
}
console.log(`Platform: ${process.platform} ${process.arch}`);
console.log(`Supported: ${SUPPORTED_TARGETS.join(" · ")}`);
await fs.promises.mkdir(BIN_ROOT, { recursive: true });
await fs.promises.mkdir(DATA_ROOT, { recursive: true });
const tmpDir = path.join(DATA_ROOT, "downloads");
await fs.promises.mkdir(tmpDir, { recursive: true });
console.log("Downloading CCAM monitoring binaries (one-time setup)…");
await installPrometheus(tmpDir);
await installGrafana(tmpDir);
await fs.promises.rm(tmpDir, { recursive: true, force: true });
if (!binariesReady()) {
throw new Error("Binary install finished but executables were not found.");
}
console.log("Done. Binaries installed to monitoring/.bin/");
console.log(` Prometheus: ${prometheusBinary()}`);
console.log(` Grafana: ${grafanaBinary()}`);
}
main().catch((err) => {
console.error(`monitoring:setup failed: ${err.message}`);
process.exit(1);
});
+88
View File
@@ -0,0 +1,88 @@
/**
* @file Archive extraction and install-dir discovery for cross-platform binaries.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const { pipeline } = require("node:stream/promises");
const { Readable } = require("node:stream");
const tar = require("tar");
const AdmZip = require("adm-zip");
const { isWindows } = require("./paths");
async function downloadFile(url, dest) {
const response = await fetch(url, { redirect: "follow" });
if (!response.ok) {
throw new Error(`Download failed (${response.status}): ${url}`);
}
if (!response.body) {
throw new Error(`Empty response body: ${url}`);
}
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(dest));
}
async function extractTar(archivePath, destDir) {
await fs.promises.mkdir(destDir, { recursive: true });
await tar.x({ file: archivePath, cwd: destDir });
}
async function extractZip(archivePath, destDir) {
await fs.promises.mkdir(destDir, { recursive: true });
const zip = new AdmZip(archivePath);
zip.extractAllTo(destDir, true);
}
async function extractArchive(archivePath, destDir) {
if (archivePath.endsWith(".zip")) {
await extractZip(archivePath, destDir);
return;
}
await extractTar(archivePath, destDir);
}
async function listChildDirs(parent) {
const entries = await fs.promises.readdir(parent, { withFileTypes: true });
return entries.filter((e) => e.isDirectory()).map((e) => path.join(parent, e.name));
}
/**
* Locate a single top-level directory after extraction. Official archives ship
* one root folder; this also tolerates minor naming differences across OS builds.
*/
async function findExtractedRoot(extractRoot, preferredNames, containsFile) {
for (const name of preferredNames) {
const candidate = path.join(extractRoot, name);
if (fs.existsSync(path.join(candidate, containsFile))) return candidate;
}
const dirs = await listChildDirs(extractRoot);
const matches = [];
for (const dir of dirs) {
if (fs.existsSync(path.join(dir, containsFile))) matches.push(dir);
}
if (matches.length === 1) return matches[0];
if (matches.length > 1) {
throw new Error(
`Multiple install directories matched in ${extractRoot}: ${matches.map(path.basename).join(", ")}`
);
}
throw new Error(
`Could not find install directory under ${extractRoot} (expected ${containsFile}).`
);
}
async function replaceDir(target, source) {
await fs.promises.rm(target, { recursive: true, force: true });
await fs.promises.mkdir(path.dirname(target), { recursive: true });
await fs.promises.cp(source, target, { recursive: true });
}
module.exports = {
downloadFile,
extractArchive,
findExtractedRoot,
replaceDir,
prometheusContainsFile: isWindows() ? "prometheus.exe" : "prometheus",
grafanaContainsFile: path.join("bin", isWindows() ? "grafana.exe" : "grafana"),
};
+158
View File
@@ -0,0 +1,158 @@
/**
* @file Shared helpers for starting and stopping the npm-managed monitoring stack.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const { spawn, spawnSync } = require("node:child_process");
const {
DATA_ROOT,
RUNTIME_PROVISIONING,
GRAFANA_DASHBOARDS,
GRAFANA_DATASOURCE_TEMPLATE,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
isWindows,
toGrafanaPath,
} = require("./paths");
function readPid(file) {
try {
const raw = fs.readFileSync(file, "utf8").trim();
const pid = Number(raw);
return Number.isInteger(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
function isRunning(pid) {
if (!pid) return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function writePid(file, pid) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, String(pid));
}
function removePid(file) {
try {
fs.unlinkSync(file);
} catch {
/* already gone */
}
}
function killProcess(pid) {
if (!pid) return;
if (isWindows()) {
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
return;
}
process.kill(pid, "SIGTERM");
}
function stopPid(file, label) {
const pid = readPid(file);
if (!pid) return false;
if (!isRunning(pid)) {
removePid(file);
return false;
}
try {
killProcess(pid);
console.log(`Stopped ${label} (pid ${pid})`);
} catch (err) {
console.warn(`Could not stop ${label} (pid ${pid}): ${err.message}`);
}
removePid(file);
return true;
}
function writeGrafanaProvisioning() {
const dashboardsDir = path.join(RUNTIME_PROVISIONING, "dashboards");
const datasourcesDir = path.join(RUNTIME_PROVISIONING, "datasources");
fs.mkdirSync(dashboardsDir, { recursive: true });
fs.mkdirSync(datasourcesDir, { recursive: true });
fs.copyFileSync(GRAFANA_DATASOURCE_TEMPLATE, path.join(datasourcesDir, "datasource.yml"));
const provider = [
"apiVersion: 1",
"",
"providers:",
" - name: CCAM",
" orgId: 1",
" type: file",
" disableDeletion: false",
" updateIntervalSeconds: 30",
" allowUiUpdates: true",
" options:",
` path: ${toGrafanaPath(GRAFANA_DASHBOARDS)}`,
" foldersFromFilesStructure: false",
"",
].join("\n");
fs.writeFileSync(path.join(dashboardsDir, "provider.yml"), provider);
}
function spawnDetached(binary, args, env, logFile) {
fs.mkdirSync(path.dirname(logFile), { recursive: true });
const logFd = fs.openSync(logFile, "a");
const child = spawn(binary, args, {
detached: !isWindows(),
stdio: ["ignore", logFd, logFd],
env: { ...process.env, ...env },
windowsHide: true,
});
if (!isWindows()) child.unref();
fs.closeSync(logFd);
return child.pid;
}
function spawnForeground(binary, args, env) {
return spawn(binary, args, {
stdio: "inherit",
env: { ...process.env, ...env },
windowsHide: true,
});
}
async function waitForHttp(url, attempts = 30, intervalMs = 500) {
for (let i = 0; i < attempts; i += 1) {
try {
const res = await fetch(url);
if (res.ok) return true;
} catch {
/* retry */
}
await new Promise((r) => setTimeout(r, intervalMs));
}
return false;
}
module.exports = {
readPid,
isRunning,
writePid,
removePid,
killProcess,
stopPid,
writeGrafanaProvisioning,
spawnDetached,
spawnForeground,
waitForHttp,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
DATA_ROOT,
RUNTIME_PROVISIONING,
};
+212
View File
@@ -0,0 +1,212 @@
/**
* @file Shared paths and version pins for the CCAM monitoring stack.
* Binaries are downloaded into monitoring/.bin/ by ensure-binaries.js so the
* stack runs with plain npm on macOS, Linux, and Windows — no Homebrew, apt,
* or global installs required.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const path = require("node:path");
const MONITORING_ROOT = path.resolve(__dirname, "..");
const REPO_ROOT = path.resolve(MONITORING_ROOT, "..");
const VERSIONS = {
prometheus: "3.2.1",
grafana: "11.6.1",
};
/** Human-readable list for error messages. */
const SUPPORTED_TARGETS = [
"macOS (Apple Silicon / Intel)",
"Linux (arm64 / amd64)",
"Windows (x64)",
];
const BIN_ROOT = path.join(MONITORING_ROOT, ".bin");
const DATA_ROOT = path.join(MONITORING_ROOT, ".data");
const PROMETHEUS_DATA = path.join(DATA_ROOT, "prometheus");
const GRAFANA_DATA = path.join(DATA_ROOT, "grafana");
const RUNTIME_PROVISIONING = path.join(DATA_ROOT, "grafana-provisioning");
const PROMETHEUS_CONFIG = path.join(MONITORING_ROOT, "prometheus", "prometheus-native.yml");
const PROMETHEUS_CONSOLES = path.join(MONITORING_ROOT, "prometheus", "consoles");
const GRAFANA_DASHBOARDS = path.join(MONITORING_ROOT, "grafana", "dashboards");
const GRAFANA_DATASOURCE_TEMPLATE = path.join(
MONITORING_ROOT,
"grafana",
"provisioning-native",
"datasources",
"datasource.yml"
);
const PROMETHEUS_PID = path.join(DATA_ROOT, "prometheus.pid");
const GRAFANA_PID = path.join(DATA_ROOT, "grafana.pid");
const PROMETHEUS_LOG = path.join(DATA_ROOT, "prometheus.log");
const GRAFANA_LOG = path.join(DATA_ROOT, "grafana.log");
function isWindows() {
return process.platform === "win32";
}
/**
* Maps Node's `process.arch` to Prometheus/Grafana release asset arch slugs.
* @returns {"arm64"|"amd64"}
*/
function releaseArch() {
if (process.arch === "arm64") return "arm64";
if (process.arch === "x64") return "amd64";
throw new Error(
`Unsupported CPU architecture "${process.arch}". Supported: ${SUPPORTED_TARGETS.join(", ")}.`
);
}
/** @returns {string} Prometheus platform slug used in release asset names. */
function prometheusPlatform() {
const arch = releaseArch();
if (process.platform === "darwin") return arch === "arm64" ? "darwin-arm64" : "darwin-amd64";
if (process.platform === "linux") return arch === "arm64" ? "linux-arm64" : "linux-amd64";
if (process.platform === "win32") return "windows-amd64";
throw new Error(
`Unsupported OS "${process.platform}". Supported: ${SUPPORTED_TARGETS.join(", ")}.`
);
}
/** @returns {string} Grafana platform slug used in release asset names. */
function grafanaPlatform() {
return prometheusPlatform();
}
function prometheusArchiveName() {
return `prometheus-${VERSIONS.prometheus}.${prometheusPlatform()}`;
}
function grafanaArchiveName() {
return `grafana-${VERSIONS.grafana}.${grafanaPlatform()}`;
}
function prometheusArchiveExt() {
return isWindows() ? "zip" : "tar.gz";
}
function grafanaArchiveExt() {
return isWindows() ? "zip" : "tar.gz";
}
function prometheusUrl() {
const name = prometheusArchiveName();
return `https://github.com/prometheus/prometheus/releases/download/v${VERSIONS.prometheus}/${name}.${prometheusArchiveExt()}`;
}
function grafanaUrl() {
const plat = grafanaPlatform();
return `https://dl.grafana.com/oss/release/grafana-${VERSIONS.grafana}.${plat}.${grafanaArchiveExt()}`;
}
function prometheusHome() {
return path.join(BIN_ROOT, "prometheus");
}
function grafanaHome() {
return path.join(BIN_ROOT, "grafana");
}
function prometheusBinary() {
const name = isWindows() ? "prometheus.exe" : "prometheus";
return path.join(prometheusHome(), name);
}
function grafanaBinary() {
const name = isWindows() ? "grafana.exe" : "grafana";
return path.join(grafanaHome(), "bin", name);
}
const GRAFANA_ADMIN_USER = "admin";
const GRAFANA_ADMIN_PASSWORD = "admin";
/** Grafana env vars that seed the default admin account on first start. */
function grafanaAdminEnv() {
return {
GF_SECURITY_ADMIN_USER: GRAFANA_ADMIN_USER,
GF_SECURITY_ADMIN_PASSWORD: GRAFANA_ADMIN_PASSWORD,
GF_USERS_ALLOW_SIGN_UP: "false",
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_UID: "ccam-overview",
};
}
function grafanaLoginLabel() {
return `${GRAFANA_ADMIN_USER} / ${GRAFANA_ADMIN_PASSWORD}`;
}
/** Grafana file providers require forward slashes even on Windows. */
function toGrafanaPath(filePath) {
return filePath.replace(/\\/g, "/");
}
/** CLI args for Prometheus 3.x (console libraries removed upstream). */
function prometheusServerArgs(configFile, storagePath = PROMETHEUS_DATA) {
return [
`--config.file=${configFile}`,
`--storage.tsdb.path=${storagePath}`,
"--web.enable-lifecycle",
`--web.console.templates=${PROMETHEUS_CONSOLES}`,
];
}
function prometheusDockerServerArgs(configFile) {
return [
`--config.file=${configFile}`,
`--storage.tsdb.path=/prometheus`,
"--web.enable-lifecycle",
`--web.console.templates=/etc/prometheus/consoles`,
];
}
const PROMETHEUS_CONSOLES_URL = "http://localhost:9090/consoles/index.html";
function binariesReady() {
const fs = require("node:fs");
return fs.existsSync(prometheusBinary()) && fs.existsSync(grafanaBinary());
}
module.exports = {
MONITORING_ROOT,
REPO_ROOT,
VERSIONS,
SUPPORTED_TARGETS,
BIN_ROOT,
DATA_ROOT,
PROMETHEUS_DATA,
GRAFANA_DATA,
RUNTIME_PROVISIONING,
PROMETHEUS_CONFIG,
PROMETHEUS_CONSOLES,
PROMETHEUS_CONSOLES_URL,
GRAFANA_DASHBOARDS,
GRAFANA_DATASOURCE_TEMPLATE,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
isWindows,
releaseArch,
prometheusPlatform,
grafanaPlatform,
prometheusArchiveName,
grafanaArchiveName,
prometheusArchiveExt,
grafanaArchiveExt,
prometheusUrl,
grafanaUrl,
prometheusHome,
grafanaHome,
prometheusBinary,
grafanaBinary,
prometheusServerArgs,
prometheusDockerServerArgs,
GRAFANA_ADMIN_USER,
GRAFANA_ADMIN_PASSWORD,
grafanaAdminEnv,
grafanaLoginLabel,
toGrafanaPath,
binariesReady,
};
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env node
/**
* @file Starts the npm-managed Prometheus + Grafana stack (no Docker / no Brew).
* Downloads binaries on first run if monitoring:setup has not been run yet.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const {
PROMETHEUS_CONFIG,
PROMETHEUS_DATA,
GRAFANA_DATA,
RUNTIME_PROVISIONING,
prometheusBinary,
prometheusServerArgs,
PROMETHEUS_CONSOLES_URL,
grafanaBinary,
grafanaHome,
grafanaAdminEnv,
grafanaLoginLabel,
binariesReady,
} = require("./paths");
const {
readPid,
isRunning,
writePid,
writeGrafanaProvisioning,
spawnDetached,
spawnForeground,
waitForHttp,
killProcess,
PROMETHEUS_PID,
GRAFANA_PID,
PROMETHEUS_LOG,
GRAFANA_LOG,
} = require("./lib");
const detached = process.argv.includes("--detach") || process.argv.includes("-d");
const foreground = process.argv.includes("--foreground") || process.argv.includes("-f");
async function ensureBinaries() {
if (binariesReady()) return;
console.log("Monitoring binaries not found — running setup…");
const script = path.join(__dirname, "ensure-binaries.js");
const result = spawnSync(process.execPath, [script], { stdio: "inherit" });
if (result.status !== 0) process.exit(result.status || 1);
}
function assertNotRunning() {
for (const [file, label] of [
[PROMETHEUS_PID, "Prometheus"],
[GRAFANA_PID, "Grafana"],
]) {
const pid = readPid(file);
if (isRunning(pid)) {
console.error(`${label} is already running (pid ${pid}). Run: npm run monitoring:down`);
process.exit(1);
}
}
}
function startPrometheus() {
fs.mkdirSync(PROMETHEUS_DATA, { recursive: true });
const args = prometheusServerArgs(PROMETHEUS_CONFIG);
const binary = prometheusBinary();
if (detached || (!foreground && !process.stdout.isTTY)) {
const pid = spawnDetached(binary, args, {}, PROMETHEUS_LOG);
writePid(PROMETHEUS_PID, pid);
return pid;
}
return spawnForeground(binary, args, {});
}
function startGrafana() {
fs.mkdirSync(GRAFANA_DATA, { recursive: true });
writeGrafanaProvisioning();
const env = {
GF_PATHS_HOME: grafanaHome(),
GF_PATHS_DATA: GRAFANA_DATA,
GF_PATHS_PROVISIONING: RUNTIME_PROVISIONING,
...grafanaAdminEnv(),
};
const args = ["server", "--homepath", grafanaHome()];
const binary = grafanaBinary();
if (detached || (!foreground && !process.stdout.isTTY)) {
const pid = spawnDetached(binary, args, env, GRAFANA_LOG);
writePid(GRAFANA_PID, pid);
return pid;
}
return spawnForeground(binary, args, env);
}
async function main() {
await ensureBinaries();
assertNotRunning();
console.log("Starting CCAM monitoring stack…");
console.log(` Prometheus config: ${PROMETHEUS_CONFIG}`);
console.log(` Grafana home: ${grafanaHome()}`);
if (detached || (!foreground && !process.stdout.isTTY)) {
const promPid = startPrometheus();
const grafPid = startGrafana();
const promOk = await waitForHttp("http://127.0.0.1:9090/-/ready");
const grafOk = await waitForHttp("http://127.0.0.1:3000/api/health");
console.log("");
console.log(
`Prometheus http://localhost:9090 (pid ${promPid})${promOk ? "" : " [still starting]"}`
);
console.log(` CCAM console: ${PROMETHEUS_CONSOLES_URL}`);
console.log(
`Grafana http://localhost:3000 (pid ${grafPid}) ${grafanaLoginLabel()}${grafOk ? "" : " [still starting]"}`
);
console.log(
"CCAM dashboards auto-provisioned (Overview, Sessions & Agents, Tokens & Events, Platform Health)."
);
console.log("Stop with: npm run monitoring:down");
if (!promOk || !grafOk) {
console.log(`Logs: ${PROMETHEUS_LOG} ${GRAFANA_LOG}`);
}
return;
}
// Foreground: Prometheus in background, Grafana in foreground (Grafana owns the tty).
const promPid = spawnDetached(
prometheusBinary(),
prometheusServerArgs(PROMETHEUS_CONFIG),
{},
PROMETHEUS_LOG
);
writePid(PROMETHEUS_PID, promPid);
console.log(`Prometheus running at http://localhost:9090 (pid ${promPid})`);
console.log(` CCAM console: ${PROMETHEUS_CONSOLES_URL}`);
console.log(
`Grafana starting at http://localhost:3000 (${grafanaLoginLabel()}) — Ctrl+C stops both`
);
const graf = startGrafana();
const shutdown = () => {
killProcess(promPid);
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
graf.on("exit", (code) => {
shutdown();
process.exit(code ?? 0);
});
}
main().catch((err) => {
console.error(`monitoring:start failed: ${err.message}`);
process.exit(1);
});
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env node
/**
* @file Stops the npm-managed Prometheus + Grafana stack started by start.js.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { stopPid, PROMETHEUS_PID, GRAFANA_PID } = require("./lib");
const stoppedGrafana = stopPid(GRAFANA_PID, "Grafana");
const stoppedPrometheus = stopPid(PROMETHEUS_PID, "Prometheus");
if (!stoppedGrafana && !stoppedPrometheus) {
console.log("Monitoring stack is not running.");
} else {
console.log("Monitoring stack stopped.");
}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env node
/**
* @file Verifies the CCAM dashboard and optional Prometheus/Grafana stack are up.
* Used after `monitoring:up` or `monitoring:docker:up` to confirm scrape health.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { GRAFANA_ADMIN_USER, GRAFANA_ADMIN_PASSWORD } = require("./paths");
const DASHBOARD_URL = process.env.CCAM_DASHBOARD_URL || "http://127.0.0.1:4820";
const PROMETHEUS_URL = process.env.CCAM_PROMETHEUS_URL || "http://127.0.0.1:9090";
const GRAFANA_URL = process.env.CCAM_GRAFANA_URL || "http://127.0.0.1:3000";
const JSON_MODE = process.argv.includes("--json");
async function checkDashboardHealth() {
try {
const res = await fetch(`${DASHBOARD_URL}/api/health`);
if (!res.ok) {
return { name: "Dashboard /api/health", ok: false, detail: `HTTP ${res.status}` };
}
const body = await res.json();
const detail = body.version ? `v${body.version}` : undefined;
return { name: "Dashboard /api/health", ok: true, detail, version: body.version };
} catch (err) {
return { name: "Dashboard /api/health", ok: false, detail: err.message };
}
}
async function check(name, url, ok = (res) => res.ok) {
try {
const res = await fetch(url);
if (!ok(res)) {
return { name, ok: false, detail: `HTTP ${res.status}` };
}
return { name, ok: true };
} catch (err) {
return { name, ok: false, detail: err.message };
}
}
async function checkPrometheusTarget(attempts = 12, intervalMs = 2500) {
for (let i = 0; i < attempts; i += 1) {
const result = await checkPrometheusTargetOnce();
if (result.ok) return result;
if (i < attempts - 1) await new Promise((r) => setTimeout(r, intervalMs));
if (i === attempts - 1) return result;
}
return { name: "Prometheus ccam target", ok: false, detail: "timeout" };
}
async function checkPrometheusTargetOnce() {
try {
const res = await fetch(`${PROMETHEUS_URL}/api/v1/targets`);
if (!res.ok) return { name: "Prometheus ccam target", ok: false, detail: `HTTP ${res.status}` };
const body = await res.json();
const target = body?.data?.activeTargets?.find((t) => t.labels?.job === "ccam");
if (!target) return { name: "Prometheus ccam target", ok: false, detail: "job not found" };
if (target.health !== "up") {
return {
name: "Prometheus ccam target",
ok: false,
detail: target.lastError || `health=${target.health}`,
};
}
return { name: "Prometheus ccam target", ok: true, detail: target.scrapeUrl };
} catch (err) {
return { name: "Prometheus ccam target", ok: false, detail: err.message };
}
}
async function checkGrafanaLogin() {
try {
const res = await fetch(`${GRAFANA_URL}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user: GRAFANA_ADMIN_USER,
password: GRAFANA_ADMIN_PASSWORD,
}),
});
if (!res.ok) {
return { name: "Grafana admin login", ok: false, detail: `HTTP ${res.status}` };
}
const body = await res.json();
if (body?.message !== "Logged in") {
return { name: "Grafana admin login", ok: false, detail: body?.message || "login rejected" };
}
return { name: "Grafana admin login", ok: true, detail: grafanaLoginDetail() };
} catch (err) {
return { name: "Grafana admin login", ok: false, detail: err.message };
}
}
function grafanaLoginDetail() {
return `${GRAFANA_ADMIN_USER} / ${GRAFANA_ADMIN_PASSWORD}`;
}
async function checkPrometheusMetrics() {
try {
const queries = [
{ name: "Prometheus ccam_up", q: "ccam_up" },
{ name: "Prometheus total sessions", q: "sum(ccam_sessions)" },
];
const results = [];
for (const { name, q } of queries) {
const res = await fetch(
`${PROMETHEUS_URL}/api/v1/query?${new URLSearchParams({ query: q })}`
);
if (!res.ok) {
results.push({ name, ok: false, detail: `HTTP ${res.status}` });
continue;
}
const body = await res.json();
const series = body?.data?.result;
if (!Array.isArray(series) || series.length === 0) {
results.push({ name, ok: false, detail: "no series (is CCAM scraping?)" });
continue;
}
const sample = series[0]?.value?.[1];
results.push({ name, ok: true, detail: `${q} = ${sample}` });
}
return results;
} catch (err) {
return [{ name: "Prometheus ccam metrics", ok: false, detail: err.message }];
}
}
async function checkPrometheusConsole() {
try {
const res = await fetch(`${PROMETHEUS_URL}/consoles/index.html`);
if (!res.ok) {
return { name: "Prometheus CCAM console", ok: false, detail: `HTTP ${res.status}` };
}
const html = await res.text();
if (!html.includes("CCAM")) {
return { name: "Prometheus CCAM console", ok: false, detail: "unexpected page body" };
}
return { name: "Prometheus CCAM console", ok: true, detail: "/consoles/index.html" };
} catch (err) {
return { name: "Prometheus CCAM console", ok: false, detail: err.message };
}
}
async function main() {
const checks = [
await checkDashboardHealth(),
await check(
"Dashboard /api/metrics",
`${DASHBOARD_URL}/api/metrics`,
(res) => res.ok && res.headers.get("content-type")?.includes("text/plain")
),
await check("Prometheus /-/ready", `${PROMETHEUS_URL}/-/ready`),
await checkPrometheusConsole(),
await check("Grafana /api/health", `${GRAFANA_URL}/api/health`),
await checkGrafanaLogin(),
await checkPrometheusTarget(),
...(await checkPrometheusMetrics()),
];
let failed = 0;
for (const c of checks) {
if (c.ok) {
if (!JSON_MODE) {
console.log(`${c.name}${c.detail ? ` (${c.detail})` : ""}`);
}
} else {
failed += 1;
if (!JSON_MODE) {
console.error(`${c.name}: ${c.detail || "failed"}`);
}
}
}
if (JSON_MODE) {
const payload = {
ok: failed === 0,
checks,
urls: {
dashboard: DASHBOARD_URL,
prometheus: PROMETHEUS_URL,
grafana: GRAFANA_URL,
metrics: `${DASHBOARD_URL}/api/metrics`,
},
};
console.log(JSON.stringify(payload, null, 2));
process.exit(failed > 0 ? 1 : 0);
}
if (failed > 0) {
console.error(`\n${failed} check(s) failed.`);
process.exit(1);
}
console.log("\nMonitoring stack OK.");
console.log(` Grafana: ${GRAFANA_URL} (${grafanaLoginDetail()})`);
console.log(` Console: ${PROMETHEUS_URL}/consoles/index.html`);
console.log(` Graph: ${PROMETHEUS_URL}/graph`);
console.log(` Metrics: ${DASHBOARD_URL}/api/metrics`);
}
main();