---
name: z8log-send-logs
description: Add structured logging to apps with Z8Log.
disable-model-invocation: true
---

# Send logs to Z8Log

Use the project's language, HTTP client, JSON library, and configuration style. Add logging for the work the user asked for. Reuse an existing logging helper when possible. This skill works on its own.

## Connection

- Default base URL: `https://z8log.com`. Use the user's server URL for a self-hosted install.
- Read `Z8LOG_DATASET_ID` and `Z8LOG_API_KEY` from the project's configuration, or use its existing equivalent names. Both values are available on the dataset's Setup tab.
- Send `POST /api/logs/{datasetId}` with `Authorization: Bearer <dataset API key>` and `Content-Type: application/json`.
- The dataset key starts with `dataset_` and can read and write one dataset. An Admin key is not a log credential. Keep the key in trusted server code or IDE configuration; do not expose it in browser or distributed client code.
- Send one JSON object per request. There is no batch endpoint.

## Log fields

| Field | Type | Rules |
| --- | --- | --- |
| `message` | string, required | Non-empty; at most 1,024 UTF-8 bytes (1 KiB). Describe what happened. |
| `level` | string, optional | `DEBUG`, `INFO`, `WARNING`, or `ERROR`. Case-insensitive. Missing or unknown values become `INFO`. |
| `flowId` | string or null, optional | Groups logs from one operation. Trimmed; must be non-empty when supplied as a string, at most 128 characters after trimming. |
| `data` | any JSON, optional | At most 2,048 UTF-8 bytes (2 KiB) after server-side JSON serialization. Missing or null is stored as null. Prefer an object with stable fields. |
| `timestamp` | ISO 8601 string or null, optional | Event time. Use `Z` or an explicit timezone offset. Missing or null uses server time. |

Message and data limits are the same on every plan. Measure UTF-8 bytes, not string length; emoji and non-ASCII text can use multiple bytes. The `flowId` limit still counts JavaScript string length. Use the project's JSON serializer for the whole body.

## Plan limits

Limits follow the dataset owner's plan and apply separately to each dataset.

| Plan | Sustained log rate | Maximum burst | Searchable history |
| --- | --- | --- | --- |
| Free | 1 log every 5 seconds | 3 logs | 3 days |
| Basic | 1 log per second | 5 logs | 7 days |
| Pro | 5 logs per second | 25 logs | 30 days |

Each admitted write uses one burst slot; slots refill at the sustained rate up to the maximum burst. All keys, clients, and write paths for the same dataset share this allowance. Rotating a key does not increase it. There is no monthly log quota. Pace delivery and use a bounded queue or an explicit drop policy for excess logs; do not accumulate an unlimited backlog.

A successful write can be hidden from queries if its timestamp is older than the plan's history window or ahead of server time. These logs remain stored. Do not resend an accepted log merely because a query does not show it.

## Useful logs

- Use `DEBUG` for diagnostic detail, `INFO` for normal events, `WARNING` for recoverable issues, and `ERROR` for failures.
- Keep messages short. Put values in `data`. Use a shared `flowId` for related events across an operation.
- Keep field names and types stable. Store measurements as JSON numbers. Fields such as `service`, `event`, `durationMs`, and `status` can help when they fit the app.
- For fields that users will query, use keys that start with a letter or underscore and contain only letters, numbers, and underscores. Nested objects are supported. Keys with dots, spaces, or hyphens cannot be addressed by the formula language.
- Leave passwords, tokens, API keys, and unnecessary personal data out of messages and data.

## Delivery and errors

Success is HTTP `200` with `{"ok":true}`. Check the HTTP status; do not treat a completed HTTP request as proof that the log was stored. Errors return a JSON `error` message.

| Status | Meaning |
| --- | --- |
| `400` | Invalid log fields. Fix the payload before retrying. |
| `401` | Missing or invalid dataset key. Check configuration. |
| `403` | `code: "account_unavailable"`: the owning account no longer exists. Retrying the same credentials will not fix it. |
| `404` | Dataset not found. Check the dataset ID and server. |
| `429` | `code: "ingestion_rate_limited"`: this write was rejected. Wait at least the `Retry-After` header's seconds before a bounded retry. The JSON body also has `retryAfter`. |
| `500` | The server could not store the log, or the request contained malformed JSON. |
| `503` | Authorization or plan lookup is unavailable. `code: "plan_unavailable"` can mean missing/unknown Clerk plan configuration or a billing lookup failure. It is not a bad log payload; do not rewrite it or rotate keys to fix this. |

Use a timeout and bounded retries for confirmed rate-limit rejections. An admitted write can consume a slot even if storage fails. A timeout or server failure can leave the write outcome uncertain; do not blindly retry, because another submission may duplicate a stored log. `flowId` does not deduplicate writes. For persistent `503`, report the failure for the operator to check authorization and Clerk plan configuration. Keep logging failures from breaking normal app work unless the user requires that behavior. Do not send a logging failure back through the same failing logger in a loop.

When reporting work, distinguish code added from logs actually sent and accepted. Send test logs only within the user's authorized task.

API reference: https://z8log.com/docs/api. For a self-hosted server, use its `/docs/api` page.
