# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Classes
for each of the high-level metric types.
"""
import enum
from typing
import Any, Dict, List, Optional, Type, Union
# noqa
from .
import pings
from .
import tags
from .
import util
# Important: if the values are ever changing here, make sure
# to also fix mozilla/glean. Otherwise language bindings may
# break there.
class Lifetime(enum.Enum):
ping = 0
application = 1
user = 2
class DataSensitivity(enum.Enum):
technical = 1
interaction = 2
stored_content = 3
web_activity = 3
# Old, deprecated name
highly_sensitive = 4
class Metric:
typename: str =
"ERROR"
glean_internal_metric_cat: str =
"glean.internal.metrics"
metric_types: Dict[str, Any] = {}
default_store_names: List[str] = [
"metrics"]
def __init__(
self,
type: str,
category: str,
name: str,
bugs: List[str],
description: str,
notification_emails: List[str],
expires: Any,
metadata: Optional[Dict] =
None,
data_reviews: Optional[List[str]] =
None,
version: int = 0,
disabled: bool =
False,
lifetime: str =
"ping",
send_in_pings: Optional[List[str]] =
None,
unit: Optional[str] =
None,
gecko_datapoint: str =
"",
no_lint: Optional[List[str]] =
None,
data_sensitivity: Optional[List[str]] =
None,
defined_in: Optional[Dict] =
None,
telemetry_mirror: Optional[str] =
None,
_config: Optional[Dict[str, Any]] =
None,
_validated: bool =
False,
):
# Avoid cyclical import
from .
import parser
self.type = type
self.category = category
self.name = name
self.bugs = bugs
self.description = description
self.notification_emails = notification_emails
self.expires = expires
if metadata
is None:
metadata = {}
self.metadata = metadata
if data_reviews
is None:
data_reviews = []
self.data_reviews = data_reviews
self.version = version
self.disabled = disabled
self.lifetime = getattr(Lifetime, lifetime)
if send_in_pings
is None:
send_in_pings = [
"default"]
self.send_in_pings = send_in_pings
self.unit = unit
self.gecko_datapoint = gecko_datapoint
if no_lint
is None:
no_lint = []
self.no_lint = no_lint
if data_sensitivity
is not None:
self.data_sensitivity = [
getattr(DataSensitivity, x)
for x
in data_sensitivity
]
self.defined_in = defined_in
if telemetry_mirror
is not None:
self.telemetry_mirror = telemetry_mirror
# _validated indicates whether this metric has already been jsonschema
# validated (but not any of the Python-level validation).
if not _validated:
data = {
"$schema": parser.METRICS_ID,
self.category: {self.name: self._serialize_input()},
}
# type: Dict[str, util.JSONType]
for error
in parser.validate(data):
raise ValueError(error)
# Store the config, but only after validation.
if _config
is None:
_config = {}
self._config = _config
# Metrics in the special category "glean.internal.metrics" need to have
# an empty category string when identifying the metrics in the ping.
if self.category == Metric.glean_internal_metric_cat:
self.category =
""
def __init_subclass__(cls, **kwargs):
# Create a mapping of all of the subclasses of this class
if cls
not in Metric.metric_types
and hasattr(cls,
"typename"):
Metric.metric_types[cls.typename] = cls
super().__init_subclass__(**kwargs)
@classmethod
def make_metric(
cls,
category: str,
name: str,
metric_info: Dict[str, util.JSONType],
config: Optional[Dict[str, Any]] =
None,
validated: bool =
False,
):
"""
Given a metric_info dictionary
from metrics.yaml,
return a metric
instance.
:param: category The category the metric lives
in
:param: name The name of the metric
:param: metric_info A dictionary of the remaining metric parameters
:param: config A dictionary containing commandline configuration
parameters
:param: validated
True if the metric has already gone through
jsonschema validation
:
return: A new Metric instance.
"""
if config
is None:
config = {}
metric_type = metric_info[
"type"]
if not isinstance(metric_type, str):
raise TypeError(f
"Unknown metric type {metric_type}")
return cls.metric_types[metric_type](
category=category,
name=name,
defined_in=getattr(metric_info,
"defined_in",
None),
_validated=validated,
_config=config,
**metric_info,
)
def serialize(self) -> Dict[str, util.JSONType]:
"""
Serialize the metric back to JSON object model.
"""
d = self.__dict__.copy()
# Convert enum fields back to strings
for key, val
in d.items():
if isinstance(val, enum.Enum):
d[key] = d[key].name
if isinstance(val, set):
d[key] = sorted(list(val))
if isinstance(val, list)
and len(val)
and isinstance(val[0], enum.Enum):
d[key] = [x.name
for x
in val]
del d[
"name"]
del d[
"category"]
if not d[
"unit"]:
d.pop(
"unit")
d.pop(
"_config",
None)
d.pop(
"_generate_enums",
None)
d.pop(
"_generate_structure",
None)
return d
def _serialize_input(self) -> Dict[str, util.JSONType]:
d = self.serialize()
modified_dict = util.remove_output_params(d,
"defined_in")
return modified_dict
def identifier(self) -> str:
"""
Create an identifier unique
for this metric.
Generally, category.name; however, Glean internal
metrics only use name.
"""
if not self.category:
return self.name
return ".".join((self.category, self.name))
def is_disabled(self) -> bool:
return self.disabled
or self.is_expired()
def is_expired(self) -> bool:
def default_handler(expires) -> bool:
return util.is_expired(expires, self._config.get(
"expire_by_version"))
return self._config.get(
"custom_is_expired", default_handler)(self.expires)
def validate_expires(self):
def default_handler(expires):
return util.validate_expires(expires, self._config.get(
"expire_by_version"))
return self._config.get(
"custom_validate_expires", default_handler)(
self.expires
)
def is_internal_metric(self) -> bool:
return self.category
in (Metric.glean_internal_metric_cat,
"")
class Boolean(Metric):
typename =
"boolean"
class String(Metric):
typename =
"string"
class StringList(Metric):
typename =
"string_list"
class Counter(Metric):
typename =
"counter"
class Quantity(Metric):
typename =
"quantity"
class TimeUnit(enum.Enum):
nanosecond = 0
microsecond = 1
millisecond = 2
second = 3
minute = 4
hour = 5
day = 6
class TimeBase(Metric):
def __init__(self, *args, **kwargs):
self.time_unit = getattr(TimeUnit, kwargs.pop(
"time_unit",
"millisecond"))
super().__init__(*args, **kwargs)
class Timespan(TimeBase):
typename =
"timespan"
class TimingDistribution(TimeBase):
typename =
"timing_distribution"
def __init__(self, *args, **kwargs):
self.time_unit = getattr(TimeUnit, kwargs.pop(
"time_unit",
"nanosecond"))
Metric.__init__(self, *args, **kwargs)
class MemoryUnit(enum.Enum):
byte = 0
kilobyte = 1
megabyte = 2
gigabyte = 3
class MemoryDistribution(Metric):
typename =
"memory_distribution"
def __init__(self, *args, **kwargs):
self.memory_unit = getattr(MemoryUnit, kwargs.pop(
"memory_unit",
"byte"))
super().__init__(*args, **kwargs)
class HistogramType(enum.Enum):
linear = 0
exponential = 1
class CustomDistribution(Metric):
typename =
"custom_distribution"
def __init__(self, *args, **kwargs):
self.range_min = kwargs.pop(
"range_min", 1)
self.range_max = kwargs.pop(
"range_max")
self.bucket_count = kwargs.pop(
"bucket_count")
self.histogram_type = getattr(
HistogramType, kwargs.pop(
"histogram_type",
"exponential")
)
super().__init__(*args, **kwargs)
class Datetime(TimeBase):
typename =
"datetime"
class Event(Metric):
typename =
"event"
default_store_names = [
"events"]
def __init__(self, *args, **kwargs):
self.extra_keys = kwargs.pop(
"extra_keys", {})
self.validate_extra_keys(self.extra_keys, kwargs.get(
"_config", {}))
super().__init__(*args, **kwargs)
self._generate_enums = [(
"allowed_extra_keys_with_types",
"Extra")]
@property
def allowed_extra_keys(self):
# Sort keys so that output is deterministic
return sorted(list(self.extra_keys.keys()))
@property
def allowed_extra_keys_with_types(self):
# Sort keys so that output is deterministic
return sorted(
[(k, v.get(
"type",
"string"))
for (k, v)
in self.extra_keys.items()],
key=
lambda x: x[0],
)
@staticmethod
def validate_extra_keys(extra_keys: Dict[str, str], config: Dict[str, Any]) ->
None:
if not config.get(
"allow_reserved")
and any(
k.startswith(
"glean.")
for k
in extra_keys.keys()
):
raise ValueError(
"Extra keys beginning with 'glean.' are reserved for "
"Glean internal use."
)
class Uuid(Metric):
typename =
"uuid"
class Url(Metric):
typename =
"url"
class Jwe(Metric):
typename =
"jwe"
def __init__(self, *args, **kwargs):
raise ValueError(
"JWE support was removed. "
"If you require this send an email to glean-team@mozilla.com."
)
class CowString(str):
"""
Wrapper
class for strings that should be represented
as a `Cow<
'static, str>` in Rust,
or `String`
in other target languages.
This wraps `str`, so unless `CowString`
is specifically
handled it acts (
and serializes)
as a string.
"""
def __init__(self, val: str):
self.inner: str = val
def __eq__(self, other):
return self.inner == other.inner
def __hash__(self):
return self.inner.__hash__()
def __lt__(self, other):
return self.inner.__lt__(other.inner)
class Labeled(Metric):
labeled =
True
def __init__(self, *args, **kwargs):
labels = kwargs.pop(
"labels",
None)
if labels
is not None:
self.ordered_labels = labels
self.labels = set([CowString(label)
for label
in labels])
else:
self.ordered_labels =
None
self.labels =
None
super().__init__(*args, **kwargs)
def serialize(self) -> Dict[str, util.JSONType]:
"""
Serialize the metric back to JSON object model.
"""
d = super().serialize()
d[
"labels"] = self.ordered_labels
del d[
"ordered_labels"]
return d
class LabeledBoolean(Labeled, Boolean):
typename =
"labeled_boolean"
class LabeledString(Labeled, String):
typename =
"labeled_string"
class LabeledCounter(Labeled, Counter):
typename =
"labeled_counter"
class LabeledCustomDistribution(Labeled, CustomDistribution):
typename =
"labeled_custom_distribution"
class LabeledMemoryDistribution(Labeled, MemoryDistribution):
typename =
"labeled_memory_distribution"
class LabeledTimingDistribution(Labeled, TimingDistribution):
typename =
"labeled_timing_distribution"
class LabeledQuantity(Labeled, Quantity):
typename =
"labeled_quantity"
class Rate(Metric):
typename =
"rate"
def __init__(self, *args, **kwargs):
self.denominator_metric = kwargs.pop(
"denominator_metric",
None)
super().__init__(*args, **kwargs)
class Denominator(Counter):
typename =
"denominator"
# A denominator is a counter with an additional list of numerators.
numerators: List[Rate] = []
class Text(Metric):
typename =
"text"
class Object(Metric):
typename =
"object"
def __init__(self, *args, **kwargs):
structure = kwargs.pop(
"structure",
None)
if not structure:
raise ValueError(
"`object` is missing required parameter `structure`")
self._generate_structure = self.validate_structure(structure)
super().__init__(*args, **kwargs)
ALLOWED_TOPLEVEL = {
"type",
"properties",
"items"}
ALLOWED_TYPES = [
"object",
"array",
"number",
"string",
"boolean"]
@staticmethod
def _validate_substructure(structure):
extra = set(structure.keys()) - Object.ALLOWED_TOPLEVEL
if extra:
extra =
", ".join(extra)
allowed =
", ".join(Object.ALLOWED_TOPLEVEL)
raise ValueError(
f
"Found additional fields: {extra}. Only allowed: {allowed}"
)
if "type" not in structure:
raise ValueError(
f
"missing `type` in object structure. Allowed: {Object.ALLOWED_TYPES}"
)
if structure[
"type"]
not in Object.ALLOWED_TYPES:
raise ValueError(
"invalid `type` in object structure. found: {}, allowed: {}".format(
structure[
"type"], Object.ALLOWED_TYPES
)
)
if structure[
"type"] ==
"object":
if "items" in structure:
raise ValueError(
"`items` not allowed in object structure")
if "properties" not in structure:
raise ValueError(
"`properties` missing for type `object`")
for key
in structure[
"properties"]:
value = structure[
"properties"][key]
structure[
"properties"][key] = Object._validate_substructure(value)
if structure[
"type"] ==
"array":
if "properties" in structure:
raise ValueError(
"`properties` not allowed in array structure")
if "items" not in structure:
raise ValueError(
"`items` missing for type `array`")
value = structure[
"items"]
structure[
"items"] = Object._validate_substructure(value)
return structure
@staticmethod
def validate_structure(structure):
if None:
raise ValueError(
"`structure` needed for object metric.")
# Different from `ALLOWED_TYPES`:
# We _require_ the root type to be an object or array.
allowed_types = [
"object",
"array"]
if "type" not in structure:
raise ValueError(
f
"missing `type` in object structure. Allowed: {allowed_types}"
)
if structure[
"type"]
not in allowed_types:
raise ValueError(
"invalid `type` in object structure. found: {}, allowed: {}".format(
structure[
"type"], allowed_types
)
)
structure = Object._validate_substructure(structure)
return structure
ObjectTree = Dict[str, Dict[str, Union[Metric, pings.Ping, tags.Tag]]]