.project
.pydevproject
.settings/
+
+# VS Code
+.vscode/
and this project adheres to `Semantic Versioning <http://semver.org/>`__.
+[1.5.0] - 2020-07-10
+--------------------
+* Add Metrics API (`RIC-381 <https://jira.o-ran-sc.org/browse/RIC-381>`_)
+
+
[1.4.0] - 2020-07-06
--------------------
* Revise Alarm manager to send via RMR wormhole (`RIC-529 <https://jira.o-ran-sc.org/browse/RIC-529>`_)
+
[1.3.0] - 2020-06-24
--------------------
* Add configuration-change API (`RIC-425 <https://jira.o-ran-sc.org/browse/RIC-425>`_)
--- /dev/null
+# ==================================================================================
+# Copyright (c) 2020 AT&T Intellectual Property.
+# Copyright (c) 2020 Nokia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==================================================================================
--- /dev/null
+# ==================================================================================
+# Copyright (c) 2020 AT&T Intellectual Property.
+# Copyright (c) 2020 Nokia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==================================================================================
+"""
+Custom Exceptions
+"""
+
+
+class EmptyReport(BaseException):
+ """No metric data items are present"""
--- /dev/null
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "properties": {
+ "reporter": {
+ "description": "the system reporting the metric",
+ "default": "the RMR source string (host:port)",
+ "type": "string"
+ },
+ "generator": {
+ "description": "the system which generated the metric",
+ "default": "reporter is assumed to be the generator if this is omitted",
+ "type": "string"
+ },
+ "timestamp": {
+ "description": "milliseconds past the UNIX epoch; e.g. 1594298319000 == 2020/07/09 12:38:39.000Z",
+ "default": "the message arrival time",
+ "type": "integer"
+ },
+ "data": {
+ "description": "one or more measurements",
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "measurement name",
+ "type": "string"
+ },
+ "type": {
+ "description": "future: measurement type such as counter, value, or similar",
+ "type": "string"
+ },
+ "value": {
+ "description": "actual value; treated as a double when forwarded",
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "value"
+ ]
+ }
+ ]
+ }
+ },
+ "required": [
+ "data"
+ ]
+}
--- /dev/null
+# ==================================================================================
+# Copyright (c) 2020 AT&T Intellectual Property.
+# Copyright (c) 2020 Nokia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==================================================================================
+"""
+Provides classes and methods to define and send metrics as RMR messages to a
+central collector. Message destination(s) are controlled by the RMR routing table.
+Message contents must comply with the JSON schema in file metric-schema.json.
+"""
+
+from ctypes import c_void_p
+import json
+import time
+from mdclogpy import Logger
+from ricxappframe.rmr import rmr
+from ricxappframe.metric.exceptions import EmptyReport
+
+##############
+# PRIVATE API
+##############
+
+mdc_logger = Logger(name=__name__)
+RETRIES = 4
+
+##############
+# PUBLIC API
+##############
+
+# constants
+RIC_METRICS = 120 # message type
+
+# Publish dict keys as constants for convenience of client code.
+KEY_REPORTER = "reporter"
+KEY_GENERATOR = "generator"
+KEY_TIMESTAMP = "timestamp"
+KEY_DATA = "data"
+KEY_DATA_ID = "id"
+KEY_DATA_TYPE = "type"
+KEY_DATA_VALUE = "value"
+
+
+class MetricData(dict):
+ """
+ A single measurement with ID, value and (optionally) type.
+ """
+ def __init__(self,
+ id: str,
+ value: str,
+ type: str = None):
+ """
+ Creates a data item with the specified members.
+
+ Parameters
+ ----------
+ id: str (required)
+ Metric ID
+
+ value: str (required)
+ Metric value; e.g., 1.
+
+ type: str (optional)
+ Metric type; e.g., "counter".
+ """
+ dict.__init__(self)
+ self[KEY_DATA_ID] = id
+ self[KEY_DATA_VALUE] = value
+ self[KEY_DATA_TYPE] = type
+
+
+class MetricsReport(dict):
+ """
+ A list of metric data items with identifying information.
+ At init sets the timestamp to the current system time in
+ milliseconds since the Epoch.
+
+ Parameters
+ ----------
+ reporter: str (optional)
+ The system that reports the data
+
+ generator: str (optional)
+ The generator that reports the data
+
+ items: List of MetricData (optional)
+ The data items for the report
+ """
+ def __init__(self,
+ reporter: str = None,
+ generator: str = None,
+ items: list = None):
+ """
+ Creates an object with the specified details and items.
+ """
+ dict.__init__(self)
+ self[KEY_REPORTER] = reporter
+ self[KEY_GENERATOR] = generator
+ self[KEY_TIMESTAMP] = int(round(time.time() * 1000))
+ self[KEY_DATA] = [] if items is None else items
+
+ def add_metric(self,
+ data: MetricData):
+ """
+ Convenience method that adds a data item to the report.
+
+ Parameters
+ ----------
+ data: MetricData
+ A measurement to add to the report
+ """
+ self[KEY_DATA].append(data)
+
+
+class MetricsManager:
+ """
+ Provides an API for an Xapp to build and send measurement reports
+ by sending messages via RMR routing to a metrics adapter/collector.
+
+ Parameters
+ ----------
+ vctx: ctypes c_void_p (required)
+ Pointer to RMR context obtained by initializing RMR.
+ The context is used to allocate space and send messages.
+
+ reporter: str (optional)
+ The source of the measurement; e.g., a temperature probe
+
+ generator: str (optional)
+ The system that collected and sent the measurement; e.g., an environment monitor.
+ """
+ def __init__(self,
+ vctx: c_void_p,
+ reporter: str = None,
+ generator: str = None):
+ """
+ Creates a metrics manager.
+ """
+ self.vctx = vctx
+ self.reporter = reporter
+ self.generator = generator
+
+ def create_report(self,
+ items: list = None):
+ """
+ Creates a MetricsReport object with the specified metrics data items.
+
+ Parameters
+ ----------
+ items: list (optional)
+ List of MetricData items
+
+ Returns
+ -------
+ MetricsReport
+ """
+ return MetricsReport(self.reporter, self.generator, items)
+
+ def send_report(self, msg: MetricsReport):
+ """
+ Serializes the MetricsReport dict to JSON and sends the result via RMR.
+ Raises an exception if the report has no MetricsData items.
+
+ Parameters
+ ----------
+ msg: MetricsReport (required)
+ Dictionary with measurement data to encode and send
+
+ Returns
+ -------
+ bool
+ True if the send succeeded (possibly with retries), False otherwise
+ """
+ if KEY_DATA not in msg or len(msg[KEY_DATA]) == 0:
+ raise EmptyReport
+ payload = json.dumps(msg).encode()
+ mdc_logger.debug("send_report: payload is {}".format(payload))
+ sbuf = rmr.rmr_alloc_msg(vctx=self.vctx, size=len(payload), payload=payload,
+ mtype=RIC_METRICS, gen_transaction_id=True)
+
+ for _ in range(0, RETRIES):
+ sbuf = rmr.rmr_send_msg(self.vctx, sbuf)
+ post_send_summary = rmr.message_summary(sbuf)
+ mdc_logger.debug("send_report: try {0} result is {1}".format(_, post_send_summary[rmr.RMR_MS_MSG_STATE]))
+ # stop trying if RMR does not indicate retry
+ if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_ERR_RETRY:
+ break
+
+ rmr.rmr_free_msg(sbuf)
+ if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_OK:
+ mdc_logger.warning("send_report: failed after {} retries".format(RETRIES))
+ return False
+
+ return True
+
+ def send_metrics(self, items: list):
+ """
+ Convenience method that creates a MetricsReport object with the specified
+ metrics data items and sends it to the metrics adapter/collector.
+
+ Parameters
+ ----------
+ items: list (required)
+ List of MetricData items
+
+ Returns
+ -------
+ bool
+ True if the send succeeded (possibly with retries), False otherwise
+ """
+ return self.send_report(self.create_report(items))
setup(
name="ricxappframe",
- version="1.4.0",
+ version="1.5.0",
packages=find_packages(exclude=["tests.*", "tests"]),
author="O-RAN Software Community",
- description="Xapp and RMR framework for python",
+ description="Xapp and RMR framework for Python",
url="https://gerrit.o-ran-sc.org/r/admin/repos/ric-plt/xapp-frame-py",
install_requires=["inotify_simple", "msgpack", "mdclogpy", "ricsdl>=2.0.3,<3.0.0"],
classifiers=[
# do NOT use localhost, seems unresolved on jenkins VMs
-# first 3 lines (port 4564) are used for xapp frame tests
-# next 4 lines (port 3563-3564) are used in the rmr submodule
+# first 4 lines (ports 4564, 4569) are used for xapp frame tests
+# last 4 lines (port 3563, 3564) are used in the rmr submodule
newrt|start
-mse| 60000 | -1 | 127.0.0.1:4564
-mse| 60001 | -1 | 127.0.0.1:4564
-mse| 100 | -1 | 127.0.0.1:4564
-mse| 0 | -1 | 127.0.0.1:3563
+mse| 60000 | -1 | 127.0.0.1:4564
+mse| 60001 | -1 | 127.0.0.1:4564
+mse| 100 | -1 | 127.0.0.1:4564
+mse| 120 | -1 | 127.0.0.1:4569
+mse| 0 | -1 | 127.0.0.1:3563
mse| 46656 | 777 | 127.0.0.1:3563
-mse| 1 | -1 | 127.0.0.1:3564
-mse| 2 | -1 | 127.0.0.1:3564
+mse| 1 | -1 | 127.0.0.1:3564
+mse| 2 | -1 | 127.0.0.1:3564
newrt|end
test alarm module teardown
"""
rmr.rmr_close(MRC_SEND)
+ rmr.rmr_close(MRC_RCV)
def test_alarm_set_get(monkeypatch):
--- /dev/null
+# =================================================================================2
+# Copyright (c) 2020 AT&T Intellectual Property.
+# Copyright (c) 2020 Nokia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==================================================================================
+import json
+import pytest
+import time
+from mdclogpy import Logger
+from ricxappframe.metric import metric
+from ricxappframe.metric.exceptions import EmptyReport
+from ricxappframe.metric.metric import MetricData, MetricsReport, MetricsManager
+from ricxappframe.rmr import rmr
+
+mdc_logger = Logger(name=__name__)
+MRC_SEND = None
+MRC_RCV = None
+SIZE = 256
+
+
+def setup_module():
+ """
+ test metric module setup
+ """
+ global MRC_SEND
+ MRC_SEND = rmr.rmr_init(b"4568", rmr.RMR_MAX_RCV_BYTES, 0x00)
+ while rmr.rmr_ready(MRC_SEND) == 0:
+ time.sleep(1)
+
+ global MRC_RCV
+ MRC_RCV = rmr.rmr_init(b"4569", rmr.RMR_MAX_RCV_BYTES, 0x00)
+ while rmr.rmr_ready(MRC_RCV) == 0:
+ time.sleep(1)
+
+ # let all the RMR output appear
+ time.sleep(2)
+ mdc_logger.debug("RMR instances initialized")
+
+
+def teardown_module():
+ """
+ test metric module teardown
+ """
+ mdc_logger.debug("Closing RMR instances")
+ rmr.rmr_close(MRC_SEND)
+ rmr.rmr_close(MRC_RCV)
+
+
+def test_metric_set_get(monkeypatch):
+ mdc_logger.debug("Testing set functions")
+ md = MetricData("id", "value", "type")
+ assert md[metric.KEY_DATA_ID] == "id"
+ assert md[metric.KEY_DATA_VALUE] == "value"
+ assert md[metric.KEY_DATA_TYPE] == "type"
+
+ mr = MetricsReport("reporter", "generator", [md])
+ assert mr[metric.KEY_REPORTER] == "reporter"
+ assert mr[metric.KEY_GENERATOR] == "generator"
+ assert len(mr[metric.KEY_DATA]) == 1
+ assert mr[metric.KEY_DATA][0] == md
+
+ mgr = metric.MetricsManager(MRC_SEND, "reporter", "generator")
+ assert mgr is not None
+ assert mgr.reporter == "reporter"
+ assert mgr.generator == "generator"
+
+ mr = MetricsReport("empty", "empty", [])
+ with pytest.raises(EmptyReport):
+ mgr.send_report(mr)
+
+
+def _receive_metric_rpt(rptr: str):
+ """
+ delays briefly, receives a message, checks the message type and reporter
+ """
+ time.sleep(0.5)
+ sbuf_rcv = rmr.rmr_alloc_msg(MRC_RCV, SIZE)
+ sbuf_rcv = rmr.rmr_torcv_msg(MRC_RCV, sbuf_rcv, 2000)
+ rcv_summary = rmr.message_summary(sbuf_rcv)
+ mdc_logger.debug("Receive result is {}".format(rcv_summary[rmr.RMR_MS_MSG_STATE]))
+ assert rcv_summary[rmr.RMR_MS_MSG_STATE] == rmr.RMR_OK
+ assert rcv_summary[rmr.RMR_MS_MSG_TYPE] == metric.RIC_METRICS
+ # parse JSON
+ data = json.loads(rcv_summary[rmr.RMR_MS_PAYLOAD].decode())
+ mdc_logger.debug("Received reporter {}".format(data[metric.KEY_REPORTER]))
+ assert data[metric.KEY_REPORTER] == rptr
+
+
+def test_metrics_manager():
+ """
+ test send functions and ensure a message arrives
+ """
+ mdc_logger.debug("Testing metrics-send functions")
+ rptr = "metr"
+
+ mgr = MetricsManager(MRC_SEND, rptr, "generator")
+ assert mgr is not None
+
+ md = MetricData("id", "value", "type")
+ assert md is not None
+
+ success = mgr.send_metrics([md])
+ assert success
+ _receive_metric_rpt(rptr)