Scrape all metrics and add them to cps documentation 00/140700/8
authorhalil.cakal <halil.cakal@est.tech>
Thu, 10 Apr 2025 13:02:54 +0000 (14:02 +0100)
committerhalil.cakal <halil.cakal@est.tech>
Wed, 23 Apr 2025 12:03:16 +0000 (13:03 +0100)
- add a new dummy annotation (TimedCustom) for Gauge metrics (cm handle state)
- add a description field for the counter annotation (CountCmHandleSearchExecution)
- add a python script (with tests) to scrape the metrics mentioned above
  using a single regex for all
- remove redundancies (distribution management) from pom.xml of
  checkstyle module since there is no artifact released from checkstyle
- import and display the output (metrics.cvs) in admin-guide docs

Issue-ID: CPS-2709

Change-Id: Iaee23f0a20c05e5aea033baacad1f23cb61e8b34
Signed-off-by: halil.cakal <halil.cakal@est.tech>
.gitignore
checkstyle/pom.xml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java [changed mode: 0755->0644]
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java [changed mode: 0755->0644]
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CountCmHandleSearchExecution.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/config/CmHandleStateGaugeConfig.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/config/TimedCustom.java [new file with mode: 0644]
docs/ScrapeMetrics.py [new file with mode: 0644]
docs/admin-guide.rst
docs/csv/metrics.csv [new file with mode: 0644]
docs/test_ScrapeMetrics.py [new file with mode: 0644]

index a721cb4..f7e2bf7 100755 (executable)
@@ -8,6 +8,9 @@
 cps-ncmp-rest-stub/dependency-reduced-pom.xml
 cps-application/archunit_store
 cps-ri/src/main/resources/changelog/db/changes/data/dmi/generated-csv/generated_yang_resource_*
+checkstyle/src/main/__pycache__/
+docs/venv/
+docs/__pycache__/
 target/
 log/
 
index 114c325..b54f229 100644 (file)
@@ -2,7 +2,7 @@
 <!--
   ============LICENSE_START=======================================================
   Copyright (C) 2020 Pantheon.tech
-  Modifications Copyright (C) 2023 Nordix Foundation
+  Modifications Copyright (C) 2023-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.
         </profile>
     </profiles>
 
-    <properties>
-        <onap.nexus.url>https://nexus.onap.org</onap.nexus.url>
-        <releaseNexusPath>/content/repositories/releases/</releaseNexusPath>
-        <sonar.skip>true</sonar.skip>
-        <snapshotNexusPath>/content/repositories/snapshots/</snapshotNexusPath>
-    </properties>
-
     <build>
         <pluginManagement>
             <plugins>
             </plugin>
         </plugins>
     </build>
-
-    <distributionManagement>
-        <repository>
-            <id>ecomp-releases</id>
-            <name>ECOMP Release Repository</name>
-            <url>${onap.nexus.url}${releaseNexusPath}</url>
-        </repository>
-        <snapshotRepository>
-            <id>ecomp-snapshots</id>
-            <name>ECOMP Snapshot Repository</name>
-            <url>${onap.nexus.url}${snapshotNexusPath}</url>
-        </snapshotRepository>
-    </distributionManagement>
 </project>
\ No newline at end of file
old mode 100755 (executable)
new mode 100644 (file)
index 6215427..387f48a
@@ -257,7 +257,8 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
      */
     @Override
     @SuppressWarnings("deprecation") // mapOldConditionProperties method will be removed in Release 12
-    @CountCmHandleSearchExecution(methodName = "searchCmHandles", interfaceName = "CPS-E-05")
+    @CountCmHandleSearchExecution(methodName = "searchCmHandles", interfaceName = "CPS-E-05",
+        description = "Search for cm handles within CPS-E-05 interface")
     public ResponseEntity<List<RestOutputCmHandle>> searchCmHandles(
             final CmHandleQueryParameters cmHandleQueryParameters) {
         final CmHandleQueryApiParameters cmHandleQueryApiParameters =
@@ -277,7 +278,8 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
      * @return                          collection of cm handle ids
      */
     @Override
-    @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-E-05")
+    @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-E-05",
+            description = "Search for cm handles within CPS-E-05 interface")
     public ResponseEntity<List<String>> searchCmHandleIds(final CmHandleQueryParameters cmHandleQueryParameters,
                                                           final Boolean outputAlternateId) {
         final CmHandleQueryApiParameters cmHandleQueryApiParameters =
old mode 100755 (executable)
new mode 100644 (file)
index e412107..5de8c12
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021-2022 Bell Canada
- *  Modifications Copyright (C) 2022-2025 Nordix Foundation
+ *  Modifications Copyright (C) 2022-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.
@@ -61,7 +61,8 @@ public class NetworkCmProxyInventoryController implements NetworkCmProxyInventor
      * @return                        list of cm handle IDs
      */
     @Override
-    @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-NCMP-I-01")
+    @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-NCMP-I-01",
+        description = "Search for cm handle ids within CPS-NCMP-I-01 interface")
     public ResponseEntity<List<String>> searchCmHandleIds(final CmHandleQueryParameters cmHandleQueryParameters,
                                                           final Boolean outputAlternateId) {
         final CmHandleQueryServiceParameters cmHandleQueryServiceParameters = ncmpRestInputMapper
index 27a0c4a..d04a2ea 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 Nordix Foundation
+ *  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.
@@ -42,4 +42,11 @@ public @interface CountCmHandleSearchExecution {
      * @return the CPS and NCMP interface name
      */
     String interfaceName();
+
+    /**
+     * Capture the description to facilitate metric scraping.
+     *
+     * @return the description of the metric.
+     */
+    String description();
 }
index f63a1bf..88bd133 100644 (file)
@@ -44,6 +44,8 @@ public class CmHandleStateGaugeConfig {
      * @return cm handle state gauge
      */
     @Bean
+    @TimedCustom(name = "cps_ncmp_inventory_cm_handles_by_state{state=ADVISED}",
+        description = "Current number of cm handles in advised state")
     public Gauge advisedCmHandles(final MeterRegistry meterRegistry) {
         return Gauge.builder(CM_HANDLE_STATE_GAUGE, cmHandlesByState,
                 value -> cmHandlesByState.get("advisedCmHandlesCount"))
@@ -59,6 +61,8 @@ public class CmHandleStateGaugeConfig {
      * @return cm handle state gauge
      */
     @Bean
+    @TimedCustom(name = "cps_ncmp_inventory_cm_handles_by_state{state=READY}",
+        description = "Current number of cm handles in ready state")
     public Gauge readyCmHandles(final MeterRegistry meterRegistry) {
         return Gauge.builder(CM_HANDLE_STATE_GAUGE, cmHandlesByState,
                 value -> cmHandlesByState.get("readyCmHandlesCount"))
@@ -74,6 +78,8 @@ public class CmHandleStateGaugeConfig {
      * @return cm handle state gauge
      */
     @Bean
+    @TimedCustom(name = "cps_ncmp_inventory_cm_handles_by_state{state=LOCKED}",
+        description = "Current number of cm handles in locked state")
     public Gauge lockedCmHandles(final MeterRegistry meterRegistry) {
         return Gauge.builder(CM_HANDLE_STATE_GAUGE, cmHandlesByState,
                 value -> cmHandlesByState.get("lockedCmHandlesCount"))
@@ -89,6 +95,8 @@ public class CmHandleStateGaugeConfig {
      * @return cm handle state gauge
      */
     @Bean
+    @TimedCustom(name = "cps_ncmp_inventory_cm_handles_by_state{state=DELETING}",
+        description = "Current number of cm handles in deleting state")
     public Gauge deletingCmHandles(final MeterRegistry meterRegistry) {
         return Gauge.builder(CM_HANDLE_STATE_GAUGE, cmHandlesByState,
                 value -> cmHandlesByState.get("deletingCmHandlesCount"))
@@ -104,6 +112,8 @@ public class CmHandleStateGaugeConfig {
      * @return cm handle state gauge
      */
     @Bean
+    @TimedCustom(name = "cps_ncmp_inventory_cm_handles_by_state{state=DELETED}",
+        description = "Number of cm handles that have been deleted since the application started")
     public Gauge deletedCmHandles(final MeterRegistry meterRegistry) {
         return Gauge.builder(CM_HANDLE_STATE_GAUGE, cmHandlesByState,
                 value -> cmHandlesByState.get("deletedCmHandlesCount"))
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/config/TimedCustom.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/config/TimedCustom.java
new file mode 100644 (file)
index 0000000..7219147
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * ============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.config;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Custom annotation to enable metric scraping.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TimedCustom {
+    /**
+     * Stores the name for a metric.
+     *
+     * @return the name of the metric.
+     */
+    String name();
+
+    /**
+     * Stores the description for a metric.
+     *
+     * @return the description of the metric.
+     */
+    String description();
+}
diff --git a/docs/ScrapeMetrics.py b/docs/ScrapeMetrics.py
new file mode 100644 (file)
index 0000000..9995178
--- /dev/null
@@ -0,0 +1,123 @@
+#  ============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=========================================================
+
+import os
+import re
+
+def find_java_files(root_dir):
+    """
+    Recursively finds all .java files within the given root directory.
+
+    Args:
+        root_dir (str): The root directory to search.
+
+    Returns:
+        list: A list of absolute paths to .java files.
+    """
+    java_files = []
+    for root, _, files in os.walk(root_dir):
+        for file in files:
+            if file.endswith(".java"):
+                java_files.append(os.path.join(root, file))
+    return java_files
+
+def scrape_metrics(file_content):
+    """
+    Matches @CountCmHandleSearchExecution, @Timed, and @TimedCustom and
+    Extracts name (confusingly labeled as 'value' in @Timed) and description from the given Java file content.
+    The regex will also handle the new line if the annotation would not fit in a single line.
+
+    Args:
+        file_content (str): The content of a Java file.
+
+    Returns:
+        list: A list of formatted metric strings.
+    """
+    pattern_regex = re.compile(r'@(CountCmHandleSearchExecution|Timed|TimedCustom)\((?:name\s*=\s*"(.*?)",?|value\s*=\s*"(.*?)",?)?.*?description\s*=\s*"(.*?)"', re.DOTALL)
+    all_metrics = []
+    matches = pattern_regex.findall(file_content)
+    for match in matches:
+        count_metric = match[0]
+        if count_metric == "CountCmHandleSearchExecution":
+            name = "cm_handle_search_invocation_total"
+        else:
+            name = match[1]
+        value = match[2]
+        description = match[3]
+        all_metrics.append(f'"{name or value}","{description}"')
+    return all_metrics
+
+def scrape_all_metrics_from_file(file_path):
+    """
+    Scrapes all defined metrics from a single Java file.
+
+    Args:
+        file_path (str): The path to the Java file.
+
+    Returns:
+        list: A list of all extracted metric strings from the file.
+    """
+    all_metrics = []
+    with open(file_path, 'r') as f:
+        java_class_content = f.read()
+        all_metrics.extend(scrape_metrics(java_class_content))
+    return all_metrics
+
+def write_metrics_to_file(metrics_data, output_file):
+    """
+    Writes the extracted metrics data to the specified output file.
+
+    Args:
+        metrics_data (list): A list of metric strings to write.
+        output_file (str): The path to the output file.
+    """
+    if metrics_data:
+        os.makedirs(os.path.dirname(output_file), exist_ok=True)
+        with open(output_file, 'w') as outfile:
+            outfile.write('"Metric Name","Description"\n')
+            for metric in metrics_data:
+                outfile.write(metric + '\n')
+        print(f"{len(metrics_data)} scraped metrics written to: {output_file}")
+
+def search_metrics_and_scrape(root_dir, output_file):
+    """
+    Orchestrates the search and scraping of metrics from Java files.
+
+    Args:
+        root_dir (str): The root directory to search for .java files.
+        output_file (str): The text file to store the metrics.
+    """
+    java_files = find_java_files(root_dir)
+    all_scraped_metrics = []
+    for java_file in java_files:
+        metrics = scrape_all_metrics_from_file(java_file)
+        all_scraped_metrics.extend(metrics)
+    write_metrics_to_file(all_scraped_metrics, output_file)
+
+if __name__ == "__main__":
+    # Get the absolute path of the current directory.
+    current_directory = os.path.dirname(os.path.abspath(__file__))
+
+    # Get the absolute path of the cps root directory.
+    cps_root_directory = os.path.abspath(os.path.join(current_directory, ".."))
+
+    # Define the location for the output file, and ensure its directory exists.
+    output_file = os.path.join(current_directory, "csv", "metrics.csv")
+
+    # Search and scrape the metrics.
+    search_metrics_and_scrape(cps_root_directory, output_file)
\ No newline at end of file
index 9009de2..447a171 100644 (file)
@@ -1,6 +1,6 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021-2025 Nordix Foundation
+.. Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
 
 .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
 .. _adminGuide:
@@ -220,6 +220,12 @@ This also includes both the liveliness state and readiness state.
 Metrics
 -------
 
+Below table lists all CPS-NCMP custom metrics
+
+.. csv-table::
+    :file: csv/metrics.csv
+    :widths: 50, 50
+
 Prometheus Metrics can be checked at the following endpoint
 
 .. code::
diff --git a/docs/csv/metrics.csv b/docs/csv/metrics.csv
new file mode 100644 (file)
index 0000000..25925dc
--- /dev/null
@@ -0,0 +1,57 @@
+"Metric Name","Description"
+"cps.ncmp.controller.get","Time taken to get resource data from datastore"
+"cm_handle_search_invocation_total","Search for cm handles within CPS-E-05 interface"
+"cm_handle_search_invocation_total","Search for cm handles within CPS-E-05 interface"
+"cm_handle_search_invocation_total","Search for cm handle ids within CPS-NCMP-I-01 interface"
+"cps.ncmp.inventory.controller.update","Time taken to handle registration request"
+"cps_ncmp_inventory_cm_handles_by_state{state=ADVISED}","Current number of cm handles in advised state"
+"cps_ncmp_inventory_cm_handles_by_state{state=READY}","Current number of cm handles in ready state"
+"cps_ncmp_inventory_cm_handles_by_state{state=LOCKED}","Current number of cm handles in locked state"
+"cps_ncmp_inventory_cm_handles_by_state{state=DELETING}","Current number of cm handles in deleting state"
+"cps_ncmp_inventory_cm_handles_by_state{state=DELETED}","Number of cm handles that have been deleted since the application started"
+"cps.ncmp.dmi.get","Time taken to fetch the resource data from operational data store for given cm handle "
+"cps.ncmp.inventory.persistence.datanode.get","Time taken to get a data node (from ncmp dmi registry)"
+"cps.ncmp.inventory.persistence.datanode.get","Time taken to get a data node (from ncmp dmi registry)"
+"cps.ncmp.inventory.module.references.from.dmi","Time taken to get all module references for a cm handle from dmi"
+"cps.ncmp.inventory.yang.resources.from.dmi","Time taken to get list of yang resources from dmi"
+"cps.ncmp.cmhandle.state.update.batch","Time taken to update a batch of cm handle states"
+"cps.rest.admin.controller.schemaset.create","Time taken to create schemaset from controller"
+"cps.data.controller.datanode.query.v1","Time taken to query data nodes"
+"cps.data.controller.datanode.query.v2","Time taken to query data nodes"
+"cps.data.controller.datanode.query.across.anchors","Time taken to query data nodes across anchors"
+"cps.data.controller.datanode.get.v1","Time taken to get data node"
+"cps.data.controller.datanode.get.v2","Time taken to get data node"
+"cps.delta.controller.get.delta","Time taken to get delta between anchors"
+"cps.delta.controller.get.delta","Time taken to get delta between anchors"
+"cps.module.persistence.schemaset.create","Time taken to store a schemaset (list of module references)"
+"cps.module.persistence.schemaset.createFromNewAndExistingModules","Time taken to store a schemaset (from new and existing)"
+"cps.data.persistence.service.datanode.query","Time taken to query data nodes"
+"cps.data.persistence.service.datanode.query.anchors","Time taken to query data nodes across all anchors or list of anchors"
+"cps.data.persistence.service.datanode.get","Time taken to get a data node"
+"cps.data.persistence.service.datanode.batch.get","Time taken to get data nodes"
+"cps.dataupdate.events.send","Time taken to send Data Update event"
+"cps.module.service.schemaset.create","Time taken to create (and store) a schemaset"
+"cps.data.service.datanode.query","Time taken to query data nodes"
+"cps.data.service.datanode.query","Time taken to query data nodes with a limit on results"
+"cps.data.service.datanode.root.save","Time taken to save a root data node"
+"cps.data.service.datanode.child.save","Time taken to save a child data node"
+"cps.data.service.list.element.save","Time taken to save list elements"
+"cps.data.service.datanode.get","Time taken to get data nodes for an xpath"
+"cps.data.service.datanode.batch.get","Time taken to get a batch of data nodes"
+"cps.data.service.datanode.leaves.update","Time taken to update a batch of leaf data nodes"
+"cps.data.service.datanode.leaves.descendants.leaves.update","Time taken to update data node leaves and existing descendants leaves"
+"cps.data.service.datanode.descendants.update","Time taken to update a data node and descendants"
+"cps.data.service.datanode.descendants.batch.update","Time taken to update a batch of data nodes and descendants"
+"cps.data.service.list.update","Time taken to update a list"
+"cps.data.service.list.batch.update","Time taken to update a batch of lists"
+"cps.data.service.datanode.delete","Time taken to delete a datanode"
+"cps.data.service.datanode.batch.delete","Time taken to delete a batch of datanodes"
+"cps.data.service.datanode.delete.anchor","Time taken to delete all datanodes for an anchor"
+"cps.data.service.datanode.delete.anchor.batch","Time taken to delete all datanodes for multiple anchors"
+"cps.data.service.list.delete","Time taken to delete a list or list element"
+"cps.delta.service.get.delta","Time taken to get delta between anchors"
+"cps.delta.service.get.delta","Time taken to get delta between anchor and a payload"
+"cps.utils.yangparser.nodedata.with.parent.parse","Time taken to parse node data with a parent"
+"cps.utils.yangparser.nodedata.with.parent.with.yangResourceMap.parse","Time taken to parse node data with a parent"
+"cps.yangtextschemasourceset.build","Time taken to build a yang text schema source set"
+"cps.yang.schemasourceset.build","Time taken to build a ODL yang Model"
diff --git a/docs/test_ScrapeMetrics.py b/docs/test_ScrapeMetrics.py
new file mode 100644 (file)
index 0000000..62365fd
--- /dev/null
@@ -0,0 +1,104 @@
+#  ============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=========================================================
+
+import unittest
+import os
+import tempfile
+import time
+from ScrapeMetrics import (
+    scrape_all_metrics_from_file
+)
+
+class TestScrapeMetrics(unittest.TestCase):
+
+    def setUp(self):
+        """Set up temporary directory and files for testing."""
+        self.temp_dir = tempfile.TemporaryDirectory()
+        self.test_root = self.temp_dir.name
+
+    def tearDown(self):
+        """Clean up temporary directory and files."""
+        self.temp_dir.cleanup()
+
+    def _create_java_file(self, relative_path, content):
+        """Helper function to create a test .java file."""
+        file_path = os.path.join(self.test_root, relative_path)
+        os.makedirs(os.path.dirname(file_path), exist_ok=True)
+        with open(file_path, 'w') as f:
+            f.write(content)
+        return file_path
+
+    def test_scrape_metrics_from_file(self):
+        """Test scraping all metrics from a single Java file."""
+        file_content = """
+            package com.example;
+
+            @CountCmHandleSearchExecution(
+                description = "A description does not fit the a single line")
+            public void myMethod() {}
+
+            @Timed(value="timed", description="A timed metric")
+            public void anotherMethod() {}
+
+            @TimedCustom(name="cps_ncmp_inventory_cm_handles_by_state{state=DELETING}", description="A custom timed metric")
+            public void anotherMethod() {}
+
+            @NotTimed
+            public void notTimedMethod() {}
+        """
+        test_file = self._create_java_file("com/example/MyService.java", file_content)
+        expected_metrics = [
+            '"cm_handle_search_invocation_total","A description does not fit the a single line"',
+            '"timed","A timed metric"',
+            '"cps_ncmp_inventory_cm_handles_by_state{state=DELETING}","A custom timed metric"'
+        ]
+        result = scrape_all_metrics_from_file(test_file)
+        self.assertEqual(len(result), 3)
+        self.assertEqual(result, expected_metrics)
+
+    def test_verify_metrics_file(self):
+        """Test if metrics.csv was modified less than 1 minute ago and has 56 lines."""
+
+        # Get the absolute path of the current directory.
+        current_directory = os.path.dirname(os.path.abspath(__file__))
+
+        metrics_file = os.path.join(current_directory, "csv/metrics.csv")
+
+        # Check if the file exists
+        self.assertTrue(os.path.exists(metrics_file), "metrics.csv does not exist.")
+
+        # Check modification time
+        modification_time_in_seconds = os.path.getmtime(metrics_file)
+        time_difference_in_seconds = time.time() - modification_time_in_seconds
+        self.assertLess(time_difference_in_seconds, 60, "metrics.csv was not modified in the last minute.")
+
+        # Check number of lines
+        with open(metrics_file, 'r') as f:
+            lines = f.readlines()
+
+        expected_number_of_metrics = 56
+        expected_number_of_lines = expected_number_of_metrics + 1  # Header
+        self.assertEqual(len(lines), expected_number_of_lines, f"metrics.csv does not have {expected_number_of_lines} lines.")
+
+if __name__ == '__main__':
+    # Ensure the script's directory is in the Python path for importing
+    import sys
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    if script_dir not in sys.path:
+        sys.path.insert(0, script_dir)
+    unittest.main()
\ No newline at end of file