From 0225bbef62bc046d3b8e31d7cbca634eea35d394 Mon Sep 17 00:00:00 2001 From: emaclee Date: Mon, 22 Dec 2025 13:42:59 +0000 Subject: [PATCH] fix: Reduce high cardinality in ProvMnS API client metrics - 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 --- .../ncmp/impl/dmi/DmiWebClientsConfiguration.java | 20 +++++- .../http/ClientRequestMetricsTagCustomizer.java | 76 ++++++++++++++++++++++ .../impl/dmi/DmiRestClientIntegrationSpec.groovy | 4 +- .../impl/dmi/DmiWebClientsConfigurationSpec.groovy | 5 +- .../ClientRequestMetricsTagCustomizerSpec.groovy | 76 ++++++++++++++++++++++ 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizer.java create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizerSpec.groovy diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfiguration.java index 4134a56ead..fef8b81c79 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfiguration.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfiguration.java @@ -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 index 0000000000..37310eb561 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizer.java @@ -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 diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy index 00bf859b12..0270de62f4 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy @@ -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) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfigurationSpec.groovy index cb209beef0..81abce5a09 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfigurationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiWebClientsConfigurationSpec.groovy @@ -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 index 0000000000..d984782d08 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/http/ClientRequestMetricsTagCustomizerSpec.groovy @@ -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 -- 2.16.6