|
7 | 7 | import zlib
|
8 | 8 | from functools import wraps, partial
|
9 | 9 | from threading import Event, Lock, Thread
|
| 10 | +from contextlib import contextmanager |
10 | 11 |
|
11 | 12 | from sentry_sdk._compat import text_type
|
12 | 13 | from sentry_sdk.hub import Hub
|
|
26 | 27 | from typing import Iterable
|
27 | 28 | from typing import Callable
|
28 | 29 | from typing import Optional
|
| 30 | + from typing import Generator |
29 | 31 | from typing import Tuple
|
30 | 32 |
|
31 | 33 | from sentry_sdk._types import BucketKey
|
|
53 | 55 | )
|
54 | 56 |
|
55 | 57 |
|
| 58 | +@contextmanager |
| 59 | +def recursion_protection(): |
| 60 | + # type: () -> Generator[bool, None, None] |
| 61 | + """Enters recursion protection and returns the old flag.""" |
| 62 | + try: |
| 63 | + in_metrics = _thread_local.in_metrics |
| 64 | + except AttributeError: |
| 65 | + in_metrics = False |
| 66 | + _thread_local.in_metrics = True |
| 67 | + try: |
| 68 | + yield in_metrics |
| 69 | + finally: |
| 70 | + _thread_local.in_metrics = in_metrics |
| 71 | + |
| 72 | + |
56 | 73 | def metrics_noop(func):
|
57 | 74 | # type: (Any) -> Any
|
| 75 | + """Convenient decorator that uses `recursion_protection` to |
| 76 | + make a function a noop. |
| 77 | + """ |
| 78 | + |
58 | 79 | @wraps(func)
|
59 | 80 | def new_func(*args, **kwargs):
|
60 | 81 | # type: (*Any, **Any) -> Any
|
61 |
| - try: |
62 |
| - in_metrics = _thread_local.in_metrics |
63 |
| - except AttributeError: |
64 |
| - in_metrics = False |
65 |
| - _thread_local.in_metrics = True |
66 |
| - try: |
| 82 | + with recursion_protection() as in_metrics: |
67 | 83 | if not in_metrics:
|
68 | 84 | return func(*args, **kwargs)
|
69 |
| - finally: |
70 |
| - _thread_local.in_metrics = in_metrics |
71 | 85 |
|
72 | 86 | return new_func
|
73 | 87 |
|
@@ -449,7 +463,16 @@ def _emit(
|
449 | 463 | encoded_metrics = _encode_metrics(flushable_buckets)
|
450 | 464 | metric_item = Item(payload=encoded_metrics, type="statsd")
|
451 | 465 | envelope = Envelope(items=[metric_item])
|
452 |
| - self._capture_func(envelope) |
| 466 | + |
| 467 | + # A malfunctioning transport might create a forever loop of metric |
| 468 | + # emission when it emits a metric in capture_envelope. We still |
| 469 | + # allow the capture to take place, but interior metric incr calls |
| 470 | + # or similar will be disabled. In the background thread this can |
| 471 | + # never happen, but in the force flush case which happens in the |
| 472 | + # foreground we might make it here unprotected. |
| 473 | + with recursion_protection(): |
| 474 | + self._capture_func(envelope) |
| 475 | + |
453 | 476 | return envelope
|
454 | 477 |
|
455 | 478 | def _serialize_tags(
|
@@ -495,8 +518,10 @@ def _get_aggregator_and_update_tags(key, tags):
|
495 | 518 |
|
496 | 519 | callback = client.options.get("_experiments", {}).get("before_emit_metric")
|
497 | 520 | if callback is not None:
|
498 |
| - if not callback(key, updated_tags): |
499 |
| - return None, updated_tags |
| 521 | + with recursion_protection() as in_metrics: |
| 522 | + if not in_metrics: |
| 523 | + if not callback(key, updated_tags): |
| 524 | + return None, updated_tags |
500 | 525 |
|
501 | 526 | return client.metrics_aggregator, updated_tags
|
502 | 527 |
|
|
0 commit comments