Skip to content
scalive
Menu
ConnectingLiveReconnectingOffline
GitHub

Lifecycle observability

Before You Start

Start with an assembled LiveApplication and the LiveSecurity or ZioHttpConfig used to build its routes. Decide which ZIO Metrics connector the application will use before relying on the built-in metrics in production.

Choose The Observation Boundary

Pass one LifecycleObserver while assembling routes to receive structured events from disconnected HTTP rendering and connected WebSocket lifecycles. It reports disconnected render and mount outcomes, joins, connected mounts and turns, handler and subscription failures, queue pressure, and lifecycle termination. Durations are monotonic nanoseconds and stop before physical WebSocket transmission.

Lifecycle observation begins inside Scalive's LiveView operation boundary. It does not report endpoint request duration, an Origin rejection before WebSocket upgrade, a failed upgrade, proxy behavior, or infrastructure health. Instrument those boundaries with ZIO HTTP middleware, application access logs, edge telemetry, and infrastructure metrics.

Use lifecycle hooks when application policy must intercept or alter a particular root or component stage. Use a LifecycleObserver for route-wide operational events that cannot alter the observed lifecycle.

Register A Safe Observer

Create a custom observer for structured logging or tracing and pass it to ZioHttp.routes:

scala
val applicationObserver = LifecycleObserver.fromFunction { event =>
  ZIO.logAnnotate("lifecycle_event", event.name) {
    ZIO.logDebug("Scalive lifecycle event")
  }
}

val liveRoutes = ZioHttp.routes(application, security, applicationObserver)

The example records only the stable event name. Do not log event.toString: failure events may contain an application-thrown cause whose message contains application data. Select and redact any additional log or span fields under the same policy as application logs.

Observers run synchronously and must remain non-blocking. Scalive catches all observer failures so they cannot fail the lifecycle being observed, but a slow observer still delays that lifecycle. If an integration performs network or file I/O, hand the event to an application-owned exporter through a non-suspending bounded offer, such as a dropping queue, rather than performing that I/O or waiting for queue capacity in observe. Define the overflow policy explicitly and record dropped exporter events in a separate low-cardinality metric.

Scalive may add event or classification cases as lifecycle coverage grows. When matching an event enum, include a fallback that safely ignores or generically records unknown cases so a library upgrade does not introduce a runtime MatchError.

Publish The Built-In Metrics

LifecycleMetrics provides an exporter-independent ZIO Metrics adapter with fixed metric names, labels, and histogram boundaries:

scala
val liveRoutes = ZioHttp.routes(
  application,
  security,
  LifecycleMetrics.observer
)

Metric updates write synchronously to ZIO's in-memory metric registry and do no network or file I/O. Install and configure the application's preferred ZIO Metrics connector to export that registry to Prometheus or another supported monitoring backend. Connector dependencies, polling, and endpoint exposure remain application concerns.

After enabling the connector, request one Live route and verify that scalive_disconnected_render_total increments. Connect its WebSocket and verify scalive_join_total and scalive_mount_total. This checks the LiveView observer, registry, connector, and exported endpoint as one path before dashboards or alerts depend on it.

Compose the metrics adapter with application-specific logging or tracing when both are needed. Observers run in composition order, and a failure in one does not prevent the next observer from receiving the event:

scala
val observer = LifecycleMetrics.observer.andThen(applicationObserver)
val liveRoutes = ZioHttp.routes(application, security, observer)

The built-in metric contract is intentionally fixed. Implement a separate LifecycleObserver when an application requires different names, labels, or histogram boundaries.

Use The Metric Families

The adapter publishes these metric families:

MetricLabelsMeaning
scalive_disconnected_render_totaloutcome, failure, stageCompleted disconnected renders.
scalive_disconnected_render_duration_secondsoutcome, failure, stageDisconnected render duration histogram.
scalive_join_totaloutcome, target, reconnect, retry, failure, stageCompleted root and nested join attempts.
scalive_join_duration_secondsoutcome, target, reconnect, retry, failure, stageJoin duration histogram.
scalive_mount_totaloutcome, phase, failure, stageCompleted disconnected and connected mounts.
scalive_mount_duration_secondsoutcome, phase, failure, stageMount duration histogram.
scalive_turn_totaloutcome, kind, failure, stageCompleted connected turns.
scalive_turn_duration_secondsoutcome, kind, failure, stageConnected turn duration histogram.
scalive_handler_failures_totalkind, failure, stageHandler failures, also represented by failed turns.
scalive_handler_failure_duration_secondskind, failure, stageFailed handler duration histogram.
scalive_subscription_failures_totaldelivery, failure, stageManaged subscription defects.
scalive_queue_depthqueue, statusObserved bounded queue depth histogram.
scalive_queue_saturation_totalqueueOffers rejected at a bounded queue or pending-command limit.
scalive_lifecycle_terminations_totalreason, failure, stageCompleted connected lifecycle terminations.

Metric families describe nested operation boundaries, not mutually exclusive incident classes. For example, a handler failure is represented by both its handler-failure metric and the enclosing failed turn. A mount failure may also fail an enclosing render, bootstrap turn, or join. Query the boundary relevant to an alert; do not sum counters across families as an incident total.

Read Labels Correctly

Every series in one metric family has the same label keys. Enum values use lower snake case.

LabelValues and interpretation
outcomesucceeded or failed for render, mount, and turn metrics; succeeded or rejected for join metrics.
targetroot or nested.
reconnecttrue only when the untrusted _mounts join counter is a positive integer; otherwise false.
retrytrue only when the untrusted _mount_attempts join counter is a positive integer; otherwise false.
phasedisconnected or connected.
kindbootstrap, browser_event, component_browser_event, message, async_completion, subscription, component_message, component_update, upload, params_patch, or internal.
deliverylossless or latest.
queueconnection_ingress, connection_pending_commands, kernel_mailbox, or writer.
statussampled for an ordinary depth observation or saturated for a rejected offer.
reasonclosed, redirected, disconnected, or failed. Disconnect strings are deliberately collapsed to disconnected.
failurenone, stage, join_rejected, mailbox_saturated, ingress_saturated, subscription_defect, or interrupted.
stagenone, mount, connected_turn_guard, handler, resource_preparation, topology_preparation, render, output_reservation, after_render, validation, identity, retirement, component_mount, component_update, component_message, component_async, component_after_render, commit, navigation, writer, upload, cleanup, or runtime.

Successful observations use failure="none" and stage="none". A stage failure uses failure="stage" and its specific stage; every other failure uses its specific failure and stage="none".

Phoenix's standard client reports _mounts and _mount_attempts with each join. LifecycleJoinAttempt exposes the parsed counters and derives isReconnect and isRetry. Missing, malformed, zero, and negative values all produce false; that value means no positive client-reported counter was observed, not authoritative proof of a first join. The counters are untrusted diagnostics and never authorization inputs.

Keep Series Bounded

Durations are converted from monotonic nanoseconds to seconds. Duration histograms use exponentially doubling finite buckets from 0.0005 to 65.536 seconds. Queue-depth histograms use exponentially doubling finite buckets from 1 to 32768. Both include a terminal overflow bucket.

The adapter clears ambient ZIO metric tags while updating its metrics. This prevents request IDs and colliding tags from changing the fixed label contract. Attach deployment-wide labels such as service, environment, or region in the metrics exporter instead.

Runtime IDs, exception messages, reconnect counters, and disconnect strings are excluded from built-in labels. Structured event fields never contain models, messages, browser payloads, credentials, or session claims. Application-thrown causes may contain application data and require the same redaction policy as application logs.