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:
@@ -0,0 +1,4 @@
|
||||
.data/
|
||||
.bin/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
@@ -0,0 +1,272 @@
|
||||
# CCAM monitoring stack (Prometheus + Grafana)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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.
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
·
|
||||
<a href="http://localhost:3000/dashboards" target="_blank" rel="noopener">All dashboards</a>
|
||||
·
|
||||
<a href="/graph">Prometheus Graph</a>
|
||||
·
|
||||
<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>"
|
||||
@@ -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>"
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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"),
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user