diff --git a/packages/check-sql/src/ms_sql/api.rs b/packages/check-sql/src/ms_sql/api.rs index 5e15520c47a..811c9fb4031 100644 --- a/packages/check-sql/src/ms_sql/api.rs +++ b/packages/check-sql/src/ms_sql/api.rs @@ -27,6 +27,7 @@ const COUNTERS_SECTION_NAME: &str = "counters"; const BLOCKED_SESSIONS_SECTION_NAME: &str = "blocked_sessions"; const TABLE_SPACES_SECTION_NAME: &str = "tablespaces"; const BACKUP_SECTION_NAME: &str = "backup"; +const TRANSACTION_LOG_SECTION_NAME: &str = "transactionlogs"; pub enum Credentials<'a> { SqlServer { user: &'a str, password: &'a str }, @@ -59,6 +60,7 @@ impl Section { pub trait Column { fn get_bigint_by_idx(&self, idx: usize) -> i64; + fn get_bigint_by_name(&self, idx: &str) -> i64; fn get_value_by_idx(&self, idx: usize) -> String; fn get_optional_value_by_idx(&self, idx: usize) -> Option; fn get_value_by_name(&self, idx: &str) -> String; @@ -114,7 +116,8 @@ impl InstanceEngine { COUNTERS_SECTION_NAME | BLOCKED_SESSIONS_SECTION_NAME | TABLE_SPACES_SECTION_NAME - | BACKUP_SECTION_NAME => { + | BACKUP_SECTION_NAME + | TRANSACTION_LOG_SECTION_NAME => { result += &self .generate_known_sections(&mut client, endpoint, §ion.name) .await; @@ -206,6 +209,10 @@ impl InstanceEngine { .await } BACKUP_SECTION_NAME => self.generate_backup_section(client, &databases, sep).await, + TRANSACTION_LOG_SECTION_NAME => { + self.generate_transaction_logs_section(endpoint, &databases, sep) + .await + } _ => format!("{} not implemented\n", name).to_string(), } } @@ -312,6 +319,38 @@ impl InstanceEngine { } } + pub async fn generate_transaction_logs_section( + &self, + endpoint: &config::ms_sql::Endpoint, + databases: &Vec, + sep: char, + ) -> String { + let format_error = |d: &str, e: &anyhow::Error| { + format!( + "{}{sep}{}|-|-|-|-|-|-|{:?}\n", + self.mssql_name(), + d.replace(' ', "_"), + e + ) + .to_string() + }; + let mut result = String::new(); + for d in databases { + match self.create_client(endpoint, Some(d.clone())).await { + Ok(mut c) => { + result += &run_query(&mut c, queries::QUERY_TRANSACTION_LOGS) + .await + .map(|rows| to_transaction_logs_entries(&self.mssql_name(), d, &rows, sep)) + .unwrap_or_else(|e| format_error(d, &e)); + } + Err(err) => { + result += &format_error(d, &err); + } + } + } + result + } + /// doesn't return error - the same behavior as plugin pub async fn generate_databases(&self, client: &mut Client) -> Vec { let result = run_query(client, queries::QUERY_DATABASES) @@ -474,6 +513,47 @@ fn to_table_spaces_entry( ) } +fn to_transaction_logs_entries( + instance_name: &str, + database_name: &str, + rows: &[Vec], + sep: char, +) -> String { + if rows.is_empty() { + return String::new(); + } + rows[0] + .iter() + .map(|row| to_transaction_logs_entry(row, instance_name, database_name, sep)) + .collect::>() + .join("") +} + +fn to_transaction_logs_entry( + row: &Row, + instance_name: &str, + database_name: &str, + sep: char, +) -> String { + let name = row.get_value_by_name("name"); + let physical_name = row.get_value_by_name("physical_name"); + let max_size = row.get_bigint_by_name("MaxSize"); + let allocated_size = row.get_bigint_by_name("AllocatedSize"); + let used_size = row.get_bigint_by_name("UsedSize"); + let unlimited = row.get_bigint_by_name("Unlimited"); + format!( + "{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}\n", + instance_name, + database_name.replace(' ', "_"), + name, + physical_name, + max_size, + allocated_size, + used_size, + unlimited + ) +} + fn to_backup_entry( instance_name: &str, database_name: &str, @@ -550,6 +630,12 @@ impl Column for Row { .unwrap_or_default() } + fn get_bigint_by_name(&self, idx: &str) -> i64 { + self.try_get::(idx) + .unwrap_or_default() + .unwrap_or_default() + } + fn get_value_by_idx(&self, idx: usize) -> String { self.try_get::<&str, usize>(idx) .unwrap_or_default() @@ -654,7 +740,7 @@ async fn generate_result( // place all futures now in vector for future asynchronous processing let tasks = instances .iter() - .map(move |instance| instance.generate_sections(ms_sql, sections.clone())); + .map(move |instance| instance.generate_sections(ms_sql, sections)); // processing here let results = stream::iter(tasks) diff --git a/packages/check-sql/src/ms_sql/queries.rs b/packages/check-sql/src/ms_sql/queries.rs index 1f7cecefcca..ee4fe4e93aa 100644 --- a/packages/check-sql/src/ms_sql/queries.rs +++ b/packages/check-sql/src/ms_sql/queries.rs @@ -153,6 +153,13 @@ END EXEC (@SQLCommand) "; +pub const QUERY_TRANSACTION_LOGS: &str = "SELECT name, physical_name,\ + cast(max_size/128 as bigint) as MaxSize,\ + cast(size/128 as bigint) as AllocatedSize,\ + cast(FILEPROPERTY (name, 'spaceused')/128 as bigint) as UsedSize,\ + case when max_size = '-1' then '1' else '0' end as Unlimited \ + FROM sys.database_files WHERE type_desc = 'LOG'"; + pub fn get_instances_query() -> String { QUERY_ALL_BASE.to_string() } diff --git a/packages/check-sql/tests/test_ms_sql.rs b/packages/check-sql/tests/test_ms_sql.rs index 64b24a53e38..fda668e34ab 100644 --- a/packages/check-sql/tests/test_ms_sql.rs +++ b/packages/check-sql/tests/test_ms_sql.rs @@ -153,6 +153,7 @@ async fn test_validate_all_instances_remote() { .contains(" error: "),); validate_table_spaces(&i, &mut c, &cfg.endpoint()).await; validate_backup(&i, &mut c).await; + validate_transaction_logs(&i, &mut c, &cfg.endpoint()).await; } Err(e) => { panic!("Unexpected error: `{:?}`", e); @@ -271,6 +272,40 @@ async fn validate_backup(instance: &InstanceEngine, client: &mut Client) { assert!(to_be_found.is_empty()); } +async fn validate_transaction_logs( + instance: &InstanceEngine, + client: &mut Client, + endpoint: &Endpoint, +) { + let databases = instance.generate_databases(client).await; + let expected: HashSet = ["master", "tempdb", "model", "msdb"] + .iter() + .map(|&s| s.to_string()) + .collect(); + let mut found: HashSet = HashSet::new(); + + let result = instance + .generate_transaction_logs_section(endpoint, &databases, '|') + .await; + let lines: Vec<&str> = result.split('\n').collect(); + assert!(lines.len() >= expected.len(), "{:?}", lines); + assert!(lines[lines.len() - 1].is_empty()); + for l in lines[..lines.len() - 1].iter() { + let values = l.split('|').collect::>(); + assert_eq!(values[0], instance.mssql_name(), "bad line: {}", l); + if expected.contains(&values[1].to_string()) { + found.insert(values[1].to_string()); + } + assert!(values[2].to_lowercase().ends_with("log"), "bad line: {}", l); + assert!(values[3].starts_with("C:\\Program"), "bad line: {}", l); + assert!(values[4].parse::().is_ok(), "bad line: {}", l); + assert!(values[5].parse::().is_ok(), "bad line: {}", l); + assert!(values[6].parse::().is_ok(), "bad line: {}", l); + assert!(values[7].parse::().is_ok(), "bad line: {}", l); + } + assert_eq!(found, expected); +} + /// This test is ignored because it requires real credentials and real server /// Intended to be used manually by dev to check whether all instances are accessible. /// TODO(sk): remove on branching