# Logs API

Logs. 16 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## GET /deployments/{deploymentId}/logs

Build and deploy logs of a deployment

Without `follow` returns the persisted lines as `{items:[{id,stream,line,createdAt}]}` (at most 10,000 lines). With `follow=true` streams `data: {stream,line,ts}` until the deployment finishes, then `event: end`. The `debug` stream (the agent's verbose trace: docker calls, health probe attempts, route changes, timings; at most 500 lines) is left out unless `streams` asks for it.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `deploymentId` (path, string, required)
- `follow` (query, boolean): Stream lines as server-sent events until the deployment finishes
- `streams` (query, string): Which streams to return: empty for build, deploy and runtime; all to include the debug stream; or a comma-separated list of build, deploy, runtime and debug

Response 200: JSON `{items}` without `follow`; a `text/event-stream` with `follow=true` (object)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /deployments/{deploymentId}/logs/download

Download a deployment's build and deploy log as a file

The persisted lines of the deployment (at most 10,000, plus 500 `debug` lines when asked for) as `format=txt` (default; `<time> [<stream>] <line>`, runtime lines without the tag) or `format=ndjson` (`{ts, stream, line}` per line), in the order they were recorded, optionally limited to a `from`/`to` time range. Secret values are redacted. Audited as `deployment.logs.download`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `deploymentId` (path, string, required)
- `format` (query, enum: txt | ndjson): txt (default) or ndjson.
- `streams` (query, string): Which streams to include: empty for build, deploy and runtime; all to include the debug stream; or a comma-separated list of build, deploy, runtime and debug
- `from` (query, string (date-time)): Only lines at or after this time (RFC 3339).
- `to` (query, string (date-time)): Only lines before this time (RFC 3339).

Response 200: The log as a file (`Content-Disposition: attachment`): plain text, one line per log line, or NDJSON (`{ts, stream, line, …}` per line). Secret values are masked. A range longer than the cap ends with a truncation note. (string)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error (`debug`)

## GET /environments/{environmentId}/logs

Merged runtime logs of every service of an environment

Same shape as the service log endpoint; entries also carry `serviceId` and `serviceName`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `environmentId` (path, string, required)
- `tail` (query, integer (int64))
- `follow` (query, boolean): Stream new lines as server-sent events

Response 200: JSON `{items}` without `follow`; a `text/event-stream` with `follow=true` (object)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error; 503 Service Unavailable

## GET /environments/{environmentId}/logs/history

Stored runtime logs of an environment

The same as the service endpoint for every service of the environment, merged by time. Entries carry `serviceId` and `serviceName`. Services whose server is offline are listed in `unavailable` instead of failing the request.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `environmentId` (path, string, required)
- `from` (query, string (date-time)): Only lines at or after this time (RFC 3339). Default: the oldest line the server keeps.
- `to` (query, string (date-time)): Only lines before this time (RFC 3339). Default: now.
- `q` (query, string): A log search query (the same language as the log views): free text, "phrases", level:error, -service:web, @status:[400 TO 499]. Evaluated on the server that stores the log.
- `limit` (query, integer (int64)): Page size (default 500, max 2000).
- `cursor` (query, string): Opaque cursor from a previous page's nextCursor.
- `order` (query, enum: desc | asc): desc (default): newest first. asc: oldest first.

Response 200: OK (LogHistoryPage)
  - `capturing` (boolean, required)
  - `items` (LogHistoryEntry[] | null, required)
  - `newest` (string (date-time))
  - `nextCursor` (string)
  - `oldest` (string (date-time)): The oldest line the server still has.
  - `partial` (boolean)
  - `queryErrors` (string[] | null): The parts of the query that were ignored.
  - `unavailable` (LogHistoryUnavailable[] | null): Services whose history could not be read (their server is offline, for example).
Errors: 400 Bad Request; 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error; 502 Bad Gateway; 504 Gateway Timeout

## GET /log-drains

List log drains

Destinations the servers ship runtime logs to. Credentials are never returned; `servers` carries the drain's health as each server's agent reports it. Requires the admin scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (DrainListOutputBody)
  - `items` (LogDrainView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## POST /log-drains

Add a log drain

Kinds: `loki` (push URL, tenant, labels; basic auth or bearer token), `otlp` (OTLP/HTTP logs URL; bearer token and/or headers), `syslog` (RFC 5424 over UDP, TCP or TCP+TLS) and `http` (JSON batches to any intake: Datadog, Better Stack, Axiom; credentials as headers, bearer or basic). Every connected server ships its lines from its own host, starting from the moment the drain is saved. Requires the admin scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): CreateDrainInputBody
  - `allProjects` (boolean): Default true: every project of the team. false: only projectIds.
  - `bufferMb` (integer (int64)): Undelivered backlog kept on disk per server (default 256, min 8).
  - `config` (DrainConfigBody, required)
  - `enabled` (boolean): Default true.
  - `kind` (enum: loki | otlp | syslog | http, required)
  - `name` (string, required)
  - `projectIds` (string[] | null)
  - `query` (string): Only ship lines matching this log search query.
  - `secrets` (LogDrainSecretsInput)
  - `serviceIds` (string[] | null): Narrow the scope to these services.

Response 201: Created (LogDrainView)
  - `allProjects` (boolean, required)
  - `bufferMb` (integer (int64), required): Undelivered backlog a drain keeps on each server's disk.
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `headerNames` (string[] | null, required): Names of the stored extra headers; their values are secret.
  - `http` (HTTPDrainConfig)
  - `id` (string, required)
  - `kind` (enum: loki | otlp | syslog | http, required)
  - `loki` (LokiDrainConfig)
  - `name` (string, required)
  - `otlp` (OTLPDrainConfig)
  - `projectIds` (string[] | null, required)
  - `query` (string, required): Only lines matching this log search query are shipped (empty: every line).
  - `secrets` (map of string, required): A mask for each credential that is set (token, username, password); never its value.
  - `servers` (LogDrainServerStatus[] | null, required): The drain's health on each server that reported it.
  - `serviceIds` (string[] | null, required)
  - `syslog` (SyslogDrainConfig)
  - `test` (LogDrainTestState, required)
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## GET /log-drains/{drainId}

Get a log drain

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `drainId` (path, string, required)

Response 200: OK (LogDrainView)
  - `allProjects` (boolean, required)
  - `bufferMb` (integer (int64), required): Undelivered backlog a drain keeps on each server's disk.
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `headerNames` (string[] | null, required): Names of the stored extra headers; their values are secret.
  - `http` (HTTPDrainConfig)
  - `id` (string, required)
  - `kind` (enum: loki | otlp | syslog | http, required)
  - `loki` (LokiDrainConfig)
  - `name` (string, required)
  - `otlp` (OTLPDrainConfig)
  - `projectIds` (string[] | null, required)
  - `query` (string, required): Only lines matching this log search query are shipped (empty: every line).
  - `secrets` (map of string, required): A mask for each credential that is set (token, username, password); never its value.
  - `servers` (LogDrainServerStatus[] | null, required): The drain's health on each server that reported it.
  - `serviceIds` (string[] | null, required)
  - `syslog` (SyslogDrainConfig)
  - `test` (LogDrainTestState, required)
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /log-drains/{drainId}

Update a log drain

Omitted fields stay as they are; empty credentials keep the stored ones (`secrets.clear` removes some). The kind cannot change.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `drainId` (path, string, required)

Request body (application/json, required): PatchDrainInputBody
  - `allProjects` (boolean)
  - `bufferMb` (integer (int64))
  - `config` (DrainConfigBody)
  - `enabled` (boolean)
  - `name` (string)
  - `projectIds` (string[])
  - `query` (string)
  - `secrets` (LogDrainSecretsInput)
  - `serviceIds` (string[])

Response 200: OK (LogDrainView)
  - `allProjects` (boolean, required)
  - `bufferMb` (integer (int64), required): Undelivered backlog a drain keeps on each server's disk.
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `headerNames` (string[] | null, required): Names of the stored extra headers; their values are secret.
  - `http` (HTTPDrainConfig)
  - `id` (string, required)
  - `kind` (enum: loki | otlp | syslog | http, required)
  - `loki` (LokiDrainConfig)
  - `name` (string, required)
  - `otlp` (OTLPDrainConfig)
  - `projectIds` (string[] | null, required)
  - `query` (string, required): Only lines matching this log search query are shipped (empty: every line).
  - `secrets` (map of string, required): A mask for each credential that is set (token, username, password); never its value.
  - `servers` (LogDrainServerStatus[] | null, required): The drain's health on each server that reported it.
  - `serviceIds` (string[] | null, required)
  - `syslog` (SyslogDrainConfig)
  - `test` (LogDrainTestState, required)
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /log-drains/{drainId}

Delete a log drain

Servers stop shipping and forget the drain's position.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `drainId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## POST /log-drains/{drainId}/test

Send a test line through a drain

Sends one test line from a connected server's agent, which is where reachability and credentials count, and returns the outcome (`ok`, a message, the HTTP status). The result is also kept on the drain's `test` state. Works on a disabled drain.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `drainId` (path, string, required)

Request body (application/json, required): TestDrainInputBody
  - `serverId` (string): Send from this server. Default: the first connected one.

Response 200: OK (LogDrainTestResult)
  - `message` (string, required)
  - `ok` (boolean, required)
  - `serverId` (string, required)
  - `serverName` (string, required)
  - `statusCode` (integer (int64))
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error; 503 Service Unavailable; 504 Gateway Timeout

## GET /servers/{serverId}/log-settings

A server's log history settings

Capture on or off, retention days and size caps, with the usage the agent last reported.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serverId` (path, string, required)

Response 200: OK (LogSettingsView)
  - `capture` (boolean, required): Capture the runtime output of this server's services. Off: nothing new is stored; what is stored stays until it expires.
  - `maxServerBytes` (integer (int64), required): Cap for the whole server; the oldest lines of any service go first.
  - `maxServiceBytes` (integer (int64), required): Cap per service; the oldest lines go first.
  - `online` (boolean, required)
  - `retentionDays` (integer (int64), required)
  - `usage` (LogServerUsage)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## PUT /servers/{serverId}/log-settings

Change a server's log history settings

Retention 1-90 days (default 7); size caps of at least 32 MB (defaults 500 MB per service, 5 GB per server; the oldest lines go first). The agent applies them at once.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serverId` (path, string, required)

Request body (application/json, required): PutSettingsInputBody
  - `capture` (boolean, required)
  - `maxServerBytes` (integer (int64), required)
  - `maxServiceBytes` (integer (int64), required)
  - `retentionDays` (integer (int64), required)

Response 200: OK (LogSettingsView)
  - `capture` (boolean, required): Capture the runtime output of this server's services. Off: nothing new is stored; what is stored stays until it expires.
  - `maxServerBytes` (integer (int64), required): Cap for the whole server; the oldest lines of any service go first.
  - `maxServiceBytes` (integer (int64), required): Cap per service; the oldest lines go first.
  - `online` (boolean, required)
  - `retentionDays` (integer (int64), required)
  - `usage` (LogServerUsage)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## GET /services/{serviceId}/logs

Runtime logs of a service

Without `follow` returns `{items:[{ts,line}]}`. With `follow=true` streams server-sent events `data: {ts,line,serviceId}`. Secret values are redacted. 503 `server_offline` when the server is not connected.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)
- `tail` (query, integer (int64))
- `follow` (query, boolean): Stream new lines as server-sent events
- `component` (query, string): Compose stacks only: show one component's lines (without it, every component, each line prefixed '<component> | ')

Response 200: JSON `{items}` without `follow`; a `text/event-stream` with `follow=true` (object)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error; 503 Service Unavailable (`server_offline`)

## GET /services/{serviceId}/logs/download

Download a service's stored runtime logs as a file

The service's runtime log from the server's own history (7 days by default) as `format=txt` (default; one `<time> <line>` per line) or `format=ndjson` (`{ts, stream, replica, deploymentId, line}` per line), oldest first, within an optional `from`/`to` range and `q` filter. Secret values are redacted exactly as in the log view. At most 200,000 lines: a longer range ends with a truncation note, so narrow it. The history comes from the server's agent: 503 `server_offline` while it is offline, 409 `history_disabled` when capture is off. Audited as `service.logs.download`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)
- `component` (query, string): Compose stacks only: only the lines of this component.
- `from` (query, string (date-time)): Only lines at or after this time (RFC 3339). Default: the oldest line the server keeps.
- `to` (query, string (date-time)): Only lines before this time (RFC 3339). Default: now.
- `q` (query, string): A log search query (the same language as the log views).
- `format` (query, enum: txt | ndjson): txt (default) or ndjson.

Response 200: The log as a file (`Content-Disposition: attachment`): plain text, one line per log line, or NDJSON (`{ts, stream, line, …}` per line). Secret values are masked. A range longer than the cap ends with a truncation note. (string)
Errors: 400 Bad Request; 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`history_disabled`); 422 Unprocessable Entity; 500 Internal Server Error; 502 Bad Gateway; 503 Service Unavailable (`server_offline`); 504 Gateway Timeout

## GET /services/{serviceId}/logs/history

Stored runtime logs of a service

A page of the service's runtime log from the server's own history (7 days by default), within an optional time range, filtered by the log search language. The server's agent answers, so history is unavailable (503 `server_offline`) while the server is offline. Secret values are redacted. Page with `nextCursor`; `partial: true` means the scan stopped early and the cursor continues it.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)
- `component` (query, string): Compose stacks only: only the lines of this component (stored lines keep the '<component> | ' prefix the agent adds).
- `from` (query, string (date-time)): Only lines at or after this time (RFC 3339). Default: the oldest line the server keeps.
- `to` (query, string (date-time)): Only lines before this time (RFC 3339). Default: now.
- `q` (query, string): A log search query (the same language as the log views): free text, "phrases", level:error, -service:web, @status:[400 TO 499]. Evaluated on the server that stores the log.
- `limit` (query, integer (int64)): Page size (default 500, max 2000).
- `cursor` (query, string): Opaque cursor from a previous page's nextCursor.
- `order` (query, enum: desc | asc): desc (default): newest first. asc: oldest first.

Response 200: OK (LogHistoryPage)
  - `capturing` (boolean, required)
  - `items` (LogHistoryEntry[] | null, required)
  - `newest` (string (date-time))
  - `nextCursor` (string)
  - `oldest` (string (date-time)): The oldest line the server still has.
  - `partial` (boolean)
  - `queryErrors` (string[] | null): The parts of the query that were ignored.
  - `unavailable` (LogHistoryUnavailable[] | null): Services whose history could not be read (their server is offline, for example).
Errors: 400 Bad Request; 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error; 502 Bad Gateway; 503 Service Unavailable (`server_offline`); 504 Gateway Timeout

## GET /task-runs/{runId}/logs

Output of a task run

Without `follow` returns the stored lines as `{items:[{id,stream,line,createdAt}]}` (redacted, at most 10,000 lines). With `follow=true` streams `data: {stream,line,ts}` until the run finishes, then `event: end` with `{status,exitCode}`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `runId` (path, string, required)
- `follow` (query, boolean): Stream lines as server-sent events until the run finishes

Response 200: JSON `{items}` without `follow`; a `text/event-stream` with `follow=true` (object)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error
