---
url: https://simpledatalogs.com/docs/api/log-lines.md
description: >-
  Send text/plain log lines in any common format; JSON, Monolog, syslog, access
  logs, logfmt and timestamp-first lines are recognized, and stack traces join
  their entry.
---

# 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](/api/events#field-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 as `device_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]` and `Next …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 out
```

That 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.

::: info 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](/api/ingest#body-shapes) with `event_id` and `ts` instead. The [Laravel SDK](/sdks/laravel) 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](/api/templates) on the deployment, and its keys' text bodies are read with it.

## Next steps

* [Text templates and Unparsed](/api/templates): typed templates for your own line layout.
* [Vector and Fluent Bit](/integrations/log-shippers): ship log files continuously.
* [Laravel SDK](/sdks/laravel): forward Laravel log channels with exact counts.
