Skip to content

feat: add optional CSV export for findings and warnings#24

Closed
stacknil wants to merge 11 commits intomainfrom
codex/feat/csv-export-v0.3
Closed

feat: add optional CSV export for findings and warnings#24
stacknil wants to merge 11 commits intomainfrom
codex/feat/csv-export-v0.3

Conversation

@stacknil
Copy link
Copy Markdown
Owner

Closes #23

Summary

  • add an explicit --csv CLI option for optional CSV export
  • emit findings.csv and warnings.csv alongside the existing reports only when CSV is explicitly requested
  • add single-host and multi-host CSV golden regression coverage while keeping existing Markdown and JSON defaults unchanged

Scope

This PR is limited to report and output-format behavior.
It does not change parser behavior, detector thresholds, enrichment, cross-host correlation, or existing Markdown/JSON report semantics.
This is a report-only change; no detector thresholds changed.

What changed

  • added --csv as an explicit CLI flag
  • added stable CSV writers for findings and warnings
  • kept the CSV schema intentionally small and reviewable
  • added checked-in golden findings.csv and warnings.csv fixtures for one single-host and one multi-host sanitized case
  • updated CLI and report-contract regression tests to cover both default no-CSV behavior and explicit CSV output
  • updated README and CHANGELOG briefly to document the new optional export

CSV schema

findings.csv

  • rule
  • subject_kind
  • subject
  • event_count
  • window_start
  • window_end
  • usernames
  • summary

warnings.csv

  • kind
  • message

Verification

  • cmake --preset dev-debug
  • cmake --build --preset dev-debug
  • ctest --preset dev-debug
  • cmake --preset ci-release
  • cmake --build --preset ci-release
  • ctest --preset ci-release

Deferred

  • host-summary CSV export
  • additional CSV fields beyond the minimal findings and warnings contracts
  • any parser-family, detector-policy, enrichment, or cross-host-correlation changes

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1499c4b8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


return 0;
}
#include "config.hpp" #include "detector.hpp" #include "parser.hpp" #include "report.hpp" #include <charconv> #include <filesystem> #include <iostream> #include <optional> #include <stdexcept> #include <string_view> namespace { struct CliOptions { std::optional<std::filesystem::path> config_path; std::optional<loglens::InputMode> input_mode; std::optional<int> assumed_year; bool emit_csv = false; std::filesystem::path input_path; std::filesystem::path output_directory; }; void print_usage() { std::cerr << "Usage: loglens [--config <config.json>] [--mode <syslog|journalctl-short-full>] [--year <YYYY>] [--csv] <input_log> [output_dir]\n"; } int parse_year_argument(std::string_view value) { int parsed_year = 0; const auto* begin = value.data(); const auto* end = value.data() + value.size(); const auto result = std::from_chars(begin, end, parsed_year); if (result.ec != std::errc{} || result.ptr != end || parsed_year <= 0) { throw std::runtime_error("invalid year value: " + std::string(value)); } return parsed_year; } CliOptions parse_cli_options(int argc, char* argv[]) { if (argc < 2) { throw std::runtime_error("missing required arguments"); } int index = 1; CliOptions options; while (index < argc) { const std::string_view argument = argv[index]; if (argument == "--config") { if (index + 1 >= argc) { throw std::runtime_error("missing path after --config"); } options.config_path = std::filesystem::path{argv[index + 1]}; index += 2; continue; } if (argument == "--mode") { if (index + 1 >= argc) { throw std::runtime_error("missing value after --mode"); } const auto parsed_mode = loglens::parse_input_mode(argv[index + 1]); if (!parsed_mode.has_value()) { throw std::runtime_error("unsupported mode: " + std::string{argv[index + 1]}); } options.input_mode = *parsed_mode; index += 2; continue; } if (argument == "--year") { if (index + 1 >= argc) { throw std::runtime_error("missing value after --year"); } options.assumed_year = parse_year_argument(argv[index + 1]); index += 2; continue; } if (argument == "--csv") { options.emit_csv = true; ++index; continue; } if (argument.starts_with('-')) { throw std::runtime_error("unknown option: " + std::string{argv[index]}); } break; } const int remaining = argc - index; if (remaining < 1 || remaining > 2) { throw std::runtime_error("invalid argument count"); } options.input_path = std::filesystem::path{argv[index]}; options.output_directory = remaining == 2 ? std::filesystem::path{argv[index + 1]} : std::filesystem::current_path(); return options; } loglens::ParserConfig resolve_parser_config(const CliOptions& options, const loglens::AppConfig& config) { const auto resolved_mode = options.input_mode.has_value() ? options.input_mode : config.input_mode; if (!resolved_mode.has_value()) { throw std::runtime_error("input mode is required; use --mode or input_mode in config.json"); } loglens::ParserConfig parser_config; parser_config.input_mode = *resolved_mode; if (parser_config.input_mode == loglens::InputMode::SyslogLegacy) { parser_config.assumed_year = options.assumed_year.has_value() ? options.assumed_year : config.timestamp.assume_year; if (!parser_config.assumed_year.has_value()) { throw std::runtime_error("syslog mode requires --year or timestamp.assume_year in config.json"); } } return parser_config; } } // namespace int main(int argc, char* argv[]) { CliOptions options; try { options = parse_cli_options(argc, argv); } catch (const std::exception& error) { print_usage(); std::cerr << "LogLens failed: " << error.what() << '\n'; return 1; } try { const auto app_config = options.config_path.has_value() ? loglens::load_app_config(*options.config_path) : loglens::AppConfig{}; const auto parser_config = resolve_parser_config(options, app_config); const loglens::AuthLogParser parser(parser_config); const auto parsed = parser.parse_file(options.input_path); const loglens::Detector detector(app_config.detector); const auto findings = detector.analyze(parsed.events); const loglens::ReportData report_data{ options.input_path, parsed.metadata, parsed.quality, parsed.events, findings, parsed.warnings, app_config.detector.auth_signal_mappings}; loglens::write_reports(report_data, options.output_directory, options.emit_csv); std::cout << "Parsed events: " << parsed.events.size() << '\n'; std::cout << "Findings: " << findings.size() << '\n'; std::cout << "Warnings: " << parsed.warnings.size() << '\n'; std::cout << "Markdown report: " << (options.output_directory / "report.md").string() << '\n'; std::cout << "JSON report: " << (options.output_directory / "report.json").string() << '\n'; if (options.emit_csv) { std::cout << "Findings CSV: " << (options.output_directory / "findings.csv").string() << '\n'; std::cout << "Warnings CSV: " << (options.output_directory / "warnings.csv").string() << '\n'; } } catch (const std::exception& error) { std::cerr << "LogLens failed: " << error.what() << '\n'; return 1; } return 0; } No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Preserve line breaks in C++ translation units

This commit rewrites src/main.cpp as a single physical line, which causes the preprocessor to treat nearly the whole file as a malformed #include directive instead of compiling the program body. In a detached checkout of 375fba0, cmake --build fails when linking loglens with undefined reference to main; the same line-flattening pattern also appears in src/report.cpp, src/report.hpp, and the modified tests. As committed, the project is not buildable.

Useful? React with 👍 / 👎.

@stacknil
Copy link
Copy Markdown
Owner Author

Closing this PR because the branch content was published with corrupted newlines. Replacing it with a fresh branch rebuilt from latest main.

@stacknil stacknil closed this Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v0.3] Add optional CSV export for findings and warnings

1 participant