Session lifecycle events reference
Every transition a workspace session goes through is one structured log record tagged with kasm.metric_name. This page lists each event, the fields it carries, where it is emitted, and how to read it back. Available in Kasm Workspaces 1.20.0 and later.
For the counters that pair with these events, see Observability metrics. For the environment variable that adds these fields to the JSON log file, see Observability configuration.
Where the records go
Kasm application logs are emitted on two channels.
| Channel | Carries the structured fields | Notes |
|---|---|---|
| OTLP to the collector, then ClickHouse | Yes, in the LogAttributes map | Also the source for the Splunk HEC, OTLP/HTTP, and syslog forwarders |
/opt/kasm/current/log/api.log | No | One JSON object per line with timestamp, level, logger, module, line, and message |
The api.log file carries the six base fields by default. Its message names the session and the transition. To add the attributes below to the file, list them in KASM_LOG_FILE_EXTRA_FIELDS. The file writes them with dots replaced by underscores, so kasm.kasm.id appears as kasm_kasm_id. The trace ID is never in the file. A consumer that needs correlation reads the OTLP data.
One event, client detached, is not emitted by the API at all. The OTel Collector derives it from the proxy's nginx access log, so it exists in ClickHouse and in any collector forwarder but never in api.log.
Common fields
| Purpose | Field on the record | Source |
|---|---|---|
| Timestamp | Timestamp (ClickHouse) or timestamp (file) | The log record's own time, UTC, millisecond precision or better |
| Event type | kasm.metric_name | One of the values in the events table |
| Session identifier | kasm.kasm.id | The session ID |
| Correlation identifier | TraceId | W3C trace ID. Records written inside an API request carry that request's trace. Worker records carry the job's trace, linked to the triggering request's span. Records forwarded by a legacy component through the API (kasm.log.ingest_path is legacy_bridge) carry no trace ID |
| Container identifier | kasm.agent.container_id | Null until compute exists, and always null for server-backed sessions |
| Region | kasm.zone.name | The deployment zone the session was placed in |
| Elapsed duration | kasm.kasm.transition_ms | Present only on kasm.provision.started, kasm.provision.ready, and kasm.provision.destroy, where it is the milliseconds since the session was requested, and on kasm.session.client_detached, where it is the milliseconds since the client attached |
Records written inside an API request also carry the request context fields kasm.kasm.user_id, kasm.kasm.user_name, client.address, and url.path. No record carries a visited URL, a page title, or input text.
Events
| Event | kasm.metric_name | Emitted from | Change in 1.20.0 |
|---|---|---|---|
| Session requested | kasm.provision.request | API, session request accepted | Existing record, now tagged |
| Provisioning started | kasm.provision.started | Worker, reconcile reserves compute | New |
| Container created | kasm.provision.create | Worker, agent create call | Existing record, new fields |
| Session ready | kasm.provision.ready | Worker, start_date set | New |
| Client attached | kasm.session.client_attached | API, proxy authorization of the session websocket | New |
| Client detached | kasm.session.client_detached | OTel Collector, from the proxy access log | New |
| Session resumed | kasm.session.resumed | API, resume command accepted | Existing record, now tagged |
| Session destroyed | kasm.provision.destroy | Worker, accounting close-out | Existing record, new fields |
| Error | kasm.provision.error | Worker, reconcile cannot converge | New |
Session requested
Written once the request has passed every check and the session row is committed. A refused request writes no lifecycle record. It increments kasm.provision.request.failed with a reason instead.
| Field | Value |
|---|---|
kasm.kasm.id | The new session |
kasm.kasm.image_id | Image launched |
kasm.kasm.image_type | Container, Server, or Server Pool |
kasm.zone.name | Zone the request resolved. Null when placement chooses the zone later |
kasm.kasm.is_cast | Launched through a cast link |
kasm.kasm.is_anonymous | The owner is an anonymous account |
kasm.kasm.keepalive_seconds | Idle lifetime decided at request time |
kasm.cast_link.id, kasm.cast_link.key | Present when launched through a cast link |
Provisioning started
Written by the first reconcile action that reserves compute: a free slot on a host, with kasm.server.id set, or the claim on a standby container. kasm.kasm.transition_ms is the time the request waited for capacity.
Container created
Written by the agent create call for one placement. The record carries the image fields kasm.kasm.image_id, kasm.kasm.image_name, kasm.kasm.image_friendly_name, and kasm.kasm.image_type, plus kasm.zone.name, kasm.server.id, kasm.kasm.is_anonymous, kasm.kasm.keepalive_seconds, the cast link fields, and the egress selection. Server-backed sessions have no create call and skip this event.
Session ready
Written when the placement's connection details are recorded and start_date is set. The record carries kasm.kasm.id, kasm.agent.container_id, kasm.server.id, kasm.zone.name, and kasm.kasm.transition_ms measured from request to ready.
Client attached
The proxy asks the API to authorize every request under /desktop/<session_id>/. Only the request that opens the session websocket counts as a client attaching: websockify for KasmVNC sessions and websocket-tunnel for Connection Proxy (Guac) sessions. Assets and polls are not counted. The record carries kasm.kasm.id, kasm.kasm.user_id, kasm.kasm.user_name, kasm.agent.container_id, kasm.server.id, kasm.kasm.is_owner, and kasm.kasm.is_shared. The paired counter is kasm.session.connect.
Client detached
nginx writes one access log row when a request finishes. For the session websocket, the request finishes when the client disconnects. The row for GET /desktop/<session_id>/vnc/websockify with status 101 is therefore the detach event, and its request_time is how long the client was attached. The OTel Collector tags that row at ingest with the following fields.
| Field | Value |
|---|---|
kasm.metric_name | kasm.session.client_detached |
kasm.kasm.id | From the request path |
kasm.kasm.transition_ms | request_time multiplied by 1000, rounded |
cookie_username | The connected user. A proxy field, not kasm.kasm.user_name |
TraceId | The websocket request's trace from the nginx OTel module |
ServiceName | kasm-proxy |
The proxy also logs its loopback hop to the agent for the same websocket under a different path. That row is not tagged, so each detach is counted once. Query strings are removed from the proxy record at ingest.
SELECT Timestamp,
LogAttributes['kasm.kasm.id'] AS session_id,
LogAttributes['kasm.kasm.transition_ms'] AS attached_ms,
LogAttributes['cookie_username'] AS user
FROM otel_logs
WHERE ServiceName = 'kasm-proxy'
AND LogAttributes['kasm.metric_name'] = 'kasm.session.client_detached'
ORDER BY Timestamp DESC;
Session resumed
Written when a resume command is accepted for a paused or stopped session. The record carries kasm.kasm.id, kasm.agent.container_id, and kasm.session.action set to resume. Pause and stop are not lifecycle events. They are reported through the kasm.session.lifecycle counter and a plain accepted log line.
Session destroyed
Written when the accounting row is closed, immediately before the session row is deleted. The record carries the existing user, image, zone, server, egress, cast, queued, and staged fields, plus kasm.agent.container_id, kasm.kasm.transition_ms measured from request to destroy, kasm.kasm.is_anonymous, kasm.kasm.keepalive_seconds, and kasm.kasm.image_type.
Error
Written when reconcile decides a session cannot converge, and for any unexpected exception in a reconcile pass.
| Field | Value |
|---|---|
kasm.kasm.id | The session |
kasm.agent.container_id, kasm.server.id | Present when compute existed |
kasm.provision.failed.reason | The reconcile reason code, when the error has one |
exception.type | The exception class name |
exception.message | The exception message |
Reading the events back
The following query returns one session's timeline from ClickHouse. Replace <session_id> with the session ID.
SELECT Timestamp, ServiceName,
LogAttributes['kasm.metric_name'] AS event,
LogAttributes['kasm.kasm.transition_ms'] AS transition_ms,
LogAttributes['kasm.zone.name'] AS zone,
LogAttributes['kasm.agent.container_id'] AS container_id,
TraceId
FROM otel_logs
WHERE LogAttributes['kasm.kasm.id'] = '<session_id>'
AND (LogAttributes['kasm.metric_name'] LIKE 'kasm.provision.%'
OR LogAttributes['kasm.metric_name'] LIKE 'kasm.session.%')
ORDER BY Timestamp;
In api.log, the same transitions appear as message lines: Accepted session request, reserved server, kasm (...) is ready, Client attached, Accepted resume, destroyed after, and cannot converge. With the following setting, each line also carries those five attributes as kasm_metric_name, kasm_kasm_id, and so on.
KASM_LOG_FILE_EXTRA_FIELDS=kasm.metric_name,kasm.kasm.id,kasm.zone.name,kasm.agent.container_id,kasm.kasm.transition_ms
The client detached event never appears in api.log, whatever the value of KASM_LOG_FILE_EXTRA_FIELDS. Read it from ClickHouse or a collector forwarder.