From 2c63137864ac4c073f5c3dfe0adc5b82945cd0fb Mon Sep 17 00:00:00 2001 From: sm-xu Date: Thu, 5 Dec 2024 15:01:37 +0000 Subject: [PATCH 01/12] 1st version of interface utils --- .../intf_utils/intf_accept_metrics.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/snappi_tests/intf_utils/intf_accept_metrics.py diff --git a/tests/snappi_tests/intf_utils/intf_accept_metrics.py b/tests/snappi_tests/intf_utils/intf_accept_metrics.py new file mode 100644 index 00000000000..63de549cb72 --- /dev/null +++ b/tests/snappi_tests/intf_utils/intf_accept_metrics.py @@ -0,0 +1,109 @@ +# This file defines the interfaces that snappi tests accept external metrics. + +# Metrics data are organized into the hierarchies below +# ResourceMetrics +# ├── ResourceID +# └── ScopeMetrics +# ├── ScopeID +# └── Metric +# ├── Name +# ├── Description +# ├── Unit +# ├── metadata +# └── data +# └── Gauge +# +# A ResourceMetrics has its ID and a list of ScopeMetrics objects. +# A ScopeMetrics has its ID and a list of Metric objects. +# A Metric has several attributes and data. So far we only have Gauge type data. +# A Gauge has a list of NumberDataPoint objects. +# A NumberDataPoint has its label, value, flags and the timestamp at which the data was collected. +# +---------------------+ +# | DataPoint 1 | +# | +---------+ +-----+ | +# +-----+ | |timestamp| |label| | +# | 1 |-->| +---------+ +-----+ | +# +-----+ | | +# | . | | +-----+ +-----+ | +# | . | | |value| |flags| | +# | . | | +-----+ +-----+ | +# | . | +---------------------+ +# | . | . +# | . | . +# | . | . +# | . | +---------------------+ +# | . | | DataPoint M | +# +-----+ | +---------+ +-----+ | +# | M |-->| |timestamp| |label| | +# +-----+ | +---------+ +-----+ | +# | | +# | +-----+ +-----+ | +# | |value| |flags| | +# | +-----+ +-----+ | +# +---------------------+ + +from typing import List, Dict, Union + +class NumberDataPoint: + def __init__(self, time_unix_nano: int, label: List[Dict[str, str]], value: Union[int, float], flags: int = None): + self.time_unix_nano = time_unix_nano # UNIX Epoch time in nanoseconds + self.label = label # The key of key-value pairs in dictionaries + self.value = value # Metric value (can be double or integer) + self.flags = flags # Optional flags + + def __repr__(self): + return (f"NumberDataPoint(label={self.label}, " + f"time_unix_nano={self.time_unix_nano}, value={self.value}, flags={self.flags})") + + +class Gauge: + def __init__(self): + self.data_points = [] # List of NumberDataPoint objects + + def add_data_point(self, data_point): + self.data_points.append(data_point) + + def __repr__(self): + return f"Gauge(data_points={self.data_points})" + + +class Metric: + def __init__(self, name, description, unit, data_points, metadata=None): + self.name = name # Metric name + self.description = description # Metric description + self.unit = unit # Metric unit (e.g., seconds, bytes) + self.data = data # Can be Gauge only + self.metadata = metadata or {} # Default to an empty dictionary if None + + def __repr__(self): + return (f"Metric(name={self.name}, description={self.description}, " + f"unit={self.unit}, data={self.data})") + + +# a ScopeMetrics object's ID is device_id +class ScopeMetrics: + def __init__(self, device_id): + self.device_id = device_id + self.metrics = [] + + def add_metric(self, metric): + self.metrics.append(metric) + + def __repr__(self): + return f"ScopeMetrics(scope={self.scope}, metrics={self.metrics})" + + +# a ResourceMetrics object's ID is test_run_id +class ResourceMetrics: + def __init__(self, test_run_id, os_version): + self.test_run_id = test_run_id + self.os_version = os_version + self.scope_metrics = [] + + def add_scope_metrics(self, scope_metric): + self.scope_metrics.append(scope_metric) + + def __repr__(self): + return f"ResourceMetrics(resource={self.resource}, scope_metrics={self.scope_metrics})" + + From 3dd931b7219213b7216272db53edb83c11f1a5c4 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Fri, 6 Dec 2024 02:39:16 +0000 Subject: [PATCH 02/12] addressed the review comments --- .../intf_utils/intf_accept_metrics.py | 170 +++++++++++------- 1 file changed, 104 insertions(+), 66 deletions(-) diff --git a/tests/snappi_tests/intf_utils/intf_accept_metrics.py b/tests/snappi_tests/intf_utils/intf_accept_metrics.py index 63de549cb72..bb3a3d2ddd0 100644 --- a/tests/snappi_tests/intf_utils/intf_accept_metrics.py +++ b/tests/snappi_tests/intf_utils/intf_accept_metrics.py @@ -1,10 +1,10 @@ # This file defines the interfaces that snappi tests accept external metrics. # Metrics data are organized into the hierarchies below -# ResourceMetrics -# ├── ResourceID -# └── ScopeMetrics -# ├── ScopeID +# TestMetrics +# ├── TestID +# └── DeviceMetrics +# ├── DeviceID # └── Metric # ├── Name # ├── Description @@ -13,97 +13,135 @@ # └── data # └── Gauge # -# A ResourceMetrics has its ID and a list of ScopeMetrics objects. -# A ScopeMetrics has its ID and a list of Metric objects. +# A TestMetrics has its ID and a list of DeviceMetrics objects. +# A DeviceMetrics has its ID and a list of Metric objects. # A Metric has several attributes and data. So far we only have Gauge type data. # A Gauge has a list of NumberDataPoint objects. # A NumberDataPoint has its label, value, flags and the timestamp at which the data was collected. -# +---------------------+ -# | DataPoint 1 | -# | +---------+ +-----+ | -# +-----+ | |timestamp| |label| | -# | 1 |-->| +---------+ +-----+ | -# +-----+ | | -# | . | | +-----+ +-----+ | -# | . | | |value| |flags| | -# | . | | +-----+ +-----+ | -# | . | +---------------------+ -# | . | . -# | . | . -# | . | . -# | . | +---------------------+ -# | . | | DataPoint M | -# +-----+ | +---------+ +-----+ | -# | M |-->| |timestamp| |label| | -# +-----+ | +---------+ +-----+ | -# | | -# | +-----+ +-----+ | -# | |value| |flags| | -# | +-----+ +-----+ | -# +---------------------+ +# +# +# +-----------+ +# |DataPoint 1| +# | +-----+ | +# | |label| | +# +-----+ | +-----+ | +# | 1 |---> | +-----+ | +# +-----+ | |value| | +# | . | | +-----+ | +# | . | | +-----+ | +# | . | | |flags| | +# | . | | +-----+ | +# | . | +-----------+ +# | . | . +# | . | . +# | . | . +# | . | +-----------+ +# | . | |DataPoint M| +# | . | | +-----+ | +# | . | | |label| | +# +-----+ | +-----+ | +# | M |---> | +-----+ | +# +-----+ | |value| | +# | +-----+ | +# | +-----+ | +# | |flags| | +# | +-----+ | +# +-----------+ + + + from typing import List, Dict, Union -class NumberDataPoint: - def __init__(self, time_unix_nano: int, label: List[Dict[str, str]], value: Union[int, float], flags: int = None): - self.time_unix_nano = time_unix_nano # UNIX Epoch time in nanoseconds - self.label = label # The key of key-value pairs in dictionaries - self.value = value # Metric value (can be double or integer) - self.flags = flags # Optional flags + +############################## Accept Metrics ############################## + +# All metrics of one TestMetrics object are from the same testbed runing the same +# software version. They are also from the same test case identified by test_run_id. +class TestMetrics: + def __init__(self, testbed_name, os_version, testcase_name, test_run_id): + self.testbed_name = testbed_name + self.os_version = os_version + self.testcase_name = testcase_name + self.test_run_id = test_run_id + self.device_metrics = [] + + def add_device_metrics(self, device_metric): + self.device_metrics.append(device_metric) def __repr__(self): - return (f"NumberDataPoint(label={self.label}, " - f"time_unix_nano={self.time_unix_nano}, value={self.value}, flags={self.flags})") + return f"TestMetrics(test={self.test}, device_metrics={self.device_metrics})" -class Gauge: - def __init__(self): - self.data_points = [] # List of NumberDataPoint objects +# All metrics of one DeviceMetrics object are from the same device identified by device_id. +class DeviceMetrics: + def __init__(self, device_id): + self.device_id = device_id + self.metrics = [] - def add_data_point(self, data_point): - self.data_points.append(data_point) + def add_metric(self, metric): + self.metrics.append(metric) def __repr__(self): - return f"Gauge(data_points={self.data_points})" + return f"DeviceMetrics(device={self.device}, metrics={self.metrics})" +# All metrics of one Metric object belong to the same category tagged by metric name, +# e.g., psu info, temperature info, port counters class Metric: - def __init__(self, name, description, unit, data_points, metadata=None): - self.name = name # Metric name - self.description = description # Metric description - self.unit = unit # Metric unit (e.g., seconds, bytes) - self.data = data # Can be Gauge only - self.metadata = metadata or {} # Default to an empty dictionary if None + def __init__(self, name, description, unit, data_points, metadata = None): + self.name = name # Metric name (e.g., psu, temperature) + self.description = description # Metric description + self.unit = unit # Metric unit (e.g., seconds, bytes) + self.data = data # Can be Gauge only + self.metadata = metadata or {} # e.g. port_id, psu_id, default to an empty dictionary if None def __repr__(self): return (f"Metric(name={self.name}, description={self.description}, " f"unit={self.unit}, data={self.data})") -# a ScopeMetrics object's ID is device_id -class ScopeMetrics: - def __init__(self, device_id): - self.device_id = device_id - self.metrics = [] +class Gauge: + def __init__(self, time_unix_nano: int): + self.time_unix_nano = time_unix_nano # UNIX Epoch time in nanoseconds + self.data_points = [] # List of NumberDataPoint objects - def add_metric(self, metric): - self.metrics.append(metric) + def add_data_point(self, data_point): + self.data_points.append(data_point) + + def __repr__(self): + return f"Gauge(data_points={self.data_points})" + + +class NumberDataPoint: + def __init__(self, label: List[Dict[str, str]], value: Union[int, float], flags: int = None): + self.label = label # The key of key-value pairs in dictionaries + self.value = value # Metric value (can be double or integer) + self.flags = flags # Optional flags def __repr__(self): - return f"ScopeMetrics(scope={self.scope}, metrics={self.metrics})" + return (f"NumberDataPoint(label={self.label}, " + f"time_unix_nano={self.time_unix_nano}, value={self.value}, flags={self.flags})") +############################## Report Metrics ############################## -# a ResourceMetrics object's ID is test_run_id -class ResourceMetrics: - def __init__(self, test_run_id, os_version): +class MetricReporterFactory: + def __init__(self, testbed_name, testcase_name, test_run_id): + self.testbed_name = testbed_name + self.testcase_name = testcase_name self.test_run_id = test_run_id - self.os_version = os_version - self.scope_metrics = [] - def add_scope_metrics(self, scope_metric): - self.scope_metrics.append(scope_metric) + def create_metrics_reporter(self): + # Create MetricsReporter here. + pass - def __repr__(self): - return f"ResourceMetrics(resource={self.resource}, scope_metrics={self.scope_metrics})" +class MetricsReporter: + def __init__(self, testbed_name, testcase_name, test_run_id): + self.testbed_name = testbed_name + self.testcase_name = testcase_name + self.test_run_id = test_run_id + def emit_metrics(metrics: TestMetrics): + # to be implemented + pass From 7d24843ed35e64a0f1ec09d1732b6ab2a8eaec49 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Sat, 7 Dec 2024 06:34:51 +0000 Subject: [PATCH 03/12] 2nd version of interface utils --- .../intf_utils/intf_accept_metrics.py | 226 +++++++----------- .../intf_utils/intf_report_metrics.py | 36 +++ 2 files changed, 127 insertions(+), 135 deletions(-) create mode 100644 tests/snappi_tests/intf_utils/intf_report_metrics.py diff --git a/tests/snappi_tests/intf_utils/intf_accept_metrics.py b/tests/snappi_tests/intf_utils/intf_accept_metrics.py index bb3a3d2ddd0..fe686320446 100644 --- a/tests/snappi_tests/intf_utils/intf_accept_metrics.py +++ b/tests/snappi_tests/intf_utils/intf_accept_metrics.py @@ -1,147 +1,103 @@ # This file defines the interfaces that snappi tests accept external metrics. - -# Metrics data are organized into the hierarchies below -# TestMetrics -# ├── TestID -# └── DeviceMetrics -# ├── DeviceID -# └── Metric -# ├── Name -# ├── Description -# ├── Unit -# ├── metadata -# └── data -# └── Gauge -# -# A TestMetrics has its ID and a list of DeviceMetrics objects. -# A DeviceMetrics has its ID and a list of Metric objects. -# A Metric has several attributes and data. So far we only have Gauge type data. -# A Gauge has a list of NumberDataPoint objects. -# A NumberDataPoint has its label, value, flags and the timestamp at which the data was collected. -# -# -# +-----------+ -# |DataPoint 1| -# | +-----+ | -# | |label| | -# +-----+ | +-----+ | -# | 1 |---> | +-----+ | -# +-----+ | |value| | -# | . | | +-----+ | -# | . | | +-----+ | -# | . | | |flags| | -# | . | | +-----+ | -# | . | +-----------+ -# | . | . -# | . | . -# | . | . -# | . | +-----------+ -# | . | |DataPoint M| -# | . | | +-----+ | -# | . | | |label| | -# +-----+ | +-----+ | -# | M |---> | +-----+ | -# +-----+ | |value| | -# | +-----+ | -# | +-----+ | -# | |flags| | -# | +-----+ | -# +-----------+ - - - +import logging +import json +import datetime +import time from typing import List, Dict, Union +from intf_report_metrics import MetricReporterFactory OtelMetricReporter - -############################## Accept Metrics ############################## - -# All metrics of one TestMetrics object are from the same testbed runing the same -# software version. They are also from the same test case identified by test_run_id. -class TestMetrics: - def __init__(self, testbed_name, os_version, testcase_name, test_run_id): +class Metric: + def __init__(self, + name, + description, + unit, + timestamp, + testbed_name, + os_version, + testcase_name, + test_run_id, + device_id, + component_id, + reporter: MetricReporterFactory, + metadata = None): + """ + Args: + name (str): metric name (e.g., psu power, sensor temperature, port stats, etc.) + description (str): brief description of the metric + unit (str): metric unit (e.g., seconds, bytes) + timestamp (int): UNIX Epoch time in nanosecond, when the metric is collected + testbed_name (str): testbed name + os_version (str): switch OS version + testcase_name (str): test case name + test_run_id (str): ID of the test run + device_id (str): switch device ID + component_id (str): ID of the component (e.g., psu, sensor, port, etc.) + reporter(obj): object of MetricReporterFactory + metadata (str): e.g. serial number, model number, etc. Default to an empty dictionary if None + Returns: + N/A + """ + self.name = name + self.description = description + self.unit = unit + self.timestamp = timestamp self.testbed_name = testbed_name self.os_version = os_version self.testcase_name = testcase_name self.test_run_id = test_run_id - self.device_metrics = [] - - def add_device_metrics(self, device_metric): - self.device_metrics.append(device_metric) + self.device_id = device_id + self.component_id = component_id + self.reporter = reporter.create_metrics_reporter() + self.metadata = metadata or {} def __repr__(self): - return f"TestMetrics(test={self.test}, device_metrics={self.device_metrics})" - - -# All metrics of one DeviceMetrics object are from the same device identified by device_id. -class DeviceMetrics: - def __init__(self, device_id): - self.device_id = device_id - self.metrics = [] - - def add_metric(self, metric): - self.metrics.append(metric) + return (f"Metric(name={self.name!r}, description={self.description!r}, " + f"unit={self.unit!r}, timestamp={self.timestamp!r}, " + f"testbed_name={self.testbed_name!r}, os_version={self.os_version!r}, " + f"testcase_name={self.testcase_name!r}, test_run_id={self.test_run_id!r}, " + f"device_id={self.device_id!r}, component_id={self.component_id!r}, " + f"reporter={self.reporter!r}), metadata={self.metadata!r})") + + +class GaugeMetric(Metric): + def __init__(self, + name, + description, + unit, + timestamp, + testbed_name, + os_version, + testcase_name, + test_run_id, + device_id, + component_id, + reporter: MetricReporterFactory, + metadata = None, + metrics: Dict[str, Union[int, str, float]] = None): + # Initialize the base class + super().__init__(name, description, unit, timestamp, testbed_name, os_version, + testcase_name, test_run_id, device_id, component_id, reporter, metadata, metrics) + + # Additional fields for GaugeMetric + self.metrics = metrics or {} + + def add_metrics(self, new_metrics: Dict[str, Union[int, str, float]]): + # Add new elements to the metrics dictionary. + # new_metrics: Dictionary containing new key-value pairs to append. + self.metrics.update(new_metrics) def __repr__(self): - return f"DeviceMetrics(device={self.device}, metrics={self.metrics})" - - -# All metrics of one Metric object belong to the same category tagged by metric name, -# e.g., psu info, temperature info, port counters -class Metric: - def __init__(self, name, description, unit, data_points, metadata = None): - self.name = name # Metric name (e.g., psu, temperature) - self.description = description # Metric description - self.unit = unit # Metric unit (e.g., seconds, bytes) - self.data = data # Can be Gauge only - self.metadata = metadata or {} # e.g. port_id, psu_id, default to an empty dictionary if None - - def __repr__(self): - return (f"Metric(name={self.name}, description={self.description}, " - f"unit={self.unit}, data={self.data})") - - -class Gauge: - def __init__(self, time_unix_nano: int): - self.time_unix_nano = time_unix_nano # UNIX Epoch time in nanoseconds - self.data_points = [] # List of NumberDataPoint objects - - def add_data_point(self, data_point): - self.data_points.append(data_point) - - def __repr__(self): - return f"Gauge(data_points={self.data_points})" - - -class NumberDataPoint: - def __init__(self, label: List[Dict[str, str]], value: Union[int, float], flags: int = None): - self.label = label # The key of key-value pairs in dictionaries - self.value = value # Metric value (can be double or integer) - self.flags = flags # Optional flags - - def __repr__(self): - return (f"NumberDataPoint(label={self.label}, " - f"time_unix_nano={self.time_unix_nano}, value={self.value}, flags={self.flags})") - -############################## Report Metrics ############################## - -class MetricReporterFactory: - def __init__(self, testbed_name, testcase_name, test_run_id): - self.testbed_name = testbed_name - self.testcase_name = testcase_name - self.test_run_id = test_run_id - - def create_metrics_reporter(self): - # Create MetricsReporter here. - pass - - -class MetricsReporter: - def __init__(self, testbed_name, testcase_name, test_run_id): - self.testbed_name = testbed_name - self.testcase_name = testcase_name - self.test_run_id = test_run_id - - def emit_metrics(metrics: TestMetrics): - # to be implemented - pass + return (f"ExtendedMetric(name={self.name!r}, " + f"description={self.description!r}, " + f"unit={self.unit!r}, " + f"timestamp={self.timestamp!r}, " + f"testbed_name={self.testbed_name!r}, " + f"os_version={self.os_version!r}, " + f"testcase_name={self.testcase_name!r}, " + f"test_run_id={self.test_run_id!r}, " + f"device_id={self.device_id!r}, " + f"component_id={self.component_id!r}, " + f"component_id={self.reporter!r}, " + f"metadata={self.metadata!r}, " + f"metrics={self.metrics!r})") diff --git a/tests/snappi_tests/intf_utils/intf_report_metrics.py b/tests/snappi_tests/intf_utils/intf_report_metrics.py new file mode 100644 index 00000000000..331b97b17c8 --- /dev/null +++ b/tests/snappi_tests/intf_utils/intf_report_metrics.py @@ -0,0 +1,36 @@ +import logging +import json +import datetime +import time + +from typing import List, Dict, Union +from intf_accept_metrics import Metric GaugeMetric + +class MetricReporterFactory: + def __init__(self, connection): + # Temporary code initializing the MetricReporterFactory with a database connection + # will be replaced with OpenTelemetry connection + self.connection = connection + self.reporter = None + + def create_metrics_reporter(self): + self.reporter = OtelMetricReporter(self.connection) + return self.reporter + +class OtelMetricReporter: + def __init__(self, connection): + # Temporary code initializing the OtelMetricReporter + # will be replaced with OpenTelemetry connection + self.connection = connection + self.metrics = [] + + def register_metric(self, metrics): + self.metrics.append(metrics) + + def report(self, timestamp): + # Temporary code to report metrics + print(f"Reporting metrics at {timestamp}") + for metric in self.metrics: + print(metric) + + From f8fb42f21ae90522a9033dbdf9845a692f2f5d18 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Wed, 11 Dec 2024 06:41:41 +0000 Subject: [PATCH 04/12] 3rd version of interface utils --- .../intf_utils/intf_accept_metrics.py | 98 ++++++++----------- .../intf_utils/intf_report_metrics.py | 51 ++++++++-- 2 files changed, 86 insertions(+), 63 deletions(-) diff --git a/tests/snappi_tests/intf_utils/intf_accept_metrics.py b/tests/snappi_tests/intf_utils/intf_accept_metrics.py index fe686320446..acbfab15d4b 100644 --- a/tests/snappi_tests/intf_utils/intf_accept_metrics.py +++ b/tests/snappi_tests/intf_utils/intf_accept_metrics.py @@ -1,4 +1,5 @@ # This file defines the interfaces that snappi tests accept external metrics. + import logging import json import datetime @@ -9,75 +10,64 @@ class Metric: def __init__(self, - name, - description, - unit, - timestamp, - testbed_name, - os_version, - testcase_name, - test_run_id, - device_id, - component_id, + name: str, + description: str, + unit: str, + timestamp: int, + device_id: str, + component_id: str, reporter: MetricReporterFactory, - metadata = None): + metadata: Dict[str, str]): """ Args: name (str): metric name (e.g., psu power, sensor temperature, port stats, etc.) description (str): brief description of the metric unit (str): metric unit (e.g., seconds, bytes) - timestamp (int): UNIX Epoch time in nanosecond, when the metric is collected - testbed_name (str): testbed name - os_version (str): switch OS version - testcase_name (str): test case name - test_run_id (str): ID of the test run + timestamp (int): UNIX Epoch time in nanoseconds when the metric is collected device_id (str): switch device ID - component_id (str): ID of the component (e.g., psu, sensor, port, etc.) - reporter(obj): object of MetricReporterFactory - metadata (str): e.g. serial number, model number, etc. Default to an empty dictionary if None - Returns: - N/A + component_id (str): ID of the component (e.g., psu, sensor, port, etc.), where metrics are produced + reporter (MetricReporterFactory): object of MetricReporterFactory + metadata (Dict[str, str]): Additional information such as serial number, model number, testbed name, OS version, etc. """ - self.name = name - self.description = description - self.unit = unit - self.timestamp = timestamp - self.testbed_name = testbed_name - self.os_version = os_version - self.testcase_name = testcase_name - self.test_run_id = test_run_id - self.device_id = device_id - self.component_id = component_id - self.reporter = reporter.create_metrics_reporter() - self.metadata = metadata or {} + self.name = name + self.description = description + self.unit = unit + self.timestamp = timestamp + self.device_id = device_id + self.component_id = component_id + self.reporter = reporter.create_metrics_reporter() + + # Ensure mandatory fields are set in metadata + self.metadata = metadata + self.metadata.update({ + "testbed_name": metadata.get("testbed_name", ""), + "os_version": metadata.get("os_version", ""), + "testcase_name": metadata.get("testcase_name", ""), + "test_run_id": metadata.get("test_run_id", "") + "model_number": metadata.get("model_number", "") + "serial_number": metadata.get("serial_number", "") + }) def __repr__(self): return (f"Metric(name={self.name!r}, description={self.description!r}, " f"unit={self.unit!r}, timestamp={self.timestamp!r}, " - f"testbed_name={self.testbed_name!r}, os_version={self.os_version!r}, " - f"testcase_name={self.testcase_name!r}, test_run_id={self.test_run_id!r}, " f"device_id={self.device_id!r}, component_id={self.component_id!r}, " - f"reporter={self.reporter!r}), metadata={self.metadata!r})") + f"reporter={self.reporter!r}, metadata={self.metadata!r})") class GaugeMetric(Metric): def __init__(self, - name, - description, - unit, - timestamp, - testbed_name, - os_version, - testcase_name, - test_run_id, - device_id, - component_id, - reporter: MetricReporterFactory, - metadata = None, - metrics: Dict[str, Union[int, str, float]] = None): + name: str, + description: str, + unit: str, + timestamp: int, + device_id: str, + component_id: str, + reporter: MetricReporterFactory, + metadata: Dict[str, str], + metrics: Dict[str, Union[int, str, float]] = None): # Initialize the base class - super().__init__(name, description, unit, timestamp, testbed_name, os_version, - testcase_name, test_run_id, device_id, component_id, reporter, metadata, metrics) + super().__init__(name, description, unit, timestamp, device_id, component_id, reporter, metadata, metrics) # Additional fields for GaugeMetric self.metrics = metrics or {} @@ -92,12 +82,8 @@ def __repr__(self): f"description={self.description!r}, " f"unit={self.unit!r}, " f"timestamp={self.timestamp!r}, " - f"testbed_name={self.testbed_name!r}, " - f"os_version={self.os_version!r}, " - f"testcase_name={self.testcase_name!r}, " - f"test_run_id={self.test_run_id!r}, " f"device_id={self.device_id!r}, " f"component_id={self.component_id!r}, " - f"component_id={self.reporter!r}, " + f"reporter={self.reporter!r}, " f"metadata={self.metadata!r}, " f"metrics={self.metrics!r})") diff --git a/tests/snappi_tests/intf_utils/intf_report_metrics.py b/tests/snappi_tests/intf_utils/intf_report_metrics.py index 331b97b17c8..9eb7402e440 100644 --- a/tests/snappi_tests/intf_utils/intf_report_metrics.py +++ b/tests/snappi_tests/intf_utils/intf_report_metrics.py @@ -1,3 +1,4 @@ +# This file defines the classes exporting metrics and records to database. import logging import json import datetime @@ -13,9 +14,25 @@ def __init__(self, connection): self.connection = connection self.reporter = None + def create_metrics_reporter(self, data_type: str = "metrics"): + """ + Creates a specific metrics reporter based on the provided type. + + Args: + type (str): The type of metrics reporter to create. Default is 'otel'. + + Returns: + An instance of the specified metrics reporter. + """ + if data_type == "metrics": + self.reporter = OtelMetricReporter(self.connection) + return self.reporter + elif data_type == "records": + return KustoReporter(self.connection) + else: + raise ValueError(f"Unsupported reporter type: {data_type}") def create_metrics_reporter(self): - self.reporter = OtelMetricReporter(self.connection) - return self.reporter + class OtelMetricReporter: def __init__(self, connection): @@ -24,13 +41,33 @@ def __init__(self, connection): self.connection = connection self.metrics = [] - def register_metric(self, metrics): + def register_metrics(self, metrics): self.metrics.append(metrics) + @abstractmethod def report(self, timestamp): - # Temporary code to report metrics - print(f"Reporting metrics at {timestamp}") - for metric in self.metrics: - print(metric) + """ + Abstract method to report metrics at a given timestamp. + Subclasses must override this method. + """ + pass +class KustoReporter: + def __init__(self, connection): + # Temporary code initializing the KustoReporter + # will be replaced with Kusto connection + self.connection = connection + self.records = [] + + def register_records(self, records): + self.records.append(records) + + @abstractmethod + def report(self, timestamp): + """ + Abstract method to report records at a given timestamp. + Subclasses must override this method. + """ + pass + From b48641a5b4697768c74ce2280074b5ae72791c7f Mon Sep 17 00:00:00 2001 From: sm-xu Date: Mon, 16 Dec 2024 18:05:36 +0000 Subject: [PATCH 05/12] metrics handling v1 --- .../intf_utils/intf_accept_metrics.py | 89 ------------------- .../intf_utils/intf_report_metrics.py | 73 --------------- tests/snappi_tests/metrics_utils/examples.py | 62 +++++++++++++ .../metrics_utils/metrics_accepter.py | 64 +++++++++++++ .../metrics_utils/metrics_reporter.py | 60 +++++++++++++ 5 files changed, 186 insertions(+), 162 deletions(-) delete mode 100644 tests/snappi_tests/intf_utils/intf_accept_metrics.py delete mode 100644 tests/snappi_tests/intf_utils/intf_report_metrics.py create mode 100644 tests/snappi_tests/metrics_utils/examples.py create mode 100644 tests/snappi_tests/metrics_utils/metrics_accepter.py create mode 100644 tests/snappi_tests/metrics_utils/metrics_reporter.py diff --git a/tests/snappi_tests/intf_utils/intf_accept_metrics.py b/tests/snappi_tests/intf_utils/intf_accept_metrics.py deleted file mode 100644 index acbfab15d4b..00000000000 --- a/tests/snappi_tests/intf_utils/intf_accept_metrics.py +++ /dev/null @@ -1,89 +0,0 @@ -# This file defines the interfaces that snappi tests accept external metrics. - -import logging -import json -import datetime -import time - -from typing import List, Dict, Union -from intf_report_metrics import MetricReporterFactory OtelMetricReporter - -class Metric: - def __init__(self, - name: str, - description: str, - unit: str, - timestamp: int, - device_id: str, - component_id: str, - reporter: MetricReporterFactory, - metadata: Dict[str, str]): - """ - Args: - name (str): metric name (e.g., psu power, sensor temperature, port stats, etc.) - description (str): brief description of the metric - unit (str): metric unit (e.g., seconds, bytes) - timestamp (int): UNIX Epoch time in nanoseconds when the metric is collected - device_id (str): switch device ID - component_id (str): ID of the component (e.g., psu, sensor, port, etc.), where metrics are produced - reporter (MetricReporterFactory): object of MetricReporterFactory - metadata (Dict[str, str]): Additional information such as serial number, model number, testbed name, OS version, etc. - """ - self.name = name - self.description = description - self.unit = unit - self.timestamp = timestamp - self.device_id = device_id - self.component_id = component_id - self.reporter = reporter.create_metrics_reporter() - - # Ensure mandatory fields are set in metadata - self.metadata = metadata - self.metadata.update({ - "testbed_name": metadata.get("testbed_name", ""), - "os_version": metadata.get("os_version", ""), - "testcase_name": metadata.get("testcase_name", ""), - "test_run_id": metadata.get("test_run_id", "") - "model_number": metadata.get("model_number", "") - "serial_number": metadata.get("serial_number", "") - }) - - def __repr__(self): - return (f"Metric(name={self.name!r}, description={self.description!r}, " - f"unit={self.unit!r}, timestamp={self.timestamp!r}, " - f"device_id={self.device_id!r}, component_id={self.component_id!r}, " - f"reporter={self.reporter!r}, metadata={self.metadata!r})") - - -class GaugeMetric(Metric): - def __init__(self, - name: str, - description: str, - unit: str, - timestamp: int, - device_id: str, - component_id: str, - reporter: MetricReporterFactory, - metadata: Dict[str, str], - metrics: Dict[str, Union[int, str, float]] = None): - # Initialize the base class - super().__init__(name, description, unit, timestamp, device_id, component_id, reporter, metadata, metrics) - - # Additional fields for GaugeMetric - self.metrics = metrics or {} - - def add_metrics(self, new_metrics: Dict[str, Union[int, str, float]]): - # Add new elements to the metrics dictionary. - # new_metrics: Dictionary containing new key-value pairs to append. - self.metrics.update(new_metrics) - - def __repr__(self): - return (f"ExtendedMetric(name={self.name!r}, " - f"description={self.description!r}, " - f"unit={self.unit!r}, " - f"timestamp={self.timestamp!r}, " - f"device_id={self.device_id!r}, " - f"component_id={self.component_id!r}, " - f"reporter={self.reporter!r}, " - f"metadata={self.metadata!r}, " - f"metrics={self.metrics!r})") diff --git a/tests/snappi_tests/intf_utils/intf_report_metrics.py b/tests/snappi_tests/intf_utils/intf_report_metrics.py deleted file mode 100644 index 9eb7402e440..00000000000 --- a/tests/snappi_tests/intf_utils/intf_report_metrics.py +++ /dev/null @@ -1,73 +0,0 @@ -# This file defines the classes exporting metrics and records to database. -import logging -import json -import datetime -import time - -from typing import List, Dict, Union -from intf_accept_metrics import Metric GaugeMetric - -class MetricReporterFactory: - def __init__(self, connection): - # Temporary code initializing the MetricReporterFactory with a database connection - # will be replaced with OpenTelemetry connection - self.connection = connection - self.reporter = None - - def create_metrics_reporter(self, data_type: str = "metrics"): - """ - Creates a specific metrics reporter based on the provided type. - - Args: - type (str): The type of metrics reporter to create. Default is 'otel'. - - Returns: - An instance of the specified metrics reporter. - """ - if data_type == "metrics": - self.reporter = OtelMetricReporter(self.connection) - return self.reporter - elif data_type == "records": - return KustoReporter(self.connection) - else: - raise ValueError(f"Unsupported reporter type: {data_type}") - def create_metrics_reporter(self): - - -class OtelMetricReporter: - def __init__(self, connection): - # Temporary code initializing the OtelMetricReporter - # will be replaced with OpenTelemetry connection - self.connection = connection - self.metrics = [] - - def register_metrics(self, metrics): - self.metrics.append(metrics) - - @abstractmethod - def report(self, timestamp): - """ - Abstract method to report metrics at a given timestamp. - Subclasses must override this method. - """ - pass - - -class KustoReporter: - def __init__(self, connection): - # Temporary code initializing the KustoReporter - # will be replaced with Kusto connection - self.connection = connection - self.records = [] - - def register_records(self, records): - self.records.append(records) - - @abstractmethod - def report(self, timestamp): - """ - Abstract method to report records at a given timestamp. - Subclasses must override this method. - """ - pass - diff --git a/tests/snappi_tests/metrics_utils/examples.py b/tests/snappi_tests/metrics_utils/examples.py new file mode 100644 index 00000000000..ac1d892245f --- /dev/null +++ b/tests/snappi_tests/metrics_utils/examples.py @@ -0,0 +1,62 @@ +import logging +import json +import datetime +import time + +from typing import List, Dict, Union +from metrics_accepter import Metric, GaugeMetric +from metrics_reporter import MetricReporterFactory, MetricsReporter + +def main(): + """ + + PSU Model Serial HW Rev Voltage (V) Current (A) Power (W) Status LED + ----- --------------- --------------- -------- ------------- ------------- ----------- -------- ----- + PSU 1 PWR-2422-HV-RED 6A011010142349Q 01 12.09 18.38 222.00 OK green + PSU 2 PWR-2422-HV-RED 6A011010142327X 01 12.10 17.72 214.00 OK green + + """ + resource_labels = { + "testbed.id": "sonic_stress_testbed", + "os.version": "11.2.3", + "testcase": "stress_test1", + "testrun.id": "202412101217" + } + + # Create a MetricReporterFactory and build a MetricReporter + factory = MetricReporterFactory() + reporter = factory.create_metrics_reporter(resource_labels) + + scope_labels = { + "device.id": "str-7060x6-64pe-stress-02", + "psu.id": "psu1", + "model": "PWR-2422-HV-RED", + "serial": "6A011010142349Q"} + + # Create a metric and pass it to the reporter + vol = GaugeMetric(name = "Voltage", + description = "PSU voltage reading", + unit = "V", + reporter = reporter) + vol.set_gauge_metric(scope_labels, 12.09) + + # Create a metric and pass it to the reporter + cur = GaugeMetric(name = "Current", + description = "PSU current reading", + unit = "A", + reporter = reporter) + cur.set_gauge_metric(scope_labels, 18.38) + + # Create a metric and pass it to the reporter + power = GaugeMetric(name = "Power", + description = "PSU power reading", + unit = "W", + reporter = reporter) + power.set_gauge_metric(scope_labels, 222.00) + + # Report all metrics at a specific timestamp + reporter.report() + +if __name__ == '__main__': + main() + diff --git a/tests/snappi_tests/metrics_utils/metrics_accepter.py b/tests/snappi_tests/metrics_utils/metrics_accepter.py new file mode 100644 index 00000000000..fd35fac4d3a --- /dev/null +++ b/tests/snappi_tests/metrics_utils/metrics_accepter.py @@ -0,0 +1,64 @@ +""" +This file defines the classes accepting metrics and records from snappi tests. +IMPORTANT: Please use the standard labels: +testbed.id, os.version, testcase, testrun.id, device.id, psu.id, port.id, sensor.id +""" + +import logging +import json +import datetime +import time + +from typing import List, Dict, Union +from metrics_reporter import MetricReporterFactory, MetricsReporter + +class Metric: + def __init__(self, + name: str, + description: str, + unit: str, + reporter: MetricReporterFactory): + """ + Args: + name (str): metric name (e.g., psu power, sensor temperature, port stats, etc.) + description (str): brief description of the metric + unit (str): metric unit (e.g., seconds, bytes) + reporter (MetricReporterFactory): object of MetricReporterFactory + """ + self.name = name + self.description = description + self.unit = unit + self.reporter = reporter + + def __repr__(self): + return (f"Metric(name={self.name!r}, " + f"description={self.description!r}, " + f"unit={self.unit!r}, " + f"reporter={self.reporter!r})") + + +class GaugeMetric(Metric): + def __init__(self, + name: str, + description: str, + unit: str, + reporter: MetricReporterFactory): + # Initialize the base class + super().__init__(name, description, unit, reporter) + + def set_gauge_metric(self, scope_labels: Dict[str, str], value: Union[int, str, float]): + # Add scope level labels and set the metric value + gauge_metric = { + "name": self.name, + "description": self.description, + "unit": self.unit, + **scope_labels, # Add scope_labels to the dictionary + "value": value # Add the metric value + } + self.reporter.update_metrics(gauge_metric) + + def __repr__(self): + return (f"GaugeMetric(name={self.name!r}, " + f"description={self.description!r}, " + f"unit={self.unit!r}, " + f"reporter={self.reporter!r})") diff --git a/tests/snappi_tests/metrics_utils/metrics_reporter.py b/tests/snappi_tests/metrics_utils/metrics_reporter.py new file mode 100644 index 00000000000..78858efd2b2 --- /dev/null +++ b/tests/snappi_tests/metrics_utils/metrics_reporter.py @@ -0,0 +1,60 @@ +# This file defines the classes reporting metrics and records to the corresponding database. + +import logging +import json +import datetime +import time +from pprint import pprint +from typing import List, Dict, Union + +#from metrics_accepter import Metric, GaugeMetric + +class MetricReporterFactory: + def __init__(self): + self.reporter = None + + def create_metrics_reporter(self, resource_labels: Dict[str, str]): + self.reporter = MetricsReporter(resource_labels) + return self.reporter + + def create_records_reporter(self, resource_labels: Dict[str, str]): + self.reporter = RecordsReporter(resource_labels) + return self.reporter + + +class MetricsReporter: + def __init__(self, resource_labels: Dict[str, str]): + # Temporary code initializing a MetricsReporter + # will be replaced with a real initializer such as OpenTelemetry + self.resource_labels = resource_labels + self.timestamp = int(time.time() * 1_000_000_000) # epoch time in nanoseconds + self.metrics = [] + + def update_metrics(self, gauge_metric: Dict[str, Union[int, str, float]]): + # add a new metric + self.metrics.append(gauge_metric) + + def report(self): + """ + Abstract method to report metrics at a given timestamp. + Subclasses must override this method. + pprint(self.metrics) + """ + pass + + +class RecordsReporter: + def __init__(self, resource_labels: Dict[str, str]): + # Temporary code initializing a RecordsReporter + # will be replaced with a real initializer such as Kusto + self.resource_labels = resource_labels + self.timestamp = int(time.time() * 1_000_000_000) # epoch time in nanoseconds + self.records = [] + + def report(self): + """ + Abstract method to report records at a given timestamp. + Subclasses must override this method. + """ + pass + From 2d06ebdff2db0f07b485e09af2e1f70671d1ab37 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Fri, 20 Dec 2024 04:45:27 +0000 Subject: [PATCH 06/12] modified classes and files --- tests/snappi_tests/metrics_utils/examples.py | 62 -------- .../metrics_utils/metrics_accepter.py | 64 -------- .../metrics_utils/metrics_reporter.py | 60 -------- tests/snappi_tests/utils/allowed_labels.json | 13 ++ tests/snappi_tests/utils/examples.py | 67 ++++++++ tests/snappi_tests/utils/metrics.py | 144 ++++++++++++++++++ tests/snappi_tests/utils/reporter_factory.py | 20 +++ 7 files changed, 244 insertions(+), 186 deletions(-) delete mode 100644 tests/snappi_tests/metrics_utils/examples.py delete mode 100644 tests/snappi_tests/metrics_utils/metrics_accepter.py delete mode 100644 tests/snappi_tests/metrics_utils/metrics_reporter.py create mode 100644 tests/snappi_tests/utils/allowed_labels.json create mode 100644 tests/snappi_tests/utils/examples.py create mode 100644 tests/snappi_tests/utils/metrics.py create mode 100644 tests/snappi_tests/utils/reporter_factory.py diff --git a/tests/snappi_tests/metrics_utils/examples.py b/tests/snappi_tests/metrics_utils/examples.py deleted file mode 100644 index ac1d892245f..00000000000 --- a/tests/snappi_tests/metrics_utils/examples.py +++ /dev/null @@ -1,62 +0,0 @@ -import logging -import json -import datetime -import time - -from typing import List, Dict, Union -from metrics_accepter import Metric, GaugeMetric -from metrics_reporter import MetricReporterFactory, MetricsReporter - -def main(): - """ - - PSU Model Serial HW Rev Voltage (V) Current (A) Power (W) Status LED - ----- --------------- --------------- -------- ------------- ------------- ----------- -------- ----- - PSU 1 PWR-2422-HV-RED 6A011010142349Q 01 12.09 18.38 222.00 OK green - PSU 2 PWR-2422-HV-RED 6A011010142327X 01 12.10 17.72 214.00 OK green - - """ - resource_labels = { - "testbed.id": "sonic_stress_testbed", - "os.version": "11.2.3", - "testcase": "stress_test1", - "testrun.id": "202412101217" - } - - # Create a MetricReporterFactory and build a MetricReporter - factory = MetricReporterFactory() - reporter = factory.create_metrics_reporter(resource_labels) - - scope_labels = { - "device.id": "str-7060x6-64pe-stress-02", - "psu.id": "psu1", - "model": "PWR-2422-HV-RED", - "serial": "6A011010142349Q"} - - # Create a metric and pass it to the reporter - vol = GaugeMetric(name = "Voltage", - description = "PSU voltage reading", - unit = "V", - reporter = reporter) - vol.set_gauge_metric(scope_labels, 12.09) - - # Create a metric and pass it to the reporter - cur = GaugeMetric(name = "Current", - description = "PSU current reading", - unit = "A", - reporter = reporter) - cur.set_gauge_metric(scope_labels, 18.38) - - # Create a metric and pass it to the reporter - power = GaugeMetric(name = "Power", - description = "PSU power reading", - unit = "W", - reporter = reporter) - power.set_gauge_metric(scope_labels, 222.00) - - # Report all metrics at a specific timestamp - reporter.report() - -if __name__ == '__main__': - main() - diff --git a/tests/snappi_tests/metrics_utils/metrics_accepter.py b/tests/snappi_tests/metrics_utils/metrics_accepter.py deleted file mode 100644 index fd35fac4d3a..00000000000 --- a/tests/snappi_tests/metrics_utils/metrics_accepter.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -This file defines the classes accepting metrics and records from snappi tests. -IMPORTANT: Please use the standard labels: -testbed.id, os.version, testcase, testrun.id, device.id, psu.id, port.id, sensor.id -""" - -import logging -import json -import datetime -import time - -from typing import List, Dict, Union -from metrics_reporter import MetricReporterFactory, MetricsReporter - -class Metric: - def __init__(self, - name: str, - description: str, - unit: str, - reporter: MetricReporterFactory): - """ - Args: - name (str): metric name (e.g., psu power, sensor temperature, port stats, etc.) - description (str): brief description of the metric - unit (str): metric unit (e.g., seconds, bytes) - reporter (MetricReporterFactory): object of MetricReporterFactory - """ - self.name = name - self.description = description - self.unit = unit - self.reporter = reporter - - def __repr__(self): - return (f"Metric(name={self.name!r}, " - f"description={self.description!r}, " - f"unit={self.unit!r}, " - f"reporter={self.reporter!r})") - - -class GaugeMetric(Metric): - def __init__(self, - name: str, - description: str, - unit: str, - reporter: MetricReporterFactory): - # Initialize the base class - super().__init__(name, description, unit, reporter) - - def set_gauge_metric(self, scope_labels: Dict[str, str], value: Union[int, str, float]): - # Add scope level labels and set the metric value - gauge_metric = { - "name": self.name, - "description": self.description, - "unit": self.unit, - **scope_labels, # Add scope_labels to the dictionary - "value": value # Add the metric value - } - self.reporter.update_metrics(gauge_metric) - - def __repr__(self): - return (f"GaugeMetric(name={self.name!r}, " - f"description={self.description!r}, " - f"unit={self.unit!r}, " - f"reporter={self.reporter!r})") diff --git a/tests/snappi_tests/metrics_utils/metrics_reporter.py b/tests/snappi_tests/metrics_utils/metrics_reporter.py deleted file mode 100644 index 78858efd2b2..00000000000 --- a/tests/snappi_tests/metrics_utils/metrics_reporter.py +++ /dev/null @@ -1,60 +0,0 @@ -# This file defines the classes reporting metrics and records to the corresponding database. - -import logging -import json -import datetime -import time -from pprint import pprint -from typing import List, Dict, Union - -#from metrics_accepter import Metric, GaugeMetric - -class MetricReporterFactory: - def __init__(self): - self.reporter = None - - def create_metrics_reporter(self, resource_labels: Dict[str, str]): - self.reporter = MetricsReporter(resource_labels) - return self.reporter - - def create_records_reporter(self, resource_labels: Dict[str, str]): - self.reporter = RecordsReporter(resource_labels) - return self.reporter - - -class MetricsReporter: - def __init__(self, resource_labels: Dict[str, str]): - # Temporary code initializing a MetricsReporter - # will be replaced with a real initializer such as OpenTelemetry - self.resource_labels = resource_labels - self.timestamp = int(time.time() * 1_000_000_000) # epoch time in nanoseconds - self.metrics = [] - - def update_metrics(self, gauge_metric: Dict[str, Union[int, str, float]]): - # add a new metric - self.metrics.append(gauge_metric) - - def report(self): - """ - Abstract method to report metrics at a given timestamp. - Subclasses must override this method. - pprint(self.metrics) - """ - pass - - -class RecordsReporter: - def __init__(self, resource_labels: Dict[str, str]): - # Temporary code initializing a RecordsReporter - # will be replaced with a real initializer such as Kusto - self.resource_labels = resource_labels - self.timestamp = int(time.time() * 1_000_000_000) # epoch time in nanoseconds - self.records = [] - - def report(self): - """ - Abstract method to report records at a given timestamp. - Subclasses must override this method. - """ - pass - diff --git a/tests/snappi_tests/utils/allowed_labels.json b/tests/snappi_tests/utils/allowed_labels.json new file mode 100644 index 00000000000..d53fa67c558 --- /dev/null +++ b/tests/snappi_tests/utils/allowed_labels.json @@ -0,0 +1,13 @@ +{ + "allowed_labels": [ + "testbed.id", + "os.version", + "testrun.id", + "testcase", + "device.id", + "psu.id", + "port.id", + "sensor.id", + "queue.id" + ] +} diff --git a/tests/snappi_tests/utils/examples.py b/tests/snappi_tests/utils/examples.py new file mode 100644 index 00000000000..5236791c430 --- /dev/null +++ b/tests/snappi_tests/utils/examples.py @@ -0,0 +1,67 @@ +import logging +import json +import datetime +import time + +from typing import List, Dict, Union +from metrics import GaugeMetric, MetricsReporter +from reporter_factory import TelemetryReporterFactory + +def main(): + """ + + PSU Model Serial HW Rev Voltage (V) Current (A) Power (W) Status LED + ----- --------------- --------------- -------- ------------- ------------- ----------- -------- ----- + PSU 1 PWR-ABCD 1Z011010112349Q 01 12.09 18.38 222.00 OK green + PSU 2 PWR-ABCD 1Z011010156787X 01 12.10 17.72 214.00 OK green + + """ + resource_labels = { + "testbed.id": "sonic_stress_testbed", + "os.version": "11.2.3", + "testcase": "stress_test1", + "testrun.id": "202412101217" + } + + # Create a MetricReporterFactory and build a MetricReporter + factory = TelemetryReporterFactory() + reporter = factory.create_metrics_reporter(resource_labels) + + scope_labels = {"device.id": "switch-A"} + + # Create a metric + voltage = GaugeMetric(name = "Voltage", + description = "Power supply unit voltage reading", + unit = "V", + reporter = reporter) + + # Create a metric + current = GaugeMetric(name = "Current", + description = "Power supply unit current reading", + unit = "A", + reporter = reporter) + + # Create a metric + power = GaugeMetric(name = "Power", + description = "Power supply unit power reading", + unit = "W", + reporter = reporter) + + # Pass metrics to the reporter + scope_labels["psu.id"] = "PSU 1" + voltage.record(scope_labels, 12.09) + current.record(scope_labels, 18.38) + power.record(scope_labels, 222.00) + + # Pass metrics to the reporter + scope_labels["psu.id"] = "PSU 2" + voltage.record(scope_labels, 12.10) + current.record(scope_labels, 17.72) + power.record(scope_labels, 214.00) + + # Report all metrics at a specific timestamp + reporter.report() + +if __name__ == '__main__': + main() + diff --git a/tests/snappi_tests/utils/metrics.py b/tests/snappi_tests/utils/metrics.py new file mode 100644 index 00000000000..b96625b9281 --- /dev/null +++ b/tests/snappi_tests/utils/metrics.py @@ -0,0 +1,144 @@ +""" +This file defines the classes receiving metrics and test results from snappi tests. +""" + +import logging +import json +import datetime +import time + +from pprint import pprint +from typing import List, Dict, Union + +# Function to load allowed labels from a JSON file +def load_allowed_labels(filename="allowed_labels.json"): + with open(filename, "r") as f: + data = json.load(f) + # print(data) + return set(data["allowed_labels"]) +allowed_labels = load_allowed_labels() + +class MetricsReporter: + def __init__(self, resource_labels: Dict[str, str]): + for label in resource_labels: + if label not in allowed_labels: + raise LabelError(f"Invalid label: {label}.") + + # Temporary code initializing a MetricsReporter + # will be replaced with a real initializer such as OpenTelemetry + self.resource_labels = resource_labels + self.metrics = [] + + def stash_metric(self, new_metric: 'GaugeMetric', labels: Dict[str, str], value: Union[int, str, float]): + # add a new metric + self.metrics.append({"labels": labels, "value": value}) + + def report(self, timestamp=None): + """ + Abstract method to report metrics at a given timestamp. + Subclasses must override this method. + The input timestamp must be UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970 + pass + """ + if timestamp is not None: + current_time = timestamp + else: + current_time = time.time_ns() + + # save the metrics in a local variable and release the metrics in the object + stashed_metrics = self.metrics + self.metrics = [] + + """ + print(f"Current time (ns): {current_time}") + pprint(self.resource_labels) + pprint(stashed_metrics) + process_stashed_metrics(current_time, stashed_metrics) + """ + + +class TestResultsReporter: + def __init__(self, resource_labels: Dict[str, str]): + for label in resource_labels: + if label not in allowed_labels: + raise LabelError(f"Invalid label: {label}.") + + # Temporary code initializing a TestResultsReporter + # will be replaced with a real initializer such as Kusto + self.resource_labels = resource_labels + self.test_results = [] + + def stash_test_results(self, labels: Dict[str, str], value: Union[int, str, float]): + # add a new test result + self.test_results.append({"labels": labels, "value": value}) + + def report(self, timestamp=None): + """ + Abstract method to report test results at a given timestamp. + Subclasses must override this method. + The input timestamp must be UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970 + """ + if timestamp is not None: + current_time = timestamp + else: + current_time = time.time_ns() + + # save the test results in a local variable and release the test results in the object + stashed_test_results = self.test_results + self.test_results = [] + + """ + print(f"Current time (ns): {current_time}") + pprint(self.resource_labels) + pprint(self.test_results) + process_stashed_test_results(current_time, stashed_test_results) + """ + + +class Metric: + def __init__(self, + name: str, + description: str, + unit: str, + reporter: MetricsReporter): + """ + Args: + name (str): metric name (e.g., psu power, sensor temperature, port stats, etc.) + description (str): brief description of the metric + unit (str): metric unit (e.g., seconds, bytes) + reporter (MetricsReporter): object of MetricsReporter + """ + self.name = name + self.description = description + self.unit = unit + self.reporter = reporter + + def __repr__(self): + return (f"Metric(name={self.name!r}, " + f"description={self.description!r}, " + f"unit={self.unit!r}, " + f"reporter=repr(self.reporter))") + + +class GaugeMetric(Metric): + def __init__(self, + name: str, + description: str, + unit: str, + reporter: MetricsReporter): + # Initialize the base class + super().__init__(name, description, unit, reporter) + + def record(self, scope_labels: Dict[str, str], value: Union[int, str, float]): + for label in scope_labels: + if label not in allowed_labels: + raise LabelError(f"Invalid label: {label}.") + + # Save the metric into the reporter + self.reporter.stash_metric(self, scope_labels, value) + + def __repr__(self): + return (f"GaugeMetric(name={self.name!r}, " + f"description={self.description!r}, " + f"unit={self.unit!r}, " + f"reporter=repr(self.reporter))") diff --git a/tests/snappi_tests/utils/reporter_factory.py b/tests/snappi_tests/utils/reporter_factory.py new file mode 100644 index 00000000000..3c62f2fb7fe --- /dev/null +++ b/tests/snappi_tests/utils/reporter_factory.py @@ -0,0 +1,20 @@ + +import logging +import json +import datetime +import time +from typing import List, Dict, Union + +from metrics import MetricsReporter, TestResultsReporter + +class TelemetryReporterFactory: + def __init__(self): + return + + def create_metrics_reporter(self, resource_labels: Dict[str, str]): + return (MetricsReporter(resource_labels)) + + def create_test_results_reporter(self, resource_labels: Dict[str, str]): + return (RecordsReporter(resource_labels)) + + From 9650f9d1638148e06d017068c31d802e10110fc2 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Sat, 21 Dec 2024 03:09:01 +0000 Subject: [PATCH 07/12] update Dec.20 --- tests/snappi_tests/utils/allowed_labels.json | 13 -- tests/snappi_tests/utils/examples.py | 28 +++-- tests/snappi_tests/utils/metrics.py | 118 +++++++------------ tests/snappi_tests/utils/reporter_factory.py | 15 +-- 4 files changed, 66 insertions(+), 108 deletions(-) delete mode 100644 tests/snappi_tests/utils/allowed_labels.json diff --git a/tests/snappi_tests/utils/allowed_labels.json b/tests/snappi_tests/utils/allowed_labels.json deleted file mode 100644 index d53fa67c558..00000000000 --- a/tests/snappi_tests/utils/allowed_labels.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "allowed_labels": [ - "testbed.id", - "os.version", - "testrun.id", - "testcase", - "device.id", - "psu.id", - "port.id", - "sensor.id", - "queue.id" - ] -} diff --git a/tests/snappi_tests/utils/examples.py b/tests/snappi_tests/utils/examples.py index 5236791c430..5affaa2b526 100644 --- a/tests/snappi_tests/utils/examples.py +++ b/tests/snappi_tests/utils/examples.py @@ -3,31 +3,33 @@ import datetime import time -from typing import List, Dict, Union -from metrics import GaugeMetric, MetricsReporter +from typing import Dict, Final, List, Union +from metrics import * from reporter_factory import TelemetryReporterFactory + def main(): """ PSU Model Serial HW Rev Voltage (V) Current (A) Power (W) Status LED ----- --------------- --------------- -------- ------------- ------------- ----------- -------- ----- PSU 1 PWR-ABCD 1Z011010112349Q 01 12.09 18.38 222.00 OK green - PSU 2 PWR-ABCD 1Z011010156787X 01 12.10 17.72 214.00 OK green + PSU 2 PWR-ABCD 1Z011010156787X 01 12.01 17.72 214.00 OK green """ resource_labels = { - "testbed.id": "sonic_stress_testbed", - "os.version": "11.2.3", - "testcase": "stress_test1", - "testrun.id": "202412101217" + METRIC_LABEL_TEST_TESTBED: "TB-XYZ", + METRIC_LABEL_TEST_BUILD: "2024.1103", + METRIC_LABEL_TEST_CASE: "mock-case", + METRIC_LABEL_TEST_FILE: "mock-test.py", + METRIC_LABEL_TEST_JOBID: "2024_1225_0621" } # Create a MetricReporterFactory and build a MetricReporter factory = TelemetryReporterFactory() - reporter = factory.create_metrics_reporter(resource_labels) + reporter = factory.create_periodic_metrics_reporter(resource_labels) - scope_labels = {"device.id": "switch-A"} + scope_labels = {METRIC_LABEL_DEVICE_ID: "switch-A"} # Create a metric voltage = GaugeMetric(name = "Voltage", @@ -48,20 +50,20 @@ def main(): reporter = reporter) # Pass metrics to the reporter - scope_labels["psu.id"] = "PSU 1" + scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 1" voltage.record(scope_labels, 12.09) current.record(scope_labels, 18.38) power.record(scope_labels, 222.00) # Pass metrics to the reporter - scope_labels["psu.id"] = "PSU 2" - voltage.record(scope_labels, 12.10) + scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 2" + voltage.record(scope_labels, 12.01) current.record(scope_labels, 17.72) power.record(scope_labels, 214.00) # Report all metrics at a specific timestamp reporter.report() + if __name__ == '__main__': main() - diff --git a/tests/snappi_tests/utils/metrics.py b/tests/snappi_tests/utils/metrics.py index b96625b9281..ec7be48b6fc 100644 --- a/tests/snappi_tests/utils/metrics.py +++ b/tests/snappi_tests/utils/metrics.py @@ -1,98 +1,74 @@ """ -This file defines the classes receiving metrics and test results from snappi tests. +This file defines the classes receiving metrics from snappi tests and processing them. """ - import logging import json import datetime import time +from copy import deepcopy from pprint import pprint -from typing import List, Dict, Union - -# Function to load allowed labels from a JSON file -def load_allowed_labels(filename="allowed_labels.json"): - with open(filename, "r") as f: - data = json.load(f) - # print(data) - return set(data["allowed_labels"]) -allowed_labels = load_allowed_labels() - -class MetricsReporter: +from typing import Dict, Final, List, Union + +# Only certain labels are allowed +METRIC_LABEL_TEST_TESTBED: Final[str] = "test.testbed" +METRIC_LABEL_TEST_BUILD: Final[str] = "test.os.version" +METRIC_LABEL_TEST_CASE: Final[str] = "test.testcase" +METRIC_LABEL_TEST_FILE: Final[str] = "test.test_file" +METRIC_LABEL_TEST_JOBID: Final[str] = "test.job_id" +METRIC_LABEL_DEVICE_ID: Final[str] = "device.id" +METRIC_LABEL_DEVICE_PORT_ID: Final[str] = "device.port.id" +METRIC_LABEL_DEVICE_PSU_ID: Final[str] = "device.psu.id" +METRIC_LABEL_DEVICE_QUEUE_ID: Final[str] = "device.queue.id" +METRIC_LABEL_DEVICE_SENSOR_ID: Final[str] = "device.sensor.id" + + +class PeriodicMetricsReporter: def __init__(self, resource_labels: Dict[str, str]): - for label in resource_labels: - if label not in allowed_labels: - raise LabelError(f"Invalid label: {label}.") - - # Temporary code initializing a MetricsReporter - # will be replaced with a real initializer such as OpenTelemetry - self.resource_labels = resource_labels + # Will be replaced with a real initializer such as OpenTelemetry + self.resource_labels = deepcopy(resource_labels) self.metrics = [] - def stash_metric(self, new_metric: 'GaugeMetric', labels: Dict[str, str], value: Union[int, str, float]): - # add a new metric - self.metrics.append({"labels": labels, "value": value}) + def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, str, float]): + # add a new periodic metric + copied_labels = deepcopy(labels) + self.metrics.append({"labels": copied_labels, "value": value}) - def report(self, timestamp=None): + def report(self, timestamp = time.time_ns()): """ - Abstract method to report metrics at a given timestamp. - Subclasses must override this method. + Report metrics at a given timestamp. The input timestamp must be UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970 - pass - """ - if timestamp is not None: - current_time = timestamp - else: - current_time = time.time_ns() # save the metrics in a local variable and release the metrics in the object stashed_metrics = self.metrics self.metrics = [] """ - print(f"Current time (ns): {current_time}") - pprint(self.resource_labels) - pprint(stashed_metrics) - process_stashed_metrics(current_time, stashed_metrics) - """ + pass -class TestResultsReporter: +class FinalMetricsReporter: def __init__(self, resource_labels: Dict[str, str]): - for label in resource_labels: - if label not in allowed_labels: - raise LabelError(f"Invalid label: {label}.") - - # Temporary code initializing a TestResultsReporter - # will be replaced with a real initializer such as Kusto - self.resource_labels = resource_labels - self.test_results = [] + # Will be replaced with a real initializer such as Kusto + self.resource_labels = deepcopy(resource_labels) + self.metrics = [] - def stash_test_results(self, labels: Dict[str, str], value: Union[int, str, float]): - # add a new test result - self.test_results.append({"labels": labels, "value": value}) + def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, str, float]): + # add a new final metric + copied_labels = deepcopy(labels) + self.metrics.append({"labels": copied_labels, "value": value}) - def report(self, timestamp=None): + def report(self, timestamp = time.time_ns()): """ - Abstract method to report test results at a given timestamp. - Subclasses must override this method. + Report metrics at a given timestamp. The input timestamp must be UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970 - """ - if timestamp is not None: - current_time = timestamp - else: - current_time = time.time_ns() - # save the test results in a local variable and release the test results in the object - stashed_test_results = self.test_results - self.test_results = [] + # save the metrics in a local variable and release the metrics in the object + stashed_metrics = self.metrics + self.metrics = [] """ - print(f"Current time (ns): {current_time}") - pprint(self.resource_labels) - pprint(self.test_results) - process_stashed_test_results(current_time, stashed_test_results) - """ + pass class Metric: @@ -100,13 +76,13 @@ def __init__(self, name: str, description: str, unit: str, - reporter: MetricsReporter): + reporter: PeriodicMetricsReporter): """ Args: name (str): metric name (e.g., psu power, sensor temperature, port stats, etc.) description (str): brief description of the metric unit (str): metric unit (e.g., seconds, bytes) - reporter (MetricsReporter): object of MetricsReporter + reporter (PeriodicMetricsReporter): object of PeriodicMetricsReporter """ self.name = name self.description = description @@ -125,17 +101,13 @@ def __init__(self, name: str, description: str, unit: str, - reporter: MetricsReporter): + reporter: PeriodicMetricsReporter): # Initialize the base class super().__init__(name, description, unit, reporter) def record(self, scope_labels: Dict[str, str], value: Union[int, str, float]): - for label in scope_labels: - if label not in allowed_labels: - raise LabelError(f"Invalid label: {label}.") - # Save the metric into the reporter - self.reporter.stash_metric(self, scope_labels, value) + self.reporter.stash_record(self, scope_labels, value) def __repr__(self): return (f"GaugeMetric(name={self.name!r}, " diff --git a/tests/snappi_tests/utils/reporter_factory.py b/tests/snappi_tests/utils/reporter_factory.py index 3c62f2fb7fe..e817e2ecdf7 100644 --- a/tests/snappi_tests/utils/reporter_factory.py +++ b/tests/snappi_tests/utils/reporter_factory.py @@ -1,20 +1,17 @@ - import logging import json import datetime import time -from typing import List, Dict, Union -from metrics import MetricsReporter, TestResultsReporter +from typing import List, Dict, Union +from metrics import PeriodicMetricsReporter, FinalMetricsReporter class TelemetryReporterFactory: def __init__(self): return - def create_metrics_reporter(self, resource_labels: Dict[str, str]): - return (MetricsReporter(resource_labels)) - - def create_test_results_reporter(self, resource_labels: Dict[str, str]): - return (RecordsReporter(resource_labels)) - + def create_periodic_metrics_reporter(self, resource_labels: Dict[str, str]): + return (PeriodicMetricsReporter(resource_labels)) + def create_final_metrics_reporter(self, resource_labels: Dict[str, str]): + return (FinalMetricsReporter(resource_labels)) From 9e0a294cc9bf277a20ec58b3786e14bae10a61a2 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Tue, 24 Dec 2024 21:35:24 +0000 Subject: [PATCH 08/12] update on 12/24 --- tests/common/telemetry/examples.py | 78 ++++++++++++++++++++ tests/snappi_tests/utils/metrics.py | 28 ++++--- tests/snappi_tests/utils/reporter_factory.py | 2 +- 3 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 tests/common/telemetry/examples.py diff --git a/tests/common/telemetry/examples.py b/tests/common/telemetry/examples.py new file mode 100644 index 00000000000..821d89ef33d --- /dev/null +++ b/tests/common/telemetry/examples.py @@ -0,0 +1,78 @@ +import logging +import json +import datetime +import time +import sys +import os +from typing import Dict, Final, List, Union + +# Add the root directory of the project to sys.path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + +from snappi_tests.utils.metrics import * +from snappi_tests.utils.reporter_factory import TelemetryReporterFactory + + +def main(): + """ + + PSU Model Serial HW Rev Voltage (V) Current (A) Power (W) Status LED + ----- --------------- --------------- -------- ------------- ------------- ----------- -------- ----- + PSU 1 PWR-ABCD 1Z011010112349Q 01 12.09 18.38 222.00 OK green + PSU 2 PWR-ABCD 1Z011010156787X 01 12.01 17.72 214.00 OK green + + """ + resource_labels = { + METRIC_LABEL_TESTBED: "TB-XYZ", + METRIC_LABEL_TEST_BUILD: "2024.1103", + METRIC_LABEL_TEST_CASE: "mock-case", + METRIC_LABEL_TEST_FILE: "mock-test.py", + METRIC_LABEL_TEST_JOBID: "2024_1225_0621" + } + + # Create a MetricReporterFactory and build a MetricReporter + factory = TelemetryReporterFactory() + reporter = factory.create_periodic_metrics_reporter(resource_labels) + + scope_labels = {METRIC_LABEL_DEVICE_ID: "switch-A"} + + # Create a metric + voltage = GaugeMetric(name = "Voltage", + description = "Power supply unit voltage reading", + unit = "V", + reporter = reporter) + + # Create a metric + current = GaugeMetric(name = "Current", + description = "Power supply unit current reading", + unit = "A", + reporter = reporter) + + # Create a metric + power = GaugeMetric(name = "Power", + description = "Power supply unit power reading", + unit = "W", + reporter = reporter) + + # Pass metrics to the reporter + scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 1" + scope_labels[METRIC_LABEL_COMPONENT_MODEL] = "PWR-ABCD" + scope_labels[METRIC_LABEL_COMPONENT_SERIAL] = "1Z011010112349Q" + voltage.record(scope_labels, 12.09) + current.record(scope_labels, 18.38) + power.record(scope_labels, 222.00) + + # Pass metrics to the reporter + scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 2" + scope_labels[METRIC_LABEL_COMPONENT_MODEL] = "PWR-ABCD" + scope_labels[METRIC_LABEL_COMPONENT_SERIAL] = "1Z011010156787X" + voltage.record(scope_labels, 12.01) + current.record(scope_labels, 17.72) + power.record(scope_labels, 214.00) + + # Report all metrics at a specific timestamp + reporter.report() + + +if __name__ == '__main__': + main() diff --git a/tests/snappi_tests/utils/metrics.py b/tests/snappi_tests/utils/metrics.py index ec7be48b6fc..28768eeca82 100644 --- a/tests/snappi_tests/utils/metrics.py +++ b/tests/snappi_tests/utils/metrics.py @@ -11,16 +11,22 @@ from typing import Dict, Final, List, Union # Only certain labels are allowed -METRIC_LABEL_TEST_TESTBED: Final[str] = "test.testbed" -METRIC_LABEL_TEST_BUILD: Final[str] = "test.os.version" -METRIC_LABEL_TEST_CASE: Final[str] = "test.testcase" -METRIC_LABEL_TEST_FILE: Final[str] = "test.test_file" -METRIC_LABEL_TEST_JOBID: Final[str] = "test.job_id" -METRIC_LABEL_DEVICE_ID: Final[str] = "device.id" +METRIC_LABEL_TESTBED: Final[str] = "testbed" +METRIC_LABEL_TEST_BUILD: Final[str] = "os.version" +METRIC_LABEL_TEST_CASE: Final[str] = "testcase" +METRIC_LABEL_TEST_FILE: Final[str] = "test_file" +METRIC_LABEL_TEST_JOBID: Final[str] = "job_id" +METRIC_LABEL_DEVICE_ID: Final[str] = "device.id" # device refers to the level of switch METRIC_LABEL_DEVICE_PORT_ID: Final[str] = "device.port.id" METRIC_LABEL_DEVICE_PSU_ID: Final[str] = "device.psu.id" METRIC_LABEL_DEVICE_QUEUE_ID: Final[str] = "device.queue.id" METRIC_LABEL_DEVICE_SENSOR_ID: Final[str] = "device.sensor.id" +METRIC_LABEL_COMPONENT_MODEL: Final[str] = "model" # component refers to the level below, i.e. parts used by a switch +METRIC_LABEL_COMPONENT_SERIAL: Final[str] = "serial" +METRIC_LABEL_CAST_DIRECTION: Final[str] = "cast_direction" # unicast or multicast +METRIC_LABEL_HARDWARE_REVISION: Final[str] = "hardware.revision" +METRIC_LABEL_PRIORITY_GROUP: Final[str] = "priority_group" +METRIC_LABEL_BUFFER_POOL: Final[str] = "buffer_pool" class PeriodicMetricsReporter: @@ -29,7 +35,7 @@ def __init__(self, resource_labels: Dict[str, str]): self.resource_labels = deepcopy(resource_labels) self.metrics = [] - def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, str, float]): + def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, float]): # add a new periodic metric copied_labels = deepcopy(labels) self.metrics.append({"labels": copied_labels, "value": value}) @@ -53,7 +59,7 @@ def __init__(self, resource_labels: Dict[str, str]): self.resource_labels = deepcopy(resource_labels) self.metrics = [] - def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, str, float]): + def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, float]): # add a new final metric copied_labels = deepcopy(labels) self.metrics.append({"labels": copied_labels, "value": value}) @@ -93,7 +99,7 @@ def __repr__(self): return (f"Metric(name={self.name!r}, " f"description={self.description!r}, " f"unit={self.unit!r}, " - f"reporter=repr(self.reporter))") + f"reporter={self.reporter})") class GaugeMetric(Metric): @@ -105,7 +111,7 @@ def __init__(self, # Initialize the base class super().__init__(name, description, unit, reporter) - def record(self, scope_labels: Dict[str, str], value: Union[int, str, float]): + def record(self, scope_labels: Dict[str, str], value: Union[int, float]): # Save the metric into the reporter self.reporter.stash_record(self, scope_labels, value) @@ -113,4 +119,4 @@ def __repr__(self): return (f"GaugeMetric(name={self.name!r}, " f"description={self.description!r}, " f"unit={self.unit!r}, " - f"reporter=repr(self.reporter))") + f"reporter={self.reporter})") diff --git a/tests/snappi_tests/utils/reporter_factory.py b/tests/snappi_tests/utils/reporter_factory.py index e817e2ecdf7..fc3b1a5f3b2 100644 --- a/tests/snappi_tests/utils/reporter_factory.py +++ b/tests/snappi_tests/utils/reporter_factory.py @@ -4,7 +4,7 @@ import time from typing import List, Dict, Union -from metrics import PeriodicMetricsReporter, FinalMetricsReporter +from snappi_tests.utils.metrics import PeriodicMetricsReporter, FinalMetricsReporter class TelemetryReporterFactory: def __init__(self): From 602f06eda9ebb68f7da3cfed67a26c26b27babc9 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Fri, 3 Jan 2025 02:56:23 +0000 Subject: [PATCH 09/12] update 01/02 --- tests/common/telemetry/examples.py | 81 +++++++++---------- .../utils => common/telemetry}/metrics.py | 36 ++++----- tests/common/telemetry/reporter_factory.py | 13 +++ tests/snappi_tests/utils/examples.py | 69 ---------------- tests/snappi_tests/utils/reporter_factory.py | 17 ---- 5 files changed, 69 insertions(+), 147 deletions(-) rename tests/{snappi_tests/utils => common/telemetry}/metrics.py (75%) create mode 100644 tests/common/telemetry/reporter_factory.py delete mode 100644 tests/snappi_tests/utils/examples.py delete mode 100644 tests/snappi_tests/utils/reporter_factory.py diff --git a/tests/common/telemetry/examples.py b/tests/common/telemetry/examples.py index 821d89ef33d..3d7e9ff0adc 100644 --- a/tests/common/telemetry/examples.py +++ b/tests/common/telemetry/examples.py @@ -1,16 +1,16 @@ -import logging -import json -import datetime -import time -import sys -import os -from typing import Dict, Final, List, Union - -# Add the root directory of the project to sys.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) - -from snappi_tests.utils.metrics import * -from snappi_tests.utils.reporter_factory import TelemetryReporterFactory +from reporter_factory import TelemetryReporterFactory +from metrics import ( + GaugeMetric, + METRIC_LABEL_TESTBED, + METRIC_LABEL_TEST_BUILD, + METRIC_LABEL_TEST_CASE, + METRIC_LABEL_TEST_FILE, + METRIC_LABEL_TEST_JOBID, + METRIC_LABEL_DEVICE_ID, + METRIC_LABEL_DEVICE_PSU_ID, + METRIC_LABEL_DEVICE_PSU_MODEL, + METRIC_LABEL_DEVICE_PSU_SERIAL +) def main(): @@ -22,7 +22,7 @@ def main(): PSU 2 PWR-ABCD 1Z011010156787X 01 12.01 17.72 214.00 OK green """ - resource_labels = { + common_labels = { METRIC_LABEL_TESTBED: "TB-XYZ", METRIC_LABEL_TEST_BUILD: "2024.1103", METRIC_LABEL_TEST_CASE: "mock-case", @@ -31,44 +31,43 @@ def main(): } # Create a MetricReporterFactory and build a MetricReporter - factory = TelemetryReporterFactory() - reporter = factory.create_periodic_metrics_reporter(resource_labels) + reporter = TelemetryReporterFactory.create_periodic_metrics_reporter(common_labels) - scope_labels = {METRIC_LABEL_DEVICE_ID: "switch-A"} + metric_labels = {METRIC_LABEL_DEVICE_ID: "switch-A"} # Create a metric - voltage = GaugeMetric(name = "Voltage", - description = "Power supply unit voltage reading", - unit = "V", - reporter = reporter) + voltage = GaugeMetric(name="Voltage", + description="Power supply unit voltage reading", + unit="V", + reporter=reporter) # Create a metric - current = GaugeMetric(name = "Current", - description = "Power supply unit current reading", - unit = "A", - reporter = reporter) + current = GaugeMetric(name="Current", + description="Power supply unit current reading", + unit="A", + reporter=reporter) # Create a metric - power = GaugeMetric(name = "Power", - description = "Power supply unit power reading", - unit = "W", - reporter = reporter) + power = GaugeMetric(name="Power", + description="Power supply unit power reading", + unit="W", + reporter=reporter) # Pass metrics to the reporter - scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 1" - scope_labels[METRIC_LABEL_COMPONENT_MODEL] = "PWR-ABCD" - scope_labels[METRIC_LABEL_COMPONENT_SERIAL] = "1Z011010112349Q" - voltage.record(scope_labels, 12.09) - current.record(scope_labels, 18.38) - power.record(scope_labels, 222.00) + metric_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 1" + metric_labels[METRIC_LABEL_DEVICE_PSU_MODEL] = "PWR-ABCD" + metric_labels[METRIC_LABEL_DEVICE_PSU_SERIAL] = "1Z011010112349Q" + voltage.record(metric_labels, 12.09) + current.record(metric_labels, 18.38) + power.record(metric_labels, 222.00) # Pass metrics to the reporter - scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 2" - scope_labels[METRIC_LABEL_COMPONENT_MODEL] = "PWR-ABCD" - scope_labels[METRIC_LABEL_COMPONENT_SERIAL] = "1Z011010156787X" - voltage.record(scope_labels, 12.01) - current.record(scope_labels, 17.72) - power.record(scope_labels, 214.00) + metric_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 2" + metric_labels[METRIC_LABEL_DEVICE_PSU_MODEL] = "PWR-ABCD" + metric_labels[METRIC_LABEL_DEVICE_PSU_SERIAL] = "1Z011010156787X" + voltage.record(metric_labels, 12.01) + current.record(metric_labels, 17.72) + power.record(metric_labels, 214.00) # Report all metrics at a specific timestamp reporter.report() diff --git a/tests/snappi_tests/utils/metrics.py b/tests/common/telemetry/metrics.py similarity index 75% rename from tests/snappi_tests/utils/metrics.py rename to tests/common/telemetry/metrics.py index 28768eeca82..79d5a2fd051 100644 --- a/tests/snappi_tests/utils/metrics.py +++ b/tests/common/telemetry/metrics.py @@ -1,14 +1,10 @@ """ This file defines the classes receiving metrics from snappi tests and processing them. """ -import logging -import json -import datetime import time from copy import deepcopy -from pprint import pprint -from typing import Dict, Final, List, Union +from typing import Dict, Final, Union # Only certain labels are allowed METRIC_LABEL_TESTBED: Final[str] = "testbed" @@ -16,23 +12,23 @@ METRIC_LABEL_TEST_CASE: Final[str] = "testcase" METRIC_LABEL_TEST_FILE: Final[str] = "test_file" METRIC_LABEL_TEST_JOBID: Final[str] = "job_id" -METRIC_LABEL_DEVICE_ID: Final[str] = "device.id" # device refers to the level of switch +METRIC_LABEL_DEVICE_ID: Final[str] = "device.id" # device refers to the level of switch METRIC_LABEL_DEVICE_PORT_ID: Final[str] = "device.port.id" METRIC_LABEL_DEVICE_PSU_ID: Final[str] = "device.psu.id" +METRIC_LABEL_DEVICE_PSU_MODEL: Final[str] = "device.psu.model" +METRIC_LABEL_DEVICE_PSU_SERIAL: Final[str] = "device.psu.serial" +METRIC_LABEL_DEVICE_PSU_HW_REV: Final[str] = "device.psu.hw_rev" # hardware revision METRIC_LABEL_DEVICE_QUEUE_ID: Final[str] = "device.queue.id" +METRIC_LABEL_DEVICE_QUEUE_CAST: Final[str] = "device.queue.cast" # unicast or multicast METRIC_LABEL_DEVICE_SENSOR_ID: Final[str] = "device.sensor.id" -METRIC_LABEL_COMPONENT_MODEL: Final[str] = "model" # component refers to the level below, i.e. parts used by a switch -METRIC_LABEL_COMPONENT_SERIAL: Final[str] = "serial" -METRIC_LABEL_CAST_DIRECTION: Final[str] = "cast_direction" # unicast or multicast -METRIC_LABEL_HARDWARE_REVISION: Final[str] = "hardware.revision" -METRIC_LABEL_PRIORITY_GROUP: Final[str] = "priority_group" -METRIC_LABEL_BUFFER_POOL: Final[str] = "buffer_pool" +METRIC_LABEL_DEVICE_PG_ID: Final[str] = "device.pg.id" # priority group +METRIC_LABEL_DEVICE_BUFFER_POOL_ID: Final[str] = "device.buffer_pool.id" class PeriodicMetricsReporter: - def __init__(self, resource_labels: Dict[str, str]): + def __init__(self, common_labels: Dict[str, str]): # Will be replaced with a real initializer such as OpenTelemetry - self.resource_labels = deepcopy(resource_labels) + self.common_labels = deepcopy(common_labels) self.metrics = [] def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, float]): @@ -40,7 +36,7 @@ def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Unio copied_labels = deepcopy(labels) self.metrics.append({"labels": copied_labels, "value": value}) - def report(self, timestamp = time.time_ns()): + def report(self, timestamp=time.time_ns()): """ Report metrics at a given timestamp. The input timestamp must be UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970 @@ -54,9 +50,9 @@ def report(self, timestamp = time.time_ns()): class FinalMetricsReporter: - def __init__(self, resource_labels: Dict[str, str]): + def __init__(self, common_labels: Dict[str, str]): # Will be replaced with a real initializer such as Kusto - self.resource_labels = deepcopy(resource_labels) + self.common_labels = deepcopy(common_labels) self.metrics = [] def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Union[int, float]): @@ -64,7 +60,7 @@ def stash_record(self, new_metric: 'Metric', labels: Dict[str, str], value: Unio copied_labels = deepcopy(labels) self.metrics.append({"labels": copied_labels, "value": value}) - def report(self, timestamp = time.time_ns()): + def report(self, timestamp=time.time_ns()): """ Report metrics at a given timestamp. The input timestamp must be UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970 @@ -111,9 +107,9 @@ def __init__(self, # Initialize the base class super().__init__(name, description, unit, reporter) - def record(self, scope_labels: Dict[str, str], value: Union[int, float]): + def record(self, metric_labels: Dict[str, str], value: Union[int, float]): # Save the metric into the reporter - self.reporter.stash_record(self, scope_labels, value) + self.reporter.stash_record(self, metric_labels, value) def __repr__(self): return (f"GaugeMetric(name={self.name!r}, " diff --git a/tests/common/telemetry/reporter_factory.py b/tests/common/telemetry/reporter_factory.py new file mode 100644 index 00000000000..c4846b653d7 --- /dev/null +++ b/tests/common/telemetry/reporter_factory.py @@ -0,0 +1,13 @@ +from typing import Dict +from metrics import PeriodicMetricsReporter, FinalMetricsReporter + + +class TelemetryReporterFactory: + def __init__(self): + return + + def create_periodic_metrics_reporter(common_labels: Dict[str, str]): + return (PeriodicMetricsReporter(common_labels)) + + def create_final_metrics_reporter(common_labels: Dict[str, str]): + return (FinalMetricsReporter(common_labels)) diff --git a/tests/snappi_tests/utils/examples.py b/tests/snappi_tests/utils/examples.py deleted file mode 100644 index 5affaa2b526..00000000000 --- a/tests/snappi_tests/utils/examples.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging -import json -import datetime -import time - -from typing import Dict, Final, List, Union -from metrics import * -from reporter_factory import TelemetryReporterFactory - - -def main(): - """ - - PSU Model Serial HW Rev Voltage (V) Current (A) Power (W) Status LED - ----- --------------- --------------- -------- ------------- ------------- ----------- -------- ----- - PSU 1 PWR-ABCD 1Z011010112349Q 01 12.09 18.38 222.00 OK green - PSU 2 PWR-ABCD 1Z011010156787X 01 12.01 17.72 214.00 OK green - - """ - resource_labels = { - METRIC_LABEL_TEST_TESTBED: "TB-XYZ", - METRIC_LABEL_TEST_BUILD: "2024.1103", - METRIC_LABEL_TEST_CASE: "mock-case", - METRIC_LABEL_TEST_FILE: "mock-test.py", - METRIC_LABEL_TEST_JOBID: "2024_1225_0621" - } - - # Create a MetricReporterFactory and build a MetricReporter - factory = TelemetryReporterFactory() - reporter = factory.create_periodic_metrics_reporter(resource_labels) - - scope_labels = {METRIC_LABEL_DEVICE_ID: "switch-A"} - - # Create a metric - voltage = GaugeMetric(name = "Voltage", - description = "Power supply unit voltage reading", - unit = "V", - reporter = reporter) - - # Create a metric - current = GaugeMetric(name = "Current", - description = "Power supply unit current reading", - unit = "A", - reporter = reporter) - - # Create a metric - power = GaugeMetric(name = "Power", - description = "Power supply unit power reading", - unit = "W", - reporter = reporter) - - # Pass metrics to the reporter - scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 1" - voltage.record(scope_labels, 12.09) - current.record(scope_labels, 18.38) - power.record(scope_labels, 222.00) - - # Pass metrics to the reporter - scope_labels[METRIC_LABEL_DEVICE_PSU_ID] = "PSU 2" - voltage.record(scope_labels, 12.01) - current.record(scope_labels, 17.72) - power.record(scope_labels, 214.00) - - # Report all metrics at a specific timestamp - reporter.report() - - -if __name__ == '__main__': - main() diff --git a/tests/snappi_tests/utils/reporter_factory.py b/tests/snappi_tests/utils/reporter_factory.py deleted file mode 100644 index fc3b1a5f3b2..00000000000 --- a/tests/snappi_tests/utils/reporter_factory.py +++ /dev/null @@ -1,17 +0,0 @@ -import logging -import json -import datetime -import time - -from typing import List, Dict, Union -from snappi_tests.utils.metrics import PeriodicMetricsReporter, FinalMetricsReporter - -class TelemetryReporterFactory: - def __init__(self): - return - - def create_periodic_metrics_reporter(self, resource_labels: Dict[str, str]): - return (PeriodicMetricsReporter(resource_labels)) - - def create_final_metrics_reporter(self, resource_labels: Dict[str, str]): - return (FinalMetricsReporter(resource_labels)) From a4cf9fded1e50999b7e1237d88333094fa7d5d3d Mon Sep 17 00:00:00 2001 From: sm-xu Date: Tue, 7 Jan 2025 19:09:24 +0000 Subject: [PATCH 10/12] update 01/07 --- tests/common/telemetry/metrics.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/common/telemetry/metrics.py b/tests/common/telemetry/metrics.py index 79d5a2fd051..9dcd312a84a 100644 --- a/tests/common/telemetry/metrics.py +++ b/tests/common/telemetry/metrics.py @@ -7,11 +7,11 @@ from typing import Dict, Final, Union # Only certain labels are allowed -METRIC_LABEL_TESTBED: Final[str] = "testbed" -METRIC_LABEL_TEST_BUILD: Final[str] = "os.version" -METRIC_LABEL_TEST_CASE: Final[str] = "testcase" -METRIC_LABEL_TEST_FILE: Final[str] = "test_file" -METRIC_LABEL_TEST_JOBID: Final[str] = "job_id" +METRIC_LABEL_TESTBED: Final[str] = "test.testbed" # testbed name/ID +METRIC_LABEL_TEST_BUILD: Final[str] = "test.os.version" # Software/build version +METRIC_LABEL_TEST_CASE: Final[str] = "test.testcase" # test case name/ID +METRIC_LABEL_TEST_FILE: Final[str] = "test.file" # test file name +METRIC_LABEL_TEST_JOBID: Final[str] = "test.job.id" # test job ID METRIC_LABEL_DEVICE_ID: Final[str] = "device.id" # device refers to the level of switch METRIC_LABEL_DEVICE_PORT_ID: Final[str] = "device.port.id" METRIC_LABEL_DEVICE_PSU_ID: Final[str] = "device.psu.id" From 6fcf3552b62339f4ddf7ee920a69391d631f3586 Mon Sep 17 00:00:00 2001 From: sm-xu Date: Thu, 9 Jan 2025 20:43:34 +0000 Subject: [PATCH 11/12] update 01/09 --- tests/common/telemetry/reporter_factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/common/telemetry/reporter_factory.py b/tests/common/telemetry/reporter_factory.py index c4846b653d7..897f65ca56a 100644 --- a/tests/common/telemetry/reporter_factory.py +++ b/tests/common/telemetry/reporter_factory.py @@ -6,8 +6,10 @@ class TelemetryReporterFactory: def __init__(self): return + @staticmethod def create_periodic_metrics_reporter(common_labels: Dict[str, str]): return (PeriodicMetricsReporter(common_labels)) + @staticmethod def create_final_metrics_reporter(common_labels: Dict[str, str]): return (FinalMetricsReporter(common_labels)) From 31f56d5c0c852ab24d12e75aae7befcbf520251b Mon Sep 17 00:00:00 2001 From: sm-xu Date: Wed, 15 Jan 2025 20:40:42 +0000 Subject: [PATCH 12/12] update 01/15 --- {tests/common => test_reporting}/telemetry/examples.py | 0 {tests/common => test_reporting}/telemetry/metrics.py | 0 {tests/common => test_reporting}/telemetry/reporter_factory.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {tests/common => test_reporting}/telemetry/examples.py (100%) rename {tests/common => test_reporting}/telemetry/metrics.py (100%) rename {tests/common => test_reporting}/telemetry/reporter_factory.py (100%) diff --git a/tests/common/telemetry/examples.py b/test_reporting/telemetry/examples.py similarity index 100% rename from tests/common/telemetry/examples.py rename to test_reporting/telemetry/examples.py diff --git a/tests/common/telemetry/metrics.py b/test_reporting/telemetry/metrics.py similarity index 100% rename from tests/common/telemetry/metrics.py rename to test_reporting/telemetry/metrics.py diff --git a/tests/common/telemetry/reporter_factory.py b/test_reporting/telemetry/reporter_factory.py similarity index 100% rename from tests/common/telemetry/reporter_factory.py rename to test_reporting/telemetry/reporter_factory.py