This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 1e84bfd33251 CAMEL-23848: Camel-PQC - add optional Micrometer
metrics/observability (#24550)
1e84bfd33251 is described below
commit 1e84bfd3325143e31e2be1ad7985d61379296b5b
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Jul 9 11:59:43 2026 +0200
CAMEL-23848: Camel-PQC - add optional Micrometer metrics/observability
(#24550)
CAMEL-23848: Camel-PQC add optional Micrometer metrics
Add optional Micrometer metrics/observability for the PQC producer. When
Micrometer is on the classpath and a MeterRegistry is available in the Camel
registry, the producer records:
- camel.pqc.operations (counter) tagged by operation, algorithm and outcome
- camel.pqc.stateful.key.remaining (gauge) tagged by algorithm, for stateful
signature keys (XMSS/XMSSMT/LMS/HSS)
Micrometer is an optional dependency: all io.micrometer references are
isolated
in PQCMicrometerMetrics, which is only loaded once a class-resolver check
confirms Micrometer is present, so there is no hard dependency and no
NoClassDefFoundError when it is absent.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
components/camel-pqc/pom.xml | 7 +
.../camel-pqc/src/main/docs/pqc-component.adoc | 24 +++
.../apache/camel/component/pqc/PQCProducer.java | 185 +++++++++++++--------
.../camel/component/pqc/metrics/PQCMetrics.java | 63 +++++++
.../pqc/metrics/PQCMicrometerMetrics.java | 88 ++++++++++
.../apache/camel/component/pqc/PQCMetricsTest.java | 119 +++++++++++++
6 files changed, 420 insertions(+), 66 deletions(-)
diff --git a/components/camel-pqc/pom.xml b/components/camel-pqc/pom.xml
index 2f17156c32a8..4003805dbd65 100644
--- a/components/camel-pqc/pom.xml
+++ b/components/camel-pqc/pom.xml
@@ -58,6 +58,13 @@
<version>${jackson2-version}</version>
</dependency>
+ <!-- Micrometer for optional PQC metrics/observability -->
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-core</artifactId>
+ <optional>true</optional>
+ </dependency>
+
<!-- Spring Vault for HashicorpVaultKeyLifecycleManager (optional) -->
<dependency>
<groupId>org.springframework.vault</groupId>
diff --git a/components/camel-pqc/src/main/docs/pqc-component.adoc
b/components/camel-pqc/src/main/docs/pqc-component.adoc
index d032860e2696..f5c0edb443eb 100644
--- a/components/camel-pqc/src/main/docs/pqc-component.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-component.adoc
@@ -101,6 +101,30 @@ See xref:others:pqc-hybrid.adoc[PQC Hybrid Cryptography]
for configuration detai
See xref:others:pqc-key-lifecycle.adoc[PQC Key Lifecycle Management] for full
details.
+== Metrics
+
+When https://micrometer.io[Micrometer] is on the classpath and a
`MeterRegistry` bean is available in the Camel
+registry, the PQC producer records metrics automatically. Micrometer is an
*optional* dependency: without it (or without
+a `MeterRegistry`) no metrics are emitted and there is no runtime overhead.
+
+[cols="2,1,4",options="header"]
+|===
+| Meter | Type | Description
+
+| `camel.pqc.operations`
+| Counter
+| Number of PQC operations performed, tagged by `operation` (sign, verify,
generateSecretKeyEncapsulation,
+ extractSecretKeyFromEncapsulation, ...), `algorithm`, and `outcome`
(`success` / `failure`).
+
+| `camel.pqc.stateful.key.remaining`
+| Gauge
+| Remaining signatures for a stateful signature key (XMSS, XMSSMT, LMS, HSS),
tagged by `algorithm`. Registered only for
+ producers configured with a stateful signature algorithm.
+|===
+
+To enable the metrics, add `camel-micrometer` (or Micrometer directly) to your
application and bind a `MeterRegistry` in
+the registry - for example a `SimpleMeterRegistry`, or the registry provided
by Spring Boot or Quarkus.
+
== Sub-Pages
The PQC component documentation is split into focused sub-pages:
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
index 646a502a85fb..aae685a542da 100644
---
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
@@ -28,6 +28,7 @@ import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
+import org.apache.camel.CamelContext;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
@@ -36,6 +37,8 @@ import org.apache.camel.component.pqc.crypto.hybrid.HybridKEM;
import org.apache.camel.component.pqc.crypto.hybrid.HybridSignature;
import org.apache.camel.component.pqc.lifecycle.KeyLifecycleManager;
import org.apache.camel.component.pqc.lifecycle.KeyMetadata;
+import org.apache.camel.component.pqc.metrics.PQCMetrics;
+import org.apache.camel.component.pqc.metrics.PQCMicrometerMetrics;
import org.apache.camel.component.pqc.stateful.StatefulKeyState;
import org.apache.camel.health.HealthCheck;
import org.apache.camel.health.HealthCheckHelper;
@@ -117,6 +120,9 @@ public class PQCProducer extends DefaultProducer {
private HealthCheck producerHealthCheck;
private WritableHealthCheckRepository healthCheckRepository;
+ // Metrics (optional; a no-op unless Micrometer and a MeterRegistry are
available)
+ private PQCMetrics metrics = PQCMetrics.NOOP;
+
public PQCProducer(Endpoint endpoint) {
super(endpoint);
}
@@ -133,72 +139,78 @@ public class PQCProducer extends DefaultProducer {
public void process(Exchange exchange) throws Exception {
PQCOperations operation = determineOperation(exchange);
enforceKeyStatus(exchange, operation);
- switch (operation) {
- case sign:
- signature(exchange);
- break;
- case verify:
- verification(exchange);
- break;
- case hybridSign:
- hybridSignature(exchange);
- break;
- case hybridVerify:
- hybridVerification(exchange);
- break;
- case generateSecretKeyEncapsulation:
- generateEncapsulation(exchange);
- break;
- case extractSecretKeyEncapsulation:
- extractEncapsulation(exchange);
- break;
- case extractSecretKeyFromEncapsulation:
- extractSecretKeyFromEncapsulation(exchange);
- break;
- case hybridGenerateSecretKeyEncapsulation:
- hybridGenerateEncapsulation(exchange);
- break;
- case hybridExtractSecretKeyEncapsulation:
- hybridExtractEncapsulation(exchange);
- break;
- case hybridExtractSecretKeyFromEncapsulation:
- hybridExtractSecretKeyFromEncapsulation(exchange);
- break;
- case generateKeyPair:
- lifecycleGenerateKeyPair(exchange);
- break;
- case exportKey:
- lifecycleExportKey(exchange);
- break;
- case importKey:
- lifecycleImportKey(exchange);
- break;
- case rotateKey:
- lifecycleRotateKey(exchange);
- break;
- case getKeyMetadata:
- lifecycleGetKeyMetadata(exchange);
- break;
- case listKeys:
- lifecycleListKeys(exchange);
- break;
- case expireKey:
- lifecycleExpireKey(exchange);
- break;
- case revokeKey:
- lifecycleRevokeKey(exchange);
- break;
- case getRemainingSignatures:
- statefulGetRemainingSignatures(exchange);
- break;
- case getKeyState:
- statefulGetKeyState(exchange);
- break;
- case deleteKeyState:
- statefulDeleteKeyState(exchange);
- break;
- default:
- throw new IllegalArgumentException("Unsupported operation");
+ boolean success = false;
+ try {
+ switch (operation) {
+ case sign:
+ signature(exchange);
+ break;
+ case verify:
+ verification(exchange);
+ break;
+ case hybridSign:
+ hybridSignature(exchange);
+ break;
+ case hybridVerify:
+ hybridVerification(exchange);
+ break;
+ case generateSecretKeyEncapsulation:
+ generateEncapsulation(exchange);
+ break;
+ case extractSecretKeyEncapsulation:
+ extractEncapsulation(exchange);
+ break;
+ case extractSecretKeyFromEncapsulation:
+ extractSecretKeyFromEncapsulation(exchange);
+ break;
+ case hybridGenerateSecretKeyEncapsulation:
+ hybridGenerateEncapsulation(exchange);
+ break;
+ case hybridExtractSecretKeyEncapsulation:
+ hybridExtractEncapsulation(exchange);
+ break;
+ case hybridExtractSecretKeyFromEncapsulation:
+ hybridExtractSecretKeyFromEncapsulation(exchange);
+ break;
+ case generateKeyPair:
+ lifecycleGenerateKeyPair(exchange);
+ break;
+ case exportKey:
+ lifecycleExportKey(exchange);
+ break;
+ case importKey:
+ lifecycleImportKey(exchange);
+ break;
+ case rotateKey:
+ lifecycleRotateKey(exchange);
+ break;
+ case getKeyMetadata:
+ lifecycleGetKeyMetadata(exchange);
+ break;
+ case listKeys:
+ lifecycleListKeys(exchange);
+ break;
+ case expireKey:
+ lifecycleExpireKey(exchange);
+ break;
+ case revokeKey:
+ lifecycleRevokeKey(exchange);
+ break;
+ case getRemainingSignatures:
+ statefulGetRemainingSignatures(exchange);
+ break;
+ case getKeyState:
+ statefulGetKeyState(exchange);
+ break;
+ case deleteKeyState:
+ statefulDeleteKeyState(exchange);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported
operation");
+ }
+ success = true;
+ } finally {
+ metrics.recordOperation(operation.name(), currentAlgorithm(),
success);
}
}
@@ -214,6 +226,35 @@ public class PQCProducer extends DefaultProducer {
return getEndpoint().getConfiguration();
}
+ private String currentAlgorithm() {
+ String alg = getConfiguration().getSignatureAlgorithm();
+ if (ObjectHelper.isEmpty(alg)) {
+ alg = getConfiguration().getKeyEncapsulationAlgorithm();
+ }
+ return ObjectHelper.isNotEmpty(alg) ? alg : "unknown";
+ }
+
+ private PQCMetrics createMetrics() {
+ CamelContext camelContext = getEndpoint().getCamelContext();
+ // Only touch Micrometer if it is actually on the classpath, keeping
it an optional dependency
+ if
(camelContext.getClassResolver().resolveClass("io.micrometer.core.instrument.MeterRegistry")
== null) {
+ return PQCMetrics.NOOP;
+ }
+ try {
+ return PQCMicrometerMetrics.create(camelContext);
+ } catch (Throwable t) {
+ LOG.debug("Micrometer metrics are not available for the PQC
producer: {}", t.getMessage());
+ return PQCMetrics.NOOP;
+ }
+ }
+
+ private static boolean isStatefulSignatureAlgorithm(String algorithm) {
+ return PQCSignatureAlgorithms.XMSS.name().equals(algorithm)
+ || PQCSignatureAlgorithms.XMSSMT.name().equals(algorithm)
+ || PQCSignatureAlgorithms.LMS.name().equals(algorithm)
+ || PQCSignatureAlgorithms.HSS.name().equals(algorithm);
+ }
+
/**
* Enforces key status checks before cryptographic operations when strict
key lifecycle mode is enabled. Looks up
* the key metadata via the configured {@link KeyLifecycleManager} using
the {@code CamelPQCKeyId} header.
@@ -411,6 +452,16 @@ public class PQCProducer extends DefaultProducer {
producerHealthCheck.setEnabled(getEndpoint().getComponent().isHealthCheckProducerEnabled());
healthCheckRepository.addHealthCheck(producerHealthCheck);
}
+
+ // Optional Micrometer metrics (no-op unless a MeterRegistry is
available)
+ metrics = createMetrics();
+ String signatureAlgorithm = getConfiguration().getSignatureAlgorithm();
+ if (isStatefulSignatureAlgorithm(signatureAlgorithm)) {
+ metrics.registerStatefulKeyGauge(signatureAlgorithm, () -> {
+ KeyPair kp = getRuntimeKeyPair();
+ return kp != null && kp.getPrivate() != null ?
getStatefulKeyRemaining(kp.getPrivate()) : -1;
+ });
+ }
}
@Override
@@ -419,6 +470,8 @@ public class PQCProducer extends DefaultProducer {
healthCheckRepository.removeHealthCheck(producerHealthCheck);
producerHealthCheck = null;
}
+ metrics.close();
+ metrics = PQCMetrics.NOOP;
super.doStop();
}
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMetrics.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMetrics.java
new file mode 100644
index 000000000000..60889522fcda
--- /dev/null
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMetrics.java
@@ -0,0 +1,63 @@
+/*
+ * 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.camel.component.pqc.metrics;
+
+import java.util.function.LongSupplier;
+
+/**
+ * Abstraction over the optional Micrometer metrics emitted by the PQC
producer.
+ * <p/>
+ * When Micrometer is not on the classpath, or no {@code MeterRegistry} is
available in the Camel registry,
+ * {@link #NOOP} is used and nothing is recorded. This interface is
deliberately free of any {@code io.micrometer} type
+ * so that {@code PQCProducer} can reference it without forcing Micrometer
onto the classpath - the concrete
+ * {@link PQCMicrometerMetrics} implementation (which does reference
Micrometer) is only loaded once Micrometer is known
+ * to be present.
+ */
+public interface PQCMetrics {
+
+ /**
+ * A no-op instance used when metrics are disabled.
+ */
+ PQCMetrics NOOP = new PQCMetrics() {
+ };
+
+ /**
+ * Records that an operation was performed.
+ *
+ * @param operation the operation name (for example {@code sign}, {@code
verify},
+ * {@code generateSecretKeyEncapsulation})
+ * @param algorithm the PQC algorithm in use
+ * @param success whether the operation completed successfully
+ */
+ default void recordOperation(String operation, String algorithm, boolean
success) {
+ }
+
+ /**
+ * Registers a gauge reporting the remaining signatures of a stateful
signature key.
+ *
+ * @param algorithm the stateful signature algorithm (XMSS,
XMSSMT, LMS, HSS)
+ * @param remainingSupplier supplies the current remaining-signature count
(negative when unavailable)
+ */
+ default void registerStatefulKeyGauge(String algorithm, LongSupplier
remainingSupplier) {
+ }
+
+ /**
+ * Removes any meters registered by this instance.
+ */
+ default void close() {
+ }
+}
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetrics.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetrics.java
new file mode 100644
index 000000000000..63d20d3642a5
--- /dev/null
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetrics.java
@@ -0,0 +1,88 @@
+/*
+ * 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.camel.component.pqc.metrics;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.LongSupplier;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import org.apache.camel.CamelContext;
+
+/**
+ * Micrometer-backed {@link PQCMetrics}. All {@code io.micrometer} references
live in this class so that it is only
+ * loaded when Micrometer is actually present on the classpath (see {@code
PQCProducer#createMetrics}).
+ */
+public class PQCMicrometerMetrics implements PQCMetrics {
+
+ public static final String METRIC_OPERATIONS = "camel.pqc.operations";
+ public static final String METRIC_STATEFUL_REMAINING =
"camel.pqc.stateful.key.remaining";
+
+ private final MeterRegistry registry;
+ private final ConcurrentMap<String, Counter> counters = new
ConcurrentHashMap<>();
+ private volatile Gauge statefulKeyGauge;
+
+ public PQCMicrometerMetrics(MeterRegistry registry) {
+ this.registry = registry;
+ }
+
+ /**
+ * Creates a Micrometer-backed metrics instance if a {@code MeterRegistry}
is available in the Camel registry,
+ * otherwise returns {@link PQCMetrics#NOOP}.
+ */
+ public static PQCMetrics create(CamelContext camelContext) {
+ Set<MeterRegistry> registries =
camelContext.getRegistry().findByType(MeterRegistry.class);
+ if (registries.isEmpty()) {
+ return PQCMetrics.NOOP;
+ }
+ return new PQCMicrometerMetrics(registries.iterator().next());
+ }
+
+ @Override
+ public void recordOperation(String operation, String algorithm, boolean
success) {
+ String outcome = success ? "success" : "failure";
+ String key = operation + '|' + algorithm + '|' + outcome;
+ counters.computeIfAbsent(key, k -> Counter.builder(METRIC_OPERATIONS)
+ .tag("operation", operation)
+ .tag("algorithm", algorithm)
+ .tag("outcome", outcome)
+ .description("Number of PQC operations performed")
+ .register(registry))
+ .increment();
+ }
+
+ @Override
+ public void registerStatefulKeyGauge(String algorithm, LongSupplier
remainingSupplier) {
+ statefulKeyGauge = Gauge.builder(METRIC_STATEFUL_REMAINING,
remainingSupplier, LongSupplier::getAsLong)
+ .tag("algorithm", algorithm)
+ .description("Remaining signatures for the stateful PQC
signature key")
+ .register(registry);
+ }
+
+ @Override
+ public void close() {
+ counters.values().forEach(registry::remove);
+ counters.clear();
+ if (statefulKeyGauge != null) {
+ registry.remove(statefulKeyGauge);
+ statefulKeyGauge = null;
+ }
+ }
+}
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCMetricsTest.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCMetricsTest.java
new file mode 100644
index 000000000000..72748f057e90
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCMetricsTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.camel.component.pqc;
+
+import java.security.Security;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Produce;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.component.pqc.metrics.PQCMicrometerMetrics;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PQCMetricsTest extends CamelTestSupport {
+
+ @BindToRegistry
+ private final MeterRegistry meterRegistry = new SimpleMeterRegistry();
+
+ @EndpointInject("mock:sign")
+ protected MockEndpoint resultSign;
+
+ @Produce("direct:sign")
+ protected ProducerTemplate templateSign;
+
+ @BeforeAll
+ public static void startup() {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:sign")
+ .to("pqc:sign?operation=sign&signatureAlgorithm=MLDSA")
+ .to("mock:sign")
+
.to("pqc:verify?operation=verify&signatureAlgorithm=MLDSA");
+ }
+ };
+ }
+
+ @Test
+ void testProducerRecordsOperationMetrics() throws Exception {
+ resultSign.expectedMessageCount(1);
+ templateSign.sendBody("Hello PQC");
+ resultSign.assertIsSatisfied();
+
+ Counter sign =
meterRegistry.find(PQCMicrometerMetrics.METRIC_OPERATIONS)
+ .tag("operation", "sign").tag("algorithm",
"MLDSA").tag("outcome", "success").counter();
+ assertNotNull(sign, "sign success counter should be registered");
+ assertTrue(sign.count() >= 1.0);
+
+ Counter verify =
meterRegistry.find(PQCMicrometerMetrics.METRIC_OPERATIONS)
+ .tag("operation", "verify").tag("outcome",
"success").counter();
+ assertNotNull(verify, "verify success counter should be registered");
+ assertTrue(verify.count() >= 1.0);
+ }
+
+ @Test
+ void testMicrometerMetricsDirectly() {
+ SimpleMeterRegistry reg = new SimpleMeterRegistry();
+ PQCMicrometerMetrics m = new PQCMicrometerMetrics(reg);
+
+ m.recordOperation("sign", "MLDSA", true);
+ m.recordOperation("sign", "MLDSA", true);
+ m.recordOperation("sign", "MLDSA", false);
+ m.registerStatefulKeyGauge("XMSS", () -> 42L);
+
+ Counter ok = reg.find(PQCMicrometerMetrics.METRIC_OPERATIONS)
+ .tag("operation", "sign").tag("outcome", "success").counter();
+ assertNotNull(ok);
+ assertEquals(2.0, ok.count());
+
+ Counter fail = reg.find(PQCMicrometerMetrics.METRIC_OPERATIONS)
+ .tag("operation", "sign").tag("outcome", "failure").counter();
+ assertNotNull(fail);
+ assertEquals(1.0, fail.count());
+
+ Gauge gauge =
reg.find(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).tag("algorithm",
"XMSS").gauge();
+ assertNotNull(gauge);
+ assertEquals(42.0, gauge.value());
+
+ // close() removes the meters it registered
+ m.close();
+ assertNull(reg.find(PQCMicrometerMetrics.METRIC_OPERATIONS).counter());
+
assertNull(reg.find(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).gauge());
+ }
+}