Skip to content

Commit 97d0d94

Browse files
committed
feat(cli): detect repeated production log errors
2 parents 3dd63eb + 36f7dfe commit 97d0d94

5 files changed

Lines changed: 372 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
*
3+
* @file LogsAnalyzer.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#ifndef VIX_LOGS_ANALYZER_HPP
14+
#define VIX_LOGS_ANALYZER_HPP
15+
16+
#include <iosfwd>
17+
#include <string>
18+
#include <vector>
19+
20+
namespace vix::commands::logs::analyzer
21+
{
22+
/**
23+
* @struct RepeatedLogEntry
24+
* @brief Represents one normalized log message that appears multiple times.
25+
*/
26+
struct RepeatedLogEntry
27+
{
28+
/**
29+
* @brief Normalized log message.
30+
*/
31+
std::string message{};
32+
33+
/**
34+
* @brief Number of times the normalized message appears.
35+
*/
36+
int count{0};
37+
};
38+
39+
/**
40+
* @struct RepeatedLogReport
41+
* @brief Summary produced by repeated log analysis.
42+
*/
43+
struct RepeatedLogReport
44+
{
45+
/**
46+
* @brief Repeated log entries sorted by descending count.
47+
*/
48+
std::vector<RepeatedLogEntry> entries{};
49+
50+
/**
51+
* @brief Total number of log lines analyzed.
52+
*/
53+
int totalLines{0};
54+
55+
/**
56+
* @brief Number of repeated message groups detected.
57+
*/
58+
int repeatedGroups{0};
59+
};
60+
61+
/**
62+
* @brief Analyze log lines and detect repeated normalized error messages.
63+
*
64+
* This function normalizes variable parts of log lines, such as timestamps,
65+
* IP addresses, numbers, and long values, then groups equivalent messages
66+
* together.
67+
*
68+
* @param lines Raw log lines to analyze.
69+
* @return Report containing repeated error groups and counters.
70+
*/
71+
RepeatedLogReport analyze_repeated_errors(
72+
const std::vector<std::string> &lines);
73+
74+
/**
75+
* @brief Print a repeated error analysis report.
76+
*
77+
* @param out Output stream.
78+
* @param report Repeated error report to print.
79+
*/
80+
void print_repeated_report(
81+
std::ostream &out,
82+
const RepeatedLogReport &report);
83+
}
84+
85+
#endif

include/vix/cli/commands/logs/LogsTypes.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ namespace vix::commands::logs
5151
*/
5252
bool errorsOnly{false};
5353

54+
/**
55+
* @brief Show repeated error summary.
56+
*/
57+
bool repeated{false};
58+
5459
/**
5560
* @brief Number of lines to show.
5661
*/

src/commands/LogsCommand.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ namespace vix::commands
149149
options.errorsOnly = options.errorsOnly ||
150150
consume_flag(args, "--errors");
151151

152+
options.repeated = consume_flag(args, "--repeated");
153+
152154
if (!consume_value(args, "--since", options.since))
153155
{
154156
ok = false;
@@ -239,6 +241,7 @@ namespace vix::commands
239241
<< " --follow Follow logs live\n"
240242
<< " -f Alias for --follow\n"
241243
<< " --errors Filter logs by common error keywords\n"
244+
<< " --repeated Detect repeated errors\n"
242245
<< " --since Filter app logs by systemd time expression\n"
243246
<< " --lines Show last N lines\n"
244247
<< " -n Alias for --lines\n"
@@ -250,6 +253,7 @@ namespace vix::commands
250253
<< " vix logs errors\n"
251254
<< " vix logs --follow\n"
252255
<< " vix logs --errors\n"
256+
<< " vix logs errors --repeated\n"
253257
<< " vix logs --since \"1 hour ago\"\n"
254258
<< " vix logs -n 200\n\n"
255259
<< "Config:\n"

src/commands/logs/LogsAnalyzer.cpp

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
*
3+
* @file LogsAnalyzer.cpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#include <vix/cli/commands/logs/LogsAnalyzer.hpp>
14+
#include <vix/cli/util/Ui.hpp>
15+
16+
#include <algorithm>
17+
#include <cctype>
18+
#include <ostream>
19+
#include <regex>
20+
#include <string>
21+
#include <unordered_map>
22+
#include <vector>
23+
24+
namespace vix::commands::logs::analyzer
25+
{
26+
namespace
27+
{
28+
std::string trim_copy(std::string value)
29+
{
30+
auto not_space = [](unsigned char ch)
31+
{
32+
return !std::isspace(ch);
33+
};
34+
35+
value.erase(
36+
value.begin(),
37+
std::find_if(value.begin(), value.end(), not_space));
38+
39+
value.erase(
40+
std::find_if(value.rbegin(), value.rend(), not_space).base(),
41+
value.end());
42+
43+
return value;
44+
}
45+
46+
std::string to_lower_copy(std::string value)
47+
{
48+
std::transform(
49+
value.begin(),
50+
value.end(),
51+
value.begin(),
52+
[](unsigned char ch)
53+
{
54+
return static_cast<char>(std::tolower(ch));
55+
});
56+
57+
return value;
58+
}
59+
60+
std::string normalize_log_line(std::string line)
61+
{
62+
line = trim_copy(line);
63+
64+
line = std::regex_replace(
65+
line,
66+
std::regex(R"(\b[0-9]{4}-[0-9]{2}-[0-9]{2}[^\s]*\b)"),
67+
"<timestamp>");
68+
69+
line = std::regex_replace(
70+
line,
71+
std::regex(R"(\b[0-9]{1,3}(\.[0-9]{1,3}){3}\b)"),
72+
"<ip>");
73+
74+
line = std::regex_replace(
75+
line,
76+
std::regex(R"(\b[0-9]+\b)"),
77+
"<num>");
78+
79+
line = std::regex_replace(
80+
line,
81+
std::regex("\"([^\"]{32,})\""),
82+
"\"<value>\"");
83+
84+
line = std::regex_replace(
85+
line,
86+
std::regex(R"('([^']{32,})')"),
87+
"'<value>'");
88+
89+
line = to_lower_copy(line);
90+
line = trim_copy(line);
91+
92+
return line;
93+
}
94+
}
95+
96+
RepeatedLogReport analyze_repeated_errors(
97+
const std::vector<std::string> &lines)
98+
{
99+
RepeatedLogReport report;
100+
report.totalLines = static_cast<int>(lines.size());
101+
102+
std::unordered_map<std::string, int> counts;
103+
104+
for (const std::string &line : lines)
105+
{
106+
const std::string normalized = normalize_log_line(line);
107+
108+
if (normalized.empty())
109+
continue;
110+
111+
++counts[normalized];
112+
}
113+
114+
for (const auto &[message, count] : counts)
115+
{
116+
if (count <= 1)
117+
continue;
118+
119+
report.entries.push_back(
120+
RepeatedLogEntry{
121+
message,
122+
count});
123+
}
124+
125+
std::sort(
126+
report.entries.begin(),
127+
report.entries.end(),
128+
[](const RepeatedLogEntry &a, const RepeatedLogEntry &b)
129+
{
130+
if (a.count != b.count)
131+
return a.count > b.count;
132+
133+
return a.message < b.message;
134+
});
135+
136+
report.repeatedGroups =
137+
static_cast<int>(report.entries.size());
138+
139+
return report;
140+
}
141+
142+
void print_repeated_report(
143+
std::ostream &out,
144+
const RepeatedLogReport &report)
145+
{
146+
vix::cli::util::section(out, "Repeated Errors");
147+
148+
vix::cli::util::kv(
149+
out,
150+
"Analyzed lines",
151+
std::to_string(report.totalLines));
152+
153+
vix::cli::util::kv(
154+
out,
155+
"Repeated groups",
156+
std::to_string(report.repeatedGroups));
157+
158+
if (report.entries.empty())
159+
{
160+
vix::cli::util::ok_line(
161+
out,
162+
"no repeated errors detected");
163+
164+
return;
165+
}
166+
167+
for (const RepeatedLogEntry &entry : report.entries)
168+
{
169+
vix::cli::util::kv(
170+
out,
171+
std::to_string(entry.count) + "x",
172+
entry.message);
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)