gool 1.3.2__tar.gz → 1.3.3__tar.gz

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.
Files changed (52) hide show
  1. {gool-1.3.2 → gool-1.3.3}/.vscode/launch.json +15 -0
  2. {gool-1.3.2 → gool-1.3.3}/PKG-INFO +1 -1
  3. {gool-1.3.2 → gool-1.3.3}/src/gool/_version.py +2 -2
  4. {gool-1.3.2 → gool-1.3.3}/src/gool/arguments_parser.py +10 -7
  5. {gool-1.3.2 → gool-1.3.3}/src/gool/log_clustering.py +2 -0
  6. {gool-1.3.2 → gool-1.3.3}/src/gool/logs_miner.py +9 -4
  7. gool-1.3.3/tests/data/log/Crafted_logs.log +5 -0
  8. gool-1.3.3/tests/data/results/Crafted_logs_time_120014.txt +5 -0
  9. {gool-1.3.2 → gool-1.3.3}/tests/test_gool.py +24 -2
  10. {gool-1.3.2 → gool-1.3.3}/tests/test_logs_miner.py +14 -14
  11. {gool-1.3.2 → gool-1.3.3}/.gitignore +0 -0
  12. {gool-1.3.2 → gool-1.3.3}/.gitlab-ci.yml +0 -0
  13. {gool-1.3.2 → gool-1.3.3}/.pre-commit-config.yaml +0 -0
  14. {gool-1.3.2 → gool-1.3.3}/.vscode/extensions.json +0 -0
  15. {gool-1.3.2 → gool-1.3.3}/.vscode/settings.json +0 -0
  16. {gool-1.3.2 → gool-1.3.3}/.vscode/tasks.json +0 -0
  17. {gool-1.3.2 → gool-1.3.3}/LICENSE +0 -0
  18. {gool-1.3.2 → gool-1.3.3}/Makefile +0 -0
  19. {gool-1.3.2 → gool-1.3.3}/README.md +0 -0
  20. {gool-1.3.2 → gool-1.3.3}/docs/api/arguments_parser.md +0 -0
  21. {gool-1.3.2 → gool-1.3.3}/docs/api/log_clustering.md +0 -0
  22. {gool-1.3.2 → gool-1.3.3}/docs/api/logs_miner.md +0 -0
  23. {gool-1.3.2 → gool-1.3.3}/docs/index.md +0 -0
  24. {gool-1.3.2 → gool-1.3.3}/docs/license.md +0 -0
  25. {gool-1.3.2 → gool-1.3.3}/misc/Dockerfile +0 -0
  26. {gool-1.3.2 → gool-1.3.3}/mkdocs.yml +0 -0
  27. {gool-1.3.2 → gool-1.3.3}/pyproject.toml +0 -0
  28. {gool-1.3.2 → gool-1.3.3}/sonar-project.properties +0 -0
  29. {gool-1.3.2 → gool-1.3.3}/src/gool/__init__.py +0 -0
  30. {gool-1.3.2 → gool-1.3.3}/src/gool/drain3.ini +0 -0
  31. {gool-1.3.2 → gool-1.3.3}/src/gool/error_code.py +0 -0
  32. {gool-1.3.2 → gool-1.3.3}/tests/data/drain3.ini +0 -0
  33. {gool-1.3.2 → gool-1.3.3}/tests/data/drain3_compare_baseline.ini +0 -0
  34. {gool-1.3.2 → gool-1.3.3}/tests/data/log/Apache_2k.log +0 -0
  35. {gool-1.3.2 → gool-1.3.3}/tests/data/log/Apache_2k_MonDec05.log +0 -0
  36. {gool-1.3.2 → gool-1.3.3}/tests/data/log/Apache_2k_SunDec04.log +0 -0
  37. {gool-1.3.2 → gool-1.3.3}/tests/data/log/Zookeeper_2k.log +0 -0
  38. {gool-1.3.2 → gool-1.3.3}/tests/data/log/Zookeeper_unordered.log +0 -0
  39. {gool-1.3.2 → gool-1.3.3}/tests/data/log/empty.log +0 -0
  40. {gool-1.3.2 → gool-1.3.3}/tests/data/results/6times_Zookeeper_2k_raw_output.txt +0 -0
  41. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Apache_2K_baseline_compare.txt +0 -0
  42. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Apache_2K_baseline_compare_common.txt +0 -0
  43. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Apache_2k_raw_output.txt +0 -0
  44. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Apache_2k_time_filter.txt +0 -0
  45. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Zookeeper_2k_depth10.txt +0 -0
  46. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Zookeeper_2k_lex_order.txt +0 -0
  47. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Zookeeper_2k_raw_output.txt +0 -0
  48. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Zookeeper_2k_sim_thres_5.txt +0 -0
  49. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Zookeeper_2k_size_order.txt +0 -0
  50. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Zookeeper_2k_warn.txt +0 -0
  51. {gool-1.3.2 → gool-1.3.3}/tests/data/results/Zookeeper_unordered.txt +0 -0
  52. {gool-1.3.2 → gool-1.3.3}/uv.lock +0 -0
@@ -14,6 +14,21 @@
14
14
  "${workspaceFolder}/tests/data/log/Zookeeper_2k.log"
15
15
  ]
16
16
  },
17
+ {
18
+ "name": "gool sample crafted logs",
19
+ "type": "debugpy",
20
+ "request": "launch",
21
+ "module": "gool.log_clustering",
22
+ "args": [
23
+ "--cfg-file",
24
+ "${workspaceFolder}/tests/data/drain3.ini",
25
+ "--similarity-threshold",
26
+ "0.65",
27
+ "-m",
28
+ "13:00:00",
29
+ "${workspaceFolder}/tests/data/log/Crafted_logs.log"
30
+ ]
31
+ },
17
32
  {
18
33
  "name": "gool sample apache date/time filtered",
19
34
  "type": "debugpy",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gool
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: cli logs clustering
5
5
  Project-URL: Homepage, https://gitlab.com/godardo/gool
6
6
  Project-URL: Repository, https://gitlab.com/godardo/gool
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.3.2'
22
- __version_tuple__ = version_tuple = (1, 3, 2)
21
+ __version__ = version = '1.3.3'
22
+ __version_tuple__ = version_tuple = (1, 3, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -204,10 +204,16 @@ def guess_time_regexp_and_format_code(time_str: str) -> tuple[str, str] | None:
204
204
  Returns:
205
205
  A tuple containing the guessed regex pattern and format code.
206
206
  """
207
- time_regex = r"(\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?)"
208
- time_format = "%H:%M:%S.%f"
207
+ time_regex = r"(\d{2}:\d{2}:\d{2})"
208
+ time_format = "%H:%M:%S"
209
209
  regex = re.compile(time_regex)
210
210
  match = regex.fullmatch(time_str)
211
+
212
+ if not match:
213
+ time_regex = r"(\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?)"
214
+ time_format = "%H:%M:%S.%f"
215
+ regex = re.compile(time_regex)
216
+ match = regex.fullmatch(time_str)
211
217
  if not match:
212
218
  time_regex = r"(\d{2}:\d{2}:\d{2}(?:,\d{1,9})?)"
213
219
  time_format = "%H:%M:%S,%f"
@@ -218,9 +224,6 @@ def guess_time_regexp_and_format_code(time_str: str) -> tuple[str, str] | None:
218
224
  matched_time = match.group(1)
219
225
  with contextlib.suppress(ValueError):
220
226
  datetime.strptime(matched_time, time_format)
221
- return time_regex, time_format
222
- with contextlib.suppress(ValueError):
223
- time_format = "%H:%M:%S"
224
- datetime.strptime(matched_time, time_format)
225
- return time_regex, time_format
227
+ return r".*" + time_regex + r".*", time_format
228
+
226
229
  return None
@@ -171,6 +171,8 @@ def display_diff_baseline(
171
171
 
172
172
  def display_data_rich(list_data: list[str], header: str) -> None:
173
173
  table = Table(title=header, highlight=True)
174
+ min_width_empty_tab = 100
175
+ table.min_width = min_width_empty_tab
174
176
  table.add_column("Template", justify="left")
175
177
  for item in list_data:
176
178
  pattern = surrogate_non_printable(item)
@@ -81,6 +81,8 @@ class LogsMiner:
81
81
  self.time_max = time_max
82
82
  self.unordered_time = unordered_time
83
83
  self.cluster_char_sizes: Counter = Counter()
84
+ # misc attr
85
+ self.time_warn_displayed = False
84
86
 
85
87
  def get_cluster_data(self) -> list[ClusterResult]:
86
88
  """
@@ -134,13 +136,17 @@ class LogsMiner:
134
136
  def _extract_time_from_line(self, line: str) -> datetime | None:
135
137
  if not self.time_pattern_regexp or not self.time_format:
136
138
  return None
137
- match = self.time_pattern_regexp.search(line)
139
+ match = self.time_pattern_regexp.match(line)
138
140
  if match:
139
141
  time_str = match.group(1)
140
142
  try:
141
143
  return datetime.strptime(time_str, self.time_format)
142
144
  except ValueError:
143
- logging.warning("Failed to parse time '%s' in line: %s", time_str, line)
145
+ if not self.time_warn_displayed:
146
+ logging.warning(
147
+ "Failed to parse time '%s' in line: %s. This warning is only displayed once", time_str, line
148
+ )
149
+ self.time_warn_displayed = True
144
150
  return None
145
151
  else:
146
152
  logging.warning("Time pattern not found in line: %s", line)
@@ -166,8 +172,7 @@ class LogsMiner:
166
172
  return False
167
173
  line_time = self._extract_time_from_line(line)
168
174
  if line_time is None:
169
- logging.warning("Could not extract time from line, skipping time check: %s", line)
170
- return False
175
+ return True
171
176
  if self.time_min and line_time < self.time_min:
172
177
  return True
173
178
  if self.time_max and line_time > self.time_max:
@@ -0,0 +1,5 @@
1
+ 12:00:12.121 | INFO | first info 1234
2
+ 12:00:12.221 | INFO | first info
3
+ 12:00:14.121 | INFO | first info 8931
4
+ 12:00:14.121 | INFO | second info
5
+ 13:00:14.121 | ERROR | second info
@@ -0,0 +1,5 @@
1
+ Log Clusters
2
+ 1 - 0 - <TIME> | INFO | first info 8931
3
+ 1 - 0 - <TIME> | INFO | second info
4
+ 1 - 0 - <TIME> | ERROR | second info
5
+
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import pytest
8
8
 
9
+ from gool.arguments_parser import guess_time_regexp_and_format_code
9
10
  from gool.log_clustering import (
10
11
  Arguments,
11
12
  ErrorCode,
@@ -33,6 +34,20 @@ class TestMisc:
33
34
  def test_surrogate_non_printable(self) -> None:
34
35
  assert surrogate_non_printable("foo") == "foo"
35
36
 
37
+ @pytest.mark.parametrize(
38
+ ("time_str", "expected"),
39
+ [
40
+ ("15:04:05", (r".*(\d{2}:\d{2}:\d{2}).*", "%H:%M:%S")),
41
+ ("15:04:05.000", (r".*(\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?).*", "%H:%M:%S.%f")),
42
+ ("15:04:05,000", (r".*(\d{2}:\d{2}:\d{2}(?:,\d{1,9})?).*", "%H:%M:%S,%f")),
43
+ ],
44
+ )
45
+ def test_guess_time_regexp_and_format_code(self, time_str: str, expected: tuple[str, str]) -> None:
46
+ assert guess_time_regexp_and_format_code(time_str) == expected
47
+
48
+ def test_guess_time_regexp_and_format_code_invalid(self) -> None:
49
+ assert guess_time_regexp_and_format_code("not-a-time") is None
50
+
36
51
  def test_create_config_file_if_needed(self, tmp_path: Path) -> None:
37
52
 
38
53
  home_config = tmp_path / "drain3.ini"
@@ -183,7 +198,7 @@ class TestNormalOutput:
183
198
  "--time-format",
184
199
  "%a %b %d %H:%M:%S %Y",
185
200
  "--time-pattern",
186
- "(\\w{3} \\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2} \\d{4})",
201
+ "\\[(\\w{3} \\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2} \\d{4}).*",
187
202
  "--time-min",
188
203
  "Mon Dec 05 18:46:00 2005",
189
204
  "--time-max",
@@ -225,7 +240,7 @@ class TestNormalOutput:
225
240
  "--time-format",
226
241
  "%a %b %d %H:%M:%S %Y",
227
242
  "--time-pattern",
228
- "(\\w{3} \\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2} \\d{4})",
243
+ "\\[(\\w{3} \\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2} \\d{4}).*",
229
244
  "--time-min",
230
245
  "Mon Dec 05 00:00:00 2005",
231
246
  "--time-max",
@@ -279,6 +294,13 @@ class TestNormalOutput:
279
294
  cmd = ["--unordered_time", "--time-max", "15:37:00,0"]
280
295
  run_gool_on_file_from_cli_and_check(cmd, input_path, expected_path)
281
296
 
297
+ def test_time_filter_low_precision(self) -> None:
298
+ input_path = input_dir_path / "Crafted_logs.log"
299
+ print(input_path)
300
+ expected_path = output_dir_path / "Crafted_logs_time_120014.txt"
301
+ cmd = ["--time-min", "12:00:14"]
302
+ run_gool_on_file_from_cli_and_check(cmd, input_path, expected_path)
303
+
282
304
  def test_version_cli_outputs_one_line(self) -> None:
283
305
  original_argv = sys.argv.copy()
284
306
  try:
@@ -11,7 +11,7 @@ from gool.logs_miner import ExceptionLineAfterMaxTime, LogsMiner
11
11
  @pytest.fixture
12
12
  def logs_miner_with_time_config() -> Any:
13
13
  """LogsMiner instance with time pattern and format configured."""
14
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
14
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
15
15
  time_format = "%Y-%m-%d %H:%M:%S"
16
16
  with patch("gool.logs_miner.TemplateMiner"), patch("gool.logs_miner.create_drain3_cfg", return_value=Mock()):
17
17
  return LogsMiner(
@@ -40,7 +40,7 @@ class TestExtractTimeFromLine:
40
40
 
41
41
  def test_extract_time_when_no_format(self) -> None:
42
42
  """Should return None when time_format is not set."""
43
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
43
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
44
44
  with patch("gool.logs_miner.TemplateMiner"), patch("gool.logs_miner.create_drain3_cfg", return_value=Mock()):
45
45
  logs_miner = LogsMiner(
46
46
  drain3_cfg_file=Mock(),
@@ -90,7 +90,7 @@ class TestCheckLineOutTimeRange:
90
90
 
91
91
  def test_returns_false_when_no_time_format(self) -> None:
92
92
  """Should return False when time_format is not set."""
93
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
93
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
94
94
  with patch("gool.logs_miner.TemplateMiner"), patch("gool.logs_miner.create_drain3_cfg", return_value=Mock()):
95
95
  logs_miner = LogsMiner(
96
96
  drain3_cfg_file=Mock(),
@@ -102,16 +102,16 @@ class TestCheckLineOutTimeRange:
102
102
  result = logs_miner._check_line_out_time_range("2024-01-15 10:30:45 Some log")
103
103
  assert result is False
104
104
 
105
- def test_returns_false_when_time_cannot_be_extracted(self, logs_miner_with_time_config: Any) -> None:
106
- """Should return False and log warning when time cannot be extracted."""
105
+ def test_returns_true_when_time_cannot_be_extracted(self, logs_miner_with_time_config: Any) -> None:
106
+ """Should return True and log warning when time cannot be extracted."""
107
107
  with patch("gool.logs_miner.logging") as mock_logging:
108
108
  result = logs_miner_with_time_config._check_line_out_time_range("No time in this message")
109
- assert result is False
109
+ assert result is True
110
110
  mock_logging.warning.assert_called()
111
111
 
112
112
  def test_returns_true_when_time_before_min(self) -> None:
113
113
  """Should return True when line time is before time_min."""
114
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
114
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
115
115
  time_format = "%Y-%m-%d %H:%M:%S"
116
116
  time_min = datetime(2024, 1, 15, 12, 0, 0)
117
117
 
@@ -131,7 +131,7 @@ class TestCheckLineOutTimeRange:
131
131
 
132
132
  def test_returns_false_when_time_after_min(self) -> None:
133
133
  """Should return False when line time is after time_min."""
134
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
134
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
135
135
  time_format = "%Y-%m-%d %H:%M:%S"
136
136
  time_min = datetime(2024, 1, 15, 10, 0, 0)
137
137
 
@@ -151,7 +151,7 @@ class TestCheckLineOutTimeRange:
151
151
 
152
152
  def test_raises_exception_when_time_after_max_ordered(self) -> None:
153
153
  """Should raise ExceptionLineAfterMaxTime when time > time_max and unordered_time is False."""
154
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
154
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
155
155
  time_format = "%Y-%m-%d %H:%M:%S"
156
156
  time_max = datetime(2024, 1, 15, 10, 0, 0)
157
157
 
@@ -172,7 +172,7 @@ class TestCheckLineOutTimeRange:
172
172
 
173
173
  def test_returns_true_when_time_after_max_unordered(self) -> None:
174
174
  """Should return True when time > time_max and unordered_time is True."""
175
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
175
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
176
176
  time_format = "%Y-%m-%d %H:%M:%S"
177
177
  time_max = datetime(2024, 1, 15, 10, 0, 0)
178
178
 
@@ -193,7 +193,7 @@ class TestCheckLineOutTimeRange:
193
193
 
194
194
  def test_returns_false_when_time_before_max(self) -> None:
195
195
  """Should return False when line time is before time_max."""
196
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
196
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
197
197
  time_format = "%Y-%m-%d %H:%M:%S"
198
198
  time_max = datetime(2024, 1, 15, 12, 0, 0)
199
199
 
@@ -213,7 +213,7 @@ class TestCheckLineOutTimeRange:
213
213
 
214
214
  def test_returns_false_when_time_within_range(self) -> None:
215
215
  """Should return False when line time is within min and max range."""
216
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
216
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
217
217
  time_format = "%Y-%m-%d %H:%M:%S"
218
218
  time_min = datetime(2024, 1, 15, 10, 0, 0)
219
219
  time_max = datetime(2024, 1, 15, 12, 0, 0)
@@ -235,7 +235,7 @@ class TestCheckLineOutTimeRange:
235
235
 
236
236
  def test_returns_false_when_time_equal_to_min(self) -> None:
237
237
  """Should return False when line time equals time_min."""
238
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
238
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
239
239
  time_format = "%Y-%m-%d %H:%M:%S"
240
240
  time_min = datetime(2024, 1, 15, 10, 30, 45)
241
241
 
@@ -255,7 +255,7 @@ class TestCheckLineOutTimeRange:
255
255
 
256
256
  def test_returns_false_when_time_equal_to_max(self) -> None:
257
257
  """Should return False when line time equals time_max."""
258
- time_pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")
258
+ time_pattern = re.compile(r".*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*")
259
259
  time_format = "%Y-%m-%d %H:%M:%S"
260
260
  time_max = datetime(2024, 1, 15, 10, 30, 45)
261
261
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes