dm-database-driver-log 0.1.1

A pluggable streaming parser for DM database driver logs
Documentation
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);

fn temp_path(name: &str) -> PathBuf {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let sequence = NEXT_TEMP.fetch_add(1, Ordering::Relaxed);
    std::env::temp_dir().join(format!(
        "dm-driver-log-integration-{name}-{nonce}-{}-{sequence}.log",
        std::process::id()
    ))
}

struct FutureDriverEvent {
    method: String,
    used_time_ms: Option<f64>,
    exec_id: Option<i64>,
    category: &'static str,
}

impl dm_database_driver_log::advanced::LogRecord for FutureDriverEvent {
    fn method(&self) -> &str {
        &self.method
    }

    fn category(&self) -> &str {
        self.category
    }

    fn used_time_ms(&self) -> Option<f64> {
        self.used_time_ms
    }

    fn exec_id(&self) -> Option<i64> {
        self.exec_id
    }
}

struct FutureDriverFormat;

impl dm_database_driver_log::advanced::LogFormat for FutureDriverFormat {
    type Event = FutureDriverEvent;

    const FRAMING: dm_database_driver_log::advanced::RecordFraming =
        dm_database_driver_log::advanced::RecordFraming::Line;

    fn is_record_start(_line: &str) -> bool {
        true
    }

    fn parse_record(
        record: &str,
        _line_number: u64,
    ) -> Result<Self::Event, dm_database_driver_log::ParseError> {
        let (method, used_time_ms, exec_id) = match record {
            "SELECT" => ("SELECT", Some(3.0), Some(100)),
            "UPDATE" => ("UPDATE", Some(0.5), Some(101)),
            _ => (record, None, None),
        };
        Ok(FutureDriverEvent {
            method: method.to_owned(),
            used_time_ms,
            exec_id,
            category: "future-driver",
        })
    }
}

#[test]
fn generic_engine_accepts_a_future_driver_adapter() {
    let path = temp_path("future-driver");
    std::fs::write(&path, "SELECT\nUPDATE\nIGNORED\n").unwrap();

    let parser =
        dm_database_driver_log::advanced::LogParserBuilder::<FutureDriverFormat>::new(&path)
            .build()
            .unwrap();
    assert_eq!(parser.path(), path.as_path());
    let selected: Vec<_> = parser
        .iter()
        .unwrap()
        .filter_by_method("SELECT")
        .collect::<Result<_, _>>()
        .unwrap();
    assert_eq!(selected.len(), 1);
    assert_eq!(selected[0].exec_id, Some(100));

    let slow: Vec<_> = parser
        .iter()
        .unwrap()
        .filter_by_used_time(1.0)
        .filter_map(Result::ok)
        .filter(|event| event.category == "future-driver")
        .collect();
    assert_eq!(slow.len(), 1);
    assert_eq!(slow[0].method, "SELECT");

    let categories: Vec<_> = parser
        .iter()
        .unwrap()
        .filter_by_category("future-driver")
        .collect::<Result<_, _>>()
        .unwrap();
    assert_eq!(categories.len(), 3);

    let executions: Vec<_> = parser
        .iter()
        .unwrap()
        .filter_by_exec_id(101)
        .collect::<Result<_, _>>()
        .unwrap();
    assert_eq!(executions.len(), 1);
    assert_eq!(parser.iter().unwrap().skip_errors().count(), 3);

    std::fs::remove_file(path).unwrap();
}

#[cfg(feature = "jdbc")]
mod jdbc {
    use super::temp_path;
    use dm_database_driver_log::{JdbcEvent, LogEvent, LogParserBuilder};

    fn jdbc(event: &LogEvent) -> &JdbcEvent {
        event.as_jdbc().expect("expected JDBC event")
    }

    const EXEC_LINE: &str = "[INFO  - 2026-09-16 17:45:19.763] tid:119 - [worker] { conn-3, pstmt-854 } executeQuery(): rs-2216; [USED TIME]: 8.5ms; [EXEC_ID]: 19010657;";
    const BIND_LINE: &str = "[INFO  - 2026-09-16 17:45:19.764] tid:119 - [worker] { conn-3, pstmt-854 } setString(Integer, String); [PARAMS]: 1, \"A06093910597367000594686\"; [USED TIME]: 0.5ms;";
    const GET_LINE: &str = "[INFO  - 2026-09-16 17:45:19.765] tid:119 - [worker] { conn-3, pstmt-854, rs-2216 } getString(String): \"value\"; [PARAMS]: \"name\"; [USED TIME]: 2.5ms;";

    #[test]
    fn runs_jdbc_file_from_input_to_filtered_results() {
        let path = temp_path("jdbc");
        std::fs::write(
            &path,
            format!("\r\n{EXEC_LINE}\r\n{BIND_LINE}\r\n{GET_LINE}\r\nnot a jdbc record"),
        )
        .unwrap();

        let parser = LogParserBuilder::new(&path).build().unwrap();
        assert_eq!(parser.format(), dm_database_driver_log::LogFormatKind::Jdbc);
        let results: Vec<_> = parser.iter().unwrap().collect();
        assert_eq!(results.len(), 4);
        assert_eq!(results[0].as_ref().unwrap().line_number(), 2);
        assert_eq!(results[1].as_ref().unwrap().line_number(), 3);
        assert_eq!(results[2].as_ref().unwrap().line_number(), 4);
        assert_eq!(results[3].as_ref().unwrap_err().line_number(), Some(5));

        let execute: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_method("executeQuery")
            .filter_map(Result::ok)
            .collect();
        assert_eq!(execute.len(), 1);
        assert_eq!(jdbc(&execute[0]).result_set_id, Some(2216));
        assert_eq!(execute[0].exec_id(), Some(19010657));

        let binds: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_category("bind")
            .filter_map(Result::ok)
            .collect();
        assert_eq!(binds.len(), 1);
        assert_eq!(jdbc(&binds[0]).param_index, Some(1));

        let slow: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_used_time(2.0)
            .filter_map(Result::ok)
            .collect();
        assert_eq!(slow.len(), 2);

        let executions: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_exec_id(19010657)
            .filter_map(Result::ok)
            .collect();
        assert_eq!(executions.len(), 1);

        let (count, total_ms) = parser.iter().unwrap().filter_map(Result::ok).fold(
            (0, 0.0),
            |(count, total_ms), event| {
                (
                    count + 1,
                    total_ms + event.used_time_ms().unwrap_or_default(),
                )
            },
        );
        assert_eq!(count, 3);
        assert_eq!(total_ms, 11.5);
        assert_eq!(parser.iter().unwrap().skip_errors().count(), 3);

        std::fs::remove_file(path).unwrap();
    }
}

#[cfg(feature = "dm-provider")]
mod dm_provider {
    use super::temp_path;
    use dm_database_driver_log::{DmProviderEvent, LogEvent, LogParserBuilder};

    fn provider(event: &LogEvent) -> &DmProviderEvent {
        event.as_dm_provider().expect("expected DM Provider event")
    }

    const ACCESS_LINE: &str = "[INFO  - 2026-09-12 08:37:44.696] tid:34 (IsBackground-True) { B@16900fb } access Cmd:4(); [USED TIME]: 0ns;";
    const SET_COMMAND_FIRST_LINE: &str = "[INFO  - 2026-09-12 08:55:30.191] tid:68 (IsBackground-True) { conn-2095 (sessId:281421579449976), command-4579 } setCommandText(String); [PARAMS]: \"SELECT * FROM T";
    const SET_COMMAND_SECOND_LINE: &str = "FROM T WHERE ID = 1\"; [USED TIME]: 0ns;";
    const EXECUTE_LINE: &str = "[SQL   - 2026-09-12 08:55:30.193] tid:68 (IsBackground-True) { conn-2095 (sessId:281421579449976), command-4579 } ExecuteDbDataReader(CommandBehavior) [RETURN]: dateReader-2971; [PARAMS]: Default; [SQL]: SELECT * FROM T\nFROM T WHERE ID = 1 [USED TIME]: 2ms; [EXEC_ID]: 908131301;";
    const READ_LINE: &str = "[INFO  - 2026-09-12 08:55:30.194] tid:68 (IsBackground-True) { conn-2095 (sessId:281421579449976), command-4579, dateReader-2971 } Read() [RETURN]: True; [USED TIME]: 500000ns;";
    const INVALID_LINE: &str =
        "[INFO  - 2026-09-12 08:55:30.195] tid:bad (IsBackground-True) { conn-2095 } Close();";

    #[test]
    fn runs_provider_file_from_multiline_input_to_filtered_results() {
        let path = temp_path("provider");
        let content = format!(
            "{ACCESS_LINE}\n{SET_COMMAND_FIRST_LINE}\n{SET_COMMAND_SECOND_LINE}\n{EXECUTE_LINE}\n{READ_LINE}\n{INVALID_LINE}"
        );
        std::fs::write(&path, content).unwrap();

        let parser = LogParserBuilder::new(&path).build().unwrap();
        assert_eq!(
            parser.format(),
            dm_database_driver_log::LogFormatKind::DmProvider
        );
        let results: Vec<_> = parser.iter().unwrap().collect();
        assert_eq!(results.len(), 5);

        let access = results[0].as_ref().unwrap();
        assert_eq!(access.line_number(), 1);
        assert_eq!(provider(access).object_id.as_deref(), Some("16900fb"));
        assert_eq!(provider(access).cmd, Some(4));

        let set_command = results[1].as_ref().unwrap();
        assert_eq!(set_command.line_number(), 2);
        assert!(set_command.raw().contains('\n'));
        assert_eq!(set_command.method(), "setCommandText");
        assert_eq!(provider(set_command).arg_types, "String");
        assert!(
            provider(set_command)
                .params
                .as_deref()
                .unwrap()
                .contains("FROM T")
        );

        let execute = results[2].as_ref().unwrap();
        assert_eq!(execute.line_number(), 4);
        assert_eq!(provider(execute).conn_id, Some(2095));
        assert_eq!(provider(execute).session_id, Some(281421579449976));
        assert_eq!(provider(execute).command_id, Some(4579));
        assert_eq!(provider(execute).result_set_id, Some(2971));
        assert_eq!(provider(execute).sql.as_deref().unwrap().lines().count(), 2);
        assert_eq!(execute.used_time_ms(), Some(2.0));
        assert_eq!(execute.exec_id(), Some(908131301));

        let read = results[3].as_ref().unwrap();
        assert_eq!(read.line_number(), 6);
        assert_eq!(read.category(), "fetch");
        assert_eq!(read.used_time_ms(), Some(0.5));
        assert_eq!(results[4].as_ref().unwrap_err().line_number(), Some(7));

        let sql_records: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_category("execute")
            .filter_map(Result::ok)
            .collect();
        assert_eq!(sql_records.len(), 1);
        assert_eq!(sql_records[0].exec_id(), Some(908131301));

        let access_records: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_method("access")
            .filter_map(Result::ok)
            .collect();
        assert_eq!(access_records.len(), 1);

        let slow: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_used_time(1.0)
            .filter_map(Result::ok)
            .collect();
        assert_eq!(slow.len(), 1);

        let executions: Vec<_> = parser
            .iter()
            .unwrap()
            .filter_by_exec_id(908131301)
            .filter_map(Result::ok)
            .collect();
        assert_eq!(executions.len(), 1);

        let (count, sql_count, total_ms) = parser.iter().unwrap().filter_map(Result::ok).fold(
            (0, 0, 0.0),
            |(count, sql_count, total_ms), event| {
                (
                    count + 1,
                    sql_count
                        + usize::from(
                            event
                                .as_dm_provider()
                                .is_some_and(|provider| provider.sql.is_some()),
                        ),
                    total_ms + event.used_time_ms().unwrap_or_default(),
                )
            },
        );
        assert_eq!(count, 4);
        assert_eq!(sql_count, 1);
        assert_eq!(total_ms, 2.5);
        assert_eq!(parser.iter().unwrap().skip_errors().count(), 4);

        std::fs::remove_file(path).unwrap();
    }
}