Function Logs

View historical and live execution logs for your functions.

View historical and live execution logs for your functions.

Read runtime and deployment logs with a platform token belonging to the project owner or a project access token. A read_only project token is sufficient. Project end-user sessions, anon keys, and service keys do not grant log access. Keep project tokens on your server and rotate them through project token management.

For SDK examples, see JavaScript, Python, and Ruby.

Runtime Logs

Use this endpoint for paginated historical runtime logs and text or structured-field search:

POST /projects/{id}/logs/search
Authorization: Bearer PLATFORM_TOKEN
Content-Type: application/json

Set resource.type to function. Omit resource.ids to return logs across all functions in the project, or include one or more function IDs to return logs for selected functions. A durable function is read the same way, by its own ID; its runtime logs cover every execution, including what each resume logged again. The q field is optional; leave it blank or omit it to list stored logs using structured filters only.

curl -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/search" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":{"type":"function","ids":["FUNC_ID"]}}'

Set resource.kind to durable or standard to read one kind of function, however many the project has:

curl -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/search" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":{"type":"function","kind":"durable"}}'

Response:

{
  "data": [
    {
      "id": "log/us-east-1/01HKG3W9M0A5V7R2J6Z0Q6Y4S9",
      "timestamp": "2024-01-01T12:00:00Z",
      "level": "info",
      "body": {
        "message": "User logged in",
        "user_id": "usr_123"
      },
      "resource": {
        "type": "function",
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "login"
      },
      "region": "us-east-1"
    }
  ],
  "limit": 100,
  "has_more": true,
  "next_cursor": "eyJwayI6..."
}

Internal platform runtime logs are filtered out; only application logs emitted by deployed functions are returned.

A Python or Ruby function's uncaught error, or one raised while it starts, is one error row whose body is the runtime's report, with the error's errorType, errorMessage, and stackTrace. Ruby logs a line before it, such as Error raised from handler method, as its own row.

A Python exception you log with logger.exception(), or with exc_info=True, keeps the whole log record as its row's body: your message, and the exception's errorType, errorMessage, and stackTrace. Node.js's console.error("...", error) already writes the stack into the message, so its body is the message. In local mode, an uncaught error's body is its record in every runtime.

Historical log rows identify the owning function or frontend with a required resource object. Deployment log rows include a deployment object when available.

Filtering

Supported request body fields:

FieldDescription
resource.typeRequired. Use function.
resource.idsOptional function IDs. Omit or pass an empty array to search all functions in the project.
resource.kindOptional. standard or durable reads only functions of that kind; omit it for both. With resource.ids, IDs of the other kind return no logs. A cursor is bound to the kind.
qOptional query. Unqualified terms search the body. Supports quoted text, implicit AND, AND/OR/NOT, parentheses, and the fields body, level, region, invocation.id, resource.id, and resource.name. The resource-name aliases function, frontend, and database are also supported. level accepts trace, debug, info, warn, error, and fatal, plus the aliases log, information, warning, err, and critical.
limitMax records to return. Default 100, max 1000.
cursorOpaque cursor from the previous response.
start_timeInclusive lower bound as an RFC3339 timestamp.
end_timeInclusive upper bound as an RFC3339 timestamp.
# Find "checkout_failed" in warn and error logs in one region
curl -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/search" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":{"type":"function","ids":["FUNC_ID"]},"q":"level:(warn OR error) region:us-east-1 body:checkout_failed","start_time":"2024-01-01T12:00:00Z","end_time":"2024-01-01T13:00:00Z"}'

Pagination

When has_more is true, pass the response next_cursor back as the cursor field on the next request with the same filters. Keep paging until has_more is false.

A search reads for a bounded time per request, so a page can hold fewer than limit events, or none, while has_more is true. This happens most with selective queries over a wide time range. Continue from next_cursor rather than treating a short or empty page as the end of the results.

curl -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/search" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":{"type":"function"},"limit":100}'

curl -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/search" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":{"type":"function"},"limit":100,"cursor":"eyJwayI6..."}'

Activity

Count logs over time to draw a histogram. The request takes the same resource and q fields as search:

curl -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/activity" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":{"type":"function"},"q":"level:error","start_time":"2024-01-01T12:03:10Z","end_time":"2024-01-01T13:03:10Z","bucket_count":4}'

Response:

{
  "data": [
    {
      "start_time": "2024-01-01T12:00:00Z",
      "end_time": "2024-01-01T12:30:00Z",
      "counts": {
        "levels": { "debug": 0, "error": 3, "fatal": 0, "info": 0, "trace": 0, "warn": 0 },
        "regions": { "us-east-1": 3 },
        "resource_ids": { "550e8400-e29b-41d4-a716-446655440000": 3 }
      },
      "total": 3
    },
    {
      "start_time": "2024-01-01T12:30:00Z",
      "end_time": "2024-01-01T13:00:00Z",
      "counts": {
        "levels": { "debug": 0, "error": 1, "fatal": 0, "info": 0, "trace": 0, "warn": 0 },
        "regions": { "us-east-1": 1 },
        "resource_ids": { "550e8400-e29b-41d4-a716-446655440000": 1 }
      },
      "total": 1
    },
    {
      "start_time": "2024-01-01T13:00:00Z",
      "end_time": "2024-01-01T13:30:00Z",
      "counts": {
        "levels": { "debug": 0, "error": 0, "fatal": 0, "info": 0, "trace": 0, "warn": 0 },
        "regions": {},
        "resource_ids": {}
      },
      "total": 0
    }
  ],
  "total": 4
}

Buckets follow these rules:

  • Counts cover the half-open window [start_time, end_time). An event exactly at end_time is not counted.
  • bucket_count is a maximum, 24 by default and at most 96. Volcano picks the smallest width that covers the window in that many buckets, so a response can hold fewer buckets than requested.
  • Widths come from a fixed set: 1s, 2s, 5s, 10s, 15s, 30s, 1m, 2m, 5m, 10m, 15m, 30m, 1h, 2h, 3h, 6h, 12h, 1d, then whole days.
  • Bucket edges fall on UTC multiples of the width, so they stay put as the window moves. The first and last buckets can extend past the window; they count only events inside it.
  • bucket_count: 1 returns one bucket that spans exactly the window.
  • The window is limited to the plan's log retention (HOBBY: 1 day, SUPERAGENT: 30 days). An older start_time is moved up to the retention limit.
  • Counts across several resources or deployments come from Volcano's activity index. Logs from before a region's index began are not counted; search still returns them.

Live Streaming

Use the stream endpoint to live-tail runtime or deployment logs:

POST /projects/{id}/logs/stream
Authorization: Bearer PLATFORM_TOKEN
Content-Type: application/json
Accept: text/event-stream

The request body supports resource, q, start_time, and limit. The q field uses the same syntax as search requests. Do not send cursor or end_time; use /logs/search for range backfills.

curl -N -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/stream" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"resource":{"type":"function","ids":["FUNC_ID"]},"q":"level:error"}'

Each log event is sent as Server-Sent Events:

id: STREAM_CURSOR
event: log
data: {"id":"LOG_EVENT_ID","timestamp":"2024-01-01T12:00:00Z","level":"info","body":"User logged in","resource":{"type":"function","id":"FUNC_ID","name":"login"},"region":"us-east-1"}

The SSE id field is an opaque stream cursor. To reconnect without replaying recent events, send the most recent id as the Last-Event-ID header or last_event_id query parameter. The log event's stable ID remains in data.id.

The cursor is bound to the request body: resource, q, start_time, and limit must match the original request when reconnecting. Resuming with a changed body returns 400 — open a new stream (without a cursor) for the new selector instead.

This is a live tail, not a gap-free backfill. On connect (or reconnect) the server delivers at most limit of the most recent matching events from the cursor position and then follows new events. If a stream stays disconnected long enough for more than limit events to accumulate, the older events in that gap are not replayed — use /logs/search to backfill a specific time range.

The stream sends : keepalive comments while idle. If a transient read fails after the stream is open, the server emits an event: warning payload and continues retrying. The warning's error field carries a safe message; internal failures are reported generically and logged server-side.

Deployment Logs

Deployment build logs use the same project log search endpoint. Add a resource.deployments.ids selector to read one or more deployments for the selected function resources. New function and frontend builds show dependency installation, project build and syntax-check output, concise progress, and actionable errors. Platform setup, packaging, and publishing diagnostics stay in internal operator logs. Existing historical logs are unchanged.

POST /projects/{id}/logs/search
Authorization: Bearer PLATFORM_TOKEN
curl -X POST "https://api.volcano.dev/projects/PROJECT_ID/logs/search" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resource":{"type":"function","ids":["FUNC_ID"],"deployments":{"ids":["DEPLOYMENT_ID"]}},"limit":100}'

Use the same selector with /logs/stream to live-tail deployment logs.

Deployment log rows include deployment context:

{
  "resource": {
    "type": "function",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "login"
  },
  "deployment": {
    "id": "8c6c7c2e-e3ff-42a5-ae4d-650ef9b45746"
  }
}

Installed Packages

After installing dependencies, a function build lists each dependency your manifest declares, with the version that was installed:

Installing dependencies...
added 42 packages in 3s
Installed packages:
  @volcano.dev/sdk@1.7.2
  pg@8.13.1
  sharp (not installed)

The list covers dependencies and optionalDependencies in package.json, the packages named in requirements.txt and the files it includes with -r, and the Gemfile gems outside the development and test groups. A requirement given as a URL or path, editable (-e) or not, is listed when it names its package, as in name @ git+https://... or #egg=name. The list shows at most 50 entries.

If a package your function imports is missing from the list, declare it in the manifest the build installs from. For Node.js, declare it under dependencies: the list leaves out devDependencies, which your function should not rely on at runtime. An entry marked (not installed) is declared, but the install did not add it; (optional, not installed) is an optional dependency the install skipped, such as one for another platform; (installed, no version) is a package the install added whose package.json names no version, as a local file: dependency may. A requirements file included from a URL is marked (not listed): the install reads it, but the list does not. The URL is shown without its credentials or query, or as <url> when it uses an environment variable. A requirement marked (not installed; environment marker) has a marker, such as ; sys_platform == "win32", that excludes the build's platform, so the install skipped it on purpose.

Read Errors

An internal failure while loading log resources returns HTTP 500 with a message identifying the operation, for example:

{ "error": "failed to list functions" }

The message omits internal connection details. Retry the request; include its X-Request-Id if you contact support.

Log levels

Every runtime log line has one of the levels trace, debug, info, warn, error, or fatal:

  • Console and logging calls. console.error, console.warn, and the other console methods set the level, as do Python's logging calls. In local mode, Python's print and Ruby's puts set it too: info, or error on standard error, whatever the line contains, including a JSON line such as structlog prints.
  • JSON lines with a level. A JSON object written straight to standard output, as pino, bunyan, and structlog do, keeps the level in its top-level level field, read with the table below. Names match in any case. The whole line must be that one well-formed object, nested at most 31 levels deep.
  • Other lines. A line written straight to standard output otherwise takes the level of its first level word: fatal; error, errored, fail, failed, failing, or failure for error; warn, warned, or warning for warn; debug; or trace. So warning: request failed is warn. A line with none of these is info, or error when it was written to standard error in local mode.

A JSON line's level maps to a stored level, with the same names the level search field accepts:

Stored levellevel numberlevel names
trace10trace
debug20debug
info30info, information, log
warn40warn, warning
error50error, err
fatal60fatal, critical
import pino from 'pino';

const logger = pino();
logger.error({ orderId: 'ord_123' }, 'card declined'); // {"level":50,...} is stored at error
logger.warn('retrying payment'); // {"level":40,...} is stored at warn

Logging Best Practices

Use structured logging so important fields can be indexed:

console.log(
  JSON.stringify({
    level: 'info',
    action: 'user_login',
    user_id: user.id,
    timestamp: new Date().toISOString(),
  }),
);

Use consistent log levels:

console.log('INFO:', ...);
console.error('ERROR:', ...);
console.warn('WARN:', ...);

See Also

On this page