Add prometheus metrics for searches and id-searches 07/140407/7
authormpriyank <priyank.maheshwari@est.tech>
Mon, 24 Feb 2025 15:23:09 +0000 (15:23 +0000)
committermpriyank <priyank.maheshwari@est.tech>
Thu, 13 Mar 2025 09:43:56 +0000 (09:43 +0000)
- Added counter to record the number of invocations for id-searches and
  searches endpoint for cm handle ids
- introducing a cps-interface tag to differentiate between inventory
  id-search and traditional id-search endpoints
- used AOP to have minimal code in the controller layers

Issue-ID: CPS-2611
Change-Id: Ib7db2a25f5f71d11872b779a23d38c1f7931410f
Signed-off-by: mpriyank <priyank.maheshwari@est.tech>
cps-application/src/test/java/org/onap/cps/architecture/ArchitectureTestBase.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounter.java [new file with mode: 0644]
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CountCmHandleSearchExecution.java [new file with mode: 0644]
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounterSpec.groovy [new file with mode: 0644]

index c1d6575..28ff7c3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2024 Nordix Foundation
+ *  Copyright (C) 2024-2025 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ public class ArchitectureTestBase {
                                                             "java..",
                                                             "lombok..",
                                                             "org.apache..",
+                                                            "org.aspectj..",
                                                             "org.mapstruct..",
                                                             "org.opendaylight..",
                                                             "org.slf4j..",
index 317f6b7..d7b38d1 100755 (executable)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Pantheon.tech
- *  Modifications Copyright (C) 2021-2024 Nordix Foundation
+ *  Modifications Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2021 highstreet technologies GmbH
  *  Modifications Copyright (C) 2021-2022 Bell Canada
  *  ================================================================================
@@ -57,6 +57,7 @@ import org.onap.cps.ncmp.rest.model.RestOutputCmHandle;
 import org.onap.cps.ncmp.rest.model.RestOutputCmHandleCompositeState;
 import org.onap.cps.ncmp.rest.model.RestOutputCmHandlePublicProperties;
 import org.onap.cps.ncmp.rest.util.CmHandleStateMapper;
+import org.onap.cps.ncmp.rest.util.CountCmHandleSearchExecution;
 import org.onap.cps.ncmp.rest.util.DataOperationRequestMapper;
 import org.onap.cps.ncmp.rest.util.DeprecationHelper;
 import org.onap.cps.ncmp.rest.util.NcmpRestInputMapper;
@@ -256,6 +257,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
      */
     @Override
     @SuppressWarnings("deprecation") // mapOldConditionProperties method will be removed in Release 12
+    @CountCmHandleSearchExecution(methodName = "searchCmHandles", interfaceName = "CPS-E-05")
     public ResponseEntity<List<RestOutputCmHandle>> searchCmHandles(
             final CmHandleQueryParameters cmHandleQueryParameters) {
         final CmHandleQueryApiParameters cmHandleQueryApiParameters =
@@ -276,6 +278,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
      * @return                          collection of cm handle ids
      */
     @Override
+    @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-E-05")
     public ResponseEntity<List<String>> searchCmHandleIds(final CmHandleQueryParameters cmHandleQueryParameters,
                                                           final Boolean outputAlternateId) {
         final CmHandleQueryApiParameters cmHandleQueryApiParameters =
index 0e27ba9..e412107 100755 (executable)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021-2022 Bell Canada
- *  Modifications Copyright (C) 2022-2024 Nordix Foundation
+ *  Modifications Copyright (C) 2022-2025 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters;
 import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse;
 import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse;
 import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration;
+import org.onap.cps.ncmp.rest.util.CountCmHandleSearchExecution;
 import org.onap.cps.ncmp.rest.util.NcmpRestInputMapper;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -60,6 +61,7 @@ public class NetworkCmProxyInventoryController implements NetworkCmProxyInventor
      * @return                        list of cm handle IDs
      */
     @Override
+    @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-NCMP-I-01")
     public ResponseEntity<List<String>> searchCmHandleIds(final CmHandleQueryParameters cmHandleQueryParameters,
                                                           final Boolean outputAlternateId) {
         final CmHandleQueryServiceParameters cmHandleQueryServiceParameters = ncmpRestInputMapper
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounter.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounter.java
new file mode 100644 (file)
index 0000000..ecd248d
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 Nordix Foundation
+ *  ================================================================================
+ *  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.rest.util;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import jakarta.validation.Valid;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters;
+import org.onap.cps.ncmp.rest.model.ConditionProperties;
+import org.springframework.stereotype.Component;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class CmHandleSearchExecutionCounter {
+
+    private static final String NO_CONDITION = "NONE";
+
+    private final MeterRegistry meterRegistry;
+
+    /**
+     * Counts the number of invocations of the methods annotated with @CountCmHandleSearchExecution based on the search
+     * conditions dynamically added. If search is executed without condition then it would be tagged as NONE, otherwise
+     * the conditions are concatenated with _ as separator.
+     *
+     * @param joinPoint                    join point
+     * @param countCmHandleSearchExecution count the cm handle search conditions
+     */
+    @Before("@annotation(countCmHandleSearchExecution)")
+    public void cmHandleSearchExecutionCounter(final JoinPoint joinPoint,
+            final CountCmHandleSearchExecution countCmHandleSearchExecution) {
+        final Object[] args = joinPoint.getArgs();
+
+        if (args.length == 0 || !(args[0] instanceof CmHandleQueryParameters cmHandleQueryParameters)) {
+            log.warn("Method {} is missing required CmHandleQueryParameters argument", joinPoint.getSignature());
+            return;
+        }
+
+        final String conditionTag = Optional.ofNullable(cmHandleQueryParameters.getCmHandleQueryParameters())
+                                            .filter(conditionTypes -> !conditionTypes.isEmpty())
+                                            .map(CmHandleSearchExecutionCounter::conditionTag)
+                                            .orElse(NO_CONDITION);
+
+        Counter.builder("cm_handle_search_invocations")
+                .tag("method", countCmHandleSearchExecution.methodName())
+                .tag("cps-interface", countCmHandleSearchExecution.interfaceName())
+                .tag("conditions", conditionTag)
+                .description("Number of invocations of search methods based on condition types")
+                .register(meterRegistry)
+                .increment();
+    }
+
+    private static String conditionTag(final List<@Valid ConditionProperties> conditionTypes) {
+        return conditionTypes.stream().map(ConditionProperties::getConditionName).sorted()
+                       .collect(Collectors.joining("_"));
+    }
+}
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CountCmHandleSearchExecution.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CountCmHandleSearchExecution.java
new file mode 100644 (file)
index 0000000..27a0c4a
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 Nordix Foundation
+ *  ================================================================================
+ *  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.rest.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CountCmHandleSearchExecution {
+
+    /**
+     * Capture the method name for which the number of invocations needs to be tracked.
+     *
+     * @return the search method name
+     */
+    String methodName();
+
+    /**
+     * Capture the CPS and NCMP interface name of the called method.
+     *
+     * @return the CPS and NCMP interface name
+     */
+    String interfaceName();
+}
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounterSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounterSpec.groovy
new file mode 100644 (file)
index 0000000..bdadfc8
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 Nordix Foundation
+ *  ================================================================================
+ *  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.rest.util
+
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry
+import org.aspectj.lang.JoinPoint
+import org.aspectj.lang.Signature
+import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters
+import org.onap.cps.ncmp.rest.model.ConditionProperties
+import spock.lang.Specification
+
+class CmHandleSearchExecutionCounterSpec extends Specification {
+
+    def meterRegistry = new SimpleMeterRegistry()
+    def mockJoinPoint = Mock(JoinPoint)
+    def mockCountCmHandleSearchExecutionAnnotation = Mock(CountCmHandleSearchExecution)
+    def mockSignature = Mock(Signature)
+
+    def objectUnderTest = new CmHandleSearchExecutionCounter(meterRegistry)
+
+    def setup() {
+        mockCountCmHandleSearchExecutionAnnotation.methodName() >> 'testMethod'
+        mockCountCmHandleSearchExecutionAnnotation.interfaceName() >> 'testInterface'
+        mockSignature.toString() >> 'testSignature'
+        mockJoinPoint.getSignature() >> mockSignature
+    }
+
+    def 'should track search with conditions'() {
+        given: 'CmHandleQueryParameters with conditions'
+            def cmHandleQueryParameters = new CmHandleQueryParameters()
+            def condition1 = new ConditionProperties(conditionName: 'condition1')
+            def condition2 = new ConditionProperties(conditionName: 'condition2')
+            cmHandleQueryParameters.addCmHandleQueryParametersItem(condition1).addCmHandleQueryParametersItem(condition2)
+        and: 'joinPoint returns the parameters'
+            mockJoinPoint.getArgs() >> [cmHandleQueryParameters]
+        when: 'the annotated method is called'
+            objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation)
+        then: 'the counter should be registered'
+            def counter = findCounter('cm_handle_search_invocations', [
+                'method'       : 'testMethod',
+                'cps-interface': 'testInterface',
+                'conditions'   : 'condition1_condition2'
+            ])
+        and: 'is incremented once'
+            assert counter.count() == 1
+    }
+
+    def 'should track search with no conditions as NONE'() {
+        given: 'empty CmHandleQueryParameters'
+            def cmHandleQueryParameters = new CmHandleQueryParameters()
+        and: 'joinPoint returns the parameters'
+            mockJoinPoint.getArgs() >> [cmHandleQueryParameters]
+        when: 'the annotated method is called'
+            objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation)
+        then: 'the counter should be registered with NONE tag'
+            def counter = findCounter('cm_handle_search_invocations', [
+                method         : 'testMethod',
+                'cps-interface': 'testInterface',
+                conditions     : 'NONE'
+            ])
+        and: 'is incremented once'
+            assert counter.count() == 1
+    }
+
+    def 'should not create counter when args are empty'() {
+        given: 'joinPoint with empty args'
+            mockJoinPoint.getArgs() >> []
+        when: 'the aspect method is called'
+            objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation)
+        then: 'no counter should be registered'
+            assert meterRegistry.find('cm_handle_search_invocations').counters().isEmpty()
+    }
+
+    def 'should not create counter when first arg is not CmHandleQueryParameters'() {
+        given: 'joinPoint with non-CmHandleQueryParameters arg'
+            mockJoinPoint.getArgs() >> ['not a CmHandleQueryParameters']
+        when: 'the aspect method is called'
+            objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation)
+        then: 'no counter should be registered'
+            assert meterRegistry.find('cm_handle_search_invocations').counters().isEmpty()
+    }
+
+    def 'should sort condition names alphabetically'() {
+        given: 'CmHandleQueryParameters with unsorted conditions'
+            def cmHandleQueryParameters = new CmHandleQueryParameters()
+            def condition1 = new ConditionProperties(conditionName: 'zCondition')
+            def condition2 = new ConditionProperties(conditionName: 'aCondition')
+            cmHandleQueryParameters.addCmHandleQueryParametersItem(condition1).addCmHandleQueryParametersItem(condition2)
+        and: 'joinPoint returns our parameters'
+            mockJoinPoint.getArgs() >> [cmHandleQueryParameters]
+        when: 'the aspect method is called'
+            objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation)
+        then: 'the counter should be registered with alphabetically sorted tags'
+            def counter = findCounter('cm_handle_search_invocations', [
+                'method'       : 'testMethod',
+                'cps-interface': 'testInterface',
+                'conditions'   : 'aCondition_zCondition'
+            ])
+        and: 'counter is incremented once'
+            assert counter.count() == 1
+    }
+
+    def findCounter(name, tags) {
+        def counterSearch = meterRegistry.find(name)
+        tags.each { key, value ->
+            counterSearch = counterSearch.tag(key, value)
+        }
+        return counterSearch.counter()
+    }
+}
\ No newline at end of file