Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial version of defining the interfaces to accept metrics #15913

Merged
merged 12 commits into from
Jan 16, 2025
62 changes: 62 additions & 0 deletions tests/snappi_tests/metrics_utils/examples.py
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label name needs to be standarized for our test cases. otherwise, there is no way to build standard dashboards.

"psu.id": "psu1",
"model": "PWR-2422-HV-RED",
"serial": "6A011010142349Q"}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the sensitive data.

# 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    # Create a metric and pass it to the reporter
    vol = GaugeMetric(name = "Voltage",
                      description = "PSU voltage reading",
                      unit = "V",
                      reporter = reporter)

    # Create a metric and pass it to the reporter
    cur = GaugeMetric(name = "Current",
                      description = "PSU current reading",
                      unit = "A",
                      reporter = reporter)

    # Create a metric and pass it to the reporter
    power = GaugeMetric(name = "Power",
                        description = "PSU power reading",
                        unit = "W",
                        reporter = reporter)

    scope_labels["psu.id"] = "PSU 1"
    vol.set_gauge_metric(scope_labels, 12.09)
    cur.set_gauge_metric(scope_labels, 18.38)
    power.set_gauge_metric(scope_labels, 222.00)

    scope_labels["psu.id"] = "PSU 2"
    vol.set_gauge_metric(scope_labels, 12.10)
    cur.set_gauge_metric(scope_labels, 17.72)
    power.set_gauge_metric(scope_labels, 214.00)


# Report all metrics at a specific timestamp
reporter.report()

if __name__ == '__main__':
main()

64 changes: 64 additions & 0 deletions tests/snappi_tests/metrics_utils/metrics_accepter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename this file to metrics.py

Copy link
Contributor

@r12f r12f Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the point is to consider the usage:

from metrics_utils.metrics_accepter import Metric, GaugeMetric # The code looks weird here
from utils.metrics import GaugeMetric # This looks more nature
from metrics_utils.metrics import GaugeMetric # This works too.

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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not the factory.

"""
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})")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reporter might not be converted to string.



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]):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename function to record, we need to support multiple metrics.

# 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be better to save the data within the metrics until it is consumed by reporter. it will be easier and more performant for counter implementation.

or if we like to store the metrics into the reporter, we can save the metrics as below to make it more future proof:

def record(self, labels: Dict[str, str], value: Union[int, str, float]):
    self.reporter.stash_metric(self, labels, value)


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})")
60 changes: 60 additions & 0 deletions tests/snappi_tests/metrics_utils/metrics_reporter.py
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move factory to a dedicated file. for reporter, we can leave in this file or move to metrics.py, no strong opinion in that.

def __init__(self):
self.reporter = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not needed.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can time.time() have nanosecond resolution? better to double check the API.

self.metrics = []

def update_metrics(self, gauge_metric: Dict[str, Union[int, str, float]]):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename update_metrics to record accordingly.

# add a new metric
self.metrics.append(gauge_metric)

def report(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp parameter is missing and should be having a default value.

"""
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp should not be here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be report function parameter.

self.records = []

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need function to push records into the self.records list.

def report(self):
"""
Abstract method to report records at a given timestamp.
Subclasses must override this method.
"""
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

report function is usually written in this way:

def report(self):
    incoming_records = self.records
    self.records = []

    self.process_incoming_records(incoming_records)


Loading