Log drains
Forward everything your application writes to your own log system, continuously and signed.
nesly collects what your application writes as it is written and keeps it for seven days, so nesly logs can answer even after the instance that wrote it is gone. A drain is the same output going somewhere else: your log platform, or a service of your own, arriving a few seconds behind and waiting for you when you come looking.
Everything the project prints goes to the drain — every instance of every deployment, and every background process.
Adding a drain
nesly logs drains add https://http-intake.example.com/api/v2/logs \
--header 'DD-API-KEY: your-key' --description 'datadog, eu'
nesly logs drains list
nesly logs drains test ldn_… # send one line now and see the answer
nesly logs drains update ldn_… --disable
nesly logs drains roll ldn_… # issue a new signing secret
nesly logs drains remove ldn_…Most collectors want their own key in a header of their choosing; add as many as five with--header. The values are stored encrypted and are never shown again — a listing gives you the names only.
Adding a drain prints a signing secret. It is shown once and is not recoverable — if you lose it, issue a new one with logs drains roll, which invalidates the old one immediately.
A new drain starts from the next line written. The output already collected stays where it is; registering a destination does not empty a week of history into it. A project may have up to two drains.
What arrives
One POST carries up to 500 lines as newline-delimited JSON — one object per line, no enclosing array — with the content typeapplication/x-ndjson.
{"time":"2026-09-07T09:14:02.114882Z","project":"prj_8f2k…","deployment":"dep_a1b2…","pod":"dep-a1b2-00003-deployment-7d9f8b6c5d-x2k9p","message":"listening on :3000"}
{"time":"2026-09-07T09:14:07.902551Z","project":"prj_8f2k…","worker":"mailer","pod":"worker-mailer-7d9f8b6c5d-q4t1z","message":"queue empty"}| time | When the line was written, from the container itself, in RFC 3339 with nanoseconds. |
| project | The project's id. |
| deployment | The deployment whose container wrote it. Absent for a background process. |
| worker | The background process that wrote it, by its name in your Procfile. Absent for a deployment. |
| pod | The instance that wrote it — the answer to 'which replica said this'. |
| message | The line, exactly as your application printed it. |
A very long line is cut at 8 KB and marked, so a runaway line cannot fill a batch on its own. Your own headers are sent with every request, followed by ours:
| Nesly-Delivery | This batch's id. A batch you have seen before is a retry, not new lines. |
| Nesly-Project | The project the batch belongs to. |
| Nesly-Lines | How many lines are in the body. |
| Nesly-Timestamp | When the request was signed, in Unix seconds. |
| Nesly-Signature | v1= followed by the hex HMAC-SHA256 of the timestamp, a full stop, and the body. |
The signature is the same scheme as webhooks— HMAC-SHA256(secret, timestamp + "." + body) over the raw bytes, with the timestamp inside the signed material so a captured batch cannot be replayed with a fresh one. The verifier on that page works here unchanged.
Delivery, retries, and what happens when your receiver is down
Answer with any 2xx once you have taken the batch. Nothing moves forward until you do: a batch that fails is sent again from the same place, so a receiver that was restarting does not leave a hole. Retries slow down as failures repeat, to at most one attempt every five minutes, and speed up again the moment a delivery succeeds.
Delivery is at-least-once. A batch can arrive twice — after a network failure that your server saw and we did not, for instance — so useNesly-Delivery to recognise one you have already stored.
Collected output is kept for seven days. If a drain is switched off or failing for longer than that, the lines it never received are removed with everything else of that age. When that happens we say so, twice: nesly logs drains list reports it, and the next batch that does get through starts with a line saying lines were missed. A drain that quietly skipped a day would be worse than no drain at all.
Addresses we will not send to
Your receiver is reached from inside our network, so it has to be somewhere on the public internet. These are refused when you add them, and checked again on every attempt against the address actually being connected to — a name that later resolves somewhere private is refused at that moment.
| Anything that is not https | The lines and their signature would travel in clear. |
| Loopback and private addresses | 127.0.0.1, 10.x, 172.16–31.x, 192.168.x, fd00::/7 and the IPv6 forms of all of them. |
| Link-local addresses | 169.254.x, including the cloud metadata address, and fe80::/10. |
| Carrier-grade NAT | 100.64.0.0/10. |
| Any port but 443 | https is the only scheme accepted, so 443 is the only port reached — including after a redirect. |