blob: 250ec89599e00e40da457a72813cecf3a2e5401a [file] [log] [blame]
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import re
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'histograms'))
import extract_histograms
import histogram_paths
import merge_xml
LOCAL_METRIC_RE = re.compile(r'metrics\.([^,]+)')
INVALID_LOCAL_METRIC_FIELD_ERROR = (
'Invalid index field specification in ukm metric %(event)s:%(metric)s, the '
'following metrics are used as index fields but are not configured to '
'support it: [%(invalid_metrics)s]\n\n'
'See https://chromium.googlesource.com/chromium/src.git/+/main/services/'
'metrics/ukm_api.md#aggregation-by-metrics-in-the-same-event for '
'instructions on how to configure them.')
def _isMetricValidAsIndexField(metric_node):
"""Checks if a given metric node can be used as a field in an index tag.
Has the following requirements:
* 'history' is the only aggregation target (no others are considered)
* there will be at most 1 'aggregation', 1 'history', and 1 'statistic'
element in a metric element
* enumerations are the only metric types that are valid
Args:
metric_node: A metric node to check.
Returns: True or False, depending on whethere the given node is valid as an
index field.
"""
aggregation_nodes = metric_node.getElementsByTagName('aggregation')
if aggregation_nodes.length != 1:
return False
history_nodes = aggregation_nodes[0].getElementsByTagName('history')
if history_nodes.length != 1:
return False
statistic_nodes = history_nodes[0].getElementsByTagName('statistics')
if statistic_nodes.length != 1:
return False
# Only enumeration type metrics are supported as index fields.
enumeration_nodes = statistic_nodes[0].getElementsByTagName('enumeration')
return bool(enumeration_nodes)
def _getIndexFields(metric_node):
"""Get a list of fields from index node descendents of a metric_node."""
aggregation_nodes = metric_node.getElementsByTagName('aggregation')
if not aggregation_nodes:
return []
history_nodes = aggregation_nodes[0].getElementsByTagName('history')
if not history_nodes:
return []
index_nodes = history_nodes[0].getElementsByTagName('index')
if not index_nodes:
return []
return [index_node.getAttribute('fields') for index_node in index_nodes]
def _getLocalMetricIndexFields(metric_node):
"""Gets a set of metric names being used as local-metric index fields."""
index_fields = _getIndexFields(metric_node)
local_metric_fields = set()
for fields in index_fields:
local_metric_fields.update(LOCAL_METRIC_RE.findall(fields))
return local_metric_fields
class UkmXmlValidation(object):
"""Validations for the content of ukm.xml."""
def __init__(self, ukm_config):
"""Attributes:
config: A XML minidom Element representing the root node of the UKM config
tree.
"""
self.config = ukm_config
def checkEventsHaveOwners(self):
"""Check that every event in the config has at least one owner."""
errors = []
for event_node in self.config.getElementsByTagName('event'):
event_name = event_node.getAttribute('name')
owner_nodes = event_node.getElementsByTagName('owner')
# Check <owner> tag is present for each event.
if not owner_nodes:
errors.append("<owner> tag is required for event '%s'." % event_name)
continue
for owner_node in owner_nodes:
# Check <owner> tag actually has some content.
if not owner_node.childNodes:
errors.append(
"<owner> tag for event '%s' should not be empty." % event_name)
continue
for email in owner_node.childNodes:
# Check <owner> tag's content is an email address, not a username.
if not ('@chromium.org' in email.data or '@google.com' in email.data):
errors.append("<owner> tag for event '%s' expects a Chromium or "
"Google email address." % event_name)
isSuccess = not errors
return (isSuccess, errors)
def checkMetricTypeIsSpecified(self):
"""Check each metric is either specified with an enum or a unit."""
errors = []
warnings = []
enum_tree = merge_xml.MergeFiles([histogram_paths.ENUMS_XML])
enums, _ = extract_histograms.ExtractEnumsFromXmlTree(enum_tree)
for event_node in self.config.getElementsByTagName('event'):
for metric_node in event_node.getElementsByTagName('metric'):
if metric_node.hasAttribute('enum'):
enum_name = metric_node.getAttribute('enum');
# Check if the enum is defined in enums.xml.
if enum_name not in enums:
errors.append("Unknown enum %s in ukm metric %s:%s." %
(enum_name, event_node.getAttribute('name'),
metric_node.getAttribute('name')))
elif not metric_node.hasAttribute('unit'):
warnings.append("Warning: Neither \'enum\' or \'unit\' is specified "
"for ukm metric %s:%s."
% (event_node.getAttribute('name'),
metric_node.getAttribute('name')))
isSuccess = not errors
return (isSuccess, errors, warnings)
def checkLocalMetricIsAggregated(self):
"""Checks that index fields don't list invalid metrics."""
errors = []
for event_node in self.config.getElementsByTagName('event'):
metric_nodes = event_node.getElementsByTagName('metric')
valid_index_field_metrics = {node.getAttribute('name')
for node in metric_nodes
if _isMetricValidAsIndexField(node)}
for metric_node in metric_nodes:
local_metric_index_fields = _getLocalMetricIndexFields(metric_node)
invalid_metrics = local_metric_index_fields - valid_index_field_metrics
if invalid_metrics:
event_name = event_node.getAttribute('name')
metric_name = metric_node.getAttribute('name')
invalid_metrics_string = ', '.join(sorted(invalid_metrics))
errors.append(INVALID_LOCAL_METRIC_FIELD_ERROR %(
{'event': event_name, 'metric': metric_name,
'invalid_metrics': invalid_metrics_string}))
is_success = not errors
return (is_success, errors)