Appearance
Text log lines
You don't need to change how your app logs. Send the lines as text/plain, one entry per line, and each line is recognized on its own.
sh
tail -n 200 storage/logs/laravel.log | curl -X POST https://edge.simpledatalogs.com/v1/ingest \
-H "Authorization: Bearer sdl_sec_…" \
-H "Content-Type: text/plain" \
--data-binary @-The body must be UTF-8. Anything else goes to Unparsed whole (invalid_utf8).
Recognized formats
Each line gets the first format that matches, in this order:
| Format | Example | Becomes |
|---|---|---|
| JSON object | {"level":"error","msg":"boom","time":1792232040} | Exactly like an NDJSON line, aliases included |
| Laravel / Monolog | [2026-10-13 12:00:01] production.ERROR: Payment failed {"order_id":7} {"request_id":"r-1"} | Time, level, message; props channel, plus context and extra when they decode |
| Syslog RFC 5424 | <34>1 2026-10-13T12:00:01.123Z web-1 app 1234 ID47 [sd@1 a="b"] message | Level from the PRI; props host, app, pid, msgid, structured_data |
| Syslog RFC 3164 | <34>Oct 13 12:00:01 web-1 app[123]: message | Needs TAG: or TAG[pid]:; without a PRI there is no level |
| Access log (nginx, Apache) | 203.0.113.9 - - [13/Oct/2026:12:00:01 +0000] "GET /pricing?plan=pro HTTP/1.1" 200 2326 "-" "Mozilla/5.0" 0.042 | Message GET /pricing 200; error for 5xx, warning for 4xx, else info; props method, path, status, bytes, protocol, referrer, user_agent, duration_ms |
| logfmt | ts=2026-10-13T12:00:01Z level=warn msg="disk almost full" free_mb=120 | Every token key=value, at least two. Known keys fill fields; the rest are typed props |
| Timestamp first | 2026-10-13 12:00:01,123 - ERROR - Payment failed | A time, an optional level word (ERROR, [warn], INFO:), then the message |
| Anything else | Payment failed for order 7 | A plain log line: the whole entry is the message, the time is the receive time, the level is empty |
A line that matches nothing is still kept, as a plain log line. It never goes to Unparsed.
- Times without an offset are UTC.
- In logfmt,
event=…makes the line an event, and identity keys such asdevice_id=…fill the identity. - A recognized line that fails a check, such as one with over 100 props, is kept as a plain line.
Stack traces
Stack traces join the entry they belong to, so one exception is one row.
- After a timestamp-first or plain line, these lines join it: indented lines,
#0 …,at …,Caused by…,Stack trace:,[stacktrace]andNext …Exception. - A Laravel entry runs until the next Laravel header, so its
[stacktrace],#0 …and closing"}lines join it. - JSON, syslog, access-log and logfmt lines are one line each.
text
2026-10-13 12:00:01 ERROR Payment failed: java.lang.IllegalStateException: card declined
at com.example.Checkout.pay(Checkout.java:42)
at com.example.Checkout.run(Checkout.java:17)
Caused by: java.net.SocketTimeoutException: read timed outThat body is one error row whose message holds all four lines.
An entry holds at most 200 lines and 64 KB; the rest starts a new plain entry. Entries never join across requests, so send a whole trace in one request.
Masking
Text lines often carry things you never meant to log. Before a text line is stored, these are masked in the message and in string props:
| Found | Stored as |
|---|---|
| Public IPv4 and IPv6 addresses | [ip] |
| Email addresses | [email] |
Bearer credentials, JWTs, well-known API keys, values under secret names such as password, api_key, token | [token] |
| Card numbers | [card] |
| Phone numbers | [phone] |
Private, loopback and link-local addresses stay. Access logs never keep the client IP, the ident or user fields, or any query string.
JSON is stored as sent
Masking applies to text lines and to the Datadog, Loki and Splunk drop-ins. JSON events, NDJSON and OTLP are structured data you chose to send, so they are stored as sent.
Retries count twice
Text lines carry no event_id, so a text request retried after a lost 202 is stored twice. If you need exact counts, send NDJSON with event_id and ts instead. The Laravel SDK does that for you.
Your own format
If your lines follow a fixed layout that none of these formats covers, such as api.stripe.com/v1/charges 1 3, set a text template on the deployment, and its keys' text bodies are read with it.
Next steps
- Text templates and Unparsed: typed templates for your own line layout.
- Vector and Fluent Bit: ship log files continuously.
- Laravel SDK: forward Laravel log channels with exact counts.