From 33e588a995a2b2b39c6065043526802c75fb5bea Mon Sep 17 00:00:00 2001 From: nvazquez Date: Mon, 9 Feb 2026 10:08:59 -0300 Subject: [PATCH 1/2] Add a Prometheus metric to track host certificate expiry --- .../metrics/PrometheusExporterImpl.java | 38 ++++++ .../metrics/PrometheusExporterImplTest.java | 108 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterImplTest.java diff --git a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java index 32ec2f532111..c38597171aaf 100644 --- a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java +++ b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.metrics; import java.math.BigDecimal; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -26,6 +27,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.commons.lang3.StringUtils; @@ -133,6 +135,8 @@ public String toString() { private ResourceCountDao _resourceCountDao; @Inject private HostTagsDao _hostTagsDao; + @Inject + private CAManager caManager; public PrometheusExporterImpl() { super(); @@ -216,6 +220,16 @@ private void addHostMetrics(final List metricsList, final long dcId, final } metricsList.add(new ItemHostVM(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), vmDao.listByHostId(host.getId()).size())); + + // Add SSL certificate expiration metric + if (caManager != null && caManager.getActiveCertificatesMap() != null) { + X509Certificate cert = caManager.getActiveCertificatesMap().getOrDefault(host.getPrivateIpAddress(), null); + if (cert != null) { + long certExpiryEpoch = cert.getNotAfter().getTime() / 1000; // Convert to epoch seconds + metricsList.add(new ItemHostCertExpiry(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), certExpiryEpoch)); + } + } + final CapacityVO coreCapacity = capacityDao.findByHostIdType(host.getId(), Capacity.CAPACITY_TYPE_CPU_CORE); if (coreCapacity == null && !host.isInMaintenanceStates()){ @@ -1049,4 +1063,28 @@ public String toMetricsString() { return String.format("%s{zone=\"%s\",cpu=\"%d\",memory=\"%d\"} %d", name, zoneName, cpu, memory, total); } } + + class ItemHostCertExpiry extends Item { + String zoneName; + String zoneUuid; + String hostName; + String hostUuid; + String hostIp; + long expiryTimestamp; + + public ItemHostCertExpiry(final String zoneName, final String zoneUuid, final String hostName, final String hostUuid, final String hostIp, final long expiry) { + super("cloudstack_host_cert_expiry_timestamp"); + this.zoneName = zoneName; + this.zoneUuid = zoneUuid; + this.hostName = hostName; + this.hostUuid = hostUuid; + this.hostIp = hostIp; + this.expiryTimestamp = expiry; + } + + @Override + public String toMetricsString() { + return String.format("%s{zone=\"%s\",hostname=\"%s\",ip=\"%s\"} %d", name, zoneName, hostName, hostIp, expiryTimestamp); + } + } } diff --git a/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterImplTest.java b/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterImplTest.java new file mode 100644 index 000000000000..40490c46f56e --- /dev/null +++ b/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterImplTest.java @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.metrics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class PrometheusExporterImplTest { + + private static final String TEST_ZONE_NAME = "zone1"; + private static final String TEST_ZONE_UUID = "zone-uuid-1"; + private static final String TEST_HOST_NAME = "host1"; + private static final String TEST_HOST_UUID = "host-uuid-1"; + private static final String TEST_HOST_IP = "192.168.1.10"; + private static final long CERT_EXPIRY_TIME = 1735689600000L; // 2025-01-01 00:00:00 UTC + private static final long CERT_EXPIRY_EPOCH = CERT_EXPIRY_TIME / 1000; + + @Test + public void testItemHostCertExpiryFormat() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + String expected = String.format( + "cloudstack_host_cert_expiry_timestamp{zone=\"%s\",hostname=\"%s\",ip=\"%s\"} %d", + TEST_ZONE_NAME, + TEST_HOST_NAME, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + assertEquals("Certificate expiry metric format should match expected format", expected, metricsString); + } + + @Test + public void testItemHostCertExpiryContainsCorrectMetricName() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + assertTrue("Metric should contain correct metric name", + metricsString.contains("cloudstack_host_cert_expiry_timestamp")); + } + + @Test + public void testItemHostCertExpiryContainsAllLabels() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + assertTrue("Metric should contain zone label", metricsString.contains("zone=\"" + TEST_ZONE_NAME + "\"")); + assertTrue("Metric should contain hostname label", metricsString.contains("hostname=\"" + TEST_HOST_NAME + "\"")); + assertTrue("Metric should contain ip label", metricsString.contains("ip=\"" + TEST_HOST_IP + "\"")); + } + + @Test + public void testItemHostCertExpiryContainsTimestampValue() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + assertTrue("Metric should contain correct timestamp value", + metricsString.endsWith(" " + CERT_EXPIRY_EPOCH)); + } +} From 842e96a0c0ddbc0f06ac5d7e8d698b6a14df15dd Mon Sep 17 00:00:00 2001 From: nvazquez Date: Mon, 9 Feb 2026 23:59:45 -0300 Subject: [PATCH 2/2] Address review comments --- .../metrics/PrometheusExporterImpl.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java index c38597171aaf..b49f11c77745 100644 --- a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java +++ b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java @@ -221,14 +221,7 @@ private void addHostMetrics(final List metricsList, final long dcId, final metricsList.add(new ItemHostVM(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), vmDao.listByHostId(host.getId()).size())); - // Add SSL certificate expiration metric - if (caManager != null && caManager.getActiveCertificatesMap() != null) { - X509Certificate cert = caManager.getActiveCertificatesMap().getOrDefault(host.getPrivateIpAddress(), null); - if (cert != null) { - long certExpiryEpoch = cert.getNotAfter().getTime() / 1000; // Convert to epoch seconds - metricsList.add(new ItemHostCertExpiry(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), certExpiryEpoch)); - } - } + addSSLCertificateExpirationMetrics(metricsList, zoneName, zoneUuid, host); final CapacityVO coreCapacity = capacityDao.findByHostIdType(host.getId(), Capacity.CAPACITY_TYPE_CPU_CORE); @@ -267,6 +260,18 @@ private void addHostMetrics(final List metricsList, final long dcId, final addHostTagsMetrics(metricsList, dcId, zoneName, zoneUuid, totalHosts, upHosts, downHosts, total, up, down); } + private void addSSLCertificateExpirationMetrics(List metricsList, String zoneName, String zoneUuid, HostVO host) { + if (caManager == null || caManager.getActiveCertificatesMap() == null) { + return; + } + X509Certificate cert = caManager.getActiveCertificatesMap().getOrDefault(host.getPrivateIpAddress(), null); + if (cert == null) { + return; + } + long certExpiryEpoch = cert.getNotAfter().getTime() / 1000; // Convert to epoch seconds + metricsList.add(new ItemHostCertExpiry(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), certExpiryEpoch)); + } + private String markTagMaps(HostVO host, Map totalHosts, Map upHosts, Map downHosts) { List hostTagVOS = _hostTagsDao.getHostTags(host.getId()); List hostTags = new ArrayList<>();