@@ -4,10 +4,23 @@ use crate::http_probe::result::ProbeResult;
44
55pub mod client;
66
7+ const NAME_LABEL : & str = "__name__" ;
78const INSTANCE_LABEL : & str = "instance" ;
89const JOB_LABEL : & str = "job" ;
910const MODULE_LABEL : & str = "module" ;
1011const TARGET_LABEL : & str = "target" ;
12+ const PHASE_LABEL : & str = "phase" ;
13+
14+ /// Label names owned by Oxybox. A user-supplied label reusing one of these would
15+ /// produce a duplicate label name, which Mimir rejects, so such labels are dropped.
16+ const RESERVED_LABELS : [ & str ; 6 ] = [
17+ NAME_LABEL ,
18+ INSTANCE_LABEL ,
19+ JOB_LABEL ,
20+ MODULE_LABEL ,
21+ TARGET_LABEL ,
22+ PHASE_LABEL ,
23+ ] ;
1124const PROBE_SUCCESS_METRIC : & str = "probe_success" ;
1225const PROBE_DURATION_METRIC : & str = "probe_duration_seconds" ;
1326const PROBE_HTTP_STATUS_METRIC : & str = "probe_http_status_code" ;
@@ -62,18 +75,30 @@ pub fn create_probe_metrics(
6275 probe_success : bool ,
6376 labels : Option < Vec < ( & str , & str ) > > ,
6477) -> Vec < prompb:: TimeSeries > {
65- let mut metrics = Vec :: new ( ) ;
66- let probe_successful = match probe_success {
67- true => 1.0 ,
68- false => 0.0 ,
78+ let custom: Vec < ( & str , & str ) > = labels
79+ . unwrap_or_default ( )
80+ . into_iter ( )
81+ . filter ( |( key, _) | {
82+ let reserved = RESERVED_LABELS . contains ( key) ;
83+ if reserved {
84+ log:: warn!(
85+ "Ignoring custom label '{key}' for {}: it collides with a reserved label" ,
86+ probe_result. url
87+ ) ;
88+ }
89+ !reserved
90+ } )
91+ . collect ( ) ;
92+ let series = |metric : & str , value : f64 , phase : Option < & str > | {
93+ let mut extra = custom. clone ( ) ;
94+ if let Some ( phase) = phase {
95+ extra. push ( ( PHASE_LABEL , phase) ) ;
96+ }
97+ create_time_series ( metric, & probe_result. url , value, Some ( extra) )
6998 } ;
7099
71- metrics. push ( create_time_series (
72- PROBE_SUCCESS_METRIC ,
73- & probe_result. url ,
74- probe_successful,
75- labels,
76- ) ) ;
100+ let probe_successful = if probe_success { 1.0 } else { 0.0 } ;
101+ let mut metrics = vec ! [ series( PROBE_SUCCESS_METRIC , probe_successful, None ) ] ;
77102
78103 let phases = [
79104 ( probe_result. dns_time , "resolve" ) ,
@@ -82,77 +107,153 @@ pub fn create_probe_metrics(
82107 ( probe_result. processing_time , "processing" ) ,
83108 ( probe_result. transfer_time , "transfer" ) ,
84109 ] ;
85-
86110 for ( duration_opt, phase) in phases. iter ( ) {
87111 if let Some ( duration) = duration_opt {
88- metrics. push ( create_time_series (
89- PROBE_HTTP_DURATION_METRIC ,
90- & probe_result. url ,
91- * duration,
92- Some ( vec ! [ ( "phase" , * phase) ] ) ,
93- ) ) ;
112+ metrics. push ( series ( PROBE_HTTP_DURATION_METRIC , * duration, Some ( * phase) ) ) ;
94113 }
95114 }
96115
97- metrics. push ( create_time_series (
116+ metrics. push ( series (
98117 PROBE_DURATION_METRIC ,
99- & probe_result. url ,
100118 probe_result. total_probe_time ,
101119 None ,
102120 ) ) ;
121+
103122 if let Some ( http_status) = probe_result. http_status {
104- metrics. push ( create_time_series (
105- PROBE_HTTP_STATUS_METRIC ,
106- & probe_result. url ,
107- http_status as f64 ,
108- None ,
109- ) ) ;
123+ metrics. push ( series ( PROBE_HTTP_STATUS_METRIC , http_status as f64 , None ) ) ;
110124 }
111125
112126 if let Some ( dns_time) = probe_result. dns_time {
113- metrics. push ( create_time_series (
114- PROBE_DNS_LOOKUP_TIME_METRIC ,
115- & probe_result. url ,
116- dns_time,
117- None ,
118- ) ) ;
127+ metrics. push ( series ( PROBE_DNS_LOOKUP_TIME_METRIC , dns_time, None ) ) ;
119128 }
120129
121- let ssl_enabled = match probe_result. cert_validity_seconds {
122- Some ( _) => 1.0 ,
123- None => 0.0 ,
130+ let ssl_enabled = if probe_result. cert_validity_seconds . is_some ( ) {
131+ 1.0
132+ } else {
133+ 0.0
124134 } ;
125-
126- metrics. push ( create_time_series (
127- PROBE_HTTP_SSL_ENABLED_METRIC ,
128- & probe_result. url ,
129- ssl_enabled,
130- None ,
131- ) ) ;
135+ metrics. push ( series ( PROBE_HTTP_SSL_ENABLED_METRIC , ssl_enabled, None ) ) ;
132136
133137 if let Some ( cert_validity_seconds) = probe_result. cert_validity_seconds {
134- metrics. push ( create_time_series (
138+ metrics. push ( series (
135139 PROBE_HTTP_SSL_EARLIEST_EXPIERY_METRIC ,
136- & probe_result. url ,
137140 cert_validity_seconds,
138141 None ,
139142 ) ) ;
140143 }
144+
141145 if let Some ( http_version) = probe_result. http_version {
142- metrics. push ( create_time_series (
143- PROBE_HTTP_VERSION_METRIC ,
144- & probe_result. url ,
145- http_version,
146- None ,
147- ) ) ;
146+ metrics. push ( series ( PROBE_HTTP_VERSION_METRIC , http_version, None ) ) ;
148147 }
149148
150- metrics. push ( create_time_series (
149+ metrics. push ( series (
151150 PROBE_HTTP_REDIRECTS_METRIC ,
152- & probe_result. url ,
153151 probe_result. redirects as f64 ,
154152 None ,
155153 ) ) ;
156154
157155 metrics
158156}
157+
158+ #[ cfg( test) ]
159+ mod tests {
160+ use super :: * ;
161+ use crate :: http_probe:: result:: ProbeResult ;
162+
163+ fn sample_probe ( ) -> ProbeResult {
164+ ProbeResult {
165+ url : "https://example.com" . to_string ( ) ,
166+ dns_time : Some ( 0.01 ) ,
167+ connect_time : Some ( 0.02 ) ,
168+ tls_time : Some ( 0.03 ) ,
169+ processing_time : Some ( 0.04 ) ,
170+ transfer_time : Some ( 0.05 ) ,
171+ cert_validity_seconds : Some ( 1_700_000_000.0 ) ,
172+ http_status : Some ( 200 ) ,
173+ http_version : Some ( 2.0 ) ,
174+ total_probe_time : 0.15 ,
175+ redirects : 1 ,
176+ }
177+ }
178+
179+ fn metric_name ( ts : & prompb:: TimeSeries ) -> String {
180+ ts. labels
181+ . iter ( )
182+ . find ( |l| l. name == "__name__" )
183+ . map ( |l| l. value . clone ( ) )
184+ . unwrap_or_default ( )
185+ }
186+
187+ #[ test]
188+ fn custom_labels_apply_to_every_series ( ) {
189+ let labels = Some ( vec ! [ ( "environment" , "production" ) , ( "region" , "eu" ) ] ) ;
190+ let metrics = create_probe_metrics ( & sample_probe ( ) , true , labels) ;
191+
192+ assert ! ( metrics. len( ) > 1 ) ;
193+ for ts in & metrics {
194+ for ( key, value) in [ ( "environment" , "production" ) , ( "region" , "eu" ) ] {
195+ assert ! (
196+ ts. labels. iter( ) . any( |l| l. name == key && l. value == value) ,
197+ "series `{}` is missing custom label {key}={value}" ,
198+ metric_name( ts) ,
199+ ) ;
200+ }
201+ }
202+ }
203+
204+ #[ test]
205+ fn phase_series_keep_their_phase_label_alongside_custom_labels ( ) {
206+ let labels = Some ( vec ! [ ( "environment" , "production" ) ] ) ;
207+ let metrics = create_probe_metrics ( & sample_probe ( ) , true , labels) ;
208+
209+ let phase_series: Vec < _ > = metrics
210+ . iter ( )
211+ . filter ( |ts| metric_name ( ts) == PROBE_HTTP_DURATION_METRIC )
212+ . collect ( ) ;
213+ assert_eq ! ( phase_series. len( ) , 5 ) ;
214+ for ts in phase_series {
215+ assert ! ( ts. labels. iter( ) . any( |l| l. name == "phase" ) ) ;
216+ assert ! (
217+ ts. labels
218+ . iter( )
219+ . any( |l| l. name == "environment" && l. value == "production" )
220+ ) ;
221+ }
222+ }
223+
224+ #[ test]
225+ fn reserved_custom_labels_are_dropped ( ) {
226+ let labels = Some ( vec ! [
227+ ( INSTANCE_LABEL , "evil" ) ,
228+ ( PHASE_LABEL , "nope" ) ,
229+ ( "environment" , "production" ) ,
230+ ] ) ;
231+ let metrics = create_probe_metrics ( & sample_probe ( ) , true , labels) ;
232+
233+ assert ! ( metrics. len( ) > 1 ) ;
234+ for ts in & metrics {
235+ assert_eq ! (
236+ ts. labels. iter( ) . filter( |l| l. name == INSTANCE_LABEL ) . count( ) ,
237+ 1 ,
238+ "series `{}` has a duplicate instance label" ,
239+ metric_name( ts) ,
240+ ) ;
241+ assert ! ( ts. labels. iter( ) . all( |l| l. value != "evil" ) ) ;
242+ assert ! ( ts. labels. iter( ) . all( |l| l. value != "nope" ) ) ;
243+ assert ! (
244+ ts. labels
245+ . iter( )
246+ . any( |l| l. name == "environment" && l. value == "production" )
247+ ) ;
248+ }
249+ }
250+
251+ #[ test]
252+ fn no_custom_labels_still_produces_valid_series ( ) {
253+ let metrics = create_probe_metrics ( & sample_probe ( ) , true , None ) ;
254+ assert ! ( metrics. iter( ) . any( |ts| metric_name( ts) == PROBE_SUCCESS_METRIC ) ) ;
255+ for ts in & metrics {
256+ assert ! ( ts. labels. iter( ) . any( |l| l. name == INSTANCE_LABEL ) ) ;
257+ }
258+ }
259+ }
0 commit comments