nginx-lens 0.3.4__tar.gz → 0.4.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 (60) hide show
  1. {nginx_lens-0.3.4/nginx_lens.egg-info → nginx_lens-0.4.0}/PKG-INFO +6 -1
  2. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/analyze.py +9 -3
  3. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/diff.py +3 -2
  4. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/graph.py +3 -2
  5. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/health.py +22 -0
  6. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/include.py +3 -2
  7. nginx_lens-0.4.0/commands/logs.py +337 -0
  8. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/resolve.py +26 -1
  9. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/route.py +1 -0
  10. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/syntax.py +1 -0
  11. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/tree.py +3 -2
  12. nginx_lens-0.4.0/exporter/csv.py +87 -0
  13. nginx_lens-0.4.0/exporter/json_yaml.py +361 -0
  14. {nginx_lens-0.3.4 → nginx_lens-0.4.0/nginx_lens.egg-info}/PKG-INFO +6 -1
  15. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/nginx_lens.egg-info/SOURCES.txt +9 -0
  16. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/nginx_lens.egg-info/requires.txt +5 -0
  17. nginx_lens-0.4.0/pyproject.toml +10 -0
  18. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/setup.py +8 -1
  19. nginx_lens-0.4.0/tests/test_diff.py +68 -0
  20. nginx_lens-0.4.0/tests/test_graph.py +64 -0
  21. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/tests/test_health.py +3 -3
  22. nginx_lens-0.4.0/tests/test_integration.py +199 -0
  23. nginx_lens-0.4.0/tests/test_logs.py +74 -0
  24. nginx_lens-0.4.0/tests/test_performance.py +80 -0
  25. nginx_lens-0.4.0/tests/test_resolve.py +67 -0
  26. nginx_lens-0.4.0/tests/test_route.py +85 -0
  27. nginx_lens-0.3.4/commands/logs.py +0 -97
  28. nginx_lens-0.3.4/pyproject.toml +0 -3
  29. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/LICENSE +0 -0
  30. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/README.md +0 -0
  31. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/__init__.py +0 -0
  32. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/base.py +0 -0
  33. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/conflicts.py +0 -0
  34. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/dead_locations.py +0 -0
  35. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/diff.py +0 -0
  36. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/duplicates.py +0 -0
  37. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/empty_blocks.py +0 -0
  38. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/include.py +0 -0
  39. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/rewrite.py +0 -0
  40. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/route.py +0 -0
  41. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/unused.py +0 -0
  42. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/analyzer/warnings.py +0 -0
  43. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/__init__.py +0 -0
  44. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/commands/cli.py +0 -0
  45. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/exporter/__init__.py +0 -0
  46. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/exporter/graph.py +0 -0
  47. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/exporter/html.py +0 -0
  48. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/exporter/markdown.py +0 -0
  49. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/nginx_lens.egg-info/dependency_links.txt +0 -0
  50. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/nginx_lens.egg-info/entry_points.txt +0 -0
  51. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/nginx_lens.egg-info/top_level.txt +0 -0
  52. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/parser/__init__.py +0 -0
  53. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/parser/nginx_parser.py +0 -0
  54. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/setup.cfg +0 -0
  55. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/tests/test_conflicts.py +0 -0
  56. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/tests/test_duplicates.py +0 -0
  57. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/tests/test_empty_blocks.py +0 -0
  58. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/tests/test_parser.py +0 -0
  59. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/upstream_checker/__init__.py +0 -0
  60. {nginx_lens-0.3.4 → nginx_lens-0.4.0}/upstream_checker/checker.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.4.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)
@@ -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):
@@ -4,6 +4,7 @@ from rich.console import Console
4
4
  from rich.table import Table
5
5
  from upstream_checker.checker import check_upstreams, resolve_upstreams
6
6
  from parser.nginx_parser import parse_nginx_config
7
+ from exporter.json_yaml import format_health_results, print_export
7
8
 
8
9
  app = typer.Typer()
9
10
  console = Console()
@@ -15,6 +16,8 @@ def health(
15
16
  mode: str = typer.Option("tcp", help="Режим проверки: tcp или http", case_sensitive=False),
16
17
  resolve: bool = typer.Option(False, "--resolve", "-r", help="Показать резолвленные IP-адреса"),
17
18
  max_workers: int = typer.Option(10, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
19
+ json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
20
+ yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
18
21
  ):
19
22
  """
20
23
  Проверяет доступность upstream-серверов, определённых в nginx.conf. Выводит таблицу.
@@ -45,6 +48,25 @@ def health(
45
48
  if resolve:
46
49
  resolved_info = resolve_upstreams(upstreams, max_workers=max_workers)
47
50
 
51
+ # Экспорт в JSON/YAML
52
+ if json or yaml:
53
+ export_data = format_health_results(results, resolved_info if resolve else None)
54
+ format_type = 'json' if json else 'yaml'
55
+ print_export(export_data, format_type)
56
+ # Exit code остается прежним
57
+ for name, servers in results.items():
58
+ for srv in servers:
59
+ if not srv["healthy"]:
60
+ exit_code = 1
61
+ if resolve and name in resolved_info:
62
+ for resolved_srv in resolved_info[name]:
63
+ if resolved_srv["address"] == srv["address"]:
64
+ if any("invalid resolve" in r for r in resolved_srv["resolved"]):
65
+ exit_code = 1
66
+ break
67
+ sys.exit(exit_code)
68
+
69
+ # Обычный вывод в таблицу
48
70
  table = Table(show_header=True, header_style="bold blue")
49
71
  table.add_column("Address")
50
72
  table.add_column("Status")
@@ -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():
@@ -0,0 +1,337 @@
1
+ import sys
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ import re
6
+ import gzip
7
+ from datetime import datetime, timedelta
8
+ from collections import Counter, defaultdict
9
+ from typing import Optional, List, Dict, Any
10
+ from exporter.json_yaml import format_logs_results, print_export
11
+ from exporter.csv import export_logs_to_csv
12
+
13
+ app = typer.Typer(help="Анализ access.log/error.log: топ-статусы, пути, IP, User-Agent, ошибки.")
14
+ console = Console()
15
+
16
+ # Улучшенный regex для парсинга nginx access log (поддерживает response time)
17
+ # Формат: IP - - [timestamp] "method path protocol" status size "referer" "user-agent" "response_time"
18
+ log_line_re = re.compile(
19
+ r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) [^\"]+" '
20
+ r'(?P<status>\d{3}) (?P<size>\S+) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"'
21
+ r'(?: "(?P<response_time>[^"]+)")?'
22
+ )
23
+
24
+ def logs(
25
+ log_path: str = typer.Argument(..., help="Путь к access.log или error.log"),
26
+ top: int = typer.Option(10, help="Сколько топ-значений выводить"),
27
+ json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
28
+ yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
29
+ csv: bool = typer.Option(False, "--csv", help="Экспортировать результаты в CSV"),
30
+ since: Optional[str] = typer.Option(None, "--since", help="Фильтр: с даты (формат: YYYY-MM-DD или YYYY-MM-DD HH:MM:SS)"),
31
+ until: Optional[str] = typer.Option(None, "--until", help="Фильтр: до даты (формат: YYYY-MM-DD или YYYY-MM-DD HH:MM:SS)"),
32
+ status: Optional[str] = typer.Option(None, "--status", help="Фильтр по статусам (например: 404,500)"),
33
+ detect_anomalies: bool = typer.Option(False, "--detect-anomalies", help="Обнаруживать аномалии в логах"),
34
+ ):
35
+ """
36
+ Анализирует access.log/error.log.
37
+
38
+ Показывает:
39
+ - Топ HTTP-статусов (404, 500 и др.)
40
+ - Топ путей
41
+ - Топ IP-адресов
42
+ - Топ User-Agent
43
+ - Топ путей с ошибками 404/500
44
+ - Анализ времени ответа (если доступно)
45
+ - Обнаружение аномалий
46
+
47
+ Пример:
48
+ nginx-lens logs /var/log/nginx/access.log --top 20
49
+ nginx-lens logs /var/log/nginx/access.log --since "2024-01-01" --status 404,500
50
+ nginx-lens logs /var/log/nginx/access.log.gz --detect-anomalies --json
51
+ """
52
+ # Парсинг фильтров
53
+ status_filter = None
54
+ if status:
55
+ status_filter = set(s.strip() for s in status.split(','))
56
+
57
+ since_dt = None
58
+ if since:
59
+ try:
60
+ if len(since) == 10: # YYYY-MM-DD
61
+ since_dt = datetime.strptime(since, "%Y-%m-%d")
62
+ else: # YYYY-MM-DD HH:MM:SS
63
+ since_dt = datetime.strptime(since, "%Y-%m-%d %H:%M:%S")
64
+ except ValueError:
65
+ console.print(f"[red]Неверный формат даты для --since: {since}. Используйте YYYY-MM-DD или YYYY-MM-DD HH:MM:SS[/red]")
66
+ sys.exit(1)
67
+
68
+ until_dt = None
69
+ if until:
70
+ try:
71
+ if len(until) == 10: # YYYY-MM-DD
72
+ until_dt = datetime.strptime(until, "%Y-%m-%d") + timedelta(days=1)
73
+ else: # YYYY-MM-DD HH:MM:SS
74
+ until_dt = datetime.strptime(until, "%Y-%m-%d %H:%M:%S")
75
+ except ValueError:
76
+ console.print(f"[red]Неверный формат даты для --until: {until}. Используйте YYYY-MM-DD или YYYY-MM-DD HH:MM:SS[/red]")
77
+ sys.exit(1)
78
+
79
+ # Чтение лога (поддержка gzip)
80
+ try:
81
+ if log_path.endswith('.gz'):
82
+ with gzip.open(log_path, 'rt', encoding='utf-8', errors='ignore') as f:
83
+ lines = list(f)
84
+ else:
85
+ with open(log_path, 'r', encoding='utf-8', errors='ignore') as f:
86
+ lines = list(f)
87
+ except FileNotFoundError:
88
+ console.print(f"[red]Файл {log_path} не найден. Проверьте путь к логу.[/red]")
89
+ sys.exit(1)
90
+ except Exception as e:
91
+ console.print(f"[red]Ошибка при чтении {log_path}: {e}[/red]")
92
+ sys.exit(1)
93
+ status_counter = Counter()
94
+ path_counter = Counter()
95
+ ip_counter = Counter()
96
+ user_agent_counter = Counter()
97
+ errors = defaultdict(list)
98
+ response_times = []
99
+ log_entries = []
100
+
101
+ # Парсинг nginx формата времени: 01/Jan/2024:00:00:00 +0000
102
+ nginx_time_format = "%d/%b/%Y:%H:%M:%S %z"
103
+
104
+ for line in lines:
105
+ m = log_line_re.search(line)
106
+ if m:
107
+ try:
108
+ # Парсинг времени
109
+ time_str = m.group('time')
110
+ log_time = datetime.strptime(time_str, nginx_time_format)
111
+
112
+ # Убираем timezone для сравнения (приводим к naive datetime)
113
+ if log_time.tzinfo:
114
+ log_time = log_time.replace(tzinfo=None)
115
+
116
+ # Фильтрация по времени
117
+ if since_dt and log_time < since_dt:
118
+ continue
119
+ if until_dt and log_time > until_dt:
120
+ continue
121
+
122
+ ip = m.group('ip')
123
+ path = m.group('path')
124
+ status = m.group('status')
125
+ method = m.group('method')
126
+ user_agent = m.group('user_agent') or ''
127
+ response_time_str = m.group('response_time')
128
+
129
+ # Фильтрация по статусам
130
+ if status_filter and status not in status_filter:
131
+ continue
132
+
133
+ # Сбор данных
134
+ entry = {
135
+ 'time': log_time,
136
+ 'ip': ip,
137
+ 'path': path,
138
+ 'status': status,
139
+ 'method': method,
140
+ 'user_agent': user_agent,
141
+ 'response_time': float(response_time_str) if response_time_str else None
142
+ }
143
+ log_entries.append(entry)
144
+
145
+ status_counter[status] += 1
146
+ path_counter[path] += 1
147
+ ip_counter[ip] += 1
148
+
149
+ if user_agent:
150
+ user_agent_counter[user_agent] += 1
151
+
152
+ if status.startswith('4') or status.startswith('5'):
153
+ errors[status].append(path)
154
+
155
+ if response_time_str:
156
+ try:
157
+ response_times.append(float(response_time_str))
158
+ except ValueError:
159
+ pass
160
+ except (ValueError, AttributeError) as e:
161
+ # Пропускаем строки с неверным форматом
162
+ continue
163
+
164
+ # Проверка на пустые результаты
165
+ if not log_entries:
166
+ if json or yaml or csv:
167
+ empty_data = {
168
+ "timestamp": __import__('datetime').datetime.now().isoformat(),
169
+ "summary": {"total_requests": 0},
170
+ "message": "Нет записей, соответствующих фильтрам"
171
+ }
172
+ if csv:
173
+ print("Category,Type,Value,Count\nNo Data,,,,No entries match filters")
174
+ else:
175
+ format_type = 'json' if json else 'yaml'
176
+ print_export(empty_data, format_type)
177
+ else:
178
+ console.print("[yellow]Нет записей, соответствующих указанным фильтрам.[/yellow]")
179
+ return
180
+
181
+ # Анализ времени ответа
182
+ response_time_stats = {}
183
+ if response_times:
184
+ response_time_stats = {
185
+ "min": min(response_times),
186
+ "max": max(response_times),
187
+ "avg": sum(response_times) / len(response_times),
188
+ "median": sorted(response_times)[len(response_times) // 2],
189
+ "p95": sorted(response_times)[int(len(response_times) * 0.95)] if response_times else 0,
190
+ "p99": sorted(response_times)[int(len(response_times) * 0.99)] if response_times else 0,
191
+ "total_requests_with_time": len(response_times)
192
+ }
193
+
194
+ # Обнаружение аномалий
195
+ anomalies = []
196
+ if detect_anomalies:
197
+ # Аномалия 1: Резкий скачок ошибок
198
+ if len(log_entries) > 100:
199
+ # Разбиваем на временные окна
200
+ window_size = max(100, len(log_entries) // 10)
201
+ error_rates = []
202
+ for i in range(0, len(log_entries), window_size):
203
+ window = log_entries[i:i+window_size]
204
+ error_count = sum(1 for e in window if e['status'].startswith('4') or e['status'].startswith('5'))
205
+ error_rates.append(error_count / len(window) if window else 0)
206
+
207
+ if len(error_rates) > 1:
208
+ avg_rate = sum(error_rates) / len(error_rates)
209
+ for i, rate in enumerate(error_rates):
210
+ if rate > avg_rate * 2: # Удвоение ошибок
211
+ anomalies.append({
212
+ "type": "error_spike",
213
+ "description": f"Резкий скачок ошибок в окне {i+1}: {rate*100:.1f}% (среднее: {avg_rate*100:.1f}%)",
214
+ "severity": "high"
215
+ })
216
+
217
+ # Аномалия 2: Медленные запросы
218
+ if response_times:
219
+ slow_threshold = response_time_stats.get("p95", 1.0) * 2
220
+ slow_requests = [e for e in log_entries if e.get('response_time') and e['response_time'] > slow_threshold]
221
+ if slow_requests:
222
+ anomalies.append({
223
+ "type": "slow_requests",
224
+ "description": f"Найдено {len(slow_requests)} медленных запросов (> {slow_threshold:.2f}s)",
225
+ "severity": "medium"
226
+ })
227
+
228
+ # Аномалия 3: Необычные паттерны IP
229
+ if len(log_entries) > 50:
230
+ ip_counts = Counter(e['ip'] for e in log_entries)
231
+ avg_ip_requests = len(log_entries) / len(ip_counts) if ip_counts else 0
232
+ suspicious_ips = [ip for ip, count in ip_counts.items() if count > avg_ip_requests * 5]
233
+ if suspicious_ips:
234
+ anomalies.append({
235
+ "type": "suspicious_ips",
236
+ "description": f"Подозрительная активность с IP: {', '.join(suspicious_ips[:5])}",
237
+ "severity": "medium"
238
+ })
239
+
240
+ # Аномалия 4: Необычные пути
241
+ if len(log_entries) > 50:
242
+ path_counts = Counter(e['path'] for e in log_entries)
243
+ avg_path_requests = len(log_entries) / len(path_counts) if path_counts else 0
244
+ unusual_paths = [path for path, count in path_counts.items() if count > avg_path_requests * 10]
245
+ if unusual_paths:
246
+ anomalies.append({
247
+ "type": "unusual_paths",
248
+ "description": f"Необычно много запросов к путям: {', '.join(unusual_paths[:3])}",
249
+ "severity": "low"
250
+ })
251
+
252
+ # Экспорт в CSV
253
+ if csv:
254
+ csv_output = export_logs_to_csv(
255
+ status_counter, path_counter, ip_counter, user_agent_counter,
256
+ errors, response_time_stats, anomalies
257
+ )
258
+ print(csv_output)
259
+ return
260
+
261
+ # Экспорт в JSON/YAML
262
+ if json or yaml:
263
+ export_data = format_logs_results(
264
+ status_counter, path_counter, ip_counter, user_agent_counter, errors, top,
265
+ response_time_stats if response_time_stats else None,
266
+ anomalies if anomalies else None
267
+ )
268
+ format_type = 'json' if json else 'yaml'
269
+ print_export(export_data, format_type)
270
+ return
271
+
272
+ # Показываем статистику по времени ответа
273
+ if response_time_stats:
274
+ table = Table(title="Response Time Statistics", show_header=True, header_style="bold green")
275
+ table.add_column("Metric")
276
+ table.add_column("Value")
277
+ for metric, value in response_time_stats.items():
278
+ if metric != "total_requests_with_time":
279
+ table.add_row(metric.replace("_", " ").title(), f"{value:.3f}s")
280
+ else:
281
+ table.add_row(metric.replace("_", " ").title(), str(int(value)))
282
+ console.print(table)
283
+
284
+ # Показываем аномалии
285
+ if anomalies:
286
+ table = Table(title="Detected Anomalies", show_header=True, header_style="bold red")
287
+ table.add_column("Type")
288
+ table.add_column("Description")
289
+ table.add_column("Severity")
290
+ for anomaly in anomalies:
291
+ severity_color = {"high": "red", "medium": "orange3", "low": "yellow"}.get(anomaly.get("severity", "low"), "white")
292
+ table.add_row(
293
+ anomaly.get("type", ""),
294
+ anomaly.get("description", ""),
295
+ f"[{severity_color}]{anomaly.get('severity', '')}[/{severity_color}]"
296
+ )
297
+ console.print(table)
298
+
299
+ # Топ статусов
300
+ table = Table(title="Top HTTP Status Codes", show_header=True, header_style="bold blue")
301
+ table.add_column("Status")
302
+ table.add_column("Count")
303
+ for status, count in status_counter.most_common(top):
304
+ table.add_row(status, str(count))
305
+ console.print(table)
306
+ # Топ путей
307
+ table = Table(title="Top Paths", show_header=True, header_style="bold blue")
308
+ table.add_column("Path")
309
+ table.add_column("Count")
310
+ for path, count in path_counter.most_common(top):
311
+ table.add_row(path, str(count))
312
+ console.print(table)
313
+ # Топ IP
314
+ table = Table(title="Top IPs", show_header=True, header_style="bold blue")
315
+ table.add_column("IP")
316
+ table.add_column("Count")
317
+ for ip, count in ip_counter.most_common(top):
318
+ table.add_row(ip, str(count))
319
+ console.print(table)
320
+ # Топ User-Agent
321
+ if user_agent_counter:
322
+ table = Table(title="Top User-Agents", show_header=True, header_style="bold blue")
323
+ table.add_column("User-Agent")
324
+ table.add_column("Count")
325
+ for ua, count in user_agent_counter.most_common(top):
326
+ table.add_row(ua, str(count))
327
+ console.print(table)
328
+ # Топ 404/500
329
+ for err in ('404', '500'):
330
+ if errors[err]:
331
+ table = Table(title=f"Top {err} Paths", show_header=True, header_style="bold blue")
332
+ table.add_column("Path")
333
+ table.add_column("Count")
334
+ c = Counter(errors[err])
335
+ for path, count in c.most_common(top):
336
+ table.add_row(path, str(count))
337
+ console.print(table)
@@ -4,6 +4,7 @@ from rich.console import Console
4
4
  from rich.table import Table
5
5
  from upstream_checker.checker import resolve_upstreams
6
6
  from parser.nginx_parser import parse_nginx_config
7
+ from exporter.json_yaml import format_resolve_results, print_export
7
8
 
8
9
  app = typer.Typer()
9
10
  console = Console()
@@ -11,6 +12,8 @@ console = Console()
11
12
  def resolve(
12
13
  config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
13
14
  max_workers: int = typer.Option(10, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
15
+ json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
16
+ yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
14
17
  ):
15
18
  """
16
19
  Резолвит DNS имена upstream-серверов в IP-адреса.
@@ -33,11 +36,33 @@ def resolve(
33
36
 
34
37
  upstreams = tree.get_upstreams()
35
38
  if not upstreams:
36
- console.print("[yellow]Не найдено ни одного upstream в конфигурации.[/yellow]")
39
+ if json or yaml:
40
+ export_data = {
41
+ "timestamp": __import__('datetime').datetime.now().isoformat(),
42
+ "upstreams": [],
43
+ "summary": {"total_upstreams": 0, "total_servers": 0}
44
+ }
45
+ format_type = 'json' if json else 'yaml'
46
+ print_export(export_data, format_type)
47
+ else:
48
+ console.print("[yellow]Не найдено ни одного upstream в конфигурации.[/yellow]")
37
49
  sys.exit(0) # Нет upstream - это не ошибка, просто нет чего проверять
38
50
 
39
51
  results = resolve_upstreams(upstreams, max_workers=max_workers)
40
52
 
53
+ # Экспорт в JSON/YAML
54
+ if json or yaml:
55
+ export_data = format_resolve_results(results)
56
+ format_type = 'json' if json else 'yaml'
57
+ print_export(export_data, format_type)
58
+ # Exit code остается прежним
59
+ for name, servers in results.items():
60
+ for srv in servers:
61
+ if not srv["resolved"] or any("invalid resolve" in r for r in srv["resolved"]):
62
+ exit_code = 1
63
+ sys.exit(exit_code)
64
+
65
+ # Обычный вывод в таблицу
41
66
  table = Table(show_header=True, header_style="bold blue")
42
67
  table.add_column("Upstream Name")
43
68
  table.add_column("Address")
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import typer
2
3
  from rich.console import Console
3
4
  from rich.panel import Panel
@@ -36,6 +36,7 @@ def syntax(
36
36
  if not os.path.isfile(config_path):
37
37
  console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
38
38
  return
39
+
39
40
  cmd = [nginx_path, "-t", "-c", os.path.abspath(config_path)]
40
41
  if hasattr(os, 'geteuid') and os.geteuid() != 0:
41
42
  cmd = ["sudo"] + cmd
@@ -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 as RichTree
@@ -38,10 +39,10 @@ def tree(
38
39
  tree_obj = parse_nginx_config(config_path)
39
40
  except FileNotFoundError:
40
41
  console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
41
- return
42
+ sys.exit(1)
42
43
  except Exception as e:
43
44
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
44
- return
45
+ sys.exit(1)
45
46
  root = RichTree(f"[bold blue]nginx.conf[/bold blue]")
46
47
  _build_tree(tree_obj.directives, root)
47
48
  if markdown:
@@ -0,0 +1,87 @@
1
+ """
2
+ Модуль для экспорта результатов в CSV формат.
3
+ """
4
+ import csv
5
+ import sys
6
+ from typing import List, Dict, Any
7
+ from io import StringIO
8
+
9
+
10
+ def export_logs_to_csv(
11
+ status_counter,
12
+ path_counter,
13
+ ip_counter,
14
+ user_agent_counter,
15
+ errors: Dict[str, List[str]],
16
+ response_times: List[Dict[str, Any]] = None,
17
+ anomalies: List[Dict[str, Any]] = None
18
+ ) -> str:
19
+ """
20
+ Экспортирует результаты анализа логов в CSV формат.
21
+
22
+ Args:
23
+ status_counter: Счетчик статусов
24
+ path_counter: Счетчик путей
25
+ ip_counter: Счетчик IP
26
+ user_agent_counter: Счетчик User-Agent
27
+ errors: Словарь ошибок по статусам
28
+ response_times: Список данных о времени ответа
29
+ anomalies: Список аномалий
30
+
31
+ Returns:
32
+ CSV строка
33
+ """
34
+ output = StringIO()
35
+ writer = csv.writer(output)
36
+
37
+ # Топ статусов
38
+ writer.writerow(["Category", "Type", "Value", "Count"])
39
+ writer.writerow(["Status Codes", "", "", ""])
40
+ for status, count in status_counter.most_common():
41
+ writer.writerow(["", "Status", status, count])
42
+
43
+ writer.writerow([])
44
+ writer.writerow(["Paths", "", "", ""])
45
+ for path, count in path_counter.most_common():
46
+ writer.writerow(["", "Path", path, count])
47
+
48
+ writer.writerow([])
49
+ writer.writerow(["IPs", "", "", ""])
50
+ for ip, count in ip_counter.most_common():
51
+ writer.writerow(["", "IP", ip, count])
52
+
53
+ if user_agent_counter:
54
+ writer.writerow([])
55
+ writer.writerow(["User-Agents", "", "", ""])
56
+ for ua, count in user_agent_counter.most_common():
57
+ writer.writerow(["", "User-Agent", ua, count])
58
+
59
+ # Ошибки
60
+ if errors:
61
+ writer.writerow([])
62
+ writer.writerow(["Errors", "", "", ""])
63
+ for status, paths in errors.items():
64
+ writer.writerow(["", f"Error {status}", f"{len(paths)} occurrences", ""])
65
+
66
+ # Response times
67
+ if response_times:
68
+ writer.writerow([])
69
+ writer.writerow(["Response Times", "", "", ""])
70
+ writer.writerow(["", "Metric", "Value", ""])
71
+ for metric, value in response_times.items():
72
+ writer.writerow(["", metric, str(value), ""])
73
+
74
+ # Аномалии
75
+ if anomalies:
76
+ writer.writerow([])
77
+ writer.writerow(["Anomalies", "Type", "Description", "Severity"])
78
+ for anomaly in anomalies:
79
+ writer.writerow([
80
+ "",
81
+ anomaly.get("type", ""),
82
+ anomaly.get("description", ""),
83
+ anomaly.get("severity", "")
84
+ ])
85
+
86
+ return output.getvalue()
87
+