nginx-lens 0.1.4__tar.gz → 0.1.5__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 (49) hide show
  1. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/PKG-INFO +1 -1
  2. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/duplicates.py +6 -4
  3. nginx_lens-0.1.5/commands/analyze.py +130 -0
  4. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/cli.py +4 -4
  5. nginx_lens-0.1.5/commands/graph.py +75 -0
  6. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/logs.py +0 -1
  7. nginx_lens-0.1.5/commands/route.py +49 -0
  8. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/syntax.py +31 -21
  9. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/nginx_lens.egg-info/PKG-INFO +1 -1
  10. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/setup.py +1 -1
  11. nginx_lens-0.1.4/commands/analyze.py +0 -74
  12. nginx_lens-0.1.4/commands/graph.py +0 -26
  13. nginx_lens-0.1.4/commands/route.py +0 -33
  14. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/__init__.py +0 -0
  15. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/base.py +0 -0
  16. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/conflicts.py +0 -0
  17. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/dead_locations.py +0 -0
  18. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/diff.py +0 -0
  19. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/empty_blocks.py +0 -0
  20. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/include.py +0 -0
  21. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/rewrite.py +0 -0
  22. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/route.py +0 -0
  23. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/unused.py +0 -0
  24. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/analyzer/warnings.py +0 -0
  25. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/__init__.py +0 -0
  26. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/diff.py +0 -0
  27. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/health.py +0 -0
  28. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/include.py +0 -0
  29. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/commands/tree.py +0 -0
  30. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/exporter/__init__.py +0 -0
  31. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/exporter/graph.py +0 -0
  32. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/exporter/html.py +0 -0
  33. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/exporter/markdown.py +0 -0
  34. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/nginx_lens.egg-info/SOURCES.txt +0 -0
  35. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/nginx_lens.egg-info/dependency_links.txt +0 -0
  36. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/nginx_lens.egg-info/entry_points.txt +0 -0
  37. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/nginx_lens.egg-info/requires.txt +0 -0
  38. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/nginx_lens.egg-info/top_level.txt +0 -0
  39. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/parser/__init__.py +0 -0
  40. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/parser/nginx_parser.py +0 -0
  41. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/pyproject.toml +0 -0
  42. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/setup.cfg +0 -0
  43. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/tests/test_conflicts.py +0 -0
  44. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/tests/test_duplicates.py +0 -0
  45. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/tests/test_empty_blocks.py +0 -0
  46. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/tests/test_health.py +0 -0
  47. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/tests/test_parser.py +0 -0
  48. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/upstream_checker/__init__.py +0 -0
  49. {nginx_lens-0.1.4 → nginx_lens-0.1.5}/upstream_checker/checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nginx-lens
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
5
5
  Author: Daniil Astrouski
6
6
  Author-email: shelovesuastra@gmail.com
@@ -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, _ in analyzer.walk(d['directives'], d):
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
@@ -0,0 +1,130 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from analyzer.conflicts import find_location_conflicts, find_listen_servername_conflicts
5
+ from analyzer.duplicates import find_duplicate_directives
6
+ from analyzer.empty_blocks import find_empty_blocks
7
+ from analyzer.warnings import find_warnings
8
+ from analyzer.unused import find_unused_variables
9
+ from parser.nginx_parser import parse_nginx_config
10
+ from analyzer.rewrite import find_rewrite_issues
11
+ from analyzer.dead_locations import find_dead_locations
12
+
13
+ app = typer.Typer()
14
+ console = Console()
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
+
44
+ def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf")):
45
+ """
46
+ Анализирует конфигурацию Nginx на типовые проблемы и best practices.
47
+
48
+ Показывает:
49
+ - Конфликты location-ов
50
+ - Дублирующиеся директивы
51
+ - Пустые блоки
52
+ - Потенциальные проблемы (proxy_pass без схемы, autoindex on, if, server_tokens on, SSL, лимиты, deprecated и др.)
53
+ - Неиспользуемые переменные
54
+ - Конфликты listen/server_name
55
+ - Проблемы с rewrite
56
+ - Мертвые location-ы
57
+
58
+ Пример:
59
+ nginx-lens analyze /etc/nginx/nginx.conf
60
+ """
61
+ tree = parse_nginx_config(config_path)
62
+ conflicts = find_location_conflicts(tree)
63
+ dups = find_duplicate_directives(tree)
64
+ empties = find_empty_blocks(tree)
65
+ warnings = find_warnings(tree)
66
+ unused_vars = find_unused_variables(tree)
67
+ listen_conflicts = find_listen_servername_conflicts(tree)
68
+ rewrite_issues = find_rewrite_issues(tree)
69
+ dead_locations = find_dead_locations(tree)
70
+
71
+ table = Table(show_header=True, header_style="bold blue")
72
+ table.add_column("issue_type")
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}]")
80
+
81
+ for c in conflicts:
82
+ add_row("location_conflict", f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}")
83
+ for d in dups:
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 ''}")
86
+ for e in empties:
87
+ add_row("empty_block", f"{e['block']} {e['arg'] or ''}")
88
+ for w in warnings:
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']}")
118
+ for v in unused_vars:
119
+ add_row("unused_variable", v['name'])
120
+ for c in listen_conflicts:
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'])}")
122
+ for r in rewrite_issues:
123
+ add_row(r['type'], r['value'])
124
+ for l in dead_locations:
125
+ add_row("dead_location", f"server: {l['server'].get('arg','')} location: {l['location'].get('arg','')}")
126
+
127
+ if table.row_count == 0:
128
+ console.print("[green]Проблем не найдено[/green]")
129
+ else:
130
+ console.print(table)
@@ -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 app as logs_app
11
- from commands.syntax import app as syntax_app
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.add_typer(logs_app, name="logs")
24
- app.add_typer(syntax_app, name="syntax")
23
+ app.command()(logs)
24
+ app.command()(syntax)
25
25
 
26
26
  if __name__ == "__main__":
27
27
  app()
@@ -0,0 +1,75 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from parser.nginx_parser import parse_nginx_config
4
+ from exporter.graph import tree_to_dot, tree_to_mermaid
5
+ from rich.text import Text
6
+
7
+ app = typer.Typer()
8
+ console = Console()
9
+
10
+ def graph(
11
+ config_path: str = typer.Argument(..., help="Путь к nginx.conf")
12
+ ):
13
+ """
14
+ Показывает все возможные маршруты nginx в виде цепочек server → location → proxy_pass → upstream → server.
15
+
16
+ Пример:
17
+ nginx-lens graph /etc/nginx/nginx.conf
18
+ """
19
+ tree = parse_nginx_config(config_path)
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)
@@ -9,7 +9,6 @@ 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="Сколько топ-значений выводить")
@@ -0,0 +1,49 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.panel import Panel
4
+ from analyzer.route import find_route
5
+ from parser.nginx_parser import parse_nginx_config
6
+ import glob
7
+ import os
8
+
9
+ app = typer.Typer()
10
+ console = Console()
11
+
12
+ def route(
13
+ config_path: str = typer.Argument(None, help="Путь к nginx.conf (если не указан — поиск по всем .conf в /etc/nginx)", show_default=False),
14
+ url: str = typer.Argument(..., help="URL для маршрутизации (например, http://host/path)")
15
+ ):
16
+ """
17
+ Показывает, какой server/location обслуживает указанный URL.
18
+
19
+ Пример:
20
+ nginx-lens route /etc/nginx/nginx.conf http://example.com/api/v1
21
+ nginx-lens route http://example.com/api/v1
22
+ """
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"))
@@ -8,11 +8,10 @@ import re
8
8
  app = typer.Typer(help="Проверка синтаксиса nginx-конфига через nginx -t с подсветкой ошибок.")
9
9
  console = Console()
10
10
 
11
- ERROR_RE = re.compile(r'in (.+?):(\d+)')
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.Argument(..., help="Путь к nginx.conf"),
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 /etc/nginx/nginx.conf
25
- nginx-lens syntax /etc/nginx/nginx.conf --nginx-path /usr/local/sbin/nginx
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
- m = ERROR_RE.search(err)
39
- if m:
40
- file, line = m.group(1), int(m.group(2))
41
- msg = err.strip().split('\n')[-1]
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
- context = []
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
- context.append((str(i+1), mark, lines[i].rstrip()))
67
+ context_lines.append(f"{mark} {lines[i].rstrip()}")
52
68
  except Exception:
53
- context = []
54
- table = Table(title="Ошибка синтаксиса", show_header=True, header_style="bold red")
55
- table.add_column("File")
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]")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nginx-lens
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
5
5
  Author: Daniil Astrouski
6
6
  Author-email: shelovesuastra@gmail.com
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="nginx-lens",
5
- version="0.1.4",
5
+ version="0.1.5",
6
6
  description="CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx",
7
7
  author="Daniil Astrouski",
8
8
  author_email="shelovesuastra@gmail.com",
@@ -1,74 +0,0 @@
1
- import typer
2
- from rich.console import Console
3
- from rich.table import Table
4
- from analyzer.conflicts import find_location_conflicts, find_listen_servername_conflicts
5
- from analyzer.duplicates import find_duplicate_directives
6
- from analyzer.empty_blocks import find_empty_blocks
7
- from analyzer.warnings import find_warnings
8
- from analyzer.unused import find_unused_variables
9
- from parser.nginx_parser import parse_nginx_config
10
- from analyzer.rewrite import find_rewrite_issues
11
- from analyzer.dead_locations import find_dead_locations
12
-
13
- app = typer.Typer()
14
- console = Console()
15
-
16
- def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf")):
17
- """
18
- Анализирует конфигурацию Nginx на типовые проблемы и best practices.
19
-
20
- Показывает:
21
- - Конфликты location-ов
22
- - Дублирующиеся директивы
23
- - Пустые блоки
24
- - Потенциальные проблемы (proxy_pass без схемы, autoindex on, if, server_tokens on, SSL, лимиты, deprecated и др.)
25
- - Неиспользуемые переменные
26
- - Конфликты listen/server_name
27
- - Проблемы с rewrite
28
- - Мертвые location-ы
29
-
30
- Пример:
31
- nginx-lens analyze /etc/nginx/nginx.conf
32
- """
33
- tree = parse_nginx_config(config_path)
34
- conflicts = find_location_conflicts(tree)
35
- dups = find_duplicate_directives(tree)
36
- empties = find_empty_blocks(tree)
37
- warnings = find_warnings(tree)
38
- unused_vars = find_unused_variables(tree)
39
- listen_conflicts = find_listen_servername_conflicts(tree)
40
- rewrite_issues = find_rewrite_issues(tree)
41
- dead_locations = find_dead_locations(tree)
42
-
43
- table = Table(show_header=True, header_style="bold blue")
44
- table.add_column("issue_type")
45
- table.add_column("issue_description")
46
-
47
- for c in conflicts:
48
- table.add_row("location_conflict", f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}")
49
- for d in dups:
50
- table.add_row("duplicate_directive", f"{d['directive']} ({d['args']}) — {d['count']} раз в блоке {d['block'].get('block', d['block'])}")
51
- for e in empties:
52
- table.add_row("empty_block", f"{e['block']} {e['arg'] or ''}")
53
- for w in warnings:
54
- if w['type'] == 'proxy_pass_no_scheme':
55
- table.add_row("proxy_pass_no_scheme", f"proxy_pass без схемы: {w['value']}")
56
- elif w['type'] == 'autoindex_on':
57
- table.add_row("autoindex_on", f"autoindex on в блоке {w['context'].get('block','')}")
58
- elif w['type'] == 'if_block':
59
- table.add_row("if_block", f"директива if внутри блока {w['context'].get('block','')}")
60
- elif w['type'] == 'server_tokens_on':
61
- table.add_row("server_tokens_on", f"server_tokens on в блоке {w['context'].get('block','')}")
62
- for v in unused_vars:
63
- table.add_row("unused_variable", v['name'])
64
- for c in listen_conflicts:
65
- table.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
- for r in rewrite_issues:
67
- table.add_row(r['type'], r['value'])
68
- for l in dead_locations:
69
- table.add_row("dead_location", f"server: {l['server'].get('arg','')} location: {l['location'].get('arg','')}")
70
-
71
- if table.row_count == 0:
72
- console.print("[green]Проблем не найдено[/green]")
73
- else:
74
- console.print(table)
@@ -1,26 +0,0 @@
1
- import typer
2
- from rich.console import Console
3
- from parser.nginx_parser import parse_nginx_config
4
- from exporter.graph import tree_to_dot, tree_to_mermaid
5
-
6
- app = typer.Typer()
7
- console = Console()
8
-
9
- def graph(
10
- config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
11
- format: str = typer.Option("dot", help="Формат: dot или mermaid")
12
- ):
13
- """
14
- Генерирует схему маршрутизации nginx (dot/mermaid).
15
-
16
- Пример:
17
- nginx-lens graph /etc/nginx/nginx.conf --format dot
18
- nginx-lens graph /etc/nginx/nginx.conf --format mermaid
19
- """
20
- tree = parse_nginx_config(config_path)
21
- if format == "dot":
22
- console.print(tree_to_dot(tree.directives))
23
- elif format == "mermaid":
24
- console.print(tree_to_mermaid(tree.directives))
25
- else:
26
- console.print("[red]Неизвестный формат: выберите dot или mermaid[/red]")
@@ -1,33 +0,0 @@
1
- import typer
2
- from rich.console import Console
3
- from rich.panel import Panel
4
- from analyzer.route import find_route
5
- from parser.nginx_parser import parse_nginx_config
6
-
7
- app = typer.Typer()
8
- console = Console()
9
-
10
- def route(
11
- config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
12
- url: str = typer.Argument(..., help="URL для маршрутизации (например, http://host/path)")
13
- ):
14
- """
15
- Показывает, какой server/location обслуживает указанный URL.
16
-
17
- Пример:
18
- nginx-lens route /etc/nginx/nginx.conf http://example.com/api/v1
19
- """
20
- tree = parse_nginx_config(config_path)
21
- res = find_route(tree, url)
22
- if not res:
23
- console.print(Panel(f"Не найден подходящий server для {url}", style="red"))
24
- return
25
- server = res['server']
26
- location = res['location']
27
- proxy_pass = res['proxy_pass']
28
- text = f"[bold]Server:[/bold] {server.get('arg','') or '[no arg]'}\n"
29
- if location:
30
- text += f"[bold]Location:[/bold] {location.get('arg','')}\n"
31
- if proxy_pass:
32
- text += f"[bold]proxy_pass:[/bold] {proxy_pass}\n"
33
- console.print(Panel(text, title="Route", style="green"))
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