nginx-lens 0.3.4__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/analyze.py +9 -3
- commands/cli.py +6 -0
- commands/completion.py +174 -0
- commands/diff.py +3 -2
- commands/graph.py +3 -2
- commands/health.py +63 -6
- commands/include.py +3 -2
- commands/logs.py +267 -19
- commands/metrics.py +495 -0
- commands/resolve.py +58 -3
- commands/route.py +1 -0
- commands/syntax.py +1 -0
- commands/tree.py +3 -2
- commands/validate.py +451 -0
- config/__init__.py +4 -0
- config/config_loader.py +200 -0
- exporter/csv.py +87 -0
- exporter/json_yaml.py +361 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/METADATA +6 -1
- nginx_lens-0.5.0.dist-info/RECORD +48 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/top_level.txt +2 -0
- upstream_checker/checker.py +47 -7
- upstream_checker/dns_cache.py +216 -0
- utils/__init__.py +4 -0
- utils/progress.py +120 -0
- nginx_lens-0.3.4.dist-info/RECORD +0 -38
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/WHEEL +0 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/entry_points.txt +0 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль для кэширования результатов DNS резолвинга.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import hashlib
|
|
8
|
+
from typing import Optional, List, Dict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DNSCache:
|
|
13
|
+
"""
|
|
14
|
+
Кэш для результатов DNS резолвинга.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, cache_dir: Optional[str] = None, ttl: int = 300):
|
|
18
|
+
"""
|
|
19
|
+
Инициализация кэша.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cache_dir: Директория для хранения кэша (по умолчанию ~/.cache/nginx-lens/)
|
|
23
|
+
ttl: Время жизни кэша в секундах (по умолчанию 5 минут)
|
|
24
|
+
"""
|
|
25
|
+
self.ttl = ttl
|
|
26
|
+
|
|
27
|
+
if cache_dir:
|
|
28
|
+
self.cache_dir = Path(cache_dir)
|
|
29
|
+
else:
|
|
30
|
+
# Используем ~/.cache/nginx-lens/ или /tmp/nginx-lens-cache/
|
|
31
|
+
home_cache = Path.home() / ".cache" / "nginx-lens"
|
|
32
|
+
tmp_cache = Path("/tmp") / "nginx-lens-cache"
|
|
33
|
+
|
|
34
|
+
# Пробуем использовать домашнюю директорию, если доступна
|
|
35
|
+
try:
|
|
36
|
+
home_cache.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
self.cache_dir = home_cache
|
|
38
|
+
except (OSError, PermissionError):
|
|
39
|
+
# Fallback на /tmp
|
|
40
|
+
try:
|
|
41
|
+
tmp_cache.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
self.cache_dir = tmp_cache
|
|
43
|
+
except (OSError, PermissionError):
|
|
44
|
+
# Последний fallback - текущая директория
|
|
45
|
+
self.cache_dir = Path.cwd() / ".nginx-lens-cache"
|
|
46
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
self.cache_file = self.cache_dir / "dns_cache.json"
|
|
49
|
+
self._cache: Dict[str, Dict] = {}
|
|
50
|
+
self._load_cache()
|
|
51
|
+
|
|
52
|
+
def _get_cache_key(self, host: str, port: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Генерирует ключ кэша для host:port.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
host: Имя хоста
|
|
58
|
+
port: Порт
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Хеш ключ для кэша
|
|
62
|
+
"""
|
|
63
|
+
key = f"{host}:{port}"
|
|
64
|
+
return hashlib.md5(key.encode()).hexdigest()
|
|
65
|
+
|
|
66
|
+
def _load_cache(self):
|
|
67
|
+
"""Загружает кэш из файла."""
|
|
68
|
+
if self.cache_file.exists():
|
|
69
|
+
try:
|
|
70
|
+
with open(self.cache_file, 'r') as f:
|
|
71
|
+
self._cache = json.load(f)
|
|
72
|
+
except (json.JSONDecodeError, IOError):
|
|
73
|
+
self._cache = {}
|
|
74
|
+
|
|
75
|
+
def _save_cache(self):
|
|
76
|
+
"""Сохраняет кэш в файл."""
|
|
77
|
+
try:
|
|
78
|
+
with open(self.cache_file, 'w') as f:
|
|
79
|
+
json.dump(self._cache, f)
|
|
80
|
+
except IOError:
|
|
81
|
+
# Игнорируем ошибки записи
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def get(self, host: str, port: str) -> Optional[List[str]]:
|
|
85
|
+
"""
|
|
86
|
+
Получает результат из кэша.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
host: Имя хоста
|
|
90
|
+
port: Порт
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Список резолвленных IP-адресов или None, если нет в кэше или истек TTL
|
|
94
|
+
"""
|
|
95
|
+
key = self._get_cache_key(host, port)
|
|
96
|
+
|
|
97
|
+
if key not in self._cache:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
cached_data = self._cache[key]
|
|
101
|
+
cached_time = cached_data.get('timestamp', 0)
|
|
102
|
+
current_time = time.time()
|
|
103
|
+
|
|
104
|
+
# Проверяем TTL
|
|
105
|
+
if current_time - cached_time > self.ttl:
|
|
106
|
+
# Удаляем устаревшую запись
|
|
107
|
+
del self._cache[key]
|
|
108
|
+
self._save_cache()
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
return cached_data.get('result')
|
|
112
|
+
|
|
113
|
+
def set(self, host: str, port: str, result: List[str]):
|
|
114
|
+
"""
|
|
115
|
+
Сохраняет результат в кэш.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
host: Имя хоста
|
|
119
|
+
port: Порт
|
|
120
|
+
result: Список резолвленных IP-адресов
|
|
121
|
+
"""
|
|
122
|
+
key = self._get_cache_key(host, port)
|
|
123
|
+
|
|
124
|
+
self._cache[key] = {
|
|
125
|
+
'timestamp': time.time(),
|
|
126
|
+
'result': result,
|
|
127
|
+
'host': host,
|
|
128
|
+
'port': port
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
self._save_cache()
|
|
132
|
+
|
|
133
|
+
def clear(self):
|
|
134
|
+
"""Очищает весь кэш."""
|
|
135
|
+
self._cache = {}
|
|
136
|
+
if self.cache_file.exists():
|
|
137
|
+
try:
|
|
138
|
+
self.cache_file.unlink()
|
|
139
|
+
except IOError:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def get_cache_info(self) -> Dict[str, any]:
|
|
143
|
+
"""
|
|
144
|
+
Возвращает информацию о кэше.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Словарь с информацией о кэше
|
|
148
|
+
"""
|
|
149
|
+
current_time = time.time()
|
|
150
|
+
valid_entries = 0
|
|
151
|
+
expired_entries = 0
|
|
152
|
+
|
|
153
|
+
for key, data in self._cache.items():
|
|
154
|
+
cached_time = data.get('timestamp', 0)
|
|
155
|
+
if current_time - cached_time <= self.ttl:
|
|
156
|
+
valid_entries += 1
|
|
157
|
+
else:
|
|
158
|
+
expired_entries += 1
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
'total_entries': len(self._cache),
|
|
162
|
+
'valid_entries': valid_entries,
|
|
163
|
+
'expired_entries': expired_entries,
|
|
164
|
+
'cache_dir': str(self.cache_dir),
|
|
165
|
+
'ttl': self.ttl
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Глобальный экземпляр кэша (будет инициализирован при первом использовании)
|
|
170
|
+
_cache_instance: Optional[DNSCache] = None
|
|
171
|
+
_cache_enabled = True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_cache(ttl: int = 300, cache_dir: Optional[str] = None) -> DNSCache:
|
|
175
|
+
"""
|
|
176
|
+
Получает глобальный экземпляр кэша.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
ttl: Время жизни кэша в секундах
|
|
180
|
+
cache_dir: Директория для кэша
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Экземпляр DNSCache
|
|
184
|
+
"""
|
|
185
|
+
global _cache_instance
|
|
186
|
+
if _cache_instance is None:
|
|
187
|
+
_cache_instance = DNSCache(cache_dir=cache_dir, ttl=ttl)
|
|
188
|
+
elif ttl != _cache_instance.ttl:
|
|
189
|
+
# Обновляем TTL если изменился
|
|
190
|
+
_cache_instance.ttl = ttl
|
|
191
|
+
return _cache_instance
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def clear_cache():
|
|
195
|
+
"""Очищает глобальный кэш."""
|
|
196
|
+
global _cache_instance
|
|
197
|
+
if _cache_instance:
|
|
198
|
+
_cache_instance.clear()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def disable_cache():
|
|
202
|
+
"""Отключает кэширование."""
|
|
203
|
+
global _cache_enabled
|
|
204
|
+
_cache_enabled = False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def enable_cache():
|
|
208
|
+
"""Включает кэширование."""
|
|
209
|
+
global _cache_enabled
|
|
210
|
+
_cache_enabled = True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def is_cache_enabled() -> bool:
|
|
214
|
+
"""Проверяет, включено ли кэширование."""
|
|
215
|
+
return _cache_enabled
|
|
216
|
+
|
utils/__init__.py
ADDED
utils/progress.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Утилиты для отображения прогресса операций.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Callable, Any
|
|
5
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn, TaskID
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProgressManager:
|
|
14
|
+
"""
|
|
15
|
+
Менеджер для отображения прогресса операций с поддержкой отмены.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, description: str = "Обработка", show_progress: bool = True):
|
|
19
|
+
"""
|
|
20
|
+
Инициализация менеджера прогресса.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
description: Описание операции
|
|
24
|
+
show_progress: Показывать ли прогресс-бар
|
|
25
|
+
"""
|
|
26
|
+
self.description = description
|
|
27
|
+
self.show_progress = show_progress
|
|
28
|
+
self.progress: Optional[Progress] = None
|
|
29
|
+
self.task_id: Optional[TaskID] = None
|
|
30
|
+
self.cancelled = False
|
|
31
|
+
|
|
32
|
+
# Обработка сигнала прерывания
|
|
33
|
+
self._original_sigint = signal.signal(signal.SIGINT, self._handle_interrupt)
|
|
34
|
+
|
|
35
|
+
def _handle_interrupt(self, signum, frame):
|
|
36
|
+
"""Обработка прерывания (Ctrl+C)."""
|
|
37
|
+
self.cancelled = True
|
|
38
|
+
if self.progress:
|
|
39
|
+
self.progress.stop()
|
|
40
|
+
console.print("\n[yellow]Операция отменена пользователем[/yellow]")
|
|
41
|
+
sys.exit(130) # Стандартный exit code для SIGINT
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
"""Вход в контекстный менеджер."""
|
|
45
|
+
if self.show_progress:
|
|
46
|
+
self.progress = Progress(
|
|
47
|
+
SpinnerColumn(),
|
|
48
|
+
TextColumn("[progress.description]{task.description}"),
|
|
49
|
+
BarColumn(),
|
|
50
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
51
|
+
TimeElapsedColumn(),
|
|
52
|
+
TimeRemainingColumn(),
|
|
53
|
+
console=console,
|
|
54
|
+
transient=False,
|
|
55
|
+
)
|
|
56
|
+
self.progress.start()
|
|
57
|
+
self.task_id = self.progress.add_task(self.description, total=None)
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
61
|
+
"""Выход из контекстного менеджера."""
|
|
62
|
+
if self.progress:
|
|
63
|
+
self.progress.stop()
|
|
64
|
+
# Восстанавливаем оригинальный обработчик сигнала
|
|
65
|
+
signal.signal(signal.SIGINT, self._original_sigint)
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def update(self, completed: int, total: Optional[int] = None, description: Optional[str] = None):
|
|
69
|
+
"""
|
|
70
|
+
Обновляет прогресс.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
completed: Количество завершенных задач
|
|
74
|
+
total: Общее количество задач (если None, используется спиннер)
|
|
75
|
+
description: Описание текущей операции
|
|
76
|
+
"""
|
|
77
|
+
if self.progress and self.task_id is not None:
|
|
78
|
+
if total is not None:
|
|
79
|
+
self.progress.update(self.task_id, total=total, completed=completed)
|
|
80
|
+
if description:
|
|
81
|
+
self.progress.update(self.task_id, description=description)
|
|
82
|
+
|
|
83
|
+
def advance(self, advance: int = 1):
|
|
84
|
+
"""
|
|
85
|
+
Увеличивает прогресс на указанное значение.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
advance: На сколько увеличить прогресс
|
|
89
|
+
"""
|
|
90
|
+
if self.progress and self.task_id is not None:
|
|
91
|
+
self.progress.advance(self.task_id, advance)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def with_progress(
|
|
95
|
+
description: str,
|
|
96
|
+
total: Optional[int] = None,
|
|
97
|
+
show_progress: bool = True
|
|
98
|
+
) -> Callable:
|
|
99
|
+
"""
|
|
100
|
+
Декоратор для добавления прогресс-бара к функции.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
description: Описание операции
|
|
104
|
+
total: Общее количество итераций (если известно)
|
|
105
|
+
show_progress: Показывать ли прогресс-бар
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Декорированная функция
|
|
109
|
+
"""
|
|
110
|
+
def decorator(func: Callable) -> Callable:
|
|
111
|
+
def wrapper(*args, **kwargs):
|
|
112
|
+
with ProgressManager(description=description, show_progress=show_progress) as pm:
|
|
113
|
+
# Передаем менеджер прогресса в функцию
|
|
114
|
+
kwargs['progress_manager'] = pm
|
|
115
|
+
if total is not None:
|
|
116
|
+
pm.update(0, total=total)
|
|
117
|
+
return func(*args, **kwargs)
|
|
118
|
+
return wrapper
|
|
119
|
+
return decorator
|
|
120
|
+
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
analyzer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
analyzer/base.py,sha256=oGKg78BfMVmuzYafc08oq9p31-jEgYolGjLkUcIdkN8,607
|
|
3
|
-
analyzer/conflicts.py,sha256=NSNZc8e2x51K41dflSUvuwlDq-rzBXU5ITi6WfxFbfU,2796
|
|
4
|
-
analyzer/dead_locations.py,sha256=uvMu5qBGTVi0Nn960x3WpRvTljGbQuVFivU4nfe36oY,1435
|
|
5
|
-
analyzer/diff.py,sha256=idvXnoLzBVUYgKi_s3uDu0v2GNMV3B8aDqTROXcdQdo,1749
|
|
6
|
-
analyzer/duplicates.py,sha256=jpy_6k-BzWxaXFt2Wb3rlulIXUEzbFe9xYRm7rWR50U,1215
|
|
7
|
-
analyzer/empty_blocks.py,sha256=7Zu4-5I5PS3bjhH0Ppq1CvM7rMTeRIc4fHx5n5vkMIw,517
|
|
8
|
-
analyzer/include.py,sha256=FhKR4VsogLknykjLD2N8jX9OtwxZcWik5oPpvp-_luE,2465
|
|
9
|
-
analyzer/rewrite.py,sha256=-jSLLG1jqmGU-dXWvU6NHCW6muB8Lfro6fXX1tDCHCQ,1834
|
|
10
|
-
analyzer/route.py,sha256=71dkmQaTrHqDTf4Up5gAsrgmgktNpXqWmxr7-0RAVtg,2370
|
|
11
|
-
analyzer/unused.py,sha256=Ixzv0bPsw9IafblVwLiAOgugdg2dGu1MJDtuoqzPZiY,1066
|
|
12
|
-
analyzer/warnings.py,sha256=zC36QMvegA2eQPvZ-P1eysrX_kXHx5A1MUKHKKNvG5c,5784
|
|
13
|
-
commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
commands/analyze.py,sha256=W6begSgXNjgKJGoGeguR3WKgHPLkClWXxxpDcqvsJdc,8343
|
|
15
|
-
commands/cli.py,sha256=brzp6xDDWIrm7ibaoT4x94hgAdBB2DVWniXoK8dRylE,782
|
|
16
|
-
commands/diff.py,sha256=C7gRIWh6DNWHzjiQBPVTn-rZ40m2KCY75Zd6Q4URJIE,2076
|
|
17
|
-
commands/graph.py,sha256=xB6KjXBkLmm5gII3e-5BMRGO7WeTwc7EFxRGzYgnme4,5947
|
|
18
|
-
commands/health.py,sha256=lxrvuD-ClMLpTJ7gs7kn50fXwA4uBlHMPWA1ZxdEtAE,4167
|
|
19
|
-
commands/include.py,sha256=5PTYG5C00-AlWfIgpQXLq9E7C9yTFSv7HrZkM5ogDps,2224
|
|
20
|
-
commands/logs.py,sha256=RkPUdIpbO9dOVL56lemreYRuAjMjcqqMxRCKOFv2gC4,3691
|
|
21
|
-
commands/resolve.py,sha256=MRruIH46tIelUyyrdrF70ai-tluuEJ13Jcj2nyRCSPA,2820
|
|
22
|
-
commands/route.py,sha256=-x_71u6ENl3iO-oxK3bdE8v5eZKf4xRCydeUyXMFVrY,3163
|
|
23
|
-
commands/syntax.py,sha256=ZWFdaL8LVv9S694wlk2aV3HJKb0OSKjw3wNgTlNvFR8,3418
|
|
24
|
-
commands/tree.py,sha256=mDfx0Aeg1EDQSYQoJ2nJIkSd_uP7ZR7pEqy7Cw3clQ0,2161
|
|
25
|
-
exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
-
exporter/graph.py,sha256=WYUrqUgCaK6KihgxAcRHaQn4oMo6b7ybC8yb_36ZIsA,3995
|
|
27
|
-
exporter/html.py,sha256=uquEM-WvBt2aV9GshgaI3UVhYd8sD0QQ-OmuNtvYUdU,798
|
|
28
|
-
exporter/markdown.py,sha256=_0mXQIhurGEZ0dO-eq9DbsuKNrgEDIblgtL3DAgYNo8,724
|
|
29
|
-
nginx_lens-0.3.4.dist-info/licenses/LICENSE,sha256=g8QXKdvZZC56rU8E12vIeYF6R4jeTWOsblOnYAda3K4,1073
|
|
30
|
-
parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
-
parser/nginx_parser.py,sha256=Sa9FtGAkvTqNzoehBvgLUWPJHLLIZYWH9ugSHW50X8s,3699
|
|
32
|
-
upstream_checker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
upstream_checker/checker.py,sha256=i3L6XqUHUH5hcyLq5PXx6wOyjzEL_Z7xYCA3FffFOrU,11257
|
|
34
|
-
nginx_lens-0.3.4.dist-info/METADATA,sha256=E7ce-3zNXvNVp7-y_TvlnmtCTIjMhid2sdqzbrpgy8U,552
|
|
35
|
-
nginx_lens-0.3.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
36
|
-
nginx_lens-0.3.4.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
|
|
37
|
-
nginx_lens-0.3.4.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
|
|
38
|
-
nginx_lens-0.3.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|