|
@@ -4,7 +4,7 @@ from collections import defaultdict
|
|
|
from collections.abc import Sequence
|
|
|
from dataclasses import dataclass
|
|
|
from datetime import timedelta
|
|
|
-from typing import Any, TypedDict
|
|
|
+from typing import Any, NotRequired, TypedDict
|
|
|
|
|
|
import sentry_sdk
|
|
|
from celery.exceptions import SoftTimeLimitExceeded
|
|
@@ -42,6 +42,8 @@ from sentry.snuba.metrics.extraction import (
|
|
|
OnDemandMetricSpecVersioning,
|
|
|
RuleCondition,
|
|
|
SpecVersion,
|
|
|
+ TagMapping,
|
|
|
+ TagSpec,
|
|
|
are_specs_equal,
|
|
|
should_use_on_demand_metrics,
|
|
|
)
|
|
@@ -57,7 +59,7 @@ logger = logging.getLogger(__name__)
|
|
|
# GENERIC METRIC EXTRACTION
|
|
|
|
|
|
# Version of the metric extraction config.
|
|
|
-_METRIC_EXTRACTION_VERSION = 3
|
|
|
+_METRIC_EXTRACTION_VERSION = 4
|
|
|
|
|
|
# Maximum number of custom metrics that can be extracted for alerts and widgets with
|
|
|
# advanced filter expressions.
|
|
@@ -831,6 +833,12 @@ _HISTOGRAM_OUTLIERS_TARGET_METRICS = {
|
|
|
"fcp": "d:transactions/measurements.fcp@millisecond",
|
|
|
}
|
|
|
|
|
|
+_HISTOGRAM_OUTLIERS_SOURCE_FIELDS = {
|
|
|
+ "duration": "event.duration",
|
|
|
+ "lcp": "event.measurements.lcp.value",
|
|
|
+ "fcp": "event.measurements.fcp.value",
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
@dataclass
|
|
|
class _DefaultThreshold:
|
|
@@ -876,8 +884,6 @@ def get_metric_conditional_tagging_rules(
|
|
|
except ProjectTransactionThreshold.DoesNotExist:
|
|
|
rules.extend(_threshold_to_rules(_DEFAULT_THRESHOLD, []))
|
|
|
|
|
|
- rules.extend(HISTOGRAM_OUTLIER_RULES)
|
|
|
-
|
|
|
return rules
|
|
|
|
|
|
|
|
@@ -1395,8 +1401,8 @@ def _parse_percentiles(value: tuple[()] | tuple[str, str, str, str, str]) -> tup
|
|
|
return p25, p75
|
|
|
|
|
|
|
|
|
-def _produce_histogram_outliers(query_results: Any) -> Sequence[MetricConditionalTaggingRule]:
|
|
|
- rules: list[MetricConditionalTaggingRule] = []
|
|
|
+def _produce_histogram_outliers(query_results: Any) -> list[TagMapping]:
|
|
|
+ tags_by_metric: dict[str, list[TagSpec]] = {}
|
|
|
for row in query_results:
|
|
|
platform = row["platform"]
|
|
|
op = row["op"]
|
|
@@ -1416,7 +1422,7 @@ def _produce_histogram_outliers(query_results: Any) -> Sequence[MetricConditiona
|
|
|
# default values from clickhouse if no data is present
|
|
|
continue
|
|
|
|
|
|
- rules.append(
|
|
|
+ tags_by_metric.setdefault(_HISTOGRAM_OUTLIERS_TARGET_METRICS[metric], []).append(
|
|
|
{
|
|
|
"condition": {
|
|
|
"op": "and",
|
|
@@ -1427,37 +1433,48 @@ def _produce_histogram_outliers(query_results: Any) -> Sequence[MetricConditiona
|
|
|
# See also https://en.wikipedia.org/wiki/Outlier#Tukey's_fences
|
|
|
{
|
|
|
"op": "gte",
|
|
|
- "name": "event.duration",
|
|
|
+ "name": _HISTOGRAM_OUTLIERS_SOURCE_FIELDS[metric],
|
|
|
"value": p75 + 3 * abs(p75 - p25),
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
- "targetMetrics": [_HISTOGRAM_OUTLIERS_TARGET_METRICS[metric]],
|
|
|
- "targetTag": "histogram_outlier",
|
|
|
- "tagValue": "outlier",
|
|
|
+ "key": "histogram_outlier",
|
|
|
+ "value": "outlier",
|
|
|
}
|
|
|
)
|
|
|
|
|
|
+ rules: list[TagMapping] = [
|
|
|
+ {"metrics": [metric], "tags": tags} for metric, tags in tags_by_metric.items()
|
|
|
+ ]
|
|
|
+
|
|
|
rules.append(
|
|
|
{
|
|
|
- "condition": {
|
|
|
- "op": "and",
|
|
|
- "inner": [
|
|
|
- {"op": "gte", "name": "event.duration", "value": 0},
|
|
|
- ],
|
|
|
- },
|
|
|
- "targetMetrics": list(_HISTOGRAM_OUTLIERS_TARGET_METRICS.values()),
|
|
|
- "targetTag": "histogram_outlier",
|
|
|
- "tagValue": "inlier",
|
|
|
+ "metrics": list(_HISTOGRAM_OUTLIERS_TARGET_METRICS.values()),
|
|
|
+ "tags": [
|
|
|
+ {
|
|
|
+ "condition": {
|
|
|
+ "op": "and",
|
|
|
+ "inner": [
|
|
|
+ {"op": "gte", "name": "event.duration", "value": 0},
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "key": "histogram_outlier",
|
|
|
+ "value": "inlier",
|
|
|
+ },
|
|
|
+ ],
|
|
|
}
|
|
|
)
|
|
|
|
|
|
rules.append(
|
|
|
{
|
|
|
- "condition": {"op": "and", "inner": []},
|
|
|
- "targetMetrics": list(_HISTOGRAM_OUTLIERS_TARGET_METRICS.values()),
|
|
|
- "targetTag": "histogram_outlier",
|
|
|
- "tagValue": "outlier",
|
|
|
+ "metrics": list(_HISTOGRAM_OUTLIERS_TARGET_METRICS.values()),
|
|
|
+ "tags": [
|
|
|
+ {
|
|
|
+ "condition": {"op": "and", "inner": []},
|
|
|
+ "key": "histogram_outlier",
|
|
|
+ "value": "outlier",
|
|
|
+ }
|
|
|
+ ],
|
|
|
}
|
|
|
)
|
|
|
|
|
@@ -1492,3 +1509,28 @@ def widget_exceeds_max_specs(
|
|
|
|
|
|
|
|
|
HISTOGRAM_OUTLIER_RULES = _produce_histogram_outliers(_HISTOGRAM_OUTLIERS_QUERY_RESULTS)
|
|
|
+
|
|
|
+
|
|
|
+class MetricExtractionGroup(TypedDict):
|
|
|
+ #: Whether a group of globally defined metrics and/or tags is enabled by default for every project.
|
|
|
+ #: This can be overridden in project configs.
|
|
|
+ isEnabled: bool
|
|
|
+ #: List of metrics to extract.
|
|
|
+ metrics: NotRequired[list[MetricSpec]]
|
|
|
+ #: List of tags to apply to previously extracted metrics.
|
|
|
+ tags: NotRequired[list[TagMapping]]
|
|
|
+
|
|
|
+
|
|
|
+class MetricExtractionGroups(TypedDict):
|
|
|
+ groups: dict[str, MetricExtractionGroup]
|
|
|
+
|
|
|
+
|
|
|
+def global_metric_extraction_groups() -> MetricExtractionGroups:
|
|
|
+ return {
|
|
|
+ "groups": {
|
|
|
+ "histogram_outliers": {
|
|
|
+ "isEnabled": True, # enabled by default
|
|
|
+ "tags": HISTOGRAM_OUTLIER_RULES,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|