← All tasks
cppcodex/cpp-t1 #5Not a task: repair changed code

Log File Pattern Analyzer (cpp, written by Codex)

envgap__codex__cpp-t1-5

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

Raw string literal R\(...)\" contains )\" which terminates prematurely"
Not a benchmark task.
  • Its repair changed source code, so it is not an environment task.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
CMakeLists.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/cpp-t1 #5 · read the task the agent was given
Codex wrote this cpp project from the task below. It does not run on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: Log File Pattern Analyzer

Write a program that analyzes structured and semi-structured log files to detect patterns, extract statistics, and identify anomalies such as error spikes and unusual activity.

FUNCTIONAL REQUIREMENTS:
- Accept a log file path as a command-line argument
- Auto-detect common log formats: Apache/Nginx access logs, syslog, and JSON-structured logs
- Parse timestamps, log levels (DEBUG, INFO, WARN, ERROR, FATAL), source identifiers, and message content
- Compute statistics: total entries, entries per log level, entries per hour/day, top 10 most frequent messages (grouped by template after removing variable parts like IPs, timestamps, and IDs)
- Detect error spikes: flag any time window where the error rate exceeds 3x the overall average error rate
- Support filtering by date range via --from and --to flags (ISO 8601 format)
- Support filtering by log level via --level flag (show that level and above)
- Print a summary report to console with counts, top patterns, and detected anomalies
- Save the full analysis as a JSON report file with --output flag (default: log_analysis.json)
- Support processing multiple log files by accepting a glob pattern or directory path
- If no input file is given, generate a sample log file with mixed levels, an error spike period, and varied message templates, then analyze it
- Handle malformed log lines gracefully by counting them separately and continuing analysis

Create a complete C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include:
- Source code
- CMakeLists.txt with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(log_file_pattern_analyzer_cpp VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_executable(log_analyzer src/main.cpp)
README.md
# Log File Pattern Analyzer (C++)

Command-line log analyzer for mixed log sources (JSON logs, Apache/Nginx access logs, syslog, and generic level logs).

## Requirements

- Ubuntu 22.04
- G++ 12+
- CMake 3.22+

## Build

```bash
cmake -S . -B build
cmake --build build
```

## Run

Analyze a file:

```bash
./build/log_analyzer /path/to/app.log
```

Analyze a directory:

```bash
./build/log_analyzer /path/to/logs
```

Analyze a glob:

```bash
./build/log_analyzer "/path/to/logs/*.log"
```

Use filters and output path:

```bash
./build/log_analyzer /path/to/app.log --from 2026-01-10T00:00:00Z --to 2026-01-11T00:00:00Z --level WARN --output report.json
```

Generate and analyze sample logs:

```bash
./build/log_analyzer
```

## Features

- Auto-detects Apache/Nginx, syslog, JSON, and generic level-based logs
- Extracts timestamp, level, source, message
- Computes:
  - total parsed entries
  - malformed line count
  - counts per level/hour/day
  - top 10 message templates
- Detects error spikes where per-window error rate exceeds 3x overall average
- Saves full JSON report (`log_analysis.json` by default)

## Expected Output

Console summary includes:

- file count
- parsed/malformed counts
- per-level counts
- top patterns
- detected anomalies
- report file location
src/main.cpp
#include <algorithm>
#include <chrono>
#include <cctype>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
#include <vector>

namespace {

const std::vector<std::string> LEVELS = {"DEBUG", "INFO", "WARN", "ERROR", "FATAL"};

int levelRank(const std::string& level) {
    for (std::size_t i = 0; i < LEVELS.size(); i++) {
        if (LEVELS[i] == level) return static_cast<int>(i);
    }
    return -1;
}

std::string normalizeLevel(std::string level) {
    for (char& c : level) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
    if (level == "WARNING") level = "WARN";
    if (level == "ERR") level = "ERROR";
    if (level == "CRITICAL") level = "FATAL";
    return levelRank(level) >= 0 ? level : "";
}

std::string trim(const std::string& input) {
    std::size_t start = 0;
    while (start < input.size() && std::isspace(static_cast<unsigned char>(input[start]))) {
        start++;
    }
    std::size_t end = input.size();
    while (end > start && std::isspace(static_cast<unsigned char>(input[end - 1]))) {
        end--;
    }
    return input.substr(start, end - start);
}

bool levelAtLeast(const std::string& level, const std::string& threshold) {
    return levelRank(level) >= levelRank(threshold);
}

std::time_t toUtcTimestamp(std::tm tm) {
#ifdef _WIN32
    return _mkgmtime(&tm);
#else
    return timegm(&tm);
#endif
}

std::optional<std::time_t> parseTimestamp(const std::string& text) {
    if (text.empty()) return std::nullopt;
    std::smatch m;
    std::regex isoZ(R"(^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?Z?$)");
    if (std::regex_match(text, m, isoZ)) {
        std::tm tm{};
        tm.tm_year = std::stoi(m[1].str()) - 1900;
        tm.tm_mon = std::stoi(m[2].str()) - 1;
        tm.tm_mday = std::stoi(m[3].str());
        tm.tm_hour = std::stoi(m[4].str());
        tm.tm_min = std::stoi(m[5].str());
        tm.tm_sec = std::stoi(m[6].str());
        return toUtcTimestamp(tm);
    }

    std::regex isoOff(R"(^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-])(\d{2}):?(\d{2})$)");
    if (std::regex_match(text, m, isoOff)) {
        std::tm tm{};
        tm.tm_year = std::stoi(m[1].str()) - 1900;
        tm.tm_mon = std::stoi(m[2].str()) - 1;
        tm.tm_mday = std::stoi(m[3].str());
        tm.tm_hour = std::stoi(m[4].str());
        tm.tm_min = std::stoi(m[5].str());
        tm.tm_sec = std::stoi(m[6].str());
        std::time_t base = toUtcTimestamp(tm);
        int sign = m[7].str() == "+" ? 1 : -1;
        int off = std::stoi(m[8].str()) * 3600 + std::stoi(m[9].str()) * 60;
        return base - sign * off;
    }

    std::regex sys(R"(^([A-Z][a-z]{2}\s+\d+\s+\d\d:\d\d:\d\d)$)");
    if (std::regex_match(text, m, sys)) {
        std::tm tm{};
        std::time_t now = std::time(nullptr);
        std::tm current{};
#ifdef _WIN32
        gmtime_s(&current, &now);
#else
        gmtime_r(&now, &current);
#endif
        std::istringstream in(m[1].str() + " " + std::to_string(current.tm_year + 1900));
        in >> std::get_time(&tm, "%b %d %H:%M:%S %Y");
        if (!in.fail()) return toUtcTimestamp(tm);
    }

    return std::nullopt;
}

std::string timeToHour(std::time_t t) {
    std::tm tm{};
#ifdef _WIN32
    gmtime_s(&tm, &t);
#else
    gmtime_r(&t, &tm);
#endif
    char buf[32];
    std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:00:00Z", &tm);
    return buf;
}

std::string timeToDay(std::time_t t) {
    std::tm tm{};
#ifdef _WIN32
    gmtime_s(&tm, &t);
#else
    gmtime_r(&t, &tm);
#endif
    char buf[16];
    std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm);
    return buf;
}

std::string jsonEscape(const std::string& text) {
    std::ostringstream out;
    for (char ch : text) {
        switch (ch) {
            case '"': out << "\\\""; break;
            case '\\': out << "\\\\"; break;
            case '\n': out << "\\n"; break;
            case '\r': out << "\\r"; break;
            case '\t': out << "\\t"; break;
            default: out << ch;
        }
    }
    return out.str();
}

std::string templateize(std::string msg) {
    msg = std::regex_replace(msg, std::regex(R"(\b\d{4}-\d{2}-\d{2}[T ][\d:.+\-Z]+\b)"), "<TIMESTAMP>");
    msg = std::regex_replace(msg, std::regex(R"(\b\d{1,3}(?:\.\d{1,3}){3}\b)"), "<IP>");
    msg = std::regex_replace(msg, std::regex(R"(\b[0-9a-f]{8}-[0-9a-f-]{27,}\b)", std::regex::icase), "<UUID>");
    msg = std::regex_replace(msg, std::regex(R"(\b0x[0-9a-f]+\b)", std::regex::icase), "<HEX>");
    msg = std::regex_replace(msg, std::regex(R"(\b(id|user|session|req|trace)[=:]?[A-Za-z0-9_-]+\b)", std::regex::icase), "<ID>");
    msg = std::regex_replace(msg, std::regex(R"(\b\d+\b)"), "<NUM>");
    msg = std::regex_replace(msg, std::regex(R"(\s+)"), " ");
    return msg;
}

struct LogEntry {
    std::string format;
    std::optional<std::time_t> timestamp;
    std::string level;
    std::string source;
    std::string message;
};

std::optional<std::string> extractJsonField(const std::string& line, const std::string& key) {
    std::regex re("\"" + key + R"("\s*:\s*("((?:\\.|[^"])*)"|[0-9.+\-]+|true|false|null))");
    std::smatch m;
    if (!std::regex_search(line, m, re)) return std::nullopt;
    std::string raw = m[1].str();
    if (!raw.empty() && raw.front() == '"' && raw.back() == '"') {
        std::string s = raw.substr(1, raw.size() - 2);
        s = std::regex_replace(s, std::regex(R"(\\")"), "\"");
        s = std::regex_replace(s, std::regex(R"(\\\\)"), "\\");
        return s;
    }
    return raw;
}

std::optional<LogEntry> parseJsonLog(const std::string& line) {
    if (line.find('{') != 0) return std::nullopt;
    auto lv = extractJsonField(line, "level");
    if (!lv) lv = extractJsonField(line, "severity");
    if (!lv) lv = extractJsonField(line, "log_level");
    std::string level = normalizeLevel(lv.value_or("INFO"));
    if (level.empty()) return std::nullopt;

    auto msg = extractJsonField(line, "message");
    if (!msg) msg = extractJsonField(line, "msg");
    if (!msg) msg = extractJsonField(line, "event");
    auto ts = extractJsonField(line, "timestamp");
    if (!ts) ts = extractJsonField(line, "time");
    if (!ts) ts = extractJsonField(line, "datetime");
    if (!ts) ts = extractJsonField(line, "date");
    auto src = extractJsonField(line, "source");
    if (!src) src = extractJsonField(line, "logger");
    if (!src) src = extractJsonField(line, "service");

    return LogEntry{"json", parseTimestamp(ts.value_or("")), level, src.value_or(""), msg.value_or("")};
}

std::optional<LogEntry> parseApache(const std::string& line) {
    std::regex re(R"(^(\d{1,3}(?:\.\d{1,3}){3})\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"([^"]*)"\s+(\d{3})\s+(\S+).*$)");
    std::smatch m;
    if (!std::regex_match(line, m, re)) return std::nullopt;
    int status = std::stoi(m[4].str());
    std::string level = status >= 500 ? "ERROR" : status >= 400 ? "WARN" : "INFO";
    return LogEntry{"apache", parseTimestamp(std::regex_replace(m[2].str(), std::regex(":"), " ")), level,
                    m[1].str(), m[3].str() + " status=" + m[4].str() + " bytes=" + m[5].str()};
}

std::optional<LogEntry> parseSyslog(const std::string& line) {
    std::regex re(R"(^([A-Z][a-z]{2}\s+\d+\s+\d\d:\d\d:\d\d)\s+(\S+)\s+([^:]+):\s*(.*)$)");
    std::smatch m;
    if (!std::regex_match(line, m, re)) return std::nullopt;
    std::string msg = m[4].str();
    std::string level = "INFO";
    std::smatch lv;
    if (std::regex_search(msg, lv, std::regex(R"(\b(DEBUG|INFO|WARN|WARNING|ERROR|FATAL|CRITICAL)\b)", std::regex::icase))) {
        std::string normalized = normalizeLevel(lv[1].str());
        if (!normalized.empty()) level = normalized;
    }
    return LogEntry{"syslog", parseTimestamp(m[1].str()), level, m[3].str(), msg};
}

std::optional<LogEntry> parseGeneric(const std::string& line) {
    std::regex re(R"(^(\d{4}-\d{2}-\d{2}[T ][\d:.+\-Z]+)?\s*\[?(DEBUG|INFO|WARN|WARNING|ERROR|FATAL|CRITICAL)\]?\s*([A-Za-z0-9_.-]+)?\s*[-:]?\s*(.*)$)", std::regex::icase);
    std::smatch m;
    if (!std::regex_match(line, m, re)) return std::nullopt;
    std::string level = normalizeLevel(m[2].str());
    if (level.empty()) return std::nullopt;
    return LogEntry{"generic", parseTimestamp(m[1].str()), level, m[3].str(), m[4].str()};
}

std::optional<LogEntry> parseLine(const std::string& line) {
    auto j = parseJsonLog(line);
    if (j) return j;
    auto a = parseApache(line);
    if (a) return a;
    auto s = parseSyslog(line);
    if (s) return s;
    return parseGeneric(line);
}

std::vector<std::filesystem::path> resolveInputs(const std::string& input) {
    std::vector<std::filesystem::path> files;
    bool wildcard = input.find('*') != std::string::npos || input.find('?') != std::string::npos;
    if (wildcard) {
        std::filesystem::path p(input);
        std::filesystem::path dir = p.has_parent_path() ? p.parent_path() : ".";
        std::string pattern = p.filename().string();
        std::string regexPattern = "^";
        for (char ch : pattern) {
            if (ch == '*') regexPattern += ".*";
            else if (ch == '?') regexPattern += ".";
            else if (std::string(".+^$()[]{}|\\").find(ch) != std::string::npos) regexPattern += std::string("\\") + ch;
            else regexPattern += ch;
        }
        regexPattern += "$";
        std::regex re(regexPattern, std::regex::icase);
        for (const auto& entry : std::filesystem::directory_iterator(dir)) {
            if (entry.is_regular_file() && std::regex_match(entry.path().filename().string(), re)) {
                files.push_back(std::filesystem::absolute(entry.path()));
            }
        }
        std::sort(files.begin(), files.end());
        return files;
    }

    std::filesystem::path abs = std::filesystem::absolute(input);
    if (std::filesystem::is_directory(abs)) {
        for (const auto& entry : std::filesystem::directory_iterator(abs)) {
            if (!entry.is_regular_file()) continue;
            std::string name = entry.path().filename().string();
            std::string lower = name;
            std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
            if (lower.ends_with(".log") || lower.ends_with(".txt") || lower.ends_with(".json") || lower.ends_with(".jsonl")) {
                files.push_back(std::filesystem::absolute(entry.path()));
            }
        }
        std::sort(files.begin(), files.end());
        return files;
    }
    if (std::filesystem::is_regular_file(abs)) {
        files.push_back(abs);
    }
    return files;
}

std::string createSampleLogs() {
    std::ostringstream out;
    std::time_t start = 0;
    {
        std::tm tm{};
        tm.tm_year = 2026 - 1900;
        tm.tm_mon = 0;
        tm.tm_mday = 10;
        tm.tm_hour = 8;
        start = toUtcTimestamp(tm);
    }
    for (int i = 0; i < 240; i++) {
        std::time_t t = start + i * 60;
        std::tm tm{};
#ifdef _WIN32
        gmtime_s(&tm, &t);
#else
        gmtime_r(&t, &tm);
#endif
        char buf[32];
        std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
        out << buf << " [" << (i % 40 == 0 ? "WARN" : "INFO")
            << "] api-gateway - Request completed id=req-" << (1000 + i) << " user=u" << (i % 20) << "\n";
    }

    std::time_t spike;
    {
        std::tm tm{};
        tm.tm_year = 2026 - 1900;
        tm.tm_mon = 0;
        tm.tm_mday = 10;
        tm.tm_hour = 12;
        spike = toUtcTimestamp(tm);
    }
    for (int i = 0; i < 60; i++) {
        std::time_t t = spike + i * 30;
        std::tm tm{};
#ifdef _WIN32
        gmtime_s(&tm, &t);
#else
        gmtime_r(&t, &tm);
#endif
        char buf[32];
        std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
        out << "{\"timestamp\":\"" << buf << "\",\"level\":\"ERROR\",\"source\":\"payment-service\",\"message\":\"Payment failure for user_id="
            << (5000 + i) << " ip=10.0.0." << (i % 10) << "\"}\n";
    }
    out << "127.0.0.1 - - [10/Jan/2026:13:10:01 +0000] \"GET /health HTTP/1.1\" 200 64\n";
    out << "Jan 10 14:00:20 host1 scheduler: WARN job id=abc123 delayed by 45s\n";
    out << "BROKEN LINE WITHOUT FORMAT\n";
    return out.str();
}

} // namespace

int main(int argc, char** argv) {
    std::optional<std::time_t> from;
    std::optional<std::time_t> to;
    std::string level = "DEBUG";
    std::string output = "log_analysis.json";
    std::string inputArg;

    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--from") {
            i++;
            if (i >= argc) {
                std::cerr << "Missing value for --from\n";
                return 1;
            }
            from = parseTimestamp(argv[i]);
            if (!from) {
                std::cerr << "Invalid --from timestamp. Use ISO 8601.\n";
                return 1;
            }
        } else if (arg == "--to") {
            i++;
            if (i >= argc) {
                std::cerr << "Missing value for --to\n";
                return 1;
            }
            to = parseTimestamp(argv[i]);
            if (!to) {
                std::cerr << "Invalid --to timestamp. Use ISO 8601.\n";
                return 1;
            }
        } else if (arg == "--level") {
            i++;
            if (i >= argc) {
                std::cerr << "Missing value for --level\n";
                return 1;
            }
            std::string nl = normalizeLevel(argv[i]);
            if (nl.empty()) {
                std::cerr << "Invalid --level. Use DEBUG|INFO|WARN|ERROR|FATAL\n";
                return 1;
            }
            level = nl;
        } else if (arg == "--output") {
            i++;
            if (i >= argc) {
                std::cerr << "Missing value for --output\n";
                return 1;
            }
            output = argv[i];
        } else {
            inputArg = arg;
        }
    }

    std::vector<std::filesystem::path> files;
    if (inputArg.empty()) {
        std::filesystem::path sample = std::filesystem::absolute("sample.log");
        std::ofstream out(sample);
        out << createSampleLogs();
        out.close();
        files = {sample};
        std::cout << "No input provided. Generated sample log: " << sample.string() << "\n";
    } else {
        files = resolveInputs(inputArg);
        if (files.empty()) {
            std::cerr << "No input log files matched.\n";
            return 1;
        }
    }

    std::vector<LogEntry> entries;
    int malformed = 0;

    for (const auto& file : files) {
        std::ifstream in(file);
        if (!in.is_open()) {
            std::cerr << "Failed to read " << file.string() << "\n";
            return 1;
        }
        std::string line;
        while (std::getline(in, line)) {
            if (trim(line).empty()) continue;
            auto parsed = parseLine(line);
            if (!parsed) {
                malformed++;
                continue;
            }
            if (!levelAtLeast(parsed->level, level)) continue;
            if (from && parsed->timestamp && *parsed->timestamp < *from) continue;
            if (to && parsed->timestamp && *parsed->timestamp > *to) continue;
            entries.push_back(*parsed);
        }
    }

    std::map<std::string, int> levelCounts;
    for (const auto& l : LEVELS) levelCounts[l] = 0;
    std::map<std::string, int> hourly;
    std::map<std::string, int> daily;
    std::map<std::string, int> templates;
    struct WindowStat { int total = 0; int errors = 0; };
    std::map<std::string, WindowStat> hourStats;

    int errorCount = 0;
    int timestamped = 0;
    for (const auto& e : entries) {
        levelCounts[e.level]++;
        templates[templateize(e.message)]++;
        if (e.level == "ERROR" || e.level == "FATAL") errorCount++;
        if (e.timestamp) {
            timestamped++;
            std::string hb = timeToHour(*e.timestamp);
            std::string db = timeToDay(*e.timestamp);
            hourly[hb]++;
            daily[db]++;
            hourStats[hb].total++;
            if (e.level == "ERROR" || e.level == "FATAL") hourStats[hb].errors++;
        }
    }
    double avgError = entries.empty() ? 0.0 : static_cast<double>(errorCount) / entries.size();

    std::vector<std::pair<std::string, int>> topTpl(templates.begin(), templates.end());
    std::sort(topTpl.begin(), topTpl.end(), [](const auto& a, const auto& b) {
        if (a.second != b.second) return a.second > b.second;
        return a.first < b.first;
    });
    if (topTpl.size() > 10) topTpl.resize(10);

    std::cout << "Log File Pattern Analyzer\n";
    std::cout << "=========================\n";
    std::cout << "Files analyzed : " << files.size() << "\n";
    std::cout << "Parsed entries : " << entries.size() << "\n";
    std::cout << "Malformed lines: " << malformed << "\n";
    std::cout << "Level filter   : " << level << "\n";
    if (from || to) {
        std::cout << "Date range     : " << (from ? std::to_string(*from) : "-")
                  << " to " << (to ? std::to_string(*to) : "-") << "\n";
    }
    std::cout << "\nEntries per level:\n";
    for (const auto& l : LEVELS) std::cout << "- " << l << ": " << levelCounts[l] << "\n";

    std::cout << "\nTop patterns:\n";
    if (topTpl.empty()) std::cout << "- (none)\n";
    for (std::size_t i = 0; i < topTpl.size(); i++) {
        std::cout << (i + 1) << ". " << topTpl[i].second << "x  " << topTpl[i].first << "\n";
    }

    std::cout << "\nDetected anomalies:\n";
    std::vector<std::tuple<std::string, int, int, double>> spikes;
    for (const auto& [window, stat] : hourStats) {
        double rate = stat.total == 0 ? 0.0 : static_cast<double>(stat.errors) / stat.total;
        if (avgError > 0 && rate > avgError * 3.0) spikes.emplace_back(window, stat.total, stat.errors, rate);
    }
    if (spikes.empty()) std::cout << "- No error spikes detected.\n";
    for (const auto& s : spikes) {
        std::cout << "- " << std::get<0>(s) << ": error_rate=" << std::fixed << std::setprecision(3)
                  << std::get<3>(s) << " avg=" << avgError
                  << " (" << std::get<2>(s) << "/" << std::get<1>(s) << ")\n";
    }

    std::filesystem::path outPath = std::filesystem::absolute(output);
    std::ofstream json(outPath);
    if (!json.is_open()) {
        std::cerr << "Failed to open output file: " << outPath.string() << "\n";
        return 1;
    }
    json << "{\n";
    json << "  \"metadata\": {\n";
    std::time_t now = std::time(nullptr);
    std::tm tm{};
#ifdef _WIN32
    gmtime_s(&tm, &now);
#else
    gmtime_r(&now, &tm);
#endif
    char tsBuf[32];
    std::strftime(tsBuf, sizeof(tsBuf), "%Y-%m-%dT%H:%M:%SZ", &tm);
    json << "    \"analyzedAt\": \"" << tsBuf << "\",\n";
    json << "    \"files\": [";
    for (std::size_t i = 0; i < files.size(); i++) {
        if (i) json << ", ";
        json << "\"" << jsonEscape(files[i].string()) << "\"";
    }
    json << "],\n";
    json << "    \"from\": " << (from ? "\"" + std::to_string(*from) + "\"" : "null") << ",\n";
    json << "    \"to\": " << (to ? "\"" + std::to_string(*to) + "\"" : "null") << ",\n";
    json << "    \"levelThreshold\": \"" << level << "\"\n";
    json << "  },\n";
    json << "  \"totals\": {\n";
    json << "    \"parsedEntries\": " << entries.size() << ",\n";
    json << "    \"malformedLines\": " << malformed << ",\n";
    json << "    \"timestampedEntries\": " << timestamped << "\n";
    json << "  },\n";
    json << "  \"entriesPerLevel\": {\n";
    for (std::size_t i = 0; i < LEVELS.size(); i++) {
        json << "    \"" << LEVELS[i] << "\": " << levelCounts[LEVELS[i]];
        if (i + 1 < LEVELS.size()) json << ",";
        json << "\n";
    }
    json << "  },\n";
    json << "  \"entriesPerHour\": {\n";
    {
        std::size_t i = 0;
        for (const auto& [k, v] : hourly) {
            json << "    \"" << k << "\": " << v;
            if (++i < hourly.size()) json << ",";
            json << "\n";
        }
    }
    json << "  },\n";
    json << "  \"entriesPerDay\": {\n";
    {
        std::size_t i = 0;
        for (const auto& [k, v] : daily) {
            json << "    \"" << k << "\": " << v;
            if (++i < daily.size()) json << ",";
            json << "\n";
        }
    }
    json << "  },\n";
    json << "  \"topMessagePatterns\": [\n";
    for (std::size_t i = 0; i < topTpl.size(); i++) {
        json << "    {\"template\": \"" << jsonEscape(topTpl[i].first) << "\", \"count\": " << topTpl[i].second << "}";
        if (i + 1 < topTpl.size()) json << ",";
        json << "\n";
    }
    json << "  ],\n";
    json << "  \"anomalies\": {\n";
    json << "    \"errorSpikes\": [\n";
    for (std::size_t i = 0; i < spikes.size(); i++) {
        auto& s = spikes[i];
        json << "      {\"windowStart\": \"" << std::get<0>(s) << "\", \"totalEntries\": " << std::get<1>(s)
             << ", \"errorEntries\": " << std::get<2>(s)
             << ", \"errorRate\": " << std::get<3>(s)
             << ", \"averageErrorRate\": " << avgError << "}";
        if (i + 1 < spikes.size()) json << ",";
        json << "\n";
    }
    json << "    ]\n";
    json << "  }\n";
    json << "}\n";
    json.close();

    std::cout << "\nSaved JSON report: " << outPath.string() << "\n";
    return 0;
}