diff --git a/CHANGELOG.md b/CHANGELOG.md index d65b3a60..abf3a09c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ Version template: # Alfred Telemetry Changelog ## [0.5.1] - UNRELEASED +### Added +* Alfresco license metrics [#82] + +[#82]: https://github.com/xenit-eu/alfred-telemetry/pull/82 + ## [0.5.0] - 2021-03-22 ### Added diff --git a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/AlfrescoStatusMetrics.java b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/AlfrescoStatusMetrics.java index fe874e24..3dbc7b33 100644 --- a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/AlfrescoStatusMetrics.java +++ b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/AlfrescoStatusMetrics.java @@ -6,14 +6,11 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.cmr.admin.RepoAdminService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; public class AlfrescoStatusMetrics implements MeterBinder { - private static final Logger LOGGER = LoggerFactory.getLogger(AlfrescoStatusMetrics.class); private static final String STATUS_PREFIX = "alfresco.status"; private RepoAdminService repoAdminService; @@ -27,7 +24,6 @@ public AlfrescoStatusMetrics(RepoAdminService repoAdminService, @Override public void bindTo(@Nonnull MeterRegistry meterRegistry) { - LOGGER.info("Registering Alfresco Status metrics"); Gauge.builder(STATUS_PREFIX + ".readonly", repoAdminService, this::getReadOnly) .description("Metric about Alfresco being in read-only mode") .register(meterRegistry); @@ -42,10 +38,6 @@ private double getReadOnly(RepoAdminService repoAdminService) { isReadOnly[0] = repoAdminService.getUsage().isReadOnly()), true); - if (isReadOnly[0]) { - return 1d; - } else { - return 0d; - } + return (isReadOnly[0] ? 1d : 0d); } } diff --git a/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/LicenseMetrics.java b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/LicenseMetrics.java new file mode 100644 index 00000000..24d4f626 --- /dev/null +++ b/alfred-telemetry-platform/src/main/java/eu/xenit/alfred/telemetry/binder/LicenseMetrics.java @@ -0,0 +1,129 @@ +package eu.xenit.alfred.telemetry.binder; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.license.LicenseDescriptor; +import org.alfresco.service.license.LicenseService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import javax.annotation.Nonnull; + +public class LicenseMetrics implements MeterBinder, ApplicationContextAware { + + private static final Logger logger = LoggerFactory.getLogger(LicenseMetrics.class); + + private static final String METRIC_NAME_LICENSE = "license"; + + private ApplicationContext ctx; + private DescriptorService descriptorService; + + public LicenseMetrics(DescriptorService descriptorService) { + this.descriptorService = descriptorService; + } + + @Override + public void bindTo(@Nonnull MeterRegistry registry) { + // do not do anything for Community + Descriptor serverDescriptor = descriptorService.getServerDescriptor(); + if (!"Enterprise".equals(serverDescriptor.getEdition())) { + logger.info("Edition={}, license metrics are not available", serverDescriptor.getEdition()); + return; + } + + Gauge.builder(METRIC_NAME_LICENSE + ".valid", ctx, LicenseMetrics::getValid) + .description("Whether the license is still valid") + .register(registry); + + Gauge.builder(METRIC_NAME_LICENSE + ".days", descriptorService, LicenseMetrics::getRemainingDays) + .description("Remaining days") + .tags("status", "remaining") + .register(registry); + Gauge.builder(METRIC_NAME_LICENSE + ".docs", descriptorService, LicenseMetrics::getMaxDocs) + .description("Max docs") + .tags("status", "max") + .register(registry); + Gauge.builder(METRIC_NAME_LICENSE + ".users", descriptorService, LicenseMetrics::getMaxUsers) + .description("Max users") + .tags("status", "max") + .register(registry); + Gauge.builder(METRIC_NAME_LICENSE + ".cluster.enabled", descriptorService, LicenseMetrics::isClusterEnabled) + .description("Clustering enabled") + .register(registry); + Gauge.builder(METRIC_NAME_LICENSE + ".encryption.enabled", descriptorService, + LicenseMetrics::isCryptodocEnabled) + .description("Encription enabled") + .register(registry); + Gauge.builder(METRIC_NAME_LICENSE + ".heartbeat.enabled", descriptorService, LicenseMetrics::isHeartbeatEnabled) + .description("Heartbeat enabled") + .register(registry); + } + + + private static double getValid(final ApplicationContext ctx) { + LicenseService licenseService = ctx.getBeansOfType(LicenseService.class, false, false).get("licenseService"); + if (licenseService != null) { + return (licenseService.isLicenseValid() ? 1 : 0); + } + return -1; + } + + private static int getRemainingDays(final DescriptorService descriptorService) { + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + if (licenseDescriptor != null && licenseDescriptor.getRemainingDays() != null) { + return licenseDescriptor.getRemainingDays(); + } + return -1; + } + + private static long getMaxDocs(final DescriptorService descriptorService) { + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + if (licenseDescriptor != null && licenseDescriptor.getMaxDocs() != null) { + return licenseDescriptor.getMaxDocs(); + } + return -1L; + } + + private static long getMaxUsers(final DescriptorService descriptorService) { + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + if (licenseDescriptor != null && licenseDescriptor.getMaxUsers() != null) { + return licenseDescriptor.getMaxUsers(); + } + return -1L; + } + + private static double isClusterEnabled(final DescriptorService descriptorService) { + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + if (licenseDescriptor != null) { + return (licenseDescriptor.isClusterEnabled() ? 1 : 0); + } + return -1; + } + + private static double isCryptodocEnabled(final DescriptorService descriptorService) { + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + if (licenseDescriptor != null) { + return (licenseDescriptor.isCryptodocEnabled() ? 1 : 0); + } + return -1; + } + + private static double isHeartbeatEnabled(final DescriptorService descriptorService) { + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + if (licenseDescriptor != null) { + return (licenseDescriptor.isHeartBeatDisabled() ? 0 : 1); + } + return -1; + } + + @Override + public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException { + this.ctx = applicationContext; + } +} diff --git a/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/alfresco-global.properties b/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/alfresco-global.properties index 487e9b78..36062d55 100644 --- a/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/alfresco-global.properties +++ b/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/alfresco-global.properties @@ -38,6 +38,9 @@ alfred.telemetry.binder.cache.enabled=false # Alfresco status alfred.telemetry.binder.alfresco-status.enabled=true +# Alfresco license - will output nothing for community +alfred.telemetry.binder.license.enabled=true + # Care4Alf support - default configuration ## 1. Care4Alf metrics global switch - disabled by default alfred.telemetry.binder.care4alf.enabled=false diff --git a/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/context/binder-context.xml b/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/context/binder-context.xml index ad9c6038..5fd65952 100644 --- a/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/context/binder-context.xml +++ b/alfred-telemetry-platform/src/main/resources/alfresco/module/alfred-telemetry-platform/context/binder-context.xml @@ -3,7 +3,7 @@ + class="eu.xenit.alfred.telemetry.binder.MeterBinderRegistrar"> @@ -14,7 +14,7 @@ + class="eu.xenit.alfred.telemetry.binder.care4alf.Care4AlfMeterBinderRegistrar"> @@ -39,6 +39,10 @@ + + + + diff --git a/alfred-telemetry-platform/src/test/java/eu/xenit/alfred/telemetry/binder/LicenseMetricsTest.java b/alfred-telemetry-platform/src/test/java/eu/xenit/alfred/telemetry/binder/LicenseMetricsTest.java new file mode 100644 index 00000000..e3d1aac5 --- /dev/null +++ b/alfred-telemetry-platform/src/test/java/eu/xenit/alfred/telemetry/binder/LicenseMetricsTest.java @@ -0,0 +1,118 @@ +package eu.xenit.alfred.telemetry.binder; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.license.LicenseDescriptor; +import org.alfresco.service.license.LicenseService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; + +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LicenseMetricsTest { + + private DescriptorService descriptorService; + private LicenseMetrics licenseMetrics; + private MeterRegistry meterRegistry; + private ApplicationContext applicationContext; + private LicenseService licenseService; + private Descriptor serverDescriptor; + private LicenseDescriptor licenseDescriptor; + + @BeforeEach + void setup() { + licenseService = mock(LicenseService.class); + applicationContext = mock(ApplicationContext.class); + descriptorService = mock(DescriptorService.class); + serverDescriptor = mock(Descriptor.class); + licenseDescriptor = mock(LicenseDescriptor.class); + + meterRegistry = new SimpleMeterRegistry(); + licenseMetrics = new LicenseMetrics(descriptorService); + licenseMetrics.setApplicationContext(applicationContext); + + when(descriptorService.getServerDescriptor()).thenReturn(serverDescriptor); + when(descriptorService.getLicenseDescriptor()).thenReturn(licenseDescriptor); + when(applicationContext.getBeansOfType(LicenseService.class, false, false)) + .thenReturn(Collections.singletonMap("licenseService", licenseService)); + when(serverDescriptor.getEdition()).thenReturn("Enterprise"); + } + + @Test + public void testCommunity() { + when(serverDescriptor.getEdition()).thenReturn("Community"); + licenseMetrics.bindTo(meterRegistry); + assertFalse(meterRegistry.getMeters().contains("license.valid")); + } + + @Test + public void testNoLicenseService() { + when(applicationContext.getBeansOfType(LicenseService.class, false, false)) + .thenReturn(Collections.singletonMap("licenseService", null)); + licenseMetrics.bindTo(meterRegistry); + assertThat(meterRegistry.get("license.valid").gauge().value(), is(-1.0)); + } + + @Test + public void testNoLicenseDescriptor() { + when(descriptorService.getLicenseDescriptor()).thenReturn(null); + licenseMetrics.bindTo(meterRegistry); + assertThat(meterRegistry.get("license.docs").tag("status", "max").gauge().value(), is(-1.0)); + } + + @Test + public void testLicenseMetrics() { + licenseMetrics.bindTo(meterRegistry); + + when(licenseService.isLicenseValid()).thenReturn(true); + assertThat(meterRegistry.get("license.valid").gauge().value(), is(1.0)); + + when(licenseService.isLicenseValid()).thenReturn(false); + assertThat(meterRegistry.get("license.valid").gauge().value(), is(0.0)); + + when(licenseDescriptor.getMaxDocs()).thenReturn(100L); + assertThat(meterRegistry.get("license.docs").tag("status", "max").gauge().value(), is(100.0)); + + when(licenseDescriptor.getMaxDocs()).thenReturn(null); + assertThat(meterRegistry.get("license.docs").tag("status", "max").gauge().value(), is(-1.0)); + + when(licenseDescriptor.getMaxUsers()).thenReturn(100L); + assertThat(meterRegistry.get("license.users").tag("status", "max").gauge().value(), is(100.0)); + + when(licenseDescriptor.getMaxUsers()).thenReturn(null); + assertThat(meterRegistry.get("license.users").tag("status", "max").gauge().value(), is(-1.0)); + + when(licenseDescriptor.getRemainingDays()).thenReturn(100); + assertThat(meterRegistry.get("license.days").tag("status", "remaining").gauge().value(), is(100.0)); + + when(licenseDescriptor.getRemainingDays()).thenReturn(null); + assertThat(meterRegistry.get("license.days").tag("status", "remaining").gauge().value(), is(-1.0)); + + when(licenseDescriptor.isClusterEnabled()).thenReturn(false); + assertThat(meterRegistry.get("license.cluster.enabled").gauge().value(), is(0.0)); + + when(licenseDescriptor.isClusterEnabled()).thenReturn(true); + assertThat(meterRegistry.get("license.cluster.enabled").gauge().value(), is(1.0)); + + when(licenseDescriptor.isCryptodocEnabled()).thenReturn(false); + assertThat(meterRegistry.get("license.encryption.enabled").gauge().value(), is(0.0)); + + when(licenseDescriptor.isCryptodocEnabled()).thenReturn(true); + assertThat(meterRegistry.get("license.encryption.enabled").gauge().value(), is(1.0)); + + when(licenseDescriptor.isHeartBeatDisabled()).thenReturn(false); + assertThat(meterRegistry.get("license.heartbeat.enabled").gauge().value(), is(1.0)); + + when(licenseDescriptor.isHeartBeatDisabled()).thenReturn(true); + assertThat(meterRegistry.get("license.heartbeat.enabled").gauge().value(), is(0.0)); + } +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index aa76320f..4491cb53 100644 --- a/docs/README.md +++ b/docs/README.md @@ -254,6 +254,24 @@ Metrics provided | :------------------------------------ | :----------------------- | | alfresco.status.readonly | | +## License metrics +The license metrics bindings will provide metrics about Alfresco license. Enterprise-only feature. + +**Control Property**: `alfred.telemetry.binder.license.enabled` + +Metrics provided + +| Name | Available tags | +| :------------------------------------ | :----------------------- | +| license.valid | | +| license.users | max | +| license.docs | max | +| license.days | remaining | +| license.cluster.enabled | | +| license.encryption.enabled | | +| license.heartbeat.enabled | | + + ## Alfresco Node metrics diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 435071a6..aceec061 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -36,6 +36,11 @@ subprojects { apply plugin: 'java' def alfrescoVersion = project.name[-2..-1] + def alfrescoEdition = "Community" + if(project.hasProperty("enterprise")) { + alfrescoEdition = "Enterprise" + } + apply from: "${project.projectDir}/overload.gradle" description = "Alfresco ${alfrescoVersion} with Alfred Micrometer" @@ -51,6 +56,8 @@ subprojects { doFirst { dockerCompose.solr.exposeAsSystemProperties(integrationTest) systemProperty 'solrFlavor', "${solrFlavor}" + systemProperty 'alfrescoEdition', alfrescoEdition + } } diff --git a/integration-tests/src/test/java/eu/xenit/alfred/telemetry/integrationtesting/MetricsEndpointSmokeTest.java b/integration-tests/src/test/java/eu/xenit/alfred/telemetry/integrationtesting/MetricsEndpointSmokeTest.java index 11072da1..8ba0d098 100644 --- a/integration-tests/src/test/java/eu/xenit/alfred/telemetry/integrationtesting/MetricsEndpointSmokeTest.java +++ b/integration-tests/src/test/java/eu/xenit/alfred/telemetry/integrationtesting/MetricsEndpointSmokeTest.java @@ -1,15 +1,17 @@ package eu.xenit.alfred.telemetry.integrationtesting; -import static io.restassured.RestAssured.given; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasItem; - -import java.util.List; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import java.util.List; +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; + class MetricsEndpointSmokeTest extends RestAssuredTest { static Stream expectedMeters() { @@ -45,6 +47,18 @@ static Stream expectedMeters() { ); } + static Stream expectedMetersEnterprise() { + return Stream.of( + "license.valid", + "license.users", + "license.docs", + "license.days", + "license.cluster.enabled", + "license.encryption.enabled", + "license.heartbeat.enabled" + ); + } + @Test void metersListedInMetricsEndpoint() { final List availableMeters = given() @@ -65,6 +79,27 @@ availableMeters, hasItem(expected)) ); } + @Test + @EnabledIfSystemProperty(named="alfrescoEdition",matches = "Enterprise") + void metersEnterpriseListedInMetricsEndpoint() { + final List availableMeters = given() + .log().ifValidationFails() + .when() + .get("s/alfred/telemetry/metrics") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract() + .jsonPath() + .getList("names"); + + expectedMeters().forEach(expected -> + assertThat( + "The metrics endpoint should contain meter '" + expected + "'", + availableMeters, hasItem(expected)) + ); + } + @ParameterizedTest @MethodSource("expectedMeters") void meterOverview(String expectedMeter) { @@ -78,4 +113,18 @@ void meterOverview(String expectedMeter) { .statusCode(200); } + @ParameterizedTest + @MethodSource("expectedMetersEnterprise") + @EnabledIfSystemProperty(named="alfrescoEdition",matches = "Enterprise") + void meterEnterpriseOverview(String expectedMeter) { + given() + .log().ifValidationFails() + .when() + .pathParam("meterName", expectedMeter) + .get("s/alfred/telemetry/metrics/{meterName}") + .then() + .log().ifValidationFails() + .statusCode(200); + } + }