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;
public class DmiWebClientsConfiguration extends WebClientConfiguration {
private final DmiHttpClientConfig dmiHttpClientConfig;
+ private final ClientRequestMetricsTagCustomizer clientRequestMetricsTagCustomizer;
/**
* Configures and creates a web client bean for DMI data services.
*/
@Bean
public WebClient dataServicesWebClient(final WebClient.Builder webClientBuilder) {
- return configureWebClient(webClientBuilder, dmiHttpClientConfig.getDataServices());
+ return configureWebClientWithMetrics(webClientBuilder, dmiHttpClientConfig.getDataServices());
}
/**
*/
@Bean
public WebClient modelServicesWebClient(final WebClient.Builder webClientBuilder) {
- return configureWebClient(webClientBuilder, dmiHttpClientConfig.getModelServices());
+ return configureWebClientWithMetrics(webClientBuilder, dmiHttpClientConfig.getModelServices());
}
/**
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);
+ }
}
--- /dev/null
+/*
+ * ============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
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
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)
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
}
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'
--- /dev/null
+/*
+ * ============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