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,
88 pub traffic_control: MetricLevel,
94 pub network: MetricLevel,
99 pub p2p: MetricLevel,
106 pub storage: MetricLevel,
112 pub rpc: MetricLevel,
117 pub epoch: MetricLevel,
124 pub runtime: MetricLevel,
131 pub hardware: MetricLevel,
137 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
146 pub overrides: BTreeMap<String, MetricLevel>,
147}
148
149impl Default for MetricGroups {
150 fn default() -> Self {
151 Self {
152 default: MetricLevel::Info,
153 consensus: MetricLevel::Warn,
154 execution: MetricLevel::Warn,
155 checkpoints: MetricLevel::Warn,
156 transactions: MetricLevel::Warn,
157 authority: MetricLevel::Warn,
158 traffic_control: MetricLevel::Warn,
159 network: MetricLevel::Warn,
160 p2p: MetricLevel::Warn,
161 storage: MetricLevel::Warn,
162 rpc: MetricLevel::Warn,
163 epoch: MetricLevel::Warn,
164 runtime: MetricLevel::Warn,
165 hardware: MetricLevel::Warn,
166 overrides: BTreeMap::new(),
167 }
168 }
169}
170
171impl MetricGroups {
172 fn modules_for_group(group: &str) -> Option<&'static [&'static str]> {
175 Some(match group {
176 "runtime" => &["iota_metrics", "telemetry_subscribers"],
177 "consensus" => &[
178 "starfish_core",
179 "iota_core::consensus_adapter",
180 "iota_core::consensus_manager",
181 "iota_core::consensus_validator",
182 "iota_core::epoch::consensus_store_pruner",
183 ],
184 "execution" => &[
185 "iota_core::execution_cache",
186 "iota_core::global_state_hasher",
187 "iota_core::module_cache_metrics",
188 "iota_types::metrics",
189 ],
190 "checkpoints" => &["iota_core::checkpoints"],
191 "transactions" => &[
192 "iota_core::quorum_driver",
193 "iota_core::transaction_driver",
194 "iota_core::transaction_orchestrator",
195 "iota_core::validator_tx_finalizer",
196 ],
197 "authority" => &[
198 "iota_core::authority",
199 "iota_core::safe_client",
200 "iota_core::signature_verifier",
201 "iota_core::validator_client_monitor",
202 "authority_grpc",
206 ],
207 "traffic-control" => &[
208 "iota_core::traffic_controller",
209 "iota_config::node_config_metrics",
210 ],
211 "network" => &[
212 "iota_network::discovery",
213 "iota_network::randomness",
214 "iota_network::state_sync",
215 ],
216 "p2p" => &["iota_metrics::metrics_network"],
217 "storage" => &["typed_store", "iota_storage", "iota_snapshot"],
218 "rpc" => &[
219 "iota_json_rpc",
220 "iota_grpc_server",
221 "iota_graphql_rpc",
222 "iota_core::jsonrpc_index",
223 "iota_core::subscription_handler",
224 ],
225 "epoch" => &[
226 "iota_core::epoch::epoch_metrics",
227 "iota_current_protocol_version",
229 "iota_binary_max_protocol_version",
230 "iota_configured_max_protocol_version",
231 ],
232 "hardware" => &["iota_metrics::hardware_metrics"],
233 _ => return None,
234 })
235 }
236
237 fn group_levels(&self) -> [(&'static str, MetricLevel); 13] {
239 [
240 ("runtime", self.runtime),
241 ("consensus", self.consensus),
242 ("execution", self.execution),
243 ("checkpoints", self.checkpoints),
244 ("transactions", self.transactions),
245 ("authority", self.authority),
246 ("traffic-control", self.traffic_control),
247 ("network", self.network),
248 ("p2p", self.p2p),
249 ("storage", self.storage),
250 ("rpc", self.rpc),
251 ("epoch", self.epoch),
252 ("hardware", self.hardware),
253 ]
254 }
255
256 fn group_modules(&self) -> [(MetricLevel, &'static [&'static str]); 13] {
258 self.group_levels().map(|(group, level)| {
259 (
260 level,
261 Self::modules_for_group(group).expect("group has modules"),
262 )
263 })
264 }
265
266 fn to_filter_string(&self) -> String {
268 let mut directives = vec![format!("default={}", self.default.as_str())];
269 for (level, modules) in self.group_modules() {
270 for module in modules {
271 directives.push(format!("{module}={}", level.as_str()));
272 }
273 }
274 for directive in self.override_directives() {
275 directives.extend(
279 Self::expand_directive(&directive)
280 .expect("override levels are typed, so the rendered directive is valid"),
281 );
282 }
283 directives.join(",")
284 }
285
286 fn to_display_string(&self) -> String {
290 let mut directives = vec![format!("default={}", self.default.as_str())];
291 for (group, level) in self.group_levels() {
292 directives.push(format!("{group}={}", level.as_str()));
293 }
294 directives.extend(self.override_directives());
295 directives.join(",")
296 }
297
298 pub(crate) fn expand_directives(filter: &str) -> Result<String, String> {
303 let (directives, errors) = Self::expand_startup_directives(filter);
304 if errors.is_empty() {
305 Ok(directives)
306 } else {
307 Err(errors.join("; "))
308 }
309 }
310
311 fn expand_startup_directives(filter: &str) -> (String, Vec<String>) {
315 let mut directives = Vec::new();
316 let mut errors = Vec::new();
317 for part in prometheus_filtered::directive_parts(filter) {
318 match Self::expand_directive(part) {
319 Ok(expanded) => directives.extend(expanded),
320 Err(err) => errors.push(err),
321 }
322 }
323 (directives.join(","), errors)
324 }
325
326 pub fn startup_filter(&self, env: Option<&str>) -> (prometheus_filtered::Filter, Vec<String>) {
335 let directives = self.to_filter_string();
336 let display = self.to_display_string();
337 let config = prometheus_filtered::FilterSource::with_display(&directives, &display);
338 match env {
339 Some(env) => {
340 let (expanded, errors) = Self::expand_startup_directives(env);
341 let filter = prometheus_filtered::Filter::from_sources(
342 config,
343 Some(prometheus_filtered::FilterSource::with_display(
344 &expanded, env,
345 )),
346 );
347 (filter, errors)
348 }
349 None => (
350 prometheus_filtered::Filter::from_sources(config, None),
351 Vec::new(),
352 ),
353 }
354 }
355
356 fn expand_directive(part: &str) -> Result<Vec<String>, String> {
357 let (pattern, level) = prometheus_filtered::split_directive(part)?;
358 Ok(match Self::modules_for_group(pattern) {
359 Some(modules) => modules
360 .iter()
361 .map(|module| format!("{module}={}", level.as_str()))
362 .collect(),
363 None => vec![part.to_owned()],
365 })
366 }
367
368 fn override_directives(&self) -> impl Iterator<Item = String> + '_ {
370 self.overrides
371 .iter()
372 .map(|(pattern, level)| format!("{pattern}={}", level.as_str()))
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use std::collections::BTreeMap;
379
380 use prometheus_filtered::Filter;
381
382 use super::{MetricGroups, MetricLevel};
383
384 fn all_trace() -> MetricGroups {
385 MetricGroups {
386 default: MetricLevel::Trace,
387 consensus: MetricLevel::Trace,
388 execution: MetricLevel::Trace,
389 checkpoints: MetricLevel::Trace,
390 transactions: MetricLevel::Trace,
391 authority: MetricLevel::Trace,
392 traffic_control: MetricLevel::Trace,
393 network: MetricLevel::Trace,
394 p2p: MetricLevel::Trace,
395 storage: MetricLevel::Trace,
396 rpc: MetricLevel::Trace,
397 epoch: MetricLevel::Trace,
398 runtime: MetricLevel::Trace,
399 hardware: MetricLevel::Trace,
400 overrides: BTreeMap::new(),
401 }
402 }
403
404 #[test]
405 fn metric_groups_default_trims_to_warn_tagged() {
406 let filter_string = MetricGroups::default().to_filter_string();
410 assert!(filter_string.starts_with("default=info,"));
411 assert!(filter_string.contains("starfish_core=warn"));
412 assert!(filter_string.contains("iota_core::execution_cache=warn"));
413 assert!(filter_string.contains("iota_grpc_server=warn"));
414 assert!(!filter_string.contains("=debug"));
416
417 let filter = Filter::parse(&filter_string);
418 assert!(filter.is_exposed("x", "starfish_core::metrics", MetricLevel::Warn));
419 assert!(!filter.is_exposed("x", "starfish_core::metrics", MetricLevel::Info));
420 assert!(filter.is_exposed("x", "iota_node::some_module", MetricLevel::Info));
421 assert!(!filter.is_exposed("x", "iota_node::some_module", MetricLevel::Debug));
422
423 let (filter, errors) = MetricGroups::default().startup_filter(Some("trace"));
427 assert!(errors.is_empty());
428 assert!(filter.is_exposed("x", "iota_core::execution_cache", MetricLevel::Trace));
429 assert!(filter.is_exposed("x", "iota_node::some_module", MetricLevel::Trace));
430 assert_eq!(filter.startup_filter_string(), "trace");
431 }
432
433 #[test]
434 fn to_display_string_keys_by_group_name() {
435 let display = MetricGroups {
437 consensus: MetricLevel::Off,
438 storage: MetricLevel::Trace,
439 ..MetricGroups::default()
440 }
441 .to_display_string();
442 assert!(display.starts_with("default=info,"));
443 assert!(display.contains("consensus=off"));
444 assert!(display.contains("storage=trace"));
445 assert!(display.contains("hardware=warn"));
446 assert!(!display.contains("::"));
448 assert!(!display.contains("starfish_core"));
449 }
450
451 #[test]
452 fn metric_groups_renders_level_per_module() {
453 let groups = MetricGroups {
454 execution: MetricLevel::Warn,
455 checkpoints: MetricLevel::Debug,
456 epoch: MetricLevel::Off,
457 ..all_trace()
458 };
459 let filter_string = groups.to_filter_string();
460 assert!(filter_string.starts_with("default=trace,"));
461 assert!(filter_string.contains("iota_core::execution_cache=warn"));
462 assert!(filter_string.contains("iota_core::checkpoints=debug"));
463 assert!(filter_string.contains("iota_core::epoch::epoch_metrics=off"));
464
465 let filter = Filter::parse(&filter_string);
466 assert!(filter.is_exposed("x", "iota_core::execution_cache", MetricLevel::Warn));
467 assert!(!filter.is_exposed("x", "iota_core::execution_cache", MetricLevel::Info));
468 assert!(filter.is_exposed("x", "iota_core::checkpoints", MetricLevel::Debug));
469 assert!(!filter.is_exposed("x", "iota_core::checkpoints", MetricLevel::Trace));
470 assert!(!filter.is_exposed("x", "iota_core::epoch::epoch_metrics", MetricLevel::Warn));
471 assert!(filter.is_exposed("x", "iota_core::quorum_driver", MetricLevel::Trace));
474 assert!(filter.is_exposed("x", "iota_node::some_module", MetricLevel::Trace));
476 }
477
478 #[test]
479 fn metric_groups_runtime_prefix_is_overridden_by_submodule_groups() {
480 let groups = MetricGroups {
484 runtime: MetricLevel::Trace,
485 p2p: MetricLevel::Warn,
486 hardware: MetricLevel::Off,
487 ..all_trace()
488 };
489 let filter = Filter::parse(&groups.to_filter_string());
490 assert!(filter.is_exposed("monitored_tasks", "iota_metrics", MetricLevel::Trace));
491 assert!(filter.is_exposed(
492 "network_peer_rtt",
493 "iota_metrics::metrics_network",
494 MetricLevel::Warn
495 ));
496 assert!(!filter.is_exposed(
497 "network_peer_rtt",
498 "iota_metrics::metrics_network",
499 MetricLevel::Info
500 ));
501 assert!(!filter.is_exposed(
502 "hw_cpu_core_count",
503 "iota_metrics::hardware_metrics",
504 MetricLevel::Warn
505 ));
506 }
507
508 #[test]
509 fn metric_groups_name_patterns_split_shared_module() {
510 let groups = MetricGroups {
514 epoch: MetricLevel::Off,
515 authority: MetricLevel::Debug,
516 ..all_trace()
517 };
518 let filter = Filter::parse(&groups.to_filter_string());
519 assert!(!filter.is_exposed(
520 "iota_current_protocol_version",
521 "iota_node::metrics",
522 MetricLevel::Warn
523 ));
524 assert!(filter.is_exposed(
525 "authority_grpc_requests",
526 "iota_node::metrics",
527 MetricLevel::Debug
528 ));
529 }
530
531 #[test]
532 fn modules_for_group_covers_every_group() {
533 let filter = MetricGroups::default().to_filter_string();
536 for group in [
537 "consensus",
538 "execution",
539 "checkpoints",
540 "transactions",
541 "authority",
542 "traffic-control",
543 "network",
544 "p2p",
545 "storage",
546 "rpc",
547 "epoch",
548 "runtime",
549 "hardware",
550 ] {
551 let modules = MetricGroups::modules_for_group(group)
552 .unwrap_or_else(|| panic!("group {group} has no modules"));
553 assert!(!modules.is_empty());
554 for module in modules {
555 assert!(filter.contains(&format!("{module}=warn")));
556 }
557 }
558 assert_eq!(MetricGroups::modules_for_group("bogus"), None);
560 }
561
562 #[test]
563 fn expand_directives_expands_groups_and_passes_raw_patterns() {
564 assert_eq!(
565 MetricGroups::expand_directives("checkpoints=off,epoch=debug").unwrap(),
566 "iota_core::checkpoints=off,iota_core::epoch::epoch_metrics=debug,\
567 iota_current_protocol_version=debug,iota_binary_max_protocol_version=debug,\
568 iota_configured_max_protocol_version=debug"
569 );
570 assert_eq!(
573 MetricGroups::expand_directives("typed_store=warn, uptime=off ,trace").unwrap(),
574 "typed_store=warn,uptime=off,trace"
575 );
576 assert_eq!(MetricGroups::expand_directives("").unwrap(), "");
577 assert_eq!(
581 MetricGroups::expand_directives("default=info,traffic-control=off").unwrap(),
582 "default=info,iota_core::traffic_controller=off,\
583 iota_config::node_config_metrics=off"
584 );
585 assert_eq!(
587 MetricGroups::expand_directives("hardware=off").unwrap(),
588 "iota_metrics::hardware_metrics=off"
589 );
590 assert_eq!(
591 MetricGroups::expand_directives("iota_metrics=off,runtime=warn").unwrap(),
592 "iota_metrics=off,iota_metrics=warn,telemetry_subscribers=warn"
593 );
594 }
595
596 #[test]
597 fn runtime_group_override_keeps_other_groups_untouched() {
598 use prometheus_filtered::FilterSource;
599
600 let groups = MetricGroups {
604 hardware: MetricLevel::Off,
605 ..MetricGroups::default()
606 };
607 let filter = prometheus_filtered::Filter::from_sources(
608 FilterSource::with_display(&groups.to_filter_string(), &groups.to_display_string()),
609 None,
610 );
611 let expanded = MetricGroups::expand_directives("runtime=trace").unwrap();
612 filter
613 .set_runtime_filter(FilterSource::with_display(&expanded, "runtime=trace"))
614 .unwrap();
615
616 assert!(filter.is_exposed("x", "iota_metrics::monitored_mpsc", MetricLevel::Trace));
618 assert!(!filter.is_exposed(
620 "hw_metrics",
621 "iota_metrics::hardware_metrics",
622 MetricLevel::Warn
623 ));
624 assert!(!filter.is_exposed("x", "iota_metrics::metrics_network", MetricLevel::Info));
625 let display = filter.filter_string();
627 assert!(display.contains("runtime=trace"), "{display}");
628 assert!(display.contains("hardware=off"), "{display}");
629 assert!(display.contains("p2p=warn"), "{display}");
630 assert!(!display.contains("runtime=warn"), "{display}");
631 }
632
633 #[test]
634 fn expand_startup_directives_drops_bad_directives() {
635 let (expanded, errors) =
637 MetricGroups::expand_startup_directives("consensus=bogus,traffic-control=off");
638 assert_eq!(
639 expanded,
640 "iota_core::traffic_controller=off,iota_config::node_config_metrics=off"
641 );
642 assert_eq!(errors.len(), 1);
643 assert!(
644 errors[0].contains("consensus=bogus"),
645 "unexpected error: {}",
646 errors[0]
647 );
648 }
649
650 #[test]
651 fn expand_directives_rejects_invalid_input() {
652 let err =
655 MetricGroups::expand_directives("consensus=bogus,storage=warn,epoch=nah").unwrap_err();
656 assert!(err.contains("consensus=bogus"), "unexpected error: {err}");
657 assert!(err.contains("epoch=nah"), "unexpected error: {err}");
658 }
659
660 #[test]
661 fn metric_groups_config_parsing() {
662 let groups: MetricGroups = serde_yaml::from_str("traffic-control: off").unwrap();
665 assert_eq!(groups.default, MetricLevel::Info);
666 assert_eq!(groups.consensus, MetricLevel::Warn);
667 assert_eq!(groups.traffic_control, MetricLevel::Off);
668 assert_eq!(groups.hardware, MetricLevel::Warn);
669 let filter = groups.to_filter_string();
670 assert!(filter.contains("iota_core::traffic_controller=off"));
671 assert!(filter.contains("starfish_core=warn"));
672
673 assert!(serde_yaml::from_str::<MetricGroups>("traffic_control: off").is_err());
676 assert!(serde_yaml::from_str::<MetricGroups>("bogus: warn").is_err());
677 }
678
679 #[test]
680 fn metric_groups_config_allows_free_overrides() {
681 let groups: MetricGroups = serde_yaml::from_str(
684 "consensus: off\n\
685 overrides:\n \"iota_core::authority::foo\": trace\n bespoke_metric: off\n \
686 certs_total: trace\n network: trace",
687 )
688 .unwrap();
689 assert_eq!(groups.consensus, MetricLevel::Off);
690
691 let filter_string = groups.to_filter_string();
692 assert!(filter_string.contains("iota_core::authority::foo=trace"));
693 assert!(filter_string.contains("bespoke_metric=off"));
694 assert!(filter_string.contains("iota_network::discovery=trace"));
697
698 let filter = Filter::parse(&filter_string);
699 assert!(filter.is_exposed("x", "iota_core::authority::foo", MetricLevel::Trace));
701 assert!(!filter.is_exposed("bespoke_metric_total", "somewhere", MetricLevel::Warn));
702 assert!(filter.is_exposed(
706 "certs_total",
707 "iota_core::execution_cache",
708 MetricLevel::Trace
709 ));
710 assert!(!filter.is_exposed("other", "iota_core::execution_cache", MetricLevel::Info));
712 assert!(filter.is_exposed("x", "iota_network::discovery", MetricLevel::Trace));
714 assert!(!filter.is_exposed("x", "iota_storage::http_key_value_store", MetricLevel::Info));
716 }
717}