nginx-lens 0.3.4__tar.gz → 0.5.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.
Files changed (71) hide show
  1. {nginx_lens-0.3.4/nginx_lens.egg-info → nginx_lens-0.5.0}/PKG-INFO +6 -1
  2. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/analyze.py +9 -3
  3. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/cli.py +6 -0
  4. nginx_lens-0.5.0/commands/completion.py +174 -0
  5. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/diff.py +3 -2
  6. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/graph.py +3 -2
  7. nginx_lens-0.5.0/commands/health.py +144 -0
  8. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/include.py +3 -2
  9. nginx_lens-0.5.0/commands/logs.py +345 -0
  10. nginx_lens-0.5.0/commands/metrics.py +495 -0
  11. nginx_lens-0.5.0/commands/resolve.py +120 -0
  12. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/route.py +1 -0
  13. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/syntax.py +1 -0
  14. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/tree.py +3 -2
  15. nginx_lens-0.5.0/commands/validate.py +451 -0
  16. nginx_lens-0.5.0/config/__init__.py +4 -0
  17. nginx_lens-0.5.0/config/config_loader.py +200 -0
  18. nginx_lens-0.5.0/exporter/csv.py +87 -0
  19. nginx_lens-0.5.0/exporter/json_yaml.py +361 -0
  20. {nginx_lens-0.3.4 → nginx_lens-0.5.0/nginx_lens.egg-info}/PKG-INFO +6 -1
  21. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/nginx_lens.egg-info/SOURCES.txt +18 -1
  22. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/nginx_lens.egg-info/requires.txt +5 -0
  23. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/nginx_lens.egg-info/top_level.txt +2 -0
  24. nginx_lens-0.5.0/pyproject.toml +10 -0
  25. nginx_lens-0.5.0/setup.py +54 -0
  26. nginx_lens-0.5.0/tests/test_diff.py +68 -0
  27. nginx_lens-0.5.0/tests/test_graph.py +64 -0
  28. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/tests/test_health.py +3 -3
  29. nginx_lens-0.5.0/tests/test_integration.py +199 -0
  30. nginx_lens-0.5.0/tests/test_logs.py +74 -0
  31. nginx_lens-0.5.0/tests/test_performance.py +80 -0
  32. nginx_lens-0.5.0/tests/test_resolve.py +67 -0
  33. nginx_lens-0.5.0/tests/test_route.py +85 -0
  34. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/upstream_checker/checker.py +47 -7
  35. nginx_lens-0.5.0/upstream_checker/dns_cache.py +216 -0
  36. nginx_lens-0.5.0/utils/__init__.py +4 -0
  37. nginx_lens-0.5.0/utils/progress.py +120 -0
  38. nginx_lens-0.3.4/commands/health.py +0 -87
  39. nginx_lens-0.3.4/commands/logs.py +0 -97
  40. nginx_lens-0.3.4/commands/resolve.py +0 -65
  41. nginx_lens-0.3.4/pyproject.toml +0 -3
  42. nginx_lens-0.3.4/setup.py +0 -23
  43. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/LICENSE +0 -0
  44. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/README.md +0 -0
  45. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/__init__.py +0 -0
  46. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/base.py +0 -0
  47. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/conflicts.py +0 -0
  48. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/dead_locations.py +0 -0
  49. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/diff.py +0 -0
  50. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/duplicates.py +0 -0
  51. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/empty_blocks.py +0 -0
  52. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/include.py +0 -0
  53. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/rewrite.py +0 -0
  54. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/route.py +0 -0
  55. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/unused.py +0 -0
  56. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/analyzer/warnings.py +0 -0
  57. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/commands/__init__.py +0 -0
  58. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/exporter/__init__.py +0 -0
  59. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/exporter/graph.py +0 -0
  60. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/exporter/html.py +0 -0
  61. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/exporter/markdown.py +0 -0
  62. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/nginx_lens.egg-info/dependency_links.txt +0 -0
  63. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/nginx_lens.egg-info/entry_points.txt +0 -0
  64. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/parser/__init__.py +0 -0
  65. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/parser/nginx_parser.py +0 -0
  66. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/setup.cfg +0 -0
  67. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/tests/test_conflicts.py +0 -0
  68. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/tests/test_duplicates.py +0 -0
  69. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/tests/test_empty_blocks.py +0 -0
  70. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/tests/test_parser.py +0 -0
  71. {nginx_lens-0.3.4 → nginx_lens-0.5.0}/upstream_checker/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nginx-lens
3
- Version: 0.3.4
3
+ Version: 0.5.0
4
4
  Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
5
5
  Author: Daniil Astrouski
6
6
  Author-email: shelovesuastra@gmail.com
@@ -10,9 +10,14 @@ Requires-Dist: typer[all]>=0.9.0
10
10
  Requires-Dist: rich>=13.0.0
11
11
  Requires-Dist: requests>=2.25.0
12
12
  Requires-Dist: dnspython>=2.0.0
13
+ Requires-Dist: pyyaml>=6.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
16
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
13
17
  Dynamic: author
14
18
  Dynamic: author-email
15
19
  Dynamic: license-file
20
+ Dynamic: provides-extra
16
21
  Dynamic: requires-dist
17
22
  Dynamic: requires-python
18
23
  Dynamic: summary
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import typer
2
3
  from rich.console import Console
3
4
  from rich.table import Table
@@ -9,6 +10,7 @@ from analyzer.unused import find_unused_variables
9
10
  from parser.nginx_parser import parse_nginx_config
10
11
  from analyzer.rewrite import find_rewrite_issues
11
12
  from analyzer.dead_locations import find_dead_locations
13
+ from exporter.json_yaml import format_analyze_results, print_export
12
14
 
13
15
  app = typer.Typer()
14
16
  console = Console()
@@ -41,7 +43,11 @@ ISSUE_META = {
41
43
  }
42
44
  SEVERITY_COLOR = {"high": "red", "medium": "orange3", "low": "yellow"}
43
45
 
44
- def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf")):
46
+ def analyze(
47
+ config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
48
+ json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
49
+ yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
50
+ ):
45
51
  """
46
52
  Анализирует конфигурацию Nginx на типовые проблемы и best practices.
47
53
 
@@ -62,10 +68,10 @@ def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf"
62
68
  tree = parse_nginx_config(config_path)
63
69
  except FileNotFoundError:
64
70
  console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
65
- return
71
+ sys.exit(1)
66
72
  except Exception as e:
67
73
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
68
- return
74
+ sys.exit(1)
69
75
  conflicts = find_location_conflicts(tree)
70
76
  dups = find_duplicate_directives(tree)
71
77
  empties = find_empty_blocks(tree)
@@ -10,6 +10,9 @@ from commands.graph import graph
10
10
  from commands.logs import logs
11
11
  from commands.syntax import syntax
12
12
  from commands.resolve import resolve
13
+ from commands.validate import validate
14
+ from commands.metrics import metrics
15
+ from commands.completion import app as completion_app
13
16
 
14
17
  app = typer.Typer(help="nginx-lens — анализ и диагностика конфигураций Nginx")
15
18
  console = Console()
@@ -24,6 +27,9 @@ app.command()(graph)
24
27
  app.command()(logs)
25
28
  app.command()(syntax)
26
29
  app.command()(resolve)
30
+ app.command()(validate)
31
+ app.command()(metrics)
32
+ app.add_typer(completion_app, name="completion", help="Генерация скриптов автодополнения")
27
33
 
28
34
  if __name__ == "__main__":
29
35
  app()
@@ -0,0 +1,174 @@
1
+ """
2
+ Команда для генерации скриптов автодополнения.
3
+ """
4
+ import sys
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich import box
9
+
10
+ app = typer.Typer()
11
+ console = Console()
12
+
13
+
14
+ def _generate_completion_script(shell: str) -> str:
15
+ """
16
+ Генерирует базовый completion скрипт для указанного shell.
17
+
18
+ Args:
19
+ shell: Тип shell (bash, zsh, fish, powershell)
20
+
21
+ Returns:
22
+ Строка с completion скриптом
23
+ """
24
+ commands = [
25
+ "health", "analyze", "tree", "diff", "route", "include",
26
+ "graph", "logs", "syntax", "resolve", "validate", "metrics"
27
+ ]
28
+
29
+ if shell == "bash":
30
+ script = f"""# Bash completion for nginx-lens
31
+ _nginx_lens_completion() {{
32
+ local cur="${{COMP_WORDS[COMP_CWORD]}}"
33
+ local commands="{' '.join(commands)}"
34
+
35
+ if [[ ${{COMP_CWORD}} -eq 1 ]]; then
36
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
37
+ else
38
+ COMPREPLY=($(compgen -f -- "$cur"))
39
+ fi
40
+ }}
41
+
42
+ complete -F _nginx_lens_completion nginx-lens
43
+ """
44
+ elif shell == "zsh":
45
+ script = f"""# Zsh completion for nginx-lens
46
+ #compdef nginx-lens
47
+
48
+ _nginx_lens() {{
49
+ local -a commands
50
+ commands=(
51
+ {' '.join(f'"{cmd}":{cmd}' for cmd in commands)}
52
+ )
53
+
54
+ _describe 'command' commands
55
+ }}
56
+
57
+ _nginx_lens "$@"
58
+ """
59
+ elif shell == "fish":
60
+ script = f"""# Fish completion for nginx-lens
61
+ complete -c nginx-lens -f -a "{' '.join(commands)}"
62
+ """
63
+ elif shell == "powershell":
64
+ script = f"""# PowerShell completion for nginx-lens
65
+ Register-ArgumentCompleter -Native -CommandName nginx-lens -ScriptBlock {{
66
+ param($commandName, $wordToComplete, $cursorPosition)
67
+
68
+ $commands = @({', '.join(f'"{cmd}"' for cmd in commands)})
69
+
70
+ $commands | Where-Object {{ $_ -like "$wordToComplete*" }} | ForEach-Object {{
71
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
72
+ }}
73
+ }}
74
+ """
75
+ else:
76
+ script = f"# Completion script for {shell}\n# Use: typer commands.cli app {shell}\n"
77
+
78
+ return script
79
+
80
+
81
+ @app.command()
82
+ def install(
83
+ shell: str = typer.Argument(..., help="Тип shell: bash, zsh, fish, powershell"),
84
+ output: str = typer.Option(None, "--output", "-o", help="Путь для сохранения скрипта (по умолчанию выводится в stdout)"),
85
+ ):
86
+ """
87
+ Генерирует скрипт автодополнения для указанного shell.
88
+
89
+ После генерации нужно добавить его в конфигурацию shell:
90
+
91
+ Bash:
92
+ nginx-lens completion install bash >> ~/.bashrc
93
+
94
+ Zsh:
95
+ nginx-lens completion install zsh >> ~/.zshrc
96
+
97
+ Fish:
98
+ nginx-lens completion install fish > ~/.config/fish/completions/nginx-lens.fish
99
+
100
+ PowerShell:
101
+ nginx-lens completion install powershell > nginx-lens-completion.ps1
102
+ # Затем выполните: . .\nginx-lens-completion.ps1
103
+ """
104
+ import subprocess
105
+ import sys
106
+
107
+ # Пробуем использовать typer CLI для генерации completion
108
+ completion_script = None
109
+ try:
110
+ result = subprocess.run(
111
+ [sys.executable, "-m", "typer", "commands.cli", "app", shell],
112
+ capture_output=True,
113
+ text=True,
114
+ check=False,
115
+ timeout=10
116
+ )
117
+
118
+ if result.returncode == 0 and result.stdout and len(result.stdout.strip()) > 0:
119
+ completion_script = result.stdout
120
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
121
+ pass
122
+
123
+ # Если typer CLI не сработал, используем базовый скрипт
124
+ if not completion_script:
125
+ completion_script = _generate_completion_script(shell)
126
+
127
+ if shell not in ["bash", "zsh", "fish", "powershell"]:
128
+ console.print(f"[red]Неподдерживаемый shell: {shell}[/red]")
129
+ console.print("[yellow]Поддерживаемые shell: bash, zsh, fish, powershell[/yellow]")
130
+ raise typer.Exit(1)
131
+
132
+ if output:
133
+ try:
134
+ with open(output, 'w') as f:
135
+ f.write(completion_script)
136
+ console.print(f"[green]✓ Скрипт автодополнения сохранен в {output}[/green]")
137
+ console.print(f"[yellow]Не забудьте добавить его в конфигурацию {shell}[/yellow]")
138
+ except Exception as e:
139
+ console.print(f"[red]Ошибка при сохранении файла: {e}[/red]")
140
+ raise typer.Exit(1)
141
+ else:
142
+ typer.echo(completion_script)
143
+ # Выводим инструкцию в stderr, чтобы не мешать скрипту
144
+ console.print("\n[yellow]Добавьте этот скрипт в конфигурацию вашего shell[/yellow]", file=sys.stderr)
145
+
146
+
147
+ @app.command()
148
+ def show_instructions():
149
+ """
150
+ Показывает инструкции по установке автодополнения для всех поддерживаемых shell.
151
+ """
152
+ console.print(Panel("[bold blue]Инструкции по установке автодополнения[/bold blue]", box=box.ROUNDED))
153
+
154
+ console.print("\n[bold]Bash:[/bold]")
155
+ console.print(" nginx-lens completion install bash >> ~/.bashrc")
156
+ console.print(" source ~/.bashrc")
157
+
158
+ console.print("\n[bold]Zsh:[/bold]")
159
+ console.print(" nginx-lens completion install zsh >> ~/.zshrc")
160
+ console.print(" source ~/.zshrc")
161
+
162
+ console.print("\n[bold]Fish:[/bold]")
163
+ console.print(" mkdir -p ~/.config/fish/completions")
164
+ console.print(" nginx-lens completion install fish > ~/.config/fish/completions/nginx-lens.fish")
165
+
166
+ console.print("\n[bold]PowerShell:[/bold]")
167
+ console.print(" nginx-lens completion install powershell > nginx-lens-completion.ps1")
168
+ console.print(" . .\\nginx-lens-completion.ps1")
169
+ console.print(" # Или добавьте в профиль PowerShell: $PROFILE")
170
+
171
+
172
+ if __name__ == "__main__":
173
+ app()
174
+
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import typer
2
3
  from rich.console import Console
3
4
  from rich.table import Table
@@ -24,10 +25,10 @@ def diff(
24
25
  lines2 = f2.readlines()
25
26
  except FileNotFoundError as e:
26
27
  console.print(f"[red]Файл {e.filename} не найден. Проверьте путь к конфигу.[/red]")
27
- return
28
+ sys.exit(1)
28
29
  except Exception as e:
29
30
  console.print(f"[red]Ошибка при чтении файлов: {e}[/red]")
30
- return
31
+ sys.exit(1)
31
32
  maxlen = max(len(lines1), len(lines2))
32
33
  # Выравниваем длины
33
34
  lines1 += [''] * (maxlen - len(lines1))
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import typer
2
3
  from rich.console import Console
3
4
  from parser.nginx_parser import parse_nginx_config
@@ -21,10 +22,10 @@ def graph(
21
22
  tree = parse_nginx_config(config_path)
22
23
  except FileNotFoundError:
23
24
  console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
24
- return
25
+ sys.exit(1)
25
26
  except Exception as e:
26
27
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
27
- return
28
+ sys.exit(1)
28
29
  routes = []
29
30
  # Для каждого server/location строим маршрут
30
31
  def walk(d, chain, upstreams):
@@ -0,0 +1,144 @@
1
+ import sys
2
+ from typing import Optional
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from upstream_checker.checker import check_upstreams, resolve_upstreams
7
+ from upstream_checker.dns_cache import disable_cache, enable_cache
8
+ from parser.nginx_parser import parse_nginx_config
9
+ from exporter.json_yaml import format_health_results, print_export
10
+ from config.config_loader import get_config
11
+ from utils.progress import ProgressManager
12
+
13
+ app = typer.Typer()
14
+ console = Console()
15
+
16
+ def health(
17
+ config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
18
+ timeout: Optional[float] = typer.Option(None, help="Таймаут проверки (сек)"),
19
+ retries: Optional[int] = typer.Option(None, help="Количество попыток"),
20
+ mode: Optional[str] = typer.Option(None, help="Режим проверки: tcp или http", case_sensitive=False),
21
+ resolve: bool = typer.Option(False, "--resolve", "-r", help="Показать резолвленные IP-адреса"),
22
+ max_workers: Optional[int] = typer.Option(None, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
23
+ json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
24
+ yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
25
+ no_cache: bool = typer.Option(False, "--no-cache", help="Отключить кэширование DNS резолвинга"),
26
+ cache_ttl: Optional[int] = typer.Option(None, "--cache-ttl", help="Время жизни кэша в секундах"),
27
+ ):
28
+ """
29
+ Проверяет доступность upstream-серверов, определённых в nginx.conf. Выводит таблицу.
30
+ Использует параллельную обработку для ускорения проверки множества upstream серверов.
31
+ Поддерживает кэширование результатов DNS резолвинга для ускорения повторных запусков.
32
+
33
+ Пример:
34
+ nginx-lens health /etc/nginx/nginx.conf
35
+ nginx-lens health /etc/nginx/nginx.conf --timeout 5 --retries 3 --mode http
36
+ nginx-lens health /etc/nginx/nginx.conf --resolve
37
+ nginx-lens health /etc/nginx/nginx.conf --max-workers 20
38
+ nginx-lens health /etc/nginx/nginx.conf --resolve --no-cache
39
+ nginx-lens health /etc/nginx/nginx.conf --resolve --cache-ttl 600
40
+ """
41
+ exit_code = 0
42
+
43
+ # Загружаем конфигурацию
44
+ config = get_config()
45
+ defaults = config.get_defaults()
46
+ cache_config = config.get_cache_config()
47
+
48
+ # Применяем значения из конфига, если не указаны через CLI
49
+ timeout = timeout if timeout is not None else defaults.get("timeout", 2.0)
50
+ retries = retries if retries is not None else defaults.get("retries", 1)
51
+ mode = mode if mode is not None else defaults.get("mode", "tcp")
52
+ max_workers = max_workers if max_workers is not None else defaults.get("max_workers", 10)
53
+ cache_ttl = cache_ttl if cache_ttl is not None else cache_config.get("ttl", defaults.get("dns_cache_ttl", 300))
54
+
55
+ # Управление кэшем
56
+ use_cache = not no_cache and cache_config.get("enabled", True)
57
+ if no_cache:
58
+ disable_cache()
59
+ else:
60
+ enable_cache()
61
+
62
+ try:
63
+ tree = parse_nginx_config(config_path)
64
+ except FileNotFoundError:
65
+ console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
66
+ sys.exit(1)
67
+ except Exception as e:
68
+ console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
69
+ sys.exit(1)
70
+
71
+ upstreams = tree.get_upstreams()
72
+
73
+ # Подсчитываем общее количество серверов для прогресс-бара
74
+ total_servers = sum(len(servers) for servers in upstreams.values())
75
+
76
+ # Проверка upstream с прогресс-баром
77
+ with ProgressManager(description="Проверка upstream серверов", show_progress=total_servers > 5) as pm:
78
+ results = check_upstreams(upstreams, timeout=timeout, retries=retries, mode=mode.lower(), max_workers=max_workers, progress_manager=pm)
79
+
80
+ # Если нужно показать резолвленные IP-адреса
81
+ resolved_info = {}
82
+ if resolve:
83
+ with ProgressManager(description="Резолвинг DNS", show_progress=total_servers > 5) as pm:
84
+ resolved_info = resolve_upstreams(upstreams, max_workers=max_workers, use_cache=use_cache, cache_ttl=cache_ttl, progress_manager=pm)
85
+
86
+ # Экспорт в JSON/YAML
87
+ if json or yaml:
88
+ export_data = format_health_results(results, resolved_info if resolve else None)
89
+ format_type = 'json' if json else 'yaml'
90
+ print_export(export_data, format_type)
91
+ # Exit code остается прежним
92
+ for name, servers in results.items():
93
+ for srv in servers:
94
+ if not srv["healthy"]:
95
+ exit_code = 1
96
+ if resolve and name in resolved_info:
97
+ for resolved_srv in resolved_info[name]:
98
+ if resolved_srv["address"] == srv["address"]:
99
+ if any("invalid resolve" in r for r in resolved_srv["resolved"]):
100
+ exit_code = 1
101
+ break
102
+ sys.exit(exit_code)
103
+
104
+ # Обычный вывод в таблицу
105
+ table = Table(show_header=True, header_style="bold blue")
106
+ table.add_column("Address")
107
+ table.add_column("Status")
108
+ if resolve:
109
+ table.add_column("Resolved IP")
110
+
111
+ for name, servers in results.items():
112
+ for srv in servers:
113
+ status = "Healthy" if srv["healthy"] else "Unhealthy"
114
+ color = "green" if srv["healthy"] else "red"
115
+
116
+ # Проверяем статус здоровья
117
+ if not srv["healthy"]:
118
+ exit_code = 1
119
+
120
+ if resolve:
121
+ resolved_list = []
122
+ if name in resolved_info:
123
+ for resolved_srv in resolved_info[name]:
124
+ if resolved_srv["address"] == srv["address"]:
125
+ resolved_list = resolved_srv["resolved"]
126
+ break
127
+
128
+ if resolved_list:
129
+ # Показываем все IP-адреса через запятую
130
+ resolved_str = ", ".join(resolved_list)
131
+ # Если есть "invalid resolve", показываем красным, иначе зеленым
132
+ if any("invalid resolve" in r for r in resolved_list):
133
+ table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[red]{resolved_str}[/red]")
134
+ exit_code = 1
135
+ else:
136
+ table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[green]{resolved_str}[/green]")
137
+ else:
138
+ table.add_row(srv["address"], f"[{color}]{status}[/{color}]", "[yellow]Failed to resolve[/yellow]")
139
+ exit_code = 1
140
+ else:
141
+ table.add_row(srv["address"], f"[{color}]{status}[/{color}]")
142
+
143
+ console.print(table)
144
+ sys.exit(exit_code)
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import typer
2
3
  from rich.console import Console
3
4
  from rich.tree import Tree
@@ -22,10 +23,10 @@ def include_tree(
22
23
  tree = build_include_tree(config_path)
23
24
  except FileNotFoundError:
24
25
  console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
25
- return
26
+ sys.exit(1)
26
27
  except Exception as e:
27
28
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
28
- return
29
+ sys.exit(1)
29
30
  rich_tree = Tree(f"[bold blue]{config_path}[/bold blue]")
30
31
  def _add(node, t):
31
32
  for k, v in t.items():