Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: metric span summaries #2522

Merged
merged 16 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"transport_zlib_compression_level": Optional[int],
"transport_num_pools": Optional[int],
"enable_metrics": Optional[bool],
"metrics_summary_sample_rate": Optional[float],
"should_summarize_metric": Optional[Callable[[str, MetricTags], bool]],
"before_emit_metric": Optional[Callable[[str, MetricTags], bool]],
},
total=False,
Expand Down
211 changes: 165 additions & 46 deletions sentry_sdk/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,58 @@
}


class LocalAggregator(object):
__slots__ = ("_measurements",)

def __init__(self):
# type: (...) -> None
self._measurements = (

Check warning on line 311 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L311

Added line #L311 was not covered by tests
{}
) # type: Dict[Tuple[str, MetricTagsInternal], Tuple[float, float, int, float]]

def add(
self,
ty, # type: MetricType
key, # type: str
value, # type: float
unit, # type: MeasurementUnit
tags, # type: MetricTagsInternal
):
# type: (...) -> None
export_key = "%s:%s@%s" % (ty, key, unit)
bucket_key = (export_key, tags)

Check warning on line 325 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L324-L325

Added lines #L324 - L325 were not covered by tests

old = self._measurements.get(bucket_key)

Check warning on line 327 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L327

Added line #L327 was not covered by tests
if old is not None:
v_min, v_max, v_count, v_sum = old
v_min = min(v_min, value)
v_max = max(v_max, value)
v_count += 1
v_sum += value

Check warning on line 333 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L329-L333

Added lines #L329 - L333 were not covered by tests
else:
v_min = v_max = v_sum = value
v_count = 1
self._measurements[bucket_key] = (v_min, v_max, v_count, v_sum)

Check warning on line 337 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L335-L337

Added lines #L335 - L337 were not covered by tests

def to_json(self):
# type: (...) -> Dict[str, Any]
rv = {}

Check warning on line 341 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L341

Added line #L341 was not covered by tests
for (export_key, tags), (
v_min,
v_max,
v_count,
v_sum,
) in self._measurements.items():
rv[export_key] = {

Check warning on line 348 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L348

Added line #L348 was not covered by tests
"tags": _tags_to_dict(tags),
"min": v_min,
"max": v_max,
"count": v_count,
"sum": v_sum,
}
return rv

Check warning on line 355 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L355

Added line #L355 was not covered by tests


class MetricsAggregator(object):
ROLLUP_IN_SECONDS = 10.0
MAX_WEIGHT = 100000
Expand Down Expand Up @@ -409,10 +461,11 @@
unit, # type: MeasurementUnit
tags, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
local_aggregator=None, # type: Optional[LocalAggregator]
):
# type: (...) -> None
if not self._ensure_thread() or self._flusher is None:
return
return None

Check warning on line 468 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L468

Added line #L468 was not covered by tests

if timestamp is None:
timestamp = time.time()
Expand All @@ -422,11 +475,12 @@
bucket_timestamp = int(
(timestamp // self.ROLLUP_IN_SECONDS) * self.ROLLUP_IN_SECONDS
)
serialized_tags = _serialize_tags(tags)

Check warning on line 478 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L478

Added line #L478 was not covered by tests
bucket_key = (
ty,
key,
unit,
self._serialize_tags(tags),
serialized_tags,
)

with self._lock:
Expand All @@ -439,11 +493,16 @@
metric = local_buckets[bucket_key] = METRIC_TYPES[ty](value)
previous_weight = 0

self._buckets_total_weight += metric.weight - previous_weight
added = metric.weight - previous_weight
self._buckets_total_weight += added

Check warning on line 497 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L496-L497

Added lines #L496 - L497 were not covered by tests

# Given the new weight we consider whether we want to force flush.
self._consider_force_flush()

if local_aggregator is not None:
local_value = float(added if ty == "s" else value)
local_aggregator.add(ty, key, local_value, unit, serialized_tags)

Check warning on line 504 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L503-L504

Added lines #L503 - L504 were not covered by tests

def kill(self):
# type: (...) -> None
if self._flusher is None:
Expand Down Expand Up @@ -479,55 +538,93 @@
self._capture_func(envelope)
return envelope

def _serialize_tags(
self, tags # type: Optional[MetricTags]
):
# type: (...) -> MetricTagsInternal
if not tags:
return ()

rv = []
for key, value in tags.items():
# If the value is a collection, we want to flatten it.
if isinstance(value, (list, tuple)):
for inner_value in value:
if inner_value is not None:
rv.append((key, text_type(inner_value)))
elif value is not None:
rv.append((key, text_type(value)))

# It's very important to sort the tags in order to obtain the
# same bucket key.
return tuple(sorted(rv))
def _serialize_tags(
tags, # type: Optional[MetricTags]
):
# type: (...) -> MetricTagsInternal
if not tags:
return ()

Check warning on line 547 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L547

Added line #L547 was not covered by tests

rv = []

Check warning on line 549 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L549

Added line #L549 was not covered by tests
for key, value in tags.items():
# If the value is a collection, we want to flatten it.
if isinstance(value, (list, tuple)):
for inner_value in value:
if inner_value is not None:
rv.append((key, text_type(inner_value)))

Check warning on line 555 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L555

Added line #L555 was not covered by tests
elif value is not None:
rv.append((key, text_type(value)))

Check warning on line 557 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L557

Added line #L557 was not covered by tests

# It's very important to sort the tags in order to obtain the
# same bucket key.
return tuple(sorted(rv))

Check warning on line 561 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L561

Added line #L561 was not covered by tests


def _tags_to_dict(tags):
# type: (MetricTagsInternal) -> Dict[str, Any]
rv = {} # type: Dict[str, Any]

Check warning on line 566 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L566

Added line #L566 was not covered by tests
for tag_name, tag_value in tags:
old_value = rv.get(tag_name)

Check warning on line 568 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L568

Added line #L568 was not covered by tests
if old_value is not None:
if isinstance(old_value, list):
old_value.append(tag_value)

Check warning on line 571 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L571

Added line #L571 was not covered by tests
else:
rv[tag_name] = [old_value, tag_value]

Check warning on line 573 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L573

Added line #L573 was not covered by tests
else:
rv[tag_name] = tag_value
return rv

Check warning on line 576 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L575-L576

Added lines #L575 - L576 were not covered by tests


def _get_aggregator_and_update_tags(key, tags):
# type: (str, Optional[MetricTags]) -> Tuple[Optional[MetricsAggregator], Optional[MetricTags]]
# type: (str, Optional[MetricTags]) -> Tuple[Optional[MetricsAggregator], Optional[LocalAggregator], Optional[MetricTags]]
"""Returns the current metrics aggregator if there is one."""
hub = sentry_sdk.Hub.current
client = hub.client
if client is None or client.metrics_aggregator is None:
return None, tags
return None, None, tags

Check warning on line 585 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L585

Added line #L585 was not covered by tests

experiments = client.options.get("_experiments", {})

Check warning on line 587 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L587

Added line #L587 was not covered by tests

updated_tags = dict(tags or ()) # type: Dict[str, MetricTagValue]
updated_tags.setdefault("release", client.options["release"])
updated_tags.setdefault("environment", client.options["environment"])

scope = hub.scope
transaction_source = scope._transaction_info.get("source")

# This workaround is needed as `scope._transaction_info` does not
# always appear to hold the information.
transaction = scope.transaction

Check warning on line 597 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L597

Added line #L597 was not covered by tests
if transaction:
transaction_name = transaction.name
transaction_source = transaction.source

Check warning on line 600 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L599-L600

Added lines #L599 - L600 were not covered by tests
else:
transaction_name = scope._transaction
transaction_source = scope._transaction_info.get("source")

Check warning on line 603 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L602-L603

Added lines #L602 - L603 were not covered by tests

local_aggregator = None

Check warning on line 605 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L605

Added line #L605 was not covered by tests
if transaction_source in GOOD_TRANSACTION_SOURCES:
transaction = scope._transaction
if transaction:
updated_tags.setdefault("transaction", transaction)
if transaction_name:
updated_tags.setdefault("transaction", transaction_name)

Check warning on line 608 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L608

Added line #L608 was not covered by tests
if scope._span is not None:
sample_rate = experiments.get("metrics_summary_sample_rate") or 0.0
should_summarize_metric_callback = experiments.get(

Check warning on line 611 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L610-L611

Added lines #L610 - L611 were not covered by tests
"should_summarize_metric"
)
if random.random() < sample_rate and (
should_summarize_metric_callback is None
or should_summarize_metric_callback(key, updated_tags)
):
local_aggregator = scope._span._get_local_aggregator()

Check warning on line 618 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L618

Added line #L618 was not covered by tests

callback = client.options.get("_experiments", {}).get("before_emit_metric")
if callback is not None:
before_emit_callback = experiments.get("before_emit_metric")

Check warning on line 620 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L620

Added line #L620 was not covered by tests
if before_emit_callback is not None:
with recursion_protection() as in_metrics:
if not in_metrics:
if not callback(key, updated_tags):
return None, updated_tags
if not before_emit_callback(key, updated_tags):
return None, None, updated_tags

Check warning on line 625 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L625

Added line #L625 was not covered by tests

return client.metrics_aggregator, updated_tags
return client.metrics_aggregator, local_aggregator, updated_tags

Check warning on line 627 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L627

Added line #L627 was not covered by tests


def incr(
Expand All @@ -539,9 +636,9 @@
):
# type: (...) -> None
"""Increments a counter."""
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
aggregator, local_aggregator, tags = _get_aggregator_and_update_tags(key, tags)

Check warning on line 639 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L639

Added line #L639 was not covered by tests
if aggregator is not None:
aggregator.add("c", key, value, unit, tags, timestamp)
aggregator.add("c", key, value, unit, tags, timestamp, local_aggregator)

Check warning on line 641 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L641

Added line #L641 was not covered by tests


class _Timing(object):
Expand All @@ -560,6 +657,7 @@
self.value = value
self.unit = unit
self.entered = None # type: Optional[float]
self._span = None # type: Optional[sentry_sdk.tracing.Span]

Check warning on line 660 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L660

Added line #L660 was not covered by tests

def _validate_invocation(self, context):
# type: (str) -> None
Expand All @@ -572,14 +670,35 @@
# type: (...) -> _Timing
self.entered = TIMING_FUNCTIONS[self.unit]()
self._validate_invocation("context-manager")
self._span = sentry_sdk.start_span(op="metric.timing", description=self.key)

Check warning on line 673 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L673

Added line #L673 was not covered by tests
if self.tags:
for key, value in self.tags.items():
if isinstance(value, (tuple, list)):
value = ",".join(sorted(map(str, value)))
self._span.set_tag(key, value)
self._span.__enter__()

Check warning on line 679 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L677-L679

Added lines #L677 - L679 were not covered by tests
return self

def __exit__(self, exc_type, exc_value, tb):
# type: (Any, Any, Any) -> None
aggregator, tags = _get_aggregator_and_update_tags(self.key, self.tags)
assert self._span, "did not enter"
aggregator, local_aggregator, tags = _get_aggregator_and_update_tags(

Check warning on line 685 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L684-L685

Added lines #L684 - L685 were not covered by tests
self.key, self.tags
)
if aggregator is not None:
elapsed = TIMING_FUNCTIONS[self.unit]() - self.entered # type: ignore
aggregator.add("d", self.key, elapsed, self.unit, tags, self.timestamp)
aggregator.add(

Check warning on line 690 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L690

Added line #L690 was not covered by tests
"d",
self.key,
elapsed,
self.unit,
tags,
self.timestamp,
local_aggregator,
)

self._span.__exit__(exc_type, exc_value, tb)
self._span = None

Check warning on line 701 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L700-L701

Added lines #L700 - L701 were not covered by tests

def __call__(self, f):
# type: (Any) -> Any
Expand Down Expand Up @@ -613,9 +732,9 @@
- it can be used as a decorator
"""
if value is not None:
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
aggregator, local_aggregator, tags = _get_aggregator_and_update_tags(key, tags)

Check warning on line 735 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L735

Added line #L735 was not covered by tests
if aggregator is not None:
aggregator.add("d", key, value, unit, tags, timestamp)
aggregator.add("d", key, value, unit, tags, timestamp, local_aggregator)

Check warning on line 737 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L737

Added line #L737 was not covered by tests
return _Timing(key, tags, timestamp, value, unit)


Expand All @@ -628,9 +747,9 @@
):
# type: (...) -> None
"""Emits a distribution."""
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
aggregator, local_aggregator, tags = _get_aggregator_and_update_tags(key, tags)

Check warning on line 750 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L750

Added line #L750 was not covered by tests
if aggregator is not None:
aggregator.add("d", key, value, unit, tags, timestamp)
aggregator.add("d", key, value, unit, tags, timestamp, local_aggregator)

Check warning on line 752 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L752

Added line #L752 was not covered by tests


def set(
Expand All @@ -642,20 +761,20 @@
):
# type: (...) -> None
"""Emits a set."""
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
aggregator, local_aggregator, tags = _get_aggregator_and_update_tags(key, tags)

Check warning on line 764 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L764

Added line #L764 was not covered by tests
if aggregator is not None:
aggregator.add("s", key, value, unit, tags, timestamp)
aggregator.add("s", key, value, unit, tags, timestamp, local_aggregator)

Check warning on line 766 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L766

Added line #L766 was not covered by tests


def gauge(
key, # type: str
value, # type: float
unit="none", # type: MetricValue
unit="none", # type: MeasurementUnit
tags=None, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
):
# type: (...) -> None
"""Emits a gauge."""
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
aggregator, local_aggregator, tags = _get_aggregator_and_update_tags(key, tags)

Check warning on line 778 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L778

Added line #L778 was not covered by tests
if aggregator is not None:
aggregator.add("g", key, value, unit, tags, timestamp)
aggregator.add("g", key, value, unit, tags, timestamp, local_aggregator)

Check warning on line 780 in sentry_sdk/metrics.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/metrics.py#L780

Added line #L780 was not covered by tests