Skip to content

Commit 0327148

Browse files
committed
feat: add copilot window for stats
Add a copilot window to look at collected stats. To see window in any mode but play click on the bar-graph observability button.
1 parent 7bf3bb9 commit 0327148

7 files changed

Lines changed: 473 additions & 0 deletions

File tree

observability-kit-micrometer/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@
2121
<groupId>com.vaadin</groupId>
2222
<artifactId>flow-server</artifactId>
2323
</dependency>
24+
<!--
25+
Dev-only: provides the DevToolsMessageHandler SPI used by the
26+
Copilot metrics panel. Optional so it is never pulled onto an
27+
application's production runtime classpath.
28+
-->
29+
<dependency>
30+
<groupId>com.vaadin</groupId>
31+
<artifactId>vaadin-dev-server</artifactId>
32+
<optional>true</optional>
33+
</dependency>
2434
<dependency>
2535
<groupId>io.micrometer</groupId>
2636
<artifactId>micrometer-core</artifactId>

observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,17 @@ public void serviceInit(ServiceInitEvent event) {
155155
ObservationRegistry or = observationRegistry != null
156156
? observationRegistry
157157
: ObservabilityKit.getObservationRegistry();
158+
// Record the bound registry so the dev-mode Copilot metrics panel can
159+
// read the live meters regardless of deployment type.
160+
ObservabilityKit.setActiveMeterRegistry(r);
158161
bind(event, r, or, s);
162+
boolean productionMode = event.getSource().getDeploymentConfiguration()
163+
.isProductionMode();
164+
if (!productionMode) {
165+
event.getSource()
166+
.addUIInitListener(uiEvent -> ObservabilityDevToolsClient
167+
.inject(uiEvent.getUI()));
168+
}
159169
}
160170

161171
void bind(ServiceInitEvent event, MeterRegistry registry,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright (C) 2000-2026 Vaadin Ltd
3+
*
4+
* This program is available under Vaadin Commercial License and Service Terms.
5+
*
6+
* See <https://vaadin.com/commercial-license-and-service-terms> for the full
7+
* license.
8+
*/
9+
package com.vaadin.observability.micrometer;
10+
11+
import java.io.IOException;
12+
import java.io.InputStream;
13+
import java.nio.charset.StandardCharsets;
14+
15+
import org.slf4j.LoggerFactory;
16+
17+
import com.vaadin.flow.component.ComponentUtil;
18+
import com.vaadin.flow.component.UI;
19+
import com.vaadin.flow.internal.StringUtil;
20+
21+
/**
22+
* Loads the in-browser Vaadin Copilot metrics panel. The panel registers itself
23+
* with Copilot's plugin API and pulls metric snapshots from the server over the
24+
* dev-tools websocket (see {@code ObservabilityDevToolsHandler}).
25+
* <p>
26+
* Injected once per UI and only in development mode; in production Copilot and
27+
* the dev-tools connection do not exist, so this is never called.
28+
*/
29+
final class ObservabilityDevToolsClient {
30+
31+
private static final String INIT_KEY = "vaadinObservabilityDevToolsInitialized";
32+
private static final String CLIENT_RESOURCE = "META-INF/frontend/VaadinObservabilityDevTools.js";
33+
34+
private ObservabilityDevToolsClient() {
35+
}
36+
37+
static void inject(UI ui) {
38+
if (ui == null || ComponentUtil.getData(ui, INIT_KEY) != null) {
39+
return;
40+
}
41+
ComponentUtil.setData(ui, INIT_KEY, Boolean.TRUE);
42+
ClassLoader loader = ObservabilityDevToolsClient.class.getClassLoader();
43+
try (InputStream in = loader.getResourceAsStream(CLIENT_RESOURCE)) {
44+
if (in == null) {
45+
LoggerFactory.getLogger(ObservabilityDevToolsClient.class).warn(
46+
"observability-kit dev-tools resource not found: {}",
47+
CLIENT_RESOURCE);
48+
return;
49+
}
50+
String js = StringUtil.removeComments(
51+
new String(in.readAllBytes(), StandardCharsets.UTF_8),
52+
true);
53+
ui.getPage().executeJs(js);
54+
} catch (IOException e) {
55+
LoggerFactory.getLogger(ObservabilityDevToolsClient.class).warn(
56+
"Could not load observability-kit dev-tools panel code", e);
57+
}
58+
}
59+
}

observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityKit.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ public final class ObservabilityKit {
2727
private static final AtomicReference<ObservationRegistry> OBSERVATION_REGISTRY = new AtomicReference<>();
2828
private static final AtomicReference<ObservabilitySettings> SETTINGS = new AtomicReference<>();
2929

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

@@ -55,6 +64,25 @@ static MeterRegistry getMeterRegistry() {
5564
return METER_REGISTRY.get();
5665
}
5766

67+
/**
68+
* Records the registry instrumentation was bound to. Called from
69+
* {@code MetricsServiceInitListener} for all deployment types.
70+
*/
71+
static void setActiveMeterRegistry(MeterRegistry registry) {
72+
ACTIVE_METER_REGISTRY.set(registry);
73+
}
74+
75+
/**
76+
* The registry instrumentation is currently publishing to, or {@code null}
77+
* if instrumentation has not been bound. Read by the dev-mode Copilot
78+
* metrics panel.
79+
*
80+
* @return the active meter registry, or {@code null}
81+
*/
82+
public static MeterRegistry getActiveMeterRegistry() {
83+
return ACTIVE_METER_REGISTRY.get();
84+
}
85+
5886
static ObservationRegistry getObservationRegistry() {
5987
return OBSERVATION_REGISTRY.get();
6088
}
@@ -68,5 +96,6 @@ static void reset() {
6896
METER_REGISTRY.set(null);
6997
OBSERVATION_REGISTRY.set(null);
7098
SETTINGS.set(null);
99+
ACTIVE_METER_REGISTRY.set(null);
71100
}
72101
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Copyright (C) 2000-2026 Vaadin Ltd
3+
*
4+
* This program is available under Vaadin Commercial License and Service Terms.
5+
*
6+
* See <https://vaadin.com/commercial-license-and-service-terms> for the full
7+
* license.
8+
*/
9+
package com.vaadin.observability.micrometer.devtools;
10+
11+
import java.util.ArrayList;
12+
import java.util.LinkedHashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.concurrent.TimeUnit;
16+
17+
import io.micrometer.core.instrument.Counter;
18+
import io.micrometer.core.instrument.DistributionSummary;
19+
import io.micrometer.core.instrument.FunctionCounter;
20+
import io.micrometer.core.instrument.Gauge;
21+
import io.micrometer.core.instrument.Measurement;
22+
import io.micrometer.core.instrument.Meter;
23+
import io.micrometer.core.instrument.MeterRegistry;
24+
import io.micrometer.core.instrument.Tag;
25+
import io.micrometer.core.instrument.Timer;
26+
import tools.jackson.databind.JsonNode;
27+
28+
import com.vaadin.base.devserver.DevToolsInterface;
29+
import com.vaadin.base.devserver.DevToolsMessageHandler;
30+
import com.vaadin.observability.micrometer.ObservabilityKit;
31+
32+
/**
33+
* Dev-mode bridge between the live Micrometer {@link MeterRegistry} and the
34+
* Vaadin Copilot metrics panel.
35+
* <p>
36+
* Discovered via the Java {@link java.util.ServiceLoader} by Flow's dev-tools
37+
* server (see {@code META-INF/services}). On request from the panel it
38+
* snapshots every {@code vaadin.*} meter and sends it to the browser over the
39+
* shared dev-tools websocket. This is a developer-only convenience view; it has
40+
* no effect in production where the dev-tools connection does not exist.
41+
*/
42+
public class ObservabilityDevToolsHandler implements DevToolsMessageHandler {
43+
44+
static final String COMMAND_REFRESH = "observability-kit-refresh";
45+
static final String COMMAND_METRICS = "observability-kit-metrics";
46+
47+
/** Only meters under this prefix are exposed to the panel. */
48+
private static final String METER_PREFIX = "vaadin.";
49+
50+
@Override
51+
public void handleConnect(DevToolsInterface devToolsInterface) {
52+
// Push an initial snapshot; the panel also pulls on demand.
53+
sendSnapshot(devToolsInterface);
54+
}
55+
56+
@Override
57+
public boolean handleMessage(String command, JsonNode data,
58+
DevToolsInterface devToolsInterface) {
59+
if (COMMAND_REFRESH.equals(command)) {
60+
sendSnapshot(devToolsInterface);
61+
return true;
62+
}
63+
return false;
64+
}
65+
66+
private void sendSnapshot(DevToolsInterface devToolsInterface) {
67+
Map<String, Object> payload = new LinkedHashMap<>();
68+
payload.put("timestamp", System.currentTimeMillis());
69+
payload.put("meters", snapshot());
70+
devToolsInterface.send(COMMAND_METRICS, payload);
71+
}
72+
73+
private List<Map<String, Object>> snapshot() {
74+
List<Map<String, Object>> meters = new ArrayList<>();
75+
MeterRegistry registry = ObservabilityKit.getActiveMeterRegistry();
76+
if (registry == null) {
77+
return meters;
78+
}
79+
for (Meter meter : registry.getMeters()) {
80+
Meter.Id id = meter.getId();
81+
if (!id.getName().startsWith(METER_PREFIX)) {
82+
continue;
83+
}
84+
Map<String, Object> entry = new LinkedHashMap<>();
85+
entry.put("name", id.getName());
86+
entry.put("type", id.getType().name());
87+
88+
Map<String, String> tags = new LinkedHashMap<>();
89+
for (Tag tag : id.getTags()) {
90+
tags.put(tag.getKey(), tag.getValue());
91+
}
92+
entry.put("tags", tags);
93+
94+
// Emit derived, interpretable values per meter type rather than raw
95+
// statistics. For timers the cumulative mean is the stable, useful
96+
// figure (TOTAL_TIME is an ever-growing sum and the SimpleMeter
97+
// registry's MAX decays to 0 between polls).
98+
if (meter instanceof Timer timer) {
99+
entry.put("count", timer.count());
100+
entry.put("mean", timer.mean(TimeUnit.MILLISECONDS));
101+
entry.put("max", timer.max(TimeUnit.MILLISECONDS));
102+
entry.put("unit", "ms");
103+
} else if (meter instanceof Counter counter) {
104+
entry.put("count", (long) counter.count());
105+
} else if (meter instanceof FunctionCounter counter) {
106+
entry.put("count", (long) counter.count());
107+
} else if (meter instanceof Gauge gauge) {
108+
entry.put("value", gauge.value());
109+
} else if (meter instanceof DistributionSummary summary) {
110+
entry.put("count", summary.count());
111+
entry.put("mean", summary.mean());
112+
entry.put("max", summary.max());
113+
if (id.getBaseUnit() != null) {
114+
entry.put("unit", id.getBaseUnit());
115+
}
116+
} else {
117+
// Unknown meter type: fall back to raw measurements.
118+
List<Map<String, Object>> measurements = new ArrayList<>();
119+
for (Measurement measurement : meter.measure()) {
120+
Map<String, Object> m = new LinkedHashMap<>();
121+
m.put("statistic", measurement.getStatistic().name());
122+
m.put("value", measurement.getValue());
123+
measurements.add(m);
124+
}
125+
entry.put("measurements", measurements);
126+
}
127+
meters.add(entry);
128+
}
129+
return meters;
130+
}
131+
}

0 commit comments

Comments
 (0)