-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add copilot window for stats #338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
be4bf59
5810c25
2977a4a
8a3e0ee
ee07a6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /** | ||
| * 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 a bundled client-side JavaScript resource into a {@link UI} exactly | ||
| * once. The classpath resource is read, stripped of comments and executed via | ||
| * {@link com.vaadin.flow.component.page.Page#executeJs}. A per-UI flag keyed by | ||
| * {@code initKey} guards against repeated injection. | ||
| */ | ||
| public final class ClientResourceLoader { | ||
|
|
||
| private ClientResourceLoader() { | ||
| } | ||
|
|
||
| /** | ||
| * Injects {@code resource} into {@code ui} once. Subsequent calls with the | ||
| * same {@code initKey} for the same UI are no-ops. Missing resources or | ||
| * read failures are logged against {@code owner} and otherwise ignored. | ||
| * | ||
| * @param ui | ||
| * the target UI; {@code null} is ignored | ||
| * @param initKey | ||
| * the per-UI data key used to ensure single injection | ||
| * @param resource | ||
| * the classpath resource path of the JavaScript to load | ||
| * @param owner | ||
| * the class whose class loader and logger are used | ||
| */ | ||
| public static void loadOnce(UI ui, String initKey, String resource, | ||
| Class<?> owner) { | ||
| if (ui == null || ComponentUtil.getData(ui, initKey) != null) { | ||
| return; | ||
| } | ||
| ComponentUtil.setData(ui, initKey, Boolean.TRUE); | ||
| try (InputStream in = owner.getClassLoader() | ||
| .getResourceAsStream(resource)) { | ||
| if (in == null) { | ||
| LoggerFactory.getLogger(owner).warn( | ||
| "observability-kit client resource not found: {}", | ||
| resource); | ||
| return; | ||
| } | ||
| String js = StringUtil.removeComments( | ||
| new String(in.readAllBytes(), StandardCharsets.UTF_8), | ||
| true); | ||
| ui.getPage().executeJs(js); | ||
| } catch (IOException e) { | ||
| LoggerFactory.getLogger(owner).warn( | ||
| "Could not load observability-kit client resource: {}", | ||
| resource, e); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /** | ||
| * 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 com.vaadin.flow.component.UI; | ||
|
|
||
| /** | ||
| * 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) { | ||
| ClientResourceLoader.loadOnce(ui, INIT_KEY, CLIENT_RESOURCE, | ||
| ObservabilityDevToolsClient.class); | ||
| } | ||
| } | ||
| 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()); | ||
|
Comment on lines
+103
to
+106
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why casting these two counters to long, while
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DistributionSummary.count() actually returns long and not double where as counter and functionCounter return double so they are cast to long to normalize the value to integer for the panel. |
||
| } 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; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This duplicates almost verbatim this
ensureClientLoadedmethod:observability-kit/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/client/MetricsCollectorElement.java
Lines 78 to 99 in b057a03
I suggest to extract e.g. a
injectClientResource(UI, key, resource)to avoid duplication.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracted to Util for reuse when needed.