> Markdown version of [Reference](https://vaadin.com/docs/next/tools/observability/reference). Section index: [llms.txt](https://vaadin.com/docs/next/tools/llms.txt)

# Observability Kit Reference

Observability Kit instruments the Vaadin runtime and records everything into your application’s Micrometer `MeterRegistry`. Metrics are plain Micrometer meters; tracing spans are emitted through the Micrometer Observation API. Both flow to whatever backend you’ve configured — see the [Integrations](https://vaadin.com/docs/next/tools/observability/integrations.md) page.

Each group of meters and spans is controlled by a feature toggle (for example `vaadin.observability.sessions`). See the [Configuration](https://vaadin.com/docs/next/tools/observability/configuration.md) page for how to turn features on or off.

> **Note: Naming Conventions**
>
> Meter names follow Micrometer’s dotted, lowercase convention (for example `vaadin.request.duration`). How a name appears in your backend depends on the conventions of that backend — Prometheus, for instance, renders `vaadin.request.duration` as `vaadin_request_duration_seconds` with `_count`, `_sum`, and `_max` suffixes. Two renames are easy to trip over: `vaadin.sessions.created` appears in Prometheus as `vaadin_sessions_total`, and `vaadin.ui.created` as `vaadin_ui_total`, because `_created` is a reserved suffix in the OpenMetrics format and is stripped from the name.
>
> Timers export count, sum, and max only by default. The histogram buckets that percentile queries need are opt-in — see [Percentiles and Histogram Buckets](https://vaadin.com/docs/next/tools/observability/integrations.md#percentiles). With tracing on, each observed timer also publishes a parallel long-task timer — an `_active_seconds` family in Prometheus — that tracks operations still in flight.

## <a id="metrics"></a>Metrics

A meter is a measurement recorded at runtime. Observability Kit records the meter types Micrometer provides:

- Counter

  A value that only increases, such as the number of sessions created.

- Gauge

  A value sampled at a point in time, such as the number of active sessions.

- Timer

  Records both a count of events and the distribution of their durations.

- Distribution summary

  Records both a count of events and the distribution of a non-time measurement, such as the number of rows read from a query.

### <a id="session-metrics"></a>Session Metrics

Controlled by `vaadin.observability.sessions`.

| Meter                      | Type    | Description                                                          |
| -------------------------- | ------- | -------------------------------------------------------------------- |
| `vaadin.sessions.active`   | Gauge   | Currently active sessions.                                           |
| `vaadin.sessions.created`  | Counter | Sessions created since startup.                                      |
| `vaadin.sessions.duration` | Timer   | Session lifetime, recorded when a session ends.                      |
| `vaadin.session.lock.wait` | Timer   | Time spent waiting to acquire the session lock. Tagged by `context`. |
| `vaadin.session.lock.hold` | Timer   | Time the session lock is held. Tagged by `context`.                  |

The `context` tag is `request` when the lock is taken during request handling, or `access` when it’s taken through `UI.access()`.

### <a id="ui-metrics"></a>UI Metrics

Controlled by `vaadin.observability.uis`.

| Meter               | Type    | Description                |
| ------------------- | ------- | -------------------------- |
| `vaadin.ui.active`  | Gauge   | Currently active UIs.      |
| `vaadin.ui.created` | Counter | UIs created since startup. |

### <a id="ui-state-metrics"></a>UI State Metrics

Controlled by `vaadin.observability.ui-state`, which is off by default. Session and UI counts tell you how many users are connected; these gauges tell you what each of them costs. See [UI State Size](#ui-state-size) for how the measurement is scheduled and how to read it.

| Meter                                   | Type  | Description                                                                                                                                                                                       |
| --------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vaadin.ui.state.nodes`                 | Gauge | State-tree nodes retained across all tracked UIs — how much UI state the server currently holds for live users.                                                                                   |
| `vaadin.ui.state.nodes.max`             | Gauge | State-tree nodes held by the largest single UI.                                                                                                                                                   |
| `vaadin.ui.state.components`            | Gauge | Server-side component instances retained across all UIs.                                                                                                                                          |
| `vaadin.ui.state.views`                 | Gauge | Route-target and router-layout instances retained across all UIs. One navigation into a nested layout legitimately retains one per level, so this is a capacity figure rather than a leak signal. |
| `vaadin.ui.state.views.stale`           | Gauge | Retained views that are no longer part of their UI’s active navigation — views that outlived it. Normally zero.                                                                                   |
| `vaadin.ui.state.size`                  | Gauge | Retained UI state in bytes, projected from the node count. Registered only when `vaadin.observability.ui-state-bytes-per-node` is set.                                                            |
| `vaadin.ui.state.sample.age.max`        | Gauge | Age in seconds of the stalest per-UI measurement in the aggregate.                                                                                                                                |
| `vaadin.session.state.nodes.max`        | Gauge | State-tree nodes held by the largest single session.                                                                                                                                              |
| `vaadin.session.uis.max`                | Gauge | Most UIs (browser tabs) held open by one session.                                                                                                                                                 |
| `vaadin.ui.state.retained.elements`     | Gauge | Elements held in the collection fields of views across all UIs — state the state tree doesn’t contain. See [State Outside the Tree](#state-outside-the-tree).                                     |
| `vaadin.ui.state.retained.elements.max` | Gauge | Elements held by the largest single view field.                                                                                                                                                   |
| `vaadin.ui.state.retained.growing`      | Gauge | View fields whose collection keeps growing across measurements without shrinking. Normally zero.                                                                                                  |

The three `retained` gauges are registered only when `vaadin.observability.ui-state-growth-samples` is above zero, which it is by default.

These gauges are aggregates only — totals and maxima, never one series per session or per UI, which would grow unbounded with traffic. They carry no tags.

### <a id="navigation-metrics"></a>Navigation Metrics

Controlled by `vaadin.observability.navigation`.

| Meter               | Type  | Description                                                                                              |
| ------------------- | ----- | -------------------------------------------------------------------------------------------------------- |
| `vaadin.navigation` | Timer | Navigation duration, from `beforeEnter` to `afterNavigation`. Tagged by `route`, `outcome`, and `error`. |

Every navigation that **starts** is recorded, including the ones that never complete — which would otherwise leave a span dangling. The `outcome` tag says how it ended:

| `outcome`   | Recorded for                                                                                                                                                                                   |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success`   | The navigation reached `afterNavigation`.                                                                                                                                                      |
| `rerouted`  | A listener called `rerouteTo()`, so this navigation was replaced by another. This is a routing decision — an access guard sending the user elsewhere — not a failure.                          |
| `forwarded` | A listener called `forwardTo()` or `forwardToUrl()`, or handed off to a client-side route.                                                                                                     |
| `error`     | The navigation failed: `rerouteToError()`, or an exception while the view was being built.                                                                                                     |
| `unknown`   | The navigation was neither completed nor redirected. A re-entrant `UI.navigate()` from a view’s `beforeEnter()` or `onAttach()` superseded it, or its UI was detached while it was still open. |

> **Important: Building an Error Rate on This Timer**
>
> Two consequences follow from timing the router’s own chain — an error view is a navigation in its own right, and it’s one that succeeds:
>
> - An unknown URL never reaches a `beforeEnter()` that could fail, so it’s recorded as the error view rendering successfully: `route=RouteNotFoundError, outcome=success`. Alert on that route rather than on `outcome`.
>
> - A view that throws while being built produces **two** samples: the failed navigation to the view (`outcome=error`), and the navigation to the error view that replaces it (`route=InternalServerError, outcome=success`).

### <a id="request-metrics"></a>Request Metrics

Controlled by `vaadin.observability.requests`.

| Meter                     | Type  | Description                                                                                                                                                                                            |
| ------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `vaadin.request.duration` | Timer | Server-side request handling time. Tagged by `vaadin.request.type`, `vaadin.interaction`, `http.method`, `outcome`, and `error`. See [Request Types](#request-types) for what the type tag classifies. |
| `vaadin.rpc.duration`     | Timer | Server-side RPC invocation time. Tagged by `type`, `outcome`, and `error`.                                                                                                                             |

> **Note: The Tags Are the Same With Tracing On or Off**
>
> With tracing on — the default — `vaadin.request.duration` is produced from the request observation, so it carries that span’s low-cardinality attributes as tags, plus the `error` tag Micrometer’s `DefaultMeterObservationHandler` adds. With tracing off, the binder records the timer directly and tags it with exactly the same keys. This is deliberate: Prometheus rejects same-named meters whose tag-key sets differ, so the two recording paths must never publish `vaadin.request.duration` under differing keys. The same holds for `vaadin.rpc.duration` and `vaadin.navigation`.
>
> The span’s `ui.id`, `vaadin.client.location`, and `vaadin.session.id` attributes are unbounded and are deliberately kept off the timer on both paths.

The `http.method` tag accepts only the standard HTTP methods; any other method a client sends is recorded as `_other`, so a request can’t create a new series by inventing one.

### <a id="request-types"></a>Request Types

Every request Vaadin handles is classified before it’s timed, and the class becomes the `vaadin.request.type` tag on `vaadin.request.duration` — and, with tracing on, the `vaadin.request.<type>` span name. The types differ so much in what they do that a single average across all of them means nothing.

| `vaadin.request.type` | What It Covers                                                                                                                                                                                                                                 |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uidl`                | A UI interaction: the request the client sends for a click, a poll, or a navigation. The one type broken down further, by the `vaadin.interaction` tag.                                                                                        |
| `bootstrap`           | A page load: the HTML document request, and the `init` request the client engine follows it with to have the UI created. The server’s side of `vaadin.client.bootstrap.duration`.                                                              |
| `stream`              | A download or an upload, served by Flow’s stream request handler. A transfer is expected to be long-running, which is why it’s kept out of the other buckets: averaged in with page loads, it both hides its own outliers and inflates theirs. |
| `push`                | A push channel request, on any transport.                                                                                                                                                                                                      |
| `heartbeat`           | The keep-alive the browser sends for an open UI.                                                                                                                                                                                               |
| `static`              | A static resource: `/VAADIN/`, `/static/`, `/themes/`, or `/sw.js`.                                                                                                                                                                            |
| `other`               | Everything left, an application’s own endpoints under the Vaadin servlet among them.                                                                                                                                                           |

A page load is recognized from the browser’s `Sec-Fetch-Dest` header, falling back to an `Accept` header that asks for `text/html` first for browsers old enough not to send it. A `fetch()` to an application endpoint that happens to sit under the Vaadin servlet is therefore not counted as one. The classification is deliberately conservative in that direction: a page load that can’t be told apart from application traffic stays `other` rather than diluting `bootstrap`.

An embedded route counts as a page load too. A request whose destination is an `iframe`, `frame`, `embed`, or `object` is served the same `index.html` and builds a UI of its own, and `vaadin.client.bootstrap.duration` records it from the browser’s end as well. A view that embeds another of its own routes therefore reports a second `bootstrap`, which is the second UI it really does build.

### <a id="resync-metrics"></a>Resync Metrics

Controlled by `vaadin.observability.resync`. These track client message-recovery events, over both HTTP and push.

| Meter           | Type    | Description                                              |
| --------------- | ------- | -------------------------------------------------------- |
| `vaadin.resync` | Counter | Client-server message-recovery events. Tagged by `type`. |

The `type` tag says what happened:

| `type`        | Recorded when                                                                                                    |
| ------------- | ---------------------------------------------------------------------------------------------------------------- |
| `resend`      | The client re-sent a message it never got a response for, and the server replayed its cached response.           |
| `resync`      | The client gave up on a missing server message and asked for a full UI-state rebuild.                            |
| `out_of_sync` | A message arrived with an ID the server didn’t expect, and the user was shown the session synchronization error. |

`resend` and `resync` mean the client lost a server message, which points at a flaky connection. `out_of_sync` usually means the UI continued on a server that held an older state of it. All three types are registered at startup, so each reports zero before its first event.

The counter is driven by events Flow fires, so it needs no servlet filter and works the same in every setup.

### <a id="error-metrics"></a>Error Metrics

Controlled by `vaadin.observability.errors`.

| Meter           | Type    | Description                                                                                  |
| --------------- | ------- | -------------------------------------------------------------------------------------------- |
| `vaadin.errors` | Counter | Every server-side failure the kit observes. Tagged by `exception`, `route`, and `component`. |

The counter covers both kinds of server-side failure:

- Exceptions that escape request handling

  For example one thrown by a custom `RequestHandler`. These reach a `VaadinRequestInterceptor`.

- Failures Flow routes to the session’s error handler

  For example a click or value-change listener that throws, a `UI.access()` body, or a detach listener. Flow catches these and hands them to `VaadinSession.getErrorHandler()` rather than letting them escape, so they never reach a request interceptor. The kit therefore decorates that handler, which is also what lets it attribute a failure to a component.

A failure during navigation — a `beforeEnter()` callback or a view constructor that throws — is neither kind. The router catches it and renders an error view in its place, so it never reaches the error handler and isn’t counted here. It shows up on the navigation timer instead, as described in [Navigation Metrics](#navigation-metrics).

All three tags derive from application classes and multiply with each other, so all three are capped at `vaadin.observability.route-cardinality-limit`. Values beyond the limit collapse to `_other`, and a `route` or `component` that can’t be resolved is `_unknown`.

> **Important: How the Error Handler Is Decorated**
>
> The decoration always delegates, so an application’s own error handler keeps receiving every error it received before. It’s applied at session init and re-applied at UI init and at the start of every RPC invocation, so installing your own handler after session init doesn’t switch error metrics off.
>
> One consequence: a handler read back from `VaadinSession.getErrorHandler()` is the kit’s wrapper rather than the instance you set. Delegating to it works as expected, and the failure is still counted exactly once. An `instanceof` check or a cast to your own type does not. Set `vaadin.observability.errors=false` to opt out of the decoration entirely.

### <a id="client-metrics"></a>Client Metrics

Controlled by `vaadin.observability.client`. These are observed in the browser and reported back to the server, subject to a rate limit of `vaadin.observability.client-rate-per-session` samples per session in each ten-second window. All tabs of a session share that budget.

| Meter                               | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vaadin.client.bootstrap.duration`  | Timer   | Browser application bootstrap time.                                                                                                                                                                                                                                                                                                                                                                                                           |
| `vaadin.client.navigation.duration` | Timer   | Browser-observed navigation time.                                                                                                                                                                                                                                                                                                                                                                                                             |
| `vaadin.client.request.duration`    | Timer   | One UIDL request as the browser saw it, from the moment the request was queued to the last byte of the response. The browser’s side of `vaadin.request.duration`; see [Interaction Timing From the Browser](#client-interaction-timing).                                                                                                                                                                                                      |
| `vaadin.client.render.duration`     | Timer   | How long Flow’s client spent applying one UIDL response to the page. Recorded only when Flow’s `requestTiming` deployment setting is on, which is the default outside production mode.                                                                                                                                                                                                                                                        |
| `vaadin.client.web_vitals.lcp`      | Timer   | Largest Contentful Paint. Reported once per page load: the candidate that’s current when the user first presses a key or a pointer, or when the page is hidden. A larger paint after that is the page responding rather than loading, and the earlier, smaller candidates would drag the mean down.                                                                                                                                           |
| `vaadin.client.web_vitals.fcp`      | Timer   | First Contentful Paint.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `vaadin.client.errors`              | Counter | Errors reported by the browser. Tagged by `kind`. What identifies one — the message, the script it came from, and the first stack frame — is kept as an insight rather than as tags; see [Interaction Insights](https://vaadin.com/docs/next/tools/observability/insights.md).                                                                                                                                                                |
| `vaadin.client.connection`          | Counter | Transitions of the browser’s connection state, tagged by `state` with the state entered. The `loading` state Flow toggles around every request isn’t reported, so this counts real connection events rather than one per interaction.                                                                                                                                                                                                         |
| `vaadin.client.connection.downtime` | Timer   | How long the browser stayed unable to reach the server, recorded once per unreachable state it passed through and tagged by `state`. Time under `reconnecting` is a connection that hiccuped; time under `connection-lost` is a server the browser had given up on. The report can only be sent once the connection is back, so a browser that never reconnects contributes nothing — the timer under-reports total downtime by construction. |
| `vaadin.client.throttled`           | Counter | Client samples rejected by the per-session rate limit. Untagged.                                                                                                                                                                                                                                                                                                                                                                              |
| `vaadin.client.dropped`             | Counter | Client samples that reached ingest but never made it into a meter: one the registry refused, and one whose reported duration isn’t a measurement at all — negative, not finite, or over an hour. A timer’s sum only ever grows, so one saturating value would skew its average for the life of the process. Untagged.                                                                                                                         |

The client timers — except `vaadin.client.connection.downtime`, which carries `state` instead — are tagged by `route`, resolved from the browser location to a route template on the server and capped by the same cardinality limit as the server-side meters. `vaadin.client.navigation.duration` additionally carries `trigger`.

The browser buffers samples and flushes them every five seconds, and when the page is hidden. Only the meters in the table above are accepted; a sample under any other name is discarded at ingest without being recorded or counted, which caps the cardinality a buggy or malicious client can create.

### <a id="client-interaction-timing"></a>Interaction Timing From the Browser

When a user says a click took a second and `vaadin.request.duration` says the server took forty milliseconds, the other nine hundred and sixty are somewhere the server can’t see: on the wire, in the browser’s request queue, or in the browser applying the response. Two of the client meters make that remainder readable, with no configuration beyond `vaadin.observability.client`.

`vaadin.client.request.duration` is the same round trip as `vaadin.request.duration`, measured at the browser’s end. It’s read off the Resource Timing entry every UIDL `POST` leaves behind, so it needs nothing from Flow and works in production. Subtract the server’s figure for the same route, and what’s left is the network:

```text
  rate(vaadin_client_request_duration_seconds_sum[5m])
/ rate(vaadin_client_request_duration_seconds_count[5m])
-
  rate(vaadin_request_duration_seconds_sum{vaadin_request_type="uidl"}[5m])
/ rate(vaadin_request_duration_seconds_count{vaadin_request_type="uidl"}[5m])
```

`vaadin.client.render.duration` is the third segment, after the network and the server: how long Flow’s client spent applying the response to the page. A response that arrives in fifty milliseconds and takes four hundred to render is a browser problem — typically a heavy component tree or an expensive renderer — and neither of the other two timers can show it. The figure is Flow’s own, published through `getProfilingData()` on each client only when Flow’s `requestTiming` deployment setting is on. That’s the default outside production mode; in production, set `vaadin.requestTiming=true` to record this meter. Without it the meter is absent rather than zero.

Four things are worth knowing about both:

- Only UIDL requests are timed

  Heartbeats, push, and static resources aren’t interactions and are left out. The collector’s own request that carries the samples to the server is left out too, so the kit doesn’t report itself.

- The request meter needs the default transport

  It’s read from Resource Timing, which sees HTTP requests. With `@Push(transport = WEBSOCKET)` the UIDL rides the websocket and leaves no entry, so only the render meter is recorded.

- Bootstrap isn’t an interaction

  The first UIDL response, which builds the page, is covered by `vaadin.client.bootstrap.duration` and excluded here.

- The route is the browser’s

  Both meters carry the route the browser was on when the response arrived, which for a navigation request is the view navigated to, as on the server’s request span.

### <a id="data-metrics"></a>Data Provider Metrics

Controlled by `vaadin.observability.data`. These measure the queries that lazy-loading components — `Grid`, `ComboBox`, `VirtualList`, and others — issue to their data providers. Where the database metrics below measure the persistence layer, these measure what the component asked for, so they apply whatever the data provider is backed by.

| Meter                         | Type                 | Description                                                                                                                                                                                                                                 |
| ----------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vaadin.data.count.duration`  | Timer                | Duration of a count query — how many items a level holds. Tagged by `outcome` and `filtered`. A hierarchical component issues one count per expanded parent, so many counts within few requests is the signature of an expensive hierarchy. |
| `vaadin.data.fetch.duration`  | Timer                | Duration of a fetch query — loading one page of items. Tagged by `outcome` and `filtered`. Measured around consumption of the items, so it covers the backend round-trip of a lazily evaluated stream.                                      |
| `vaadin.data.fetch.requested` | Distribution summary | Items a fetch query asked for. Tagged by `route`.                                                                                                                                                                                           |
| `vaadin.data.fetch.rows`      | Distribution summary | Items a fetch query actually returned. Tagged by `route`.                                                                                                                                                                                   |

Compare `vaadin.data.fetch.rows` against `vaadin.data.fetch.requested` to spot a component asking for far more than it renders, or a data provider returning short pages. The two duration timers carry no `route` tag; use the `vaadin.data.component` span attribute or the [interaction insights](https://vaadin.com/docs/next/tools/observability/insights.md) to attribute a slow query to a view.

When tracing is enabled, each query also opens a span — see [Data Provider Spans](#data-spans).

### <a id="database-metrics"></a>Database Metrics

Controlled by `vaadin.observability.database` (off by default, Spring Boot starter only). When enabled, every `DataSource` bean is wrapped so that JDBC access — Spring Data, `JdbcTemplate`, or raw JDBC — is measured, attributed to the Vaadin `route` that triggered it. See [Database Monitoring](#database-monitoring) for how this works and when to use it.

| Meter                  | Type                 | Description                                                                                                                                                                                                   |
| ---------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vaadin.db.fetch.rows` | Distribution summary | Rows read from a JDBC result set. Tagged by `route`. Publishes p95 and p99 percentiles out of the box, so the alerting described in [Database Monitoring](#database-monitoring) needs no extra configuration. |
| `vaadin.db.query`      | Timer                | JDBC query duration. Tagged by `route`. Produced alongside the `vaadin.db.query` span when both database monitoring and tracing are enabled.                                                                  |

### <a id="common-tags"></a>Common Tag Values

| Tag         | Values                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `outcome`   | `success` or `error`. On `vaadin.navigation`, also `rerouted`, `forwarded`, and `unknown` — see [Navigation Metrics](#navigation-metrics).                                                                                                                                                                                                                                                                                                |
| `error`     | The simple class name of the exception that ended the operation, or `none` when it raised none. Distinct from `exception`, which tags the `vaadin.errors` counter. With tracing off, the kit records the request and RPC timers itself and caps their `error` values with the same exception-type budget as `exception`, collapsing types beyond it to `_other`. With tracing on, the tag is written by Micrometer’s observation handler. |
| `route`     | The target route template. Distinct values are capped by `vaadin.observability.route-cardinality-limit`; beyond the limit they collapse to `_other`, and an unresolvable route is `_unknown`.                                                                                                                                                                                                                                             |
| `component` | On `vaadin.errors`, the simple class name of the component the failure was thrown for. Capped by the same cardinality limit as `route`: `_unknown` when it can’t be resolved, `_other` beyond the limit.                                                                                                                                                                                                                                  |
| `context`   | `request` or `access`.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `type`      | On RPC meters and spans, the RPC invocation type as reported by Flow — `event` for a DOM event, `mSync` for a property sync, `publishedEventHandler` for an `@ClientCallable` method, `channel` for a return channel, and `navigation`. On `vaadin.resync`, the recovery kind: `resend`, `resync`, or `out_of_sync`.                                                                                                                      |
| `exception` | The simple class name of the counted exception, capped by the same cardinality limit as `route`. Types beyond the limit are bucketed as `_other`.                                                                                                                                                                                                                                                                                         |
| `filtered`  | On the data provider meters, whether the query carried a filter: `true` or `false`. This separates a combo box loading matches for typed text from one loading the whole data set.                                                                                                                                                                                                                                                        |
| `trigger`   | On `vaadin.client.navigation.duration`, what moved the browser: `back` for history navigation, or `programmatic` for a `pushState`/`replaceState` call, with `_unknown` for a report that is neither.                                                                                                                                                                                                                                     |
| `kind`      | On `vaadin.client.errors`, the source of the browser error: `uncaught` or `promise`, with `_unknown` for a report that is neither.                                                                                                                                                                                                                                                                                                        |
| `state`     | On `vaadin.client.connection`, the state entered: `connected`, `reconnecting` (the first request failed, the client is retrying), or `connection-lost` (the client exhausted its retries). On `vaadin.client.connection.downtime`, only the two unreachable states appear — a browser only spends downtime being unreachable, so `connected` would be a contradiction there. Either meter buckets a value outside its set as `_unknown`.  |

> **Note: JVM, Process, and Connection-Pool Metrics**
>
> Observability Kit doesn’t record JVM, process, or database connection-pool metrics itself. Those come from Micrometer’s standard binders — Spring Boot Actuator registers them out of the box, and you can add others as needed. The kit’s own database metrics above measure query behavior per route, not the connection pool.

## <a id="tracing"></a>Tracing

When tracing is enabled (`vaadin.observability.traces`, the default) and an `ObservationRegistry` is available, the kit drives the core request lifecycle through the Observation API. Each observation produces a tracing span and, through Micrometer’s `DefaultMeterObservationHandler`, the matching timer above — one measurement, recorded two ways.

To export spans, add a Micrometer tracing bridge (for example OpenTelemetry or Zipkin); see the [Integrations](https://vaadin.com/docs/next/tools/observability/integrations.md) page.

The kit produces the following spans:

| Span                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `vaadin.request.<type>`     | The root span for each Vaadin request. A UIDL request is named by its interaction — `vaadin.request.rpc`, `vaadin.request.poll`, or `vaadin.request.navigation` — and other requests by their type: `vaadin.request.bootstrap`, `vaadin.request.stream`, `vaadin.request.heartbeat`, `vaadin.request.push`, `vaadin.request.static`, or `vaadin.request.other`. See [Request Types](#request-types). Carries the request-level attributes below. |
| `vaadin.navigation <route>` | A navigation, nested under the request that triggered it.                                                                                                                                                                                                                                                                                                                                                                                        |
| `vaadin.rpc.<type>`         | A server-side RPC invocation (DOM event, `@ClientCallable`, property sync, or return channel), nested under the request.                                                                                                                                                                                                                                                                                                                         |
| `vaadin.executor.task`      | One task run on the Vaadin service executor, nested under whatever trace was active when the task was submitted. See [Background Work](#background-work).                                                                                                                                                                                                                                                                                        |
| `vaadin.data.count`         | A data provider count query, nested under the request or RPC span that triggered the load. Controlled by `vaadin.observability.data`.                                                                                                                                                                                                                                                                                                            |
| `vaadin.data.fetch`         | A data provider fetch query, nested under the request or RPC span that triggered the load. Controlled by `vaadin.observability.data`.                                                                                                                                                                                                                                                                                                            |
| `vaadin.db.query`           | A single JDBC query, nested under the request or RPC span that ran it. Emitted only when database monitoring is enabled (see [Database Monitoring](#database-monitoring)).                                                                                                                                                                                                                                                                       |

With both data provider and database monitoring on, a slow interaction opens up in full: the `vaadin.rpc.<type>` span that the user triggered, the `vaadin.data.fetch` span for the page the component asked for, and the individual `vaadin.db.query` spans that fetch ran.

### <a id="http-observation"></a>Spring HTTP Server Observation

In Spring deployments the kit also enriches Spring’s own HTTP server observation. Each Vaadin request gets its type lifted into the HTTP span, and a UIDL request additionally gets the active view’s route template set as the path pattern. The `uri` tag on `http.server.requests` and the HTTP span name then read `/orders/:id`, instead of bucketing all UI traffic into a single `/vaadin/uidl` entry, so per-view HTTP latency stays answerable from the standard Spring metrics. A UIDL request whose view’s route template can’t be resolved stays under `/vaadin/uidl`; the kit never falls back to the view’s class name or to the literal browser path there.

Every other [request type](#request-types) keeps the type itself as its path pattern — `/vaadin/bootstrap`, `/vaadin/stream`, and so on. This also keeps the per-transfer URLs of downloads and uploads, each carrying a UI ID and a one-time security key, out of the `uri` tag.

The templates pass the same `vaadin.observability.route-cardinality-limit` cap as the kit’s own `route` tags, and at most 50 distinct view templates ever reach the `uri` tag, regardless of configuration. The rest collapse into `/_other`. The fixed budget is half of the default of Spring Boot’s `management.metrics.web.server.max-uri-tags` (100). Once **that** cap is crossed, Spring Boot denies new `http.server.requests` series outright, first come first served across every endpoint of the application, so the kit stays well clear of it. Raise `max-uri-tags` when Actuator endpoints and REST controllers need more room, not to get more view templates through.

The enrichment is independent of the `traces` setting: it also applies when only metrics are collected.

### <a id="background-work"></a>Background Work

With tracing enabled, the kit wraps the Vaadin service `Executor` so that the trace context active when a task is **submitted** is restored when the task **runs**. A background task started from a request thread therefore stays in the same trace across the thread hop, under its own `vaadin.executor.task` span, instead of appearing as an unrelated root span.

This covers the tasks submitted to that executor: the signal effects Vaadin re-evaluates on it, the signal result notifications it dispatches through it, and the background work applications are expected to hand to it — typically a task that ends by pushing its result through `UI.access()`. It isn’t every `UI.access()` call: a plain call from a background thread queues a command that whichever thread unlocks the session drains, so it never passes through the executor and gets no span of its own. A notification task is itself a `UI.access()` call, so its span always covers the dispatch, but covers the notification body only when the session lock happens to be free. Otherwise the task returns as soon as the command is enqueued, and the body runs later on the unlocking thread, outside the span.

Work you hand to an executor of your own isn’t wrapped. To keep such work in the trace, submit it through the Vaadin service executor, or propagate the context yourself with Micrometer’s `ContextSnapshot`.

### <a id="span-attributes"></a>Span Attributes

The root `vaadin.request` span carries these attributes:

| Attribute                | Description                                                                                                                                                                                                              |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `vaadin.request.type`    | The request type: `uidl`, `bootstrap`, `stream`, `push`, `heartbeat`, `static`, or `other`. See [Request Types](#request-types).                                                                                         |
| `vaadin.interaction`     | What the request actually did: `poll`, `navigation`, or `rpc`, and `none` for requests where no interaction applies, such as heartbeats and static resources.                                                            |
| `http.method`            | The HTTP method of the request.                                                                                                                                                                                          |
| `outcome`                | `success` or `error`.                                                                                                                                                                                                    |
| `ui.id`                  | The ID of the UI associated with the request, or `_unknown`. Span-only, since UI IDs are unbounded.                                                                                                                      |
| `vaadin.client.location` | The browser location the request was sent from, or `_unknown`. Span-only: it’s the literal path, not a route template. For templated, cardinality-capped view attribution, use the `route` tag on the navigation meters. |
| `vaadin.session.id`      | The HTTP session ID. Present only when `vaadin.observability.traces-session-id` is enabled, which it isn’t by default. Span-only, since session IDs are unbounded.                                                       |

The nested spans carry the tags of their corresponding meters: `vaadin.navigation <route>` carries `route` and `outcome`; `vaadin.rpc.<type>` carries `type`.

The RPC span additionally carries two span-only, high-cardinality attributes when they can be resolved: `vaadin.rpc.event` (the invocation name, such as a DOM event name, invoked method name, or navigation location) and `vaadin.rpc.component` (the class name of the targeted `Component`). These are attached to the span only, never as timer tags, because of their cardinality. Together they let you trace a failure back to the interaction that caused it — which component, and which event.

Flow doesn’t report an invocation name for property syncs, which is how a field’s value change arrives at the server. Those spans carry `type` and `vaadin.rpc.component`, but no `vaadin.rpc.event`.

The `vaadin.db.query` span carries `route`, a `db.rows` attribute with the number of rows read, and — when `vaadin.observability.database-statement` is enabled — the parameterized SQL as `db.statement`. A request, RPC, or data fetch span under which more queries ran than `vaadin.observability.database-span-limit` allows carries `vaadin.db.queries.unspanned`, the number of queries that got no span of their own; see [N+1 Loads and the Span Limit](#span-limit).

The `vaadin.executor.task` span carries no attributes of its own. Its value is structural: it shows where a submitted task ran, under the trace it was submitted from.

### <a id="data-spans"></a>Data Provider Spans

The `vaadin.data.count` and `vaadin.data.fetch` spans carry `filtered` and `outcome` as low-cardinality attributes, alongside these span-only ones:

| Attribute               | Description                                                                                                                                                                                         |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vaadin.data.component` | The class name of the component whose data is being loaded. Span-only, because of its cardinality — this is what attributes a slow query to a view, since the duration timers carry no `route` tag. |
| `vaadin.data.offset`    | Index of the first item a fetch query asked for. Fetch spans only.                                                                                                                                  |
| `vaadin.data.limit`     | Number of items a fetch query asked for. Fetch spans only.                                                                                                                                          |
| `vaadin.data.rows`      | Number of items a fetch query returned. Fetch spans only, and only when the query succeeded.                                                                                                        |

### <a id="errors"></a>Errors

When a request or a nested operation fails, its observation is marked as error, so the span records the exception and the `outcome` tag becomes `error`.

For an exception thrown inside a component listener, this happens on the `vaadin.rpc.<type>` span — the one that also carries the component and event. The enclosing `vaadin.request` span is marked `outcome=error` as well: Flow hands such an exception to the session’s `ErrorHandler` rather than letting it escape request handling, and the kit relays that back to the request observation, so the request doesn’t claim success for an interaction that failed. On Spring, the error is also propagated to the enclosing Spring HTTP observation, so the surrounding HTTP server span reports it too. The failure also increments the `vaadin.errors` counter; see [Error Metrics](#error-metrics).

The same failure is also retained as an interaction insight, which reports it without a tracing backend and points at the application stack frame behind it. See the [Interaction Insights](https://vaadin.com/docs/next/tools/observability/insights.md) page.

## <a id="ui-state-size"></a>UI State Size

Session and UI counts tell you how many users are connected; they say nothing about what each of them costs. Because Flow keeps every open tab’s component tree in server memory, **size** is the signal that predicts when a server-driven application has to scale: a hundred users on a dashboard with three grids cost nothing like a hundred users on a login form.

Turn the measurement on with:

`application.properties`

```properties
vaadin.observability.ui-state=true
```

Each UI then reports its own state-tree size, and the kit publishes the aggregates listed in [UI State Metrics](#ui-state-metrics). Charted next to `vaadin.sessions.active`, they answer a question the counts can’t: state climbing while the session count is flat means capacity is going into what users have open, not into how many of them there are.

Watch the maxima as much as the totals. `vaadin.ui.state.nodes.max` and `vaadin.session.state.nodes.max` describe the worst-case tab and the worst-case user, and it’s the tail that exhausts a heap, not the mean. `vaadin.ui.state.views.stale` is the one leak signal in the set: anything above zero means views are outliving their navigation. A plain view count can’t tell you that, because one navigation into a nested layout legitimately retains a view per level.

### <a id="how-measurement-is-scheduled"></a>How Measurement Is Scheduled

A component tree may only be read under its own session lock, so no UI is ever measured by another user’s request thread. Every UI measures itself: at UI init, after each navigation, and when an RPC invocation ends — the last of these throttled to one tree walk per UI per `vaadin.observability.ui-state-sample-interval` milliseconds.

This is why the feature is off by default: it costs a tree walk that ordinary request handling doesn’t. It’s also why an idle user contributes their state as of their last interaction, and why `vaadin.ui.state.sample.age.max` exists — it publishes how stale the oldest measurement in the aggregate is, so a reading can be judged rather than assumed current.

The cost of one walk is proportional to the size of the tree it measures, and it’s paid on the request thread while the session lock is held. The interval is therefore what bounds the overhead: tree size times interaction rate, capped at one walk per UI per interval. The default of ten seconds keeps a capacity trend legible on a grid-heavy application with many concurrent users. Lower it for a sharper signal, and raise it if the measurement becomes visible in `vaadin.session.lock.hold`.

### <a id="nodes-not-bytes"></a>Nodes, Not Bytes

A node count is a proxy for retained heap, not a measurement of it: one `Grid` node backed by 100,000 rows counts as a single node. The kit therefore publishes no byte figure by default, because a guessed per-user cost is worse than a missing one.

If you measure the cost for your own application — settle the heap, build a number of copies of a representative view, keep them reachable, and read the difference from `MemoryMXBean` — set the result and the projection becomes available as `vaadin.ui.state.size`:

`application.properties`

```properties
vaadin.observability.ui-state-bytes-per-node=96
```

Divided into the heap headroom, that’s an estimate of how many more tabs the instance can hold.

### <a id="state-outside-the-tree"></a>State Outside the Tree

The tree gauges can’t see what a view keeps in its own fields. A view that appends each refresh to a `List` — to show what changed since the last one, say — grows on the heap every time, while its component tree, and every gauge above, stays flat. Only the heap shows it, and the heap can’t say which view is to blame.

So with `ui-state` on, each measurement also reads the collections that views hold: the instance fields of every route target and router layout in the tree, and of their superclasses up to the first Flow class, whose declared type is a `Collection`, a `Map`, or an array. A field’s size includes the collections nested directly inside it, so a `List<List<T>>` that gains a whole result set per refresh grows by that result set. The kit publishes the totals as `vaadin.ui.state.retained.elements` and `vaadin.ui.state.retained.elements.max`.

It then follows each field of each view instance from one measurement of its UI to the next. A field that has grown at `vaadin.observability.ui-state-growth-samples` measurements (5 by default) without shrinking in between is counted in `vaadin.ui.state.retained.growing`. A measurement that finds the size unchanged neither counts nor breaks the run, because measurements follow interactions, not the code that adds to the field. The gauge is normally zero, so it can be alerted on directly:

```yaml
- alert: VaadinViewStateGrowing
  expr: max(vaadin_ui_state_retained_growing) > 0
  for: 10m
  annotations:
    summary: A view keeps accumulating state; see /actuator/vaadin/observability
```

Which view and field is growing isn’t a tag, since that would add a series per view class. The insights endpoint reports it instead, as a [`growing-view-state` insight](https://vaadin.com/docs/next/tools/observability/insights.md#growing-view-state): one per field, naming the class and the field, how large it is in the worst UI, what it started from, and how many open UIs show the same growth.

What’s deliberately not read: only `java.util` implementations are asked for their size, because another collection may do work to answer — a lazy JPA association loads itself on `size()` — and nothing reachable from an element is followed. These figures count what a field holds, not how many bytes that is.

Some collections legitimately grow for a while, like a chat log or rows a user keeps adding. Raise `ui-state-growth-samples` if they’re reported too early, or set it to `0` to turn the collection reading off.

## <a id="database-monitoring"></a>Database Monitoring

With the Spring Boot starter, the kit can watch how many rows your queries return and how long they take, without touching application code. Enable it with:

`application.properties`

```properties
vaadin.observability.database=true
```

Every `DataSource` bean is then wrapped so that each JDBC `ResultSet` reports its row count into the `vaadin.db.fetch.rows` distribution summary, **tagged by the Vaadin `route`** that triggered the fetch. This lets you see which view issues the large reads. Watch the p95/p99 of that summary and alert on it in your backend — for example a Prometheus rule on `vaadin_db_fetch_rows` — to catch runaway result sets in production.

This is off by default: it reaches outside the Vaadin runtime into the persistence layer and adds a small per-row cost. It covers all JDBC access — Spring Data, `JdbcTemplate`, and raw JDBC — that flows through a managed `DataSource`. Row counting is best-effort and attributes to `_unknown` when no view is active, such as for background tasks.

The same counting also feeds the interaction insights: a slow data query insight reports how many SQL queries answering it took and how many rows they read. See [What a Slow Data Query Cost in the Database](https://vaadin.com/docs/next/tools/observability/insights.md#slow-query-sql).

### <a id="locating-slow-or-large-queries-in-a-trace"></a>Locating Slow or Large Queries in a Trace

When tracing is also enabled (`vaadin.observability.traces=true`, the default), each query additionally opens a `vaadin.db.query` span. Because it starts on the request-handling thread inside the Vaadin request span, it nests under that request or RPC span automatically. In Jaeger — or any backend fed by your Micrometer tracing bridge — you can open a slow interaction and see the individual queries it ran, each carrying the `route` and a `db.rows` attribute. The same observation also yields a `vaadin.db.query` duration timer: database time per view.

The span doesn’t include the SQL text by default. Set `vaadin.observability.database-statement=true` to attach the parameterized statement as `db.statement`. This is useful for pinpointing the offending query, but it’s opt-in because SQL is higher cardinality and can be sensitive.

### <a id="span-limit"></a>N+1 Loads and the Span Limit

An N+1 load — one query for a list, then one more per row, typically an eagerly or lazily loaded JPA relation — can issue tens of thousands of queries for a single click. A span for each would overflow the tracing exporter’s queue (OpenTelemetry’s `BatchSpanProcessor` holds 2048 by default), and the exporter then drops spans from every request, not only the one at fault.

At most `vaadin.observability.database-span-limit` (100 by default) query spans are therefore started under any one parent span — a Vaadin request, an RPC, or a data provider fetch. Past that, a query:

- still gets its `vaadin.db.query` observation, so it counts toward the same timer, with any tags your `ObservationFilter` instances or `management.observations.key-values.*` properties add, and an `ObservationPredicate` that disables the observation applies to it too — database time per view isn’t under-reported for exactly the load that’s slow;

- still counts toward the query and row totals of the [slow data query insight](https://vaadin.com/docs/next/tools/observability/insights.md#slow-query-sql);

- gets no span of its own: the starter registers a tracing handler ahead of Spring Boot’s default one that takes these observations and creates no span. Instead, the parent span carries `vaadin.db.queries.unspanned` with the number left out.

In a trace, an N+1 load therefore looks like a request or fetch span with `vaadin.db.queries.unspanned` set, the first 100 query spans under it — most with `db.rows=1` — and one query with a large `db.rows` near their start. A query with no enclosing span, such as one on an application thread with no observation, has nothing to count against and isn’t limited.

The handler is registered only when Micrometer Tracing is on the classpath, which is the case whenever the application exports spans.

## <a id="extending-built-in-instrumentation"></a>Extending Built-In Instrumentation

To record your own metrics and spans alongside these, see the [Custom Instrumentation](https://vaadin.com/docs/next/tools/observability/customization.md) page. Custom meters and spans share the same registry and backend, so keep your names and tag cardinality consistent with the conventions above.

`4E9CED65-0EA1-4590-956A-6198F0F90482`
