Proxy Logs
Section titled “Proxy Logs”Use these endpoints to query, summarize, and live-tail the proxy access logs stored per container. GET /_logs searches historical entries, GET /_logs/stats returns aggregate counts, and GET /_logs/stream opens a Server-Sent Events connection for real-time tailing.
The container-scoped base URL follows the pattern https://{projectId}-{containerId}-logs-1.{server}.containers.hoody.com. Replace {projectId}, {containerId}, and {server} with your values.
Query historical logs
Section titled “Query historical logs”GET /_logs
Section titled “GET /_logs”Search and filter through the stored request, response, and event logs. Supports pagination, time-range filtering, and narrowing by service, kind, level, or source.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | No | Page size. A value above 1000 is treated as 1000. Default: 200. |
offset | query | integer | No | Entries to skip: counted from the newest match, or with afterId from the oldest entry after the cursor. Ignored with last. Default: 0. |
projectId | query | string | No | Filter to a single project. |
containerId | query | string | No | Filter to a single container. |
serviceName | query | string | No | Filter to one service. The per-container logs URL ignores it, so filter the returned entries client-side. GET /_logs/stream honours it. |
level | query | string | No | Log level: exactly ONE of debug, info, warn, or error. A comma-separated value matches nothing. |
includeRequestBody | query | boolean | No | Include the request body payload in each entry. Default: false. |
includeResponseBody | query | boolean | No | Include the response body payload in each entry. Default: false. |
last | query | integer | No | Return only the newest N matching entries, oldest first, in one response: total is the number returned and offset does not apply. Takes precedence over afterId. |
afterId | query | integer | No | Return the entries whose row id (id) is greater than this, oldest first. total counts every such entry and offset pages through them. |
kind | query | string | No | Entry kind. One of request, response, or event. |
method | query | string | No | Filter by HTTP method. |
source | query | string | No | Filter by origin. One of backend or edge. |
sinceMs | query | integer | No | Only entries whose tsMs is at least this (Unix milliseconds). |
untilMs | query | integer | No | Only entries whose tsMs is at most this (Unix milliseconds). |
curl -G "https://{projectId}-{containerId}-logs-1.{server}.containers.hoody.com/_logs" \ -H "Authorization: Bearer $HOODY_TOKEN" \ --data-urlencode "limit=50" \ --data-urlencode "level=error" \ --data-urlencode "sinceMs=1730000000000"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-logs-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
for await (const item of client.proxyLogs.listIterator({ limit: 50, level: 'error', sinceMs: 1730000000000 })) { console.log(item);}{ "entries": [ { "id": 1923, "traceId": "5f2c1a7d-1b9e-4d2c-9c33-1f9b9a8d6b21", "tsMs": 1730001234567, "tsIso": "2025-10-27T10:20:34.567Z", "kind": "response", "level": "error", "projectId": "67e89abc123def456789abcd", "containerId": "890abcdef12345678901cdef", "serviceName": "tunnel", "method": "POST", "url": "/api/v1/orders", "clientIp": "203.0.113.42", "status": 502, "data": {}, "source": "backend" } ], "total": 137, "limit": 50, "offset": 0}| Field | Type | Description |
|---|---|---|
entries | array | Matching log entries. |
entries[].id | integer | SQLite row ID of the entry. |
entries[].traceId | string | UUID linking request/response/event records. |
entries[].tsMs | integer | Timestamp in Unix milliseconds. |
entries[].tsIso | string | Timestamp in ISO 8601 format. |
entries[].kind | string | One of request, response, or event. |
entries[].level | string | One of debug, info, warn, or error. |
entries[].projectId | string | Owning project ID. |
entries[].containerId | string | Owning container ID. |
entries[].serviceName | string | Originating service. |
entries[].method | string | HTTP method, when present. |
entries[].url | string | Request URL, when present. |
entries[].clientIp | string | Client IP, when present. |
entries[].status | integer | HTTP status code, when present. |
entries[].data | object | Arbitrary entry payload. |
entries[].source | string | One of backend or edge. |
total | integer | Total matching entries. |
limit | integer | Page size used. |
offset | integer | Offset used. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
audit_write_blocked | Log reads paused | The logs service is not accepting reads right now. | Retry later. |
audit_write_failed | Log read could not be recorded | Recording the read failed, so the read was refused. | Retry the request. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Log reads rate limited | This container’s logs were read too often in a short time. | Wait for the number of seconds in Retry-After (2 s), then retry. |
audit_quota_exceeded | Log read quota used up | This project has used up its share of log reads for now. Other projects are not affected. | Wait for the number of seconds in Retry-After (60 s), then retry. |
403 Forbidden
Section titled “403 Forbidden”Returned when the request is denied.
{ "error": "Forbidden", "errorCode": "LOGS_FORBIDDEN"}| Field | Type | Description |
|---|---|---|
error | string | Human-readable error message. |
errorCode | string | Machine-readable error code. |
Get log statistics
Section titled “Get log statistics”GET /_logs/stats
Section titled “GET /_logs/stats”Returns aggregate counts of stored log entries broken down by level, project, container, and service. Useful for dashboards and quick health checks.
This endpoint takes no parameters.
curl "https://{projectId}-{containerId}-logs-1.{server}.containers.hoody.com/_logs/stats" \ -H "Authorization: Bearer $HOODY_TOKEN"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-logs-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.proxyLogs.getStats();{ "total": 48213, "byLevel": { "debug": 30012, "info": 14001, "warn": 3800, "error": 400 }, "byProject": { "67e89abc123def456789abcd": 27013, "fb02feed5678ab901234cdef": 21200 }, "byContainer": { "890abcdef12345678901cdef": 48213 }, "byService": { "tunnel": 30012, "egress": 12010, "control": 6191 }}| Field | Type | Description |
|---|---|---|
total | integer | Total number of stored log entries. |
byLevel | object | Counts grouped by level (debug, info, warn, error). |
byProject | object | Counts grouped by project ID. |
byContainer | object | Counts grouped by container ID. |
byService | object | Counts grouped by service name. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
audit_write_blocked | Log reads paused | The logs service is not accepting reads right now. | Retry later. |
audit_write_failed | Log read could not be recorded | Recording the read failed, so the read was refused. | Retry the request. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Log reads rate limited | This container’s logs were read too often in a short time. | Wait for the number of seconds in Retry-After (2 s), then retry. |
audit_quota_exceeded | Log read quota used up | This project has used up its share of log reads for now. Other projects are not affected. | Wait for the number of seconds in Retry-After (60 s), then retry. |
Live-tail logs over Server-Sent Events
Section titled “Live-tail logs over Server-Sent Events”GET /_logs/stream
Section titled “GET /_logs/stream”Opens a persistent Server-Sent Events connection that streams new log entries as they arrive. Use this for live-tail UIs and alerting pipelines.
Framing
Section titled “Framing”Each live log entry is one frame with a numeric id: line:
id: 12345data: <LogEntry JSON>Not every frame has an id:. A fresh connection (no Last-Event-ID) first receives one id-less data: frame holding a JSON array of recent matching entries, when any exist; with none, its first frame is a live entry with its id:. The gap event below has no id: either, and heartbeats are comments. Keep the last id you received; a frame without one does not change it.
An entry whose frame would exceed 256 KiB is sent as a stub that keeps its id and carries truncated: true and originalFrameBytes.
Reconnect resume
Section titled “Reconnect resume”Clients MAY send Last-Event-ID: <id> on reconnect, with the id of the last frame they received. The server then replays the buffered frames newer than that id that match the stream’s filters, instead of the initial batch.
The replay buffer holds at most the 2,000 most recent frames and at most 8 MiB of them, whichever limit is reached first; the oldest frames are dropped first. Every stream on the server shares this buffer, so one container’s entries can leave it after far fewer than 2,000 of its own. The server can also end a stream once it has sent a per-connection byte budget; reconnecting with Last-Event-ID continues from where it stopped.
Named events
Section titled “Named events”event: gap: sent first on a resume when the buffer no longer reaches back to yourLast-Event-ID; the entries in between are not replayed. It has noid:. Its data is{"after": "<your Last-Event-ID>", "resumedFrom": "<oldest buffered id>"}. Read what you missed fromGET /_logs(match ontraceIdandtsMs), then carry on with the frames that follow.event: purged: carries anid:anddata: {}. It stands in for a buffered entry whose logs were deleted, so your cursor still moves past it.event: scope-destroyed: container destroyed; stream closes immediately after. Clients should exit cleanly.event: reset: the stream is being reset and closes right after. Discard the lastidyou saw and reconnect withoutLast-Event-ID.
Server restart
Section titled “Server restart”A restart ends every open stream without an event. Frame ids then resume at least 10,000 past the last value the server saved, and the replay buffer starts empty, so reconnecting with an id from before the restart gets a gap event followed by new frames.
A : keepalive\n\n comment heartbeat every 5 s keeps the connection alive. The server also sends one : open\n\n comment as soon as the stream is set up, after any initial batch, replay or gap event, so a stream with nothing to deliver yet still starts at once. Both are SSE comments, not frames: ignore them.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | query | string | No | Filter to a single project. |
containerId | query | string | No | Filter to a single container. |
serviceName | query | string | No | Filter the stream to one service, e.g. tunnel. Honoured here, unlike GET /_logs. |
kind | query | string | No | Filter by entry kind. One of request, response, or event. |
level | query | string | No | Filter by log level. One of debug, info, warn, or error. |
source | query | string | No | Filter by origin. One of backend or edge. |
afterId | query | integer | No | Limits the initial batch of a fresh connection to entries whose row id (id) is greater than this: the same id that GET /_logs returns and takes as afterId. Live frames are unaffected. |
Last-Event-ID | header | string | No | The numeric id of the last event received. On reconnect the server skips buffered entries whose id is at most this value. |
curl -N "https://{projectId}-{containerId}-logs-1.{server}.containers.hoody.com/_logs/stream?level=error&serviceName=tunnel" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Last-Event-ID: 1923"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-logs-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.proxyLogs.stream({ level: 'error', serviceName: 'tunnel', LastEventID: '1923' });id: 1924data: {"id":1924,"traceId":"5f2c1a7d-1b9e-4d2c-9c33-1f9b9a8d6b21","tsMs":1730001234568,"tsIso":"2025-10-27T10:20:34.568Z","kind":"response","level":"error","projectId":"67e89abc123def456789abcd","containerId":"890abcdef12345678901cdef","serviceName":"tunnel","method":"POST","url":"/api/v1/orders","clientIp":"203.0.113.42","status":502,"data":{},"source":"backend"}
: keepaliveThe body is a continuous Server-Sent Events stream. Each id-bearing data: line carries a JSON-serialized LogEntry matching the shape returned by GET /_logs. The first frame of a fresh connection, when there are recent matching entries, is an id-less data: frame whose payload is a JSON array of them; when there are none to replay, the first frame is a live entry carrying its own id: line. Subsequent frames after a reconnect may begin with event: gap or event: purged as described above. A : keepalive comment is emitted every 5 s, and one : open comment when the stream is set up.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
audit_write_blocked | Log reads paused | The logs service is not accepting reads right now. | Retry later. |
audit_write_failed | Log read could not be recorded | Recording the read failed, so the read was refused. | Retry the request. |
check_timeout | Permission check timed out | The permission check for opening the stream did not finish in time. | Retry the request. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Log reads rate limited | This container’s logs were read too often in a short time. | Wait for the number of seconds in Retry-After (2 s), then retry. |
audit_quota_exceeded | Log read quota used up | This project has used up its share of log reads for now. Other projects are not affected. | Wait for the number of seconds in Retry-After (60 s), then retry. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
scope_mismatch | Stream scope mismatch | The project or container named in the request is not the one this stream was opened for. | Open the stream again from the container’s own logs URL. |
403 Logs token missing or invalid, or the gateway policy denied the request
Section titled “403 Logs token missing or invalid, or the gateway policy denied the request”Returned when the request is denied.
429 Rate limited
Section titled “429 Rate limited”Returned when rate limits are exceeded; reconnect with backoff.