blob: 9f41f90d3ea1abcf499a6750b762e1be72c3a509 [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,
Chip Boling67b674a2019-02-08 11:42:18 -060043 grouped=False, freq_override=False, **kwargs):
44 """
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
71
72 def update(self, pm_config):
73 # TODO: Move any common steps into base class
74 raise NotImplementedError('Your derived class should override this method')
75
76 def make_proto(self, pm_config=None):
77 raise NotImplementedError('Your derived class should override this method')
78
79 def start_collector(self, callback=None):
80 """
81 Start the collection loop for an adapter if the frequency > 0
82
83 :param callback: (callable) Function to call to collect PM data
84 """
85 self.log.info("starting-pm-collection", device_name=self.name)
86 if callback is None:
87 callback = self.collect_and_publish_metrics
88
89 if self.lc is None:
90 self.lc = LoopingCall(callback)
91
92 if self.default_freq > 0:
93 self.lc.start(interval=self.default_freq / 10)
94
95 def stop_collector(self):
96 """ Stop the collection loop"""
97 if self.lc is not None and self.default_freq > 0:
98 self.lc.stop()
99
100 def collect_group_metrics(self, group_name, group, names, config):
101 """
102 Collect the metrics for a specific PM group.
103
104 This common collection method expects that the 'group object' provide as the second
105 parameter supports an attribute or property with the name of the value to
106 retrieve.
107
108 :param group_name: (str) The unique collection name. The name should not contain spaces.
109 :param group: (object) The object to query for the value of various attributes (PM names)
110 :param names: (set) A collection of PM names that, if implemented as a property in the object,
111 will return a value to store in the returned PM dictionary
112 :param config: (PMConfig) PM Configuration settings. The enabled flag is examined to determine
113 if the data associated with a PM Name will be collected.
114
115 :return: (MetricInformation) collected metrics
116 """
117 assert ' ' not in group_name, 'Spaces are not allowed in metric titles, use an underscore'
118
119 if group is None:
120 return None
121
122 metrics = dict()
123 context = dict()
124
125 now = getattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
126 if hasattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
127 else arrow.utcnow().float_timestamp
128
129 if now is None:
130 return None # No metrics available at this time for collection
131
132 for (metric, t) in names:
133 if config[metric].type == PmConfig.CONTEXT and hasattr(group, metric):
134 context[metric] = str(getattr(group, metric))
135
136 elif config[metric].type in (PmConfig.COUNTER, PmConfig.GAUGE, PmConfig.STATE):
137 if config[metric].enabled and hasattr(group, metric):
138 metrics[metric] = getattr(group, metric)
139
140 # Check length of metric data. Will be zero if if/when individual group
141 # metrics can be disabled and all are (or or not supported by the
142 # underlying adapter)
143 if len(metrics) == 0:
144 return None
145
146 return MetricInformation(metadata=MetricMetaData(title=group_name,
147 ts=now,
148 logical_device_id=self.logical_device_id,
149 serial_no=self.serial_number,
150 device_id=self.device_id,
151 context=context),
152 metrics=metrics)
153
154 def collect_metrics(self, data=None):
155 """
156 Collect metrics for this adapter.
157
158 The adapter type (OLT, ONU, ..) should provide a derived class where this
159 method iterates through all metrics and collects them up in a dictionary with
160 the group/metric name as the key, and the metric values as the contents.
161
162 The data collected (or passed in) is a list of pairs/tuples. Each
163 pair is composed of a MetricMetaData metadata-portion and list of MetricValuePairs
164 that contains a single individual metric or list of metrics if this is a
165 group metric.
166
167 This method is called for each adapter at a fixed frequency.
168 TODO: Currently all group metrics are collected on a single timer tick.
169 This needs to be fixed as independent group or instance collection is
170 desirable.
171
172 :param data: (list) Existing list of collected metrics (MetricInformation).
173 This is provided to allow derived classes to call into
174 further encapsulated classes.
175
176 :return: (list) metadata and metrics pairs - see description above
177 """
178 raise NotImplementedError('Your derived class should override this method')
179
180 def collect_and_publish_metrics(self):
181 """ Request collection of all enabled metrics and publish them """
182 try:
183 data = self.collect_metrics()
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400184 raised_ts = arrow.utcnow().timestamp
185 self.publish_metrics(data, raised_ts)
Chip Boling67b674a2019-02-08 11:42:18 -0600186
187 except Exception as e:
188 self.log.exception('failed-to-collect-kpis', e=e)
189
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400190 def publish_metrics(self, data, raised_ts):
Chip Boling67b674a2019-02-08 11:42:18 -0600191 """
192 Publish the metrics during a collection.
193
194 The data collected (or passed in) is a list of dictionary pairs/tuple. Each
195 pair is composed of a metadata-portion and a metrics-portion that contains
196 information for a specific instance of an individual metric or metric group.
197
198 :param data: (list) Existing list of collected metrics (MetricInformation)
199 to convert to a KPIEvent and publish
200 """
Yongjie Zhang415a2962019-07-03 15:46:50 -0400201 self.log.debug('publish-metrics', data=data)
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400202 event_header = self.event_mgr.get_event_header(EventType.KPI_EVENT2,
203 self._category,
204 self._sub_category,
205 self._event,
206 raised_ts)
Chip Boling67b674a2019-02-08 11:42:18 -0600207 if len(data):
208 try:
209 # TODO: Existing adapters use the KpiEvent, if/when all existing
210 # adapters use the shared KPI library, we may want to
211 # deprecate the KPIEvent
Devmalya Paul0d3abf02019-07-31 18:34:27 -0400212 event_body = KpiEvent2(
213 type=KpiEventType.slice,
214 ts=arrow.utcnow().float_timestamp,
215 slice_data=data
216 )
217 self.event_mgr.send_event(event_header, event_body)
Chip Boling67b674a2019-02-08 11:42:18 -0600218
219 except Exception as e:
220 self.log.exception('failed-to-submit-kpis', e=e)
221
222 # TODO: Need to support on-demand counter update if provided by the PM 'group'.
223 # Currently we expect PM data to be periodically polled by a separate
224 # mechanism. The on-demand counter update should be optional in case the
225 # particular device adapter group of data is polled anyway for other reasons.