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(¤t, &now);
#else
gmtime_r(&now, ¤t);
#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;
}