Add metrics API
[ric-plt/xapp-frame-py.git] / ricxappframe / metric / metric.py
1 # ==================================================================================
2 #       Copyright (c) 2020 AT&T Intellectual Property.
3 #       Copyright (c) 2020 Nokia
4 #
5 #   Licensed under the Apache License, Version 2.0 (the "License");
6 #   you may not use this file except in compliance with the License.
7 #   You may obtain a copy of the License at
8 #
9 #          http://www.apache.org/licenses/LICENSE-2.0
10 #
11 #   Unless required by applicable law or agreed to in writing, software
12 #   distributed under the License is distributed on an "AS IS" BASIS,
13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 #   See the License for the specific language governing permissions and
15 #   limitations under the License.
16 # ==================================================================================
17 """
18 Provides classes and methods to define and send metrics as RMR messages to a
19 central collector. Message destination(s) are controlled by the RMR routing table.
20 Message contents must comply with the JSON schema in file metric-schema.json.
21 """
22
23 from ctypes import c_void_p
24 import json
25 import time
26 from mdclogpy import Logger
27 from ricxappframe.rmr import rmr
28 from ricxappframe.metric.exceptions import EmptyReport
29
30 ##############
31 # PRIVATE API
32 ##############
33
34 mdc_logger = Logger(name=__name__)
35 RETRIES = 4
36
37 ##############
38 # PUBLIC API
39 ##############
40
41 # constants
42 RIC_METRICS = 120  # message type
43
44 # Publish dict keys as constants for convenience of client code.
45 KEY_REPORTER = "reporter"
46 KEY_GENERATOR = "generator"
47 KEY_TIMESTAMP = "timestamp"
48 KEY_DATA = "data"
49 KEY_DATA_ID = "id"
50 KEY_DATA_TYPE = "type"
51 KEY_DATA_VALUE = "value"
52
53
54 class MetricData(dict):
55     """
56     A single measurement with ID, value and (optionally) type.
57     """
58     def __init__(self,
59                  id: str,
60                  value: str,
61                  type: str = None):
62         """
63         Creates a data item with the specified members.
64
65         Parameters
66         ----------
67         id: str (required)
68             Metric ID
69
70         value: str (required)
71             Metric value; e.g., 1.
72
73         type: str (optional)
74             Metric type; e.g., "counter".
75         """
76         dict.__init__(self)
77         self[KEY_DATA_ID] = id
78         self[KEY_DATA_VALUE] = value
79         self[KEY_DATA_TYPE] = type
80
81
82 class MetricsReport(dict):
83     """
84     A list of metric data items with identifying information.
85     At init sets the timestamp to the current system time in
86     milliseconds since the Epoch.
87
88     Parameters
89     ----------
90     reporter: str (optional)
91         The system that reports the data
92
93     generator: str (optional)
94         The generator that reports the data
95
96     items: List of MetricData (optional)
97         The data items for the report
98     """
99     def __init__(self,
100                  reporter: str = None,
101                  generator: str = None,
102                  items: list = None):
103         """
104         Creates an object with the specified details and items.
105         """
106         dict.__init__(self)
107         self[KEY_REPORTER] = reporter
108         self[KEY_GENERATOR] = generator
109         self[KEY_TIMESTAMP] = int(round(time.time() * 1000))
110         self[KEY_DATA] = [] if items is None else items
111
112     def add_metric(self,
113                    data: MetricData):
114         """
115         Convenience method that adds a data item to the report.
116
117         Parameters
118         ----------
119         data: MetricData
120             A measurement to add to the report
121         """
122         self[KEY_DATA].append(data)
123
124
125 class MetricsManager:
126     """
127     Provides an API for an Xapp to build and send measurement reports
128     by sending messages via RMR routing to a metrics adapter/collector.
129
130     Parameters
131     ----------
132     vctx: ctypes c_void_p (required)
133         Pointer to RMR context obtained by initializing RMR.
134         The context is used to allocate space and send messages.
135
136     reporter: str (optional)
137         The source of the measurement; e.g., a temperature probe
138
139     generator: str (optional)
140         The system that collected and sent the measurement; e.g., an environment monitor.
141     """
142     def __init__(self,
143                  vctx: c_void_p,
144                  reporter: str = None,
145                  generator: str = None):
146         """
147         Creates a metrics manager.
148         """
149         self.vctx = vctx
150         self.reporter = reporter
151         self.generator = generator
152
153     def create_report(self,
154                       items: list = None):
155         """
156         Creates a MetricsReport object with the specified metrics data items.
157
158         Parameters
159         ----------
160         items: list (optional)
161             List of MetricData items
162
163         Returns
164         -------
165         MetricsReport
166         """
167         return MetricsReport(self.reporter, self.generator, items)
168
169     def send_report(self, msg: MetricsReport):
170         """
171         Serializes the MetricsReport dict to JSON and sends the result via RMR.
172         Raises an exception if the report has no MetricsData items.
173
174         Parameters
175         ----------
176         msg: MetricsReport (required)
177             Dictionary with measurement data to encode and send
178
179         Returns
180         -------
181         bool
182             True if the send succeeded (possibly with retries), False otherwise
183         """
184         if KEY_DATA not in msg or len(msg[KEY_DATA]) == 0:
185             raise EmptyReport
186         payload = json.dumps(msg).encode()
187         mdc_logger.debug("send_report: payload is {}".format(payload))
188         sbuf = rmr.rmr_alloc_msg(vctx=self.vctx, size=len(payload), payload=payload,
189                                  mtype=RIC_METRICS, gen_transaction_id=True)
190
191         for _ in range(0, RETRIES):
192             sbuf = rmr.rmr_send_msg(self.vctx, sbuf)
193             post_send_summary = rmr.message_summary(sbuf)
194             mdc_logger.debug("send_report: try {0} result is {1}".format(_, post_send_summary[rmr.RMR_MS_MSG_STATE]))
195             # stop trying if RMR does not indicate retry
196             if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_ERR_RETRY:
197                 break
198
199         rmr.rmr_free_msg(sbuf)
200         if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_OK:
201             mdc_logger.warning("send_report: failed after {} retries".format(RETRIES))
202             return False
203
204         return True
205
206     def send_metrics(self, items: list):
207         """
208         Convenience method that creates a MetricsReport object with the specified
209         metrics data items and sends it to the metrics adapter/collector.
210
211         Parameters
212         ----------
213         items: list (required)
214             List of MetricData items
215
216         Returns
217         -------
218         bool
219             True if the send succeeded (possibly with retries), False otherwise
220         """
221         return self.send_report(self.create_report(items))