Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
92646ee
feat: add meter/observation name constants and settings validation
heruan Jun 5, 2026
2117f00
feat: create ObservationRegistry with default handler when tracing en…
heruan Jun 5, 2026
354b6ff
feat: add session created/duration meters and session-lock metrics
heruan Jun 5, 2026
9957098
feat: add route tag resolver, navigation metrics, and request-interac…
heruan Jun 5, 2026
6057e39
test: assert session-lock context tag (access + request); make Sessio…
heruan Jun 5, 2026
bd77e73
feat: add UI metrics binder and wire UI-init listener
heruan Jun 5, 2026
7569a80
test: isolate UI-disabled test registry and cover poll-listener path
heruan Jun 5, 2026
7ec5640
feat: add request/error metrics binder with observation path
heruan Jun 5, 2026
1db619c
fix: clear request thread-locals at start and strip URL fragment from…
heruan Jun 5, 2026
ebfa025
feat: add tracing executor and finalize observation/DI wiring in serv…
heruan Jun 5, 2026
d683393
feat: add server-side RPC metrics and spans via RpcInvocationListener
heruan Jun 5, 2026
547d392
test: cover RPC observation error + traces-disabled paths; align outc…
heruan Jun 5, 2026
557f3e2
feat: add client-side metrics collection (browser to server) without …
heruan Jun 5, 2026
d041aa4
fix: resolve route template before length cap; cover client collector…
heruan Jun 5, 2026
feeb1ab
fix: register UI listener for client-only config; restore navigation …
heruan Jun 5, 2026
cc98931
refactor: strip comments from client JS before injection; drop stale …
heruan Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions observability-kit-micrometer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>context-propagation</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,52 @@ public final class MeterNames {
/** Gauge: number of currently active Vaadin sessions. */
public static final String SESSIONS_ACTIVE = "vaadin.sessions.active";

public static final String SESSIONS_CREATED = "vaadin.sessions.created";
public static final String SESSIONS_DURATION = "vaadin.sessions.duration";

public static final String SESSION_LOCK_WAIT = "vaadin.session.lock.wait";
public static final String SESSION_LOCK_HOLD = "vaadin.session.lock.hold";

public static final String UI_ACTIVE = "vaadin.ui.active";
public static final String UI_CREATED = "vaadin.ui.created";

public static final String NAVIGATION = "vaadin.navigation";

public static final String REQUEST_DURATION = "vaadin.request.duration";

public static final String ERRORS = "vaadin.errors";

public static final String CLIENT_BOOTSTRAP_DURATION = "vaadin.client.bootstrap.duration";
public static final String CLIENT_NAVIGATION_DURATION = "vaadin.client.navigation.duration";
public static final String CLIENT_RPC_DURATION = "vaadin.client.rpc.duration";
public static final String CLIENT_WEB_VITALS_LCP = "vaadin.client.web_vitals.lcp";
public static final String CLIENT_WEB_VITALS_FCP = "vaadin.client.web_vitals.fcp";
public static final String CLIENT_ERRORS = "vaadin.client.errors";
public static final String CLIENT_DROPPED = "vaadin.client.dropped";
public static final String CLIENT_THROTTLED = "vaadin.client.throttled";

public static final String TAG_ROUTE = "route";
public static final String TAG_OUTCOME = "outcome";
public static final String TAG_EXCEPTION = "exception";
public static final String TAG_TRIGGER = "trigger";
public static final String TAG_KIND = "kind";
public static final String TAG_CONTEXT = "context";

public static final String OUTCOME_SUCCESS = "success";
public static final String OUTCOME_ERROR = "error";

public static final String CONTEXT_REQUEST = "request";
public static final String CONTEXT_ACCESS = "access";

public static final String ROUTE_OTHER = "_other";
public static final String ROUTE_UNKNOWN = "_unknown";

/** Timer: server-side RPC invocation duration. */
public static final String RPC_DURATION = "vaadin.rpc.duration";

/** Tag key: RPC invocation type. */
public static final String TAG_TYPE = "type";

private MeterNames() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,189 @@
*/
package com.vaadin.observability.micrometer;

import java.util.Objects;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.observation.ObservationRegistry;

import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.observability.micrometer.trace.TracingExecutor;

/**
* Wires Observability Kit instrumentation into a {@code VaadinService} at
* initialization. The no-arg constructor (used by the Java SPI) resolves the
* registry and settings from {@link ObservabilityKit}; the registry/settings
* constructor supports dependency-injection environments.
* initialization.
* <p>
* Three construction paths:
* <ul>
* <li>Spring/Boot — instantiated as a bean with explicit {@code meterRegistry},
* {@code observationRegistry} (optional), and {@code settings} arguments.</li>
* <li>Spring/Boot without observation registry — use the two-arg
* constructor.</li>
* <li>Standalone — the no-arg constructor is invoked by the Java
* {@link java.util.ServiceLoader}; the registry and configuration are looked up
* from {@link ObservabilityKit} at {@code serviceInit} time. If
* {@link ObservabilityKit#install} was never called, the listener silently
* no-ops.</li>
* </ul>
* <p>
* When an {@link ObservationRegistry} is available and
* {@link ObservabilitySettings#isTraces()} is on, the listener also wraps the
* service's executor with a {@link TracingExecutor} so trace context flows
* across {@code UI.access(...)} boundaries.
*/
public class MetricsServiceInitListener implements VaadinServiceInitListener {

private final MeterRegistry registry;
private final MeterRegistry meterRegistry;
private final ObservationRegistry observationRegistry;
private final ObservabilitySettings settings;

/**
* Constructor used by {@link java.util.ServiceLoader}. Resolves the
* registry, observation registry, and settings lazily from
* {@link ObservabilityKit}.
*/
public MetricsServiceInitListener() {
this(null, null);
this.meterRegistry = null;
this.observationRegistry = null;
this.settings = null;
}

/**
* Constructor used by DI containers that don't provide an
* {@link ObservationRegistry}.
*
* @param meterRegistry
* Micrometer meter registry, not {@code null}
* @param settings
* instrumentation settings, not {@code null}
*/
public MetricsServiceInitListener(MeterRegistry meterRegistry,
ObservabilitySettings settings) {
this(meterRegistry, null, settings);
}

public MetricsServiceInitListener(MeterRegistry registry,
/**
* Constructor used by DI containers.
*
* @param meterRegistry
* Micrometer meter registry, not {@code null}
* @param observationRegistry
* Micrometer observation registry, may be {@code null} to
* disable Observation-based instrumentation
* @param settings
* instrumentation settings, not {@code null}
*/
public MetricsServiceInitListener(MeterRegistry meterRegistry,
ObservationRegistry observationRegistry,
ObservabilitySettings settings) {
this.registry = registry;
this.settings = settings;
this.meterRegistry = Objects.requireNonNull(meterRegistry,
"meterRegistry");
this.observationRegistry = observationRegistry;
this.settings = Objects.requireNonNull(settings, "settings");
if (observationRegistry != null && settings.isTraces()) {
installDefaultObservationHandlers(observationRegistry,
meterRegistry);
}
}

/**
* Registers default {@link io.micrometer.observation.ObservationHandler}s
* that make Observations produce
* {@link io.micrometer.core.instrument.Timer}s.
* <p>
* The default implementation installs
* {@link DefaultMeterObservationHandler}. Spring Boot deployments override
* this method to no-op because the Boot Actuator's
* {@code ObservationAutoConfiguration} already registers the same handler.
*
* @param observationRegistry
* the registry to configure
* @param meterRegistry
* the meter registry to attach timers to
*/
protected void installDefaultObservationHandlers(
ObservationRegistry observationRegistry,
MeterRegistry meterRegistry) {
observationRegistry.observationConfig().observationHandler(
new DefaultMeterObservationHandler(meterRegistry));
}

/**
* Hook for DI integrations to enrich the framework-level HTTP observation
* (e.g. Spring's {@code ServerHttpObservationFilter} span) with
* Vaadin-specific information so the parent HTTP span renders informatively
* in the trace UI. Called from {@link RequestMetricsBinder} after the
* Vaadin request type has been determined and before the
* {@code vaadin.request.<type>} child observation is started.
* <p>
* Default implementation no-ops, keeping the framework-agnostic core free
* of Spring imports. The Spring/Boot integration modules override this to
* call into their respective HTTP-observation APIs.
*
* @param request
* the current Vaadin request
* @param requestType
* the classified request type (e.g. {@code uidl},
* {@code heartbeat}, {@code push}, {@code static},
* {@code other})
*/
protected void enrichHttpObservation(VaadinRequest request,
String requestType) {
// no-op by default
}

@Override
public void serviceInit(ServiceInitEvent event) {
MeterRegistry meterRegistry = registry != null ? registry
MeterRegistry r = meterRegistry != null ? meterRegistry
: ObservabilityKit.getMeterRegistry();
ObservabilitySettings effectiveSettings = settings != null ? settings
ObservabilitySettings s = settings != null ? settings
: ObservabilityKit.getSettings();
if (meterRegistry == null || effectiveSettings == null) {
if (r == null || s == null) {
return;
}
ObservationRegistry or = observationRegistry != null
? observationRegistry
: ObservabilityKit.getObservationRegistry();
bind(event, r, or, s);
}

void bind(ServiceInitEvent event, MeterRegistry registry,
ObservationRegistry observationRegistry,
ObservabilitySettings settings) {
VaadinService service = event.getSource();
if (effectiveSettings.isSessions()) {
SessionMetricsBinder binder = new SessionMetricsBinder(
meterRegistry);

if (settings.isSessions()) {
SessionMetricsBinder binder = new SessionMetricsBinder(registry);
service.addSessionInitListener(binder);
service.addSessionDestroyListener(binder);
service.addSessionLockListener(
new SessionLockMetricsBinder(registry));
}

if (settings.isUis() || settings.isNavigation()
|| settings.isClient()) {
service.addUIInitListener(new UiMetricsBinder(registry,
observationRegistry, settings));
}

if (settings.isRequests() || settings.isErrors()) {
event.addVaadinRequestInterceptor(
new RequestMetricsBinder(registry, observationRegistry,
settings, this::enrichHttpObservation));
}

if (settings.isRequests()) {
service.addRpcInvocationListener(new RpcMetricsBinder(registry,
observationRegistry, settings));
}

if (settings.isTraces() && observationRegistry != null) {
event.getExecutor().ifPresent(exec -> event.setExecutor(
new TracingExecutor(exec, observationRegistry)));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Copyright (C) 2000-2026 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See <https://vaadin.com/commercial-license-and-service-terms> for the full
* license.
*/
package com.vaadin.observability.micrometer;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;

import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.AfterNavigationEvent;
import com.vaadin.flow.router.AfterNavigationListener;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterListener;
import com.vaadin.observability.micrometer.trace.ObservationNames;

/**
* Times each navigation from {@code beforeEnter} to {@code afterNavigation}.
* <p>
* When an {@link ObservationRegistry} is supplied and
* {@link ObservabilitySettings#isTraces()} is on, the navigation is observed
* (producing both a span and, through a registered
* {@code DefaultMeterObservationHandler}, the Timer). Otherwise the binder
* falls back to direct Timer recording. Per-UI state is stored as a UI
* attribute so concurrent UIs are tracked independently.
*/
final class NavigationMetricsBinder
implements BeforeEnterListener, AfterNavigationListener {

private static final String SAMPLE_KEY = NavigationMetricsBinder.class
.getName() + ".sample";
private static final String ROUTE_KEY = NavigationMetricsBinder.class
.getName() + ".route";
private static final String OBSERVATION_KEY = NavigationMetricsBinder.class
.getName() + ".observation";
private static final String OBSERVATION_SCOPE_KEY = NavigationMetricsBinder.class
.getName() + ".observation.scope";

private final MeterRegistry registry;
private final ObservationRegistry observationRegistry;
private final ObservabilitySettings config;
private final RouteTagResolver routes;

NavigationMetricsBinder(MeterRegistry registry, RouteTagResolver routes) {
this(registry, null, ObservabilitySettings.builder().build(), routes);
}

NavigationMetricsBinder(MeterRegistry registry,
ObservationRegistry observationRegistry,
ObservabilitySettings config, RouteTagResolver routes) {
this.registry = registry;
this.observationRegistry = observationRegistry;
this.config = config;
this.routes = routes;
}

private boolean useObservation() {
return config.isTraces() && observationRegistry != null;
}

@Override
public void beforeEnter(BeforeEnterEvent event) {
UI ui = event.getUI();
String route = routes.tagFor(event.getNavigationTarget());
ComponentUtil.setData(ui, ROUTE_KEY, route);
if (useObservation()) {
// Tell the enclosing request span this UIDL request navigated.
RequestInteraction.mark(ObservationNames.INTERACTION_NAVIGATION);
Observation obs = Observation
.createNotStarted(MeterNames.NAVIGATION,
observationRegistry)
.contextualName(ObservationNames.NAVIGATION + " " + route)
.lowCardinalityKeyValue(ObservationNames.KEY_ROUTE, route)
.start();
ComponentUtil.setData(ui, OBSERVATION_KEY, obs);
ComponentUtil.setData(ui, OBSERVATION_SCOPE_KEY, obs.openScope());
} else {
ComponentUtil.setData(ui, SAMPLE_KEY, Timer.start(registry));
}
}

@Override
public void afterNavigation(AfterNavigationEvent event) {
UI ui = UI.getCurrent();
if (ui == null) {
return;
}
Object sample = ComponentUtil.getData(ui, SAMPLE_KEY);
Object route = ComponentUtil.getData(ui, ROUTE_KEY);
Object obsObj = ComponentUtil.getData(ui, OBSERVATION_KEY);
Object scopeObj = ComponentUtil.getData(ui, OBSERVATION_SCOPE_KEY);
ComponentUtil.setData(ui, SAMPLE_KEY, null);
ComponentUtil.setData(ui, ROUTE_KEY, null);
ComponentUtil.setData(ui, OBSERVATION_KEY, null);
ComponentUtil.setData(ui, OBSERVATION_SCOPE_KEY, null);
if (sample instanceof Timer.Sample s) {
s.stop(registry.timer(MeterNames.NAVIGATION, MeterNames.TAG_ROUTE,
route instanceof String r ? r : MeterNames.ROUTE_UNKNOWN,
MeterNames.TAG_OUTCOME, MeterNames.OUTCOME_SUCCESS));
}
if (scopeObj instanceof Observation.Scope scope) {
scope.close();
}
if (obsObj instanceof Observation obs) {
obs.lowCardinalityKeyValue(ObservationNames.KEY_OUTCOME,
ObservationNames.OUTCOME_SUCCESS);
obs.stop();
}
}
}
Loading
Loading