blob: 0a340332ec407eaccd055325344b1bbe806190e8 [file] [log] [blame]
Chip Boling67b674a2019-02-08 11:42:18 -06001# Copyright 2017-present Open Networking Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Zack Williams84a71e92019-11-15 09:00:19 -070015from __future__ import absolute_import, division
Chip Boling67b674a2019-02-08 11:42:18 -060016import structlog
17import arrow
18from twisted.internet.task import LoopingCall
William Kurkianede82e92019-03-05 13:02:57 -050019from voltha_protos.events_pb2 import KpiEvent2, KpiEventType, MetricInformation, MetricMetaData
Devmalya Paul0d3abf02019-07-31 18:34:27 -040020from voltha_protos.events_pb2 import Event, EventType, EventCategory, EventSubCategory
William Kurkianede82e92019-03-05 13:02:57 -050021from voltha_protos.device_pb2 import PmConfig
Chip Boling67b674a2019-02-08 11:42:18 -060022
23
24class AdapterPmMetrics(object):
25 """
26 Base class for Device Adapter PM Metrics Manager
27
28 Device specific (OLT, ONU, OpenOMCI, ...) will derive groups of PM information
29 and this base class is primarily used to provide a consistent interface to configure,
30 start, and stop statistics collection.
31 """
32 DEFAULT_FREQUENCY_KEY = 'default-collection-frequency'
33 DEFAULT_COLLECTION_FREQUENCY = 15 * 10 # 1/10ths of a second
34
35 # If the collection object has a property of the following name, it will be used
36 # to retrieve the UTC Collection Timestamp (UTC seconds since epoch). If the collection
37 # object does not support this attribute, the current time will be used. If the attribute
38 # is supported, but returns None, this signals that no metrics are currently available
39 # for collection.
40 TIMESTAMP_ATTRIBUTE = 'timestamp'
41
Devmalya Paul0d3abf02019-07-31 18:34:27 -040042 def __init__(self, event_mgr, core_proxy, device_id, logical_device_id, serial_number,
Rohan Agrawal01092e92020-06-29 10:59:59 +000043 grouped=False, freq_override=False, max_skew=5, **kwargs):
Chip Boling67b674a2019-02-08 11:42:18 -060044 """
45 Initializer for shared Device Adapter PM metrics manager
46
Arun Aroraa587f992019-03-14 09:54:29 +000047 :param core_proxy: (CoreProxy) Gateway between CORE and an adapter
Chip Boling67b674a2019-02-08 11:42:18 -060048 :param device_id: (str) Device ID
49 :param logical_device_id: (str) VOLTHA Logical Device ID
50 :param grouped: (bool) Flag indicating if statistics are managed as a group
51 :param freq_override: (bool) Flag indicating if frequency collection can be specified
52 on a per group basis
53 :param kwargs: (dict) Device Adapter specific values
54 """
55 self.log = structlog.get_logger(device_id=device_id)
Devmalya Paul0d3abf02019-07-31 18:34:27 -040056 self.event_mgr = event_mgr
Chip Boling67b674a2019-02-08 11:42:18 -060057 self.device_id = device_id
Arun Aroraa587f992019-03-14 09:54:29 +000058 self.core_proxy = core_proxy
59 self.name = core_proxy.listening_topic
Matt Jeanneret4ad333a2019-02-14 19:10:44 -050060 self.logical_device_id = logical_device_id
William Kurkian6e643802019-04-02 12:49:59 -040061 self.serial_number = serial_number
Chip Boling67b674a2019-02-08 11:42:18 -060062 self.default_freq = kwargs.get(AdapterPmMetrics.DEFAULT_FREQUENCY_KEY,
63 AdapterPmMetrics.DEFAULT_COLLECTION_FREQUENCY)
Devmalya Paul0d3abf02019-07-31 18:34:27 -040064 self._event = "KPI_EVENT"
65 self._category = EventCategory.EQUIPMENT
66 self._sub_category = EventSubCategory.ONU
Chip Boling67b674a2019-02-08 11:42:18 -060067 self.grouped = grouped
68 self.freq_override = grouped and freq_override
69 self.lc = None
70 self.pm_group_metrics = dict() # name -> PmGroupConfig
Rohan Agrawal01092e92020-06-29 10:59:59 +000071 self.max_skew = max_skew
Chip Boling67b674a2019-02-08 11:42:18 -060072
73 def update(self, pm_config):
74 # TODO: Move any common steps into base class
75 raise NotImplementedError('Your derived class should override this method')
76
77 def make_proto(self, pm_config=None):
78 raise NotImplementedError('Your derived class should override this method')
79
80 def start_collector(self, callback=None):
81 """
82 Start the collection loop for an adapter if the frequency > 0
83
84 :param callback: (callable) Function to call to collect PM data
85 """
Scott Bakerde7c61a2020-01-15 14:22:11 -080086 self.log.info("starting-pm-collection", device_name=self.name, default_freq=self.default_freq)
Chip Boling67b674a2019-02-08 11:42:18 -060087 if callback is None:
88 callback = self.collect_and_publish_metrics
89
90 if self.lc is None:
91 self.lc = LoopingCall(callback)
92
93 if self.default_freq > 0:
94 self.lc.start(interval=self.default_freq / 10)
95
96 def stop_collector(self):
97 """ Stop the collection loop"""
98 if self.lc is not None and self.default_freq > 0:
99 self.lc.stop()
100
101 def collect_group_metrics(self, group_name, group, names, config):
102 """
103 Collect the metrics for a specific PM group.
104
105 This common collection method expects that the 'group object' provide as the second
106 parameter supports an attribute or property with the name of the value to
107 retrieve.
108
109 :param group_name: (str) The unique collection name. The name should not contain spaces.
110 :param group: (object) The object to query for the value of various attributes (PM names)
111 :param names: (set) A collection of PM names that, if implemented as a property in the object,
112 will return a value to store in the returned PM dictionary
113 :param config: (PMConfig) PM Configuration settings. The enabled flag is examined to determine
114 if the data associated with a PM Name will be collected.
115
116 :return: (MetricInformation) collected metrics
117 """
118 assert ' ' not in group_name, 'Spaces are not allowed in metric titles, use an underscore'
119
120 if group is None:
121 return None
122
123 metrics = dict()
124 context = dict()
125
126 now = getattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
127 if hasattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
128 else arrow.utcnow().float_timestamp
129
130 if now is None:
131 return None # No metrics available at this time for collection
132
133 for (metric, t) in names:
134 if config[metric].type == PmConfig.CONTEXT and hasattr(group, metric):
135 context[metric] = str(getattr(group, metric))
136
137 elif config[metric].type in (PmConfig.COUNTER, PmConfig.GAUGE, PmConfig.STATE):
138 if config[metric].enabled and hasattr(group, metric):
139 metrics[metric] = getattr(group, metric)
140
141 # Check length of metric data. Will be zero if if/when individual group
142 # metrics can be disabled and all are (or or not supported by the
143 # underlying adapter)
144 if len(metrics) == 0:
145 return None
146
147 return MetricInformation(metadata=MetricMetaData(title=group_name,
148 ts=now,
149 logical_device_id=self.logical_device_id,
150 serial_no=self.serial_number,
151 device_id=self.device_id,
152 context=context),
153 metrics=metrics)
154
155 def collect_metrics(self, data=None):
156 """
157 Collect metrics for this adapter.
158
159 The adapter type (OLT, ONU, ..) should provide a derived class where this
160 method iterates through all metrics and collects them up in a dictionary with
161 the group/metric name as the key, and the metric values as the contents.
162
163 The data collected (or passed in) is a list of pairs/tuples. Each
164 pair is composed of a MetricMetaData metadata-portion and list of MetricValuePairs
165 that contains a single individual metric or list of metrics if this is a
166 group metric.
167
168 This method is called for each adapter at a fixed frequency.
169 TODO: Currently all group metrics are collected on a single timer tick.
170 This needs to be fixed as independent group or instance collection is
171 desirable.
172
173 :param data: (list) Existing list of collected metrics (MetricInformation).
174 This is provided to allow derived classes to call into
175 further encapsulated classes.
176
177 :return: (list) metadata and metrics pairs - see description above
178 """
179 raise NotImplementedError('Your derived class should override this method')
180
181 def collect_and_publish_metrics(self):
182 """ Request collection of all enabled metrics and publish them """
183 try:
184 data = self.collect_metrics()
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400185 raised_ts = arrow.utcnow().timestamp
186 self.publish_metrics(data, raised_ts)
Chip Boling67b674a2019-02-08 11:42:18 -0600187
188 except Exception as e:
189 self.log.exception('failed-to-collect-kpis', e=e)
190
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400191 def publish_metrics(self, data, raised_ts):
Chip Boling67b674a2019-02-08 11:42:18 -0600192 """
193 Publish the metrics during a collection.
194
195 The data collected (or passed in) is a list of dictionary pairs/tuple. Each
196 pair is composed of a metadata-portion and a metrics-portion that contains
197 information for a specific instance of an individual metric or metric group.
198
199 :param data: (list) Existing list of collected metrics (MetricInformation)
200 to convert to a KPIEvent and publish
201 """
Yongjie Zhang415a2962019-07-03 15:46:50 -0400202 self.log.debug('publish-metrics', data=data)
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400203 event_header = self.event_mgr.get_event_header(EventType.KPI_EVENT2,
204 self._category,
205 self._sub_category,
206 self._event,
207 raised_ts)
Chip Boling67b674a2019-02-08 11:42:18 -0600208 if len(data):
209 try:
210 # TODO: Existing adapters use the KpiEvent, if/when all existing
211 # adapters use the shared KPI library, we may want to
212 # deprecate the KPIEvent
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400213 event_body = KpiEvent2(
214 type=KpiEventType.slice,
215 ts=arrow.utcnow().float_timestamp,
216 slice_data=data
217 )
218 self.event_mgr.send_event(event_header, event_body)
Chip Boling67b674a2019-02-08 11:42:18 -0600219
220 except Exception as e:
221 self.log.exception('failed-to-submit-kpis', e=e)
222
223 # TODO: Need to support on-demand counter update if provided by the PM 'group'.
224 # Currently we expect PM data to be periodically polled by a separate
225 # mechanism. The on-demand counter update should be optional in case the
226 # particular device adapter group of data is polled anyway for other reasons.