monasca-agent/monasca_agent/collector/checks/utils.py
2019-07-22 15:03:24 +00:00

962 lines
38 KiB
Python

# (C) Copyright 2015,2017-2018 Hewlett Packard Enterprise Development LP
# (C) Copyright 2017 KylinCloud
# Copyright 2018 OP5 AB
# 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.
import base64
import json
import logging
import math
from numbers import Number
import os
import re
import requests
from monasca_agent.common import exceptions
from monasca_agent.common import keystone
log = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 20
def add_basic_auth(request, username, password):
"""A helper to add basic authentication to a urllib2 request.
We do this across a variety of checks so it's good to have this in one place.
"""
auth_str = base64.encodestring('%s:%s' % (username, password)).strip()
request.add_header('Authorization', 'Basic %s' % auth_str)
return request
def get_tenant_name(tenants, tenant_id):
tenant_name = None
for tenant in tenants:
if tenant.id == tenant_id:
tenant_name = tenant.name
break
return tenant_name
def get_tenant_list(config, log):
tenants = []
try:
log.debug("Retrieving Keystone tenant list")
client = keystone.get_client(**config)
if 'v2' in client.__module__:
tenants = client.tenants.list()
else:
tenants = client.projects.list()
except Exception as e:
msg = "Unable to get tenant list from keystone: {0}"
log.error(msg.format(e))
return tenants
def convert_memory_string_to_bytes(memory_string):
"""Conversion from memory represented in string format to bytes"""
if "m" in memory_string:
memory = float(memory_string.split('m')[0])
return memory / 1000
elif "K" in memory_string:
memory = float(memory_string.split('K')[0])
return _compute_memory_bytes(memory_string, memory, 1)
elif "M" in memory_string:
memory = float(memory_string.split('M')[0])
return _compute_memory_bytes(memory_string, memory, 2)
elif "G" in memory_string:
memory = float(memory_string.split('G')[0])
return _compute_memory_bytes(memory_string, memory, 3)
elif "T" in memory_string:
memory = float(memory_string.split('T')[0])
return _compute_memory_bytes(memory_string, memory, 4)
else:
return float(memory_string)
def _compute_memory_bytes(memory_string, memory, power):
if "i" in memory_string:
return memory * math.pow(1024, power)
return memory * math.pow(1000, power)
class KubernetesConnector(object):
"""Class for connecting to Kubernetes API from within a container running
in a Kubernetes environment
"""
CACERT_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token' # nosec B105
def __init__(self, connection_timeout):
self.api_url = None
self.api_verify = None
self.api_request_header = None
if connection_timeout is None:
self.connection_timeout = DEFAULT_TIMEOUT
else:
self.connection_timeout = connection_timeout
self._set_kubernetes_api_connection_info()
self._set_kubernetes_service_account_info()
def _set_kubernetes_api_connection_info(self):
"""Set kubernetes API string from default container environment
variables
"""
api_host = os.environ.get('KUBERNETES_SERVICE_HOST', "kubernetes.default")
api_port = os.environ.get('KUBERNETES_SERVICE_PORT', "443")
self.api_url = "https://{}:{}".format(api_host, api_port)
def _set_kubernetes_service_account_info(self):
"""Set cert and token info to included on requests to the API"""
try:
with open(self.TOKEN_PATH) as token_file:
token = token_file.read()
except Exception as e:
log.error("Unable to read token - {}. Defaulting to using no token".format(e))
token = None
self.api_request_header = {'Authorization': 'Bearer {}'.format(token)} if token else None
self.api_verify = self.CACERT_PATH if os.path.exists(self.CACERT_PATH) else False
def get_agent_pod_host(self, return_host_name=False):
"""Obtain host the agent is running on in Kubernetes.
Used when trying to connect to services running on the node (Kubelet, cAdvisor)
"""
# Get pod name and namespace from environment variables
pod_name = os.environ.get("AGENT_POD_NAME")
pod_namespace = os.environ.get("AGENT_POD_NAMESPACE")
if not pod_name:
raise exceptions.MissingEnvironmentVariables(
"pod_name is not set as environment variables cannot derive"
" host from Kubernetes API")
if not pod_namespace:
raise exceptions.MissingEnvironmentVariables(
"pod_namespace is not set as environment variables cannot "
"derive host from Kubernetes API")
pod_url = "/api/v1/namespaces/{}/pods/{}".format(pod_namespace, pod_name)
try:
agent_pod = self.get_request(pod_url)
except Exception as e:
exception_message = "Could not get agent pod from Kubernetes API" \
" to get host IP with error - {}".format(e)
log.exception(exception_message)
raise exceptions.KubernetesAPIConnectionError(exception_message)
if not return_host_name:
return agent_pod['status']['hostIP']
else:
return agent_pod['spec']['nodeName']
def get_request(self, request_endpoint, as_json=True, retried=False):
"""Sends request to Kubernetes API with given endpoint.
Will retry the request once, with updated token/cert, if unauthorized.
"""
api_url = self.api_url
if api_url[-1] == '/':
api_url = api_url[:-1]
if request_endpoint[0] == '/':
request_endpoint = request_endpoint[1:]
request_url = "{}/{}".format(api_url, request_endpoint)
result = requests.get(request_url,
timeout=self.connection_timeout,
headers=self.api_request_header,
verify=self.api_verify)
if result.status_code >= 300:
if result.status_code == 401 and not retried:
log.info("Could not authenticate with Kubernetes API at the"
" first time. Rereading in cert and token.")
self._set_kubernetes_service_account_info()
return self.get_request(request_endpoint, as_json=as_json,
retried=True)
exception_message = "Could not obtain data from {} with the " \
"given status code {} and return text {}".\
format(request_url, result.status_code, result.text)
raise exceptions.KubernetesAPIConnectionError(exception_message)
return result.json() if as_json else result
class DynamicCheckHelper(object):
"""Supplements existing check class with reusable functionality to transform
third-party metrics into Monasca ones in a configurable way
"""
COUNTERS_KEY = 'counters'
RATES_KEY = 'rates'
GAUGES_KEY = 'gauges'
CONFIG_SECTIONS = [GAUGES_KEY, RATES_KEY, COUNTERS_KEY]
GAUGE = 0
RATE = 1
COUNTER = 2
SKIP = 3
METRIC_TYPES = [GAUGE, RATE, COUNTER]
DEFAULT_GROUP = ""
class MetricSpec(object):
"""Describes how to filter and map input metrics to Monasca metrics
"""
def __init__(self, metric_type, metric_name):
"""Construct a metric-specification
:param metric_type: one of GAUGE, RATE, COUNTER, SKIP
:param metric_name: normalized name of the metric as reported to Monasca
"""
self.metric_type = metric_type
self.metric_name = metric_name
@staticmethod
def _normalize_dim_value(value):
"""Normalize an input value
* Replace \\x?? values with _
* Replace illegal characters
- according to ANTLR grammar:
( '}' | '{' | '&' | '|' | '>' | '<' | '=' | ',' | ')' | '(' | ' ' | '"' )
- according to Python API validation: "<>={}(),\"\\\\|;&"
* Truncate to 255 chars
:param value: input value
:return: valid dimension value
"""
return re.sub(r'[|\\;,&=\']', '-',
re.sub(r'[(}>]', ']',
re.sub(r'[({<]', '[', value.replace(r'\x2d', '-').
replace(r'\x7e', '~'))))[:255]
class DimMapping(object):
"""Describes how to transform dictionary like metadata attached to a metric into Monasca dimensions
"""
def __init__(self, dimension, regex='(.*)', separator=None):
"""C'tor
:param dimension to be mapped to
:param regex: regular expression used to extract value from source value
:param separator: used to concatenate match-groups
"""
self.dimension = dimension
self.regex = regex
self.separator = separator
self.cregex = re.compile(regex) if regex != '(.*)' else None
def map_value(self, source_value):
"""Transform source value into target dimension value
:param source_value: label value to transform
:return: transformed dimension value or None if the regular
expression did not match. An empty result (caused by the regex
having no match-groups) indicates that the label is used for filtering
but not mapped to a dimension.
"""
if self.cregex:
match_groups = self.cregex.match(source_value)
if match_groups:
return DynamicCheckHelper._normalize_dim_value(
self.separator.join(match_groups.groups()))
else:
return None
else:
return DynamicCheckHelper._normalize_dim_value(source_value)
@staticmethod
def _build_dimension_map(config):
"""Builds dimension mappings for the given configuration element
:param config: 'mappings' element of config
:return: dictionary mapping source labels to applicable DimMapping objects
"""
result = {}
for dim, spec in config.get('dimensions', {}).items():
if isinstance(spec, dict):
label = spec.get('source_key', dim)
sepa = spec.get('separator', '-')
regex = spec.get('regex', '(.*)')
else:
label = spec
regex = '(.*)'
sepa = None
# note: source keys can be mapped to multiple dimensions
arr = result.get(label, [])
mapping = DynamicCheckHelper.DimMapping(dimension=dim, regex=regex, separator=sepa)
arr.append(mapping)
result[label] = arr
return result
def __init__(self, check, prefix=None, default_mapping=None):
"""C'tor
:param check: Target check instance
"""
self._check = check
self._prefix = prefix
self._groups = {}
self._metric_map = {}
self._dimension_map = {}
self._metric_cache = {}
self._grp_metric_map = {}
self._grp_dimension_map = {}
self._grp_metric_cache = {}
self._metric_to_group = {}
for inst in self._check.instances:
iname = inst['name']
mappings = inst.get('mapping', default_mapping)
if mappings:
# build global name filter and rate/gauge assignment
self._metric_map[iname] = mappings
self._metric_cache[iname] = {}
# build global dimension map
self._dimension_map[iname] = DynamicCheckHelper._build_dimension_map(mappings)
# check if groups are used
groups = mappings.get('groups')
self._metric_to_group[iname] = {}
self._groups[iname] = []
if groups:
self._groups[iname] = list(groups.keys())
self._grp_metric_map[iname] = {}
self._grp_metric_cache[iname] = {}
self._grp_dimension_map[iname] = {}
for grp, gspec in groups.items():
self._grp_metric_map[iname][grp] = gspec
self._grp_metric_cache[iname][grp] = {}
self._grp_dimension_map[iname][grp] =\
DynamicCheckHelper._build_dimension_map(gspec)
# add the global mappings as pseudo group, so that it is considered when
# searching for metrics
self._groups[iname].append(DynamicCheckHelper.DEFAULT_GROUP)
self._grp_metric_map[iname][DynamicCheckHelper.DEFAULT_GROUP] =\
self._metric_map[iname]
self._grp_metric_cache[iname][DynamicCheckHelper.DEFAULT_GROUP] =\
self._metric_cache[iname]
self._grp_dimension_map[iname][DynamicCheckHelper.DEFAULT_GROUP] =\
self._dimension_map[iname]
else:
raise exceptions.CheckException(
'instance %s is not supported: no element "mapping" found!', iname)
def _get_group(self, instance, metric):
"""Search the group for a metric. Can be used only when metric names unambiguous across groups.
:param metric: input metric
:return: group name or None (if no group matches)
"""
iname = instance['name']
group = self._metric_to_group[iname].get(metric)
if group is None:
for g in self._groups[iname]:
spec = self._fetch_metric_spec(instance, metric, g)
if spec and spec.metric_type != DynamicCheckHelper.SKIP:
self._metric_to_group[iname][metric] = g
return g
return group
def _fetch_metric_spec(self, instance, metric, group=None):
"""Checks whether a metric is enabled by the instance configuration
:param instance: instance containing the check configuration
:param metric: metric as reported from metric data source (before mapping)
:param group: optional metric group, will be used as dot-separated prefix
"""
instance_name = instance['name']
# filter and classify the metric
if group is not None:
metric_cache = self._grp_metric_cache[instance_name].get(group, {})
metric_map = self._grp_metric_map[instance_name].get(group, {})
return DynamicCheckHelper._lookup_metric(metric, metric_cache, metric_map)
else:
metric_cache = self._metric_cache[instance_name]
metric_map = self._metric_map[instance_name]
return DynamicCheckHelper._lookup_metric(metric, metric_cache, metric_map)
def is_enabled_metric(self, instance, metric, group=None):
return self._fetch_metric_spec(
instance, metric, group).metric_type != DynamicCheckHelper.SKIP
def push_metric_dict(
self,
instance,
metric_dict,
labels=None,
group=None,
timestamp=None,
fixed_dimensions=None,
default_dimensions=None,
max_depth=0,
curr_depth=0,
prefix='',
index=-1):
"""This will extract metrics and dimensions from a dictionary.
The following mappings are applied:
Simple recursive composition of metric names:
Input:
{
'server': {
'requests': 12
}
}
Configuration:
mapping:
counters:
- server_requests
Output:
server_requests=12
Mapping of textual values to dimensions to distinguish array elements. Make sure that tests
attributes are sufficient to distinguish the array elements. If not use the build-in
'index' dimension.
Input:
{
'server': [
{
'role': 'master,
'node_name': 'server0',
'requests': 1500
},
{
'role': 'slave',
'node_name': 'server1',
'requests': 1000
},
{
'role': 'slave',
'node_name': 'server2',
'requests': 500
}
}
}
Configuration:
mapping:
dimensions:
server_role: role
node_name: node_name
rates:
- requests
Output:
server_requests{server_role=master, node_name=server0} = 1500.0
server_requests{server_role=slave, node_name=server1} = 1000.0
server_requests{server_role=slave, node_name=server2} = 500.0
Distinguish array elements where no textual attribute are available or no mapping has been
configured for them.
In that case an 'index' dimension will be attached to the metric which has to be mapped
properly.
Input:
{
'server': [
{
'requests': 1500
},
{
'requests': 1000
},
{
'requests': 500
}
}
}
Configuration:
mapping:
dimensions:
server_no: index # index is a predefined label
counters:
- server_requests
Result:
server_requests{server_no=0} = 1500.0
server_requests{server_no=1} = 1000.0
server_requests{server_no=2} = 500.0
:param instance: Instance to submit to
:param metric_dict: input data as dictionary
:param labels: labels to be mapped to dimensions
:param group: group to use for mapping labels and prefixing
:param timestamp: timestamp to report for the measurement
:param fixed_dimensions: dimensions which are always added with fixed values
:param default_dimensions: dimensions to be added, can be overwritten by actual data in
metric_dict
:param max_depth: max. depth to recurse
:param curr_depth: depth of recursion
:param prefix: prefix to prepend to any metric
:param index: current index when traversing through a list
"""
# when traversing through an array, each element must be distinguished with dimensions
# therefore additional dimensions need to be calculated from the siblings
# of the actual number valued fields
if default_dimensions is None:
default_dimensions = {}
if fixed_dimensions is None:
fixed_dimensions = {}
if labels is None:
labels = {}
if index != -1:
ext_labels = self.extract_dist_labels(
instance['name'], group, metric_dict, labels.copy(), index)
if not ext_labels:
log.debug(
"skipping array due to lack of mapped dimensions for group %s "
"(at least 'index' should be supported)",
group if group else '<root>')
return
else:
ext_labels = labels.copy()
for element, child in metric_dict.items():
# if child is a dictionary, then recurse
if isinstance(child, dict) and curr_depth < max_depth:
self.push_metric_dict(
instance,
child,
ext_labels,
group,
timestamp,
fixed_dimensions,
default_dimensions,
max_depth,
curr_depth + 1,
prefix + element + '_')
# if child is a number, assume that it is a metric (it will be filtered
# out by the rate/gauge names)
elif isinstance(child, Number):
self.push_metric(
instance,
prefix + element,
float(child),
ext_labels,
group,
timestamp,
fixed_dimensions,
default_dimensions)
# if it is a list, then each array needs to be added. Additional dimensions must be
# found in order to distinguish the measurements.
elif isinstance(child, list):
for i, child_element in enumerate(child):
if isinstance(child_element, dict):
if curr_depth < max_depth:
self.push_metric_dict(
instance,
child_element,
ext_labels,
group,
timestamp,
fixed_dimensions,
default_dimensions,
max_depth,
curr_depth + 1,
prefix + element + '_',
index=i)
elif isinstance(child_element, Number):
if len(self._get_mappings(instance['name'], group, 'index')) > 0:
idx_labels = ext_labels.copy()
idx_labels['index'] = str(i)
self.push_metric(
instance,
prefix + element,
float(child_element),
idx_labels,
group,
timestamp,
fixed_dimensions,
default_dimensions)
else:
log.debug(
"skipping array due to lack of mapped 'index' dimensions"
"for group %s",
group if group else '<root>')
else:
log.debug(
'nested arrays are not supported for configurable extraction of'
'element %s', element)
def extract_dist_labels(self, instance_name, group, metric_dict, labels, index):
"""Extract additional distinguishing labels from metric dictionary. All top-level
attributes which are strings and for which a dimension mapping is available will be
transformed into dimensions.
:param instance_name: instance to be used
:param group: metric group or None for root/unspecified group
:param metric_dict: input dictionary containing the metric at the top-level
:param labels: labels dictionary to extend with the additional found metrics
:param index: index value to be used as fallback if no labels can be derived from
string-valued attributes or the derived labels are not mapped in the config.
:return: Extended labels, already including the 'labels' passed into this method
"""
ext_labels = None
# collect additional dimensions first from non-metrics
for element, child in metric_dict.items():
if isinstance(
child,
str) and len(
self._get_mappings(
instance_name,
group,
element)) > 0:
if not ext_labels:
ext_labels = labels.copy()
ext_labels[element] = child
# if no additional labels supplied just take the index (if it is mapped)
if not ext_labels and len(self._get_mappings(instance_name, group, 'index')) > 0:
if not ext_labels:
ext_labels = labels.copy()
ext_labels['index'] = str(index)
return ext_labels
def push_metric(
self,
instance,
metric,
value,
labels=None,
group=None,
timestamp=None,
fixed_dimensions=None,
default_dimensions=None):
"""Pushes a meter using the configured mapping information to determine metric_type
and map the name and dimensions
:param instance: instance containing the check configuration
:param value: metric value (float)
:param metric: metric as reported from metric data source (before mapping)
:param labels: labels/tags as reported from the metric data source (before mapping)
:param timestamp: optional timestamp to handle rates properly
:param group: specify the metric group, otherwise it will be determined from the metric name
:param fixed_dimensions:
:param default_dimensions:
"""
# determine group automatically if not specified
if fixed_dimensions is None:
fixed_dimensions = {}
if labels is None:
labels = {}
if default_dimensions is None:
default_dimensions = {}
if group is None:
group = self._get_group(instance, metric)
metric_entry = self._fetch_metric_spec(instance, metric, group)
if metric_entry.metric_type == DynamicCheckHelper.SKIP:
return False
if self._prefix:
metric_prefix = self._prefix + '.'
else:
metric_prefix = ''
if group:
metric_prefix += group + '.'
# determine the metric name
metric_name = metric_prefix + metric_entry.metric_name
# determine the target dimensions
dims = self._map_dimensions(instance['name'], labels, group, default_dimensions)
if dims is None:
# regex for at least one dimension filtered the metric out
return True
# apply fixed default dimensions
if fixed_dimensions:
dims.update(fixed_dimensions)
log.debug(
'push %s %s = %s {%s}',
metric_entry.metric_type,
metric_entry.metric_name,
value,
dims)
if metric_entry.metric_type == DynamicCheckHelper.RATE:
self._check.rate(metric_name, float(value), dimensions=dims)
elif metric_entry.metric_type == DynamicCheckHelper.GAUGE:
self._check.gauge(metric_name, float(value), timestamp=timestamp, dimensions=dims)
elif metric_entry.metric_type == DynamicCheckHelper.COUNTER:
self._check.increment(metric_name, float(value), dimensions=dims)
return True
def get_mapped_metrics(self, instance):
"""Returns input metric names or regex for which a mapping has been defined
:param instance: instance to consider
:return: array of metrics
"""
metric_list = []
iname = instance['name']
# collect level-0 metrics
metric_map = self._metric_map[iname]
metric_list.extend(metric_map.get(DynamicCheckHelper.GAUGES_KEY, []))
metric_list.extend(metric_map.get(DynamicCheckHelper.RATES_KEY, []))
metric_list.extend(metric_map.get(DynamicCheckHelper.COUNTERS_KEY, []))
# collect group specific metrics
grp_metric_map = self._grp_metric_map.get(iname, {})
for gname, gmmap in grp_metric_map.items():
metric_list.extend(gmmap.get(DynamicCheckHelper.GAUGES_KEY, []))
metric_list.extend(gmmap.get(DynamicCheckHelper.RATES_KEY, []))
metric_list.extend(gmmap.get(DynamicCheckHelper.COUNTERS_KEY, []))
return metric_list
def _map_dimensions(self, instance_name, labels, group, default_dimensions):
"""Transforms labels attached to input metrics into Monasca dimensions
:param default_dimensions:
:param group:
:param instance_name:
:param labels:
:return: mapped dimensions or None if the dimensions filter did not match
and the metric needs to be filtered
"""
dims = default_dimensions.copy()
# map all specified dimension all keys
for labelname, labelvalue in labels.items():
mapping_arr = self._get_mappings(instance_name, group, labelname)
target_dim = None
for map_spec in mapping_arr:
try:
# map the dimension name
target_dim = map_spec.dimension
# apply the mapping function to the value
if target_dim not in dims: # do not overwrite
mapped_value = map_spec.map_value(labelvalue)
if mapped_value is None:
# None means: filter it out based on dimension value
return None
elif mapped_value != '':
dims[target_dim] = mapped_value
# else the dimension will not map
except (IndexError, AttributeError): # probably the regex was faulty
log.exception(
'dimension %s value could not be mapped from %s: regex for mapped'
'dimension %s does not match %s',
target_dim, labelvalue, labelname, map_spec.regex)
return None
return dims
def _get_mappings(self, instance_name, group, labelname):
# obtain mappings
# check group-specific ones first
if group:
mapping_arr = self._grp_dimension_map[instance_name].get(group, {}).get(labelname, [])
else:
mapping_arr = []
# fall-back to global ones
mapping_arr.extend(self._dimension_map[instance_name].get(labelname, []))
return mapping_arr
@staticmethod
def _create_metric_spec(metric, metric_type, metric_map):
"""Get or create MetricSpec if metric is in list for metric_type
:param metric: incoming metric name
:param metric_type: GAUGE, RATE, COUNTER
:param metric_map: dictionary with mapping configuration for a metric group or
the entire instance
:return: new MetricSpec entry or None if metric is not listed as metric_type
"""
re_list = metric_map.get(DynamicCheckHelper.CONFIG_SECTIONS[metric_type], [])
for rx in re_list:
match_groups = re.match(rx, metric)
if match_groups:
metric_entry = DynamicCheckHelper.MetricSpec(
metric_type=metric_type,
metric_name=DynamicCheckHelper._normalize_metricname(
metric,
match_groups))
return metric_entry
return None
@staticmethod
def _lookup_metric(metric, metric_cache, metric_map):
"""Search cache for a MetricSpec and create if missing
:param metric: input metric name
:param metric_cache: cache to use
:param metric_map: mapping config element to consider
:return: MetricSpec for the output metric
"""
i = DynamicCheckHelper.GAUGE
metric_entry = metric_cache.get(metric, None)
while not metric_entry and i < len(DynamicCheckHelper.METRIC_TYPES):
metric_entry = DynamicCheckHelper._create_metric_spec(
metric, DynamicCheckHelper.METRIC_TYPES[i], metric_map)
i += 1
if not metric_entry:
# fall-through
metric_entry = DynamicCheckHelper.MetricSpec(
metric_type=DynamicCheckHelper.SKIP,
metric_name=DynamicCheckHelper._normalize_metricname(metric))
metric_cache[metric] = metric_entry
return metric_entry
@staticmethod
def _normalize_metricname(metric, match_groups=None):
# map metric name first
if match_groups and match_groups.lastindex \
and match_groups.lastindex > 0:
metric = '_'.join(match_groups.groups())
metric = re.sub(
'(?!^)([A-Z]+)',
r'_\1',
metric.replace(
'.',
'_')).replace(
'__',
'_').lower()
metric = re.sub(r"[,+*\-/()\[\]{}]", "_", metric)
# Eliminate multiple _
metric = re.sub(r"__+", "_", metric)
# Don't start/end with _
metric = re.sub(r"^_", "", metric)
metric = re.sub(r"_$", "", metric)
# Drop ._ and _.
metric = re.sub(r"\._", ".", metric)
metric = re.sub(r"_\.", ".", metric)
return metric
def get_pod_dimensions(kubernetes_connector, pod_metadata, kubernetes_labels):
pod_name = pod_metadata['name']
pod_dimensions = {'pod_name': pod_name, 'namespace': pod_metadata['namespace']}
if "labels" in pod_metadata:
pod_labels = pod_metadata['labels']
for label in kubernetes_labels:
if label in pod_labels:
pod_dimensions[label] = pod_labels[label]
pod_owner_dimension_set = get_pod_owner(kubernetes_connector, pod_metadata)
if pod_owner_dimension_set:
pod_dimensions[pod_owner_dimension_set[0]] = pod_owner_dimension_set[1]
pod_dimensions["owner_type"] = pod_owner_dimension_set[0]
return pod_dimensions
def _attempt_to_get_owner_name(kubernetes_connector, resource_endpoint):
try:
resource = kubernetes_connector.get_request(resource_endpoint)
resource_metadata = resource['metadata']
pod_owner_pair = _parse_manifest_for_owner(resource_metadata)
return None if not pod_owner_pair else pod_owner_pair[1]
except Exception as e:
log.info("Could not connect to api to get owner data - {}".format(e))
return None
return None
def _parse_manifest_for_owner(resource_metadata):
resource_name = resource_metadata['name']
owner_references = resource_metadata.get('ownerReferences', None)
if owner_references:
try:
if len(owner_references) > 1:
log.warn("More then one owner for resource {}".format(resource_name))
owner_reference = owner_references[0]
resource_owner_type = owner_reference['kind']
resource_owner_name = owner_reference['name']
return resource_owner_type, resource_owner_name
except Exception:
log.info("Could not get owner from ownerReferences"
"for resource {}".format(resource_name))
# Try to get owner from annotations
else:
try:
resource_created_by = json.loads(
resource_metadata['annotations']['kubernetes.io/created-by'])
resource_owner_type = resource_created_by['reference']['kind']
resource_owner_name = resource_created_by['reference']['name']
return resource_owner_type, resource_owner_name
except Exception:
log.info("Could not get resource owner from annotations"
"for resource {}".format(resource_name))
return None
def _get_pod_owner_pair(kubernetes_connector, pod_owner_type, pod_owner_name, pod_namespace):
if pod_owner_type == "ReplicationController":
return 'replication_controller', pod_owner_name
elif pod_owner_type == "ReplicaSet":
if not kubernetes_connector:
log.info("Can not set deployment name as connection information to API is not set. "
"Setting ReplicaSet as dimension")
deployment_name = None
else:
replicaset_endpoint = "/apis/extensions/v1beta1/namespaces/{}/replicasets/{}".format(
pod_namespace, pod_owner_name)
deployment_name = _attempt_to_get_owner_name(kubernetes_connector, replicaset_endpoint)
if not deployment_name:
return 'replica_set', pod_owner_name
else:
return 'deployment', deployment_name
elif pod_owner_type == "DaemonSet":
return 'daemon_set', pod_owner_name
elif pod_owner_type == "StatefulSet":
return 'stateful_set', pod_owner_name
elif pod_owner_type == "Job":
if not kubernetes_connector:
log.info("Can not set cronjob name as connection information to API is not set. "
"Setting job as dimension")
cronjob_name = None
else:
job_endpoint = "/apis/batch/v1/namespaces/{}/jobs/{}".format(
pod_namespace, pod_owner_name)
cronjob_name = _attempt_to_get_owner_name(kubernetes_connector, job_endpoint)
if not cronjob_name:
return 'job', pod_owner_name
else:
return 'cronjob', cronjob_name
else:
log.warn("Unsupported pod owner kind {}".format(pod_owner_type))
return None
def get_pod_owner(kubernetes_connector, pod_metadata):
pod_namespace = pod_metadata['namespace']
owner_pair = _parse_manifest_for_owner(pod_metadata)
if owner_pair:
return _get_pod_owner_pair(
kubernetes_connector,
owner_pair[0],
owner_pair[1],
pod_namespace)
return None