b8bb2ff4c3
Use the six library to get monasca-agent to work with python2.7 and python3. Story: 2004148 Task: 27621 Change-Id: I0de315967dd5a745741fda0c53ce8cc85cda8cc5 Signed-off-by: Chuck Short <chucks@redhat.com>
332 lines
14 KiB
Python
332 lines
14 KiB
Python
# (C) Copyright 2015-2017 Hewlett Packard Enterprise Development LP
|
|
# 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.
|
|
|
|
"""Base class for Checks.
|
|
|
|
If you are writing your own checks you should subclass the AgentCheck class.
|
|
The Check class is being deprecated so don't write new checks with it.
|
|
"""
|
|
# This file uses 'print' as a function rather than a statement, a la Python3
|
|
from __future__ import print_function
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
import yaml
|
|
|
|
import monasca_agent.common.aggregator as aggregator
|
|
import monasca_agent.common.metrics as metrics_pkg
|
|
import monasca_agent.common.util as util
|
|
|
|
|
|
class AgentCheck(util.Dimensions):
|
|
|
|
def __init__(self, name, init_config, agent_config, instances=None):
|
|
"""Initialize a new check.
|
|
|
|
:param name: The name of the check
|
|
:param init_config: The config for initializing the check
|
|
:param agent_config: The global configuration for the agent
|
|
:param instances: A list of configuration objects for each instance.
|
|
"""
|
|
super(AgentCheck, self).__init__(agent_config)
|
|
self.name = name
|
|
self.init_config = init_config
|
|
self.white_list = init_config.get('white_list', None)
|
|
self.hostname = util.get_hostname()
|
|
self.log = logging.getLogger('%s.%s' % (__name__, name))
|
|
threshold = agent_config.get('recent_point_threshold', None)
|
|
tenant_id = agent_config.get('global_delegated_tenant', None)
|
|
self.aggregator = (
|
|
aggregator.MetricsAggregator(self.hostname,
|
|
recent_point_threshold=threshold,
|
|
tenant_id=tenant_id))
|
|
|
|
self.instances = instances or []
|
|
self.library_versions = None
|
|
|
|
def instance_count(self):
|
|
"""Return the number of instances that are configured for this check.
|
|
"""
|
|
return len(self.instances)
|
|
|
|
def submit_metric(self, metric, value, metric_type, dimensions,
|
|
delegated_tenant, hostname, device_name, value_meta,
|
|
timestamp=None):
|
|
# If there is no white list, then report all the metrics
|
|
dimensions_white_list = dimensions.copy()
|
|
if self.white_list:
|
|
if 'metrics' not in self.white_list.keys():
|
|
return
|
|
else:
|
|
metrics = self.white_list['metrics']
|
|
if metric not in metrics:
|
|
return
|
|
# If there is a white list, then only report the metrics listed
|
|
# in white list. Also check if there are dimension key value
|
|
# pairs specified in the metrics section of white list, if
|
|
# there is make sure the keys are in dimensions before
|
|
# submitting the metric. If not, set to the corresponding
|
|
# value in white list.
|
|
dim_key_values = {}
|
|
if metrics.get(metric):
|
|
dim_key_values = list(metrics.get(metric).values())[0]
|
|
else:
|
|
# If white list has a "dimensions" section, set the key
|
|
# value dimension pairs to all the metrics. But the
|
|
# dimensions under "metrics" section has higher priority.
|
|
if 'dimensions' in self.white_list.keys():
|
|
dim_key_values = self.white_list['dimensions']
|
|
for dim_kv in dim_key_values.items():
|
|
if dim_kv[0] not in dimensions_white_list.keys():
|
|
dimensions_white_list[dim_kv[0]] = dim_kv[1]
|
|
try:
|
|
self.aggregator.submit_metric(metric,
|
|
value,
|
|
metric_type,
|
|
dimensions_white_list,
|
|
delegated_tenant,
|
|
hostname,
|
|
device_name,
|
|
value_meta,
|
|
timestamp)
|
|
except Exception as e:
|
|
self.log.exception("invalid metric: {}".format(e))
|
|
|
|
def gauge(self, metric, value, dimensions=None, delegated_tenant=None, hostname=None,
|
|
device_name=None, timestamp=None, value_meta=None):
|
|
"""Record the value of a gauge, with optional dimensions, hostname, value metadata and device name.
|
|
|
|
:param metric: The name of the metric
|
|
:param value: The value of the gauge
|
|
:param dimensions: (optional) A dictionary of dimensions for this metric
|
|
:param delegated_tenant: (optional) Submit metrics on behalf of this tenant ID.
|
|
:param hostname: (optional) A hostname for this metric. Defaults to the current hostname.
|
|
:param device_name: (optional) The device name for this metric
|
|
:param timestamp: (optional) The timestamp for this metric value
|
|
:param value_meta: Additional metadata about this value
|
|
"""
|
|
self.submit_metric(metric,
|
|
value,
|
|
metrics_pkg.Gauge,
|
|
dimensions,
|
|
delegated_tenant,
|
|
hostname,
|
|
device_name,
|
|
value_meta,
|
|
timestamp)
|
|
|
|
def increment(self, metric, value=1, dimensions=None, delegated_tenant=None,
|
|
hostname=None, device_name=None, value_meta=None):
|
|
"""Increment a counter with optional dimensions, hostname and device name.
|
|
|
|
:param metric: The name of the metric
|
|
:param value: The value to increment by
|
|
:param dimensions: (optional) A dictionary of dimensions for this metric
|
|
:param delegated_tenant: (optional) Submit metrics on behalf of this tenant ID.
|
|
:param hostname: (optional) A hostname for this metric. Defaults to the current hostname.
|
|
:param device_name: (optional) The device name for this metric
|
|
:param value_meta: Additional metadata about this value
|
|
"""
|
|
self.submit_metric(metric,
|
|
value,
|
|
metrics_pkg.Counter,
|
|
dimensions,
|
|
delegated_tenant,
|
|
hostname,
|
|
device_name,
|
|
value_meta)
|
|
|
|
def decrement(self, metric, value=1, dimensions=None, delegated_tenant=None,
|
|
hostname=None, device_name=None, value_meta=None):
|
|
"""Decrement a counter with optional dimensions, hostname and device name.
|
|
|
|
:param metric: The name of the metric
|
|
:param value: The value to decrement by
|
|
:param dimensions: (optional) A dictionary of dimensions for this metric
|
|
:param delegated_tenant: (optional) Submit metrics on behalf of this tenant ID.
|
|
:param hostname: (optional) A hostname for this metric. Defaults to the current hostname.
|
|
:param device_name: (optional) The device name for this metric
|
|
:param value_meta: Additional metadata about this value
|
|
"""
|
|
value *= -1
|
|
self.submit_metric(metric,
|
|
value,
|
|
metrics_pkg.Counter,
|
|
dimensions,
|
|
delegated_tenant,
|
|
hostname,
|
|
device_name,
|
|
value_meta)
|
|
|
|
def rate(self, metric, value, dimensions=None, delegated_tenant=None,
|
|
hostname=None, device_name=None, value_meta=None):
|
|
"""Submit a point for a metric that will be calculated as a rate on flush.
|
|
|
|
Values will persist across each call to `check` if there is not enough
|
|
point to generate a rate on the flush.
|
|
|
|
:param metric: The name of the metric
|
|
:param value: The value of the rate
|
|
:param dimensions: (optional) A dictionary of dimensions for this metric
|
|
:param delegated_tenant: (optional) Submit metrics on behalf of this tenant ID.
|
|
:param hostname: (optional) A hostname for this metric. Defaults to the current hostname.
|
|
:param device_name: (optional) The device name for this metric
|
|
:param value_meta: Additional metadata about this value
|
|
"""
|
|
self.submit_metric(metric,
|
|
value,
|
|
metrics_pkg.Rate,
|
|
dimensions,
|
|
delegated_tenant,
|
|
hostname,
|
|
device_name,
|
|
value_meta)
|
|
|
|
def get_metrics(self, prettyprint=False):
|
|
"""Get all metrics, including the ones that are tagged.
|
|
|
|
@return the list of samples
|
|
@rtype list of Measurement objects from monasca_agent.common.metrics
|
|
"""
|
|
metrics = self.aggregator.flush()
|
|
if prettyprint:
|
|
for metric in metrics:
|
|
measurement = metric['measurement']
|
|
print(" Timestamp: {0}".format(measurement['timestamp']))
|
|
print(" Name: {0}".format(measurement['name']))
|
|
print(" Value: {0}".format(measurement['value']))
|
|
print(" Dimensions: ", end='')
|
|
line = 0
|
|
dimensions = measurement['dimensions']
|
|
for name in dimensions:
|
|
if line != 0:
|
|
print(" " * 13, end='')
|
|
print("{0}={1}".format(name, dimensions[name]))
|
|
line += 1
|
|
|
|
print(" Value Meta: ", end='')
|
|
value_meta = measurement['value_meta']
|
|
if value_meta:
|
|
line = 0
|
|
for name in value_meta:
|
|
if line != 0:
|
|
print(" " * 13, end='')
|
|
print("{0}={1}".format(name, value_meta[name]))
|
|
line += 1
|
|
else:
|
|
print('None')
|
|
print("-" * 24)
|
|
|
|
return metrics
|
|
|
|
def get_library_info(self):
|
|
if self.library_versions is not None:
|
|
return self.library_versions
|
|
try:
|
|
self.library_versions = self.get_library_versions()
|
|
except NotImplementedError:
|
|
pass
|
|
|
|
def get_library_versions(self):
|
|
"""Should return a string that shows which version
|
|
|
|
of the needed libraries are used
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def prepare_run(self):
|
|
"""Do any setup required before running all instances"""
|
|
return
|
|
|
|
def run(self):
|
|
"""Run all instances.
|
|
"""
|
|
self.prepare_run()
|
|
|
|
for i, instance in enumerate(self.instances):
|
|
try:
|
|
self.check(instance)
|
|
except Exception:
|
|
self.log.exception("Check '%s' instance #%s failed" % (self.name, i))
|
|
|
|
def check(self, instance):
|
|
"""Overridden by the check class. This will be called to run the check.
|
|
|
|
:param instance: A dict with the instance information. This will vary
|
|
depending on your config structure.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def stop(self):
|
|
"""To be executed when the agent is being stopped to clean resources.
|
|
"""
|
|
pass
|
|
|
|
@classmethod
|
|
def from_yaml(cls, path_to_yaml=None, agentConfig=None, yaml_text=None, check_name=None):
|
|
"""A method used for testing your check without running the agent.
|
|
"""
|
|
|
|
if path_to_yaml:
|
|
check_name = os.path.basename(path_to_yaml).split('.')[0]
|
|
try:
|
|
f = open(path_to_yaml)
|
|
except IOError:
|
|
raise Exception('Unable to open yaml config: %s' % path_to_yaml)
|
|
yaml_text = f.read()
|
|
f.close()
|
|
|
|
config = yaml.safe_load(yaml_text)
|
|
check = cls(check_name, config.get('init_config') or {}, agentConfig or {})
|
|
|
|
return check, config.get('instances', [])
|
|
|
|
@staticmethod
|
|
def normalize(metric, prefix=None):
|
|
"""Turn a metric into a well-formed metric name prefix.b.c
|
|
|
|
:param metric The metric name to normalize
|
|
:param prefix A prefix to to add to the normalized name, default None
|
|
"""
|
|
name = re.sub(r"[,\+\*\-/()\[\]{}]", "_", metric)
|
|
# Eliminate multiple _
|
|
name = re.sub(r"__+", "_", name)
|
|
# Don't start/end with _
|
|
name = re.sub(r"^_", "", name)
|
|
name = re.sub(r"_$", "", name)
|
|
# Drop ._ and _.
|
|
name = re.sub(r"\._", ".", name)
|
|
name = re.sub(r"_\.", ".", name)
|
|
|
|
if prefix is not None:
|
|
return prefix + "." + name
|
|
else:
|
|
return name
|
|
|
|
@staticmethod
|
|
def read_config(instance, key, message=None, cast=None, optional=False):
|
|
val = instance.get(key)
|
|
if val is None:
|
|
if optional is False:
|
|
message = message or 'Must provide `%s` value in instance config' % key
|
|
raise Exception(message)
|
|
else:
|
|
return val
|
|
|
|
if cast is None:
|
|
return val
|
|
else:
|
|
return cast(val)
|