Logs, Ingestion & APM
Send logs from anything that can make an HTTP request, search them, manage the log sources your platforms drain into, and read traces, errors and session recordings.
Paths are relative to https://api.zharp.io/api/v1. Sending data uses an X-API-KEY header — an Ingest only key is made for this, though any key works. Searching and everything else uses Authorization: Bearer with a read key, or admin to change log sources. See Authentication.
/agent/logsSend logs (what the SDKs use)POST/logs/ingestSend logs — a plain listGET/logsSearch logsGET/logs/facetsServices, environments, versions, sourcesGET/logs/servicesEvery service nameGET/logs/drop-rulesLogs the workspace doesn't keepPOST/logs/drop-rulesStop keeping matching logsDELETE/logs/drop-rules/:idKeep them againGET/log-sourcesList log sourcesPOST/log-sourcesCreate a log sourcePOST/log-sources/:id/rollNew token for a sourcePOST/ingest/:provider/:tokenA platform's log drainPOST/agent/tracesSend tracesGET/apm/servicesServices with tracesGET/apm/tracesRecent tracesGET/apm/traces/:traceIdOne traceGET/apm/errorsErrors by kindGET/agent/listHosts running the agentSending logs
There's an SDK for JavaScript, Python, Go, PHP & Laravel, Android and Dart/Flutter — they batch, retry and capture crashes for you. Anything else can send the same requests itself.
/agent/logsThe key can go in the path instead, for senders that can't set headers: /agent/logs/<key>. Up to 10 MB a request, any number of lines; send X-Zharp-SDK: yourname/1.0 if you're writing a client.
logslogs[].message(empty message).logs[].timestamplogs[].serviceunlabeled.logs[].leveldebug, info (default), warn, error, fatal — or warning, critical, syslog numbers and the like, which map onto those.logs[].environmentenv works too.logs[].versionlogs[].sourcelogs[].metadatastack here shows as a stack trace.curl -X POST "https://api.zharp.io/api/v1/agent/logs" \
-H "X-API-KEY: $ZHARP_INGEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"logs": [
{
"timestamp": "2026-10-26T09:12:01.000Z",
"service": "checkout",
"level": "error",
"message": "Payment failed",
"environment": "production",
"version": "1.4.2",
"source": "node",
"metadata": { "orderId": "A-1042", "stack": "Error: card declined\\n at charge (pay.js:42)" }
}
]
}'{ "statusCode": 200, "message": "Logs ingested successfully" }400No lines, or a field of the wrong type (levelas a number, say).401No key, or a key that's invalid, disabled or expired.413A request over 10 MB — send smaller batches.429The workspace has sent its log allowance for the month — 1 GB on Free, 30 GB on Pro, 200 GB on Business.errorCodeislimitExceeded; logs are accepted again on the 1st.
/logs/ingest(body){ "logs": [...] } works too.curl -X POST "https://api.zharp.io/api/v1/logs/ingest" \
-H "X-API-KEY: $ZHARP_INGEST_KEY" \
-H "Content-Type: application/json" \
-d '[{ "service": "checkout", "level": "warn", "message": "Slow response from Stripe", "metadata": { "ms": 3200 } }]'{ "message": "Logs ingested successfully" }400No lines, or a field of the wrong type.401No valid key.413Over 10 MB.429The month's log allowance is used up.
What a workspace sends is counted each calendar month (UTC): 1 GB on Free, 30 GB on Pro, 200 GB on Business, unlimited on Enterprise. A batch that crosses the line is still accepted; the next is refused until the 1st. See this month's use with GET /workspaces/:id/usage.
Searching logs
/logsid is a string — the numbers are too large for JavaScript. Page with offset; there's no total. A line's parsed fields are in metadata.parsed, with the parser that read them in metadata.parser.
serviceNameenvironmentenv works too). Lists as above.versionsourcenginx, postgresql…levelerror, warn…searchfilterhttp.status_code>=500, client.ip=10.0.0.4, error.type:* (has it). Repeat for up to 10, all of which must hold.fromtolimitoffsetcurl "https://api.zharp.io/api/v1/logs?serviceName=checkout&level=error&filter=http.status_code%3E%3D500&limit=50" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "Logs retrieved successfully", "data": [ {
"id": "918273",
"workspaceId": 3,
"sourceId": null,
"serviceName": "checkout",
"environment": "production",
"serviceVersion": "1.4.2",
"source": "node",
"level": "error",
"message": "Payment failed",
"timestamp": "2026-10-26T09:12:01.000Z",
"metadata": { "orderId": "A-1042", "stack": "…" },
"createdAt": "2026-10-26T09:12:01.512Z"
} ] }400A value out of range, or a filter that can't be read.
/logs/facetshourscurl "https://api.zharp.io/api/v1/logs/facets?hours=24" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{
"message": "Log services retrieved",
"data": {
"hours": 24,
"services": [{ "serviceName": "checkout", "environment": "production", "lines": 5120, "lastSeen": "…", "versions": ["1.4.2"], "sources": ["node"] }],
"environments": ["production", "staging"],
"versions": ["1.4.2"],
"sources": ["node", "nginx"]
}
}/logs/servicescurl "https://api.zharp.io/api/v1/logs/services" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "Log services retrieved", "data": ["api", "checkout", "unlabeled"] }Excluding logs
A drop rule stops the workspace keeping logs it doesn't want — a cluster's system pods, health-check noise. Matching lines are dropped the moment they arrive, from every agent, SDK and log source: they aren't stored, don't reach the live tail and don't count toward the month's allowance. Lines already stored stay until they expire.
/logs/drop-rulescurl "https://api.zharp.io/api/v1/logs/drop-rules" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "Log drop rules retrieved", "data": [{ "id": 3, "field": "service", "pattern": "aws-ebs-*", "createdAt": "2026-10-27T09:12:00.000Z" }] }/logs/drop-rulesfieldservice, environment, source or message.pattern* matching anything — aws-ebs-*, kube-*. For a message: text it contains, any case (not a regex). Up to 200 characters; a pattern that is only * is refused.curl -X POST "https://api.zharp.io/api/v1/logs/drop-rules" \
-H "Authorization: Bearer $ZHARP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "field": "service", "pattern": "aws-ebs-*" }'{ "message": "Matching logs will no longer be kept", "data": { "id": 3, "field": "service", "pattern": "aws-ebs-*", "createdAt": "…" } }400An unknown field, an empty or too-broad pattern, or more than 100 rules.409The same rule already exists.
/logs/drop-rules/:ididcurl -X DELETE "https://api.zharp.io/api/v1/logs/drop-rules/3" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "Matching logs will be kept again", "data": null }404No such rule in this workspace.
Log sources
A log source is a URL a platform drains its logs into — Vercel, Render, Heroku, Fly.io, Cloudflare, AWS CloudWatch and others. Each has its own token, so you can roll or remove one without touching the rest. See Log Streaming.
| provider | Platform |
|---|---|
vercel, netlify, render, flyio, railway, heroku, digitalocean | Hosting platforms' log drains |
cloudflare, supabase | Logpush and Supabase log drains |
aws-cloudwatch, gcp-logging, azure-monitor | Cloud providers' log export |
docker-logs, k8s-logs, datadog-forwarder | Containers and forwarders |
/log-sourcesstatus is waiting (nothing yet), receiving (lines in the last 5 minutes) or idle. token is masked unless the key is admin and its owner isn't Read-only. GET /log-sources/:id adds the last day's line count.
providercurl "https://api.zharp.io/api/v1/log-sources" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{
"message": "Log sources retrieved",
"data": [{ "id": 4, "provider": "vercel", "name": "Vercel", "token": "zh_src_9f…", "lastReceivedAt": "…", "createdAt": "…", "status": "receiving", "linesPerMin": 12.4 }]
}/log-sourcesprovidernamecurl -X POST "https://api.zharp.io/api/v1/log-sources" \
-H "Authorization: Bearer $ZHARP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "vercel",
"name": "Vercel — production"
}'{ "message": "Log source created", "data": { "id": 4, "provider": "vercel", "name": "Vercel — production", "token": "zh_src_9f2c…", "status": "waiting", "linesPerMin": 0, … } }400An unknown provider, or a name that's too long.
/log-sources/:id/rollDELETE /log-sources/:id removes a source; lines it already sent stay.
idcurl -X POST "https://api.zharp.io/api/v1/log-sources/4/roll" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "Log source token rolled", "data": { "id": 4, "token": "zh_src_51ab…", … } }404No such source in this workspace.
/ingest/:provider/:tokenUp to 5,000 lines a request; more are dropped and counted in truncated.
providertokenAuthorization: Bearer, an X-Zharp-Token header or ?token=, for platforms that put it there.curl -X POST "https://api.zharp.io/api/v1/ingest/vercel/zh_src_9f2c…" \
-H "Content-Type: application/x-ndjson" \
--data-binary $'{"message":"GET / 200","projectName":"shop"}\n'{ "received": 1 }401A missing token, or one for another source or provider.413Over 10 MB.429The month's log allowance is used up.
Traces and APM
/agent/tracesresourceSpans), as a collector exports it, or a flat { "spans": [...] } list as the JavaScript APM SDK sends. Up to 5,000 spans a request.Spans already stored are skipped, so a retried batch isn't doubled. See Tracing Setup.
resourceSpansservice.name, deployment.environment and service.version are read from the resource.spanstraceId, spanId, parentSpanId, name, kind, serviceName, startTime/endTime (ISO) or …UnixNano, statusCode, attributes.curl -X POST "https://api.zharp.io/api/v1/agent/traces" \ -H "X-API-KEY: $ZHARP_INGEST_KEY" \ -H "Content-Type: application/json" \ -d @otlp-export.json
{ "statusCode": 200, "message": "Traces stored", "data": { "received": 120, "stored": 118 } }401No valid key.413Over 5,000 spans, or over 10 MB.
Reading traces
Every /apm read takes a window — 15m, 1h (default), 6h, 24h or 7d — and needs the monitors:view permission, which every role has.
/apm/serviceserrorRate is a percentage.
window15m, 1h (default), 6h, 24h or 7d.curl "https://api.zharp.io/api/v1/apm/services?window=24h" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{
"message": "Services retrieved",
"data": { "window": "24h", "services": [{ "serviceName": "checkout", "spans": 4210, "traces": 380, "errors": 12, "errorRate": 0.3, "avgMs": 41.3, "p95Ms": 180.2, "lastSeen": "…", "environments": ["production"], "versions": ["1.4.2"] }] }
}/apm/operationsservicewindow15m, 1h (default), 6h, 24h or 7d.limitcurl "https://api.zharp.io/api/v1/apm/operations?service=checkout" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "…", "data": { "window": "1h", "service": "checkout", "operations": [{ "name": "GET /orders/:id", "calls": 900, "errors": 3, "errorRate": 0.3, "avgMs": 22.1, "p95Ms": 95.4, "maxMs": 812 }] } }400Noservice.
/apm/tracesserviceoperationerrorsOnlytrue for traces with an error anywhere in them.window15m, 1h (default), 6h, 24h or 7d.limitcurl "https://api.zharp.io/api/v1/apm/traces?service=checkout&errorsOnly=true" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "…", "data": { "window": "1h", "traces": [{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "name": "POST /checkout", "serviceName": "web", "startTime": "…", "durationMs": 182.4, "statusCode": "ERROR" }] } }/apm/traces/:traceIdtraceIdcurl "https://api.zharp.io/api/v1/apm/traces/4bf92f3577b34da6a3ce929d0e0e4736" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{
"message": "…",
"data": {
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"startTime": "…",
"durationMs": 182,
"spanCount": 14,
"errorCount": 1,
"services": ["web", "checkout"],
"spans": [{ "spanId": "00f067aa0ba902b7", "parentSpanId": null, "name": "POST /checkout", "kind": "SERVER", "serviceName": "web", "durationMs": 182, "offsetMs": 0, "statusCode": "OK", "attributes": { … } }, …]
}
}404No such trace in this workspace.
/apm/errorsSession recordings from the browser SDK are listed with GET /apm/replays and fetched whole with GET /apm/replays/:sessionId.
servicewindow15m, 1h (default), 6h, 24h or 7d.curl "https://api.zharp.io/api/v1/apm/errors?window=24h" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{ "message": "…", "data": { "window": "24h", "errors": [{ "serviceName": "checkout", "name": "POST /pay", "message": "card_declined", "count": 17, "lastSeen": "…", "latestTraceId": "…" }] } }Agents
/agent/liststatus:active(seen in the last 2 minutes),idle, orofflineafter 15.kind:host,docker,kubernetesorsidecar.GET /agent/databaseslists the databases agents report, for private database monitors.PATCH /agent/:id/renamewith{ "name": "web-prod" }(anadminkey) sets its display name; an empty name goes back to the hostname.
curl "https://api.zharp.io/api/v1/agent/list" \ -H "Authorization: Bearer $ZHARP_API_KEY"
{
"statusCode": 200,
"message": "Agents retrieved successfully",
"data": [{ "id": 3, "hostname": "web-01", "customName": null, "os": "linux", "version": "1.6.2", "kind": "host", "status": "active", "lastSeenAt": "…", "metrics": { … } }]
}