@@ -340,6 +340,58 @@ def _encode_locations(timestamp, code_locations):
340340}
341341
342342
343+ class LocalAggregator (object ):
344+ __slots__ = ("_measurements" ,)
345+
346+ def __init__ (self ):
347+ # type: (...) -> None
348+ self ._measurements = (
349+ {}
350+ ) # type: Dict[Tuple[str, MetricTagsInternal], Tuple[float, float, int, float]]
351+
352+ def add (
353+ self ,
354+ ty , # type: MetricType
355+ key , # type: str
356+ value , # type: float
357+ unit , # type: MeasurementUnit
358+ tags , # type: MetricTagsInternal
359+ ):
360+ # type: (...) -> None
361+ export_key = "%s:%s@%s" % (ty , key , unit )
362+ bucket_key = (export_key , tags )
363+
364+ old = self ._measurements .get (bucket_key )
365+ if old is not None :
366+ v_min , v_max , v_count , v_sum = old
367+ v_min = min (v_min , value )
368+ v_max = max (v_max , value )
369+ v_count += 1
370+ v_sum += value
371+ else :
372+ v_min = v_max = v_sum = value
373+ v_count = 1
374+ self ._measurements [bucket_key ] = (v_min , v_max , v_count , v_sum )
375+
376+ def to_json (self ):
377+ # type: (...) -> Dict[str, Any]
378+ rv = {}
379+ for (export_key , tags ), (
380+ v_min ,
381+ v_max ,
382+ v_count ,
383+ v_sum ,
384+ ) in self ._measurements .items ():
385+ rv [export_key ] = {
386+ "tags" : _tags_to_dict (tags ),
387+ "min" : v_min ,
388+ "max" : v_max ,
389+ "count" : v_count ,
390+ "sum" : v_sum ,
391+ }
392+ return rv
393+
394+
343395class MetricsAggregator (object ):
344396 ROLLUP_IN_SECONDS = 10.0
345397 MAX_WEIGHT = 100000
@@ -455,11 +507,12 @@ def add(
455507 unit , # type: MeasurementUnit
456508 tags , # type: Optional[MetricTags]
457509 timestamp = None , # type: Optional[Union[float, datetime]]
510+ local_aggregator = None , # type: Optional[LocalAggregator]
458511 stacklevel = 0 , # type: int
459512 ):
460513 # type: (...) -> None
461514 if not self ._ensure_thread () or self ._flusher is None :
462- return
515+ return None
463516
464517 if timestamp is None :
465518 timestamp = time .time ()
@@ -469,11 +522,12 @@ def add(
469522 bucket_timestamp = int (
470523 (timestamp // self .ROLLUP_IN_SECONDS ) * self .ROLLUP_IN_SECONDS
471524 )
525+ serialized_tags = _serialize_tags (tags )
472526 bucket_key = (
473527 ty ,
474528 key ,
475529 unit ,
476- self . _serialize_tags ( tags ) ,
530+ serialized_tags ,
477531 )
478532
479533 with self ._lock :
@@ -486,7 +540,8 @@ def add(
486540 metric = local_buckets [bucket_key ] = METRIC_TYPES [ty ](value )
487541 previous_weight = 0
488542
489- self ._buckets_total_weight += metric .weight - previous_weight
543+ added = metric .weight - previous_weight
544+ self ._buckets_total_weight += added
490545
491546 # Store code location once per metric and per day (of bucket timestamp)
492547 if self ._enable_code_locations :
@@ -509,6 +564,10 @@ def add(
509564 # Given the new weight we consider whether we want to force flush.
510565 self ._consider_force_flush ()
511566
567+ if local_aggregator is not None :
568+ local_value = float (added if ty == "s" else value )
569+ local_aggregator .add (ty , key , local_value , unit , serialized_tags )
570+
512571 def kill (self ):
513572 # type: (...) -> None
514573 if self ._flusher is None :
@@ -554,55 +613,87 @@ def _emit(
554613 return envelope
555614 return None
556615
557- def _serialize_tags (
558- self , tags # type: Optional[MetricTags]
559- ):
560- # type: (...) -> MetricTagsInternal
561- if not tags :
562- return ()
563-
564- rv = []
565- for key , value in iteritems (tags ):
566- # If the value is a collection, we want to flatten it.
567- if isinstance (value , (list , tuple )):
568- for inner_value in value :
569- if inner_value is not None :
570- rv .append ((key , text_type (inner_value )))
571- elif value is not None :
572- rv .append ((key , text_type (value )))
573616
574- # It's very important to sort the tags in order to obtain the
575- # same bucket key.
576- return tuple (sorted (rv ))
617+ def _serialize_tags (
618+ tags , # type: Optional[MetricTags]
619+ ):
620+ # type: (...) -> MetricTagsInternal
621+ if not tags :
622+ return ()
623+
624+ rv = []
625+ for key , value in iteritems (tags ):
626+ # If the value is a collection, we want to flatten it.
627+ if isinstance (value , (list , tuple )):
628+ for inner_value in value :
629+ if inner_value is not None :
630+ rv .append ((key , text_type (inner_value )))
631+ elif value is not None :
632+ rv .append ((key , text_type (value )))
633+
634+ # It's very important to sort the tags in order to obtain the
635+ # same bucket key.
636+ return tuple (sorted (rv ))
637+
638+
639+ def _tags_to_dict (tags ):
640+ # type: (MetricTagsInternal) -> Dict[str, Any]
641+ rv = {} # type: Dict[str, Any]
642+ for tag_name , tag_value in tags :
643+ old_value = rv .get (tag_name )
644+ if old_value is not None :
645+ if isinstance (old_value , list ):
646+ old_value .append (tag_value )
647+ else :
648+ rv [tag_name ] = [old_value , tag_value ]
649+ else :
650+ rv [tag_name ] = tag_value
651+ return rv
577652
578653
579654def _get_aggregator_and_update_tags (key , tags ):
580- # type: (str, Optional[MetricTags]) -> Tuple[Optional[MetricsAggregator], Optional[MetricTags]]
655+ # type: (str, Optional[MetricTags]) -> Tuple[Optional[MetricsAggregator], Optional[LocalAggregator], Optional[ MetricTags]]
581656 """Returns the current metrics aggregator if there is one."""
582657 hub = sentry_sdk .Hub .current
583658 client = hub .client
584659 if client is None or client .metrics_aggregator is None :
585- return None , tags
660+ return None , None , tags
661+
662+ experiments = client .options .get ("_experiments" , {})
586663
587664 updated_tags = dict (tags or ()) # type: Dict[str, MetricTagValue]
588665 updated_tags .setdefault ("release" , client .options ["release" ])
589666 updated_tags .setdefault ("environment" , client .options ["environment" ])
590667
591668 scope = hub .scope
669+ local_aggregator = None
670+
671+ # We go with the low-level API here to access transaction information as
672+ # this one is the same between just errors and errors + performance
592673 transaction_source = scope ._transaction_info .get ("source" )
593674 if transaction_source in GOOD_TRANSACTION_SOURCES :
594- transaction = scope ._transaction
595- if transaction :
596- updated_tags .setdefault ("transaction" , transaction )
675+ transaction_name = scope ._transaction
676+ if transaction_name :
677+ updated_tags .setdefault ("transaction" , transaction_name )
678+ if scope ._span is not None :
679+ sample_rate = experiments .get ("metrics_summary_sample_rate" ) or 0.0
680+ should_summarize_metric_callback = experiments .get (
681+ "should_summarize_metric"
682+ )
683+ if random .random () < sample_rate and (
684+ should_summarize_metric_callback is None
685+ or should_summarize_metric_callback (key , updated_tags )
686+ ):
687+ local_aggregator = scope ._span ._get_local_aggregator ()
597688
598- callback = client . options . get ( "_experiments" , {}) .get ("before_emit_metric" )
599- if callback is not None :
689+ before_emit_callback = experiments .get ("before_emit_metric" )
690+ if before_emit_callback is not None :
600691 with recursion_protection () as in_metrics :
601692 if not in_metrics :
602- if not callback (key , updated_tags ):
603- return None , updated_tags
693+ if not before_emit_callback (key , updated_tags ):
694+ return None , None , updated_tags
604695
605- return client .metrics_aggregator , updated_tags
696+ return client .metrics_aggregator , local_aggregator , updated_tags
606697
607698
608699def incr (
@@ -615,9 +706,11 @@ def incr(
615706):
616707 # type: (...) -> None
617708 """Increments a counter."""
618- aggregator , tags = _get_aggregator_and_update_tags (key , tags )
709+ aggregator , local_aggregator , tags = _get_aggregator_and_update_tags (key , tags )
619710 if aggregator is not None :
620- aggregator .add ("c" , key , value , unit , tags , timestamp , stacklevel )
711+ aggregator .add (
712+ "c" , key , value , unit , tags , timestamp , local_aggregator , stacklevel
713+ )
621714
622715
623716class _Timing (object ):
@@ -637,6 +730,7 @@ def __init__(
637730 self .value = value
638731 self .unit = unit
639732 self .entered = None # type: Optional[float]
733+ self ._span = None # type: Optional[sentry_sdk.tracing.Span]
640734 self .stacklevel = stacklevel
641735
642736 def _validate_invocation (self , context ):
@@ -650,17 +744,37 @@ def __enter__(self):
650744 # type: (...) -> _Timing
651745 self .entered = TIMING_FUNCTIONS [self .unit ]()
652746 self ._validate_invocation ("context-manager" )
747+ self ._span = sentry_sdk .start_span (op = "metric.timing" , description = self .key )
748+ if self .tags :
749+ for key , value in self .tags .items ():
750+ if isinstance (value , (tuple , list )):
751+ value = "," .join (sorted (map (str , value )))
752+ self ._span .set_tag (key , value )
753+ self ._span .__enter__ ()
653754 return self
654755
655756 def __exit__ (self , exc_type , exc_value , tb ):
656757 # type: (Any, Any, Any) -> None
657- aggregator , tags = _get_aggregator_and_update_tags (self .key , self .tags )
758+ assert self ._span , "did not enter"
759+ aggregator , local_aggregator , tags = _get_aggregator_and_update_tags (
760+ self .key , self .tags
761+ )
658762 if aggregator is not None :
659763 elapsed = TIMING_FUNCTIONS [self .unit ]() - self .entered # type: ignore
660764 aggregator .add (
661- "d" , self .key , elapsed , self .unit , tags , self .timestamp , self .stacklevel
765+ "d" ,
766+ self .key ,
767+ elapsed ,
768+ self .unit ,
769+ tags ,
770+ self .timestamp ,
771+ local_aggregator ,
772+ self .stacklevel ,
662773 )
663774
775+ self ._span .__exit__ (exc_type , exc_value , tb )
776+ self ._span = None
777+
664778 def __call__ (self , f ):
665779 # type: (Any) -> Any
666780 self ._validate_invocation ("decorator" )
@@ -698,9 +812,11 @@ def timing(
698812 - it can be used as a decorator
699813 """
700814 if value is not None :
701- aggregator , tags = _get_aggregator_and_update_tags (key , tags )
815+ aggregator , local_aggregator , tags = _get_aggregator_and_update_tags (key , tags )
702816 if aggregator is not None :
703- aggregator .add ("d" , key , value , unit , tags , timestamp , stacklevel )
817+ aggregator .add (
818+ "d" , key , value , unit , tags , timestamp , local_aggregator , stacklevel
819+ )
704820 return _Timing (key , tags , timestamp , value , unit , stacklevel )
705821
706822
@@ -714,9 +830,11 @@ def distribution(
714830):
715831 # type: (...) -> None
716832 """Emits a distribution."""
717- aggregator , tags = _get_aggregator_and_update_tags (key , tags )
833+ aggregator , local_aggregator , tags = _get_aggregator_and_update_tags (key , tags )
718834 if aggregator is not None :
719- aggregator .add ("d" , key , value , unit , tags , timestamp , stacklevel )
835+ aggregator .add (
836+ "d" , key , value , unit , tags , timestamp , local_aggregator , stacklevel
837+ )
720838
721839
722840def set (
@@ -729,21 +847,25 @@ def set(
729847):
730848 # type: (...) -> None
731849 """Emits a set."""
732- aggregator , tags = _get_aggregator_and_update_tags (key , tags )
850+ aggregator , local_aggregator , tags = _get_aggregator_and_update_tags (key , tags )
733851 if aggregator is not None :
734- aggregator .add ("s" , key , value , unit , tags , timestamp , stacklevel )
852+ aggregator .add (
853+ "s" , key , value , unit , tags , timestamp , local_aggregator , stacklevel
854+ )
735855
736856
737857def gauge (
738858 key , # type: str
739859 value , # type: float
740- unit = "none" , # type: MetricValue
860+ unit = "none" , # type: MeasurementUnit
741861 tags = None , # type: Optional[MetricTags]
742862 timestamp = None , # type: Optional[Union[float, datetime]]
743863 stacklevel = 0 , # type: int
744864):
745865 # type: (...) -> None
746866 """Emits a gauge."""
747- aggregator , tags = _get_aggregator_and_update_tags (key , tags )
867+ aggregator , local_aggregator , tags = _get_aggregator_and_update_tags (key , tags )
748868 if aggregator is not None :
749- aggregator .add ("g" , key , value , unit , tags , timestamp , stacklevel )
869+ aggregator .add (
870+ "g" , key , value , unit , tags , timestamp , local_aggregator , stacklevel
871+ )
0 commit comments