1use std::collections::BTreeMap;
42
43pub use prometheus_filtered::MetricLevel;
44use serde::{Deserialize, Serialize};
45
46#[derive(Debug, Clone, Deserialize, Serialize)]
49#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
50pub struct MetricGroups {
51 pub default: MetricLevel,
53 pub consensus: MetricLevel,
59 pub execution: MetricLevel,
65 pub checkpoints: MetricLevel,
69 pub transactions: MetricLevel,
75 pub authority: MetricLevel,
89 pub traffic_control: MetricLevel,
95 pub network: MetricLevel,
100 pub p2p: MetricLevel,
107 pub storage: MetricLevel,
113 pub rpc: MetricLevel,
121 pub epoch: MetricLevel,
128 pub runtime: MetricLevel,
135 pub hardware: MetricLevel,
141 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
150 pub overrides: BTreeMap<String, MetricLevel>,
151}
152
153impl Default for MetricGroups {
154 fn default() -> Self {
155 Self {
156 default: MetricLevel::Info,
157 consensus: MetricLevel::Warn,
158 execution: MetricLevel::Warn,
159 checkpoints: MetricLevel::Warn,
160 transactions: MetricLevel::Warn,
161 authority: MetricLevel::Warn,
162 traffic_control: MetricLevel::Warn,
163 network: MetricLevel::Warn,
164 p2p: MetricLevel::Warn,
165 storage: MetricLevel::Warn,
166 rpc: MetricLevel::Warn,
167 epoch: MetricLevel::Warn,
168 runtime: MetricLevel::Warn,
169 hardware: MetricLevel::Warn,
170 overrides: BTreeMap::new(),
171 }
172 }
173}
174
175impl MetricGroups {
176 fn modules_for_group(group: &str) -> Option<&'static [&'static str]> {
179 Some(match group {
180 "runtime" => &["iota_metrics", "telemetry_subscribers"],
181 "consensus" => &[
182 "starfish_core",
183 "iota_core::consensus_adapter",
184 "iota_core::consensus_manager",
185 "iota_core::consensus_validator",
186 "iota_core::epoch::consensus_store_pruner",
187 ],
188 "execution" => &[
189 "iota_core::execution_cache",
190 "iota_core::global_state_hasher",
191 "iota_core::module_cache_metrics",
192 "iota_types::metrics",
193 ],
194 "checkpoints" => &["iota_core::checkpoints"],
195 "transactions" => &[
196 "iota_core::quorum_driver",
197 "iota_core::transaction_driver",
198 "iota_core::transaction_orchestrator",
199 "iota_core::validator_tx_finalizer",
200 ],
201 "authority" => &[
202 "iota_core::authority",
203 "iota_core::safe_client",
204 "iota_core::signature_verifier",
205 "iota_core::validator_client_monitor",
206 "authority_grpc",
210 ],
211 "traffic-control" => &[
212 "iota_core::traffic_controller",
213 "iota_config::node_config_metrics",
214 ],
215 "network" => &[
216 "iota_network::discovery",
217 "iota_network::randomness",
218 "iota_network::state_sync",
219 ],
220 "p2p" => &["iota_metrics::metrics_network"],
221 "storage" => &["typed_store", "iota_storage", "iota_snapshot"],
222 "rpc" => &[
223 "iota_json_rpc",
224 "iota_grpc_server",
225 "iota_graphql_rpc",
226 "iota_http",
227 "iota_core::jsonrpc_index",
228 "iota_core::subscription_handler",
229 ],
230 "epoch" => &[
231 "iota_core::epoch::epoch_metrics",
232 "iota_current_protocol_version",
234 "iota_binary_max_protocol_version",
235 "iota_configured_max_protocol_version",
236 ],
237 "hardware" => &["iota_metrics::hardware_metrics"],
238 _ => return None,
239 })
240 }
241
242 fn group_levels(&self) -> [(&'static str, MetricLevel); 13] {
244 [
245 ("runtime", self.runtime),
246 ("consensus", self.consensus),
247 ("execution", self.execution),
248 ("checkpoints", self.checkpoints),
249 ("transactions", self.transactions),
250 ("authority", self.authority),
251 ("traffic-control", self.traffic_control),
252 ("network", self.network),
253 ("p2p", self.p2p),
254 ("storage", self.storage),
255 ("rpc", self.rpc),
256 ("epoch", self.epoch),
257 ("hardware", self.hardware),
258 ]
259 }
260
261 fn group_modules(&self) -> [(MetricLevel, &'static [&'static str]); 13] {
263 self.group_levels().map(|(group, level)| {
264 (
265 level,
266 Self::modules_for_group(group).expect("group has modules"),
267 )
268 })
269 }
270
271 fn to_filter_string(&self) -> String {
273 let mut directives = vec![format!("default={}", self.default.as_str())];
274 for (level, modules) in self.group_modules() {
275 for module in modules {
276 directives.push(format!("{module}={}", level.as_str()));
277 }
278 }
279 for directive in self.override_directives() {
280 directives.extend(
284 Self::expand_directive(&directive)
285 .expect("override levels are typed, so the rendered directive is valid"),
286 );
287 }
288 directives.join(",")
289 }
290
291 fn to_display_string(&self) -> String {
295 let mut directives = vec![format!("default={}", self.default.as_str())];
296 for (group, level) in self.group_levels() {
297 directives.push(format!("{group}={}", level.as_str()));
298 }
299 directives.extend(self.override_directives());
300 directives.join(",")
301 }
302
303 pub(crate) fn expand_directives(filter: &str) -> Result<String, String> {
308 let (directives, errors) = Self::expand_startup_directives(filter);
309 if errors.is_empty() {
310 Ok(directives)
311 } else {
312 Err(errors.join("; "))
313 }
314 }
315
316 fn expand_startup_directives(filter: &str) -> (String, Vec<String>) {
320 let mut directives = Vec::new();
321 let mut errors = Vec::new();
322 for part in prometheus_filtered::directive_parts(filter) {
323 match Self::expand_directive(part) {
324 Ok(expanded) => directives.extend(expanded),
325 Err(err) => errors.push(err),
326 }
327 }
328 (directives.join(","), errors)
329 }
330
331 pub fn startup_filter(&self, env: Option<&str>) -> (prometheus_filtered::Filter, Vec<String>) {
340 let directives = self.to_filter_string();
341 let display = self.to_display_string();
342 let config = prometheus_filtered::FilterSource::with_display(&directives, &display);
343 match env {
344 Some(env) => {
345 let (expanded, errors) = Self::expand_startup_directives(env);
346 let filter = prometheus_filtered::Filter::from_sources(
347 config,
348 Some(prometheus_filtered::FilterSource::with_display(
349 &expanded, env,
350 )),
351 );
352 (filter, errors)
353 }
354 None => (
355 prometheus_filtered::Filter::from_sources(config, None),
356 Vec::new(),
357 ),
358 }
359 }
360
361 fn expand_directive(part: &str) -> Result<Vec<String>, String> {
362 let (pattern, level) = prometheus_filtered::split_directive(part)?;
363 Ok(match Self::modules_for_group(pattern) {
364 Some(modules) => modules
365 .iter()
366 .map(|module| format!("{module}={}", level.as_str()))
367 .collect(),
368 None => vec![part.to_owned()],
370 })
371 }
372
373 fn override_directives(&self) -> impl Iterator<Item = String> + '_ {
375 self.overrides
376 .iter()
377 .map(|(pattern, level)| format!("{pattern}={}", level.as_str()))
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use std::collections::BTreeMap;
384
385 use prometheus_filtered::Filter;
386
387 use super::{MetricGroups, MetricLevel};
388
389 fn all_trace() -> MetricGroups {
390 MetricGroups {
391 default: MetricLevel::Trace,
392 consensus: MetricLevel::Trace,
393 execution: MetricLevel::Trace,
394 checkpoints: MetricLevel::Trace,
395 transactions: MetricLevel::Trace,
396 authority: MetricLevel::Trace,
397 traffic_control: MetricLevel::Trace,
398 network: MetricLevel::Trace,
399 p2p: MetricLevel::Trace,
400 storage: MetricLevel::Trace,
401 rpc: MetricLevel::Trace,
402 epoch: MetricLevel::Trace,
403 runtime: MetricLevel::Trace,
404 hardware: MetricLevel::Trace,
405 overrides: BTreeMap::new(),
406 }
407 }
408
409 #[test]
410 fn metric_groups_default_trims_to_warn_tagged() {
411 let filter_string = MetricGroups::default().to_filter_string();
415 assert!(filter_string.starts_with("default=info,"));
416 assert!(filter_string.contains("starfish_core=warn"));
417 assert!(filter_string.contains("iota_core::execution_cache=warn"));
418 assert!(filter_string.contains("iota_grpc_server=warn"));
419 assert!(!filter_string.contains("=debug"));
421
422 let filter = Filter::parse(&filter_string);
423 assert!(filter.is_exposed("x", "starfish_core::metrics", MetricLevel::Warn));
424 assert!(!filter.is_exposed("x", "starfish_core::metrics", MetricLevel::Info));
425 assert!(filter.is_exposed("x", "iota_node::some_module", MetricLevel::Info));
426 assert!(!filter.is_exposed("x", "iota_node::some_module", MetricLevel::Debug));
427
428 let (filter, errors) = MetricGroups::default().startup_filter(Some("trace"));
432 assert!(errors.is_empty());
433 assert!(filter.is_exposed("x", "iota_core::execution_cache", MetricLevel::Trace));
434 assert!(filter.is_exposed("x", "iota_node::some_module", MetricLevel::Trace));
435 assert_eq!(filter.startup_filter_string(), "trace");
436 }
437
438 #[test]
439 fn to_display_string_keys_by_group_name() {
440 let display = MetricGroups {
442 consensus: MetricLevel::Off,
443 storage: MetricLevel::Trace,
444 ..MetricGroups::default()
445 }
446 .to_display_string();
447 assert!(display.starts_with("default=info,"));
448 assert!(display.contains("consensus=off"));
449 assert!(display.contains("storage=trace"));
450 assert!(display.contains("hardware=warn"));
451 assert!(!display.contains("::"));
453 assert!(!display.contains("starfish_core"));
454 }
455
456 #[test]
457 fn metric_groups_renders_level_per_module() {
458 let groups = MetricGroups {
459 execution: MetricLevel::Warn,
460 checkpoints: MetricLevel::Debug,
461 epoch: MetricLevel::Off,
462 ..all_trace()
463 };
464 let filter_string = groups.to_filter_string();
465 assert!(filter_string.starts_with("default=trace,"));
466 assert!(filter_string.contains("iota_core::execution_cache=warn"));
467 assert!(filter_string.contains("iota_core::checkpoints=debug"));
468 assert!(filter_string.contains("iota_core::epoch::epoch_metrics=off"));
469
470 let filter = Filter::parse(&filter_string);
471 assert!(filter.is_exposed("x", "iota_core::execution_cache", MetricLevel::Warn));
472 assert!(!filter.is_exposed("x", "iota_core::execution_cache", MetricLevel::Info));
473 assert!(filter.is_exposed("x", "iota_core::checkpoints", MetricLevel::Debug));
474 assert!(!filter.is_exposed("x", "iota_core::checkpoints", MetricLevel::Trace));
475 assert!(!filter.is_exposed("x", "iota_core::epoch::epoch_metrics", MetricLevel::Warn));
476 assert!(filter.is_exposed("x", "iota_core::quorum_driver", MetricLevel::Trace));
479 assert!(filter.is_exposed("x", "iota_node::some_module", MetricLevel::Trace));
481 }
482
483 #[test]
484 fn metric_groups_runtime_prefix_is_overridden_by_submodule_groups() {
485 let groups = MetricGroups {
489 runtime: MetricLevel::Trace,
490 p2p: MetricLevel::Warn,
491 hardware: MetricLevel::Off,
492 ..all_trace()
493 };
494 let filter = Filter::parse(&groups.to_filter_string());
495 assert!(filter.is_exposed("monitored_tasks", "iota_metrics", MetricLevel::Trace));
496 assert!(filter.is_exposed(
497 "network_peer_rtt",
498 "iota_metrics::metrics_network",
499 MetricLevel::Warn
500 ));
501 assert!(!filter.is_exposed(
502 "network_peer_rtt",
503 "iota_metrics::metrics_network",
504 MetricLevel::Info
505 ));
506 assert!(!filter.is_exposed(
507 "hw_cpu_core_count",
508 "iota_metrics::hardware_metrics",
509 MetricLevel::Warn
510 ));
511 }
512
513 #[test]
514 fn metric_groups_name_patterns_split_shared_module() {
515 let groups = MetricGroups {
519 epoch: MetricLevel::Off,
520 authority: MetricLevel::Debug,
521 ..all_trace()
522 };
523 let filter = Filter::parse(&groups.to_filter_string());
524 assert!(!filter.is_exposed(
525 "iota_current_protocol_version",
526 "iota_node::metrics",
527 MetricLevel::Warn
528 ));
529 assert!(filter.is_exposed(
530 "authority_grpc_requests",
531 "iota_node::metrics",
532 MetricLevel::Debug
533 ));
534 }
535
536 #[test]
537 fn modules_for_group_covers_every_group() {
538 let filter = MetricGroups::default().to_filter_string();
541 for group in [
542 "consensus",
543 "execution",
544 "checkpoints",
545 "transactions",
546 "authority",
547 "traffic-control",
548 "network",
549 "p2p",
550 "storage",
551 "rpc",
552 "epoch",
553 "runtime",
554 "hardware",
555 ] {
556 let modules = MetricGroups::modules_for_group(group)
557 .unwrap_or_else(|| panic!("group {group} has no modules"));
558 assert!(!modules.is_empty());
559 for module in modules {
560 assert!(filter.contains(&format!("{module}=warn")));
561 }
562 }
563 assert_eq!(MetricGroups::modules_for_group("bogus"), None);
565 }
566
567 #[test]
568 fn expand_directives_expands_groups_and_passes_raw_patterns() {
569 assert_eq!(
570 MetricGroups::expand_directives("checkpoints=off,epoch=debug").unwrap(),
571 "iota_core::checkpoints=off,iota_core::epoch::epoch_metrics=debug,\
572 iota_current_protocol_version=debug,iota_binary_max_protocol_version=debug,\
573 iota_configured_max_protocol_version=debug"
574 );
575 assert_eq!(
578 MetricGroups::expand_directives("typed_store=warn, uptime=off ,trace").unwrap(),
579 "typed_store=warn,uptime=off,trace"
580 );
581 assert_eq!(MetricGroups::expand_directives("").unwrap(), "");
582 assert_eq!(
586 MetricGroups::expand_directives("default=info,traffic-control=off").unwrap(),
587 "default=info,iota_core::traffic_controller=off,\
588 iota_config::node_config_metrics=off"
589 );
590 assert_eq!(
592 MetricGroups::expand_directives("hardware=off").unwrap(),
593 "iota_metrics::hardware_metrics=off"
594 );
595 assert_eq!(
596 MetricGroups::expand_directives("iota_metrics=off,runtime=warn").unwrap(),
597 "iota_metrics=off,iota_metrics=warn,telemetry_subscribers=warn"
598 );
599 }
600
601 #[test]
602 fn runtime_group_override_keeps_other_groups_untouched() {
603 use prometheus_filtered::FilterSource;
604
605 let groups = MetricGroups {
609 hardware: MetricLevel::Off,
610 ..MetricGroups::default()
611 };
612 let filter = prometheus_filtered::Filter::from_sources(
613 FilterSource::with_display(&groups.to_filter_string(), &groups.to_display_string()),
614 None,
615 );
616 let expanded = MetricGroups::expand_directives("runtime=trace").unwrap();
617 filter
618 .set_runtime_filter(FilterSource::with_display(&expanded, "runtime=trace"))
619 .unwrap();
620
621 assert!(filter.is_exposed("x", "iota_metrics::monitored_mpsc", MetricLevel::Trace));
623 assert!(!filter.is_exposed(
625 "hw_metrics",
626 "iota_metrics::hardware_metrics",
627 MetricLevel::Warn
628 ));
629 assert!(!filter.is_exposed("x", "iota_metrics::metrics_network", MetricLevel::Info));
630 let display = filter.filter_string();
632 assert!(display.contains("runtime=trace"), "{display}");
633 assert!(display.contains("hardware=off"), "{display}");
634 assert!(display.contains("p2p=warn"), "{display}");
635 assert!(!display.contains("runtime=warn"), "{display}");
636 }
637
638 #[test]
639 fn expand_startup_directives_drops_bad_directives() {
640 let (expanded, errors) =
642 MetricGroups::expand_startup_directives("consensus=bogus,traffic-control=off");
643 assert_eq!(
644 expanded,
645 "iota_core::traffic_controller=off,iota_config::node_config_metrics=off"
646 );
647 assert_eq!(errors.len(), 1);
648 assert!(
649 errors[0].contains("consensus=bogus"),
650 "unexpected error: {}",
651 errors[0]
652 );
653 }
654
655 #[test]
656 fn expand_directives_rejects_invalid_input() {
657 let err =
660 MetricGroups::expand_directives("consensus=bogus,storage=warn,epoch=nah").unwrap_err();
661 assert!(err.contains("consensus=bogus"), "unexpected error: {err}");
662 assert!(err.contains("epoch=nah"), "unexpected error: {err}");
663 }
664
665 #[test]
666 fn metric_groups_config_parsing() {
667 let groups: MetricGroups = serde_yaml::from_str("traffic-control: off").unwrap();
670 assert_eq!(groups.default, MetricLevel::Info);
671 assert_eq!(groups.consensus, MetricLevel::Warn);
672 assert_eq!(groups.traffic_control, MetricLevel::Off);
673 assert_eq!(groups.hardware, MetricLevel::Warn);
674 let filter = groups.to_filter_string();
675 assert!(filter.contains("iota_core::traffic_controller=off"));
676 assert!(filter.contains("starfish_core=warn"));
677
678 assert!(serde_yaml::from_str::<MetricGroups>("traffic_control: off").is_err());
681 assert!(serde_yaml::from_str::<MetricGroups>("bogus: warn").is_err());
682 }
683
684 #[test]
685 fn metric_groups_config_allows_free_overrides() {
686 let groups: MetricGroups = serde_yaml::from_str(
689 "consensus: off\n\
690 overrides:\n \"iota_core::authority::foo\": trace\n bespoke_metric: off\n \
691 certs_total: trace\n network: trace",
692 )
693 .unwrap();
694 assert_eq!(groups.consensus, MetricLevel::Off);
695
696 let filter_string = groups.to_filter_string();
697 assert!(filter_string.contains("iota_core::authority::foo=trace"));
698 assert!(filter_string.contains("bespoke_metric=off"));
699 assert!(filter_string.contains("iota_network::discovery=trace"));
702
703 let filter = Filter::parse(&filter_string);
704 assert!(filter.is_exposed("x", "iota_core::authority::foo", MetricLevel::Trace));
706 assert!(!filter.is_exposed("bespoke_metric_total", "somewhere", MetricLevel::Warn));
707 assert!(filter.is_exposed(
711 "certs_total",
712 "iota_core::execution_cache",
713 MetricLevel::Trace
714 ));
715 assert!(!filter.is_exposed("other", "iota_core::execution_cache", MetricLevel::Info));
717 assert!(filter.is_exposed("x", "iota_network::discovery", MetricLevel::Trace));
719 assert!(!filter.is_exposed("x", "iota_storage::http_key_value_store", MetricLevel::Info));
721 }
722}