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
48 changes: 48 additions & 0 deletions observability-kit-micrometer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@
<name>Observability Kit :: Micrometer</name>
<description>Framework-agnostic Micrometer instrumentation for Vaadin Flow.</description>

<properties>
<cvdl.name>vaadin-observability-kit</cvdl.name>
</properties>

<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-server</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>license-checker</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
Expand Down Expand Up @@ -50,6 +58,46 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>observability-kit.properties</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>observability-kit.properties</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<CvdlName>${cvdl.name}</CvdlName>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinRequest;
Expand Down Expand Up @@ -44,6 +46,9 @@
*/
public class MetricsServiceInitListener implements VaadinServiceInitListener {

private static final Logger LOGGER = LoggerFactory
.getLogger(MetricsServiceInitListener.class);

private final MeterRegistry meterRegistry;
private final ObservationRegistry observationRegistry;
private final ObservabilitySettings settings;
Expand Down Expand Up @@ -152,6 +157,16 @@ public void serviceInit(ServiceInitEvent event) {
if (r == null || s == null) {
return;
}
boolean productionMode = event.getSource().getDeploymentConfiguration()
.isProductionMode();
if (!ObservabilityLicense.isLicensed(productionMode)) {
LOGGER.warn(
"No valid {} license found. Observability Kit instrumentation "
+ "will not be registered and no telemetry will be collected. "
+ "See https://vaadin.com/commercial-license-and-service-terms",
ObservabilityLicense.PRODUCT_NAME);
return;
}
ObservationRegistry or = observationRegistry != null
? observationRegistry
: ObservabilityKit.getObservationRegistry();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* 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.util.Properties;

import com.vaadin.pro.licensechecker.BuildType;
import com.vaadin.pro.licensechecker.Capabilities;
import com.vaadin.pro.licensechecker.Capability;
import com.vaadin.pro.licensechecker.LicenseChecker;
import com.vaadin.pro.licensechecker.LicenseException;
import com.vaadin.pro.licensechecker.MissingLicenseKeyException;

/**
* Validates the Observability Kit commercial license.
* <p>
* Unlike a license-checking {@code VaadinServiceInitListener}, this does not
* fail application startup. It is consulted by
* {@link MetricsServiceInitListener} as a gate: when the kit is not licensed,
* the instrumentation (meter and observation registries, binders and trackers)
* is simply not registered, and the application keeps running without
* telemetry.
*/
final class ObservabilityLicense {

static final String PROPERTIES_RESOURCE = "observability-kit.properties";

static final String VERSION_PROPERTY = "observability-kit.version";

static final String PRODUCT_NAME = "vaadin-observability-kit";

static final String PRODUCT_VERSION;

static {
final var properties = loadAllProperties(PROPERTIES_RESOURCE);
PRODUCT_VERSION = properties.getProperty(VERSION_PROPERTY);
}

private ObservabilityLicense() {
}

/**
* Checks whether Observability Kit is licensed for use.
* <p>
* Production builds are validated at build time, so this returns
* {@code true} without a runtime check. Development builds are validated
* against the local license key; a missing, invalid or outdated key makes
* this return {@code false} instead of throwing, so the caller can skip
* registering instrumentation rather than failing the whole application.
*
* @param productionMode
* whether the deployment runs in production mode
* @return {@code true} if the kit may register its instrumentation
*/
static boolean isLicensed(boolean productionMode) {
if (productionMode) {
return true;
}
try {
// A null BuildType allows trial licensing builds. The no-key
// handler throws instead of opening a browser so the check stays
// non-blocking, and the zero timeout avoids waiting for a download.
BuildType buildType = null;
LicenseChecker.checkLicense(PRODUCT_NAME, PRODUCT_VERSION,
buildType, url -> {
throw new MissingLicenseKeyException(
"No license key present");
}, 0, Capabilities.of(Capability.PRE_TRIAL));
return true;
} catch (LicenseException e) {
return false;
}
}

static Properties loadAllProperties(String propertiesResource) {
final var cl = ObservabilityLicense.class.getClassLoader();
try (final var stream = cl.getResourceAsStream(propertiesResource)) {
final var properties = new Properties();
properties.load(stream);
return properties;
} catch (NullPointerException | IOException e) {
throw new ExceptionInInitializerError(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
observability-kit.version=${project.version}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* 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.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.UIInitListener;
import com.vaadin.flow.server.VaadinRequestInterceptor;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.pro.licensechecker.LicenseChecker;
import com.vaadin.pro.licensechecker.LicenseException;

import static com.vaadin.observability.micrometer.ObservabilityLicense.loadAllProperties;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class MetricsServiceInitListenerLicenseTest {

private VaadinService service;
private ServiceInitEvent event;

@BeforeEach
void setUp() {
service = mock(VaadinService.class, RETURNS_DEEP_STUBS);
event = mock(ServiceInitEvent.class);
when(event.getSource()).thenReturn(service);
ObservabilityKit.install(new SimpleMeterRegistry(),
ObservabilitySettings.builder().build());
}

@AfterEach
void tearDown() {
ObservabilityKit.reset();
}

@Test
void developmentMode_withoutValidLicense_skipsInstrumentation() {
when(service.getDeploymentConfiguration().isProductionMode())
.thenReturn(false);

try (var licenseChecker = mockStatic(LicenseChecker.class)) {
licenseChecker
.when(() -> LicenseChecker.checkLicense(any(), any(), any(),
any(), anyInt(), any()))
.thenThrow(new LicenseException("no valid license"));

new MetricsServiceInitListener().serviceInit(event);
}

verify(service, never()).addSessionInitListener(any());
verify(service, never()).addUIInitListener(any());
verify(event, never()).addVaadinRequestInterceptor(any());
}

@Test
void developmentMode_withValidLicense_registersInstrumentation() {
when(service.getDeploymentConfiguration().isProductionMode())
.thenReturn(false);

// An unstubbed static checkLicense is a no-op, i.e. a valid license
try (var licenseChecker = mockStatic(LicenseChecker.class)) {
new MetricsServiceInitListener().serviceInit(event);
}

verify(service).addUIInitListener(any(UIInitListener.class));
verify(event).addVaadinRequestInterceptor(
any(VaadinRequestInterceptor.class));
}

@Test
void productionMode_registersWithoutCheckingLicense() {
when(service.getDeploymentConfiguration().isProductionMode())
.thenReturn(true);

try (var licenseChecker = mockStatic(LicenseChecker.class)) {
new MetricsServiceInitListener().serviceInit(event);

// Production builds are validated at build time, not at runtime
licenseChecker.verifyNoInteractions();
}

verify(service).addUIInitListener(any(UIInitListener.class));
}

@Test
void loadAllProperties_throwsError_whenResourceMissing() {
assertThrows(ExceptionInInitializerError.class,
() -> loadAllProperties("non-existent.properties"));
}
}
Loading
Loading