nginx-lens 0.1.3__py3-none-any.whl → 0.1.5__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.
- analyzer/duplicates.py +6 -4
- commands/analyze.py +71 -15
- commands/cli.py +4 -4
- commands/graph.py +60 -11
- commands/logs.py +1 -2
- commands/route.py +31 -15
- commands/syntax.py +32 -22
- {nginx_lens-0.1.3.dist-info → nginx_lens-0.1.5.dist-info}/METADATA +1 -1
- {nginx_lens-0.1.3.dist-info → nginx_lens-0.1.5.dist-info}/RECORD +12 -12
- {nginx_lens-0.1.3.dist-info → nginx_lens-0.1.5.dist-info}/WHEEL +0 -0
- {nginx_lens-0.1.3.dist-info → nginx_lens-0.1.5.dist-info}/entry_points.txt +0 -0
- {nginx_lens-0.1.3.dist-info → nginx_lens-0.1.5.dist-info}/top_level.txt +0 -0
analyzer/duplicates.py
CHANGED
|
@@ -3,15 +3,16 @@ from typing import List, Dict, Any
|
|
|
3
3
|
|
|
4
4
|
def find_duplicate_directives(tree) -> List[Dict[str, Any]]:
|
|
5
5
|
"""
|
|
6
|
-
Находит дублирующиеся директивы внутри одного
|
|
7
|
-
Возвращает список: [{block, directive, count}]
|
|
6
|
+
Находит дублирующиеся директивы внутри одного блока (без вложенности).
|
|
7
|
+
Возвращает список: [{block, directive, count, location}]
|
|
8
8
|
"""
|
|
9
9
|
analyzer = Analyzer(tree)
|
|
10
10
|
duplicates = []
|
|
11
11
|
for d, parent in analyzer.walk():
|
|
12
12
|
if 'directives' in d:
|
|
13
|
+
# Считаем только прямые дочерние директивы (без вложенных блоков)
|
|
13
14
|
seen = {}
|
|
14
|
-
for sub
|
|
15
|
+
for sub in d['directives']:
|
|
15
16
|
if 'directive' in sub:
|
|
16
17
|
key = (sub['directive'], str(sub.get('args')))
|
|
17
18
|
seen[key] = seen.get(key, 0) + 1
|
|
@@ -21,6 +22,7 @@ def find_duplicate_directives(tree) -> List[Dict[str, Any]]:
|
|
|
21
22
|
'block': d,
|
|
22
23
|
'directive': directive,
|
|
23
24
|
'args': args,
|
|
24
|
-
'count': count
|
|
25
|
+
'count': count,
|
|
26
|
+
'location': d.get('arg')
|
|
25
27
|
})
|
|
26
28
|
return duplicates
|
commands/analyze.py
CHANGED
|
@@ -13,6 +13,34 @@ from analyzer.dead_locations import find_dead_locations
|
|
|
13
13
|
app = typer.Typer()
|
|
14
14
|
console = Console()
|
|
15
15
|
|
|
16
|
+
# Карта советов и критичности для issue_type
|
|
17
|
+
ISSUE_META = {
|
|
18
|
+
'location_conflict': ("Возможное пересечение location. Это не всегда ошибка: порядок и типы location могут быть корректны. Проверьте, что порядок и типы location соответствуют вашим ожиданиям. Если всё ок — игнорируйте предупреждение.", "medium"),
|
|
19
|
+
'duplicate_directive': ("Оставьте только одну директиву с нужным значением в этом блоке.", "medium"),
|
|
20
|
+
'empty_block': ("Удалите или заполните пустой блок.", "low"),
|
|
21
|
+
'proxy_pass_no_scheme': ("Добавьте http:// или https:// в proxy_pass.", "medium"),
|
|
22
|
+
'autoindex_on': ("Отключите autoindex, если не требуется публикация файлов.", "medium"),
|
|
23
|
+
'if_block': ("Избегайте if внутри location, используйте map/try_files.", "medium"),
|
|
24
|
+
'server_tokens_on': ("Отключите server_tokens для безопасности.", "low"),
|
|
25
|
+
'ssl_missing': ("Укажите путь к SSL-сертификату/ключу.", "high"),
|
|
26
|
+
'ssl_protocols_weak': ("Отключите устаревшие протоколы TLS.", "high"),
|
|
27
|
+
'ssl_ciphers_weak': ("Используйте современные шифры.", "high"),
|
|
28
|
+
'listen_443_no_ssl': ("Добавьте ssl к listen 443.", "high"),
|
|
29
|
+
'listen_443_no_http2': ("Добавьте http2 к listen 443 для производительности.", "low"),
|
|
30
|
+
'no_limit_req_conn': ("Добавьте limit_req/limit_conn для защиты от DDoS.", "medium"),
|
|
31
|
+
'missing_security_header': ("Добавьте security-заголовок.", "medium"),
|
|
32
|
+
'deprecated': ("Замените устаревшую директиву.", "medium"),
|
|
33
|
+
'limit_too_small': ("Увеличьте лимит до рекомендуемого значения.", "medium"),
|
|
34
|
+
'limit_too_large': ("Уменьшите лимит до разумного значения.", "medium"),
|
|
35
|
+
'unused_variable': ("Удалите неиспользуемую переменную.", "low"),
|
|
36
|
+
'listen_servername_conflict': ("Измените listen/server_name для устранения конфликта.", "high"),
|
|
37
|
+
'rewrite_cycle': ("Проверьте rewrite на циклические правила.", "high"),
|
|
38
|
+
'rewrite_conflict': ("Проверьте порядок и уникальность rewrite.", "medium"),
|
|
39
|
+
'rewrite_no_flag': ("Добавьте last/break/redirect/permanent к rewrite.", "low"),
|
|
40
|
+
'dead_location': ("Удалите неиспользуемый location или используйте его.", "low"),
|
|
41
|
+
}
|
|
42
|
+
SEVERITY_COLOR = {"high": "red", "medium": "orange3", "low": "yellow"}
|
|
43
|
+
|
|
16
44
|
def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf")):
|
|
17
45
|
"""
|
|
18
46
|
Анализирует конфигурацию Nginx на типовые проблемы и best practices.
|
|
@@ -43,30 +71,58 @@ def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf"
|
|
|
43
71
|
table = Table(show_header=True, header_style="bold blue")
|
|
44
72
|
table.add_column("issue_type")
|
|
45
73
|
table.add_column("issue_description")
|
|
74
|
+
table.add_column("solution")
|
|
75
|
+
|
|
76
|
+
def add_row(issue_type, desc):
|
|
77
|
+
solution, severity = ISSUE_META.get(issue_type, ("", "low"))
|
|
78
|
+
color = SEVERITY_COLOR.get(severity, "yellow")
|
|
79
|
+
table.add_row(f"[{color}]{issue_type}[/{color}]", desc, f"[{color}]{solution}[/{color}]")
|
|
46
80
|
|
|
47
81
|
for c in conflicts:
|
|
48
|
-
|
|
82
|
+
add_row("location_conflict", f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}")
|
|
49
83
|
for d in dups:
|
|
50
|
-
|
|
84
|
+
loc = d.get('location')
|
|
85
|
+
add_row("duplicate_directive", f"{d['directive']} ({d['args']}) — {d['count']} раз в блоке {d['block'].get('block', d['block'])}{' location: '+str(loc) if loc else ''}")
|
|
51
86
|
for e in empties:
|
|
52
|
-
|
|
87
|
+
add_row("empty_block", f"{e['block']} {e['arg'] or ''}")
|
|
53
88
|
for w in warnings:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
t = w['type']
|
|
90
|
+
if t == 'proxy_pass_no_scheme':
|
|
91
|
+
add_row(t, f"proxy_pass без схемы: {w['value']}")
|
|
92
|
+
elif t == 'autoindex_on':
|
|
93
|
+
add_row(t, f"autoindex on в блоке {w['context'].get('block','')}")
|
|
94
|
+
elif t == 'if_block':
|
|
95
|
+
add_row(t, f"директива if внутри блока {w['context'].get('block','')}")
|
|
96
|
+
elif t == 'server_tokens_on':
|
|
97
|
+
add_row(t, f"server_tokens on в блоке {w['context'].get('block','')}")
|
|
98
|
+
elif t == 'ssl_missing':
|
|
99
|
+
add_row(t, f"{w['directive']} не указан")
|
|
100
|
+
elif t == 'ssl_protocols_weak':
|
|
101
|
+
add_row(t, f"ssl_protocols содержит устаревшие протоколы: {w['value']}")
|
|
102
|
+
elif t == 'ssl_ciphers_weak':
|
|
103
|
+
add_row(t, f"ssl_ciphers содержит слабые шифры: {w['value']}")
|
|
104
|
+
elif t == 'listen_443_no_ssl':
|
|
105
|
+
add_row(t, f"listen без ssl: {w['value']}")
|
|
106
|
+
elif t == 'listen_443_no_http2':
|
|
107
|
+
add_row(t, f"listen 443 без http2: {w['value']}")
|
|
108
|
+
elif t == 'no_limit_req_conn':
|
|
109
|
+
add_row(t, f"server без limit_req/limit_conn")
|
|
110
|
+
elif t == 'missing_security_header':
|
|
111
|
+
add_row(t, f"отсутствует security header: {w['value']}")
|
|
112
|
+
elif t == 'deprecated':
|
|
113
|
+
add_row(t, f"устаревшая директива: {w['directive']} — {w['value']}")
|
|
114
|
+
elif t == 'limit_too_small':
|
|
115
|
+
add_row(t, f"слишком маленькое значение: {w['directive']} = {w['value']}")
|
|
116
|
+
elif t == 'limit_too_large':
|
|
117
|
+
add_row(t, f"слишком большое значение: {w['directive']} = {w['value']}")
|
|
62
118
|
for v in unused_vars:
|
|
63
|
-
|
|
119
|
+
add_row("unused_variable", v['name'])
|
|
64
120
|
for c in listen_conflicts:
|
|
65
|
-
|
|
121
|
+
add_row("listen_servername_conflict", f"server1: {c['server1'].get('arg','')} server2: {c['server2'].get('arg','')} listen: {','.join(c['listen'])} server_name: {','.join(c['server_name'])}")
|
|
66
122
|
for r in rewrite_issues:
|
|
67
|
-
|
|
123
|
+
add_row(r['type'], r['value'])
|
|
68
124
|
for l in dead_locations:
|
|
69
|
-
|
|
125
|
+
add_row("dead_location", f"server: {l['server'].get('arg','')} location: {l['location'].get('arg','')}")
|
|
70
126
|
|
|
71
127
|
if table.row_count == 0:
|
|
72
128
|
console.print("[green]Проблем не найдено[/green]")
|
commands/cli.py
CHANGED
|
@@ -7,8 +7,8 @@ from commands.diff import diff
|
|
|
7
7
|
from commands.route import route
|
|
8
8
|
from commands.include import include_tree
|
|
9
9
|
from commands.graph import graph
|
|
10
|
-
from commands.logs import
|
|
11
|
-
from commands.syntax import
|
|
10
|
+
from commands.logs import logs
|
|
11
|
+
from commands.syntax import syntax
|
|
12
12
|
|
|
13
13
|
app = typer.Typer(help="nginx-lens — анализ и диагностика конфигураций Nginx")
|
|
14
14
|
console = Console()
|
|
@@ -20,8 +20,8 @@ app.command()(diff)
|
|
|
20
20
|
app.command()(route)
|
|
21
21
|
app.command()(include_tree)
|
|
22
22
|
app.command()(graph)
|
|
23
|
-
app.
|
|
24
|
-
app.
|
|
23
|
+
app.command()(logs)
|
|
24
|
+
app.command()(syntax)
|
|
25
25
|
|
|
26
26
|
if __name__ == "__main__":
|
|
27
27
|
app()
|
commands/graph.py
CHANGED
|
@@ -2,25 +2,74 @@ import typer
|
|
|
2
2
|
from rich.console import Console
|
|
3
3
|
from parser.nginx_parser import parse_nginx_config
|
|
4
4
|
from exporter.graph import tree_to_dot, tree_to_mermaid
|
|
5
|
+
from rich.text import Text
|
|
5
6
|
|
|
6
7
|
app = typer.Typer()
|
|
7
8
|
console = Console()
|
|
8
9
|
|
|
9
10
|
def graph(
|
|
10
|
-
config_path: str = typer.Argument(..., help="Путь к nginx.conf")
|
|
11
|
-
format: str = typer.Option("dot", help="Формат: dot или mermaid")
|
|
11
|
+
config_path: str = typer.Argument(..., help="Путь к nginx.conf")
|
|
12
12
|
):
|
|
13
13
|
"""
|
|
14
|
-
|
|
14
|
+
Показывает все возможные маршруты nginx в виде цепочек server → location → proxy_pass → upstream → server.
|
|
15
15
|
|
|
16
16
|
Пример:
|
|
17
|
-
nginx-lens graph /etc/nginx/nginx.conf
|
|
18
|
-
nginx-lens graph /etc/nginx/nginx.conf --format mermaid
|
|
17
|
+
nginx-lens graph /etc/nginx/nginx.conf
|
|
19
18
|
"""
|
|
20
19
|
tree = parse_nginx_config(config_path)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
routes = []
|
|
21
|
+
# Для каждого server/location строим маршрут
|
|
22
|
+
def walk(d, chain, upstreams):
|
|
23
|
+
if d.get('block') == 'server':
|
|
24
|
+
srv = d.get('arg','') or '[no arg]'
|
|
25
|
+
for sub in d.get('directives', []):
|
|
26
|
+
walk(sub, chain + [('server', srv)], upstreams)
|
|
27
|
+
elif d.get('block') == 'location':
|
|
28
|
+
loc = d.get('arg','')
|
|
29
|
+
for sub in d.get('directives', []):
|
|
30
|
+
walk(sub, chain + [('location', loc)], upstreams)
|
|
31
|
+
elif d.get('directive') == 'proxy_pass':
|
|
32
|
+
val = d.get('args','')
|
|
33
|
+
# ищем, есть ли такой upstream
|
|
34
|
+
up_name = None
|
|
35
|
+
if val.startswith('http://') or val.startswith('https://'):
|
|
36
|
+
up = val.split('://',1)[1].split('/',1)[0]
|
|
37
|
+
if up in upstreams:
|
|
38
|
+
up_name = up
|
|
39
|
+
if up_name:
|
|
40
|
+
for srv in upstreams[up_name]:
|
|
41
|
+
routes.append(chain + [('proxy_pass', val), ('upstream', up_name), ('upstream_server', srv)])
|
|
42
|
+
else:
|
|
43
|
+
routes.append(chain + [('proxy_pass', val)])
|
|
44
|
+
elif d.get('upstream'):
|
|
45
|
+
# собираем upstream-ы
|
|
46
|
+
upstreams[d['upstream']] = d.get('servers',[])
|
|
47
|
+
# рекурсивно по всем директивам
|
|
48
|
+
for sub in d.get('directives', []):
|
|
49
|
+
walk(sub, chain, upstreams)
|
|
50
|
+
# Собираем upstream-ы
|
|
51
|
+
upstreams = {}
|
|
52
|
+
for d in tree.directives:
|
|
53
|
+
if d.get('upstream'):
|
|
54
|
+
upstreams[d['upstream']] = d.get('servers',[])
|
|
55
|
+
# Строим маршруты
|
|
56
|
+
for d in tree.directives:
|
|
57
|
+
walk(d, [], upstreams)
|
|
58
|
+
if not routes:
|
|
59
|
+
console.print("[yellow]Не найдено ни одного маршрута[/yellow]")
|
|
60
|
+
return
|
|
61
|
+
# Красивый вывод
|
|
62
|
+
for route in routes:
|
|
63
|
+
t = Text()
|
|
64
|
+
for i, (typ, val) in enumerate(route):
|
|
65
|
+
if typ == 'server':
|
|
66
|
+
t.append(f"server: {val}", style="bold blue")
|
|
67
|
+
elif typ == 'location':
|
|
68
|
+
t.append(f" -> location: {val}", style="yellow")
|
|
69
|
+
elif typ == 'proxy_pass':
|
|
70
|
+
t.append(f" -> proxy_pass: {val}", style="green")
|
|
71
|
+
elif typ == 'upstream':
|
|
72
|
+
t.append(f" -> upstream: {val}", style="magenta")
|
|
73
|
+
elif typ == 'upstream_server':
|
|
74
|
+
t.append(f" -> server: {val}", style="grey50")
|
|
75
|
+
console.print(t)
|
commands/logs.py
CHANGED
|
@@ -4,12 +4,11 @@ from rich.table import Table
|
|
|
4
4
|
import re
|
|
5
5
|
from collections import Counter, defaultdict
|
|
6
6
|
|
|
7
|
-
app = typer.Typer()
|
|
7
|
+
app = typer.Typer(help="Анализ access.log/error.log: топ-статусы, пути, IP, User-Agent, ошибки.")
|
|
8
8
|
console = Console()
|
|
9
9
|
|
|
10
10
|
log_line_re = re.compile(r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) [^\"]+" (?P<status>\d{3})')
|
|
11
11
|
|
|
12
|
-
@app.command()
|
|
13
12
|
def logs(
|
|
14
13
|
log_path: str = typer.Argument(..., help="Путь к access.log или error.log"),
|
|
15
14
|
top: int = typer.Option(10, help="Сколько топ-значений выводить")
|
commands/route.py
CHANGED
|
@@ -3,12 +3,14 @@ from rich.console import Console
|
|
|
3
3
|
from rich.panel import Panel
|
|
4
4
|
from analyzer.route import find_route
|
|
5
5
|
from parser.nginx_parser import parse_nginx_config
|
|
6
|
+
import glob
|
|
7
|
+
import os
|
|
6
8
|
|
|
7
9
|
app = typer.Typer()
|
|
8
10
|
console = Console()
|
|
9
11
|
|
|
10
12
|
def route(
|
|
11
|
-
config_path: str = typer.Argument(
|
|
13
|
+
config_path: str = typer.Argument(None, help="Путь к nginx.conf (если не указан — поиск по всем .conf в /etc/nginx)", show_default=False),
|
|
12
14
|
url: str = typer.Argument(..., help="URL для маршрутизации (например, http://host/path)")
|
|
13
15
|
):
|
|
14
16
|
"""
|
|
@@ -16,18 +18,32 @@ def route(
|
|
|
16
18
|
|
|
17
19
|
Пример:
|
|
18
20
|
nginx-lens route /etc/nginx/nginx.conf http://example.com/api/v1
|
|
21
|
+
nginx-lens route http://example.com/api/v1
|
|
19
22
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
configs = []
|
|
24
|
+
if config_path:
|
|
25
|
+
configs = [config_path]
|
|
26
|
+
else:
|
|
27
|
+
configs = glob.glob("/etc/nginx/**/*.conf", recursive=True)
|
|
28
|
+
if not configs:
|
|
29
|
+
console.print(Panel("Не найдено ни одного .conf файла в /etc/nginx", style="red"))
|
|
30
|
+
return
|
|
31
|
+
for conf in configs:
|
|
32
|
+
try:
|
|
33
|
+
tree = parse_nginx_config(conf)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
continue # пропускаем битые/невалидные
|
|
36
|
+
res = find_route(tree, url)
|
|
37
|
+
if res:
|
|
38
|
+
server = res['server']
|
|
39
|
+
location = res['location']
|
|
40
|
+
proxy_pass = res['proxy_pass']
|
|
41
|
+
text = f"[bold]Config:[/bold] {conf}\n"
|
|
42
|
+
text += f"[bold]Server:[/bold] {server.get('arg','') or '[no arg]'}\n"
|
|
43
|
+
if location:
|
|
44
|
+
text += f"[bold]Location:[/bold] {location.get('arg','')}\n"
|
|
45
|
+
if proxy_pass:
|
|
46
|
+
text += f"[bold]proxy_pass:[/bold] {proxy_pass}\n"
|
|
47
|
+
console.print(Panel(text, title="Route", style="green"))
|
|
48
|
+
return
|
|
49
|
+
console.print(Panel(f"Ни один ваш конфиг в /etc/nginx не обрабатывает этот URL", style="red"))
|
commands/syntax.py
CHANGED
|
@@ -5,14 +5,13 @@ import subprocess
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
7
|
|
|
8
|
-
app = typer.Typer()
|
|
8
|
+
app = typer.Typer(help="Проверка синтаксиса nginx-конфига через nginx -t с подсветкой ошибок.")
|
|
9
9
|
console = Console()
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
ERRORS_RE = re.compile(r'in (.+?):(\d+)(?:\s*\n)?(.+?)(?=\nin |$)', re.DOTALL)
|
|
12
12
|
|
|
13
|
-
@app.command()
|
|
14
13
|
def syntax(
|
|
15
|
-
config_path: str = typer.
|
|
14
|
+
config_path: str = typer.Option(None, "-c", "--config", help="Путь к кастомному nginx.conf"),
|
|
16
15
|
nginx_path: str = typer.Option("nginx", help="Путь к бинарю nginx (по умолчанию 'nginx')")
|
|
17
16
|
):
|
|
18
17
|
"""
|
|
@@ -21,26 +20,43 @@ def syntax(
|
|
|
21
20
|
В случае ошибки показывает место в виде таблицы с контекстом.
|
|
22
21
|
|
|
23
22
|
Пример:
|
|
24
|
-
nginx-lens syntax
|
|
25
|
-
nginx-lens syntax
|
|
23
|
+
nginx-lens syntax -c ./mynginx.conf
|
|
24
|
+
nginx-lens syntax
|
|
26
25
|
"""
|
|
26
|
+
if not config_path:
|
|
27
|
+
candidates = [
|
|
28
|
+
"/etc/nginx/nginx.conf",
|
|
29
|
+
"/usr/local/etc/nginx/nginx.conf",
|
|
30
|
+
"./nginx.conf"
|
|
31
|
+
]
|
|
32
|
+
config_path = next((p for p in candidates if os.path.isfile(p)), None)
|
|
33
|
+
if not config_path:
|
|
34
|
+
console.print("[red]Не удалось найти nginx.conf. Укажите путь через -c.[/red]")
|
|
35
|
+
return
|
|
27
36
|
cmd = [nginx_path, "-t", "-c", os.path.abspath(config_path)]
|
|
28
37
|
try:
|
|
29
38
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
30
39
|
if result.returncode == 0:
|
|
31
40
|
console.print("[green]Синтаксис nginx-конфига корректен[/green]")
|
|
41
|
+
return
|
|
32
42
|
else:
|
|
33
43
|
console.print("[red]Ошибка синтаксиса![/red]")
|
|
34
44
|
console.print(result.stdout)
|
|
35
45
|
console.print(result.stderr)
|
|
36
|
-
# Парсим
|
|
46
|
+
# Парсим все ошибки
|
|
37
47
|
err = result.stderr or result.stdout
|
|
38
|
-
|
|
39
|
-
if
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
errors = list(ERRORS_RE.finditer(err))
|
|
49
|
+
if not errors:
|
|
50
|
+
console.print("[red]Не удалось определить файл и строку ошибки[/red]")
|
|
51
|
+
return
|
|
52
|
+
table = Table(title="Ошибки синтаксиса", show_header=True, header_style="bold red")
|
|
53
|
+
table.add_column("config_file")
|
|
54
|
+
table.add_column("issue_message")
|
|
55
|
+
table.add_column("context")
|
|
56
|
+
for m in errors:
|
|
57
|
+
file, line, msg = m.group(1), int(m.group(2)), m.group(3).strip().split('\n')[0]
|
|
42
58
|
# Читаем контекст
|
|
43
|
-
|
|
59
|
+
context_lines = []
|
|
44
60
|
try:
|
|
45
61
|
with open(file) as f:
|
|
46
62
|
lines = f.readlines()
|
|
@@ -48,16 +64,10 @@ def syntax(
|
|
|
48
64
|
end = min(len(lines), line+2)
|
|
49
65
|
for i in range(start, end):
|
|
50
66
|
mark = "->" if i+1 == line else " "
|
|
51
|
-
|
|
67
|
+
context_lines.append(f"{mark} {lines[i].rstrip()}")
|
|
52
68
|
except Exception:
|
|
53
|
-
|
|
54
|
-
table
|
|
55
|
-
|
|
56
|
-
table.add_column("Line")
|
|
57
|
-
table.add_column("Message")
|
|
58
|
-
table.add_column("Context")
|
|
59
|
-
for ln, mark, code in context:
|
|
60
|
-
table.add_row(file, ln, msg if mark == "->" else "", f"{mark} {code}")
|
|
61
|
-
console.print(table)
|
|
69
|
+
context_lines = []
|
|
70
|
+
table.add_row(file, msg, '\n'.join(context_lines))
|
|
71
|
+
console.print(table)
|
|
62
72
|
except FileNotFoundError:
|
|
63
73
|
console.print(f"[red]Не найден бинарь nginx: {nginx_path}[/red]")
|
|
@@ -3,7 +3,7 @@ analyzer/base.py,sha256=oGKg78BfMVmuzYafc08oq9p31-jEgYolGjLkUcIdkN8,607
|
|
|
3
3
|
analyzer/conflicts.py,sha256=NSNZc8e2x51K41dflSUvuwlDq-rzBXU5ITi6WfxFbfU,2796
|
|
4
4
|
analyzer/dead_locations.py,sha256=uvMu5qBGTVi0Nn960x3WpRvTljGbQuVFivU4nfe36oY,1435
|
|
5
5
|
analyzer/diff.py,sha256=idvXnoLzBVUYgKi_s3uDu0v2GNMV3B8aDqTROXcdQdo,1749
|
|
6
|
-
analyzer/duplicates.py,sha256=
|
|
6
|
+
analyzer/duplicates.py,sha256=jpy_6k-BzWxaXFt2Wb3rlulIXUEzbFe9xYRm7rWR50U,1215
|
|
7
7
|
analyzer/empty_blocks.py,sha256=7Zu4-5I5PS3bjhH0Ppq1CvM7rMTeRIc4fHx5n5vkMIw,517
|
|
8
8
|
analyzer/include.py,sha256=FhKR4VsogLknykjLD2N8jX9OtwxZcWik5oPpvp-_luE,2465
|
|
9
9
|
analyzer/rewrite.py,sha256=-jSLLG1jqmGU-dXWvU6NHCW6muB8Lfro6fXX1tDCHCQ,1834
|
|
@@ -11,15 +11,15 @@ analyzer/route.py,sha256=2xxQooQEsfn10tzGCZUoP32T0OnTMnPB6qRgBR6not8,2345
|
|
|
11
11
|
analyzer/unused.py,sha256=Ixzv0bPsw9IafblVwLiAOgugdg2dGu1MJDtuoqzPZiY,1066
|
|
12
12
|
analyzer/warnings.py,sha256=zC36QMvegA2eQPvZ-P1eysrX_kXHx5A1MUKHKKNvG5c,5784
|
|
13
13
|
commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
commands/analyze.py,sha256=
|
|
15
|
-
commands/cli.py,sha256=
|
|
14
|
+
commands/analyze.py,sha256=QmsyeIlGqvhuRqlUFpPrzg90X5yDVJ3BSIGPhkCHYgI,8026
|
|
15
|
+
commands/cli.py,sha256=9HwDJ-po5al0ceb4Wkyw5F2wzqxbJTo0CbHQ2AQ8obo,722
|
|
16
16
|
commands/diff.py,sha256=VeZsUu1BYrDubDFrmM5xC4DUWZvWUjFRHfddXm3-I3c,1373
|
|
17
|
-
commands/graph.py,sha256=
|
|
17
|
+
commands/graph.py,sha256=MkPasTYSnpG4i3ge_l36t0YUawE3e71v4RWZF9BlJ8A,3084
|
|
18
18
|
commands/health.py,sha256=d2bBui0qauQtO4Ll9cjniKR1Y5dYJBQzG9CECDnsUQ4,1365
|
|
19
19
|
commands/include.py,sha256=zPqJpbbSU_6S3L4ntFncPmyFWba8smvdCRog_SFuAFI,1907
|
|
20
|
-
commands/logs.py,sha256=
|
|
21
|
-
commands/route.py,sha256=
|
|
22
|
-
commands/syntax.py,sha256=
|
|
20
|
+
commands/logs.py,sha256=8tnGsgNy_B97S3O0D_6bvOVfNvAyqeUNotlDOdjltgw,3420
|
|
21
|
+
commands/route.py,sha256=MpSI88lqiqkjwA4q-P7gUXmkDicJXYonTwFVC6fNs44,2012
|
|
22
|
+
commands/syntax.py,sha256=iy5JHtqLWs4OtNYuM1xDESlfJ9tOJ4RdNi1KDTfEO1k,3147
|
|
23
23
|
commands/tree.py,sha256=NEhNU66_e0JCsD4xh4315TM-xwo8NkPwc00lZ4saPzE,1844
|
|
24
24
|
exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
exporter/graph.py,sha256=WYUrqUgCaK6KihgxAcRHaQn4oMo6b7ybC8yb_36ZIsA,3995
|
|
@@ -29,8 +29,8 @@ parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
29
29
|
parser/nginx_parser.py,sha256=JqZ3clNy4Nf-bmbsx_rJUL7EgRoB79b87eEu_isMeqg,3577
|
|
30
30
|
upstream_checker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
upstream_checker/checker.py,sha256=9-6CMUTN7gXUACP8EwX722QogfujZyV-WWWUeM3a79k,455
|
|
32
|
-
nginx_lens-0.1.
|
|
33
|
-
nginx_lens-0.1.
|
|
34
|
-
nginx_lens-0.1.
|
|
35
|
-
nginx_lens-0.1.
|
|
36
|
-
nginx_lens-0.1.
|
|
32
|
+
nginx_lens-0.1.5.dist-info/METADATA,sha256=D8emGIWms0maTGCxi5cKe_RMQ9a1veunrpre0tNJUzo,476
|
|
33
|
+
nginx_lens-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
34
|
+
nginx_lens-0.1.5.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
|
|
35
|
+
nginx_lens-0.1.5.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
|
|
36
|
+
nginx_lens-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|