fix: Reduce high cardinality in ProvMnS API client metrics 90/142790/5
authoremaclee <lee.anjella.macabuhay@est.tech>
Mon, 22 Dec 2025 13:42:59 +0000 (13:42 +0000)
committeremaclee <lee.anjella.macabuhay@est.tech>
Mon, 22 Dec 2025 17:35:48 +0000 (17:35 +0000)
- Add ClientRequestMetricsTagCustomizer to template FDN values in URI metrics
- Apply custom metrics handler to DMI WebClients
- Template URI from /ProvMnS/v1/SubNetwork=A/ManagedElement=E to /ProvMnS/v1/{fdn}
- Preserve query parameters as templates for operational visibility
- Fix existing tests to handle new WebClient configuration dependencies

Issue-ID: CPS-3067
Change-Id: I98b972a9d84f6e6710e82ae73bc5499771c3a340
Signed-off-by: emaclee <lee.anjella.macabuhay@est.tech>
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfiguration.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizer.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfigurationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizerSpec.groovy [new file with mode: 0644]

index 4134a56..fef8b81 100644 (file)
@@ -22,6 +22,8 @@ package org.onap.cps.ncmp.impl.dmi;
 
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.ncmp.config.DmiHttpClientConfig;
+import org.onap.cps.ncmp.config.ServiceConfig;
+import org.onap.cps.ncmp.impl.provmns.http.ClientRequestMetricsTagCustomizer;
 import org.onap.cps.ncmp.impl.utils.http.WebClientConfiguration;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -32,6 +34,7 @@ import org.springframework.web.reactive.function.client.WebClient;
 public class DmiWebClientsConfiguration extends WebClientConfiguration {
 
     private final DmiHttpClientConfig dmiHttpClientConfig;
+    private final ClientRequestMetricsTagCustomizer clientRequestMetricsTagCustomizer;
 
     /**
      * Configures and creates a web client bean for DMI data services.
@@ -41,7 +44,7 @@ public class DmiWebClientsConfiguration extends WebClientConfiguration {
      */
     @Bean
     public WebClient dataServicesWebClient(final WebClient.Builder webClientBuilder) {
-        return configureWebClient(webClientBuilder, dmiHttpClientConfig.getDataServices());
+        return configureWebClientWithMetrics(webClientBuilder, dmiHttpClientConfig.getDataServices());
     }
 
     /**
@@ -52,7 +55,7 @@ public class DmiWebClientsConfiguration extends WebClientConfiguration {
      */
     @Bean
     public WebClient modelServicesWebClient(final WebClient.Builder webClientBuilder) {
-        return configureWebClient(webClientBuilder, dmiHttpClientConfig.getModelServices());
+        return configureWebClientWithMetrics(webClientBuilder, dmiHttpClientConfig.getModelServices());
     }
 
     /**
@@ -65,4 +68,17 @@ public class DmiWebClientsConfiguration extends WebClientConfiguration {
     public WebClient healthChecksWebClient(final WebClient.Builder webClientBuilder) {
         return configureWebClient(webClientBuilder, dmiHttpClientConfig.getHealthCheckServices());
     }
+
+    /**
+     * Configures WebClient with custom metrics observation convention for ProvMnS API calls.
+     *
+     * @param webClientBuilder The builder instance to create the WebClient.
+     * @param serviceConfig The service configuration.
+     * @return a WebClient instance configured with custom metrics.
+     */
+    private WebClient configureWebClientWithMetrics(final WebClient.Builder webClientBuilder,
+                                                    final ServiceConfig serviceConfig) {
+        webClientBuilder.observationConvention(clientRequestMetricsTagCustomizer);
+        return configureWebClient(webClientBuilder, serviceConfig);
+    }
 }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizer.java
new file mode 100644 (file)
index 0000000..37310eb
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ *  ================================================================================
+ *  Licensed 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.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.impl.provmns.http;
+
+import io.micrometer.common.KeyValues;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.ClientRequestObservationContext;
+import org.springframework.web.reactive.function.client.ClientRequestObservationConvention;
+import org.springframework.web.reactive.function.client.DefaultClientRequestObservationConvention;
+
+/**
+ * Custom ClientRequestObservationConvention to reduce high cardinality metrics
+ * by masking ProvMnS API URIs that contain FDN (Fully Distinguished Name) values.
+ */
+@Component
+public class ClientRequestMetricsTagCustomizer extends DefaultClientRequestObservationConvention implements
+        ClientRequestObservationConvention {
+
+    @Value("${rest.api.provmns-base-path:/ProvMnS}")
+    private String provMnsBasePath;
+
+    @Override
+    public KeyValues getLowCardinalityKeyValues(final ClientRequestObservationContext clientRequestObservationContext) {
+        return super.getLowCardinalityKeyValues(clientRequestObservationContext).and(
+                additionalTags(clientRequestObservationContext));
+    }
+
+    /**
+     * Creates additional tags for the metrics, specifically masking ProvMnS API URIs
+     * to reduce cardinality by replacing actual FDN values with a template placeholder.
+     *
+     * @param clientRequestObservationContext the client request observation context
+     * @return KeyValues containing the modified URI tag if applicable
+     */
+    protected KeyValues additionalTags(final ClientRequestObservationContext clientRequestObservationContext) {
+        final String uriTemplate = clientRequestObservationContext.getUriTemplate();
+        final String provMnsApiPath = provMnsBasePath + "/v1/";
+        if (uriTemplate != null && uriTemplate.contains(provMnsApiPath)) {
+            final String queryParameters = extractQueryParameters(uriTemplate);
+            final String maskedUri = provMnsApiPath + "{fdn}" + queryParameters;
+            return KeyValues.of("uri", maskedUri);
+        } else {
+            return KeyValues.empty();
+        }
+    }
+
+    /**
+     * Extracts query parameters from the URI template.
+     *
+     * @param uriTemplate the original URI template
+     * @return query parameters string (including the '?' prefix) or empty string if none
+     */
+    private String extractQueryParameters(final String uriTemplate) {
+        final int queryIndex = uriTemplate.indexOf('?');
+        return queryIndex != -1 ? uriTemplate.substring(queryIndex) : "";
+    }
+}
\ No newline at end of file
index 00bf859..0270de6 100644 (file)
@@ -25,6 +25,7 @@ import okhttp3.mockwebserver.MockResponse
 import okhttp3.mockwebserver.MockWebServer
 import org.onap.cps.ncmp.api.exceptions.DmiClientRequestException
 import org.onap.cps.ncmp.config.DmiHttpClientConfig
+import org.onap.cps.ncmp.impl.provmns.http.ClientRequestMetricsTagCustomizer
 import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.http.HttpStatus
@@ -48,7 +49,8 @@ class DmiRestClientIntegrationSpec extends Specification {
     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
     def webClientBuilder = WebClient.builder().baseUrl(baseUrl.toString())
-    def dmiWebClientsConfiguration = new DmiWebClientsConfiguration(new DmiHttpClientConfig())
+    def mockClientRequestMetricsTagCustomizer = Mock(ClientRequestMetricsTagCustomizer)
+    def dmiWebClientsConfiguration = new DmiWebClientsConfiguration(new DmiHttpClientConfig(), mockClientRequestMetricsTagCustomizer)
     def webClientForMockServer = dmiWebClientsConfiguration.dataServicesWebClient(webClientBuilder)
 
     def objectUnderTest = new DmiRestClient(mockDmiServiceAuthenticationProperties, jsonObjectMapper, webClientForMockServer, webClientForMockServer, webClientForMockServer)
index cb209be..81abce5 100644 (file)
@@ -22,6 +22,7 @@ package org.onap.cps.ncmp.impl.dmi
 
 
 import org.onap.cps.ncmp.config.DmiHttpClientConfig
+import org.onap.cps.ncmp.impl.provmns.http.ClientRequestMetricsTagCustomizer
 import org.springframework.boot.context.properties.EnableConfigurationProperties
 import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.test.context.ContextConfiguration
@@ -41,8 +42,8 @@ class DmiWebClientsConfigurationSpec extends Specification {
     }
 
     def dmiHttpClientConfiguration = Spy(DmiHttpClientConfig.class)
-
-    def objectUnderTest = new DmiWebClientsConfiguration(dmiHttpClientConfiguration)
+    def mockClientRequestMetricsTagCustomizer = Mock(ClientRequestMetricsTagCustomizer)
+    def objectUnderTest = new DmiWebClientsConfiguration(dmiHttpClientConfiguration, mockClientRequestMetricsTagCustomizer)
 
     def 'Web client for data services.'() {
         when: 'creating a web client for dmi data services'
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizerSpec.groovy
new file mode 100644 (file)
index 0000000..d984782
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ *  ================================================================================
+ *  Licensed 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.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.impl.provmns.http
+
+import io.micrometer.common.KeyValues
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.TestPropertySource
+import org.springframework.web.reactive.function.client.ClientRequest
+import org.springframework.web.reactive.function.client.ClientRequestObservationContext
+import spock.lang.Specification
+
+@SpringBootTest(classes = [ClientRequestMetricsTagCustomizer])
+@TestPropertySource(properties = ["rest.api.provmns-base-path=/ProvMnS"])
+class ClientRequestMetricsTagCustomizerSpec extends Specification {
+
+    @Autowired
+    ClientRequestMetricsTagCustomizer objectUnderTest
+
+    def 'Customize metrics via client request observation'() {
+        given: 'a request to a network device'
+            def context = Mock(ClientRequestObservationContext)
+            context.getUriTemplate() >> 'http://some-service/ProvMnS/v1/parent=A/child=E'
+            context.getCarrier() >> Mock(ClientRequest)
+
+        when: 'metrics are collected for this request'
+            def result = objectUnderTest.getLowCardinalityKeyValues(context)
+
+        then: 'the device-specific URL is masked to prevent too many metrics'
+            def additionalTags = objectUnderTest.additionalTags(context)
+            additionalTags.stream().anyMatch { it.key == 'uri' && it.value == '/ProvMnS/v1/{fdn}' }
+    }
+
+    def 'Mask URIs for ProvMns : #scenario'() {
+        given: 'a request to a network device'
+            def context = Mock(ClientRequestObservationContext)
+            context.getUriTemplate() >> inputUri
+
+        when: 'the URL is processed for metrics'
+            def result = objectUnderTest.additionalTags(context)
+
+        then: 'device-specific parts are replaced with a template'
+            if (expectedUri) {
+                result.stream().anyMatch { keyValue ->
+                    keyValue.key == 'uri' && keyValue.value == expectedUri
+                }
+            } else {
+                result == KeyValues.empty()
+            }
+
+        where:
+            scenario          | inputUri                                                             | expectedUri
+            'device path'     | 'http://some-service/ProvMnS/v1/parent=A/child=E'                    | '/ProvMnS/v1/{fdn}'
+            'with filters'    | 'http://some-service/ProvMnS/v1/parent=A/child=E?filter1=1&filter2=2'| '/ProvMnS/v1/{fdn}?filter1=1&filter2=2'
+            'filters only'    | 'http://some-service/ProvMnS/v1/?filter1=1'                          | '/ProvMnS/v1/{fdn}?filter1=1'
+            'non-matching URI'| 'http://some-service/other-api/v1/resource'                          | null
+    }
+}
\ No newline at end of file