Ingest
One endpoint, one JSON object per line or up to 1,000 in a batch, one bearer token per source. Everything Pingtower knows starts here.
POST /v1/ingest, authenticated with a pti_ source token. Nothing in the body names a project or a source; the token decides.
Payload
| Field | Type | Meaning |
|---|---|---|
messageREQUIRED | string | The line. Template extraction runs on this. Up to 64 KiB. |
level | integer | Severity. 100 debug, 200 info, 300 warn, 400 error, 500 critical. Omitted or 0, it is 200. |
keys | object | Up to 128 named values, names up to 128 bytes, values up to 4096 bytes. Each value is substituted out of the message to form the template, is available to rule conditions, and can group alerts through identifier_keys. Scalars only; they are stored as strings. |
tags | string[] | Up to 64 free-form labels a rule can match on, each up to 256 characters. |
env | string | The environment, such as prod. Up to 128 bytes. |
host | string | The machine, container or pod. Up to 128 bytes. |
run | string | A label you choose: a deploy number, a CI build, a job run. Up to 128 bytes. |
trace_id | string | A trace id. Up to 64 bytes. |
{
"message": "payment gateway timeout after 30012ms for order o_48113",
"level": 400,
"keys": { "order": "o_48113", "gateway": "stripe", "latency_ms": 30012 },
"tags": ["billing", "prod"],
"env": "prod",
"host": "api-2"
}
The server timestamps the line on arrival. There is no client timestamp field.
env, host, run and trace_id are stored as their own indexed fields, which log search filters and counts by. Each is optional. When one is left out, a key of the same name fills it if its value fits, so a client that already sends env or host in keys needs no change. A top-level field over its size is a 400; an over-size key is just not copied.
Response
202 Accepted with {"alerted": true|false, "template_id": "…"} for a single event. A batch answers 200 with a result per event. alerted says whether any rule fired on this line, which makes a one-line smoke test possible. template_id is the SHA-256 of the recovered template.
| Status | Means |
|---|---|
400 | The body failed validation, such as a key or tag over its size. fields[] says which. |
401 | The bearer is missing, malformed, or not an ingest token. |
429 | Over the plan’s ingest rate, or the tenant’s storage quota. Retry-After says how long to wait. |
500 | The daemon could not store the line. |
What happens to a line
- Blocked values and secrets. A value someone blocked is replaced with
[redacted]first, so nothing after this step sees it. The line is then checked for things shaped like credentials; a match flags it and can open a secret warning, but detection never changes the line. - Template extraction. Every value in
keysis found in the message and replaced by{{ .name }}. Longest value first, every occurrence, soorder o_48113andorder o_48120share the templateorder {{ .order }}. A message with no key hits is its own template. - Rule evaluation. Every rule in the project is evaluated against the line’s level, source, tags, template and keys.
- Dedup. A line whose template matches an alert that is already firing bumps that alert’s count instead of opening a new one. See Rules for how
identifier_keyssplit that. - Retention. If the source has
retain_logs, which new sources do by default, the raw line is kept so the log tail and log search can show it. Retention is capped by plan and by the daemon’s flags; a cap costs you history, never an alert.
keys, nothing else. Put every variable part of the message, ids, durations, hosts, in keys, or two lines that differ only there will open two alerts.Batches
A body can carry many events: a JSON array of event objects, or NDJSON, one event object per line, sent with Content-Type: application/x-ndjson. A body that starts with [ is read as an array whatever its content type. Anything else is one event, and data after that one object is a 400.
| Limit | Value |
|---|---|
| Events per batch | 1 to 1,000. More, none, or an array that is not valid JSON is refused whole with 400. |
| Body | 256 KiB, the same cap as a single event. |
curl -s "$PT/v1/ingest" \
-H "Authorization: Bearer pti_…" \
-H "Content-Type: application/x-ndjson" \
--data-binary $'{"message":"user bob logged in","keys":{"name":"bob"}}\n{"message":"disk full","level":500}\n'
A batch answers 200 with one result per event, in order:
{
"accepted": 1,
"results": [
{ "status": 202, "template_id": "…", "alerted": false },
{ "status": 429, "error": "rate limited" }
]
}
status | Means |
|---|---|
202 | Stored. template_id and alerted as for a single event. |
400 | That event failed validation or was not JSON. error says why. The rest of the batch is unaffected. |
429 | Over the ingest rate (rate limited) or the storage quota (storage quota exceeded). |
500 | That event could not be stored. |
Each event takes its own share of the plan’s ingest rate, so a batch is not a way past it. Once the rate runs out partway through, that event and every one after it answer 429 while the ones before are kept. Resend only the events whose status is not 202 or 400.
Backpressure
Over the rate you get 429 and a Retry-After, or 429 slots in a batch. The addons agent spools to disk and drains when the tower is reachable again; if you write your own sender, do the same rather than dropping lines.