@@ -12,7 +12,9 @@ use std::sync::{Arc, Mutex};
1212use mz_compute_client:: metrics:: { CommandMetrics , HistoryMetrics } ;
1313use mz_ore:: cast:: CastFrom ;
1414use mz_ore:: metric;
15- use mz_ore:: metrics:: { MetricTag , MetricVisibility , MetricsRegistry , UIntGauge , raw} ;
15+ use mz_ore:: metrics:: {
16+ MakeCollectorOpts , MetricTag , MetricVisibility , MetricsRegistry , UIntGauge , raw,
17+ } ;
1618use mz_repr:: { GlobalId , SharedRow } ;
1719use prometheus:: core:: { AtomicF64 , GenericCounter } ;
1820use prometheus:: proto:: LabelPair ;
@@ -83,160 +85,190 @@ pub struct ComputeMetrics {
8385 subscribe_snapshots_skipped_total : IntCounter ,
8486}
8587
88+ /// Applies the per-role const label to `opts`, unless `role` is `Solo`.
89+ ///
90+ /// The two named roles (maintenance, interactive) each get a distinct `role` label so a second
91+ /// compute runtime in the same process registers a distinct series rather than colliding with the
92+ /// first. `Solo` omits the label so a single-runtime deployment registers exactly as it did before
93+ /// a second runtime existed.
94+ fn with_role (
95+ mut opts : MakeCollectorOpts ,
96+ role : crate :: server:: ComputeRuntimeRole ,
97+ ) -> MakeCollectorOpts {
98+ if let Some ( label) = role. label ( ) {
99+ opts. opts = opts. opts . const_label ( "role" , label) ;
100+ }
101+ opts
102+ }
103+
86104impl ComputeMetrics {
87- pub fn register_with ( registry : & MetricsRegistry ) -> Self {
105+ /// Registers the compute metrics for `role` into `registry`.
106+ ///
107+ /// The two named roles carry a `role` const label so that a second compute runtime in the same
108+ /// process registers a distinct series rather than colliding with the first. `Solo` carries no
109+ /// such label.
110+ pub fn register_with (
111+ registry : & MetricsRegistry ,
112+ role : crate :: server:: ComputeRuntimeRole ,
113+ ) -> Self {
88114 let workload_class = Arc :: new ( Mutex :: new ( None ) ) ;
89115
90116 // Apply a `workload_class` label to all metrics in the registry when we
91117 // have a known workload class.
92- registry. register_postprocessor ( {
93- let workload_class = Arc :: clone ( & workload_class) ;
94- move |metrics| {
95- let workload_class: Option < String > =
96- workload_class. lock ( ) . expect ( "lock poisoned" ) . clone ( ) ;
97- let Some ( workload_class) = workload_class else {
98- return ;
99- } ;
100- for metric in metrics {
101- for metric in metric. mut_metric ( ) {
102- let mut label = LabelPair :: default ( ) ;
103- label. set_name ( "workload_class" . into ( ) ) ;
104- label. set_value ( workload_class. clone ( ) ) ;
105-
106- let mut labels = metric. take_label ( ) ;
107- labels. push ( label) ;
108- metric. set_label ( labels) ;
118+ //
119+ // The postprocessor rewrites every metric in the whole registry, so only the maintenance
120+ // runtime registers it. A second registration from the interactive runtime would push the
121+ // label twice onto each metric and produce a duplicate-label scrape error.
122+ if role. owns_process_globals ( ) {
123+ registry. register_postprocessor ( {
124+ let workload_class = Arc :: clone ( & workload_class) ;
125+ move |metrics| {
126+ let workload_class: Option < String > =
127+ workload_class. lock ( ) . expect ( "lock poisoned" ) . clone ( ) ;
128+ let Some ( workload_class) = workload_class else {
129+ return ;
130+ } ;
131+ for metric in metrics {
132+ for metric in metric. mut_metric ( ) {
133+ let mut label = LabelPair :: default ( ) ;
134+ label. set_name ( "workload_class" . into ( ) ) ;
135+ label. set_value ( workload_class. clone ( ) ) ;
136+
137+ let mut labels = metric. take_label ( ) ;
138+ labels. push ( label) ;
139+ metric. set_label ( labels) ;
140+ }
109141 }
110142 }
111- }
112- } ) ;
143+ } ) ;
144+ }
113145
114146 Self {
115147 workload_class,
116- history_command_count : registry. register ( metric ! (
148+ history_command_count : registry. register ( with_role ( metric ! (
117149 name: "mz_compute_replica_history_command_count" ,
118150 help: "The number of commands in the replica's command history." ,
119151 var_labels: [ "worker_id" , "command_type" ] ,
120- ) ) ,
121- history_dataflow_count : registry. register ( metric ! (
152+ ) , role ) ) ,
153+ history_dataflow_count : registry. register ( with_role ( metric ! (
122154 name: "mz_compute_replica_history_dataflow_count" ,
123155 help: "The number of dataflows in the replica's command history." ,
124156 var_labels: [ "worker_id" ] ,
125157 visibility: MetricVisibility :: Public ,
126158 tags: [ MetricTag :: Compute ] ,
127- ) ) ,
128- reconciliation_reused_dataflows_count_total : registry. register ( metric ! (
159+ ) , role ) ) ,
160+ reconciliation_reused_dataflows_count_total : registry. register ( with_role ( metric ! (
129161 name: "mz_compute_reconciliation_reused_dataflows_count_total" ,
130162 help: "The total number of dataflows that were reused during compute reconciliation." ,
131163 var_labels: [ "worker_id" ] ,
132- ) ) ,
133- reconciliation_replaced_dataflows_count_total : registry. register ( metric ! (
164+ ) , role ) ) ,
165+ reconciliation_replaced_dataflows_count_total : registry. register ( with_role ( metric ! (
134166 name: "mz_compute_reconciliation_replaced_dataflows_count_total" ,
135167 help: "The total number of dataflows that were replaced during compute reconciliation." ,
136168 var_labels: [ "worker_id" , "reason" ] ,
137- ) ) ,
138- arrangement_maintenance_seconds_total : registry. register ( metric ! (
169+ ) , role ) ) ,
170+ arrangement_maintenance_seconds_total : registry. register ( with_role ( metric ! (
139171 name: "mz_arrangement_maintenance_seconds_total" ,
140172 help: "The total time spent maintaining arrangements." ,
141173 var_labels: [ "worker_id" ] ,
142174 visibility: MetricVisibility :: Public ,
143175 tags: [ MetricTag :: Compute ] ,
144- ) ) ,
145- arrangement_maintenance_active_info : registry. register ( metric ! (
176+ ) , role ) ) ,
177+ arrangement_maintenance_active_info : registry. register ( with_role ( metric ! (
146178 name: "mz_arrangement_maintenance_active_info" ,
147179 help: "Whether maintenance is currently occuring." ,
148180 var_labels: [ "worker_id" ] ,
149- ) ) ,
150- timely_step_duration_seconds : registry. register ( metric ! (
181+ ) , role ) ) ,
182+ timely_step_duration_seconds : registry. register ( with_role ( metric ! (
151183 name: "mz_timely_step_duration_seconds" ,
152184 help: "The time spent in each compute step_or_park call" ,
153185 const_labels: { "cluster" => "compute" } ,
154186 var_labels: [ "worker_id" ] ,
155187 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 32.0 ) ,
156- ) ) ,
157- shared_row_heap_capacity_bytes : registry. register ( metric ! (
188+ ) , role ) ) ,
189+ shared_row_heap_capacity_bytes : registry. register ( with_role ( metric ! (
158190 name: "mz_dataflow_shared_row_heap_capacity_bytes" ,
159191 help: "The heap capacity of the shared row." ,
160192 var_labels: [ "worker_id" ] ,
161- ) ) ,
162- persist_peek_seconds : registry. register ( metric ! (
193+ ) , role ) ) ,
194+ persist_peek_seconds : registry. register ( with_role ( metric ! (
163195 name: "mz_persist_peek_seconds" ,
164196 help: "Time spent in (experimental) Persist fast-path peeks." ,
165197 var_labels: [ "worker_id" ] ,
166198 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
167- ) ) ,
168- stashed_peek_seconds : registry. register ( metric ! (
199+ ) , role ) ) ,
200+ stashed_peek_seconds : registry. register ( with_role ( metric ! (
169201 name: "mz_stashed_peek_seconds" ,
170202 help: "Time spent reading a peek result and stashing it in the peek result stash (aka. persist blob)." ,
171203 var_labels: [ "worker_id" ] ,
172204 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
173- ) ) ,
174- handle_command_duration_seconds : registry. register ( metric ! (
205+ ) , role ) ) ,
206+ handle_command_duration_seconds : registry. register ( with_role ( metric ! (
175207 name: "mz_cluster_handle_command_duration_seconds" ,
176208 help: "Time spent in handling commands." ,
177209 const_labels: { "cluster" => "compute" } ,
178210 var_labels: [ "worker_id" , "command_type" ] ,
179211 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
180- ) ) ,
181- index_peek_total_seconds : registry. register ( metric ! (
212+ ) , role ) ) ,
213+ index_peek_total_seconds : registry. register ( with_role ( metric ! (
182214 name: "mz_index_peek_total_seconds" ,
183215 help: "Total time processing index peeks, from process_peek entry to response. Excluding peeks that use the peek response stash." ,
184216 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
185- ) ) ,
186- index_peek_seek_fulfillment_seconds : registry. register ( metric ! (
217+ ) , role ) ) ,
218+ index_peek_seek_fulfillment_seconds : registry. register ( with_role ( metric ! (
187219 name: "mz_index_peek_seek_fulfillment_seconds" ,
188220 help: "Time in seek_fulfillment method including frontier checks and data collection." ,
189221 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
190- ) ) ,
191- index_peek_error_scan_seconds : registry. register ( metric ! (
222+ ) , role ) ) ,
223+ index_peek_error_scan_seconds : registry. register ( with_role ( metric ! (
192224 name: "mz_index_peek_error_scan_seconds" ,
193225 help: "Time scanning the error trace for errors." ,
194226 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
195- ) ) ,
196- index_peek_cursor_setup_seconds : registry. register ( metric ! (
227+ ) , role ) ) ,
228+ index_peek_cursor_setup_seconds : registry. register ( with_role ( metric ! (
197229 name: "mz_index_peek_cursor_setup_seconds" ,
198230 help: "Time setting up cursor and literal constraints." ,
199231 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
200- ) ) ,
201- index_peek_row_iteration_seconds : registry. register ( metric ! (
232+ ) , role ) ) ,
233+ index_peek_row_iteration_seconds : registry. register ( with_role ( metric ! (
202234 name: "mz_index_peek_row_iteration_seconds" ,
203235 help: "Time iterating rows and evaluating MFP." ,
204236 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
205- ) ) ,
206- index_peek_result_sort_seconds : registry. register ( metric ! (
237+ ) , role ) ) ,
238+ index_peek_result_sort_seconds : registry. register ( with_role ( metric ! (
207239 name: "mz_index_peek_result_sort_seconds" ,
208240 help: "Time sorting intermediate results during peek collection." ,
209241 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
210- ) ) ,
211- index_peek_frontier_check_seconds : registry. register ( metric ! (
242+ ) , role ) ) ,
243+ index_peek_frontier_check_seconds : registry. register ( with_role ( metric ! (
212244 name: "mz_index_peek_frontier_check_seconds" ,
213245 help: "Time checking trace frontiers." ,
214246 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
215- ) ) ,
216- index_peek_row_collection_seconds : registry. register ( metric ! (
247+ ) , role ) ) ,
248+ index_peek_row_collection_seconds : registry. register ( with_role ( metric ! (
217249 name: "mz_index_peek_row_collection_seconds" ,
218250 help: "Time constructing RowCollection from peek results." ,
219251 buckets: mz_ore:: stats:: histogram_seconds_buckets( 0.000_128 , 8.0 ) ,
220- ) ) ,
221- replica_expiration_timestamp_seconds : registry. register ( metric ! (
252+ ) , role ) ) ,
253+ replica_expiration_timestamp_seconds : registry. register ( with_role ( metric ! (
222254 name: "mz_dataflow_replica_expiration_timestamp_seconds" ,
223255 help: "The replica expiration timestamp in seconds since epoch." ,
224256 var_labels: [ "worker_id" ] ,
225- ) ) ,
226- replica_expiration_remaining_seconds : registry. register ( metric ! (
257+ ) , role ) ) ,
258+ replica_expiration_remaining_seconds : registry. register ( with_role ( metric ! (
227259 name: "mz_dataflow_replica_expiration_remaining_seconds" ,
228260 help: "The remaining seconds until replica expiration. Can go negative, can lag behind." ,
229261 var_labels: [ "worker_id" ] ,
230- ) ) ,
231- collection_count : registry. register ( metric ! (
262+ ) , role ) ) ,
263+ collection_count : registry. register ( with_role ( metric ! (
232264 name: "mz_compute_collection_count" ,
233265 help: "The number and hydration status of maintained compute collections." ,
234266 var_labels: [ "worker_id" , "type" , "hydrated" ] ,
235- ) ) ,
236- subscribe_snapshots_skipped_total : registry. register ( metric ! (
267+ ) , role ) ) ,
268+ subscribe_snapshots_skipped_total : registry. register ( with_role ( metric ! (
237269 name: "mz_subscribe_snapshots_skipped_total" ,
238270 help: "The number of collection snapshots that were skipped by the subscribe snapshot optimization." ,
239- ) ) ,
271+ ) , role ) ) ,
240272 }
241273 }
242274
@@ -514,3 +546,64 @@ impl Drop for CollectionMetrics {
514546 . dec_collection_count ( self . collection_type , self . collection_hydrated ) ;
515547 }
516548}
549+
550+ #[ cfg( test) ]
551+ mod tests {
552+ use std:: collections:: BTreeSet ;
553+
554+ use mz_ore:: metrics:: MetricsRegistry ;
555+
556+ use super :: ComputeMetrics ;
557+ use crate :: server:: ComputeRuntimeRole ;
558+
559+ /// The `Solo` (single-runtime) role registers exactly as compute did before a second runtime
560+ /// existed: no metric carries a `role` label, so single-runtime dashboards and exact-match
561+ /// alerts are byte-unchanged.
562+ #[ mz_ore:: test]
563+ fn solo_runtime_omits_role_label ( ) {
564+ let registry = MetricsRegistry :: new ( ) ;
565+ let metrics = ComputeMetrics :: register_with ( & registry, ComputeRuntimeRole :: Solo ) ;
566+ // Instantiate the per-worker children so the `*Vec` families emit rows to inspect.
567+ let _worker = metrics. for_worker ( 0 ) ;
568+
569+ for family in registry. gather ( ) {
570+ for metric in family. get_metric ( ) {
571+ for label in metric. get_label ( ) {
572+ assert_ne ! (
573+ label. name( ) ,
574+ "role" ,
575+ "solo metric {} unexpectedly carries a role label" ,
576+ family. name( ) ,
577+ ) ;
578+ }
579+ }
580+ }
581+ }
582+
583+ /// The two named roles each carry their own `role` label, so two runtimes in one process
584+ /// register distinct series rather than colliding. Registering both on one registry also
585+ /// exercises the non-collision that lets them coexist.
586+ #[ mz_ore:: test]
587+ fn named_roles_carry_distinct_role_label ( ) {
588+ let registry = MetricsRegistry :: new ( ) ;
589+ let maintenance = ComputeMetrics :: register_with ( & registry, ComputeRuntimeRole :: Maintenance ) ;
590+ let interactive = ComputeMetrics :: register_with ( & registry, ComputeRuntimeRole :: Interactive ) ;
591+ let _maintenance_worker = maintenance. for_worker ( 0 ) ;
592+ let _interactive_worker = interactive. for_worker ( 0 ) ;
593+
594+ let mut roles = BTreeSet :: new ( ) ;
595+ for family in registry. gather ( ) {
596+ for metric in family. get_metric ( ) {
597+ let role = metric
598+ . get_label ( )
599+ . iter ( )
600+ . find ( |label| label. name ( ) == "role" )
601+ . unwrap_or_else ( || panic ! ( "metric {} missing a role label" , family. name( ) ) ) ;
602+ roles. insert ( role. value ( ) . to_string ( ) ) ;
603+ }
604+ }
605+
606+ assert ! ( roles. contains( "maintenance" ) , "roles seen: {roles:?}" ) ;
607+ assert ! ( roles. contains( "interactive" ) , "roles seen: {roles:?}" ) ;
608+ }
609+ }
0 commit comments