Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions observability-kit-micrometer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
<groupId>com.vaadin</groupId>
<artifactId>flow-server</artifactId>
</dependency>
<!--
Dev-only: provides the DevToolsMessageHandler SPI used by the
Copilot metrics panel. Optional so it is never pulled onto an
application's production runtime classpath.
-->
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-dev-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.observation.ObservationRegistry;

import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
Expand Down Expand Up @@ -155,7 +156,20 @@ public void serviceInit(ServiceInitEvent event) {
ObservationRegistry or = observationRegistry != null
? observationRegistry
: ObservabilityKit.getObservationRegistry();
// Record the bound registry so the dev-mode Copilot metrics panel can
// read the live meters regardless of deployment type.
ObservabilityKit.setActiveMeterRegistry(r);
bind(event, r, or, s);
DeploymentConfiguration configuration = event.getSource()
.getDeploymentConfiguration();
boolean productionMode = configuration != null
? configuration.isProductionMode()
: true;
if (!productionMode) {
event.getSource()
.addUIInitListener(uiEvent -> ObservabilityDevToolsClient
.inject(uiEvent.getUI()));
}
}

void bind(ServiceInitEvent event, MeterRegistry registry,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* 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 java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.internal.StringUtil;

/**
* Loads the in-browser Vaadin Copilot metrics panel. The panel registers itself
* with Copilot's plugin API and pulls metric snapshots from the server over the
* dev-tools websocket (see {@code ObservabilityDevToolsHandler}).
* <p>
* Injected once per UI and only in development mode; in production Copilot and
* the dev-tools connection do not exist, so this is never called.
*/
final class ObservabilityDevToolsClient {

private static final String INIT_KEY = "vaadinObservabilityDevToolsInitialized";
private static final String CLIENT_RESOURCE = "META-INF/frontend/VaadinObservabilityDevTools.js";

private ObservabilityDevToolsClient() {
}

static void inject(UI ui) {
if (ui == null || ComponentUtil.getData(ui, INIT_KEY) != null) {
return;
}
ComponentUtil.setData(ui, INIT_KEY, Boolean.TRUE);
ClassLoader loader = ObservabilityDevToolsClient.class.getClassLoader();
try (InputStream in = loader.getResourceAsStream(CLIENT_RESOURCE)) {
if (in == null) {
LoggerFactory.getLogger(ObservabilityDevToolsClient.class).warn(
"observability-kit dev-tools resource not found: {}",
CLIENT_RESOURCE);
return;
}
String js = StringUtil.removeComments(
new String(in.readAllBytes(), StandardCharsets.UTF_8),
true);
ui.getPage().executeJs(js);
} catch (IOException e) {
LoggerFactory.getLogger(ObservabilityDevToolsClient.class).warn(
"Could not load observability-kit dev-tools panel code", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ public final class ObservabilityKit {
private static final AtomicReference<ObservationRegistry> OBSERVATION_REGISTRY = new AtomicReference<>();
private static final AtomicReference<ObservabilitySettings> SETTINGS = new AtomicReference<>();

/**
* The registry instrumentation was actually bound to, recorded at
* {@code serviceInit} time. Unlike {@link #METER_REGISTRY} (only populated
* by {@link #install} in standalone deployments) this is set for every
* deployment type, including Spring where the registry arrives via DI. Used
* by the dev-mode Copilot metrics panel to read the live meters.
*/
private static final AtomicReference<MeterRegistry> ACTIVE_METER_REGISTRY = new AtomicReference<>();

private ObservabilityKit() {
}

Expand Down Expand Up @@ -55,6 +64,25 @@ static MeterRegistry getMeterRegistry() {
return METER_REGISTRY.get();
}

/**
* Records the registry instrumentation was bound to. Called from
* {@code MetricsServiceInitListener} for all deployment types.
*/
static void setActiveMeterRegistry(MeterRegistry registry) {
ACTIVE_METER_REGISTRY.set(registry);
}

/**
* The registry instrumentation is currently publishing to, or {@code null}
* if instrumentation has not been bound. Read by the dev-mode Copilot
* metrics panel.
*
* @return the active meter registry, or {@code null}
*/
public static MeterRegistry getActiveMeterRegistry() {
return ACTIVE_METER_REGISTRY.get();
}

static ObservationRegistry getObservationRegistry() {
return OBSERVATION_REGISTRY.get();
}
Expand All @@ -68,5 +96,6 @@ static void reset() {
METER_REGISTRY.set(null);
OBSERVATION_REGISTRY.set(null);
SETTINGS.set(null);
ACTIVE_METER_REGISTRY.set(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* 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.devtools;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.FunctionCounter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.Measurement;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import tools.jackson.databind.JsonNode;

import com.vaadin.base.devserver.DevToolsInterface;
import com.vaadin.base.devserver.DevToolsMessageHandler;
import com.vaadin.observability.micrometer.ObservabilityKit;

/**
* Dev-mode bridge between the live Micrometer {@link MeterRegistry} and the
* Vaadin Copilot metrics panel.
* <p>
* Discovered via the Java {@link java.util.ServiceLoader} by Flow's dev-tools
* server (see {@code META-INF/services}). On request from the panel it
* snapshots every {@code vaadin.*} meter and sends it to the browser over the
* shared dev-tools websocket. This is a developer-only convenience view; it has
* no effect in production where the dev-tools connection does not exist.
*/
public class ObservabilityDevToolsHandler implements DevToolsMessageHandler {

static final String COMMAND_REFRESH = "observability-kit-refresh";
static final String COMMAND_METRICS = "observability-kit-metrics";

/** Only meters under this prefix are exposed to the panel. */
private static final String METER_PREFIX = "vaadin.";

@Override
public void handleConnect(DevToolsInterface devToolsInterface) {
// Push an initial snapshot; the panel also pulls on demand.
sendSnapshot(devToolsInterface);
}

@Override
public boolean handleMessage(String command, JsonNode data,
DevToolsInterface devToolsInterface) {
if (COMMAND_REFRESH.equals(command)) {
sendSnapshot(devToolsInterface);
return true;
}
return false;
}

private void sendSnapshot(DevToolsInterface devToolsInterface) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("timestamp", System.currentTimeMillis());
payload.put("meters", snapshot());
devToolsInterface.send(COMMAND_METRICS, payload);
}

private List<Map<String, Object>> snapshot() {
List<Map<String, Object>> meters = new ArrayList<>();
MeterRegistry registry = ObservabilityKit.getActiveMeterRegistry();
if (registry == null) {
return meters;
}
for (Meter meter : registry.getMeters()) {
Meter.Id id = meter.getId();
if (!id.getName().startsWith(METER_PREFIX)) {
continue;
}
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("name", id.getName());
entry.put("type", id.getType().name());

Map<String, String> tags = new LinkedHashMap<>();
for (Tag tag : id.getTags()) {
tags.put(tag.getKey(), tag.getValue());
}
entry.put("tags", tags);

// Emit derived, interpretable values per meter type rather than raw
// statistics. For timers the cumulative mean is the stable, useful
// figure (TOTAL_TIME is an ever-growing sum and the SimpleMeter
// registry's MAX decays to 0 between polls).
if (meter instanceof Timer timer) {
entry.put("count", timer.count());
entry.put("mean", timer.mean(TimeUnit.MILLISECONDS));
entry.put("max", timer.max(TimeUnit.MILLISECONDS));
entry.put("unit", "ms");
} else if (meter instanceof Counter counter) {
entry.put("count", (long) counter.count());
} else if (meter instanceof FunctionCounter counter) {
entry.put("count", (long) counter.count());
} else if (meter instanceof Gauge gauge) {
entry.put("value", gauge.value());
} else if (meter instanceof DistributionSummary summary) {
entry.put("count", summary.count());
entry.put("mean", summary.mean());
entry.put("max", summary.max());
if (id.getBaseUnit() != null) {
entry.put("unit", id.getBaseUnit());
}
} else {
// Unknown meter type: fall back to raw measurements.
List<Map<String, Object>> measurements = new ArrayList<>();
for (Measurement measurement : meter.measure()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("statistic", measurement.getStatistic().name());
m.put("value", measurement.getValue());
measurements.add(m);
}
entry.put("measurements", measurements);
}
meters.add(entry);
}
return meters;
}
}
Loading
Loading