44
44
//!
45
45
//! [our docs]: https://develop.sentry.dev/delightful-developer-metrics/
46
46
47
+ mod normalization;
48
+
47
49
use std:: borrow:: Cow ;
48
- use std:: collections:: hash_map:: { DefaultHasher , Entry } ;
50
+ use std:: collections:: hash_map:: Entry ;
49
51
use std:: collections:: { BTreeMap , BTreeSet , HashMap } ;
50
- use std:: fmt:: { self , Write } ;
52
+ use std:: fmt:: { self , Display } ;
51
53
use std:: sync:: { Arc , Mutex } ;
52
54
use std:: thread:: { self , JoinHandle } ;
53
55
use std:: time:: { Duration , SystemTime , UNIX_EPOCH } ;
54
56
57
+ use normalization:: normalized_name:: NormalizedName ;
58
+ use normalization:: normalized_tags:: NormalizedTags ;
59
+ use normalization:: normalized_unit:: NormalizedUnit ;
55
60
use sentry_types:: protocol:: latest:: { Envelope , EnvelopeItem } ;
56
61
57
62
use crate :: client:: TransportArc ;
@@ -168,15 +173,23 @@ impl MetricValue {
168
173
}
169
174
}
170
175
176
+ impl Display for MetricValue {
177
+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
178
+ match self {
179
+ Self :: Counter ( v) => write ! ( f, "{}" , v) ,
180
+ Self :: Distribution ( v) => write ! ( f, "{}" , v) ,
181
+ Self :: Gauge ( v) => write ! ( f, "{}" , v) ,
182
+ Self :: Set ( v) => write ! ( f, "{}" , v) ,
183
+ }
184
+ }
185
+ }
186
+
171
187
/// Hashes the given set value.
172
188
///
173
189
/// Sets only guarantee 32-bit accuracy, but arbitrary strings are allowed on the protocol. Upon
174
190
/// parsing, they are hashed and only used as hashes subsequently.
175
191
fn hash_set_value ( string : & str ) -> u32 {
176
- use std:: hash:: Hasher ;
177
- let mut hasher = DefaultHasher :: default ( ) ;
178
- hasher. write ( string. as_bytes ( ) ) ;
179
- hasher. finish ( ) as u32
192
+ crc32fast:: hash ( string. as_bytes ( ) )
180
193
}
181
194
182
195
#[ derive( Clone , Copy , Debug , PartialEq , Eq , Hash , PartialOrd , Ord ) ]
@@ -510,6 +523,24 @@ impl Metric {
510
523
client. add_metric ( self ) ;
511
524
}
512
525
}
526
+
527
+ /// Convert the metric into an [`Envelope`] containing a single [`EnvelopeItem::Statsd`].
528
+ pub fn to_envelope ( self ) -> Envelope {
529
+ let timestamp = SystemTime :: now ( )
530
+ . duration_since ( UNIX_EPOCH )
531
+ . unwrap_or_default ( )
532
+ . as_secs ( ) ;
533
+ let data = format ! (
534
+ "{}@{}:{}|{}|#{}|T{}" ,
535
+ NormalizedName :: from( self . name. as_ref( ) ) ,
536
+ NormalizedUnit :: from( self . unit) ,
537
+ self . value,
538
+ self . value. ty( ) ,
539
+ NormalizedTags :: from( self . tags) ,
540
+ timestamp
541
+ ) ;
542
+ Envelope :: from_item ( EnvelopeItem :: Statsd ( data. into_bytes ( ) ) )
543
+ }
513
544
}
514
545
515
546
/// A builder for metrics.
@@ -550,6 +581,26 @@ impl MetricBuilder {
550
581
self
551
582
}
552
583
584
+ /// Adds multiple tags to the metric.
585
+ ///
586
+ /// Tags allow you to add dimensions to metrics. They are key-value pairs that can be filtered
587
+ /// or grouped by in Sentry.
588
+ ///
589
+ /// When sent to Sentry via [`MetricBuilder::send`] or when added to a
590
+ /// [`Client`](crate::Client), the client may add default tags to the metrics, such as the
591
+ /// `release` or the `environment` from the Scope.
592
+ pub fn with_tags < T , K , V > ( mut self , tags : T ) -> Self
593
+ where
594
+ T : IntoIterator < Item = ( K , V ) > ,
595
+ K : Into < MetricStr > ,
596
+ V : Into < MetricStr > ,
597
+ {
598
+ tags. into_iter ( ) . for_each ( |( k, v) | {
599
+ self . metric . tags . insert ( k. into ( ) , v. into ( ) ) ;
600
+ } ) ;
601
+ self
602
+ }
603
+
553
604
/// Sets the timestamp for the metric.
554
605
///
555
606
/// By default, the timestamp is set to the current time when the metric is built or sent.
@@ -723,9 +774,13 @@ fn get_default_tags(options: &ClientOptions) -> TagMap {
723
774
if let Some ( ref release) = options. release {
724
775
tags. insert ( "release" . into ( ) , release. clone ( ) ) ;
725
776
}
726
- if let Some ( ref environment) = options. environment {
727
- tags. insert ( "environment" . into ( ) , environment. clone ( ) ) ;
728
- }
777
+ tags. insert (
778
+ "environment" . into ( ) ,
779
+ options
780
+ . environment
781
+ . clone ( )
782
+ . unwrap_or ( Cow :: Borrowed ( "production" ) ) ,
783
+ ) ;
729
784
tags
730
785
}
731
786
@@ -778,11 +833,8 @@ impl Worker {
778
833
779
834
for ( timestamp, buckets) in buckets {
780
835
for ( key, value) in buckets {
781
- write ! ( & mut out, "{}" , SafeKey ( key. name. as_ref( ) ) ) ?;
782
- if key. unit != MetricUnit :: None {
783
- write ! ( & mut out, "@{}" , key. unit) ?;
784
- }
785
-
836
+ write ! ( & mut out, "{}" , NormalizedName :: from( key. name. as_ref( ) ) ) ?;
837
+ write ! ( & mut out, "@{}" , NormalizedUnit :: from( key. unit) ) ?;
786
838
match value {
787
839
BucketValue :: Counter ( c) => {
788
840
write ! ( & mut out, ":{}" , c) ?;
@@ -807,16 +859,9 @@ impl Worker {
807
859
}
808
860
809
861
write ! ( & mut out, "|{}" , key. ty. as_str( ) ) ?;
810
-
811
- for ( i, ( k, v) ) in key. tags . iter ( ) . chain ( & self . default_tags ) . enumerate ( ) {
812
- match i {
813
- 0 => write ! ( & mut out, "|#" ) ?,
814
- _ => write ! ( & mut out, "," ) ?,
815
- }
816
-
817
- write ! ( & mut out, "{}:{}" , SafeKey ( k. as_ref( ) ) , SafeVal ( v. as_ref( ) ) ) ?;
818
- }
819
-
862
+ let normalized_tags =
863
+ NormalizedTags :: from ( key. tags ) . with_default_tags ( & self . default_tags ) ;
864
+ write ! ( & mut out, "|#{}" , normalized_tags) ?;
820
865
writeln ! ( & mut out, "|T{}" , timestamp) ?;
821
866
}
822
867
}
@@ -922,51 +967,6 @@ impl Drop for MetricAggregator {
922
967
}
923
968
}
924
969
925
- fn safe_fmt < F > ( f : & mut fmt:: Formatter < ' _ > , string : & str , mut check : F ) -> fmt:: Result
926
- where
927
- F : FnMut ( char ) -> bool ,
928
- {
929
- let mut valid = true ;
930
-
931
- for c in string. chars ( ) {
932
- if check ( c) {
933
- valid = true ;
934
- f. write_char ( c) ?;
935
- } else if valid {
936
- valid = false ;
937
- f. write_char ( '_' ) ?;
938
- }
939
- }
940
-
941
- Ok ( ( ) )
942
- }
943
-
944
- // Helper that serializes a string into a safe format for metric names or tag keys.
945
- struct SafeKey < ' s > ( & ' s str ) ;
946
-
947
- impl < ' s > fmt:: Display for SafeKey < ' s > {
948
- fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
949
- safe_fmt ( f, self . 0 , |c| {
950
- c. is_ascii_alphanumeric ( ) || matches ! ( c, '_' | '-' | '.' | '/' )
951
- } )
952
- }
953
- }
954
-
955
- // Helper that serializes a string into a safe format for tag values.
956
- struct SafeVal < ' s > ( & ' s str ) ;
957
-
958
- impl < ' s > fmt:: Display for SafeVal < ' s > {
959
- fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
960
- safe_fmt ( f, self . 0 , |c| {
961
- c. is_alphanumeric ( )
962
- || matches ! (
963
- c,
964
- '_' | ':' | '/' | '@' | '.' | '{' | '}' | '[' | ']' | '$' | '-'
965
- )
966
- } )
967
- }
968
- }
969
-
970
970
#[ cfg( test) ]
971
971
mod tests {
972
972
use crate :: test:: { with_captured_envelopes, with_captured_envelopes_options} ;
@@ -1007,7 +1007,10 @@ mod tests {
1007
1007
} ) ;
1008
1008
1009
1009
let metrics = get_single_metrics ( & envelopes) ;
1010
- assert_eq ! ( metrics, format!( "my.metric:1|c|#and:more,foo:bar|T{ts}" ) ) ;
1010
+ assert_eq ! (
1011
+ metrics,
1012
+ format!( "my.metric@none:1|c|#and:more,environment:production,foo:bar|T{ts}" )
1013
+ ) ;
1011
1014
}
1012
1015
1013
1016
#[ test]
@@ -1022,7 +1025,10 @@ mod tests {
1022
1025
} ) ;
1023
1026
1024
1027
let metrics = get_single_metrics ( & envelopes) ;
1025
- assert_eq ! ( metrics, format!( "my.metric@custom:1|c|T{ts}" ) ) ;
1028
+ assert_eq ! (
1029
+ metrics,
1030
+ format!( "my.metric@custom:1|c|#environment:production|T{ts}" )
1031
+ ) ;
1026
1032
}
1027
1033
1028
1034
#[ test]
@@ -1034,7 +1040,10 @@ mod tests {
1034
1040
} ) ;
1035
1041
1036
1042
let metrics = get_single_metrics ( & envelopes) ;
1037
- assert_eq ! ( metrics, format!( "my_metric:1|c|T{ts}" ) ) ;
1043
+ assert_eq ! (
1044
+ metrics,
1045
+ format!( "my___metric@none:1|c|#environment:production|T{ts}" )
1046
+ ) ;
1038
1047
}
1039
1048
1040
1049
#[ test]
@@ -1051,29 +1060,17 @@ mod tests {
1051
1060
let metrics = get_single_metrics ( & envelopes) ;
1052
1061
assert_eq ! (
1053
1062
metrics,
1054
- format!( "my.metric:1|c|#foo-bar_blub:_ $föö{{}}|T{ts}" )
1063
+ format!( "my.metric@none :1|c|#environment:production, foo-barblub:% $föö{{}}|T{ts}" )
1055
1064
) ;
1056
1065
}
1057
1066
1058
- #[ test]
1059
- fn test_own_namespace ( ) {
1060
- let ( time, ts) = current_time ( ) ;
1061
-
1062
- let envelopes = with_captured_envelopes ( || {
1063
- Metric :: count ( "ns/my.metric" ) . with_time ( time) . send ( ) ;
1064
- } ) ;
1065
-
1066
- let metrics = get_single_metrics ( & envelopes) ;
1067
- assert_eq ! ( metrics, format!( "ns/my.metric:1|c|T{ts}" ) ) ;
1068
- }
1069
-
1070
1067
#[ test]
1071
1068
fn test_default_tags ( ) {
1072
1069
let ( time, ts) = current_time ( ) ;
1073
1070
1074
1071
let options = ClientOptions {
1075
1072
release : Some ( "[email protected] " . into ( ) ) ,
1076
- environment : Some ( "production " . into ( ) ) ,
1073
+ environment : Some ( "development " . into ( ) ) ,
1077
1074
..Default :: default ( )
1078
1075
} ;
1079
1076
@@ -1090,7 +1087,7 @@ mod tests {
1090
1087
let metrics = get_single_metrics ( & envelopes) ;
1091
1088
assert_eq ! (
1092
1089
metrics,
1093
- format!
( "requests:1|c|#foo:bar,environment:production ,release:[email protected] |T{ts}" )
1090
+ format!
( "requests@none :1|c|#environment:development,foo:bar ,release:[email protected] |T{ts}" )
1094
1091
) ;
1095
1092
}
1096
1093
@@ -1104,7 +1101,10 @@ mod tests {
1104
1101
} ) ;
1105
1102
1106
1103
let metrics = get_single_metrics ( & envelopes) ;
1107
- assert_eq ! ( metrics, format!( "my.metric:3|c|T{ts}" ) ) ;
1104
+ assert_eq ! (
1105
+ metrics,
1106
+ format!( "my.metric@none:3|c|#environment:production|T{ts}" )
1107
+ ) ;
1108
1108
}
1109
1109
1110
1110
#[ test]
@@ -1121,7 +1121,10 @@ mod tests {
1121
1121
} ) ;
1122
1122
1123
1123
let metrics = get_single_metrics ( & envelopes) ;
1124
- assert_eq ! ( metrics, format!( "my.metric@second:0.2:0.1|d|T{ts}" ) ) ;
1124
+ assert_eq ! (
1125
+ metrics,
1126
+ format!( "my.metric@second:0.2:0.1|d|#environment:production|T{ts}" )
1127
+ ) ;
1125
1128
}
1126
1129
1127
1130
#[ test]
@@ -1138,7 +1141,10 @@ mod tests {
1138
1141
} ) ;
1139
1142
1140
1143
let metrics = get_single_metrics ( & envelopes) ;
1141
- assert_eq ! ( metrics, format!( "my.metric:2:1|d|T{ts}" ) ) ;
1144
+ assert_eq ! (
1145
+ metrics,
1146
+ format!( "my.metric@none:2:1|d|#environment:production|T{ts}" )
1147
+ ) ;
1142
1148
}
1143
1149
1144
1150
#[ test]
@@ -1153,7 +1159,10 @@ mod tests {
1153
1159
} ) ;
1154
1160
1155
1161
let metrics = get_single_metrics ( & envelopes) ;
1156
- assert_eq ! ( metrics, format!( "my.metric:3410894750:3817476724|s|T{ts}" ) ) ;
1162
+ assert_eq ! (
1163
+ metrics,
1164
+ format!( "my.metric@none:907060870:980881731|s|#environment:production|T{ts}" )
1165
+ ) ;
1157
1166
}
1158
1167
1159
1168
#[ test]
@@ -1167,7 +1176,10 @@ mod tests {
1167
1176
} ) ;
1168
1177
1169
1178
let metrics = get_single_metrics ( & envelopes) ;
1170
- assert_eq ! ( metrics, format!( "my.metric:1.5:1:2:4.5:3|g|T{ts}" ) ) ;
1179
+ assert_eq ! (
1180
+ metrics,
1181
+ format!( "my.metric@none:1.5:1:2:4.5:3|g|#environment:production|T{ts}" )
1182
+ ) ;
1171
1183
}
1172
1184
1173
1185
#[ test]
@@ -1182,8 +1194,8 @@ mod tests {
1182
1194
let metrics = get_single_metrics ( & envelopes) ;
1183
1195
println ! ( "{metrics}" ) ;
1184
1196
1185
- assert ! ( metrics. contains( & format!( "my.metric:1|c|T{ts}" ) ) ) ;
1186
- assert ! ( metrics. contains( & format!( "my.dist:2|d|T{ts}" ) ) ) ;
1197
+ assert ! ( metrics. contains( & format!( "my.metric@none :1|c|#environment:production |T{ts}" ) ) ) ;
1198
+ assert ! ( metrics. contains( & format!( "my.dist@none :2|d|#environment:production |T{ts}" ) ) ) ;
1187
1199
}
1188
1200
1189
1201
#[ test]
0 commit comments