gool 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gool/__init__.py +0 -0
- gool/_version.py +24 -0
- gool/log_clustering.py +427 -0
- gool-1.0.0.dist-info/METADATA +54 -0
- gool-1.0.0.dist-info/RECORD +8 -0
- gool-1.0.0.dist-info/WHEEL +4 -0
- gool-1.0.0.dist-info/entry_points.txt +2 -0
- gool-1.0.0.dist-info/licenses/LICENSE +21 -0
gool/__init__.py
ADDED
|
File without changes
|
gool/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
gool/log_clustering.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
This script computes clusters of similar log lines from the provided log files.
|
|
4
|
+
It uses the drain3 library to extract templates from log lines.
|
|
5
|
+
It can filter log lines based on a regex pattern and display the clusters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
import itertools
|
|
10
|
+
import logging
|
|
11
|
+
import pathlib
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from collections import Counter, namedtuple
|
|
16
|
+
from collections.abc import Generator, Iterator
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from math import ceil, log10
|
|
19
|
+
from typing import Annotated, Any, Union
|
|
20
|
+
|
|
21
|
+
import tyro
|
|
22
|
+
from drain3 import TemplateMiner # type: ignore
|
|
23
|
+
from drain3.masking import MaskingInstruction # type: ignore
|
|
24
|
+
from drain3.template_miner_config import TemplateMinerConfig # type: ignore
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.logging import RichHandler
|
|
27
|
+
from rich.progress import BarColumn, Progress, TaskID, TaskProgressColumn, TextColumn
|
|
28
|
+
from rich.table import Table
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from gool._version import __version__
|
|
32
|
+
except ImportError:
|
|
33
|
+
__version__ = "unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ErrorCode(Enum):
|
|
37
|
+
"""Error codes for the log clustering script."""
|
|
38
|
+
|
|
39
|
+
NO_LOG_FILES = -1
|
|
40
|
+
INVALID_TREE_DEPTH = -2
|
|
41
|
+
INVALID_REGEX = -3
|
|
42
|
+
FILE_NOT_FOUND = -4
|
|
43
|
+
IO_ERROR = -5
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
error_console = Console(file=sys.stderr, stderr=True)
|
|
47
|
+
console = Console()
|
|
48
|
+
|
|
49
|
+
logging.basicConfig(
|
|
50
|
+
format="%(module)s : %(message)s",
|
|
51
|
+
datefmt="%H:%M:%S.%f",
|
|
52
|
+
level=logging.INFO,
|
|
53
|
+
handlers=[RichHandler(console=error_console)],
|
|
54
|
+
)
|
|
55
|
+
logging.getLogger("drain3").setLevel(logging.WARNING)
|
|
56
|
+
|
|
57
|
+
HOME_CFG_FILE = pathlib.Path.home() / ".drain3.ini"
|
|
58
|
+
KB_FACTOR = 1000
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclasses.dataclass
|
|
62
|
+
class Arguments:
|
|
63
|
+
"""
|
|
64
|
+
Use drain algorithm to cluster similar log lines
|
|
65
|
+
from the provided log files.
|
|
66
|
+
|
|
67
|
+
Arguments for the log clustering script :
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# logs files paths to process
|
|
71
|
+
logfile_paths: tyro.conf.Positional[tuple[pathlib.Path, ...]]
|
|
72
|
+
# configuration file for the drain3 template miner.
|
|
73
|
+
cfg_file: Annotated[pathlib.Path, tyro.conf.arg(aliases=["-c"])] = HOME_CFG_FILE
|
|
74
|
+
|
|
75
|
+
# If set, filter input log lines which does not match the regex (re python module syntax).
|
|
76
|
+
# Example: '.*(\| Warning |\| Error ).*'
|
|
77
|
+
filter: Annotated[str, tyro.conf.arg(aliases=["-f"])] = ""
|
|
78
|
+
# If set, the clusters will be ordered lexicographically.
|
|
79
|
+
lex_order: Annotated[tyro.conf.FlagCreatePairsOff[bool], tyro.conf.arg(aliases=["-l"])] = False
|
|
80
|
+
# The clusters will be ordered by this total length. Where total length is the sum of all
|
|
81
|
+
# log lines lengths belonging to the cluster.
|
|
82
|
+
size_order: Annotated[tyro.conf.FlagCreatePairsOff[bool], tyro.conf.arg(aliases=["-z"])] = False
|
|
83
|
+
# Similarity threshold for the template miner to group lines together.
|
|
84
|
+
# A higher value will lead to create more clusters. Drain default value is 0.4.
|
|
85
|
+
similarity_threshold: Annotated[Union[float, None], tyro.conf.arg(aliases=["-s"])] = None
|
|
86
|
+
# depth of the tree to build the templates miner,
|
|
87
|
+
# a higher value will lead to create more clusters.
|
|
88
|
+
# The higher the value, the more tokens of the log lines
|
|
89
|
+
# will be considered to build the clusters. Increase this value
|
|
90
|
+
# to make clustering rely on distant tokens. Drain default value is 4.
|
|
91
|
+
tree_depth: Annotated[Union[int, None], tyro.conf.arg(aliases=["-d"], default=4)] = None
|
|
92
|
+
# If set, output clusters in plain text format without colors or table formatting,
|
|
93
|
+
# making it easy to process with bash tools like grep, awk, or cut.
|
|
94
|
+
raw: Annotated[tyro.conf.FlagCreatePairsOff[bool], tyro.conf.arg(aliases=["-r"])] = False
|
|
95
|
+
# return the version and exit
|
|
96
|
+
version: Annotated[tyro.conf.FlagCreatePairsOff[bool], tyro.conf.arg(aliases=["-v"])] = False
|
|
97
|
+
|
|
98
|
+
def __post_init__(self) -> None:
|
|
99
|
+
if self.version:
|
|
100
|
+
return
|
|
101
|
+
if self.logfile_paths is None or len(self.logfile_paths) == 0:
|
|
102
|
+
error_message = "No log files provided. Please specify at least one log file."
|
|
103
|
+
logging.critical(error_message)
|
|
104
|
+
sys.exit(ErrorCode.NO_LOG_FILES.value)
|
|
105
|
+
if self.tree_depth and self.tree_depth < 3:
|
|
106
|
+
error_message = f"The tree depth is set to {self.tree_depth}. Minimum value is 3."
|
|
107
|
+
logging.critical(error_message)
|
|
108
|
+
sys.exit(ErrorCode.INVALID_TREE_DEPTH.value)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_drain3_cfg(args: Arguments) -> Any:
|
|
112
|
+
"""
|
|
113
|
+
Create a default configuration for the drain3 template miner.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
TemplateMinerConfig: A configuration object for the drain3 template miner.
|
|
117
|
+
"""
|
|
118
|
+
drain3_cfg = TemplateMinerConfig()
|
|
119
|
+
if args.cfg_file.exists():
|
|
120
|
+
logging.info("Loading configuration from %s", args.cfg_file)
|
|
121
|
+
drain3_cfg.load(args.cfg_file)
|
|
122
|
+
else:
|
|
123
|
+
logging.info(
|
|
124
|
+
"Configuration file %s not found. Using default configuration to mask IP, time, and hex values.",
|
|
125
|
+
args.cfg_file,
|
|
126
|
+
)
|
|
127
|
+
mask_time = MaskingInstruction(r"(\d{2}:\d{2}:\d{2}(.\d+|))", "TIME")
|
|
128
|
+
mask_ip = MaskingInstruction(
|
|
129
|
+
r"((?<=[^A-Za-z0-9])|^)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})((?=[^A-Za-z0-9])|$)",
|
|
130
|
+
"IP",
|
|
131
|
+
)
|
|
132
|
+
mask_hex = MaskingInstruction(
|
|
133
|
+
r"((?<=[^A-Za-z0-9])|^)(0[xX][0-9a-fA-F]+)((?=[^A-Za-z0-9])|$)",
|
|
134
|
+
"HEX",
|
|
135
|
+
)
|
|
136
|
+
# Add masking instructions to the configuration
|
|
137
|
+
drain3_cfg.masking_instructions += [mask_time, mask_ip, mask_hex]
|
|
138
|
+
|
|
139
|
+
if args.similarity_threshold:
|
|
140
|
+
drain3_cfg.drain_sim_th = args.similarity_threshold
|
|
141
|
+
if args.tree_depth:
|
|
142
|
+
drain3_cfg.drain_depth = args.tree_depth
|
|
143
|
+
return drain3_cfg
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def estimate_lines(path: pathlib.Path, nb_sample_lines: int = 1000) -> int:
|
|
147
|
+
"""
|
|
148
|
+
Estimate total lines based on file size and sample average.
|
|
149
|
+
Args:
|
|
150
|
+
path (pathlib.Path): Path to the log file.
|
|
151
|
+
nb_sample_lines (int): Number of lines to sample for average calculation.
|
|
152
|
+
"""
|
|
153
|
+
file_size = path.stat().st_size
|
|
154
|
+
if file_size == 0:
|
|
155
|
+
return 0
|
|
156
|
+
# Sample first 'sample_lines' to get avg bytes per line
|
|
157
|
+
avg_bytes_per_line = 0
|
|
158
|
+
with open(path, "rb") as f: # Use binary for accurate byte counting
|
|
159
|
+
for i, line in enumerate(f):
|
|
160
|
+
if i >= nb_sample_lines:
|
|
161
|
+
break
|
|
162
|
+
avg_bytes_per_line += len(line)
|
|
163
|
+
if nb_sample_lines > 0:
|
|
164
|
+
avg_bytes_per_line //= nb_sample_lines
|
|
165
|
+
# Estimate total lines
|
|
166
|
+
estimated = int(file_size / avg_bytes_per_line) if avg_bytes_per_line > 0 else 0
|
|
167
|
+
return max(estimated, 1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def create_file_line_generators(
|
|
171
|
+
logfile_paths: tuple[pathlib.Path, ...],
|
|
172
|
+
progress: Progress,
|
|
173
|
+
) -> itertools.chain:
|
|
174
|
+
"""
|
|
175
|
+
Create progress tasks for all files and return a chained generator yielding lines from all files.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
logfile_paths (tuple[pathlib.Path, ...]): Paths to the log files.
|
|
179
|
+
progress (Progress): The progress instance to track file processing.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Generator: A single generator yielding lines from all files.
|
|
183
|
+
"""
|
|
184
|
+
generators = []
|
|
185
|
+
for logfile_path in logfile_paths:
|
|
186
|
+
sample_nb_lines = 20000
|
|
187
|
+
number_of_lines = estimate_lines(logfile_path, sample_nb_lines)
|
|
188
|
+
task_id = progress.add_task(f"{pathlib.Path(logfile_path).name}", total=number_of_lines)
|
|
189
|
+
|
|
190
|
+
def line_generator(path: pathlib.Path, tid: TaskID, nb_lines: int) -> Generator[str, None, None]:
|
|
191
|
+
"""Generator that yields lines from a file and updates progress."""
|
|
192
|
+
with open(path, encoding="utf-8", errors="surrogateescape") as f:
|
|
193
|
+
for line in f:
|
|
194
|
+
progress.update(tid, advance=1)
|
|
195
|
+
yield line
|
|
196
|
+
progress.update(tid, completed=nb_lines)
|
|
197
|
+
progress.stop_task(tid)
|
|
198
|
+
|
|
199
|
+
generators.append(line_generator(logfile_path, task_id, number_of_lines))
|
|
200
|
+
|
|
201
|
+
# Chain all generators into one
|
|
202
|
+
return itertools.chain(*generators)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def add_log_lines_to_miner(
|
|
206
|
+
template_miner: Any,
|
|
207
|
+
line_generator: Iterator[str],
|
|
208
|
+
regex: Union[re.Pattern[str], None],
|
|
209
|
+
) -> tuple[int, Counter]:
|
|
210
|
+
"""
|
|
211
|
+
Add log lines from a generator to the template miner.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
template_miner (TemplateMiner): The template miner instance.
|
|
215
|
+
line_generator (Generator): A generator yielding log lines.
|
|
216
|
+
regex (re.Pattern | None): Regex pattern to filter log lines.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
tuple[int, Counter]: The number of lines added and cluster sizes counter.
|
|
220
|
+
"""
|
|
221
|
+
start_time = time.perf_counter()
|
|
222
|
+
total_nb_lines = 0
|
|
223
|
+
cluster_char_sizes: Counter = Counter()
|
|
224
|
+
|
|
225
|
+
for line in line_generator:
|
|
226
|
+
if not line:
|
|
227
|
+
continue
|
|
228
|
+
if regex and not regex.match(line):
|
|
229
|
+
continue
|
|
230
|
+
total_nb_lines += 1
|
|
231
|
+
result = template_miner.add_log_message(line)
|
|
232
|
+
cluster_char_sizes[result["cluster_id"]] += len(line)
|
|
233
|
+
end_time = time.perf_counter()
|
|
234
|
+
elapsed_time = end_time - start_time
|
|
235
|
+
logging.info(
|
|
236
|
+
"Processed %d lines in %.2f seconds (%.2f lines/second).",
|
|
237
|
+
total_nb_lines,
|
|
238
|
+
elapsed_time,
|
|
239
|
+
total_nb_lines / elapsed_time if elapsed_time > 0 else 0,
|
|
240
|
+
)
|
|
241
|
+
return total_nb_lines, cluster_char_sizes
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def surrogate_non_printable(s: str) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Surrogate non-printable characters from a string.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
s (str): The input string.
|
|
250
|
+
Returns:
|
|
251
|
+
str: The cleaned string.
|
|
252
|
+
"""
|
|
253
|
+
return s.encode("utf-8", errors="surrogateescape").decode("utf-8")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def compute_margin_for_display(max_number: int) -> int:
|
|
257
|
+
"""
|
|
258
|
+
Compute the margin for displaying numbers.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
max_number (int): The maximum number to display.
|
|
262
|
+
Returns:
|
|
263
|
+
int: The computed margin.
|
|
264
|
+
"""
|
|
265
|
+
b10 = log10(max_number)
|
|
266
|
+
margin = ceil(log10(max_number)) if b10 != int(b10) else int(b10 + 1)
|
|
267
|
+
return margin
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def display_clusters(
|
|
271
|
+
template_miner: Any,
|
|
272
|
+
cluster_char_sizes: Counter,
|
|
273
|
+
order_by: str = "count",
|
|
274
|
+
raw: bool = False,
|
|
275
|
+
) -> int:
|
|
276
|
+
"""
|
|
277
|
+
Display all clusters in a table with 3 columns: Count - Char Size (KB) - Template.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
template_miner (TemplateMiner): the template miner which has been filled with log lines.
|
|
281
|
+
cluster_char_sizes (Counter): a counter of the total sizes of each cluster.
|
|
282
|
+
order_by (str): How to order clusters: "count", "size", or "template". Defaults to "count".
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
int: The total number of lines in all clusters.
|
|
286
|
+
"""
|
|
287
|
+
ClusterResult = namedtuple("ClusterResult", ["count", "char_size", "template"])
|
|
288
|
+
clusters_data = [
|
|
289
|
+
ClusterResult(
|
|
290
|
+
count=cluster.size,
|
|
291
|
+
char_size=cluster_char_sizes[cluster.cluster_id],
|
|
292
|
+
template=cluster.get_template(),
|
|
293
|
+
)
|
|
294
|
+
for cluster in template_miner.drain.clusters
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
if not clusters_data:
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
# Sort based on order_by parameter
|
|
301
|
+
if order_by == "size":
|
|
302
|
+
clusters_data.sort(key=lambda x: x.char_size, reverse=True)
|
|
303
|
+
elif order_by == "template":
|
|
304
|
+
clusters_data.sort(key=lambda x: x.template)
|
|
305
|
+
else: # default to "count"
|
|
306
|
+
clusters_data.sort(key=lambda x: x.count, reverse=True)
|
|
307
|
+
|
|
308
|
+
# Add rows to the table or print plain text
|
|
309
|
+
total_nb_lines_clusters = 0
|
|
310
|
+
if raw:
|
|
311
|
+
# Plain text output for bash processing
|
|
312
|
+
count_margin = compute_margin_for_display(max(cluster.count for cluster in clusters_data))
|
|
313
|
+
size_margin = compute_margin_for_display(max(cluster.char_size for cluster in clusters_data) // KB_FACTOR)
|
|
314
|
+
for cluster in clusters_data:
|
|
315
|
+
pattern = surrogate_non_printable(cluster.template)
|
|
316
|
+
count_str = f"{cluster.count}"
|
|
317
|
+
size_kb = cluster.char_size // KB_FACTOR
|
|
318
|
+
size_str = f"{size_kb}"
|
|
319
|
+
console.print(
|
|
320
|
+
f"{count_str:>{count_margin}} - {size_str:>{size_margin}} - {pattern}",
|
|
321
|
+
soft_wrap=True,
|
|
322
|
+
markup=False,
|
|
323
|
+
)
|
|
324
|
+
total_nb_lines_clusters += cluster.count
|
|
325
|
+
else:
|
|
326
|
+
# Create a Rich table
|
|
327
|
+
table = Table(title="Log Clusters", highlight=True)
|
|
328
|
+
table.add_column("Count", justify="right", style="cyan", no_wrap=True)
|
|
329
|
+
table.add_column("Char Size (KB)", justify="right", style="magenta", no_wrap=True)
|
|
330
|
+
table.add_column("Template", justify="left")
|
|
331
|
+
|
|
332
|
+
for cluster in clusters_data:
|
|
333
|
+
pattern = surrogate_non_printable(cluster.template)
|
|
334
|
+
count_str = f"{cluster.count:,}".replace(",", " ")
|
|
335
|
+
size_kb = cluster.char_size // KB_FACTOR
|
|
336
|
+
size_str = f"{size_kb:,}".replace(",", " ")
|
|
337
|
+
table.add_row(count_str, size_str, pattern)
|
|
338
|
+
total_nb_lines_clusters += cluster.count
|
|
339
|
+
|
|
340
|
+
# Print the table
|
|
341
|
+
console.print(table, markup=False)
|
|
342
|
+
|
|
343
|
+
return total_nb_lines_clusters
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def main(args: Arguments) -> int:
|
|
347
|
+
"""
|
|
348
|
+
Extract the cluster templates from the provided log files.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
args (Arguments): paths and options for the log extractor
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
int: 0 if everything went well
|
|
355
|
+
"""
|
|
356
|
+
drain3_cfg = create_drain3_cfg(args)
|
|
357
|
+
template_miner = TemplateMiner(config=drain3_cfg)
|
|
358
|
+
regex = None
|
|
359
|
+
try:
|
|
360
|
+
regex = re.compile(args.filter) if args.filter else None
|
|
361
|
+
except re.error as e:
|
|
362
|
+
logging.critical("Invalid regex pattern: %s. Error: %s", args.filter, e)
|
|
363
|
+
sys.exit(ErrorCode.INVALID_REGEX.value)
|
|
364
|
+
total_nb_lines, cluster_char_sizes = 0, None
|
|
365
|
+
try:
|
|
366
|
+
with Progress(
|
|
367
|
+
TextColumn("[progress.description]{task.description}"),
|
|
368
|
+
BarColumn(),
|
|
369
|
+
TaskProgressColumn(),
|
|
370
|
+
console=error_console,
|
|
371
|
+
) as progress:
|
|
372
|
+
line_generator = create_file_line_generators(args.logfile_paths, progress)
|
|
373
|
+
total_nb_lines, cluster_char_sizes = add_log_lines_to_miner(template_miner, line_generator, regex)
|
|
374
|
+
except FileNotFoundError as e:
|
|
375
|
+
logging.critical("File not found: %s", e.filename)
|
|
376
|
+
sys.exit(ErrorCode.FILE_NOT_FOUND.value)
|
|
377
|
+
except OSError as e:
|
|
378
|
+
logging.critical("I/O error(%s): %s", e.errno, e.strerror)
|
|
379
|
+
sys.exit(ErrorCode.IO_ERROR.value)
|
|
380
|
+
|
|
381
|
+
def display_results() -> int:
|
|
382
|
+
order = "count"
|
|
383
|
+
if args.lex_order:
|
|
384
|
+
order = "template"
|
|
385
|
+
elif args.size_order:
|
|
386
|
+
order = "size"
|
|
387
|
+
total_nb_lines_clusters = display_clusters(template_miner, cluster_char_sizes, order_by=order, raw=args.raw)
|
|
388
|
+
return total_nb_lines_clusters
|
|
389
|
+
|
|
390
|
+
def sanity_check(total_nb_lines_clusters: int) -> int:
|
|
391
|
+
"""Check if the total number of lines in clusters matches the processed lines."""
|
|
392
|
+
result = 0
|
|
393
|
+
if total_nb_lines_clusters != total_nb_lines:
|
|
394
|
+
logging.error(
|
|
395
|
+
"The number of lines in the clusters (%d) does "
|
|
396
|
+
"not match the total number of lines processed (%d)."
|
|
397
|
+
"Maybe you should increase [DRAIN]/max_clusters parameter.",
|
|
398
|
+
total_nb_lines_clusters,
|
|
399
|
+
total_nb_lines,
|
|
400
|
+
)
|
|
401
|
+
result = 1
|
|
402
|
+
return result
|
|
403
|
+
|
|
404
|
+
total_nb_lines_clusters = display_results()
|
|
405
|
+
return sanity_check(total_nb_lines_clusters)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def main_cli() -> int:
|
|
409
|
+
"""
|
|
410
|
+
Main entry point for the CLI.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
int: Exit code.
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
cfg = tyro.cli(Arguments)
|
|
417
|
+
if cfg.version:
|
|
418
|
+
print(__version__)
|
|
419
|
+
return 0
|
|
420
|
+
return main(cfg)
|
|
421
|
+
except KeyboardInterrupt:
|
|
422
|
+
error_console.print("\n[red]Process interrupted by user[/red]")
|
|
423
|
+
return ErrorCode.IO_ERROR.value
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
if __name__ == "__main__":
|
|
427
|
+
main_cli()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gool
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: cli logs clustering
|
|
5
|
+
Project-URL: Homepage, https://github.com/leCapi/gool
|
|
6
|
+
Project-URL: Repository, https://github.com/leCapi/gool
|
|
7
|
+
Project-URL: Documentation, https://github.com/leCapi/gool
|
|
8
|
+
Author: Olivier GODARD
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: python
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: <4.0,>=3.10
|
|
21
|
+
Requires-Dist: drain3~=0.9
|
|
22
|
+
Requires-Dist: rich~=14.0
|
|
23
|
+
Requires-Dist: tyro<=1.0.8,>=0.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# gool, repo usage
|
|
27
|
+
|
|
28
|
+
The gool repository uses uv and git. Version is taken from git tag. The repo provide setup for VSCode.
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
Below the more useful commands.
|
|
33
|
+
|
|
34
|
+
setup the uv virtual environment and install pre-commit hooks :
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
make install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Generate and launch the documentation server :
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
make docs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
All commands:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
make help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
Repository initiated with [fpgmaas/cookiecutter-uv](https://github.com/fpgmaas/cookiecutter-uv).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
gool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
gool/_version.py,sha256=JAAyU3al4wmBf3BF-Umm1J_GrCIT-yS_yWEGHqKRVHc,520
|
|
3
|
+
gool/log_clustering.py,sha256=dhWlwaItagXJ_eTH-ivtTiUxMI4hcud-_8I0GTrkfYM,15449
|
|
4
|
+
gool-1.0.0.dist-info/METADATA,sha256=a4hgzEyAxQpfln6BHLvGV7ygIA1iLeh_CB5oj1OzvwI,1396
|
|
5
|
+
gool-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
gool-1.0.0.dist-info/entry_points.txt,sha256=0mQBhFOgYtj1BakyfMIkdTAaNx4fURkLuBYxKBIADW0,54
|
|
7
|
+
gool-1.0.0.dist-info/licenses/LICENSE,sha256=tsxRB5OyrYVPFO3godPt7uqDdLPPKtKr_sTGchF3UZ8,1071
|
|
8
|
+
gool-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Olivier GODARD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|