nginx-lens 0.4.0__py3-none-any.whl → 0.5.0__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.
commands/metrics.py ADDED
@@ -0,0 +1,495 @@
1
+ import sys
2
+ from typing import Optional, Dict, Any, List
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from rich.panel import Panel
7
+ from rich import box
8
+ from collections import Counter, defaultdict
9
+
10
+ from parser.nginx_parser import parse_nginx_config
11
+
12
+ app = typer.Typer()
13
+ console = Console()
14
+
15
+
16
+ def _count_blocks(tree, block_type: str) -> int:
17
+ """
18
+ Подсчитывает количество блоков определенного типа в дереве конфигурации.
19
+
20
+ Args:
21
+ tree: Дерево конфигурации Nginx
22
+ block_type: Тип блока (например, 'server', 'location', 'upstream')
23
+
24
+ Returns:
25
+ Количество блоков
26
+ """
27
+ # Для upstream используем get_upstreams()
28
+ if block_type == 'upstream':
29
+ if hasattr(tree, 'get_upstreams'):
30
+ upstreams = tree.get_upstreams()
31
+ return len(upstreams)
32
+ return 0
33
+
34
+ count = 0
35
+
36
+ def traverse(directives):
37
+ nonlocal count
38
+ for directive in directives:
39
+ if directive.get('block') == block_type:
40
+ count += 1
41
+ if 'directives' in directive:
42
+ traverse(directive['directives'])
43
+
44
+ if hasattr(tree, 'directives'):
45
+ traverse(tree.directives)
46
+
47
+ return count
48
+
49
+
50
+ def _count_directives(tree) -> Dict[str, int]:
51
+ """
52
+ Подсчитывает количество директив каждого типа в конфигурации.
53
+
54
+ Args:
55
+ tree: Дерево конфигурации Nginx
56
+
57
+ Returns:
58
+ Словарь {directive_name: count}
59
+ """
60
+ directive_counts = Counter()
61
+
62
+ def traverse(directives):
63
+ for directive in directives:
64
+ if 'directive' in directive:
65
+ directive_counts[directive['directive']] += 1
66
+ if 'directives' in directive:
67
+ traverse(directive['directives'])
68
+
69
+ if hasattr(tree, 'directives'):
70
+ traverse(tree.directives)
71
+
72
+ return dict(directive_counts)
73
+
74
+
75
+ def _get_upstream_servers_count(tree) -> Dict[str, int]:
76
+ """
77
+ Подсчитывает количество серверов в каждом upstream.
78
+
79
+ Args:
80
+ tree: Дерево конфигурации Nginx
81
+
82
+ Returns:
83
+ Словарь {upstream_name: server_count}
84
+ """
85
+ upstreams = tree.get_upstreams() if hasattr(tree, 'get_upstreams') else {}
86
+ return {name: len(servers) for name, servers in upstreams.items()}
87
+
88
+
89
+ def _collect_metrics(config_path: str) -> Dict[str, Any]:
90
+ """
91
+ Собирает метрики о конфигурации Nginx.
92
+
93
+ Args:
94
+ config_path: Путь к конфигурационному файлу
95
+
96
+ Returns:
97
+ Словарь с метриками
98
+ """
99
+ try:
100
+ tree = parse_nginx_config(config_path)
101
+ except FileNotFoundError:
102
+ console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
103
+ sys.exit(1)
104
+ except Exception as e:
105
+ console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
106
+ sys.exit(1)
107
+
108
+ # Подсчет блоков
109
+ upstream_count = _count_blocks(tree, 'upstream')
110
+ server_count = _count_blocks(tree, 'server')
111
+ location_count = _count_blocks(tree, 'location')
112
+
113
+ # Подсчет директив
114
+ directive_counts = _count_directives(tree)
115
+
116
+ # Подсчет серверов в upstream
117
+ upstream_servers = _get_upstream_servers_count(tree)
118
+ total_upstream_servers = sum(upstream_servers.values())
119
+
120
+ # Статистика по server блокам
121
+ server_stats = _get_server_stats(tree)
122
+
123
+ metrics = {
124
+ "config_path": config_path,
125
+ "blocks": {
126
+ "upstream": upstream_count,
127
+ "server": server_count,
128
+ "location": location_count,
129
+ },
130
+ "upstream_servers": {
131
+ "total": total_upstream_servers,
132
+ "by_upstream": upstream_servers,
133
+ },
134
+ "directives": directive_counts,
135
+ "server_stats": server_stats,
136
+ }
137
+
138
+ return metrics
139
+
140
+
141
+ def _get_server_stats(tree) -> Dict[str, Any]:
142
+ """
143
+ Собирает статистику по server блокам.
144
+
145
+ Args:
146
+ tree: Дерево конфигурации Nginx
147
+
148
+ Returns:
149
+ Словарь со статистикой
150
+ """
151
+ stats = {
152
+ "with_ssl": 0,
153
+ "with_http2": 0,
154
+ "listen_ports": Counter(),
155
+ "server_names": [],
156
+ }
157
+
158
+ def traverse(directives):
159
+ current_server = None
160
+ for directive in directives:
161
+ if directive.get('block') == 'server':
162
+ current_server = {
163
+ 'has_ssl': False,
164
+ 'has_http2': False,
165
+ 'listen_ports': [],
166
+ 'server_names': [],
167
+ }
168
+ elif current_server is not None:
169
+ if directive.get('directive') == 'listen':
170
+ args = directive.get('args', '')
171
+ if 'ssl' in args:
172
+ current_server['has_ssl'] = True
173
+ if 'http2' in args:
174
+ current_server['has_http2'] = True
175
+ # Извлекаем порт
176
+ port = _extract_port(args)
177
+ if port:
178
+ current_server['listen_ports'].append(port)
179
+ stats["listen_ports"][port] += 1
180
+ elif directive.get('directive') == 'server_name':
181
+ server_names = directive.get('args', '').split()
182
+ current_server['server_names'].extend(server_names)
183
+ stats["server_names"].extend(server_names)
184
+
185
+ if 'directives' in directive:
186
+ traverse(directive['directives'])
187
+
188
+ if hasattr(tree, 'directives'):
189
+ traverse(tree.directives)
190
+
191
+ # Подсчитываем серверы с SSL и HTTP2
192
+ def count_servers(directives):
193
+ for directive in directives:
194
+ if directive.get('block') == 'server':
195
+ has_ssl = False
196
+ has_http2 = False
197
+ for d in directive.get('directives', []):
198
+ if d.get('directive') == 'listen':
199
+ args = d.get('args', '')
200
+ if 'ssl' in args:
201
+ has_ssl = True
202
+ if 'http2' in args:
203
+ has_http2 = True
204
+ if has_ssl:
205
+ stats["with_ssl"] += 1
206
+ if has_http2:
207
+ stats["with_http2"] += 1
208
+ if 'directives' in directive:
209
+ count_servers(directive['directives'])
210
+
211
+ if hasattr(tree, 'directives'):
212
+ count_servers(tree.directives)
213
+
214
+ stats["listen_ports"] = dict(stats["listen_ports"])
215
+ stats["server_names"] = list(set(stats["server_names"]))
216
+
217
+ return stats
218
+
219
+
220
+ def _extract_port(listen_args: str) -> Optional[int]:
221
+ """
222
+ Извлекает порт из директивы listen.
223
+
224
+ Args:
225
+ listen_args: Аргументы директивы listen
226
+
227
+ Returns:
228
+ Номер порта или None
229
+ """
230
+ import re
231
+ # Ищем порт в формате :80, :443, listen 80, listen 443
232
+ match = re.search(r':(\d+)', listen_args)
233
+ if match:
234
+ return int(match.group(1))
235
+ match = re.search(r'\b(\d+)\b', listen_args)
236
+ if match:
237
+ return int(match.group(1))
238
+ return None
239
+
240
+
241
+ def _export_prometheus(metrics: Dict[str, Any]) -> str:
242
+ """
243
+ Экспортирует метрики в формате Prometheus.
244
+
245
+ Args:
246
+ metrics: Словарь с метриками
247
+
248
+ Returns:
249
+ Строка в формате Prometheus
250
+ """
251
+ lines = []
252
+ lines.append("# HELP nginx_lens_upstream_count Total number of upstream blocks")
253
+ lines.append("# TYPE nginx_lens_upstream_count gauge")
254
+ lines.append(f'nginx_lens_upstream_count{{config="{metrics["config_path"]}"}} {metrics["blocks"]["upstream"]}')
255
+
256
+ lines.append("# HELP nginx_lens_server_count Total number of server blocks")
257
+ lines.append("# TYPE nginx_lens_server_count gauge")
258
+ lines.append(f'nginx_lens_server_count{{config="{metrics["config_path"]}"}} {metrics["blocks"]["server"]}')
259
+
260
+ lines.append("# HELP nginx_lens_location_count Total number of location blocks")
261
+ lines.append("# TYPE nginx_lens_location_count gauge")
262
+ lines.append(f'nginx_lens_location_count{{config="{metrics["config_path"]}"}} {metrics["blocks"]["location"]}')
263
+
264
+ lines.append("# HELP nginx_lens_upstream_servers_total Total number of upstream servers")
265
+ lines.append("# TYPE nginx_lens_upstream_servers_total gauge")
266
+ lines.append(f'nginx_lens_upstream_servers_total{{config="{metrics["config_path"]}"}} {metrics["upstream_servers"]["total"]}')
267
+
268
+ # Метрики по upstream
269
+ for upstream_name, server_count in metrics["upstream_servers"]["by_upstream"].items():
270
+ lines.append(f'nginx_lens_upstream_servers{{config="{metrics["config_path"]}",upstream="{upstream_name}"}} {server_count}')
271
+
272
+ # Метрики по директивам (топ-10)
273
+ top_directives = sorted(metrics["directives"].items(), key=lambda x: x[1], reverse=True)[:10]
274
+ for directive, count in top_directives:
275
+ safe_directive = directive.replace('-', '_').replace('.', '_')
276
+ lines.append(f'nginx_lens_directive_count{{config="{metrics["config_path"]}",directive="{directive}"}} {count}')
277
+
278
+ # Метрики по портам
279
+ for port, count in metrics["server_stats"]["listen_ports"].items():
280
+ lines.append(f'nginx_lens_listen_port_count{{config="{metrics["config_path"]}",port="{port}"}} {count}')
281
+
282
+ # Метрики SSL и HTTP2
283
+ lines.append(f'nginx_lens_servers_with_ssl{{config="{metrics["config_path"]}"}} {metrics["server_stats"]["with_ssl"]}')
284
+ lines.append(f'nginx_lens_servers_with_http2{{config="{metrics["config_path"]}"}} {metrics["server_stats"]["with_http2"]}')
285
+
286
+ return '\n'.join(lines)
287
+
288
+
289
+ def metrics(
290
+ config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
291
+ compare_with: Optional[str] = typer.Option(None, "--compare", "-c", help="Путь к другому конфигу для сравнения"),
292
+ prometheus: bool = typer.Option(False, "--prometheus", "-p", help="Экспортировать в формате Prometheus"),
293
+ json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
294
+ yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
295
+ ):
296
+ """
297
+ Собирает метрики о конфигурации Nginx.
298
+
299
+ Показывает:
300
+ - Количество upstream, server, location блоков
301
+ - Количество серверов в каждом upstream
302
+ - Статистику по директивам
303
+ - Статистику по server блокам (SSL, HTTP2, порты)
304
+
305
+ Поддерживает сравнение метрик между двумя версиями конфига.
306
+ Экспорт в Prometheus format для интеграции с мониторингом.
307
+
308
+ Пример:
309
+ nginx-lens metrics /etc/nginx/nginx.conf
310
+ nginx-lens metrics /etc/nginx/nginx.conf --prometheus
311
+ nginx-lens metrics /etc/nginx/nginx.conf --compare /etc/nginx/nginx.conf.old
312
+ """
313
+ # Собираем метрики
314
+ metrics_data = _collect_metrics(config_path)
315
+
316
+ # Если нужно сравнить с другим конфигом
317
+ if compare_with:
318
+ compare_metrics = _collect_metrics(compare_with)
319
+ metrics_data["comparison"] = _compare_metrics(metrics_data, compare_metrics)
320
+
321
+ # Экспорт в Prometheus
322
+ if prometheus:
323
+ prometheus_output = _export_prometheus(metrics_data)
324
+ console.print(prometheus_output)
325
+ sys.exit(0)
326
+
327
+ # Экспорт в JSON/YAML
328
+ if json or yaml:
329
+ from exporter.json_yaml import print_export
330
+ export_data = {
331
+ "timestamp": __import__('datetime').datetime.now().isoformat(),
332
+ "metrics": metrics_data
333
+ }
334
+ format_type = 'json' if json else 'yaml'
335
+ print_export(export_data, format_type)
336
+ sys.exit(0)
337
+
338
+ # Обычный вывод
339
+ _print_metrics_table(metrics_data, compare_with is not None)
340
+ sys.exit(0)
341
+
342
+
343
+ def _compare_metrics(metrics1: Dict[str, Any], metrics2: Dict[str, Any]) -> Dict[str, Any]:
344
+ """
345
+ Сравнивает метрики двух конфигураций.
346
+
347
+ Args:
348
+ metrics1: Метрики первой конфигурации
349
+ metrics2: Метрики второй конфигурации
350
+
351
+ Returns:
352
+ Словарь с различиями
353
+ """
354
+ comparison = {
355
+ "blocks": {},
356
+ "upstream_servers": {},
357
+ "directives": {},
358
+ }
359
+
360
+ # Сравнение блоков
361
+ for block_type in ["upstream", "server", "location"]:
362
+ count1 = metrics1["blocks"][block_type]
363
+ count2 = metrics2["blocks"][block_type]
364
+ diff = count1 - count2
365
+ comparison["blocks"][block_type] = {
366
+ "current": count1,
367
+ "previous": count2,
368
+ "diff": diff,
369
+ "percent_change": (diff / count2 * 100) if count2 > 0 else 0
370
+ }
371
+
372
+ # Сравнение upstream серверов
373
+ total1 = metrics1["upstream_servers"]["total"]
374
+ total2 = metrics2["upstream_servers"]["total"]
375
+ comparison["upstream_servers"]["total"] = {
376
+ "current": total1,
377
+ "previous": total2,
378
+ "diff": total1 - total2,
379
+ "percent_change": ((total1 - total2) / total2 * 100) if total2 > 0 else 0
380
+ }
381
+
382
+ # Сравнение директив (топ-10)
383
+ all_directives = set(metrics1["directives"].keys()) | set(metrics2["directives"].keys())
384
+ top_directives = sorted(all_directives, key=lambda d: metrics1["directives"].get(d, 0) + metrics2["directives"].get(d, 0), reverse=True)[:10]
385
+
386
+ for directive in top_directives:
387
+ count1 = metrics1["directives"].get(directive, 0)
388
+ count2 = metrics2["directives"].get(directive, 0)
389
+ diff = count1 - count2
390
+ comparison["directives"][directive] = {
391
+ "current": count1,
392
+ "previous": count2,
393
+ "diff": diff,
394
+ "percent_change": (diff / count2 * 100) if count2 > 0 else 0
395
+ }
396
+
397
+ return comparison
398
+
399
+
400
+ def _print_metrics_table(metrics_data: Dict[str, Any], show_comparison: bool = False):
401
+ """
402
+ Выводит метрики в виде таблицы.
403
+
404
+ Args:
405
+ metrics_data: Словарь с метриками
406
+ show_comparison: Показывать ли сравнение
407
+ """
408
+ console.print(Panel("[bold blue]Метрики конфигурации Nginx[/bold blue]", box=box.ROUNDED))
409
+
410
+ # Таблица блоков
411
+ table = Table(title="Блоки", show_header=True, header_style="bold blue")
412
+ table.add_column("Тип блока")
413
+ table.add_column("Количество")
414
+
415
+ if show_comparison:
416
+ table.add_column("Предыдущее")
417
+ table.add_column("Изменение")
418
+
419
+ blocks = metrics_data["blocks"]
420
+ comparison = metrics_data.get("comparison", {})
421
+
422
+ for block_type in ["upstream", "server", "location"]:
423
+ count = blocks[block_type]
424
+ if show_comparison and "blocks" in comparison:
425
+ prev = comparison["blocks"].get(block_type, {}).get("previous", 0)
426
+ diff = comparison["blocks"].get(block_type, {}).get("diff", 0)
427
+ color = "green" if diff >= 0 else "red"
428
+ table.add_row(
429
+ block_type,
430
+ str(count),
431
+ str(prev),
432
+ f"[{color}]{diff:+d}[/{color}]"
433
+ )
434
+ else:
435
+ table.add_row(block_type, str(count))
436
+
437
+ console.print(table)
438
+
439
+ # Таблица upstream серверов
440
+ if metrics_data["upstream_servers"]["by_upstream"]:
441
+ table = Table(title="Upstream серверы", show_header=True, header_style="bold blue")
442
+ table.add_column("Upstream")
443
+ table.add_column("Количество серверов")
444
+
445
+ for upstream_name, server_count in sorted(metrics_data["upstream_servers"]["by_upstream"].items()):
446
+ table.add_row(upstream_name, str(server_count))
447
+
448
+ table.add_row("[bold]Всего[/bold]", f"[bold]{metrics_data['upstream_servers']['total']}[/bold]")
449
+ console.print(table)
450
+
451
+ # Таблица топ-директив
452
+ top_directives = sorted(metrics_data["directives"].items(), key=lambda x: x[1], reverse=True)[:10]
453
+ if top_directives:
454
+ table = Table(title="Топ-10 директив", show_header=True, header_style="bold blue")
455
+ table.add_column("Директива")
456
+ table.add_column("Количество")
457
+
458
+ if show_comparison and "directives" in comparison:
459
+ table.add_column("Предыдущее")
460
+ table.add_column("Изменение")
461
+
462
+ for directive, count in top_directives:
463
+ if show_comparison and directive in comparison["directives"]:
464
+ comp = comparison["directives"][directive]
465
+ prev = comp["previous"]
466
+ diff = comp["diff"]
467
+ color = "green" if diff >= 0 else "red"
468
+ table.add_row(
469
+ directive,
470
+ str(count),
471
+ str(prev),
472
+ f"[{color}]{diff:+d}[/{color}]"
473
+ )
474
+ else:
475
+ table.add_row(directive, str(count))
476
+
477
+ console.print(table)
478
+
479
+ # Статистика по server блокам
480
+ server_stats = metrics_data["server_stats"]
481
+ if server_stats["listen_ports"] or server_stats["with_ssl"] > 0 or server_stats["with_http2"] > 0:
482
+ table = Table(title="Статистика server блоков", show_header=True, header_style="bold blue")
483
+ table.add_column("Метрика")
484
+ table.add_column("Значение")
485
+
486
+ table.add_row("С SSL", str(server_stats["with_ssl"]))
487
+ table.add_row("С HTTP/2", str(server_stats["with_http2"]))
488
+
489
+ if server_stats["listen_ports"]:
490
+ top_ports = sorted(server_stats["listen_ports"].items(), key=lambda x: x[1], reverse=True)[:5]
491
+ for port, count in top_ports:
492
+ table.add_row(f"Порт {port}", str(count))
493
+
494
+ console.print(table)
495
+
commands/resolve.py CHANGED
@@ -1,30 +1,55 @@
1
1
  import sys
2
+ from typing import Optional
2
3
  import typer
3
4
  from rich.console import Console
4
5
  from rich.table import Table
5
6
  from upstream_checker.checker import resolve_upstreams
7
+ from upstream_checker.dns_cache import disable_cache, enable_cache, clear_cache
6
8
  from parser.nginx_parser import parse_nginx_config
7
9
  from exporter.json_yaml import format_resolve_results, print_export
10
+ from config.config_loader import get_config
11
+ from utils.progress import ProgressManager
8
12
 
9
13
  app = typer.Typer()
10
14
  console = Console()
11
15
 
12
16
  def resolve(
13
17
  config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
14
- max_workers: int = typer.Option(10, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
18
+ max_workers: Optional[int] = typer.Option(None, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
15
19
  json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
16
20
  yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
21
+ no_cache: bool = typer.Option(False, "--no-cache", help="Отключить кэширование DNS резолвинга"),
22
+ cache_ttl: Optional[int] = typer.Option(None, "--cache-ttl", help="Время жизни кэша в секундах"),
17
23
  ):
18
24
  """
19
25
  Резолвит DNS имена upstream-серверов в IP-адреса.
20
26
  Использует параллельную обработку для ускорения резолвинга множества upstream серверов.
27
+ Поддерживает кэширование результатов DNS резолвинга для ускорения повторных запусков.
21
28
 
22
29
  Пример:
23
30
  nginx-lens resolve /etc/nginx/nginx.conf
24
31
  nginx-lens resolve /etc/nginx/nginx.conf --max-workers 20
32
+ nginx-lens resolve /etc/nginx/nginx.conf --no-cache
33
+ nginx-lens resolve /etc/nginx/nginx.conf --cache-ttl 600
25
34
  """
26
35
  exit_code = 0
27
36
 
37
+ # Загружаем конфигурацию
38
+ config = get_config()
39
+ defaults = config.get_defaults()
40
+ cache_config = config.get_cache_config()
41
+
42
+ # Применяем значения из конфига, если не указаны через CLI
43
+ max_workers = max_workers if max_workers is not None else defaults.get("max_workers", 10)
44
+ cache_ttl = cache_ttl if cache_ttl is not None else cache_config.get("ttl", defaults.get("dns_cache_ttl", 300))
45
+
46
+ # Управление кэшем
47
+ use_cache = not no_cache and cache_config.get("enabled", True)
48
+ if no_cache:
49
+ disable_cache()
50
+ else:
51
+ enable_cache()
52
+
28
53
  try:
29
54
  tree = parse_nginx_config(config_path)
30
55
  except FileNotFoundError:
@@ -48,7 +73,12 @@ def resolve(
48
73
  console.print("[yellow]Не найдено ни одного upstream в конфигурации.[/yellow]")
49
74
  sys.exit(0) # Нет upstream - это не ошибка, просто нет чего проверять
50
75
 
51
- results = resolve_upstreams(upstreams, max_workers=max_workers)
76
+ # Подсчитываем общее количество серверов для прогресс-бара
77
+ total_servers = sum(len(servers) for servers in upstreams.values())
78
+
79
+ # Резолвинг с прогресс-баром
80
+ with ProgressManager(description="Резолвинг DNS", show_progress=total_servers > 5) as pm:
81
+ results = resolve_upstreams(upstreams, max_workers=max_workers, use_cache=use_cache, cache_ttl=cache_ttl, progress_manager=pm)
52
82
 
53
83
  # Экспорт в JSON/YAML
54
84
  if json or yaml: