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 page.
Each group of meters and spans is controlled by a feature toggle (for example vaadin.observability.sessions).
See the Configuration 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 that its conventions — Prometheus, for instance, renders vaadin.request.duration as vaadin_request_duration_seconds with a _count, _sum, and bucket suffixes.
|
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.
Session Metrics
Controlled by vaadin.observability.sessions.
| Meter | Type | Description |
|---|---|---|
| Gauge | Currently active sessions. |
| Counter | Sessions created since startup. |
| Timer | Session lifetime, recorded when a session ends. |
| Timer | Time spent waiting to acquire the session lock.
Tagged by |
| Timer | Time the session lock is held.
Tagged by |
The context tag is request when the lock is taken during request handling, or access when it’s taken through UI.access().
UI Metrics
Controlled by vaadin.observability.uis.
| Meter | Type | Description |
|---|---|---|
| Gauge | Currently active UIs. |
| Counter | UIs created since startup. |
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 for how the measurement is scheduled and how to read it.
| Meter | Type | Description |
|---|---|---|
| Gauge | State-tree nodes retained across all tracked UIs — how much UI state the server currently holds for live users. |
| Gauge | State-tree nodes held by the largest single UI. |
| Gauge | Server-side component instances retained across all UIs. |
| 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. |
| Gauge | Retained views that are no longer part of their UI’s active navigation — views that outlived it. Normally zero. |
| Gauge | Retained UI state in bytes, projected from the node count.
Registered only when |
| Gauge | Age in seconds of the stalest per-UI measurement in the aggregate. |
| Gauge | State-tree nodes held by the largest single session. |
| Gauge | Most UIs (browser tabs) held open by one session. |
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.
Navigation Metrics
Controlled by vaadin.observability.navigation.
| Meter | Type | Description |
|---|---|---|
| Timer | Navigation duration, from |
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 |
|---|---|
| The navigation reached |
| A listener called |
| A listener called |
| The navigation failed: |
| The navigation was neither completed nor redirected.
A re-entrant |
|
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:
|
Request Metrics
Controlled by vaadin.observability.requests.
| Meter | Type | Description |
|---|---|---|
| Timer | Server-side request handling time.
Tagged by |
| Timer | Server-side RPC invocation time.
Tagged by |
|
Note
|
The Tags Are the Same With Tracing On or Off
With tracing on — the default — The span’s |
Resync Metrics
Controlled by vaadin.observability.resync.
These track UIDL message-recovery events, which indicate a flaky client-server connection.
| Meter | Type | Description |
|---|---|---|
| Counter | Client-server message-recovery events.
Tagged by |
Error Metrics
Controlled by vaadin.observability.errors.
| Meter | Type | Description |
|---|---|---|
| Counter | Every server-side failure the kit observes.
Tagged by |
The counter covers both kinds of server-side failure:
- Exceptions that escape request handling
-
For example one thrown by a custom
RequestHandler. These reach aVaadinRequestInterceptor. - Failures Flow routes to the session’s error handler
-
Everything a user can trigger — a click or value-change listener that throws, a
UI.access()body, a detach listener, or abeforeEnter()callback. Flow catches these and hands them toVaadinSession.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.
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 |
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 UI in each ten-second window.
| Meter | Type | Description |
|---|---|---|
| Timer | Browser application bootstrap time. |
| Timer | Browser-observed navigation time. |
| Timer | Largest Contentful Paint. |
| Timer | First Contentful Paint. |
| Counter | Errors reported by the browser.
Tagged by |
| Counter | Client samples rejected by the per-UI rate limit. Untagged. |
| Counter | Client samples dropped before recording, for example a sample submitted under a name that isn’t on the ingest allowlist. Untagged. |
Every client timer is 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.
Server round-trip timing isn’t collected in the browser.
Use the server-side vaadin.request.duration and vaadin.rpc.duration timers for that.
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 dropped at ingest, which caps the cardinality a buggy or malicious client can create.
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 |
|---|---|---|
| Timer | Duration of a count query — how many items a level holds.
Tagged by |
| Timer | Duration of a fetch query — loading one page of items.
Tagged by |
| Distribution summary | Items a fetch query asked for.
Tagged by |
| Distribution summary | Items a fetch query actually returned.
Tagged by |
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 to attribute a slow query to a view.
When tracing is enabled, each query also opens a span — see Data Provider Spans.
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 for how this works and when to use it.
| Meter | Type | Description |
|---|---|---|
| Distribution summary | Rows read from a JDBC result set.
Tagged by |
| Timer | JDBC query duration.
Tagged by |
Common Tag Values
| Tag | Values |
|---|---|
|
|
| The simple class name of the exception that ended the operation, or |
| The target route template.
Distinct values are capped by |
| On |
|
|
| On RPC meters and spans, the RPC invocation type as reported by Flow — |
| The simple class name of the counted exception, capped by the same cardinality limit as |
| On the data provider meters, whether the query carried a filter: |
| On |
| On |
|
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.
|
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 page.
The kit produces the following spans:
| Span | Description |
|---|---|
| The root span for each Vaadin request. Carries the request-level attributes below. |
| A navigation, nested under the request that triggered it. |
| A server-side RPC invocation (DOM event, |
| One task run on the Vaadin service executor, nested under whatever trace was active when the task was submitted. See Background Work. |
| A data provider count query, nested under the request or RPC span that triggered the load.
Controlled by |
| A data provider fetch query, nested under the request or RPC span that triggered the load.
Controlled by |
| A single JDBC query, nested under the request or RPC span that ran it. Emitted only when database monitoring is enabled (see 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.
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.ui.access span, instead of appearing as an unrelated root span.
This covers the executor Vaadin dispatches signal effects and result notifications on, and that applications are expected to use for their own background work — typically a task that ends by pushing its result through UI.access().
It isn’t UI.access() itself: a command queued with UI.access() runs on whichever thread unlocks the session, and is recorded there.
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.
Span Attributes
The root vaadin.request span carries these attributes:
| Attribute | Description |
|---|---|
| The protocol-level request type: |
| What the request actually did: |
| The HTTP method of the request. |
|
|
| The ID of the UI associated with the request, or |
| The browser location the request was sent from, or |
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.
The vaadin.ui.access 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.
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 |
|---|---|
| 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 |
| Index of the first item a fetch query asked for. Fetch spans only. |
| Number of items a fetch query asked for. Fetch spans only. |
| Number of items a fetch query returned. Fetch spans only, and only when the query succeeded. |
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.
The failure also increments the vaadin.errors counter; see 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 page.
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:
Source code
application.properties
application.propertiesvaadin.observability.ui-state=trueEach UI then reports its own state-tree size, and the kit publishes the aggregates listed in 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.
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.
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:
Source code
application.properties
application.propertiesvaadin.observability.ui-state-bytes-per-node=96Divided into the heap headroom, that’s an estimate of how many more tabs the instance can hold.
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:
Source code
application.properties
application.propertiesvaadin.observability.database=trueEvery 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.
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.
Extending Built-In Instrumentation
To record your own metrics and spans alongside these, see the Custom Instrumentation 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