| name | metrics-ingest |
| description | Send time-series data points to the time-series-dashboard ingest API from curl, Python, or Node.js. Use this skill when the user wants to push metrics, test the ingest pipeline, or set up a data collection script. |
| tools | ["Bash","Write"] |
metrics-ingest skill
This skill covers all patterns for ingesting data into the time-series-dashboard application.
Prerequisites
You need an API key. Create one via the admin UI (Settings > API Keys) or via the admin API:
CSRF=$(curl -b /tmp/tsd_cookies.txt -s http://localhost:3000/api/csrf | jq -r '.token')
curl -b /tmp/tsd_cookies.txt -X POST http://localhost:3000/api/keys \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d '{"name": "my-script"}'
export TSD_KEY="tsd_live_xxxx"
export TSD_HOST="http://localhost:3000"
Single data point ingest
curl
curl -X POST "$TSD_HOST/api/ingest/cpu_usage" \
-H "Authorization: Bearer $TSD_KEY" \
-H "Content-Type: application/json" \
-d '{"value": 72.5}'
curl -X POST "$TSD_HOST/api/ingest/cpu_usage" \
-H "Authorization: Bearer $TSD_KEY" \
-H "Content-Type: application/json" \
-d "{\"value\": 72.5, \"ts\": $(date +%s000), \"tags\": {\"host\": \"web-01\", \"region\": \"us-east\"}}"
Python
import requests
import time
TSD_HOST = "http://localhost:3000"
TSD_KEY = "tsd_live_xxxx"
def ingest(slug: str, value: float, tags: dict = None):
payload = {"value": value}
if tags:
payload["tags"] = tags
r = requests.post(
f"{TSD_HOST}/api/ingest/{slug}",
json=payload,
headers={"Authorization": f"Bearer {TSD_KEY}"},
timeout=5,
)
r.raise_for_status()
ingest("cpu_usage", 72.5)
ingest("cpu_usage", 72.5, tags={"host": "web-01"})
Node.js
const TSD_HOST = "http://localhost:3000";
const TSD_KEY = "tsd_live_xxxx";
async function ingest(slug, value, tags = undefined) {
const res = await fetch(`${TSD_HOST}/api/ingest/${slug}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TSD_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ value, ...(tags && { tags }) }),
});
if (!res.ok) throw new Error(`Ingest failed: ${res.status}`);
}
await ingest("cpu_usage", 72.5);
await ingest("cpu_usage", 72.5, { host: "web-01" });
Batch ingest (up to 1000 points per request)
curl
curl -X POST "$TSD_HOST/api/ingest/cpu_usage/batch" \
-H "Authorization: Bearer $TSD_KEY" \
-H "Content-Type: application/json" \
-d '[
{"value": 70.0, "ts": 1742479140000},
{"value": 71.5, "ts": 1742479200000},
{"value": 72.5, "ts": 1742479260000}
]'
Python - backfill loop
import requests
import time
def ingest_batch(slug: str, points: list[dict]):
"""Each point: {"value": float, "ts": int (ms), "tags": dict (optional)}"""
r = requests.post(
f"{TSD_HOST}/api/ingest/{slug}/batch",
json=points,
headers={"Authorization": f"Bearer {TSD_KEY}"},
timeout=30,
)
r.raise_for_status()
return r.json()
import random
now_ms = int(time.time() * 1000)
one_hour_ago = now_ms - 3600 * 1000
step_ms = 1000
CHUNK = 1000
points = []
ts = one_hour_ago
while ts <= now_ms:
points.append({"value": round(random.uniform(40, 90), 2), "ts": ts})
ts += step_ms
if len(points) == CHUNK:
ingest_batch("cpu_usage", points)
points = []
if points:
ingest_batch("cpu_usage", points)
print("Backfill complete")
Node.js - batch helper
async function ingestBatch(slug, points) {
const res = await fetch(`${TSD_HOST}/api/ingest/${slug}/batch`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TSD_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(points),
});
if (!res.ok) throw new Error(`Batch ingest failed: ${res.status}`);
return res.json();
}
Continuous polling script (Python)
Collect a system metric every second and push it:
import psutil
import requests
import time
TSD_HOST = "http://localhost:3000"
TSD_KEY = "tsd_live_xxxx"
SLUG = "cpu_usage"
INTERVAL = 1
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {TSD_KEY}"})
print(f"Starting ingest loop for {SLUG} every {INTERVAL}s. Ctrl+C to stop.")
while True:
value = psutil.cpu_percent(interval=None)
try:
session.post(
f"{TSD_HOST}/api/ingest/{SLUG}",
json={"value": value},
timeout=3,
).raise_for_status()
except Exception as e:
print(f"Ingest error: {e}")
time.sleep(INTERVAL)
Validation rules
The server enforces these rules on every ingest request. Violations return HTTP 422.
| Rule | Detail |
|---|
value must be finite | NaN, Infinity, -Infinity are rejected |
value must be a number | Strings are rejected |
ts must be integer ms | If provided, must be a valid Unix millisecond timestamp |
tags must be flat | Nested objects in tags are rejected |
| Batch max size | Maximum 1000 points per batch request |
Rate limits
| Route | Limit |
|---|
/api/ingest/* | 1000 requests/minute per IP |
| Admin routes | 60 requests/minute per IP |
WebSocket live subscription (receive, not send)
After the server receives a new data point via HTTP ingest, it pushes the point to all WebSocket clients subscribed to dashboards that include the metric.
const ws = new WebSocket("ws://localhost:3000/ws");
ws.onopen = () => {
ws.send(JSON.stringify({ type: "subscribe", dashboardId: "dash_abc123" }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "data") {
console.log(`${msg.metricId} at ${msg.point.ts}: ${msg.point.value}`);
} else if (msg.type === "alert") {
console.warn(`ALERT: ${msg.ruleName} - ${msg.metricId} = ${msg.value} (threshold: ${msg.threshold})`);
}
};
Testing ingest quickly
CSRF=$(curl -b /tmp/tsd_cookies.txt -s http://localhost:3000/api/csrf | jq -r '.token')
curl -b /tmp/tsd_cookies.txt -X POST http://localhost:3000/api/metrics \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d '{"name":"test_gauge","slug":"test_gauge","type":"gauge","unit":"units","color":"#0891b2"}'
curl -X POST "$TSD_HOST/api/ingest/test_gauge" \
-H "Authorization: Bearer $TSD_KEY" \
-H "Content-Type: application/json" \
-d '{"value": 42}'
curl -b /tmp/tsd_cookies.txt \
"http://localhost:3000/api/metrics/test_gauge/data?from=-5m"
curl -X POST "$TSD_HOST/api/ingest/test_gauge" \
-H "Authorization: Bearer $TSD_KEY" \
-H "Content-Type: application/json" \
-d '{"value": null}'