logreducer 3.5.1__tar.gz → 3.5.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.
- {logreducer-3.5.1 → logreducer-3.5.3}/PKG-INFO +19 -17
- {logreducer-3.5.1 → logreducer-3.5.3}/README.md +9 -5
- {logreducer-3.5.1 → logreducer-3.5.3}/pyproject.toml +35 -23
- {logreducer-3.5.1 → logreducer-3.5.3}/pyproject.toml.orig +47 -23
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/anomaly.py +6 -7
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/config.py +3 -1
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/core.py +65 -16
- logreducer-3.5.3/src/logreducer/logging_config.py +237 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/memory.py +10 -12
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/patterns.py +32 -30
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sinks.py +10 -1
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sql.py +1 -1
- logreducer-3.5.3/src/logreducer/template_mining.py +453 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/temporal.py +26 -42
- logreducer-3.5.1/src/logreducer/logging_config.py +0 -193
- {logreducer-3.5.1 → logreducer-3.5.3}/LICENSE +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/NOTICE +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/__init__.py +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/cli.py +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/clickhouse.py +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/kafka.py +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/py.typed +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sampling.py +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/sources.py +0 -0
- {logreducer-3.5.1 → logreducer-3.5.3}/src/logreducer/target.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logreducer
|
|
3
|
-
Version: 3.5.
|
|
3
|
+
Version: 3.5.3
|
|
4
4
|
Summary: Reduce GB-scale logs to a representative sample - streaming Python library and CLI with pattern mining and anomaly detection
|
|
5
5
|
Keywords: log,logging,reduction,analysis,pattern-extraction,memory-efficient,streaming,anomaly-detection
|
|
6
6
|
Author: HyperI Team
|
|
@@ -17,17 +17,15 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
18
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
19
|
Classifier: Operating System :: OS Independent
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist: confluent-kafka>=2.14.0 ; extra == 'kafka'
|
|
30
|
-
Requires-Dist: sqlalchemy>=2.0.30 ; extra == 'sql'
|
|
20
|
+
Requires-Dist: psutil>=7.2.2
|
|
21
|
+
Requires-Dist: numpy>=2.5.3
|
|
22
|
+
Requires-Dist: scikit-learn>=1.9.1
|
|
23
|
+
Requires-Dist: typer>=0.27.3
|
|
24
|
+
Requires-Dist: clickhouse-connect>=1.9.0 ; extra == 'clickhouse'
|
|
25
|
+
Requires-Dist: xxhash>=4.0.1 ; extra == 'enhanced'
|
|
26
|
+
Requires-Dist: datasketch>=2.0.0 ; extra == 'enhanced'
|
|
27
|
+
Requires-Dist: confluent-kafka>=2.15.1 ; extra == 'kafka'
|
|
28
|
+
Requires-Dist: sqlalchemy>=2.1.3 ; extra == 'sql'
|
|
31
29
|
Maintainer: HyperI Team
|
|
32
30
|
Maintainer-email: HyperI Team <dev@hyperi.io>
|
|
33
31
|
Requires-Python: >=3.14
|
|
@@ -337,8 +335,9 @@ reducer = logreducer.LogReducer(mode="hybrid", config=cfg)
|
|
|
337
335
|
# ...or let logreducer read your env cascade directly: prefixed beats bare.
|
|
338
336
|
cfg = logreducer.BigDialConfig.from_env("MYAPP", "LOGREDUCER") # MYAPP_MAX_MEMORY_GB wins
|
|
339
337
|
|
|
340
|
-
# 2. Logging: your app owns the
|
|
341
|
-
# its records flow through YOUR handlers,
|
|
338
|
+
# 2. Logging: your app owns the stdlib logging handlers; logreducer attaches
|
|
339
|
+
# nothing and its records flow through YOUR handlers, at YOUR level,
|
|
340
|
+
# formatted by YOUR standard.
|
|
342
341
|
logreducer.setup_logging(enable=True, own_sinks=False)
|
|
343
342
|
```
|
|
344
343
|
|
|
@@ -368,7 +367,7 @@ Logging is off by default. Enable it and pick a format via env vars:
|
|
|
368
367
|
LOG_LEVEL=DEBUG LOG_FORMAT=json logreducer app.log --log
|
|
369
368
|
```
|
|
370
369
|
|
|
371
|
-
`LOG_FORMAT=json` emits one JSON object per line for log aggregators; the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
|
|
370
|
+
Logging is the standard library `logging` module, under the `logreducer` logger. `LOG_FORMAT=json` emits one JSON object per line for log aggregators (keys `timestamp`, `level`, `target`, `function`, `line_number`, `message`, `fields`); the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
|
|
372
371
|
|
|
373
372
|
## Development
|
|
374
373
|
|
|
@@ -386,6 +385,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the toolchain, test layout (including
|
|
|
386
385
|
|
|
387
386
|
Copyright 2026 HYPERI PTY LIMITED.
|
|
388
387
|
|
|
388
|
+
Credits: template mining (`src/logreducer/template_mining.py`) is derived from [Drain3](https://github.com/logpai/Drain3), Copyright (c) 2020-2022 International Business Machines and the Drain3 project contributors, MIT licence. Drain3 builds on the Drain parser in LogPAI's [logparser](https://github.com/logpai/logparser). The module header carries Drain3's licence text in full.
|
|
389
|
+
|
|
389
390
|
## Context
|
|
390
391
|
|
|
391
392
|
Depth: [docs/architecture.md](docs/architecture.md).
|
|
@@ -406,6 +407,7 @@ it is given and drops the rest.
|
|
|
406
407
|
| `src/logreducer/sources.py`, `sinks.py` | the two structural protocols that ARE the integration surface, plus `FileSource` and `FileSink` |
|
|
407
408
|
| `src/logreducer/sql.py`, `clickhouse.py`, `kafka.py` | the optional adapters, one extra each |
|
|
408
409
|
| `src/logreducer/patterns.py`, `anomaly.py`, `temporal.py` | Drain3 mining and fuzzy dedup, Isolation Forest, time-window grouping |
|
|
410
|
+
| `src/logreducer/template_mining.py` | the Drain template miner, derived from Drain3 (credited under License) |
|
|
409
411
|
| `src/logreducer/memory.py` | `MemoryMonitor`, `BoundedDeduplicator`, the file read strategies |
|
|
410
412
|
| `src/logreducer/sampling.py`, `target.py` | dialect-aware SQL sampling, and the `reduce_to_target` batch loop |
|
|
411
413
|
| `src/logreducer/config.py` | `BigDialConfig`, the level presets, the `from_env` cascade |
|
|
@@ -438,7 +440,7 @@ Individually: `uv run ruff format`, `uv run ruff check --fix`,
|
|
|
438
440
|
| Assume a reused `LogReducer` starts clean | Nothing -- `_reset_components()` rebuilds them per run | The dedup seen-set, both Drain3 miners and the fuzzy LSH accumulate per line. Carried into the next run they drop every line as already-seen and return empty (`core.py`) |
|
|
439
441
|
| Share one deduplicator across hybrid's two passes | Rebuild it before the anomaly pass | The pattern pass consumes the seen-set, so the anomaly pass sees everything as seen and no anomalies survive. Hybrid silently degrades to pattern-only (`core.py`) |
|
|
440
442
|
| Assume an unknown config keyword is ignored | Read the `ValueError` and fix the name | A silently dropped override means a user's tuning quietly does nothing (`core.py`) |
|
|
441
|
-
|
|
|
443
|
+
| Assume every template is the final Drain3 one | Allow for a cluster the `max_clusters` cap evicted | `extract_patterns` re-reads each template from the live cluster once the feed ends, so a varying cluster carries its `<*>` slots and the priority boost counts them. A cluster LRU-evicted mid-feed is gone by then and keeps the template it had when first seen (`patterns.py`) |
|
|
442
444
|
| Hand-edit the version | Land a conventional commit and let semantic-release do it | The old regime wrote `VERSION` by hand and took five commits to get tagging working. CONTRIBUTING makes it a standing rule |
|
|
443
445
|
|
|
444
446
|
### Where this sits
|
|
@@ -451,7 +453,7 @@ One declared edge in `dfe-infra/suite.yaml`, and it runs the surprising way.
|
|
|
451
453
|
|
|
452
454
|
`dfe-stack suite --consumer logreducer` returns an empty edge list: no repo in
|
|
453
455
|
that graph is something this library depends on. The runtime dependencies
|
|
454
|
-
(
|
|
456
|
+
(psutil, numpy, scikit-learn, typer) are packages, declared in
|
|
455
457
|
`pyproject.toml`, which is where they belong.
|
|
456
458
|
|
|
457
459
|
The reason dfe-engine gives for not declaring the dependency -- pin it "once it
|
|
@@ -293,8 +293,9 @@ reducer = logreducer.LogReducer(mode="hybrid", config=cfg)
|
|
|
293
293
|
# ...or let logreducer read your env cascade directly: prefixed beats bare.
|
|
294
294
|
cfg = logreducer.BigDialConfig.from_env("MYAPP", "LOGREDUCER") # MYAPP_MAX_MEMORY_GB wins
|
|
295
295
|
|
|
296
|
-
# 2. Logging: your app owns the
|
|
297
|
-
# its records flow through YOUR handlers,
|
|
296
|
+
# 2. Logging: your app owns the stdlib logging handlers; logreducer attaches
|
|
297
|
+
# nothing and its records flow through YOUR handlers, at YOUR level,
|
|
298
|
+
# formatted by YOUR standard.
|
|
298
299
|
logreducer.setup_logging(enable=True, own_sinks=False)
|
|
299
300
|
```
|
|
300
301
|
|
|
@@ -324,7 +325,7 @@ Logging is off by default. Enable it and pick a format via env vars:
|
|
|
324
325
|
LOG_LEVEL=DEBUG LOG_FORMAT=json logreducer app.log --log
|
|
325
326
|
```
|
|
326
327
|
|
|
327
|
-
`LOG_FORMAT=json` emits one JSON object per line for log aggregators; the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
|
|
328
|
+
Logging is the standard library `logging` module, under the `logreducer` logger. `LOG_FORMAT=json` emits one JSON object per line for log aggregators (keys `timestamp`, `level`, `target`, `function`, `line_number`, `message`, `fields`); the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above.
|
|
328
329
|
|
|
329
330
|
## Development
|
|
330
331
|
|
|
@@ -342,6 +343,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the toolchain, test layout (including
|
|
|
342
343
|
|
|
343
344
|
Copyright 2026 HYPERI PTY LIMITED.
|
|
344
345
|
|
|
346
|
+
Credits: template mining (`src/logreducer/template_mining.py`) is derived from [Drain3](https://github.com/logpai/Drain3), Copyright (c) 2020-2022 International Business Machines and the Drain3 project contributors, MIT licence. Drain3 builds on the Drain parser in LogPAI's [logparser](https://github.com/logpai/logparser). The module header carries Drain3's licence text in full.
|
|
347
|
+
|
|
345
348
|
## Context
|
|
346
349
|
|
|
347
350
|
Depth: [docs/architecture.md](docs/architecture.md).
|
|
@@ -362,6 +365,7 @@ it is given and drops the rest.
|
|
|
362
365
|
| `src/logreducer/sources.py`, `sinks.py` | the two structural protocols that ARE the integration surface, plus `FileSource` and `FileSink` |
|
|
363
366
|
| `src/logreducer/sql.py`, `clickhouse.py`, `kafka.py` | the optional adapters, one extra each |
|
|
364
367
|
| `src/logreducer/patterns.py`, `anomaly.py`, `temporal.py` | Drain3 mining and fuzzy dedup, Isolation Forest, time-window grouping |
|
|
368
|
+
| `src/logreducer/template_mining.py` | the Drain template miner, derived from Drain3 (credited under License) |
|
|
365
369
|
| `src/logreducer/memory.py` | `MemoryMonitor`, `BoundedDeduplicator`, the file read strategies |
|
|
366
370
|
| `src/logreducer/sampling.py`, `target.py` | dialect-aware SQL sampling, and the `reduce_to_target` batch loop |
|
|
367
371
|
| `src/logreducer/config.py` | `BigDialConfig`, the level presets, the `from_env` cascade |
|
|
@@ -394,7 +398,7 @@ Individually: `uv run ruff format`, `uv run ruff check --fix`,
|
|
|
394
398
|
| Assume a reused `LogReducer` starts clean | Nothing -- `_reset_components()` rebuilds them per run | The dedup seen-set, both Drain3 miners and the fuzzy LSH accumulate per line. Carried into the next run they drop every line as already-seen and return empty (`core.py`) |
|
|
395
399
|
| Share one deduplicator across hybrid's two passes | Rebuild it before the anomaly pass | The pattern pass consumes the seen-set, so the anomaly pass sees everything as seen and no anomalies survive. Hybrid silently degrades to pattern-only (`core.py`) |
|
|
396
400
|
| Assume an unknown config keyword is ignored | Read the `ValueError` and fix the name | A silently dropped override means a user's tuning quietly does nothing (`core.py`) |
|
|
397
|
-
|
|
|
401
|
+
| Assume every template is the final Drain3 one | Allow for a cluster the `max_clusters` cap evicted | `extract_patterns` re-reads each template from the live cluster once the feed ends, so a varying cluster carries its `<*>` slots and the priority boost counts them. A cluster LRU-evicted mid-feed is gone by then and keeps the template it had when first seen (`patterns.py`) |
|
|
398
402
|
| Hand-edit the version | Land a conventional commit and let semantic-release do it | The old regime wrote `VERSION` by hand and took five commits to get tagging working. CONTRIBUTING makes it a standing rule |
|
|
399
403
|
|
|
400
404
|
### Where this sits
|
|
@@ -407,7 +411,7 @@ One declared edge in `dfe-infra/suite.yaml`, and it runs the surprising way.
|
|
|
407
411
|
|
|
408
412
|
`dfe-stack suite --consumer logreducer` returns an empty edge list: no repo in
|
|
409
413
|
that graph is something this library depends on. The runtime dependencies
|
|
410
|
-
(
|
|
414
|
+
(psutil, numpy, scikit-learn, typer) are packages, declared in
|
|
411
415
|
`pyproject.toml`, which is where they belong.
|
|
412
416
|
|
|
413
417
|
The reason dfe-engine gives for not declaring the dependency -- pin it "once it
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "logreducer"
|
|
7
|
-
version = "3.5.
|
|
7
|
+
version = "3.5.3"
|
|
8
8
|
description = "Reduce GB-scale logs to a representative sample - streaming Python library and CLI with pattern mining and anomaly detection"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -35,12 +35,10 @@ keywords = [
|
|
|
35
35
|
"anomaly-detection",
|
|
36
36
|
]
|
|
37
37
|
dependencies = [
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"scikit-learn>=1.5.0",
|
|
43
|
-
"typer>=0.25.0",
|
|
38
|
+
"psutil>=7.2.2",
|
|
39
|
+
"numpy>=2.5.3",
|
|
40
|
+
"scikit-learn>=1.9.1",
|
|
41
|
+
"typer>=0.27.3",
|
|
44
42
|
]
|
|
45
43
|
|
|
46
44
|
[[project.authors]]
|
|
@@ -53,12 +51,12 @@ email = "dev@hyperi.io"
|
|
|
53
51
|
|
|
54
52
|
[project.optional-dependencies]
|
|
55
53
|
enhanced = [
|
|
56
|
-
"xxhash>=
|
|
57
|
-
"datasketch>=
|
|
54
|
+
"xxhash>=4.0.1",
|
|
55
|
+
"datasketch>=2.0.0",
|
|
58
56
|
]
|
|
59
|
-
sql = ["sqlalchemy>=2.
|
|
60
|
-
clickhouse = ["clickhouse-connect>=1.0"]
|
|
61
|
-
kafka = ["confluent-kafka>=2.
|
|
57
|
+
sql = ["sqlalchemy>=2.1.3"]
|
|
58
|
+
clickhouse = ["clickhouse-connect>=1.9.0"]
|
|
59
|
+
kafka = ["confluent-kafka>=2.15.1"]
|
|
62
60
|
|
|
63
61
|
[project.urls]
|
|
64
62
|
Homepage = "https://github.com/hyperi-io/logreducer"
|
|
@@ -72,6 +70,7 @@ logreducer = "logreducer.cli:main"
|
|
|
72
70
|
|
|
73
71
|
[tool.uv]
|
|
74
72
|
index-strategy = "unsafe-best-match"
|
|
73
|
+
exclude-newer = "7 days"
|
|
75
74
|
override-dependencies = []
|
|
76
75
|
|
|
77
76
|
[[tool.uv.index]]
|
|
@@ -150,6 +149,7 @@ ignore = [
|
|
|
150
149
|
fixable = ["ALL"]
|
|
151
150
|
unfixable = []
|
|
152
151
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
152
|
+
external = ["V"]
|
|
153
153
|
|
|
154
154
|
[tool.ruff.lint.per-file-ignores]
|
|
155
155
|
"tests/**" = [
|
|
@@ -164,6 +164,15 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
|
164
164
|
"PT012",
|
|
165
165
|
"RUF043",
|
|
166
166
|
]
|
|
167
|
+
"src/**" = [
|
|
168
|
+
"D105",
|
|
169
|
+
"D107",
|
|
170
|
+
]
|
|
171
|
+
"src/logreducer/sampling.py" = ["S608"]
|
|
172
|
+
"src/logreducer/clickhouse.py" = ["S608"]
|
|
173
|
+
"src/logreducer/core.py" = ["S311"]
|
|
174
|
+
"src/logreducer/memory.py" = ["S311"]
|
|
175
|
+
"src/logreducer/target.py" = ["S311"]
|
|
167
176
|
|
|
168
177
|
[tool.ruff.lint.pydocstyle]
|
|
169
178
|
convention = "google"
|
|
@@ -260,40 +269,43 @@ exclude = [
|
|
|
260
269
|
"scripts/",
|
|
261
270
|
]
|
|
262
271
|
|
|
272
|
+
[tool.vulture]
|
|
273
|
+
min_confidence = 80
|
|
274
|
+
|
|
263
275
|
[tool.vermin]
|
|
264
276
|
targets = ["3.12"]
|
|
265
277
|
no_tips = true
|
|
266
278
|
|
|
267
279
|
[tool.hatch.build.targets.sdist]
|
|
268
280
|
exclude = [
|
|
269
|
-
"/
|
|
281
|
+
"/CURSOR.md",
|
|
270
282
|
"/GEMINI.md",
|
|
271
|
-
"
|
|
283
|
+
"/.cursor",
|
|
272
284
|
"/.windsurf",
|
|
273
|
-
"/CURSOR.md",
|
|
274
|
-
"/CLAUDE.md",
|
|
275
|
-
"/.claude",
|
|
276
285
|
"/.gemini",
|
|
286
|
+
"/CLAUDE.md",
|
|
277
287
|
"/.github/copilot-instructions.md",
|
|
288
|
+
"/.claude",
|
|
289
|
+
"/ci",
|
|
290
|
+
"/hyperi-ai",
|
|
278
291
|
"/STATE.md",
|
|
279
|
-
"/.cursor",
|
|
280
292
|
]
|
|
281
293
|
|
|
282
294
|
[dependency-groups]
|
|
283
295
|
dev = [
|
|
284
296
|
"pytest>=9.0.0",
|
|
285
297
|
"pytest-cov>=7.0.0",
|
|
286
|
-
"ruff>=0.
|
|
298
|
+
"ruff>=0.16.10",
|
|
287
299
|
"ty>=0.0.34",
|
|
288
300
|
"mypy>=2.0",
|
|
289
301
|
"bandit[toml]>=1.7.0",
|
|
290
302
|
"pip-audit>=2.6.0",
|
|
291
303
|
"vulture>=2.16",
|
|
292
304
|
"vermin>=1.6.0",
|
|
293
|
-
"sqlalchemy>=2.
|
|
294
|
-
"clickhouse-connect>=1.0",
|
|
295
|
-
"confluent-kafka>=2.
|
|
296
|
-
"testcontainers[clickhouse,kafka,postgres,mysql]>=4.
|
|
305
|
+
"sqlalchemy>=2.1.3",
|
|
306
|
+
"clickhouse-connect>=1.9.0",
|
|
307
|
+
"confluent-kafka>=2.15.1",
|
|
308
|
+
"testcontainers[clickhouse,kafka,postgres,mysql]>=4.15.0",
|
|
297
309
|
"psycopg[binary]>=3.2",
|
|
298
310
|
"pymysql>=1.1.0",
|
|
299
311
|
"python-dotenv>=1.0.0",
|
|
@@ -6,7 +6,7 @@ build-backend = "uv_build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "logreducer"
|
|
9
|
-
version = "3.5.
|
|
9
|
+
version = "3.5.3"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name = "HyperI Team", email = "dev@hyperi.io" },
|
|
12
12
|
]
|
|
@@ -43,27 +43,25 @@ keywords = [
|
|
|
43
43
|
"anomaly-detection",
|
|
44
44
|
]
|
|
45
45
|
dependencies = [
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"scikit-learn>=1.5.0",
|
|
51
|
-
"typer>=0.25.0",
|
|
46
|
+
"psutil>=7.2.2",
|
|
47
|
+
"numpy>=2.5.3",
|
|
48
|
+
"scikit-learn>=1.9.1",
|
|
49
|
+
"typer>=0.27.3",
|
|
52
50
|
]
|
|
53
51
|
|
|
54
52
|
[project.optional-dependencies]
|
|
55
53
|
enhanced = [
|
|
56
|
-
"xxhash>=
|
|
57
|
-
"datasketch>=
|
|
54
|
+
"xxhash>=4.0.1",
|
|
55
|
+
"datasketch>=2.0.0",
|
|
58
56
|
]
|
|
59
57
|
sql = [
|
|
60
|
-
"sqlalchemy>=2.
|
|
58
|
+
"sqlalchemy>=2.1.3",
|
|
61
59
|
]
|
|
62
60
|
clickhouse = [
|
|
63
|
-
"clickhouse-connect>=1.0",
|
|
61
|
+
"clickhouse-connect>=1.9.0",
|
|
64
62
|
]
|
|
65
63
|
kafka = [
|
|
66
|
-
"confluent-kafka>=2.
|
|
64
|
+
"confluent-kafka>=2.15.1",
|
|
67
65
|
]
|
|
68
66
|
|
|
69
67
|
[project.urls]
|
|
@@ -78,6 +76,7 @@ logreducer = "logreducer.cli:main"
|
|
|
78
76
|
|
|
79
77
|
[tool.uv]
|
|
80
78
|
index-strategy = "unsafe-best-match"
|
|
79
|
+
exclude-newer = "7 days"
|
|
81
80
|
override-dependencies = []
|
|
82
81
|
index = [
|
|
83
82
|
{ name = "pypi", url = "https://pypi.org/simple", default = true },
|
|
@@ -160,6 +159,9 @@ fixable = [
|
|
|
160
159
|
]
|
|
161
160
|
unfixable = []
|
|
162
161
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
162
|
+
external = [
|
|
163
|
+
"V",
|
|
164
|
+
]
|
|
163
165
|
|
|
164
166
|
[tool.ruff.lint.per-file-ignores]
|
|
165
167
|
"tests/**" = [
|
|
@@ -174,6 +176,25 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
|
174
176
|
"PT012",
|
|
175
177
|
"RUF043",
|
|
176
178
|
]
|
|
179
|
+
"src/**" = [
|
|
180
|
+
"D105",
|
|
181
|
+
"D107",
|
|
182
|
+
]
|
|
183
|
+
"src/logreducer/sampling.py" = [
|
|
184
|
+
"S608",
|
|
185
|
+
]
|
|
186
|
+
"src/logreducer/clickhouse.py" = [
|
|
187
|
+
"S608",
|
|
188
|
+
]
|
|
189
|
+
"src/logreducer/core.py" = [
|
|
190
|
+
"S311",
|
|
191
|
+
]
|
|
192
|
+
"src/logreducer/memory.py" = [
|
|
193
|
+
"S311",
|
|
194
|
+
]
|
|
195
|
+
"src/logreducer/target.py" = [
|
|
196
|
+
"S311",
|
|
197
|
+
]
|
|
177
198
|
|
|
178
199
|
[tool.ruff.lint.pydocstyle]
|
|
179
200
|
convention = "google"
|
|
@@ -278,6 +299,9 @@ exclude = [
|
|
|
278
299
|
"scripts/",
|
|
279
300
|
]
|
|
280
301
|
|
|
302
|
+
[tool.vulture]
|
|
303
|
+
min_confidence = 80
|
|
304
|
+
|
|
281
305
|
[tool.vermin]
|
|
282
306
|
targets = [
|
|
283
307
|
"3.12",
|
|
@@ -286,34 +310,34 @@ no_tips = true
|
|
|
286
310
|
|
|
287
311
|
[tool.hatch.build.targets.sdist]
|
|
288
312
|
exclude = [
|
|
289
|
-
"/
|
|
313
|
+
"/CURSOR.md",
|
|
290
314
|
"/GEMINI.md",
|
|
291
|
-
"
|
|
315
|
+
"/.cursor",
|
|
292
316
|
"/.windsurf",
|
|
293
|
-
"/CURSOR.md",
|
|
294
|
-
"/CLAUDE.md",
|
|
295
|
-
"/.claude",
|
|
296
317
|
"/.gemini",
|
|
318
|
+
"/CLAUDE.md",
|
|
297
319
|
"/.github/copilot-instructions.md",
|
|
320
|
+
"/.claude",
|
|
321
|
+
"/ci",
|
|
322
|
+
"/hyperi-ai",
|
|
298
323
|
"/STATE.md",
|
|
299
|
-
"/.cursor",
|
|
300
324
|
]
|
|
301
325
|
|
|
302
326
|
[dependency-groups]
|
|
303
327
|
dev = [
|
|
304
328
|
"pytest>=9.0.0",
|
|
305
329
|
"pytest-cov>=7.0.0",
|
|
306
|
-
"ruff>=0.
|
|
330
|
+
"ruff>=0.16.10",
|
|
307
331
|
"ty>=0.0.34",
|
|
308
332
|
"mypy>=2.0",
|
|
309
333
|
"bandit[toml]>=1.7.0",
|
|
310
334
|
"pip-audit>=2.6.0",
|
|
311
335
|
"vulture>=2.16",
|
|
312
336
|
"vermin>=1.6.0",
|
|
313
|
-
"sqlalchemy>=2.
|
|
314
|
-
"clickhouse-connect>=1.0",
|
|
315
|
-
"confluent-kafka>=2.
|
|
316
|
-
"testcontainers[clickhouse,kafka,postgres,mysql]>=4.
|
|
337
|
+
"sqlalchemy>=2.1.3",
|
|
338
|
+
"clickhouse-connect>=1.9.0",
|
|
339
|
+
"confluent-kafka>=2.15.1",
|
|
340
|
+
"testcontainers[clickhouse,kafka,postgres,mysql]>=4.15.0",
|
|
317
341
|
"psycopg[binary]>=3.2",
|
|
318
342
|
"pymysql>=1.1.0",
|
|
319
343
|
"python-dotenv>=1.0.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Anomaly detection utilities
|
|
3
|
-
"""
|
|
1
|
+
"""Anomaly detection utilities."""
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
6
|
|
|
7
7
|
# Try optional imports
|
|
8
8
|
try:
|
|
@@ -15,7 +15,7 @@ except ImportError:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class AnomalyDetector:
|
|
18
|
-
"""Detect anomalous log lines"""
|
|
18
|
+
"""Detect anomalous log lines."""
|
|
19
19
|
|
|
20
20
|
def __init__(self, contamination: float = 0.1):
|
|
21
21
|
self.contamination = contamination
|
|
@@ -25,8 +25,7 @@ class AnomalyDetector:
|
|
|
25
25
|
logger.warning("scikit-learn not available, anomaly detection disabled")
|
|
26
26
|
|
|
27
27
|
def detect_anomalies(self, lines: list[str]) -> tuple[list[str], list[str]]:
|
|
28
|
-
"""
|
|
29
|
-
Separate anomalous and normal lines
|
|
28
|
+
"""Separate anomalous and normal lines.
|
|
30
29
|
|
|
31
30
|
Returns:
|
|
32
31
|
(anomalous_lines, normal_lines)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Configuration and tuning parameters for LogReducer."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
import types
|
|
5
6
|
import typing
|
|
@@ -7,7 +8,8 @@ from dataclasses import dataclass, fields
|
|
|
7
8
|
from enum import Enum
|
|
8
9
|
|
|
9
10
|
import psutil
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class ProcessingLevel(Enum):
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Core LogReducer implementation
|
|
3
|
-
"""
|
|
1
|
+
"""Core LogReducer implementation."""
|
|
4
2
|
|
|
5
3
|
import json
|
|
6
4
|
import os
|
|
@@ -10,7 +8,7 @@ from dataclasses import replace
|
|
|
10
8
|
from datetime import datetime
|
|
11
9
|
from enum import Enum
|
|
12
10
|
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Literal, overload
|
|
14
12
|
|
|
15
13
|
from .anomaly import AnomalyDetector
|
|
16
14
|
from .config import BigDialConfig, OutputFormat, ProcessingLevel, ProcessingMode, get_preset_config
|
|
@@ -24,8 +22,7 @@ from .temporal import TemporalProcessor
|
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
class LogReducer:
|
|
27
|
-
"""
|
|
28
|
-
Main log reduction class
|
|
25
|
+
"""Main log reduction class.
|
|
29
26
|
|
|
30
27
|
Example:
|
|
31
28
|
reducer = LogReducer(level="enhanced")
|
|
@@ -41,8 +38,7 @@ class LogReducer:
|
|
|
41
38
|
config: BigDialConfig | None = None,
|
|
42
39
|
**kwargs: Any,
|
|
43
40
|
) -> None:
|
|
44
|
-
"""
|
|
45
|
-
Initialize LogReducer
|
|
41
|
+
"""Initialize LogReducer.
|
|
46
42
|
|
|
47
43
|
Args:
|
|
48
44
|
level: Processing level (standard/enhanced/maximum)
|
|
@@ -134,13 +130,41 @@ class LogReducer:
|
|
|
134
130
|
else:
|
|
135
131
|
self.temporal_processor = None
|
|
136
132
|
|
|
133
|
+
@overload
|
|
134
|
+
def reduce(
|
|
135
|
+
self,
|
|
136
|
+
source: Source,
|
|
137
|
+
output_file: str | None = None,
|
|
138
|
+
return_metadata: Literal[False] = False,
|
|
139
|
+
sink: Sink | None = None,
|
|
140
|
+
) -> list[str]: ...
|
|
141
|
+
|
|
142
|
+
@overload
|
|
143
|
+
def reduce(
|
|
144
|
+
self,
|
|
145
|
+
source: Source,
|
|
146
|
+
output_file: str | None = None,
|
|
147
|
+
*,
|
|
148
|
+
return_metadata: Literal[True],
|
|
149
|
+
sink: Sink | None = None,
|
|
150
|
+
) -> dict[str, Any]: ...
|
|
151
|
+
|
|
152
|
+
@overload
|
|
137
153
|
def reduce(
|
|
138
154
|
self,
|
|
139
155
|
source: Source,
|
|
140
156
|
output_file: str | None = None,
|
|
141
157
|
return_metadata: bool = False,
|
|
142
158
|
sink: Sink | None = None,
|
|
143
|
-
) -> list[str] | dict:
|
|
159
|
+
) -> list[str] | dict[str, Any]: ...
|
|
160
|
+
|
|
161
|
+
def reduce(
|
|
162
|
+
self,
|
|
163
|
+
source: Source,
|
|
164
|
+
output_file: str | None = None,
|
|
165
|
+
return_metadata: bool = False,
|
|
166
|
+
sink: Sink | None = None,
|
|
167
|
+
) -> list[str] | dict[str, Any]:
|
|
144
168
|
"""Reduce a source of log lines to a representative sample.
|
|
145
169
|
|
|
146
170
|
This is the core entry point: it operates on an abstraction, not on IO.
|
|
@@ -241,12 +265,37 @@ class LogReducer:
|
|
|
241
265
|
}
|
|
242
266
|
return result_lines
|
|
243
267
|
|
|
268
|
+
@overload
|
|
269
|
+
def process_file(
|
|
270
|
+
self,
|
|
271
|
+
input_file: str,
|
|
272
|
+
output_file: str | None = None,
|
|
273
|
+
return_metadata: Literal[False] = False,
|
|
274
|
+
) -> list[str]: ...
|
|
275
|
+
|
|
276
|
+
@overload
|
|
277
|
+
def process_file(
|
|
278
|
+
self,
|
|
279
|
+
input_file: str,
|
|
280
|
+
output_file: str | None = None,
|
|
281
|
+
*,
|
|
282
|
+
return_metadata: Literal[True],
|
|
283
|
+
) -> dict[str, Any]: ...
|
|
284
|
+
|
|
285
|
+
@overload
|
|
286
|
+
def process_file(
|
|
287
|
+
self,
|
|
288
|
+
input_file: str,
|
|
289
|
+
output_file: str | None = None,
|
|
290
|
+
return_metadata: bool = False,
|
|
291
|
+
) -> list[str] | dict[str, Any]: ...
|
|
292
|
+
|
|
244
293
|
def process_file(
|
|
245
294
|
self,
|
|
246
295
|
input_file: str,
|
|
247
296
|
output_file: str | None = None,
|
|
248
297
|
return_metadata: bool = False,
|
|
249
|
-
) -> list[str] | dict:
|
|
298
|
+
) -> list[str] | dict[str, Any]:
|
|
250
299
|
"""Reduce a log file - convenience wrapper over reduce() + FileSource.
|
|
251
300
|
|
|
252
301
|
Args:
|
|
@@ -302,7 +351,7 @@ class LogReducer:
|
|
|
302
351
|
return unique_lines
|
|
303
352
|
|
|
304
353
|
def _process_anomaly_mode(self, source: Source) -> list[str]:
|
|
305
|
-
"""Process using anomaly detection"""
|
|
354
|
+
"""Process using anomaly detection."""
|
|
306
355
|
if not self.anomaly_detector:
|
|
307
356
|
if self.config.enable_logging:
|
|
308
357
|
self.logger.warning("Anomaly detector not available, falling back to pattern mode")
|
|
@@ -329,7 +378,7 @@ class LogReducer:
|
|
|
329
378
|
return result
|
|
330
379
|
|
|
331
380
|
def _process_temporal_mode(self, source: Source) -> list[str]:
|
|
332
|
-
"""Process using temporal analysis"""
|
|
381
|
+
"""Process using temporal analysis."""
|
|
333
382
|
if not self.temporal_processor:
|
|
334
383
|
if self.config.enable_logging:
|
|
335
384
|
self.logger.warning("Temporal processor not available, falling back to pattern mode")
|
|
@@ -356,7 +405,7 @@ class LogReducer:
|
|
|
356
405
|
return result
|
|
357
406
|
|
|
358
407
|
def _process_hybrid_mode(self, source: Source) -> list[str]:
|
|
359
|
-
"""Process using combined approach"""
|
|
408
|
+
"""Process using combined approach."""
|
|
360
409
|
if self.config.enable_logging:
|
|
361
410
|
self.logger.info("Hybrid mode: combining pattern and anomaly detection")
|
|
362
411
|
|
|
@@ -400,7 +449,7 @@ class LogReducer:
|
|
|
400
449
|
}
|
|
401
450
|
|
|
402
451
|
def _save_output(self, lines: list[str], output_file: str) -> None:
|
|
403
|
-
"""Save output to file in specified format"""
|
|
452
|
+
"""Save output to file in specified format."""
|
|
404
453
|
output_path = Path(output_file)
|
|
405
454
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
406
455
|
|
|
@@ -453,7 +502,7 @@ class LogReducer:
|
|
|
453
502
|
self.logger.info(f"Metadata saved to {meta_file}")
|
|
454
503
|
|
|
455
504
|
def _print_summary(self) -> None:
|
|
456
|
-
"""Log processing summary"""
|
|
505
|
+
"""Log processing summary."""
|
|
457
506
|
self.logger.info("\n" + "=" * 60)
|
|
458
507
|
self.logger.info("LOG REDUCTION SUMMARY")
|
|
459
508
|
self.logger.info("=" * 60)
|
|
@@ -474,7 +523,7 @@ class LogReducer:
|
|
|
474
523
|
self.logger.info("=" * 60)
|
|
475
524
|
|
|
476
525
|
def estimate_processing(self, file_path: str) -> dict:
|
|
477
|
-
"""Estimate processing requirements before running"""
|
|
526
|
+
"""Estimate processing requirements before running."""
|
|
478
527
|
file_size = os.path.getsize(file_path)
|
|
479
528
|
file_size_gb = file_size / (1024**3)
|
|
480
529
|
|