nginx-lens 0.3.2__py3-none-any.whl → 0.3.4__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.

Potentially problematic release.


This version of nginx-lens might be problematic. Click here for more details.

commands/health.py CHANGED
@@ -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
@@ -13,31 +14,36 @@ def health(
13
14
  retries: int = typer.Option(1, help="Количество попыток"),
14
15
  mode: str = typer.Option("tcp", help="Режим проверки: tcp или http", case_sensitive=False),
15
16
  resolve: bool = typer.Option(False, "--resolve", "-r", help="Показать резолвленные IP-адреса"),
17
+ max_workers: int = typer.Option(10, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
16
18
  ):
17
19
  """
18
20
  Проверяет доступность upstream-серверов, определённых в nginx.conf. Выводит таблицу.
21
+ Использует параллельную обработку для ускорения проверки множества upstream серверов.
19
22
 
20
23
  Пример:
21
24
  nginx-lens health /etc/nginx/nginx.conf
22
25
  nginx-lens health /etc/nginx/nginx.conf --timeout 5 --retries 3 --mode http
23
26
  nginx-lens health /etc/nginx/nginx.conf --resolve
27
+ nginx-lens health /etc/nginx/nginx.conf --max-workers 20
24
28
  """
29
+ exit_code = 0
30
+
25
31
  try:
26
32
  tree = parse_nginx_config(config_path)
27
33
  except FileNotFoundError:
28
34
  console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
29
- return
35
+ sys.exit(1)
30
36
  except Exception as e:
31
37
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
32
- return
38
+ sys.exit(1)
33
39
 
34
40
  upstreams = tree.get_upstreams()
35
- results = check_upstreams(upstreams, timeout=timeout, retries=retries, mode=mode.lower())
41
+ results = check_upstreams(upstreams, timeout=timeout, retries=retries, mode=mode.lower(), max_workers=max_workers)
36
42
 
37
43
  # Если нужно показать резолвленные IP-адреса
38
44
  resolved_info = {}
39
45
  if resolve:
40
- resolved_info = resolve_upstreams(upstreams)
46
+ resolved_info = resolve_upstreams(upstreams, max_workers=max_workers)
41
47
 
42
48
  table = Table(show_header=True, header_style="bold blue")
43
49
  table.add_column("Address")
@@ -50,6 +56,10 @@ def health(
50
56
  status = "Healthy" if srv["healthy"] else "Unhealthy"
51
57
  color = "green" if srv["healthy"] else "red"
52
58
 
59
+ # Проверяем статус здоровья
60
+ if not srv["healthy"]:
61
+ exit_code = 1
62
+
53
63
  if resolve:
54
64
  resolved_list = []
55
65
  if name in resolved_info:
@@ -64,11 +74,14 @@ def health(
64
74
  # Если есть "invalid resolve", показываем красным, иначе зеленым
65
75
  if any("invalid resolve" in r for r in resolved_list):
66
76
  table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[red]{resolved_str}[/red]")
77
+ exit_code = 1
67
78
  else:
68
79
  table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[green]{resolved_str}[/green]")
69
80
  else:
70
81
  table.add_row(srv["address"], f"[{color}]{status}[/{color}]", "[yellow]Failed to resolve[/yellow]")
82
+ exit_code = 1
71
83
  else:
72
84
  table.add_row(srv["address"], f"[{color}]{status}[/{color}]")
73
85
 
74
86
  console.print(table)
87
+ sys.exit(exit_code)
commands/resolve.py CHANGED
@@ -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,28 +10,33 @@ console = Console()
9
10
 
10
11
  def resolve(
11
12
  config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
13
+ max_workers: int = typer.Option(10, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
12
14
  ):
13
15
  """
14
16
  Резолвит DNS имена upstream-серверов в IP-адреса.
17
+ Использует параллельную обработку для ускорения резолвинга множества upstream серверов.
15
18
 
16
19
  Пример:
17
20
  nginx-lens resolve /etc/nginx/nginx.conf
21
+ nginx-lens resolve /etc/nginx/nginx.conf --max-workers 20
18
22
  """
23
+ exit_code = 0
24
+
19
25
  try:
20
26
  tree = parse_nginx_config(config_path)
21
27
  except FileNotFoundError:
22
28
  console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
23
- return
29
+ sys.exit(1)
24
30
  except Exception as e:
25
31
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
26
- return
32
+ sys.exit(1)
27
33
 
28
34
  upstreams = tree.get_upstreams()
29
35
  if not upstreams:
30
36
  console.print("[yellow]Не найдено ни одного upstream в конфигурации.[/yellow]")
31
- return
37
+ sys.exit(0) # Нет upstream - это не ошибка, просто нет чего проверять
32
38
 
33
- results = resolve_upstreams(upstreams)
39
+ results = resolve_upstreams(upstreams, max_workers=max_workers)
34
40
 
35
41
  table = Table(show_header=True, header_style="bold blue")
36
42
  table.add_column("Upstream Name")
@@ -47,10 +53,13 @@ def resolve(
47
53
  # Если есть "invalid resolve", показываем красным, иначе зеленым
48
54
  if any("invalid resolve" in r for r in resolved_list):
49
55
  table.add_row(upstream_name, srv["address"], f"[red]{resolved_str}[/red]")
56
+ exit_code = 1
50
57
  else:
51
58
  table.add_row(upstream_name, srv["address"], f"[green]{resolved_str}[/green]")
52
59
  else:
53
60
  table.add_row(upstream_name, srv["address"], "[red]Failed to resolve[/red]")
61
+ exit_code = 1
54
62
 
55
63
  console.print(table)
64
+ sys.exit(exit_code)
56
65
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nginx-lens
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
5
5
  Author: Daniil Astrouski
6
6
  Author-email: shelovesuastra@gmail.com
@@ -15,10 +15,10 @@ commands/analyze.py,sha256=W6begSgXNjgKJGoGeguR3WKgHPLkClWXxxpDcqvsJdc,8343
15
15
  commands/cli.py,sha256=brzp6xDDWIrm7ibaoT4x94hgAdBB2DVWniXoK8dRylE,782
16
16
  commands/diff.py,sha256=C7gRIWh6DNWHzjiQBPVTn-rZ40m2KCY75Zd6Q4URJIE,2076
17
17
  commands/graph.py,sha256=xB6KjXBkLmm5gII3e-5BMRGO7WeTwc7EFxRGzYgnme4,5947
18
- commands/health.py,sha256=JAlUKx2kXhi2eigSQXG7GxuQMPydYzojgV2qaRcoLWw,3429
18
+ commands/health.py,sha256=lxrvuD-ClMLpTJ7gs7kn50fXwA4uBlHMPWA1ZxdEtAE,4167
19
19
  commands/include.py,sha256=5PTYG5C00-AlWfIgpQXLq9E7C9yTFSv7HrZkM5ogDps,2224
20
20
  commands/logs.py,sha256=RkPUdIpbO9dOVL56lemreYRuAjMjcqqMxRCKOFv2gC4,3691
21
- commands/resolve.py,sha256=LVfp-cva-XlKOOIS5Da908mBGgjq1DtZlsahMUylAsk,2151
21
+ commands/resolve.py,sha256=MRruIH46tIelUyyrdrF70ai-tluuEJ13Jcj2nyRCSPA,2820
22
22
  commands/route.py,sha256=-x_71u6ENl3iO-oxK3bdE8v5eZKf4xRCydeUyXMFVrY,3163
23
23
  commands/syntax.py,sha256=ZWFdaL8LVv9S694wlk2aV3HJKb0OSKjw3wNgTlNvFR8,3418
24
24
  commands/tree.py,sha256=mDfx0Aeg1EDQSYQoJ2nJIkSd_uP7ZR7pEqy7Cw3clQ0,2161
@@ -26,13 +26,13 @@ exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  exporter/graph.py,sha256=WYUrqUgCaK6KihgxAcRHaQn4oMo6b7ybC8yb_36ZIsA,3995
27
27
  exporter/html.py,sha256=uquEM-WvBt2aV9GshgaI3UVhYd8sD0QQ-OmuNtvYUdU,798
28
28
  exporter/markdown.py,sha256=_0mXQIhurGEZ0dO-eq9DbsuKNrgEDIblgtL3DAgYNo8,724
29
- nginx_lens-0.3.2.dist-info/licenses/LICENSE,sha256=g8QXKdvZZC56rU8E12vIeYF6R4jeTWOsblOnYAda3K4,1073
29
+ nginx_lens-0.3.4.dist-info/licenses/LICENSE,sha256=g8QXKdvZZC56rU8E12vIeYF6R4jeTWOsblOnYAda3K4,1073
30
30
  parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  parser/nginx_parser.py,sha256=Sa9FtGAkvTqNzoehBvgLUWPJHLLIZYWH9ugSHW50X8s,3699
32
32
  upstream_checker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- upstream_checker/checker.py,sha256=ZwAlIW6yh0OVdeI44dpZUeSUbByE04gBxTUsRMRUgU8,9496
34
- nginx_lens-0.3.2.dist-info/METADATA,sha256=DanBz8o77g84wsKtgPzMKCa2IrX8VVrq78DOFuJeTdA,552
35
- nginx_lens-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- nginx_lens-0.3.2.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
37
- nginx_lens-0.3.2.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
38
- nginx_lens-0.3.2.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -3,7 +3,8 @@
3
3
  import socket
4
4
  import time
5
5
  import http.client
6
- from typing import Dict, List
6
+ from typing import Dict, List, Tuple
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
8
  try:
8
9
  import dns.resolver
9
10
  import dns.exception
@@ -80,14 +81,12 @@ def resolve_address(address: str) -> List[str]:
80
81
  return []
81
82
  host, port = parts
82
83
 
83
- # Если это уже IPv4 адрес, возвращаем как есть
84
84
  try:
85
85
  socket.inet_aton(host)
86
86
  return [host_port]
87
87
  except socket.error:
88
88
  pass
89
89
 
90
- # Проверяем IPv6 (в квадратных скобках)
91
90
  if host.startswith("[") and host.endswith("]"):
92
91
  ipv6_host = host[1:-1]
93
92
  try:
@@ -96,11 +95,9 @@ def resolve_address(address: str) -> List[str]:
96
95
  except (socket.error, OSError):
97
96
  pass
98
97
 
99
- # Используем dnspython для детального DNS резолвинга
100
98
  if DNS_AVAILABLE:
101
99
  return _resolve_with_dns(host, port)
102
100
  else:
103
- # Fallback на стандартный socket, если dnspython недоступен
104
101
  return _resolve_with_socket(host, port)
105
102
  except (ValueError, IndexError, AttributeError):
106
103
  return []
@@ -112,25 +109,21 @@ def _resolve_with_dns(host: str, port: str) -> List[str]:
112
109
  cname_info = None
113
110
  invalid_type = None
114
111
 
115
- # Сначала проверяем CNAME для исходного хоста
116
112
  try:
117
113
  cname_answer = dns.resolver.resolve(host, 'CNAME', raise_on_no_answer=False)
118
114
  if cname_answer:
119
115
  cname_target = str(cname_answer[0].target).rstrip('.')
120
116
  cname_info = cname_target
121
117
 
122
- # Если есть CNAME, проверяем A записи для CNAME цели (не для исходного хоста)
123
118
  try:
124
119
  a_answer = dns.resolver.resolve(cname_target, 'A', raise_on_no_answer=False)
125
120
  if a_answer:
126
- # CNAME ведет на валидные A записи
127
121
  resolved_ips = []
128
122
  for rdata in a_answer:
129
123
  ip = str(rdata.address)
130
124
  resolved_ips.append(f"{ip}:{port} (via {cname_info})")
131
125
  return resolved_ips
132
126
  else:
133
- # Нет A записей для CNAME цели, проверяем другие типы
134
127
  try:
135
128
  txt_answer = dns.resolver.resolve(cname_target, 'TXT', raise_on_no_answer=False)
136
129
  if txt_answer:
@@ -145,7 +138,6 @@ def _resolve_with_dns(host: str, port: str) -> List[str]:
145
138
  except:
146
139
  pass
147
140
  if not invalid_type:
148
- # Проверяем другие невалидные типы
149
141
  try:
150
142
  ns_answer = dns.resolver.resolve(cname_target, 'NS', raise_on_no_answer=False)
151
143
  if ns_answer:
@@ -161,7 +153,6 @@ def _resolve_with_dns(host: str, port: str) -> List[str]:
161
153
  except Exception:
162
154
  pass
163
155
 
164
- # Если нет CNAME, получаем A записи для исходного хоста
165
156
  try:
166
157
  a_answer = dns.resolver.resolve(host, 'A', raise_on_no_answer=False)
167
158
  if a_answer:
@@ -181,13 +172,10 @@ def _resolve_with_dns(host: str, port: str) -> List[str]:
181
172
  def _resolve_with_socket(host: str, port: str) -> List[str]:
182
173
  """Fallback резолвинг через socket (без информации о CNAME)."""
183
174
  try:
184
- # gethostbyname_ex возвращает (hostname, aliaslist, ipaddrlist)
185
175
  _, _, ipaddrlist = socket.gethostbyname_ex(host)
186
- # Фильтруем только IPv4 адреса (IPv6 обрабатываются отдельно)
187
176
  resolved_ips = []
188
177
  for ip in ipaddrlist:
189
178
  try:
190
- # Проверяем, что это IPv4
191
179
  socket.inet_aton(ip)
192
180
  resolved_ips.append(f"{ip}:{port}")
193
181
  except socket.error:
@@ -198,11 +186,16 @@ def _resolve_with_socket(host: str, port: str) -> List[str]:
198
186
 
199
187
 
200
188
  def resolve_upstreams(
201
- upstreams: Dict[str, List[str]]
189
+ upstreams: Dict[str, List[str]],
190
+ max_workers: int = 10
202
191
  ) -> Dict[str, List[dict]]:
203
192
  """
204
193
  Резолвит DNS имена upstream-серверов в IP-адреса.
205
194
 
195
+ Args:
196
+ upstreams: Словарь upstream серверов
197
+ max_workers: Максимальное количество потоков для параллельной обработки
198
+
206
199
  Возвращает:
207
200
  {
208
201
  "backend": [
@@ -213,27 +206,71 @@ def resolve_upstreams(
213
206
  ]
214
207
  }
215
208
  """
216
- results = {}
209
+ # Собираем все задачи для параллельной обработки
210
+ tasks = []
211
+ task_to_key = {}
212
+
217
213
  for name, servers in upstreams.items():
218
- results[name] = []
219
- for srv in servers:
220
- resolved = resolve_address(srv)
221
- results[name].append({
222
- "address": srv,
223
- "resolved": resolved
224
- })
214
+ for idx, srv in enumerate(servers):
215
+ key = (name, idx, srv)
216
+ tasks.append((key, srv))
217
+ task_to_key[key] = (name, idx)
218
+
219
+ results = {}
220
+ for name in upstreams.keys():
221
+ results[name] = [None] * len(upstreams[name])
222
+
223
+ # Если нет задач, возвращаем пустой результат
224
+ if not tasks:
225
+ return results
226
+
227
+ # Параллельная обработка резолвинга
228
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
229
+ future_to_key = {executor.submit(resolve_address, srv): key for key, srv in tasks}
230
+
231
+ for future in as_completed(future_to_key):
232
+ key = future_to_key[future]
233
+ name, idx = task_to_key[key]
234
+ try:
235
+ resolved = future.result()
236
+ results[name][idx] = {
237
+ "address": key[2],
238
+ "resolved": resolved
239
+ }
240
+ except Exception:
241
+ results[name][idx] = {
242
+ "address": key[2],
243
+ "resolved": []
244
+ }
245
+
225
246
  return results
226
247
 
227
248
 
249
+ def _check_single_upstream(srv: str, timeout: float, retries: int, mode: str) -> Tuple[str, bool]:
250
+ """Вспомогательная функция для проверки одного upstream сервера."""
251
+ if mode.lower() == "http":
252
+ healthy = check_http(srv, timeout, retries)
253
+ else:
254
+ healthy = check_tcp(srv, timeout, retries)
255
+ return (srv, healthy)
256
+
257
+
228
258
  def check_upstreams(
229
259
  upstreams: Dict[str, List[str]],
230
260
  timeout: float = 2.0,
231
261
  retries: int = 1,
232
- mode: str = "tcp"
262
+ mode: str = "tcp",
263
+ max_workers: int = 10
233
264
  ) -> Dict[str, List[dict]]:
234
265
  """
235
266
  Проверяет доступность upstream-серверов.
236
- mode: "tcp" (по умолчанию) или "http"
267
+
268
+ Args:
269
+ upstreams: Словарь upstream серверов
270
+ timeout: Таймаут проверки (сек)
271
+ retries: Количество попыток
272
+ mode: "tcp" (по умолчанию) или "http"
273
+ max_workers: Максимальное количество потоков для параллельной обработки
237
274
 
238
275
  Возвращает:
239
276
  {
@@ -243,13 +280,38 @@ def check_upstreams(
243
280
  ]
244
281
  }
245
282
  """
246
- results = {}
283
+ # Собираем все задачи для параллельной обработки
284
+ tasks = []
285
+ task_to_key = {}
286
+
247
287
  for name, servers in upstreams.items():
248
- results[name] = []
249
- for srv in servers:
250
- if mode.lower() == "http":
251
- healthy = check_http(srv, timeout, retries)
252
- else:
253
- healthy = check_tcp(srv, timeout, retries)
254
- results[name].append({"address": srv, "healthy": healthy})
288
+ for idx, srv in enumerate(servers):
289
+ key = (name, idx, srv)
290
+ tasks.append((key, srv))
291
+ task_to_key[key] = (name, idx)
292
+
293
+ results = {}
294
+ for name in upstreams.keys():
295
+ results[name] = [None] * len(upstreams[name])
296
+
297
+ # Если нет задач, возвращаем пустой результат
298
+ if not tasks:
299
+ return results
300
+
301
+ # Параллельная обработка проверок
302
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
303
+ future_to_key = {
304
+ executor.submit(_check_single_upstream, srv, timeout, retries, mode): key
305
+ for key, srv in tasks
306
+ }
307
+
308
+ for future in as_completed(future_to_key):
309
+ key = future_to_key[future]
310
+ name, idx = task_to_key[key]
311
+ try:
312
+ srv, healthy = future.result()
313
+ results[name][idx] = {"address": srv, "healthy": healthy}
314
+ except Exception:
315
+ results[name][idx] = {"address": key[2], "healthy": False}
316
+
255
317
  return results