Skip to content

Commit a9c77d4

Browse files
committed
EX-10318: add metrics for osc.*.state.current_state
Add metrics for osc.*.state.current_state For lustrefs-exporter, if current_state is FULL or IDLE, the metrics value is 1. Or the metrics value is 0 Signed-off-by: Feng Lei <flei@ddn.com>
1 parent 80d4f80 commit a9c77d4

12 files changed

Lines changed: 402 additions & 8 deletions

File tree

lustre-collector/src/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,13 @@ impl From<combine::stream::easy::Errors<char, &str, usize>> for LustreCollectorE
3131
LustreCollectorError::CombineEasyError(err.map_range(|_| ""))
3232
}
3333
}
34+
35+
impl<'a> From<combine::stream::easy::Errors<char, &'a str, combine::stream::PointerOffset<str>>>
36+
for LustreCollectorError
37+
{
38+
fn from(
39+
err: combine::stream::easy::Errors<char, &'a str, combine::stream::PointerOffset<str>>,
40+
) -> Self {
41+
LustreCollectorError::CombineEasyError(err.map_range(|_| "").map_position(|_| 0))
42+
}
43+
}

lustre-collector/src/lib.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,76 @@ mod top_level_parser;
2626
pub mod types;
2727

2828
pub use crate::error::LustreCollectorError;
29-
use combine::parser::EasyParser;
29+
use combine::{EasyParser, Parser, parser::token::satisfy};
3030
pub use lnetctl_parser::{parse as parse_lnetctl_output, parse_lnetctl_stats};
3131
pub use node_stats_parsers::{parse_cpustats_output, parse_meminfo_output};
3232
use std::{io, str};
3333
pub use types::*;
3434

35+
/// Normalize lctl output to valid YAML format
36+
/// - If a line ends with '=', replace '=' with ':'
37+
/// - For all other non-empty lines, prepend a space
38+
fn normalize_lctl_output<Input>() -> impl Parser<Input, Output = String>
39+
where
40+
Input: combine::Stream<Token = char>,
41+
Input::Error: combine::ParseError<Input::Token, Input::Range, Input::Position>,
42+
{
43+
use combine::{
44+
attempt, choice, many, many1,
45+
parser::char::{char, newline},
46+
};
47+
48+
let normalize_line = choice((
49+
// Line ending with '=' -> replace with ':'
50+
attempt(
51+
(many1(satisfy(|c| c != '\n' && c != '=')), char('='))
52+
.map(|(s, _): (String, _)| format!("{}:", s)),
53+
),
54+
// Other non-empty line -> prepend a space
55+
many1(satisfy(|c| c != '\n')).map(|s: String| format!(" {}", s)),
56+
));
57+
58+
many(normalize_line.skip(newline())).map(|lines: Vec<String>| lines.join("\n"))
59+
}
60+
61+
pub fn parse_osc_state_output(
62+
osc_state_output: &[u8],
63+
) -> Result<Vec<Record>, LustreCollectorError> {
64+
let osc_state_str = str::from_utf8(osc_state_output)?;
65+
66+
// Preprocess the output using combine parser to convert lctl format to valid YAML:
67+
// - If a line ends with '=', replace '=' with ':'
68+
// - For non-empty lines that don't end with '=', add a space at the beginning
69+
let (processed_str, _) = normalize_lctl_output()
70+
.easy_parse(osc_state_str)
71+
.map_err(LustreCollectorError::from)?;
72+
73+
let osc_states: OscStates = serde_yaml::from_str(&processed_str)?;
74+
75+
// Convert each OSC state to a ControllerStats record
76+
let records: Vec<Record> = osc_states
77+
.into_iter()
78+
.map(|(key, value)| {
79+
// Remove "osc." prefix and ".state" suffix from keys
80+
let cleaned_key = key
81+
.strip_prefix("osc.")
82+
.unwrap_or(&key)
83+
.strip_suffix(".state")
84+
.unwrap_or(&key)
85+
.to_string();
86+
87+
Record::Controller(ControllerStats::OscState(ControllerStat {
88+
kind: ControllerVariant::Osc,
89+
param: Param("state".to_string()),
90+
controller: Controller(cleaned_key),
91+
value,
92+
}))
93+
})
94+
.collect();
95+
96+
Ok(records)
97+
}
98+
3599
fn check_output(
36100
records: Vec<Record>,
37101
state: &str,

lustre-collector/src/main.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
use clap::{Arg, ValueEnum, value_parser};
66
use lustre_collector::{
77
error::LustreCollectorError, mgs::mgs_fs_parser, parse_lctl_output, parse_lnetctl_output,
8-
parse_lnetctl_stats, parse_mgs_fs_output, parse_recovery_status_output, parser,
9-
recovery_status_parser, types::Record,
8+
parse_lnetctl_stats, parse_mgs_fs_output, parse_osc_state_output, parse_recovery_status_output,
9+
parser, recovery_status_parser, types::Record,
1010
};
1111
use std::{
1212
fmt, panic,
@@ -81,6 +81,15 @@ fn get_lnetctl_stats_output() -> Result<Vec<u8>, LustreCollectorError> {
8181
Ok(r.stdout)
8282
}
8383

84+
fn get_osc_state_output() -> Result<Vec<u8>, LustreCollectorError> {
85+
let r = Command::new("lctl")
86+
.arg("get_param")
87+
.arg("osc.*.state")
88+
.output()?;
89+
90+
Ok(r.stdout)
91+
}
92+
8493
fn main() -> ExitCode {
8594
match run() {
8695
Ok(()) => ExitCode::SUCCESS,
@@ -144,6 +153,13 @@ fn run() -> Result<(), LustreCollectorError> {
144153
Ok(recovery_statuses)
145154
});
146155

156+
let osc_state_handle = thread::spawn(move || -> Result<Vec<Record>, LustreCollectorError> {
157+
let osc_state_output = get_osc_state_output()?;
158+
let osc_states = parse_osc_state_output(&osc_state_output)?;
159+
160+
Ok(osc_states)
161+
});
162+
147163
let lnetctl_net_show_output = Command::new("lnetctl")
148164
.args(["net", "show", "-v", "4"])
149165
.output()
@@ -172,10 +188,16 @@ fn run() -> Result<(), LustreCollectorError> {
172188
Err(e) => panic::resume_unwind(e),
173189
};
174190

191+
let mut osc_state_records = match osc_state_handle.join() {
192+
Ok(r) => r.unwrap_or_default(),
193+
Err(e) => panic::resume_unwind(e),
194+
};
195+
175196
lctl_record.append(&mut lnet_record);
176197
lctl_record.append(&mut mgs_fs_record);
177198
lctl_record.append(&mut recovery_status_records);
178199
lctl_record.append(&mut lnetctl_stats_record);
200+
lctl_record.append(&mut osc_state_records);
179201

180202
let x = match format {
181203
Format::Json => serde_json::to_string(&lctl_record)?,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
source: lustre-collector/src/types.rs
3+
expression: records
4+
---
5+
[
6+
Controller(
7+
OscState(
8+
ControllerStat {
9+
kind: Osc,
10+
param: Param(
11+
"state",
12+
),
13+
controller: Controller(
14+
"fs-OST0000-osc-MDT0000",
15+
),
16+
value: OscState {
17+
current_state: "FULL",
18+
},
19+
},
20+
),
21+
),
22+
Controller(
23+
OscState(
24+
ControllerStat {
25+
kind: Osc,
26+
param: Param(
27+
"state",
28+
),
29+
controller: Controller(
30+
"fs-OST0000-osc-ffff8d639e4f0800",
31+
),
32+
value: OscState {
33+
current_state: "CONNECTING",
34+
},
35+
},
36+
),
37+
),
38+
Controller(
39+
OscState(
40+
ControllerStat {
41+
kind: Osc,
42+
param: Param(
43+
"state",
44+
),
45+
controller: Controller(
46+
"fs-OST0001-osc-MDT0000",
47+
),
48+
value: OscState {
49+
current_state: "IDLE",
50+
},
51+
},
52+
),
53+
),
54+
]

lustre-collector/src/types.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ impl Deref for Target {
2929
}
3030
}
3131

32+
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
33+
/// The Lustre controller corresponding to these stats (e.g., OSC, MDC).
34+
pub struct Controller(pub String);
35+
36+
impl Deref for Controller {
37+
type Target = str;
38+
39+
fn deref(&self) -> &Self::Target {
40+
&self.0
41+
}
42+
}
43+
3244
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
3345
/// The name of the stat.
3446
pub struct Param(pub String);
@@ -324,6 +336,29 @@ impl Deref for TargetVariant {
324336
}
325337
}
326338

339+
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize, Clone, Copy)]
340+
pub enum ControllerVariant {
341+
Osc,
342+
}
343+
344+
impl fmt::Display for ControllerVariant {
345+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
346+
match *self {
347+
ControllerVariant::Osc => write!(f, "OSC"),
348+
}
349+
}
350+
}
351+
352+
impl Deref for ControllerVariant {
353+
type Target = str;
354+
355+
fn deref(&self) -> &Self::Target {
356+
match *self {
357+
ControllerVariant::Osc => "OSC",
358+
}
359+
}
360+
}
361+
327362
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
328363
/// Stats specific to a target.
329364
pub struct TargetStat<T> {
@@ -333,6 +368,15 @@ pub struct TargetStat<T> {
333368
pub value: T,
334369
}
335370

371+
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
372+
/// Stats specific to a controller (e.g., OSC, MDC).
373+
pub struct ControllerStat<T> {
374+
pub kind: ControllerVariant,
375+
pub param: Param,
376+
pub controller: Controller,
377+
pub value: T,
378+
}
379+
336380
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
337381
/// Stats from parsing `ost.OSS.<PARAM>.stats`
338382
pub struct OssStat {
@@ -532,6 +576,12 @@ pub enum TargetStats {
532576
QuotaStatsOsd(TargetStat<QuotaStatsOsd>),
533577
}
534578

579+
/// The controller stats currently collected
580+
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
581+
pub enum ControllerStats {
582+
OscState(ControllerStat<OscState>),
583+
}
584+
535585
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
536586
pub enum LNetStats {
537587
SendCount(LNetStat<i64>),
@@ -555,6 +605,7 @@ pub enum Record {
555605
LustreService(LustreServiceStats),
556606
Node(NodeStats),
557607
Target(TargetStats),
608+
Controller(ControllerStats),
558609
}
559610

560611
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
@@ -602,9 +653,17 @@ pub enum QuotaKind {
602653
Prj,
603654
}
604655

656+
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
657+
pub struct OscState {
658+
pub current_state: String,
659+
}
660+
661+
pub type OscStates = std::collections::HashMap<String, OscState>;
662+
605663
#[cfg(test)]
606664
mod tests {
607665
use super::*;
666+
use insta::assert_debug_snapshot;
608667
use std::convert::TryInto;
609668

610669
#[test]
@@ -633,4 +692,79 @@ mod tests {
633692
Err(e) => panic!("Error occurred: {:?}", e),
634693
}
635694
}
695+
696+
#[test]
697+
fn test_osc_states_parse() {
698+
// Test data matching fixtures/osc_states.json
699+
// state_history is present but will be ignored during parsing
700+
let yaml_data = r#"
701+
osc.fs-OST0000-osc-MDT0000.state:
702+
current_state: FULL
703+
state_history:
704+
- [1775627216, CONNECTING]
705+
- [1775627216, FULL]
706+
osc.fs-OST0000-osc-ffff8d639e4f0800.state:
707+
current_state: CONNECTING
708+
state_history:
709+
- [1775627490, CONNECTING]
710+
osc.fs-OST0001-osc-MDT0000.state:
711+
current_state: IDLE
712+
state_history:
713+
- [1775627600, CONNECTING]
714+
- [1775627600, IDLE]
715+
"#;
716+
717+
let osc_states: OscStates = serde_yaml::from_str(yaml_data).unwrap();
718+
719+
// Verify we have 3 entries
720+
assert_eq!(osc_states.len(), 3);
721+
722+
// Verify original keys (before cleaning)
723+
let state0 = osc_states.get("osc.fs-OST0000-osc-MDT0000.state").unwrap();
724+
assert_eq!(state0.current_state, "FULL");
725+
726+
let state1 = osc_states
727+
.get("osc.fs-OST0000-osc-ffff8d639e4f0800.state")
728+
.unwrap();
729+
assert_eq!(state1.current_state, "CONNECTING");
730+
731+
let state2 = osc_states.get("osc.fs-OST0001-osc-MDT0000.state").unwrap();
732+
assert_eq!(state2.current_state, "IDLE");
733+
}
734+
735+
#[test]
736+
fn test_osc_states_parse_with_cleaned_keys() {
737+
use crate::parse_osc_state_output;
738+
739+
// Test data matching fixtures/osc_states.json in raw lctl format (before sed transformation)
740+
let raw_lctl_output = r#"osc.fs-OST0000-osc-MDT0000.state=
741+
current_state: FULL
742+
state_history:
743+
- [1775627216, CONNECTING]
744+
- [1775627216, FULL]
745+
osc.fs-OST0000-osc-ffff8d639e4f0800.state=
746+
current_state: CONNECTING
747+
state_history:
748+
- [1775627490, CONNECTING]
749+
osc.fs-OST0001-osc-MDT0000.state=
750+
current_state: IDLE
751+
state_history:
752+
- [1775627600, CONNECTING]
753+
- [1775627600, IDLE]
754+
"#;
755+
756+
let mut records = parse_osc_state_output(raw_lctl_output.as_bytes()).unwrap();
757+
758+
// Sort records by controller name to ensure consistent ordering for snapshot testing
759+
records.sort_by(|a, b| match (a, b) {
760+
(
761+
Record::Controller(ControllerStats::OscState(a_stat)),
762+
Record::Controller(ControllerStats::OscState(b_stat)),
763+
) => a_stat.controller.0.cmp(&b_stat.controller.0),
764+
_ => std::cmp::Ordering::Equal,
765+
});
766+
767+
// Use snapshot testing to verify the complete output structure
768+
assert_debug_snapshot!(records);
769+
}
636770
}

0 commit comments

Comments
 (0)