xml_validations.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. # Copyright 2019 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. import os
  5. import re
  6. import sys
  7. sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'histograms'))
  8. import extract_histograms
  9. import histogram_paths
  10. import merge_xml
  11. LOCAL_METRIC_RE = re.compile(r'metrics\.([^,]+)')
  12. INVALID_LOCAL_METRIC_FIELD_ERROR = (
  13. 'Invalid index field specification in ukm metric %(event)s:%(metric)s, the '
  14. 'following metrics are used as index fields but are not configured to '
  15. 'support it: [%(invalid_metrics)s]\n\n'
  16. 'See https://chromium.googlesource.com/chromium/src.git/+/main/services/'
  17. 'metrics/ukm_api.md#aggregation-by-metrics-in-the-same-event for '
  18. 'instructions on how to configure them.')
  19. def _isMetricValidAsIndexField(metric_node):
  20. """Checks if a given metric node can be used as a field in an index tag.
  21. Has the following requirements:
  22. * 'history' is the only aggregation target (no others are considered)
  23. * there will be at most 1 'aggregation', 1 'history', and 1 'statistic'
  24. element in a metric element
  25. * enumerations are the only metric types that are valid
  26. Args:
  27. metric_node: A metric node to check.
  28. Returns: True or False, depending on whethere the given node is valid as an
  29. index field.
  30. """
  31. aggregation_nodes = metric_node.getElementsByTagName('aggregation')
  32. if aggregation_nodes.length != 1:
  33. return False
  34. history_nodes = aggregation_nodes[0].getElementsByTagName('history')
  35. if history_nodes.length != 1:
  36. return False
  37. statistic_nodes = history_nodes[0].getElementsByTagName('statistics')
  38. if statistic_nodes.length != 1:
  39. return False
  40. # Only enumeration type metrics are supported as index fields.
  41. enumeration_nodes = statistic_nodes[0].getElementsByTagName('enumeration')
  42. return bool(enumeration_nodes)
  43. def _getIndexFields(metric_node):
  44. """Get a list of fields from index node descendents of a metric_node."""
  45. aggregation_nodes = metric_node.getElementsByTagName('aggregation')
  46. if not aggregation_nodes:
  47. return []
  48. history_nodes = aggregation_nodes[0].getElementsByTagName('history')
  49. if not history_nodes:
  50. return []
  51. index_nodes = history_nodes[0].getElementsByTagName('index')
  52. if not index_nodes:
  53. return []
  54. return [index_node.getAttribute('fields') for index_node in index_nodes]
  55. def _getLocalMetricIndexFields(metric_node):
  56. """Gets a set of metric names being used as local-metric index fields."""
  57. index_fields = _getIndexFields(metric_node)
  58. local_metric_fields = set()
  59. for fields in index_fields:
  60. local_metric_fields.update(LOCAL_METRIC_RE.findall(fields))
  61. return local_metric_fields
  62. class UkmXmlValidation(object):
  63. """Validations for the content of ukm.xml."""
  64. def __init__(self, ukm_config):
  65. """Attributes:
  66. config: A XML minidom Element representing the root node of the UKM config
  67. tree.
  68. """
  69. self.config = ukm_config
  70. def checkEventsHaveOwners(self):
  71. """Check that every event in the config has at least one owner."""
  72. errors = []
  73. for event_node in self.config.getElementsByTagName('event'):
  74. event_name = event_node.getAttribute('name')
  75. owner_nodes = event_node.getElementsByTagName('owner')
  76. # Check <owner> tag is present for each event.
  77. if not owner_nodes:
  78. errors.append("<owner> tag is required for event '%s'." % event_name)
  79. continue
  80. for owner_node in owner_nodes:
  81. # Check <owner> tag actually has some content.
  82. if not owner_node.childNodes:
  83. errors.append(
  84. "<owner> tag for event '%s' should not be empty." % event_name)
  85. continue
  86. for email in owner_node.childNodes:
  87. # Check <owner> tag's content is an email address, not a username.
  88. if not ('@chromium.org' in email.data or '@google.com' in email.data):
  89. errors.append("<owner> tag for event '%s' expects a Chromium or "
  90. "Google email address." % event_name)
  91. isSuccess = not errors
  92. return (isSuccess, errors)
  93. def checkMetricTypeIsSpecified(self):
  94. """Check each metric is either specified with an enum or a unit."""
  95. errors = []
  96. warnings = []
  97. enum_tree = merge_xml.MergeFiles([histogram_paths.ENUMS_XML])
  98. enums, _ = extract_histograms.ExtractEnumsFromXmlTree(enum_tree)
  99. for event_node in self.config.getElementsByTagName('event'):
  100. for metric_node in event_node.getElementsByTagName('metric'):
  101. if metric_node.hasAttribute('enum'):
  102. enum_name = metric_node.getAttribute('enum');
  103. # Check if the enum is defined in enums.xml.
  104. if enum_name not in enums:
  105. errors.append("Unknown enum %s in ukm metric %s:%s." %
  106. (enum_name, event_node.getAttribute('name'),
  107. metric_node.getAttribute('name')))
  108. elif not metric_node.hasAttribute('unit'):
  109. warnings.append("Warning: Neither \'enum\' or \'unit\' is specified "
  110. "for ukm metric %s:%s."
  111. % (event_node.getAttribute('name'),
  112. metric_node.getAttribute('name')))
  113. isSuccess = not errors
  114. return (isSuccess, errors, warnings)
  115. def checkLocalMetricIsAggregated(self):
  116. """Checks that index fields don't list invalid metrics."""
  117. errors = []
  118. for event_node in self.config.getElementsByTagName('event'):
  119. metric_nodes = event_node.getElementsByTagName('metric')
  120. valid_index_field_metrics = {node.getAttribute('name')
  121. for node in metric_nodes
  122. if _isMetricValidAsIndexField(node)}
  123. for metric_node in metric_nodes:
  124. local_metric_index_fields = _getLocalMetricIndexFields(metric_node)
  125. invalid_metrics = local_metric_index_fields - valid_index_field_metrics
  126. if invalid_metrics:
  127. event_name = event_node.getAttribute('name')
  128. metric_name = metric_node.getAttribute('name')
  129. invalid_metrics_string = ', '.join(sorted(invalid_metrics))
  130. errors.append(INVALID_LOCAL_METRIC_FIELD_ERROR %(
  131. {'event': event_name, 'metric': metric_name,
  132. 'invalid_metrics': invalid_metrics_string}))
  133. is_success = not errors
  134. return (is_success, errors)