blob: fe14feef5ad5e0de232d30bd51b6f4a152672128 [file] [log] [blame]
William Kurkian6f436d02019-02-06 16:25:01 -05001# 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
15import structlog
16import arrow
17from twisted.internet.task import LoopingCall
18from voltha.protos.events_pb2 import KpiEvent2, KpiEventType, MetricInformation, MetricMetaData
19from voltha.protos.device_pb2 import PmConfig
20
21
22class AdapterPmMetrics(object):
23 """
24 Base class for Device Adapter PM Metrics Manager
25
26 Device specific (OLT, ONU, OpenOMCI, ...) will derive groups of PM information
27 and this base class is primarily used to provide a consistent interface to configure,
28 start, and stop statistics collection.
29 """
30 DEFAULT_FREQUENCY_KEY = 'default-collection-frequency'
31 DEFAULT_COLLECTION_FREQUENCY = 15 * 10 # 1/10ths of a second
32
33 # If the collection object has a property of the following name, it will be used
34 # to retrieve the UTC Collection Timestamp (UTC seconds since epoch). If the collection
35 # object does not support this attribute, the current time will be used. If the attribute
36 # is supported, but returns None, this signals that no metrics are currently available
37 # for collection.
38 TIMESTAMP_ATTRIBUTE = 'timestamp'
39
40 def __init__(self, adapter_agent, device_id, logical_device_id,
41 grouped=False, freq_override=False, **kwargs):
42 """
43 Initializer for shared Device Adapter PM metrics manager
44
45 :param adapter_agent: (AdapterAgent) Adapter agent for the device
46 :param device_id: (str) Device ID
47 :param logical_device_id: (str) VOLTHA Logical Device ID
48 :param grouped: (bool) Flag indicating if statistics are managed as a group
49 :param freq_override: (bool) Flag indicating if frequency collection can be specified
50 on a per group basis
51 :param kwargs: (dict) Device Adapter specific values
52 """
53 self.log = structlog.get_logger(device_id=device_id)
54 self.device_id = device_id
55 self.adapter_agent = adapter_agent
56 self.name = adapter_agent.adapter_name
57 # Sanitize the vcore ID in the logical device ID
58 self.logical_device_id = '0000' + logical_device_id[4:]
59 device = self.adapter_agent.get_device(self.device_id)
60 self.serial_number = device.serial_number
61
62 self.default_freq = kwargs.get(AdapterPmMetrics.DEFAULT_FREQUENCY_KEY,
63 AdapterPmMetrics.DEFAULT_COLLECTION_FREQUENCY)
64 self.grouped = grouped
65 self.freq_override = grouped and freq_override
66 self.lc = None
67 self.pm_group_metrics = dict() # name -> PmGroupConfig
68
69 def update(self, pm_config):
70 # TODO: Move any common steps into base class
71 raise NotImplementedError('Your derived class should override this method')
72
73 def make_proto(self, pm_config=None):
74 raise NotImplementedError('Your derived class should override this method')
75
76 def start_collector(self, callback=None):
77 """
78 Start the collection loop for an adapter if the frequency > 0
79
80 :param callback: (callable) Function to call to collect PM data
81 """
82 self.log.info("starting-pm-collection", device_name=self.name)
83 if callback is None:
84 callback = self.collect_and_publish_metrics
85
86 if self.lc is None:
87 self.lc = LoopingCall(callback)
88
89 if self.default_freq > 0:
90 self.lc.start(interval=self.default_freq / 10)
91
92 def stop_collector(self):
93 """ Stop the collection loop"""
94 if self.lc is not None and self.default_freq > 0:
95 self.lc.stop()
96
97 def collect_group_metrics(self, group_name, group, names, config):
98 """
99 Collect the metrics for a specific PM group.
100
101 This common collection method expects that the 'group object' provide as the second
102 parameter supports an attribute or property with the name of the value to
103 retrieve.
104
105 :param group_name: (str) The unique collection name. The name should not contain spaces.
106 :param group: (object) The object to query for the value of various attributes (PM names)
107 :param names: (set) A collection of PM names that, if implemented as a property in the object,
108 will return a value to store in the returned PM dictionary
109 :param config: (PMConfig) PM Configuration settings. The enabled flag is examined to determine
110 if the data associated with a PM Name will be collected.
111
112 :return: (MetricInformation) collected metrics
113 """
114 assert ' ' not in group_name, 'Spaces are not allowed in metric titles, use an underscore'
115
116 if group is None:
117 return None
118
119 metrics = dict()
120 context = dict()
121
122 now = getattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
123 if hasattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
124 else arrow.utcnow().float_timestamp
125
126 if now is None:
127 return None # No metrics available at this time for collection
128
129 for (metric, t) in names:
130 if config[metric].type == PmConfig.CONTEXT and hasattr(group, metric):
131 context[metric] = str(getattr(group, metric))
132
133 elif config[metric].type in (PmConfig.COUNTER, PmConfig.GAUGE, PmConfig.STATE):
134 if config[metric].enabled and hasattr(group, metric):
135 metrics[metric] = getattr(group, metric)
136
137 # Check length of metric data. Will be zero if if/when individual group
138 # metrics can be disabled and all are (or or not supported by the
139 # underlying adapter)
140 if len(metrics) == 0:
141 return None
142
143 return MetricInformation(metadata=MetricMetaData(title=group_name,
144 ts=now,
145 logical_device_id=self.logical_device_id,
146 serial_no=self.serial_number,
147 device_id=self.device_id,
148 context=context),
149 metrics=metrics)
150
151 def collect_metrics(self, data=None):
152 """
153 Collect metrics for this adapter.
154
155 The adapter type (OLT, ONU, ..) should provide a derived class where this
156 method iterates through all metrics and collects them up in a dictionary with
157 the group/metric name as the key, and the metric values as the contents.
158
159 The data collected (or passed in) is a list of pairs/tuples. Each
160 pair is composed of a MetricMetaData metadata-portion and list of MetricValuePairs
161 that contains a single individual metric or list of metrics if this is a
162 group metric.
163
164 This method is called for each adapter at a fixed frequency.
165 TODO: Currently all group metrics are collected on a single timer tick.
166 This needs to be fixed as independent group or instance collection is
167 desirable.
168
169 :param data: (list) Existing list of collected metrics (MetricInformation).
170 This is provided to allow derived classes to call into
171 further encapsulated classes.
172
173 :return: (list) metadata and metrics pairs - see description above
174 """
175 raise NotImplementedError('Your derived class should override this method')
176
177 def collect_and_publish_metrics(self):
178 """ Request collection of all enabled metrics and publish them """
179 try:
180 data = self.collect_metrics()
181 self.publish_metrics(data)
182
183 except Exception as e:
184 self.log.exception('failed-to-collect-kpis', e=e)
185
186 def publish_metrics(self, data):
187 """
188 Publish the metrics during a collection.
189
190 The data collected (or passed in) is a list of dictionary pairs/tuple. Each
191 pair is composed of a metadata-portion and a metrics-portion that contains
192 information for a specific instance of an individual metric or metric group.
193
194 :param data: (list) Existing list of collected metrics (MetricInformation)
195 to convert to a KPIEvent and publish
196 """
197 self.log.debug('publish-metrics')
198
199 if len(data):
200 try:
201 # TODO: Existing adapters use the KpiEvent, if/when all existing
202 # adapters use the shared KPI library, we may want to
203 # deprecate the KPIEvent
204 kpi_event = KpiEvent2(
205 type=KpiEventType.slice,
206 ts=arrow.utcnow().float_timestamp,
207 slice_data=data
208 )
209 self.adapter_agent.submit_kpis(kpi_event)
210
211 except Exception as e:
212 self.log.exception('failed-to-submit-kpis', e=e)
213
214 # TODO: Need to support on-demand counter update if provided by the PM 'group'.
215 # Currently we expect PM data to be periodically polled by a separate
216 # mechanism. The on-demand counter update should be optional in case the
217 # particular device adapter group of data is polled anyway for other reasons.