1
1
import os
2
2
import io
3
3
import re
4
+ import sys
4
5
import threading
5
6
import random
6
7
import time
11
12
from contextlib import contextmanager
12
13
13
14
import sentry_sdk
14
- from sentry_sdk ._compat import text_type
15
- from sentry_sdk .utils import now , nanosecond_time , to_timestamp
15
+ from sentry_sdk ._compat import text_type , utc_from_timestamp , iteritems
16
+ from sentry_sdk .utils import (
17
+ now ,
18
+ nanosecond_time ,
19
+ to_timestamp ,
20
+ serialize_frame ,
21
+ json_dumps ,
22
+ )
16
23
from sentry_sdk .envelope import Envelope , Item
17
24
from sentry_sdk .tracing import (
18
25
TRANSACTION_SOURCE_ROUTE ,
24
31
25
32
if TYPE_CHECKING :
26
33
from typing import Any
34
+ from typing import Callable
27
35
from typing import Dict
36
+ from typing import Generator
28
37
from typing import Iterable
29
- from typing import Callable
38
+ from typing import List
30
39
from typing import Optional
31
- from typing import Generator
40
+ from typing import Set
32
41
from typing import Tuple
33
42
from typing import Union
34
43
35
44
from sentry_sdk ._types import BucketKey
36
45
from sentry_sdk ._types import DurationUnit
37
46
from sentry_sdk ._types import FlushedMetricValue
38
47
from sentry_sdk ._types import MeasurementUnit
48
+ from sentry_sdk ._types import MetricMetaKey
39
49
from sentry_sdk ._types import MetricTagValue
40
50
from sentry_sdk ._types import MetricTags
41
51
from sentry_sdk ._types import MetricTagsInternal
46
56
_thread_local = threading .local ()
47
57
_sanitize_key = partial (re .compile (r"[^a-zA-Z0-9_/.-]+" ).sub , "_" )
48
58
_sanitize_value = partial (re .compile (r"[^\w\d_:/@\.{}\[\]$-]+" , re .UNICODE ).sub , "_" )
59
+ _set = set # set is shadowed below
49
60
50
61
GOOD_TRANSACTION_SOURCES = frozenset (
51
62
[
57
68
)
58
69
59
70
71
+ def get_code_location (stacklevel ):
72
+ # type: (int) -> Optional[Dict[str, Any]]
73
+ try :
74
+ frm = sys ._getframe (stacklevel + 4 )
75
+ except Exception :
76
+ return None
77
+
78
+ return serialize_frame (
79
+ frm , include_local_variables = False , include_source_context = False
80
+ )
81
+
82
+
60
83
@contextmanager
61
84
def recursion_protection ():
62
85
# type: () -> Generator[bool, None, None]
@@ -247,7 +270,7 @@ def _encode_metrics(flushable_buckets):
247
270
# relay side emission and should not happen commonly.
248
271
249
272
for timestamp , buckets in flushable_buckets :
250
- for bucket_key , metric in buckets . items ( ):
273
+ for bucket_key , metric in iteritems ( buckets ):
251
274
metric_type , metric_name , metric_unit , metric_tags = bucket_key
252
275
metric_name = _sanitize_key (metric_name )
253
276
_write (metric_name .encode ("utf-8" ))
@@ -283,6 +306,20 @@ def _encode_metrics(flushable_buckets):
283
306
return out .getvalue ()
284
307
285
308
309
+ def _encode_locations (timestamp , code_locations ):
310
+ # type: (int, Iterable[Tuple[MetricMetaKey, Dict[str, Any]]]) -> bytes
311
+ mapping = {} # type: Dict[str, List[Any]]
312
+
313
+ for key , loc in code_locations :
314
+ metric_type , name , unit = key
315
+ mri = "{}:{}@{}" .format (metric_type , _sanitize_key (name ), unit )
316
+
317
+ loc ["type" ] = "location"
318
+ mapping .setdefault (mri , []).append (loc )
319
+
320
+ return json_dumps ({"timestamp" : timestamp , "mapping" : mapping })
321
+
322
+
286
323
METRIC_TYPES = {
287
324
"c" : CounterMetric ,
288
325
"g" : GaugeMetric ,
@@ -311,9 +348,13 @@ class MetricsAggregator(object):
311
348
def __init__ (
312
349
self ,
313
350
capture_func , # type: Callable[[Envelope], None]
351
+ enable_code_locations = False , # type: bool
314
352
):
315
353
# type: (...) -> None
316
354
self .buckets = {} # type: Dict[int, Any]
355
+ self ._enable_code_locations = enable_code_locations
356
+ self ._seen_locations = _set () # type: Set[Tuple[int, MetricMetaKey]]
357
+ self ._pending_locations = {} # type: Dict[int, List[Tuple[MetricMetaKey, Any]]]
317
358
self ._buckets_total_weight = 0
318
359
self ._capture_func = capture_func
319
360
self ._lock = Lock ()
@@ -366,9 +407,7 @@ def _flush_loop(self):
366
407
367
408
def _flush (self ):
368
409
# type: (...) -> None
369
- flushable_buckets = self ._flushable_buckets ()
370
- if flushable_buckets :
371
- self ._emit (flushable_buckets )
410
+ self ._emit (self ._flushable_buckets (), self ._flushable_locations ())
372
411
373
412
def _flushable_buckets (self ):
374
413
# type: (...) -> (Iterable[Tuple[int, Dict[BucketKey, Metric]]])
@@ -385,21 +424,28 @@ def _flushable_buckets(self):
385
424
self ._force_flush = False
386
425
else :
387
426
flushable_buckets = []
388
- for buckets_timestamp , buckets in self .buckets . items ( ):
427
+ for buckets_timestamp , buckets in iteritems ( self .buckets ):
389
428
# If the timestamp of the bucket is newer that the rollup we want to skip it.
390
429
if buckets_timestamp <= cutoff :
391
430
flushable_buckets .append ((buckets_timestamp , buckets ))
392
431
393
432
# We will clear the elements while holding the lock, in order to avoid requesting it downstream again.
394
433
for buckets_timestamp , buckets in flushable_buckets :
395
- for _ , metric in buckets . items ( ):
434
+ for _ , metric in iteritems ( buckets ):
396
435
weight_to_remove += metric .weight
397
436
del self .buckets [buckets_timestamp ]
398
437
399
438
self ._buckets_total_weight -= weight_to_remove
400
439
401
440
return flushable_buckets
402
441
442
+ def _flushable_locations (self ):
443
+ # type: (...) -> Dict[int, List[Tuple[MetricMetaKey, Dict[str, Any]]]]
444
+ with self ._lock :
445
+ locations = self ._pending_locations
446
+ self ._pending_locations = {}
447
+ return locations
448
+
403
449
@metrics_noop
404
450
def add (
405
451
self ,
@@ -409,6 +455,7 @@ def add(
409
455
unit , # type: MeasurementUnit
410
456
tags , # type: Optional[MetricTags]
411
457
timestamp = None , # type: Optional[Union[float, datetime]]
458
+ stacklevel = 0 , # type: int
412
459
):
413
460
# type: (...) -> None
414
461
if not self ._ensure_thread () or self ._flusher is None :
@@ -441,6 +488,24 @@ def add(
441
488
442
489
self ._buckets_total_weight += metric .weight - previous_weight
443
490
491
+ # Store code location once per metric and per day (of bucket timestamp)
492
+ if self ._enable_code_locations :
493
+ meta_key = (ty , key , unit )
494
+ start_of_day = utc_from_timestamp (timestamp ).replace (
495
+ hour = 0 , minute = 0 , second = 0 , microsecond = 0 , tzinfo = None
496
+ )
497
+ start_of_day = int (to_timestamp (start_of_day ))
498
+
499
+ if (start_of_day , meta_key ) not in self ._seen_locations :
500
+ self ._seen_locations .add ((start_of_day , meta_key ))
501
+ loc = get_code_location (stacklevel )
502
+ if loc is not None :
503
+ # Group metadata by day to make flushing more efficient.
504
+ # There needs to be one envelope item per timestamp.
505
+ self ._pending_locations .setdefault (start_of_day , []).append (
506
+ (meta_key , loc )
507
+ )
508
+
444
509
# Given the new weight we consider whether we want to force flush.
445
510
self ._consider_force_flush ()
446
511
@@ -471,13 +536,23 @@ def _consider_force_flush(self):
471
536
def _emit (
472
537
self ,
473
538
flushable_buckets , # type: (Iterable[Tuple[int, Dict[BucketKey, Metric]]])
539
+ code_locations , # type: Dict[int, List[Tuple[MetricMetaKey, Dict[str, Any]]]]
474
540
):
475
- # type: (...) -> Envelope
476
- encoded_metrics = _encode_metrics (flushable_buckets )
477
- metric_item = Item (payload = encoded_metrics , type = "statsd" )
478
- envelope = Envelope (items = [metric_item ])
479
- self ._capture_func (envelope )
480
- return envelope
541
+ # type: (...) -> Optional[Envelope]
542
+ envelope = Envelope ()
543
+
544
+ if flushable_buckets :
545
+ encoded_metrics = _encode_metrics (flushable_buckets )
546
+ envelope .add_item (Item (payload = encoded_metrics , type = "statsd" ))
547
+
548
+ for timestamp , locations in iteritems (code_locations ):
549
+ encoded_locations = _encode_locations (timestamp , locations )
550
+ envelope .add_item (Item (payload = encoded_locations , type = "metric_meta" ))
551
+
552
+ if envelope .items :
553
+ self ._capture_func (envelope )
554
+ return envelope
555
+ return None
481
556
482
557
def _serialize_tags (
483
558
self , tags # type: Optional[MetricTags]
@@ -487,7 +562,7 @@ def _serialize_tags(
487
562
return ()
488
563
489
564
rv = []
490
- for key , value in tags . items ( ):
565
+ for key , value in iteritems ( tags ):
491
566
# If the value is a collection, we want to flatten it.
492
567
if isinstance (value , (list , tuple )):
493
568
for inner_value in value :
@@ -536,12 +611,13 @@ def incr(
536
611
unit = "none" , # type: MeasurementUnit
537
612
tags = None , # type: Optional[MetricTags]
538
613
timestamp = None , # type: Optional[Union[float, datetime]]
614
+ stacklevel = 0 , # type: int
539
615
):
540
616
# type: (...) -> None
541
617
"""Increments a counter."""
542
618
aggregator , tags = _get_aggregator_and_update_tags (key , tags )
543
619
if aggregator is not None :
544
- aggregator .add ("c" , key , value , unit , tags , timestamp )
620
+ aggregator .add ("c" , key , value , unit , tags , timestamp , stacklevel )
545
621
546
622
547
623
class _Timing (object ):
@@ -552,6 +628,7 @@ def __init__(
552
628
timestamp , # type: Optional[Union[float, datetime]]
553
629
value , # type: Optional[float]
554
630
unit , # type: DurationUnit
631
+ stacklevel , # type: int
555
632
):
556
633
# type: (...) -> None
557
634
self .key = key
@@ -560,6 +637,7 @@ def __init__(
560
637
self .value = value
561
638
self .unit = unit
562
639
self .entered = None # type: Optional[float]
640
+ self .stacklevel = stacklevel
563
641
564
642
def _validate_invocation (self , context ):
565
643
# type: (str) -> None
@@ -579,7 +657,9 @@ def __exit__(self, exc_type, exc_value, tb):
579
657
aggregator , tags = _get_aggregator_and_update_tags (self .key , self .tags )
580
658
if aggregator is not None :
581
659
elapsed = TIMING_FUNCTIONS [self .unit ]() - self .entered # type: ignore
582
- aggregator .add ("d" , self .key , elapsed , self .unit , tags , self .timestamp )
660
+ aggregator .add (
661
+ "d" , self .key , elapsed , self .unit , tags , self .timestamp , self .stacklevel
662
+ )
583
663
584
664
def __call__ (self , f ):
585
665
# type: (Any) -> Any
@@ -589,7 +669,11 @@ def __call__(self, f):
589
669
def timed_func (* args , ** kwargs ):
590
670
# type: (*Any, **Any) -> Any
591
671
with timing (
592
- key = self .key , tags = self .tags , timestamp = self .timestamp , unit = self .unit
672
+ key = self .key ,
673
+ tags = self .tags ,
674
+ timestamp = self .timestamp ,
675
+ unit = self .unit ,
676
+ stacklevel = self .stacklevel + 1 ,
593
677
):
594
678
return f (* args , ** kwargs )
595
679
@@ -602,6 +686,7 @@ def timing(
602
686
unit = "second" , # type: DurationUnit
603
687
tags = None , # type: Optional[MetricTags]
604
688
timestamp = None , # type: Optional[Union[float, datetime]]
689
+ stacklevel = 0 , # type: int
605
690
):
606
691
# type: (...) -> _Timing
607
692
"""Emits a distribution with the time it takes to run the given code block.
@@ -615,8 +700,8 @@ def timing(
615
700
if value is not None :
616
701
aggregator , tags = _get_aggregator_and_update_tags (key , tags )
617
702
if aggregator is not None :
618
- aggregator .add ("d" , key , value , unit , tags , timestamp )
619
- return _Timing (key , tags , timestamp , value , unit )
703
+ aggregator .add ("d" , key , value , unit , tags , timestamp , stacklevel )
704
+ return _Timing (key , tags , timestamp , value , unit , stacklevel )
620
705
621
706
622
707
def distribution (
@@ -625,12 +710,13 @@ def distribution(
625
710
unit = "none" , # type: MeasurementUnit
626
711
tags = None , # type: Optional[MetricTags]
627
712
timestamp = None , # type: Optional[Union[float, datetime]]
713
+ stacklevel = 0 , # type: int
628
714
):
629
715
# type: (...) -> None
630
716
"""Emits a distribution."""
631
717
aggregator , tags = _get_aggregator_and_update_tags (key , tags )
632
718
if aggregator is not None :
633
- aggregator .add ("d" , key , value , unit , tags , timestamp )
719
+ aggregator .add ("d" , key , value , unit , tags , timestamp , stacklevel )
634
720
635
721
636
722
def set (
@@ -639,12 +725,13 @@ def set(
639
725
unit = "none" , # type: MeasurementUnit
640
726
tags = None , # type: Optional[MetricTags]
641
727
timestamp = None , # type: Optional[Union[float, datetime]]
728
+ stacklevel = 0 , # type: int
642
729
):
643
730
# type: (...) -> None
644
731
"""Emits a set."""
645
732
aggregator , tags = _get_aggregator_and_update_tags (key , tags )
646
733
if aggregator is not None :
647
- aggregator .add ("s" , key , value , unit , tags , timestamp )
734
+ aggregator .add ("s" , key , value , unit , tags , timestamp , stacklevel )
648
735
649
736
650
737
def gauge (
@@ -653,9 +740,10 @@ def gauge(
653
740
unit = "none" , # type: MetricValue
654
741
tags = None , # type: Optional[MetricTags]
655
742
timestamp = None , # type: Optional[Union[float, datetime]]
743
+ stacklevel = 0 , # type: int
656
744
):
657
745
# type: (...) -> None
658
746
"""Emits a gauge."""
659
747
aggregator , tags = _get_aggregator_and_update_tags (key , tags )
660
748
if aggregator is not None :
661
- aggregator .add ("g" , key , value , unit , tags , timestamp )
749
+ aggregator .add ("g" , key , value , unit , tags , timestamp , stacklevel )
0 commit comments