nm-tool-forge 0.1.0__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.
- nm_tool_forge-0.1.0/LICENSE +21 -0
- nm_tool_forge-0.1.0/PKG-INFO +198 -0
- nm_tool_forge-0.1.0/README.md +167 -0
- nm_tool_forge-0.1.0/pyproject.toml +67 -0
- nm_tool_forge-0.1.0/setup.cfg +4 -0
- nm_tool_forge-0.1.0/src/loganalysis/__init__.py +16 -0
- nm_tool_forge-0.1.0/src/loganalysis/__main__.py +5 -0
- nm_tool_forge-0.1.0/src/loganalysis/analysis.py +175 -0
- nm_tool_forge-0.1.0/src/loganalysis/cli.py +88 -0
- nm_tool_forge-0.1.0/src/loganalysis/constants.py +126 -0
- nm_tool_forge-0.1.0/src/loganalysis/converters.py +150 -0
- nm_tool_forge-0.1.0/src/loganalysis/csv_export.py +18 -0
- nm_tool_forge-0.1.0/src/loganalysis/encoding.py +22 -0
- nm_tool_forge-0.1.0/src/loganalysis/filesystem.py +26 -0
- nm_tool_forge-0.1.0/src/loganalysis/models.py +63 -0
- nm_tool_forge-0.1.0/src/loganalysis/normalization.py +97 -0
- nm_tool_forge-0.1.0/src/loganalysis/parsing.py +69 -0
- nm_tool_forge-0.1.0/src/loganalysis/report_html.py +378 -0
- nm_tool_forge-0.1.0/src/loganalysis/report_markdown.py +209 -0
- nm_tool_forge-0.1.0/src/loganalysis/report_models.py +31 -0
- nm_tool_forge-0.1.0/src/loganalysis/report_pdf.py +74 -0
- nm_tool_forge-0.1.0/src/loganalysis/selftest.py +58 -0
- nm_tool_forge-0.1.0/src/nm_tool_forge.egg-info/PKG-INFO +198 -0
- nm_tool_forge-0.1.0/src/nm_tool_forge.egg-info/SOURCES.txt +31 -0
- nm_tool_forge-0.1.0/src/nm_tool_forge.egg-info/dependency_links.txt +1 -0
- nm_tool_forge-0.1.0/src/nm_tool_forge.egg-info/entry_points.txt +3 -0
- nm_tool_forge-0.1.0/src/nm_tool_forge.egg-info/requires.txt +10 -0
- nm_tool_forge-0.1.0/src/nm_tool_forge.egg-info/top_level.txt +1 -0
- nm_tool_forge-0.1.0/tests/test_analysis.py +53 -0
- nm_tool_forge-0.1.0/tests/test_normalization.py +23 -0
- nm_tool_forge-0.1.0/tests/test_parsing.py +38 -0
- nm_tool_forge-0.1.0/tests/test_report_html.py +42 -0
- nm_tool_forge-0.1.0/tests/test_report_markdown.py +44 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nm-tool-forge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Analyze MigMan log files and generate aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
5
|
+
Author-email: Stefan Ewald <s.ew@outlook.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jack736-ui/migman_log
|
|
8
|
+
Project-URL: Issues, https://github.com/Jack736-ui/migman_log/issues
|
|
9
|
+
Keywords: migman,logs,analysis,reporting,csv,markdown,pdf
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: chardet>=5.0
|
|
23
|
+
Provides-Extra: pdf
|
|
24
|
+
Requires-Dist: weasyprint>=62; extra == "pdf"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
28
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.11; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# nm-tool-forge
|
|
33
|
+
|
|
34
|
+
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
35
|
+
|
|
36
|
+
The project uses a package-ready `src` layout. The legacy `log_analysis.py` file remains available as a thin compatibility entry point for older local setups.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- Parse logical log entries from multi-line text logs
|
|
41
|
+
- Normalize recurring error patterns for better aggregation
|
|
42
|
+
- Generate aggregated CSV reports
|
|
43
|
+
- Generate Markdown summary reports
|
|
44
|
+
- Optionally convert reports to HTML and PDF
|
|
45
|
+
- Keep a backup copy of analyzed log files
|
|
46
|
+
- Run built-in self-tests from the CLI
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Basic installation from a local checkout:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
python -m pip install .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Installation with optional PDF support and developer tools:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
python -m pip install .[pdf,dev]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Command-line usage
|
|
63
|
+
|
|
64
|
+
After installation, both entry points are available:
|
|
65
|
+
|
|
66
|
+
```powershell
|
|
67
|
+
python -m loganalysis --help
|
|
68
|
+
loganalysis --help
|
|
69
|
+
nm-tool-forge --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Typical analysis run:
|
|
73
|
+
|
|
74
|
+
```powershell
|
|
75
|
+
nm-tool-forge --logs-dir logs --out-dir log_analyse_out
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Analysis with HTML/PDF conversion:
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
nm-tool-forge --logs-dir logs --out-dir log_analyse_out --convert
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Self-test mode:
|
|
85
|
+
|
|
86
|
+
```powershell
|
|
87
|
+
python -m loganalysis --self-test
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Legacy compatibility call:
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
python .\log_analysis.py --convert
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Supported CLI options
|
|
97
|
+
|
|
98
|
+
- `--logs-dir`
|
|
99
|
+
- `--out-dir`
|
|
100
|
+
- `--backup-dir`
|
|
101
|
+
- `--top-examples`
|
|
102
|
+
- `--convert`
|
|
103
|
+
- `--self-test`
|
|
104
|
+
|
|
105
|
+
## Library usage
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
|
|
110
|
+
from loganalysis import (
|
|
111
|
+
analyze_file,
|
|
112
|
+
convert_report_md_to_html_pdf,
|
|
113
|
+
iter_logical_entries,
|
|
114
|
+
normalize_message,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
result = analyze_file(Path("logs/app.txt"))
|
|
118
|
+
print(result["norm_counts"])
|
|
119
|
+
|
|
120
|
+
print(normalize_message(
|
|
121
|
+
'Conversion: X =3100110. 138 The record was not found in table "Teile".'
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
for entry in iter_logical_entries(Path("logs/app.txt")):
|
|
125
|
+
print(entry)
|
|
126
|
+
|
|
127
|
+
convert_report_md_to_html_pdf(
|
|
128
|
+
Path("log_analyse_out/report.md"),
|
|
129
|
+
Path("log_analyse_out/report.html"),
|
|
130
|
+
Path("log_analyse_out/report.pdf"),
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Project structure
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
.
|
|
138
|
+
├─ pyproject.toml
|
|
139
|
+
├─ src/loganalysis/
|
|
140
|
+
├─ tests/
|
|
141
|
+
├─ docs/
|
|
142
|
+
└─ log_analysis.py
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Important modules:
|
|
146
|
+
|
|
147
|
+
- `analysis.py` - file-level and overall aggregation
|
|
148
|
+
- `parsing.py` - logical entry detection and parsing
|
|
149
|
+
- `normalization.py` - message normalization
|
|
150
|
+
- `report_markdown.py` - Markdown report model and rendering
|
|
151
|
+
- `report_html.py` - HTML/CSS rendering
|
|
152
|
+
- `report_pdf.py` - PDF engine selection and fallback handling
|
|
153
|
+
- `converters.py` - Markdown-to-HTML/PDF conversion
|
|
154
|
+
- `cli.py` - command-line entry point
|
|
155
|
+
|
|
156
|
+
## HTML/PDF conversion
|
|
157
|
+
|
|
158
|
+
Report conversion is intentionally optional:
|
|
159
|
+
|
|
160
|
+
- `report.md` remains the primary human-readable output
|
|
161
|
+
- `report.html` is generated from the internal report model
|
|
162
|
+
- `report.pdf` is created when supported PDF tooling is available
|
|
163
|
+
|
|
164
|
+
PDF engine preference order:
|
|
165
|
+
|
|
166
|
+
1. `weasyprint`
|
|
167
|
+
2. `wkhtmltopdf`
|
|
168
|
+
3. `pandoc` + `xelatex` or `pdflatex`
|
|
169
|
+
|
|
170
|
+
If no supported PDF engine is available, the analysis still succeeds and generates Markdown and HTML output.
|
|
171
|
+
|
|
172
|
+
Windows-specific setup notes:
|
|
173
|
+
|
|
174
|
+
- `docs/install_gtk_weasyprint_windows.md`
|
|
175
|
+
- `docs/install_xelatex_windows.md`
|
|
176
|
+
|
|
177
|
+
## Tests
|
|
178
|
+
|
|
179
|
+
```powershell
|
|
180
|
+
pytest
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Local build
|
|
184
|
+
|
|
185
|
+
```powershell
|
|
186
|
+
python -m build
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Expected artifacts:
|
|
190
|
+
|
|
191
|
+
- `dist/*.tar.gz`
|
|
192
|
+
- `dist/*.whl`
|
|
193
|
+
|
|
194
|
+
## Notes
|
|
195
|
+
|
|
196
|
+
The package name on PyPI/TestPyPI is `nm-tool-forge`, while the current Python import package remains `loganalysis`.
|
|
197
|
+
|
|
198
|
+
This keeps the first public release small and low-risk. A later follow-up release can still rename the import package if desired.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# nm-tool-forge
|
|
2
|
+
|
|
3
|
+
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
4
|
+
|
|
5
|
+
The project uses a package-ready `src` layout. The legacy `log_analysis.py` file remains available as a thin compatibility entry point for older local setups.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Parse logical log entries from multi-line text logs
|
|
10
|
+
- Normalize recurring error patterns for better aggregation
|
|
11
|
+
- Generate aggregated CSV reports
|
|
12
|
+
- Generate Markdown summary reports
|
|
13
|
+
- Optionally convert reports to HTML and PDF
|
|
14
|
+
- Keep a backup copy of analyzed log files
|
|
15
|
+
- Run built-in self-tests from the CLI
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Basic installation from a local checkout:
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
python -m pip install .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Installation with optional PDF support and developer tools:
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
python -m pip install .[pdf,dev]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Command-line usage
|
|
32
|
+
|
|
33
|
+
After installation, both entry points are available:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
python -m loganalysis --help
|
|
37
|
+
loganalysis --help
|
|
38
|
+
nm-tool-forge --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Typical analysis run:
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
nm-tool-forge --logs-dir logs --out-dir log_analyse_out
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Analysis with HTML/PDF conversion:
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
nm-tool-forge --logs-dir logs --out-dir log_analyse_out --convert
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Self-test mode:
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
python -m loganalysis --self-test
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Legacy compatibility call:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
python .\log_analysis.py --convert
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Supported CLI options
|
|
66
|
+
|
|
67
|
+
- `--logs-dir`
|
|
68
|
+
- `--out-dir`
|
|
69
|
+
- `--backup-dir`
|
|
70
|
+
- `--top-examples`
|
|
71
|
+
- `--convert`
|
|
72
|
+
- `--self-test`
|
|
73
|
+
|
|
74
|
+
## Library usage
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from pathlib import Path
|
|
78
|
+
|
|
79
|
+
from loganalysis import (
|
|
80
|
+
analyze_file,
|
|
81
|
+
convert_report_md_to_html_pdf,
|
|
82
|
+
iter_logical_entries,
|
|
83
|
+
normalize_message,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
result = analyze_file(Path("logs/app.txt"))
|
|
87
|
+
print(result["norm_counts"])
|
|
88
|
+
|
|
89
|
+
print(normalize_message(
|
|
90
|
+
'Conversion: X =3100110. 138 The record was not found in table "Teile".'
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
for entry in iter_logical_entries(Path("logs/app.txt")):
|
|
94
|
+
print(entry)
|
|
95
|
+
|
|
96
|
+
convert_report_md_to_html_pdf(
|
|
97
|
+
Path("log_analyse_out/report.md"),
|
|
98
|
+
Path("log_analyse_out/report.html"),
|
|
99
|
+
Path("log_analyse_out/report.pdf"),
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Project structure
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
.
|
|
107
|
+
├─ pyproject.toml
|
|
108
|
+
├─ src/loganalysis/
|
|
109
|
+
├─ tests/
|
|
110
|
+
├─ docs/
|
|
111
|
+
└─ log_analysis.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Important modules:
|
|
115
|
+
|
|
116
|
+
- `analysis.py` - file-level and overall aggregation
|
|
117
|
+
- `parsing.py` - logical entry detection and parsing
|
|
118
|
+
- `normalization.py` - message normalization
|
|
119
|
+
- `report_markdown.py` - Markdown report model and rendering
|
|
120
|
+
- `report_html.py` - HTML/CSS rendering
|
|
121
|
+
- `report_pdf.py` - PDF engine selection and fallback handling
|
|
122
|
+
- `converters.py` - Markdown-to-HTML/PDF conversion
|
|
123
|
+
- `cli.py` - command-line entry point
|
|
124
|
+
|
|
125
|
+
## HTML/PDF conversion
|
|
126
|
+
|
|
127
|
+
Report conversion is intentionally optional:
|
|
128
|
+
|
|
129
|
+
- `report.md` remains the primary human-readable output
|
|
130
|
+
- `report.html` is generated from the internal report model
|
|
131
|
+
- `report.pdf` is created when supported PDF tooling is available
|
|
132
|
+
|
|
133
|
+
PDF engine preference order:
|
|
134
|
+
|
|
135
|
+
1. `weasyprint`
|
|
136
|
+
2. `wkhtmltopdf`
|
|
137
|
+
3. `pandoc` + `xelatex` or `pdflatex`
|
|
138
|
+
|
|
139
|
+
If no supported PDF engine is available, the analysis still succeeds and generates Markdown and HTML output.
|
|
140
|
+
|
|
141
|
+
Windows-specific setup notes:
|
|
142
|
+
|
|
143
|
+
- `docs/install_gtk_weasyprint_windows.md`
|
|
144
|
+
- `docs/install_xelatex_windows.md`
|
|
145
|
+
|
|
146
|
+
## Tests
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
pytest
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Local build
|
|
153
|
+
|
|
154
|
+
```powershell
|
|
155
|
+
python -m build
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Expected artifacts:
|
|
159
|
+
|
|
160
|
+
- `dist/*.tar.gz`
|
|
161
|
+
- `dist/*.whl`
|
|
162
|
+
|
|
163
|
+
## Notes
|
|
164
|
+
|
|
165
|
+
The package name on PyPI/TestPyPI is `nm-tool-forge`, while the current Python import package remains `loganalysis`.
|
|
166
|
+
|
|
167
|
+
This keeps the first public release small and low-risk. A later follow-up release can still rename the import package if desired.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nm-tool-forge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Analyze MigMan log files and generate aggregated CSV, Markdown, HTML, and optional PDF reports."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Stefan Ewald", email = "s.ew@outlook.de" }
|
|
15
|
+
]
|
|
16
|
+
keywords = ["migman", "logs", "analysis", "reporting", "csv", "markdown", "pdf"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Topic :: Utilities",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
dependencies = [
|
|
30
|
+
"chardet>=5.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
pdf = [
|
|
35
|
+
"weasyprint>=62",
|
|
36
|
+
]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.0",
|
|
39
|
+
"build>=1.2",
|
|
40
|
+
"twine>=5.0",
|
|
41
|
+
"ruff>=0.11",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/Jack736-ui/migman_log"
|
|
46
|
+
Issues = "https://github.com/Jack736-ui/migman_log/issues"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
nm-tool-forge = "loganalysis.cli:main"
|
|
50
|
+
loganalysis = "loganalysis.cli:main"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools]
|
|
53
|
+
package-dir = { "" = "src" }
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
addopts = "--basetemp=tests_tmp"
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 120
|
|
64
|
+
target-version = "py310"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .analysis import analyze_file, run_analysis
|
|
4
|
+
from .converters import convert_report_md_to_html_pdf
|
|
5
|
+
from .normalization import normalize_message
|
|
6
|
+
from .parsing import iter_logical_entries
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"analyze_file",
|
|
10
|
+
"convert_report_md_to_html_pdf",
|
|
11
|
+
"iter_logical_entries",
|
|
12
|
+
"normalize_message",
|
|
13
|
+
"run_analysis",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter, defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .constants import DEFAULT_OUT_DIR, DEFAULT_TOP_EXAMPLES, EXIT_NO_LOG_FILES
|
|
7
|
+
from .csv_export import write_csv
|
|
8
|
+
from .encoding import count_physical_lines, detect_encoding
|
|
9
|
+
from .filesystem import backup_file, ensure_dir
|
|
10
|
+
from .models import AnalysisConfig, AnalysisRunResult, AnalysisSummary, FileAnalysis, MessageKey
|
|
11
|
+
from .normalization import normalize_message
|
|
12
|
+
from .parsing import iter_logical_entries, parse_entry
|
|
13
|
+
from .report_markdown import build_markdown_report
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NoLogFilesError(FileNotFoundError):
|
|
17
|
+
"""Raised when no matching log files are found for an analysis run."""
|
|
18
|
+
|
|
19
|
+
exit_code = EXIT_NO_LOG_FILES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def analyze_file(file_path: Path) -> FileAnalysis:
|
|
23
|
+
"""Analyze one log file and aggregate raw and normalized message counts."""
|
|
24
|
+
|
|
25
|
+
raw_counts: Counter[MessageKey] = Counter()
|
|
26
|
+
norm_counts: Counter[MessageKey] = Counter()
|
|
27
|
+
norm_examples: dict[MessageKey, Counter[str]] = defaultdict(Counter)
|
|
28
|
+
|
|
29
|
+
unknown_lines = 0
|
|
30
|
+
total_entries = 0
|
|
31
|
+
|
|
32
|
+
encoding = detect_encoding(file_path)
|
|
33
|
+
total_lines = count_physical_lines(file_path, encoding=encoding)
|
|
34
|
+
|
|
35
|
+
for entry in iter_logical_entries(file_path, encoding=encoding):
|
|
36
|
+
total_entries += 1
|
|
37
|
+
parsed = parse_entry(entry)
|
|
38
|
+
if not parsed:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
severity = parsed.severity
|
|
42
|
+
message = parsed.message
|
|
43
|
+
|
|
44
|
+
if severity == "UNKNOWN":
|
|
45
|
+
unknown_lines += 1
|
|
46
|
+
|
|
47
|
+
raw_counts[(severity, message)] += 1
|
|
48
|
+
|
|
49
|
+
normalized_message = normalize_message(message)
|
|
50
|
+
norm_key = (severity, normalized_message)
|
|
51
|
+
norm_counts[norm_key] += 1
|
|
52
|
+
norm_examples[norm_key][message] += 1
|
|
53
|
+
|
|
54
|
+
return FileAnalysis(
|
|
55
|
+
file=file_path,
|
|
56
|
+
total_lines=total_lines,
|
|
57
|
+
total_entries=total_entries,
|
|
58
|
+
unknown_lines=unknown_lines,
|
|
59
|
+
raw_counts=raw_counts,
|
|
60
|
+
norm_counts=norm_counts,
|
|
61
|
+
norm_examples=dict(norm_examples),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def sorted_rows(counter: Counter[MessageKey]) -> list[tuple[str, str, int]]:
|
|
66
|
+
"""Return aggregated rows in a stable severity/count/message order."""
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
(severity, message, count)
|
|
70
|
+
for (severity, message), count in sorted(
|
|
71
|
+
counter.items(),
|
|
72
|
+
key=lambda item: (item[0][0], -item[1], item[0][1]),
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def find_log_files(logs_dir: Path) -> list[Path]:
|
|
78
|
+
"""Return all supported log files from the configured logs directory."""
|
|
79
|
+
|
|
80
|
+
return sorted(logs_dir.glob("*.txt"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_default_config(
|
|
84
|
+
*,
|
|
85
|
+
logs_dir: Path,
|
|
86
|
+
out_dir: Path | None = None,
|
|
87
|
+
backup_dir: Path | None = None,
|
|
88
|
+
top_examples: int = DEFAULT_TOP_EXAMPLES,
|
|
89
|
+
convert: bool = False,
|
|
90
|
+
) -> AnalysisConfig:
|
|
91
|
+
"""Build an analysis configuration with default output locations."""
|
|
92
|
+
|
|
93
|
+
resolved_out_dir = out_dir or Path(DEFAULT_OUT_DIR)
|
|
94
|
+
resolved_backup_dir = backup_dir or (resolved_out_dir / "backup")
|
|
95
|
+
return AnalysisConfig(
|
|
96
|
+
logs_dir=logs_dir,
|
|
97
|
+
out_dir=resolved_out_dir,
|
|
98
|
+
backup_dir=resolved_backup_dir,
|
|
99
|
+
top_examples=top_examples,
|
|
100
|
+
convert=convert,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_analysis(config: AnalysisConfig) -> AnalysisRunResult:
|
|
105
|
+
"""Run the full analysis workflow and write CSV and report outputs."""
|
|
106
|
+
|
|
107
|
+
logs_dir = config.logs_dir
|
|
108
|
+
out_dir = config.out_dir
|
|
109
|
+
backup_dir = config.backup_dir or (out_dir / "backup")
|
|
110
|
+
|
|
111
|
+
ensure_dir(out_dir)
|
|
112
|
+
ensure_dir(backup_dir)
|
|
113
|
+
|
|
114
|
+
log_files = find_log_files(logs_dir)
|
|
115
|
+
if not log_files:
|
|
116
|
+
raise NoLogFilesError(f"No *.txt files found in: {logs_dir.resolve()}")
|
|
117
|
+
|
|
118
|
+
summary = AnalysisSummary(
|
|
119
|
+
analyses=[],
|
|
120
|
+
global_raw=Counter(),
|
|
121
|
+
global_norm=Counter(),
|
|
122
|
+
global_norm_examples={},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
for log_file in log_files:
|
|
126
|
+
backup_path = backup_file(log_file, backup_dir)
|
|
127
|
+
analysis = analyze_file(log_file)
|
|
128
|
+
analysis.backup_path = backup_path
|
|
129
|
+
summary.analyses.append(analysis)
|
|
130
|
+
|
|
131
|
+
summary.global_raw.update(analysis.raw_counts)
|
|
132
|
+
summary.global_norm.update(analysis.norm_counts)
|
|
133
|
+
for key, counter in analysis.norm_examples.items():
|
|
134
|
+
summary.global_norm_examples.setdefault(key, Counter()).update(counter)
|
|
135
|
+
|
|
136
|
+
write_csv(
|
|
137
|
+
out_dir / f"{log_file.stem}.aggregated.csv",
|
|
138
|
+
sorted_rows(analysis.raw_counts),
|
|
139
|
+
headers=["SEVERITY", "MESSAGE", "COUNT"],
|
|
140
|
+
)
|
|
141
|
+
write_csv(
|
|
142
|
+
out_dir / f"{log_file.stem}.aggregated.normalized.csv",
|
|
143
|
+
sorted_rows(analysis.norm_counts),
|
|
144
|
+
headers=["SEVERITY", "MESSAGE_NORMALIZED", "COUNT"],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
write_csv(
|
|
148
|
+
out_dir / "summary.all_files.csv",
|
|
149
|
+
sorted_rows(summary.global_raw),
|
|
150
|
+
headers=["SEVERITY", "MESSAGE", "COUNT"],
|
|
151
|
+
)
|
|
152
|
+
write_csv(
|
|
153
|
+
out_dir / "summary.all_files.normalized.csv",
|
|
154
|
+
sorted_rows(summary.global_norm),
|
|
155
|
+
headers=["SEVERITY", "MESSAGE_NORMALIZED", "COUNT"],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
report_path = out_dir / "report.md"
|
|
159
|
+
report_path.write_text(build_markdown_report(summary, config), encoding="utf-8", newline="\n")
|
|
160
|
+
|
|
161
|
+
result = AnalysisRunResult(
|
|
162
|
+
out_dir=out_dir,
|
|
163
|
+
backup_dir=backup_dir,
|
|
164
|
+
report_path=report_path,
|
|
165
|
+
summary=summary,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if config.convert:
|
|
169
|
+
from .converters import convert_report_md_to_html_pdf
|
|
170
|
+
|
|
171
|
+
result.html_path = out_dir / "report.html"
|
|
172
|
+
result.pdf_path = out_dir / "report.pdf"
|
|
173
|
+
result.convert_status = convert_report_md_to_html_pdf(result.report_path, result.html_path, result.pdf_path)
|
|
174
|
+
|
|
175
|
+
return result
|