Skip to content
Open
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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ public void serviceInit(ServiceInitEvent event) {
boolean productionMode = event.getSource().getDeploymentConfiguration()
.isProductionMode();
if (!ObservabilityLicense.isLicensed(productionMode)) {
ObservabilityKit.setLicenseMissing(true);
LOGGER.warn(
"No valid {} license found. Observability Kit instrumentation "
+ "will not be registered and no telemetry will be collected. "
Expand All @@ -234,14 +235,10 @@ public void serviceInit(ServiceInitEvent event) {
: ObservabilityKit.getObservationRegistry();
// Record the bound registry so the dev-mode Copilot metrics panel can
// read the live meters regardless of deployment type.
ObservabilityKit.setLicenseMissing(false);
ObservabilityKit.setActiveMeterRegistry(r);
ObservabilityUsage.markAsUsed(s);
bind(event, r, or, s);
if (!productionMode) {
event.getSource()
.addUIInitListener(uiEvent -> ObservabilityDevToolsClient
.inject(uiEvent.getUI()));
}
}

void bind(ServiceInitEvent event, MeterRegistry registry,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package com.vaadin.observability.micrometer;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import io.micrometer.core.instrument.MeterRegistry;
Expand Down Expand Up @@ -55,6 +56,13 @@ public final class ObservabilityKit {
*/
private static final AtomicReference<RecentClientErrors> RECENT_CLIENT_ERRORS = new AtomicReference<>();

/**
* Whether {@code serviceInit} skipped binding because the kit has no valid
* license. Read by the dev-mode Copilot panel, which says so rather than
* showing empty sections that read like an idle application.
*/
private static final AtomicBoolean LICENSE_MISSING = new AtomicBoolean();

private ObservabilityKit() {
}

Expand Down Expand Up @@ -91,6 +99,24 @@ static void setActiveMeterRegistry(MeterRegistry registry) {
ACTIVE_METER_REGISTRY.set(registry);
}

/**
* Records whether the license check at {@code serviceInit} failed. Called
* from {@code MetricsServiceInitListener}.
*/
static void setLicenseMissing(boolean missing) {
LICENSE_MISSING.set(missing);
}

/**
* Whether instrumentation was not bound because the kit has no valid
* license. Read by the dev-mode Copilot metrics panel.
*
* @return {@code true} if the last license check failed
*/
public static boolean isLicenseMissing() {
return LICENSE_MISSING.get();
}

/**
* The registry instrumentation is currently publishing to, or {@code null}
* if instrumentation has not been bound. Read by the dev-mode Copilot
Expand Down Expand Up @@ -171,5 +197,6 @@ static void reset() {
RECENT_INTERACTIONS.set(null);
RECENT_QUERIES.set(null);
RECENT_CLIENT_ERRORS.set(null);
LICENSE_MISSING.set(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,35 @@

import java.util.List;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.observability.micrometer.ClientResourceLoader;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.observability.micrometer.ObservabilitySettings;

/**
* Hidden helper component attached to each UI when client metrics are enabled.
* Exposes a {@link ClientCallable} that receives batches of
* {@link ClientSample} from the in-browser collector. Loads the client JS on
* first attach and re-attaches itself to its UI if removed.
* {@link ClientSample} from the in-browser collector, whose script defines the
* element and starts collecting once it is attached. Re-attaches itself to its
* UI if removed.
*/
@JsModule("./VaadinMetricsClient.js")
@Tag("vaadin-metrics-collector")
public final class MetricsCollectorElement extends Component {

private static final String CLIENT_INIT_KEY = "vaadinMetricsClientInitialized";
private static final String CLIENT_RESOURCE = "META-INF/frontend/VaadinMetricsClient.js";

/**
* Tells the in-browser collector whether to gather the message of an error
* it reports. Decided on the server rather than left to its retention rule:
* a message nothing would keep should not be buffered in the tab and posted
* either, since the browser holds its buffer in {@code sessionStorage}
* across a reload and an outage.
*/
private static final String DETAILS_PRELUDE = "window.__vaadinMicrometerDetails=%s;";
private static final String DETAILS_ATTRIBUTE = "details";

private final transient ClientMetricsBinder binder;
private final transient ClientRateLimiter limiter;
private final boolean collectErrorMessages;

/**
* @deprecated use
Expand All @@ -66,10 +63,10 @@ public MetricsCollectorElement(ClientMetricsBinder binder,
public MetricsCollectorElement(ClientMetricsBinder binder,
ObservabilitySettings settings, boolean collectErrorMessages) {
this.binder = binder;
this.collectErrorMessages = collectErrorMessages;
this.limiter = new ClientRateLimiter(
settings.getClientRatePerSession());
getElement().getStyle().set("display", "none");
getElement().setAttribute(DETAILS_ATTRIBUTE, collectErrorMessages);
addDetachListener(event -> {
UI ui = event.getUI();
if (ui != null && !ui.isClosing()) {
Expand All @@ -78,13 +75,6 @@ public MetricsCollectorElement(ClientMetricsBinder binder,
});
}

@Override
protected void onAttach(AttachEvent event) {
ClientResourceLoader.loadOnce(event.getUI(), CLIENT_INIT_KEY,
CLIENT_RESOURCE, MetricsCollectorElement.class,
DETAILS_PRELUDE.formatted(collectErrorMessages));
}

@ClientCallable
public void recordSamples(List<ClientSample> samples) {
if (binder == null || samples == null || samples.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import com.vaadin.base.devserver.DevToolsInterface;
import com.vaadin.base.devserver.DevToolsMessageHandler;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.observability.micrometer.ObservabilityKit;
import com.vaadin.observability.micrometer.insights.InsightsService;

Expand All @@ -46,7 +47,11 @@
* meter table is only worth polling while the panel is open, whereas the panel
* watches for new insights whether or not anyone is looking at it, and that
* background poll should not snapshot the whole registry every time.
* <p>
* The panel itself is the development-only module declared here, which the
* frontend build picks up because this class is a dev-tools entry point.
*/
@JsModule(value = "./VaadinObservabilityDevTools.js", developmentOnly = true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this change the panel script only reached the browser after serviceInit had passed the license and configuration checks. Now the module is bundled and registers itself with Copilot on every dev-mode page load, and the handler is always registered through the service loader. So an unlicensed or unconfigured app still gets the Observability panel in Copilot, polling an empty registry and showing "instrumentation: inactive".

If we want to keep the old behaviour (no license, no panel), the panel could ask the handler whether the kit is active before adding itself. Or the handler could answer with nothing when there is no active registry, and the panel could stay hidden in that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@heruan Good catch, this now keeps the old "no license, no panel" behaviour. I combined your two suggestions:

  • ObservabilityDevToolsHandler stays silent until the kit has an active registry, i.e. it is licensed and serviceInit has run. It still claims its commands, so nothing gets queued for other handlers.
  • The panel script no longer calls addPanel in init. It adds itself to Copilot, once, when the first metrics or insights answer arrives. It keeps polling, so if the app restarts in the same tab with a valid license, the panel shows up.

An unlicensed or unconfigured app now gets no Observability entry in Copilot, and never sees instrumentation: inactive from an unbound kit. Tests: kitNotActive_answersNothingSoThePanelStaysHidden in ObservabilityDevToolsHandlerTest, and a case in VaadinObservabilityDevTools.test.js checking there is no panel before an answer and exactly one after.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better if the panel always showed up and even had the correct content but all numbers would be missing + there would be a text about needing a license for it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Artur- Agreed, done. The panel is back to always registering with Copilot, and the "wait for the first answer" gating is gone. What changed:

  • MetricsServiceInitListener records a failed license check in ObservabilityKit.isLicenseMissing(), a new public getter next to getActiveMeterRegistry().
  • The meter snapshot the handler sends now includes licensed.
  • When licensed is false, the panel shows a notice at the top: "Observability Kit needs a license. Without one, no metrics or insights are collected", with a link to the license terms. The Insights and Metrics sections keep their usual layout but read "Not collected without a license." That replaces the hints about the insights setting and about interacting with the app, which would be misleading here.

Tests: licenseMissing_stillAnswersAndSaysSo in ObservabilityDevToolsHandlerTest, an assertion in the license test that the flag is recorded, and a JS case checking the notice appears when unlicensed and not otherwise.

public class ObservabilityDevToolsHandler implements DevToolsMessageHandler {

static final String COMMAND_REFRESH = "observability-kit-refresh";
Expand Down Expand Up @@ -82,6 +87,9 @@ public boolean handleMessage(String command, JsonNode data,
private void sendSnapshot(DevToolsInterface devToolsInterface) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("timestamp", System.currentTimeMillis());
// The panel is shown whether or not the kit is licensed; this is what
// lets it say why its sections are empty.
payload.put("licensed", !ObservabilityKit.isLicenseMissing());
payload.put("meters", snapshot());
devToolsInterface.send(COMMAND_METRICS, payload);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// Copyright 2000-2026 Vaadin Ltd.
// Licensed under the Vaadin Commercial License and Service Terms.
//
// In-browser collector for observability-kit. Injected per UI by
// MetricsCollectorElement via Page.executeJs. The IIFE is idempotent so the
// re-attach path does not double-install hooks.
(function () {
// In-browser collector for observability-kit, bundled through the @JsModule
// on MetricsCollectorElement. Loading the module only defines the
// <vaadin-metrics-collector> element; nothing is collected until the server
// attaches one, which it does only when client metrics are enabled. The
// install is idempotent so the re-attach path does not double-install hooks.
var COLLECTOR_TAG = 'vaadin-metrics-collector';

function installCollector() {
if (window.__vaadinMicrometerInstalled) {
return;
}
window.__vaadinMicrometerInstalled = true;

var COLLECTOR_TAG = 'vaadin-metrics-collector';
var BUFFER_MAX = 200;
var FLUSH_INTERVAL_MS = 5000;
// How long a handed-over batch may go unanswered before a flush takes it
Expand Down Expand Up @@ -473,15 +476,17 @@
return text.length > DETAIL_MAX ? text.slice(0, DETAIL_MAX) : text;
}

// Whether the application asked for error messages to be collected. Set by
// the server ahead of this script. The message is gathered only when it is
// on, rather than gathered and discarded later: a browser error message can
// quote whatever the page was working with, and buffering one puts it in
// sessionStorage and then on the wire, neither of which a server-side
// retention rule can undo. The server applies the same rule again, for the
// page that was already open when the setting changed.
// Whether the application asked for error messages to be collected, which the
// server says with the `details` attribute on the collector element. The
// message is gathered only when it is on, rather than gathered and discarded
// later: a browser error message can quote whatever the page was working
// with, and buffering one puts it in sessionStorage and then on the wire,
// neither of which a server-side retention rule can undo. The server applies
// the same rule again, for the page that was already open when the setting
// changed.
function detailsEnabled() {
return window.__vaadinMicrometerDetails === true;
var el = document.querySelector(COLLECTOR_TAG);
return !!el && el.hasAttribute('details');
}


Expand Down Expand Up @@ -1269,4 +1274,15 @@
return buffer.length;
}
};
})();
}

if (!customElements.get(COLLECTOR_TAG)) {
customElements.define(
COLLECTOR_TAG,
class extends HTMLElement {
connectedCallback() {
installCollector();
}
}
);
}
Loading
Loading