Skip to content

Commit

Permalink
feat: Code locations for metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Nov 20, 2023
1 parent 91676ec commit 019f82a
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 7 deletions.
44 changes: 37 additions & 7 deletions sentry_sdk/metrics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import io
import re
import sys
import threading
import random
import time
Expand Down Expand Up @@ -57,6 +58,19 @@
)


def get_code_location(stacklevel):
try:
frm = sys._getframe(stacklevel + 3)
except Exception:
return None
return {
"line": frm.f_lineno,
"module": frm.f_globals.get("__name__"),
"filename": frm.f_code.co_filename,
"function": frm.f_code.co_name,
}


@contextmanager
def recursion_protection():
# type: () -> Generator[bool, None, None]
Expand Down Expand Up @@ -314,6 +328,7 @@ def __init__(
):
# type: (...) -> None
self.buckets = {} # type: Dict[int, Any]
self.code_locations = {} # type: Dict[BucketKey, Any]
self._buckets_total_weight = 0
self._capture_func = capture_func
self._lock = Lock()
Expand Down Expand Up @@ -409,6 +424,7 @@ def add(
unit, # type: MeasurementUnit
tags, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
stacklevel=1, # type: int
):
# type: (...) -> None
if not self._ensure_thread() or self._flusher is None:
Expand Down Expand Up @@ -441,6 +457,12 @@ def add(

self._buckets_total_weight += metric.weight - previous_weight

# Store code location once per bucket
if bucket_key not in self.code_locations:
loc = get_code_location(stacklevel)
if loc is not None:
self.code_locations[bucket_key] = loc

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

Expand Down Expand Up @@ -536,6 +558,7 @@ def incr(
unit="none", # type: MeasurementUnit
tags=None, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
stacklevel=1, # type: int
):
# type: (...) -> None
"""Increments a counter."""
Expand All @@ -552,6 +575,7 @@ def __init__(
timestamp, # type: Optional[Union[float, datetime]]
value, # type: Optional[float]
unit, # type: DurationUnit
stacklevel # type: int
):
# type: (...) -> None
self.key = key
Expand All @@ -560,6 +584,7 @@ def __init__(
self.value = value
self.unit = unit
self.entered = None # type: Optional[float]
self.stacklevel = stacklevel

def _validate_invocation(self, context):
# type: (str) -> None
Expand All @@ -579,7 +604,7 @@ def __exit__(self, exc_type, exc_value, tb):
aggregator, tags = _get_aggregator_and_update_tags(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("d", self.key, elapsed, self.unit, tags, self.timestamp, self.stacklevel)

def __call__(self, f):
# type: (Any) -> Any
Expand All @@ -589,7 +614,8 @@ def __call__(self, f):
def timed_func(*args, **kwargs):
# type: (*Any, **Any) -> Any
with timing(
key=self.key, tags=self.tags, timestamp=self.timestamp, unit=self.unit
key=self.key, tags=self.tags, timestamp=self.timestamp, unit=self.unit,
stacklevel=self.stacklevel + 1
):
return f(*args, **kwargs)

Expand All @@ -602,6 +628,7 @@ def timing(
unit="second", # type: DurationUnit
tags=None, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
stacklevel=1, # type: int
):
# type: (...) -> _Timing
"""Emits a distribution with the time it takes to run the given code block.
Expand All @@ -615,8 +642,8 @@ def timing(
if value is not None:
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
if aggregator is not None:
aggregator.add("d", key, value, unit, tags, timestamp)
return _Timing(key, tags, timestamp, value, unit)
aggregator.add("d", key, value, unit, tags, timestamp, stacklevel)
return _Timing(key, tags, timestamp, value, unit, stacklevel)


def distribution(
Expand All @@ -625,12 +652,13 @@ def distribution(
unit="none", # type: MeasurementUnit
tags=None, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
stacklevel=1, # type: int
):
# type: (...) -> None
"""Emits a distribution."""
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
if aggregator is not None:
aggregator.add("d", key, value, unit, tags, timestamp)
aggregator.add("d", key, value, unit, tags, timestamp, stacklevel)


def set(
Expand All @@ -639,12 +667,13 @@ def set(
unit="none", # type: MeasurementUnit
tags=None, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
stacklevel=1, # type: int
):
# type: (...) -> None
"""Emits a set."""
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
if aggregator is not None:
aggregator.add("s", key, value, unit, tags, timestamp)
aggregator.add("s", key, value, unit, tags, timestamp, stacklevel)


def gauge(
Expand All @@ -653,9 +682,10 @@ def gauge(
unit="none", # type: MetricValue
tags=None, # type: Optional[MetricTags]
timestamp=None, # type: Optional[Union[float, datetime]]
stacklevel=1, # type: int
):
# type: (...) -> None
"""Emits a gauge."""
aggregator, tags = _get_aggregator_and_update_tags(key, tags)
if aggregator is not None:
aggregator.add("g", key, value, unit, tags, timestamp)
aggregator.add("g", key, value, unit, tags, timestamp, stacklevel)
33 changes: 33 additions & 0 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ def test_timing(sentry_init, capture_envelopes):
"environment": "not-fun-env",
}

loc = Hub.current.client.metrics_aggregator.code_locations
first_loc, = loc.values()
assert first_loc["filename"] == __file__
assert first_loc["line"] > 0
assert first_loc["module"] == "tests.test_metrics"
assert first_loc["function"] == "test_timing"


def test_timing_decorator(sentry_init, capture_envelopes):
sentry_init(
Expand Down Expand Up @@ -147,6 +154,18 @@ def amazing_nano():
"environment": "not-fun-env",
}

loc = Hub.current.client.metrics_aggregator.code_locations
assert len(loc) == 2
first_loc, second_loc = loc.values()
assert first_loc["filename"] == __file__
assert first_loc["line"] > 0
assert first_loc["module"] == "tests.test_metrics"
assert first_loc["function"] == "test_timing_decorator"
assert second_loc["filename"] == __file__
assert second_loc["line"] > 0
assert second_loc["module"] == "tests.test_metrics"
assert second_loc["function"] == "test_timing_decorator"


def test_timing_basic(sentry_init, capture_envelopes):
sentry_init(
Expand Down Expand Up @@ -180,6 +199,13 @@ def test_timing_basic(sentry_init, capture_envelopes):
"environment": "not-fun-env",
}

loc = Hub.current.client.metrics_aggregator.code_locations
first_loc, = loc.values()
assert first_loc["filename"] == __file__
assert first_loc["line"] > 0
assert first_loc["module"] == "tests.test_metrics"
assert first_loc["function"] == "test_timing_basic"


def test_distribution(sentry_init, capture_envelopes):
sentry_init(
Expand Down Expand Up @@ -504,6 +530,13 @@ def test_tag_serialization(sentry_init, capture_envelopes):
"environment": "not-fun-env",
}

loc = Hub.current.client.metrics_aggregator.code_locations
first_loc = next(iter(loc.values()))
assert first_loc["filename"] == __file__
assert first_loc["line"] > 0
assert first_loc["module"] == "tests.test_metrics"
assert first_loc["function"] == "test_tag_serialization"


def test_flush_recursion_protection(sentry_init, capture_envelopes, monkeypatch):
sentry_init(
Expand Down

0 comments on commit 019f82a

Please sign in to comment.