1use crate::AnalyzeConfigOverrideSummary;
2use serde::Serialize;
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6mod descriptors;
7mod overrides;
8mod toml;
9pub use descriptors::analyze_option_descriptors;
10
11#[non_exhaustive]
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
27pub struct AnalyzeOptions {
28 pub queueing: QueueingOptions,
30 pub blocking: BlockingOptions,
32 pub executor: ExecutorOptions,
34 pub downstream: DownstreamOptions,
36 pub confidence: ConfidenceOptions,
38 pub evidence: EvidenceOptions,
40 pub route: RouteOptions,
42 pub temporal: TemporalOptions,
44}
45
46#[non_exhaustive]
48#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
49pub struct QueueingOptions {
50 pub trigger_permille: u64,
52}
53
54impl Default for QueueingOptions {
55 fn default() -> Self {
56 Self {
57 trigger_permille: 300,
58 }
59 }
60}
61
62#[non_exhaustive]
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65pub struct BlockingOptions {
66 pub min_nonzero_samples_for_signal: usize,
68 pub strong_p95_threshold: u64,
70 pub strong_peak_threshold: u64,
72 pub strong_nonzero_share_permille: u64,
74 pub strong_min_samples: usize,
76}
77
78impl Default for BlockingOptions {
79 fn default() -> Self {
80 Self {
81 min_nonzero_samples_for_signal: 2,
82 strong_p95_threshold: 12,
83 strong_peak_threshold: 20,
84 strong_nonzero_share_permille: 700,
85 strong_min_samples: 30,
86 }
87 }
88}
89
90#[non_exhaustive]
92#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
93pub struct ExecutorOptions {
94 pub min_global_queue_p95_for_signal: u64,
96}
97
98impl Default for ExecutorOptions {
99 fn default() -> Self {
100 Self {
101 min_global_queue_p95_for_signal: 1,
102 }
103 }
104}
105
106#[non_exhaustive]
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109pub struct DownstreamOptions {
110 pub min_stage_samples: usize,
112 pub blocking_correlated_stage_patterns: Vec<String>,
114 pub blocking_correlation_score_margin: u8,
116}
117
118impl Default for DownstreamOptions {
119 fn default() -> Self {
120 Self {
121 min_stage_samples: 3,
122 blocking_correlated_stage_patterns: vec![
123 "spawn_blocking".to_owned(),
124 "blocking_path".to_owned(),
125 "blocking".to_owned(),
126 ],
127 blocking_correlation_score_margin: 2,
128 }
129 }
130}
131
132#[non_exhaustive]
134#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
135pub struct ConfidenceOptions {
136 pub medium_score_threshold: u8,
138 pub high_score_threshold: u8,
140 pub ambiguity_min_score: u8,
142 pub ambiguity_score_gap: u8,
144}
145
146impl Default for ConfidenceOptions {
147 fn default() -> Self {
148 Self {
149 medium_score_threshold: 65,
150 high_score_threshold: 85,
151 ambiguity_min_score: 60,
152 ambiguity_score_gap: 4,
153 }
154 }
155}
156
157#[non_exhaustive]
159#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
160pub struct EvidenceOptions {
161 pub low_completed_request_threshold: usize,
163}
164
165impl Default for EvidenceOptions {
166 fn default() -> Self {
167 Self {
168 low_completed_request_threshold: 20,
169 }
170 }
171}
172
173#[non_exhaustive]
175#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
176pub struct RouteOptions {
177 pub min_request_count: usize,
179 pub breakdown_limit: usize,
181 pub emit_on_divergent_suspects: bool,
183 pub slowest_to_fastest_p95_ratio_numerator: u64,
185 pub slowest_to_fastest_p95_ratio_denominator: u64,
187 pub slowest_to_global_p95_ratio_numerator: u64,
189 pub slowest_to_global_p95_ratio_denominator: u64,
191}
192
193impl Default for RouteOptions {
194 fn default() -> Self {
195 Self {
196 min_request_count: 3,
197 breakdown_limit: 10,
198 emit_on_divergent_suspects: true,
199 slowest_to_fastest_p95_ratio_numerator: 3,
200 slowest_to_fastest_p95_ratio_denominator: 2,
201 slowest_to_global_p95_ratio_numerator: 5,
202 slowest_to_global_p95_ratio_denominator: 4,
203 }
204 }
205}
206
207#[non_exhaustive]
209#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
210pub struct TemporalOptions {
211 pub min_request_count: usize,
213 pub min_segment_request_count: usize,
215 pub share_shift_permille: u64,
217 pub p95_shift_ratio_numerator: u64,
219 pub p95_shift_ratio_denominator: u64,
221 pub emit_on_suspect_shift: bool,
223 pub suppress_runtime_sparse_suspect_shift_without_supporting_movement: bool,
225}
226
227impl Default for TemporalOptions {
228 fn default() -> Self {
229 Self {
230 min_request_count: 20,
231 min_segment_request_count: 8,
232 share_shift_permille: 200,
233 p95_shift_ratio_numerator: 3,
234 p95_shift_ratio_denominator: 2,
235 emit_on_suspect_shift: true,
236 suppress_runtime_sparse_suspect_shift_without_supporting_movement: true,
237 }
238 }
239}
240
241fn push_non_default_override(
242 out: &mut Vec<AnalyzeConfigOverrideSummary>,
243 path: &str,
244 value: String,
245) {
246 out.push(AnalyzeConfigOverrideSummary {
247 path: path.to_string(),
248 value,
249 });
250}
251
252fn queueing_non_default_overrides(
253 out: &mut Vec<AnalyzeConfigOverrideSummary>,
254 options: &AnalyzeOptions,
255 defaults: &AnalyzeOptions,
256) {
257 if options.queueing.trigger_permille != defaults.queueing.trigger_permille {
258 push_non_default_override(
259 out,
260 "queueing.trigger_permille",
261 options.queueing.trigger_permille.to_string(),
262 );
263 }
264}
265
266fn blocking_non_default_overrides(
267 out: &mut Vec<AnalyzeConfigOverrideSummary>,
268 options: &AnalyzeOptions,
269 defaults: &AnalyzeOptions,
270) {
271 if options.blocking.min_nonzero_samples_for_signal
272 != defaults.blocking.min_nonzero_samples_for_signal
273 {
274 push_non_default_override(
275 out,
276 "blocking.min_nonzero_samples_for_signal",
277 options.blocking.min_nonzero_samples_for_signal.to_string(),
278 );
279 }
280 if options.blocking.strong_p95_threshold != defaults.blocking.strong_p95_threshold {
281 push_non_default_override(
282 out,
283 "blocking.strong_p95_threshold",
284 options.blocking.strong_p95_threshold.to_string(),
285 );
286 }
287 if options.blocking.strong_peak_threshold != defaults.blocking.strong_peak_threshold {
288 push_non_default_override(
289 out,
290 "blocking.strong_peak_threshold",
291 options.blocking.strong_peak_threshold.to_string(),
292 );
293 }
294 if options.blocking.strong_nonzero_share_permille
295 != defaults.blocking.strong_nonzero_share_permille
296 {
297 push_non_default_override(
298 out,
299 "blocking.strong_nonzero_share_permille",
300 options.blocking.strong_nonzero_share_permille.to_string(),
301 );
302 }
303 if options.blocking.strong_min_samples != defaults.blocking.strong_min_samples {
304 push_non_default_override(
305 out,
306 "blocking.strong_min_samples",
307 options.blocking.strong_min_samples.to_string(),
308 );
309 }
310}
311
312fn executor_non_default_overrides(
313 out: &mut Vec<AnalyzeConfigOverrideSummary>,
314 options: &AnalyzeOptions,
315 defaults: &AnalyzeOptions,
316) {
317 if options.executor.min_global_queue_p95_for_signal
318 != defaults.executor.min_global_queue_p95_for_signal
319 {
320 push_non_default_override(
321 out,
322 "executor.min_global_queue_p95_for_signal",
323 options.executor.min_global_queue_p95_for_signal.to_string(),
324 );
325 }
326}
327
328fn downstream_non_default_overrides(
329 out: &mut Vec<AnalyzeConfigOverrideSummary>,
330 options: &AnalyzeOptions,
331 defaults: &AnalyzeOptions,
332) {
333 if options.downstream.min_stage_samples != defaults.downstream.min_stage_samples {
334 push_non_default_override(
335 out,
336 "downstream.min_stage_samples",
337 options.downstream.min_stage_samples.to_string(),
338 );
339 }
340 if options.downstream.blocking_correlated_stage_patterns
341 != defaults.downstream.blocking_correlated_stage_patterns
342 {
343 push_non_default_override(
344 out,
345 "downstream.blocking_correlated_stage_patterns",
346 options
347 .downstream
348 .blocking_correlated_stage_patterns
349 .join(","),
350 );
351 }
352 if options.downstream.blocking_correlation_score_margin
353 != defaults.downstream.blocking_correlation_score_margin
354 {
355 push_non_default_override(
356 out,
357 "downstream.blocking_correlation_score_margin",
358 options
359 .downstream
360 .blocking_correlation_score_margin
361 .to_string(),
362 );
363 }
364}
365
366fn confidence_non_default_overrides(
367 out: &mut Vec<AnalyzeConfigOverrideSummary>,
368 options: &AnalyzeOptions,
369 defaults: &AnalyzeOptions,
370) {
371 if options.confidence.medium_score_threshold != defaults.confidence.medium_score_threshold {
372 push_non_default_override(
373 out,
374 "confidence.medium_score_threshold",
375 options.confidence.medium_score_threshold.to_string(),
376 );
377 }
378 if options.confidence.high_score_threshold != defaults.confidence.high_score_threshold {
379 push_non_default_override(
380 out,
381 "confidence.high_score_threshold",
382 options.confidence.high_score_threshold.to_string(),
383 );
384 }
385 if options.confidence.ambiguity_min_score != defaults.confidence.ambiguity_min_score {
386 push_non_default_override(
387 out,
388 "confidence.ambiguity_min_score",
389 options.confidence.ambiguity_min_score.to_string(),
390 );
391 }
392 if options.confidence.ambiguity_score_gap != defaults.confidence.ambiguity_score_gap {
393 push_non_default_override(
394 out,
395 "confidence.ambiguity_score_gap",
396 options.confidence.ambiguity_score_gap.to_string(),
397 );
398 }
399}
400
401fn evidence_non_default_overrides(
402 out: &mut Vec<AnalyzeConfigOverrideSummary>,
403 options: &AnalyzeOptions,
404 defaults: &AnalyzeOptions,
405) {
406 if options.evidence.low_completed_request_threshold
407 != defaults.evidence.low_completed_request_threshold
408 {
409 push_non_default_override(
410 out,
411 "evidence.low_completed_request_threshold",
412 options.evidence.low_completed_request_threshold.to_string(),
413 );
414 }
415}
416
417fn route_non_default_overrides(
418 out: &mut Vec<AnalyzeConfigOverrideSummary>,
419 options: &AnalyzeOptions,
420 defaults: &AnalyzeOptions,
421) {
422 if options.route.min_request_count != defaults.route.min_request_count {
423 push_non_default_override(
424 out,
425 "route.min_request_count",
426 options.route.min_request_count.to_string(),
427 );
428 }
429 if options.route.breakdown_limit != defaults.route.breakdown_limit {
430 push_non_default_override(
431 out,
432 "route.breakdown_limit",
433 options.route.breakdown_limit.to_string(),
434 );
435 }
436 if options.route.emit_on_divergent_suspects != defaults.route.emit_on_divergent_suspects {
437 push_non_default_override(
438 out,
439 "route.emit_on_divergent_suspects",
440 options.route.emit_on_divergent_suspects.to_string(),
441 );
442 }
443 if options.route.slowest_to_fastest_p95_ratio_numerator
444 != defaults.route.slowest_to_fastest_p95_ratio_numerator
445 {
446 push_non_default_override(
447 out,
448 "route.slowest_to_fastest_p95_ratio_numerator",
449 options
450 .route
451 .slowest_to_fastest_p95_ratio_numerator
452 .to_string(),
453 );
454 }
455 if options.route.slowest_to_fastest_p95_ratio_denominator
456 != defaults.route.slowest_to_fastest_p95_ratio_denominator
457 {
458 push_non_default_override(
459 out,
460 "route.slowest_to_fastest_p95_ratio_denominator",
461 options
462 .route
463 .slowest_to_fastest_p95_ratio_denominator
464 .to_string(),
465 );
466 }
467 if options.route.slowest_to_global_p95_ratio_numerator
468 != defaults.route.slowest_to_global_p95_ratio_numerator
469 {
470 push_non_default_override(
471 out,
472 "route.slowest_to_global_p95_ratio_numerator",
473 options
474 .route
475 .slowest_to_global_p95_ratio_numerator
476 .to_string(),
477 );
478 }
479 if options.route.slowest_to_global_p95_ratio_denominator
480 != defaults.route.slowest_to_global_p95_ratio_denominator
481 {
482 push_non_default_override(
483 out,
484 "route.slowest_to_global_p95_ratio_denominator",
485 options
486 .route
487 .slowest_to_global_p95_ratio_denominator
488 .to_string(),
489 );
490 }
491}
492
493fn temporal_non_default_overrides(
494 out: &mut Vec<AnalyzeConfigOverrideSummary>,
495 options: &AnalyzeOptions,
496 defaults: &AnalyzeOptions,
497) {
498 if options.temporal.min_request_count != defaults.temporal.min_request_count {
499 push_non_default_override(
500 out,
501 "temporal.min_request_count",
502 options.temporal.min_request_count.to_string(),
503 );
504 }
505 if options.temporal.min_segment_request_count != defaults.temporal.min_segment_request_count {
506 push_non_default_override(
507 out,
508 "temporal.min_segment_request_count",
509 options.temporal.min_segment_request_count.to_string(),
510 );
511 }
512 if options.temporal.share_shift_permille != defaults.temporal.share_shift_permille {
513 push_non_default_override(
514 out,
515 "temporal.share_shift_permille",
516 options.temporal.share_shift_permille.to_string(),
517 );
518 }
519 if options.temporal.p95_shift_ratio_numerator != defaults.temporal.p95_shift_ratio_numerator {
520 push_non_default_override(
521 out,
522 "temporal.p95_shift_ratio_numerator",
523 options.temporal.p95_shift_ratio_numerator.to_string(),
524 );
525 }
526 if options.temporal.p95_shift_ratio_denominator != defaults.temporal.p95_shift_ratio_denominator
527 {
528 push_non_default_override(
529 out,
530 "temporal.p95_shift_ratio_denominator",
531 options.temporal.p95_shift_ratio_denominator.to_string(),
532 );
533 }
534 if options.temporal.emit_on_suspect_shift != defaults.temporal.emit_on_suspect_shift {
535 push_non_default_override(
536 out,
537 "temporal.emit_on_suspect_shift",
538 options.temporal.emit_on_suspect_shift.to_string(),
539 );
540 }
541 if options
542 .temporal
543 .suppress_runtime_sparse_suspect_shift_without_supporting_movement
544 != defaults
545 .temporal
546 .suppress_runtime_sparse_suspect_shift_without_supporting_movement
547 {
548 push_non_default_override(
549 out,
550 "temporal.suppress_runtime_sparse_suspect_shift_without_supporting_movement",
551 options
552 .temporal
553 .suppress_runtime_sparse_suspect_shift_without_supporting_movement
554 .to_string(),
555 );
556 }
557}
558impl AnalyzeOptions {
559 #[must_use]
561 pub fn non_default_overrides(&self) -> Vec<AnalyzeConfigOverrideSummary> {
562 let defaults = Self::default();
563 let mut out = Vec::new();
564 queueing_non_default_overrides(&mut out, self, &defaults);
565 blocking_non_default_overrides(&mut out, self, &defaults);
566 executor_non_default_overrides(&mut out, self, &defaults);
567 downstream_non_default_overrides(&mut out, self, &defaults);
568 confidence_non_default_overrides(&mut out, self, &defaults);
569 evidence_non_default_overrides(&mut out, self, &defaults);
570 route_non_default_overrides(&mut out, self, &defaults);
571 temporal_non_default_overrides(&mut out, self, &defaults);
572 out.sort_by(|a, b| a.path.cmp(&b.path));
573 out
574 }
575 #[must_use]
577 pub fn with_queueing(mut self, f: impl FnOnce(&mut QueueingOptions)) -> Self {
578 f(&mut self.queueing);
579 self
580 }
581 #[must_use]
583 pub fn with_blocking(mut self, f: impl FnOnce(&mut BlockingOptions)) -> Self {
584 f(&mut self.blocking);
585 self
586 }
587 #[must_use]
589 pub fn with_executor(mut self, f: impl FnOnce(&mut ExecutorOptions)) -> Self {
590 f(&mut self.executor);
591 self
592 }
593 #[must_use]
595 pub fn with_downstream(mut self, f: impl FnOnce(&mut DownstreamOptions)) -> Self {
596 f(&mut self.downstream);
597 self
598 }
599 #[must_use]
601 pub fn with_confidence(mut self, f: impl FnOnce(&mut ConfidenceOptions)) -> Self {
602 f(&mut self.confidence);
603 self
604 }
605 #[must_use]
607 pub fn with_evidence(mut self, f: impl FnOnce(&mut EvidenceOptions)) -> Self {
608 f(&mut self.evidence);
609 self
610 }
611 #[must_use]
613 pub fn with_route(mut self, f: impl FnOnce(&mut RouteOptions)) -> Self {
614 f(&mut self.route);
615 self
616 }
617 #[must_use]
619 pub fn with_temporal(mut self, f: impl FnOnce(&mut TemporalOptions)) -> Self {
620 f(&mut self.temporal);
621 self
622 }
623 #[allow(clippy::too_many_lines)]
629 pub fn validate(&self) -> Result<(), AnalyzeConfigError> {
630 let invalid =
631 |path, message: String| Err(AnalyzeConfigError::InvalidConfigValue { path, message });
632 if self.queueing.trigger_permille > 1000 {
633 return invalid("queueing.trigger_permille", "must be <= 1000".into());
634 }
635 if self.blocking.strong_nonzero_share_permille > 1000 {
636 return invalid(
637 "blocking.strong_nonzero_share_permille",
638 "must be <= 1000".into(),
639 );
640 }
641 if self.confidence.medium_score_threshold > self.confidence.high_score_threshold {
642 return invalid(
643 "confidence.medium_score_threshold",
644 "must be <= confidence.high_score_threshold".into(),
645 );
646 }
647 if self.confidence.high_score_threshold > 100 {
648 return invalid("confidence.high_score_threshold", "must be <= 100".into());
649 }
650 if self.confidence.ambiguity_min_score > 100 {
651 return invalid("confidence.ambiguity_min_score", "must be <= 100".into());
652 }
653 if self.confidence.ambiguity_score_gap > 100 {
654 return invalid("confidence.ambiguity_score_gap", "must be <= 100".into());
655 }
656 if self.downstream.blocking_correlation_score_margin > 100 {
657 return invalid(
658 "downstream.blocking_correlation_score_margin",
659 "must be <= 100".into(),
660 );
661 }
662 if self.route.breakdown_limit == 0 {
663 return invalid("route.breakdown_limit", "must be > 0".into());
664 }
665 for (num_path, den_path, num, den) in [
666 (
667 "route.slowest_to_fastest_p95_ratio_numerator",
668 "route.slowest_to_fastest_p95_ratio_denominator",
669 self.route.slowest_to_fastest_p95_ratio_numerator,
670 self.route.slowest_to_fastest_p95_ratio_denominator,
671 ),
672 (
673 "route.slowest_to_global_p95_ratio_numerator",
674 "route.slowest_to_global_p95_ratio_denominator",
675 self.route.slowest_to_global_p95_ratio_numerator,
676 self.route.slowest_to_global_p95_ratio_denominator,
677 ),
678 ] {
679 if num == 0 {
680 return invalid(num_path, "must be > 0".into());
681 }
682 if den == 0 {
683 return invalid(den_path, "must be > 0".into());
684 }
685 if num < den {
686 return invalid(num_path, format!("must be >= {den_path}"));
687 }
688 }
689 if self.temporal.min_segment_request_count == 0 {
690 return invalid("temporal.min_segment_request_count", "must be > 0".into());
691 }
692 if self.temporal.min_segment_request_count.saturating_mul(2)
693 > self.temporal.min_request_count
694 {
695 return invalid(
696 "temporal.min_segment_request_count",
697 "min_segment_request_count * 2 must be <= temporal.min_request_count".into(),
698 );
699 }
700 if self.temporal.share_shift_permille > 1000 {
701 return invalid("temporal.share_shift_permille", "must be <= 1000".into());
702 }
703 if self.temporal.p95_shift_ratio_numerator == 0 {
704 return invalid("temporal.p95_shift_ratio_numerator", "must be > 0".into());
705 }
706 if self.temporal.p95_shift_ratio_denominator == 0 {
707 return invalid("temporal.p95_shift_ratio_denominator", "must be > 0".into());
708 }
709 if self.temporal.p95_shift_ratio_numerator < self.temporal.p95_shift_ratio_denominator {
710 return invalid(
711 "temporal.p95_shift_ratio_numerator",
712 "must be >= temporal.p95_shift_ratio_denominator".into(),
713 );
714 }
715 if self
716 .downstream
717 .blocking_correlated_stage_patterns
718 .is_empty()
719 {
720 return invalid(
721 "downstream.blocking_correlated_stage_patterns",
722 "must not be empty".into(),
723 );
724 }
725 if self
726 .downstream
727 .blocking_correlated_stage_patterns
728 .iter()
729 .any(|p| p.trim().is_empty())
730 {
731 return invalid(
732 "downstream.blocking_correlated_stage_patterns",
733 "entries must be non-empty after trim".into(),
734 );
735 }
736 Ok(())
737 }
738}
739
740#[derive(Debug, Clone, PartialEq, Eq)]
742pub enum AnalyzeConfigError {
743 InvalidOverrideSyntax {
745 raw: String,
747 },
748 UnknownOverridePath {
750 path: String,
752 suggestion: Option<&'static str>,
754 },
755 InvalidOverrideValue {
757 path: &'static str,
759 value: String,
761 expected: &'static str,
763 },
764 InvalidConfigValue {
766 path: &'static str,
768 message: String,
770 },
771 MissingAnalyzerTable,
773 MissingSchemaVersion,
775 UnsupportedSchemaVersion {
777 found: u64,
779 supported: u64,
781 },
782 InvalidToml {
784 message: String,
786 },
787}
788impl Display for AnalyzeConfigError {
789 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
790 match self {
791 Self::InvalidOverrideSyntax { raw } => write!(f, "invalid override syntax: {raw}"),
792 Self::UnknownOverridePath { path, suggestion } => {
793 if let Some(s) = suggestion {
794 write!(f, "unknown analyzer option '{path}'; did you mean '{s}'?")
795 } else {
796 write!(f, "unknown analyzer option '{path}'")
797 }
798 }
799 Self::InvalidOverrideValue {
800 path,
801 value,
802 expected,
803 } => write!(
804 f,
805 "invalid override value for '{path}': '{value}' (expected {expected})"
806 ),
807 Self::InvalidConfigValue { path, message } => {
808 write!(f, "invalid config value at '{path}': {message}")
809 }
810 Self::MissingAnalyzerTable => write!(f, "missing [analyzer] table"),
811 Self::MissingSchemaVersion => write!(f, "missing schema_version"),
812 Self::UnsupportedSchemaVersion { found, supported } => write!(
813 f,
814 "unsupported schema_version {found}; supported {supported}"
815 ),
816 Self::InvalidToml { message } => write!(f, "invalid toml: {message}"),
817 }
818 }
819}
820impl Error for AnalyzeConfigError {}
821
822#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
823pub struct AnalyzeOptionDescriptor {
825 pub path: &'static str,
827 pub default_value: &'static str,
829 pub value_type: &'static str,
831 pub affects: &'static str,
833 pub description: &'static str,
835 pub increasing: Option<&'static str>,
837 pub decreasing: Option<&'static str>,
839}
840impl AnalyzeOptionDescriptor {
841 #[must_use]
843 pub const fn new(
844 path: &'static str,
845 default_value: &'static str,
846 value_type: &'static str,
847 affects: &'static str,
848 description: &'static str,
849 increasing: Option<&'static str>,
850 decreasing: Option<&'static str>,
851 ) -> Self {
852 Self {
853 path,
854 default_value,
855 value_type,
856 affects,
857 description,
858 increasing,
859 decreasing,
860 }
861 }
862}