diff --git a/observability-kit-micrometer/pom.xml b/observability-kit-micrometer/pom.xml index 68e09f6..5d56b70 100644 --- a/observability-kit-micrometer/pom.xml +++ b/observability-kit-micrometer/pom.xml @@ -16,11 +16,19 @@ Observability Kit :: Micrometer Framework-agnostic Micrometer instrumentation for Vaadin Flow. + + vaadin-observability-kit + + com.vaadin flow-server + + com.vaadin + license-checker + io.micrometer micrometer-core @@ -50,6 +58,46 @@ mockito-core test + + org.mockito + mockito-junit-jupiter + test + + + + + src/main/resources + false + + observability-kit.properties + + + + src/main/resources + true + + observability-kit.properties + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + ${cvdl.name} + + + + + + + diff --git a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java index 0a3696a..9a78ff9 100644 --- a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java +++ b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java @@ -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; @@ -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; @@ -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(); diff --git a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityLicense.java b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityLicense.java new file mode 100644 index 0000000..ba8b47d --- /dev/null +++ b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityLicense.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See 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. + *

+ * 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. + *

+ * 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); + } + } +} diff --git a/observability-kit-micrometer/src/main/resources/observability-kit.properties b/observability-kit-micrometer/src/main/resources/observability-kit.properties new file mode 100644 index 0000000..d2fa87a --- /dev/null +++ b/observability-kit-micrometer/src/main/resources/observability-kit.properties @@ -0,0 +1 @@ +observability-kit.version=${project.version} diff --git a/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerLicenseTest.java b/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerLicenseTest.java new file mode 100644 index 0000000..29e4b39 --- /dev/null +++ b/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerLicenseTest.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See 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")); + } +} diff --git a/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTest.java b/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTest.java index 98572f2..076429d 100644 --- a/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTest.java +++ b/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTest.java @@ -22,6 +22,7 @@ import com.vaadin.flow.server.communication.RpcInvocationListener; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -35,11 +36,22 @@ void tearDown() { ObservabilityKit.reset(); } + /** + * A production-mode service, so the license gate passes without a runtime + * license check and these tests can focus on binder registration. + */ + private static VaadinService licensedService() { + VaadinService service = mock(VaadinService.class, RETURNS_DEEP_STUBS); + when(service.getDeploymentConfiguration().isProductionMode()) + .thenReturn(true); + return service; + } + @Test void registersSessionBinderWhenSessionsEnabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -66,7 +78,7 @@ void doesNothingWhenNotInstalled() { void skipsSessionBinderWhenSessionsDisabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().sessions(false).build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -81,7 +93,7 @@ void skipsSessionBinderWhenSessionsDisabled() { void registersUiInitListenerWhenUisEnabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -95,7 +107,7 @@ void registersUiInitListenerWhenNavigationEnabledAndUisDisabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().uis(false).navigation(true) .build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -109,7 +121,7 @@ void skipsUiInitListenerWhenUisAndNavigationAndClientDisabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().uis(false).navigation(false) .client(false).build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -123,7 +135,7 @@ void registersUiInitListenerWhenOnlyClientEnabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().uis(false).navigation(false) .client(true).build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -136,7 +148,7 @@ void registersUiInitListenerWhenOnlyClientEnabled() { void registersRequestInterceptorWhenRequestsEnabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -151,7 +163,7 @@ void registersRequestInterceptorWhenOnlyErrorsEnabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().requests(false).errors(true) .build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -166,7 +178,7 @@ void skipsRequestInterceptorWhenRequestsAndErrorsDisabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().requests(false).errors(false) .build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -179,7 +191,7 @@ void skipsRequestInterceptorWhenRequestsAndErrorsDisabled() { void registersRpcInvocationListenerWhenRequestsEnabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -193,7 +205,7 @@ void registersRpcInvocationListenerWhenRequestsEnabled() { void skipsRpcInvocationListenerWhenRequestsDisabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().requests(false).build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); @@ -207,7 +219,7 @@ void skipsRpcInvocationListenerWhenOnlyErrorsEnabled() { ObservabilityKit.install(new SimpleMeterRegistry(), ObservabilitySettings.builder().requests(false).errors(true) .build()); - VaadinService service = mock(VaadinService.class); + VaadinService service = licensedService(); ServiceInitEvent event = mock(ServiceInitEvent.class); when(event.getSource()).thenReturn(service); diff --git a/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTracesTest.java b/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTracesTest.java index 6eab547..17e106d 100644 --- a/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTracesTest.java +++ b/observability-kit-micrometer/src/test/java/com/vaadin/observability/micrometer/MetricsServiceInitListenerTracesTest.java @@ -37,7 +37,10 @@ void executorIsWrappedWhenTracesEnabled() { new SimpleMeterRegistry(), obs, ObservabilitySettings.builder().build()); - VaadinService service = Mockito.mock(VaadinService.class); + VaadinService service = Mockito.mock(VaadinService.class, + Mockito.RETURNS_DEEP_STUBS); + Mockito.when(service.getDeploymentConfiguration().isProductionMode()) + .thenReturn(true); ServiceInitEvent event = new ServiceInitEvent(service); Executor original = Runnable::run; event.setExecutor(original); @@ -57,7 +60,10 @@ void executorIsNotWrappedWhenTracesDisabled() { new SimpleMeterRegistry(), obs, ObservabilitySettings.builder().traces(false).build()); - VaadinService service = Mockito.mock(VaadinService.class); + VaadinService service = Mockito.mock(VaadinService.class, + Mockito.RETURNS_DEEP_STUBS); + Mockito.when(service.getDeploymentConfiguration().isProductionMode()) + .thenReturn(true); ServiceInitEvent event = new ServiceInitEvent(service); Executor original = Runnable::run; event.setExecutor(original); @@ -74,7 +80,10 @@ void executorIsNotWrappedWhenObservationRegistryAbsent() { new SimpleMeterRegistry(), null, ObservabilitySettings.builder().build()); - VaadinService service = Mockito.mock(VaadinService.class); + VaadinService service = Mockito.mock(VaadinService.class, + Mockito.RETURNS_DEEP_STUBS); + Mockito.when(service.getDeploymentConfiguration().isProductionMode()) + .thenReturn(true); ServiceInitEvent event = new ServiceInitEvent(service); Executor original = Runnable::run; event.setExecutor(original); diff --git a/pom.xml b/pom.xml index cb47b85..767975c 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,7 @@ UTF-8 3.13.0 + 3.4.1 3.2.1 3.4.1 3.2.5 @@ -149,6 +150,11 @@ maven-compiler-plugin ${maven.compiler.version} + + org.apache.maven.plugins + maven-jar-plugin + ${maven.jar.version} + org.apache.maven.plugins maven-source-plugin