pingera-cli 0.1.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.
- pingera_cli/__init__.py +11 -0
- pingera_cli/__main__.py +9 -0
- pingera_cli/commands/__init__.py +11 -0
- pingera_cli/commands/auth.py +214 -0
- pingera_cli/commands/base.py +251 -0
- pingera_cli/commands/checks.py +1011 -0
- pingera_cli/commands/on_demand_checks.py +646 -0
- pingera_cli/formatters/__init__.py +13 -0
- pingera_cli/formatters/base_formatter.py +45 -0
- pingera_cli/formatters/generic_formatter.py +51 -0
- pingera_cli/formatters/multistep_formatter.py +141 -0
- pingera_cli/formatters/registry.py +35 -0
- pingera_cli/formatters/ssl_formatter.py +104 -0
- pingera_cli/formatters/synthetic_formatter.py +350 -0
- pingera_cli/formatters/web_formatter.py +78 -0
- pingera_cli/main.py +214 -0
- pingera_cli/utils/__init__.py +8 -0
- pingera_cli/utils/config.py +187 -0
- pingera_cli/utils/console.py +87 -0
- pingera_cli-0.1.0.dist-info/METADATA +199 -0
- pingera_cli-0.1.0.dist-info/RECORD +25 -0
- pingera_cli-0.1.0.dist-info/WHEEL +5 -0
- pingera_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pingera_cli-0.1.0.dist-info/licenses/LICENSE +22 -0
- pingera_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
On-demand checks commands for PingeraCLI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from .base import BaseCommand
|
|
14
|
+
from ..utils.config import get_api_key
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OnDemandChecksCommand(BaseCommand):
|
|
18
|
+
"""
|
|
19
|
+
Commands for managing on-demand monitoring checks
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, output_format: Optional[str] = None):
|
|
23
|
+
super().__init__(output_format)
|
|
24
|
+
|
|
25
|
+
def get_client(self):
|
|
26
|
+
"""Get Pingera SDK client with authentication for on-demand checks"""
|
|
27
|
+
api_key = get_api_key()
|
|
28
|
+
if not api_key:
|
|
29
|
+
self.display_error("API key not found. Use 'pngr auth login --api-key <key>' to set it.")
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from pingera import ApiClient, Configuration
|
|
34
|
+
from pingera.api import OnDemandChecksApi
|
|
35
|
+
|
|
36
|
+
# Configure the client
|
|
37
|
+
configuration = Configuration()
|
|
38
|
+
configuration.host = "https://api.pingera.ru"
|
|
39
|
+
configuration.api_key['apiKeyAuth'] = api_key
|
|
40
|
+
|
|
41
|
+
# Create API client
|
|
42
|
+
api_client = ApiClient(configuration)
|
|
43
|
+
return OnDemandChecksApi(api_client)
|
|
44
|
+
except ImportError:
|
|
45
|
+
self.display_error("Pingera SDK not installed. Install with: pip install pingera-sdk")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
self.display_error(f"Failed to initialize client: {str(e)}")
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
def execute_custom_check(self, url: Optional[str] = None, check_type: str = "web", host: Optional[str] = None, port: Optional[int] = None, timeout: int = 30, name: str = "On-demand check", parameters: Optional[str] = None, pw_script_file: Optional[str] = None, wait_for_result: bool = False):
|
|
52
|
+
"""Execute custom on-demand check"""
|
|
53
|
+
try:
|
|
54
|
+
import json
|
|
55
|
+
import os
|
|
56
|
+
checks_api = self.get_client()
|
|
57
|
+
|
|
58
|
+
# Import the SDK models
|
|
59
|
+
from pingera.models import ExecuteCustomCheckRequest
|
|
60
|
+
|
|
61
|
+
# Build check data
|
|
62
|
+
check_data = {
|
|
63
|
+
"name": name,
|
|
64
|
+
"type": check_type,
|
|
65
|
+
"timeout": timeout
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Add URL if provided (required for web, api, ssl checks)
|
|
69
|
+
if url is not None:
|
|
70
|
+
check_data["url"] = url
|
|
71
|
+
|
|
72
|
+
# Add host/port for TCP/SSL checks
|
|
73
|
+
if host is not None:
|
|
74
|
+
check_data["host"] = host
|
|
75
|
+
if port is not None:
|
|
76
|
+
check_data["port"] = port
|
|
77
|
+
|
|
78
|
+
# Handle pw_script_file option
|
|
79
|
+
params_dict = {}
|
|
80
|
+
if pw_script_file is not None:
|
|
81
|
+
if not os.path.exists(pw_script_file):
|
|
82
|
+
self.display_error(f"Playwright script file not found: {pw_script_file}")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(pw_script_file, 'r', encoding='utf-8') as f:
|
|
87
|
+
pw_script_content = f.read().strip()
|
|
88
|
+
|
|
89
|
+
if not pw_script_content:
|
|
90
|
+
self.display_error(f"Playwright script file is empty: {pw_script_file}")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
params_dict["pw_script"] = pw_script_content
|
|
94
|
+
self.display_info(f"Loaded Playwright script from: {pw_script_file}")
|
|
95
|
+
|
|
96
|
+
except IOError as e:
|
|
97
|
+
self.display_error(f"Failed to read Playwright script file: {str(e)}")
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
# Parse parameters JSON if provided
|
|
101
|
+
if parameters is not None:
|
|
102
|
+
try:
|
|
103
|
+
parsed_params = json.loads(parameters)
|
|
104
|
+
# Merge with pw_script from file if both are provided
|
|
105
|
+
params_dict.update(parsed_params)
|
|
106
|
+
except json.JSONDecodeError as e:
|
|
107
|
+
self.display_error(f"Invalid JSON in --parameters: {str(e)}")
|
|
108
|
+
self.display_info("Example: --parameters '{\"pw_script\": \"const { test } = require('@playwright/test'); test('example', async ({ page }) => { await page.goto('https://example.com'); });\", \"regions\": [\"US\", \"EU\"]}'")
|
|
109
|
+
raise typer.Exit(1)
|
|
110
|
+
|
|
111
|
+
# Add parameters to check_data if we have any
|
|
112
|
+
if params_dict:
|
|
113
|
+
check_data["parameters"] = params_dict
|
|
114
|
+
|
|
115
|
+
# Validation based on check type
|
|
116
|
+
if check_type in ['web', 'api'] and not url:
|
|
117
|
+
self.display_error(f"URL is required for {check_type} checks")
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
if check_type in ['tcp'] and not host:
|
|
121
|
+
self.display_error(f"Host is required for {check_type} checks")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
if check_type in ['ssl'] and not (url or host):
|
|
125
|
+
self.display_error(f"Either URL or host is required for {check_type} checks")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
|
|
128
|
+
if check_type in ['synthetic', 'multistep'] and not params_dict.get('pw_script'):
|
|
129
|
+
self.display_error(f"Playwright script is required for {check_type} checks. Use --pw-script-file or --parameters with pw_script")
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
|
|
132
|
+
# Create the request
|
|
133
|
+
check_request = ExecuteCustomCheckRequest(**check_data)
|
|
134
|
+
|
|
135
|
+
# Execute the check
|
|
136
|
+
response = checks_api.v1_checks_execute_post(check_request)
|
|
137
|
+
job_id = response.job_id
|
|
138
|
+
|
|
139
|
+
# Build success message
|
|
140
|
+
success_details = [f"Job ID: {job_id}", f"Type: {check_type}"]
|
|
141
|
+
if url:
|
|
142
|
+
success_details.append(f"URL: {url}")
|
|
143
|
+
if host:
|
|
144
|
+
success_details.append(f"Host: {host}")
|
|
145
|
+
if port:
|
|
146
|
+
success_details.append(f"Port: {port}")
|
|
147
|
+
if pw_script_file:
|
|
148
|
+
success_details.append(f"Script: loaded from {pw_script_file}")
|
|
149
|
+
|
|
150
|
+
if wait_for_result:
|
|
151
|
+
# Wait for the job to complete and show the result
|
|
152
|
+
self._wait_and_show_result(job_id, success_details)
|
|
153
|
+
else:
|
|
154
|
+
if self.output_format in ['json', 'yaml']:
|
|
155
|
+
self.output_data({
|
|
156
|
+
"job_id": job_id,
|
|
157
|
+
"check_type": check_type,
|
|
158
|
+
"url": url,
|
|
159
|
+
"host": host,
|
|
160
|
+
"port": port,
|
|
161
|
+
"name": name,
|
|
162
|
+
"status": "queued"
|
|
163
|
+
})
|
|
164
|
+
else:
|
|
165
|
+
self.display_success(
|
|
166
|
+
f"On-demand check queued successfully!\n" + "\n".join(success_details) + f"\n\nUse 'pngr checks jobs result {job_id}' to check status.",
|
|
167
|
+
"✅ Check Queued"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self.display_error(f"Failed to execute custom check: {str(e)}")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
|
|
174
|
+
def execute_existing_check(self, check_id: str, wait_for_result: bool = False):
|
|
175
|
+
"""Execute existing check on demand"""
|
|
176
|
+
try:
|
|
177
|
+
checks_api = self.get_client()
|
|
178
|
+
|
|
179
|
+
# Execute existing check
|
|
180
|
+
response = checks_api.v1_checks_check_id_execute_post(check_id=check_id)
|
|
181
|
+
job_id = response.job_id
|
|
182
|
+
|
|
183
|
+
if wait_for_result:
|
|
184
|
+
# Wait for the job to complete and show the result
|
|
185
|
+
success_details = [f"Job ID: {job_id}", f"Check ID: {check_id}"]
|
|
186
|
+
self._wait_and_show_result(job_id, success_details)
|
|
187
|
+
else:
|
|
188
|
+
if self.output_format in ['json', 'yaml']:
|
|
189
|
+
self.output_data({
|
|
190
|
+
"job_id": job_id,
|
|
191
|
+
"check_id": check_id,
|
|
192
|
+
"status": "queued"
|
|
193
|
+
})
|
|
194
|
+
else:
|
|
195
|
+
self.display_success(
|
|
196
|
+
f"Existing check executed successfully!\nJob ID: {job_id}\nCheck ID: {check_id}\n\nUse 'pngr checks jobs result {job_id}' to check status.",
|
|
197
|
+
"✅ Check Executed"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
self.display_error(f"Failed to execute existing check: {str(e)}")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def list_jobs(self, page: int = 1, page_size: int = 20):
|
|
206
|
+
"""List check jobs"""
|
|
207
|
+
try:
|
|
208
|
+
checks_api = self.get_client()
|
|
209
|
+
|
|
210
|
+
# List check jobs
|
|
211
|
+
response = checks_api.v1_checks_jobs_get(
|
|
212
|
+
page=page,
|
|
213
|
+
per_page=page_size
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not hasattr(response, 'jobs') or not response.jobs:
|
|
217
|
+
if self.output_format in ['json', 'yaml']:
|
|
218
|
+
self.output_data({"jobs": [], "total": 0, "message": "No jobs found"})
|
|
219
|
+
else:
|
|
220
|
+
self.display_info("No jobs found.")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
if self.output_format in ['json', 'yaml']:
|
|
224
|
+
jobs_data = []
|
|
225
|
+
for job in response.jobs:
|
|
226
|
+
# Extract name and type from check_parameters
|
|
227
|
+
name = None
|
|
228
|
+
check_type = None
|
|
229
|
+
host = None
|
|
230
|
+
url = None
|
|
231
|
+
|
|
232
|
+
if hasattr(job, 'check_parameters') and job.check_parameters:
|
|
233
|
+
params = job.check_parameters
|
|
234
|
+
name = params.get('name')
|
|
235
|
+
check_type = params.get('type')
|
|
236
|
+
host = params.get('host')
|
|
237
|
+
# For web checks, construct URL from host/url
|
|
238
|
+
if check_type == 'web' and 'url' in params:
|
|
239
|
+
url = params['url']
|
|
240
|
+
elif host:
|
|
241
|
+
url = host
|
|
242
|
+
|
|
243
|
+
job_dict = {
|
|
244
|
+
"job_id": str(job.id) if job.id else None,
|
|
245
|
+
"name": name,
|
|
246
|
+
"type": check_type,
|
|
247
|
+
"url": url,
|
|
248
|
+
"host": host,
|
|
249
|
+
"status": job.status if hasattr(job, 'status') else None,
|
|
250
|
+
"job_type": job.job_type if hasattr(job, 'job_type') else None,
|
|
251
|
+
"check_id": job.check_id if hasattr(job, 'check_id') else None,
|
|
252
|
+
"created_at": job.created_at.isoformat() if hasattr(job, 'created_at') and job.created_at else None,
|
|
253
|
+
"started_at": job.started_at.isoformat() if hasattr(job, 'started_at') and job.started_at else None,
|
|
254
|
+
"completed_at": job.completed_at.isoformat() if hasattr(job, 'completed_at') and job.completed_at else None,
|
|
255
|
+
"error_message": job.error_message if hasattr(job, 'error_message') else None
|
|
256
|
+
}
|
|
257
|
+
jobs_data.append(job_dict)
|
|
258
|
+
|
|
259
|
+
# Include pagination info from response
|
|
260
|
+
pagination_info = {}
|
|
261
|
+
if hasattr(response, 'pagination') and response.pagination:
|
|
262
|
+
pagination_info = {
|
|
263
|
+
"page": response.pagination.get('page', page),
|
|
264
|
+
"per_page": response.pagination.get('per_page', page_size),
|
|
265
|
+
"total": response.pagination.get('total', len(jobs_data)),
|
|
266
|
+
"pages": response.pagination.get('pages', 1),
|
|
267
|
+
"has_next": response.pagination.get('has_next', False),
|
|
268
|
+
"has_prev": response.pagination.get('has_prev', False)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
self.output_data({
|
|
272
|
+
"jobs": jobs_data,
|
|
273
|
+
"pagination": pagination_info if pagination_info else {"page": page, "per_page": page_size, "total": len(jobs_data)}
|
|
274
|
+
})
|
|
275
|
+
else:
|
|
276
|
+
table = Table(title="Check Jobs")
|
|
277
|
+
table.add_column("Job ID", style="cyan")
|
|
278
|
+
table.add_column("Name", style="green")
|
|
279
|
+
table.add_column("Type", style="blue")
|
|
280
|
+
table.add_column("Target", style="yellow")
|
|
281
|
+
table.add_column("Status", style="magenta")
|
|
282
|
+
table.add_column("Job Type", style="dim")
|
|
283
|
+
table.add_column("Created", style="dim")
|
|
284
|
+
|
|
285
|
+
for job in response.jobs:
|
|
286
|
+
# Extract details from check_parameters
|
|
287
|
+
name = "Unknown"
|
|
288
|
+
check_type = "Unknown"
|
|
289
|
+
target = "Unknown"
|
|
290
|
+
|
|
291
|
+
if hasattr(job, 'check_parameters') and job.check_parameters:
|
|
292
|
+
params = job.check_parameters
|
|
293
|
+
name = params.get('name', 'Unknown')
|
|
294
|
+
check_type = params.get('type', 'Unknown')
|
|
295
|
+
|
|
296
|
+
# Determine target based on check type
|
|
297
|
+
if check_type == 'web' and 'url' in params:
|
|
298
|
+
target = params['url']
|
|
299
|
+
elif 'host' in params:
|
|
300
|
+
port = params.get('port')
|
|
301
|
+
target = f"{params['host']}" + (f":{port}" if port else "")
|
|
302
|
+
else:
|
|
303
|
+
target = params.get('url', params.get('host', 'Unknown'))
|
|
304
|
+
|
|
305
|
+
# Status with color coding
|
|
306
|
+
status_color = "green" if hasattr(job, 'status') and job.status == 'completed' else "yellow" if hasattr(job, 'status') and job.status in ['running', 'pending'] else "red"
|
|
307
|
+
status_display = f"[{status_color}]{job.status}[/{status_color}]" if hasattr(job, 'status') and job.status else "-"
|
|
308
|
+
|
|
309
|
+
table.add_row(
|
|
310
|
+
str(job.id) if job.id else "-",
|
|
311
|
+
name,
|
|
312
|
+
check_type,
|
|
313
|
+
target,
|
|
314
|
+
status_display,
|
|
315
|
+
job.job_type if hasattr(job, 'job_type') and job.job_type else "-",
|
|
316
|
+
job.created_at.strftime("%Y-%m-%d %H:%M") if hasattr(job, 'created_at') and job.created_at else "Unknown"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
self.console.print(table)
|
|
320
|
+
|
|
321
|
+
# Show pagination info
|
|
322
|
+
if hasattr(response, 'pagination') and response.pagination:
|
|
323
|
+
pagination = response.pagination
|
|
324
|
+
total_items = pagination.get('total', len(response.jobs))
|
|
325
|
+
total_pages = pagination.get('pages', 1)
|
|
326
|
+
current_page = pagination.get('page', page)
|
|
327
|
+
per_page = pagination.get('per_page', page_size)
|
|
328
|
+
|
|
329
|
+
self.console.print(f"\n[dim]Showing {len(response.jobs)} jobs • Page {current_page} of {total_pages} • {total_items} total items • {per_page} per page[/dim]")
|
|
330
|
+
else:
|
|
331
|
+
self.console.print(f"\n[dim]Found {len(response.jobs)} jobs[/dim]")
|
|
332
|
+
|
|
333
|
+
# Add helpful tip about getting job details
|
|
334
|
+
self.console.print(f"\n[dim]💡 For detailed job information, use: [white]pngr checks jobs status <job_id>[/white][/dim]")
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.display_error(f"Failed to list jobs: {str(e)}")
|
|
338
|
+
raise typer.Exit(1)
|
|
339
|
+
|
|
340
|
+
def get_job_status(self, job_id: str):
|
|
341
|
+
"""Get job status"""
|
|
342
|
+
try:
|
|
343
|
+
checks_api = self.get_client()
|
|
344
|
+
|
|
345
|
+
# Get job status
|
|
346
|
+
job_status = checks_api.v1_checks_jobs_job_id_get(job_id=job_id)
|
|
347
|
+
|
|
348
|
+
if self.output_format in ['json', 'yaml']:
|
|
349
|
+
# Include full job data for JSON/YAML output
|
|
350
|
+
job_data = {
|
|
351
|
+
"job_id": job_id,
|
|
352
|
+
"status": job_status.status if hasattr(job_status, 'status') else None,
|
|
353
|
+
"job_type": job_status.job_type if hasattr(job_status, 'job_type') else None,
|
|
354
|
+
"check_id": job_status.check_id if hasattr(job_status, 'check_id') else None,
|
|
355
|
+
"created_at": job_status.created_at.isoformat() if hasattr(job_status, 'created_at') and job_status.created_at else None,
|
|
356
|
+
"started_at": job_status.started_at.isoformat() if hasattr(job_status, 'started_at') and job_status.started_at else None,
|
|
357
|
+
"completed_at": job_status.completed_at.isoformat() if hasattr(job_status, 'completed_at') and job_status.completed_at else None,
|
|
358
|
+
"error_message": job_status.error_message if hasattr(job_status, 'error_message') else None,
|
|
359
|
+
"check_parameters": job_status.check_parameters if hasattr(job_status, 'check_parameters') else None,
|
|
360
|
+
"result": job_status.result if hasattr(job_status, 'result') else None
|
|
361
|
+
}
|
|
362
|
+
self.output_data(job_data)
|
|
363
|
+
else:
|
|
364
|
+
# Rich formatted detailed view
|
|
365
|
+
self._display_detailed_job_status(job_status, job_id)
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
self.display_error(f"Failed to get job status: {str(e)}")
|
|
369
|
+
raise typer.Exit(1)
|
|
370
|
+
|
|
371
|
+
def _display_detailed_job_status(self, job_status, job_id: str):
|
|
372
|
+
"""Display detailed job status information in a rich format"""
|
|
373
|
+
|
|
374
|
+
# Determine status color and emoji
|
|
375
|
+
status_emoji = "✅" if hasattr(job_status, 'status') and job_status.status == 'completed' else "🏃" if hasattr(job_status, 'status') and job_status.status in ['running', 'pending'] else "❌"
|
|
376
|
+
status_color = "green" if hasattr(job_status, 'status') and job_status.status == 'completed' else "yellow" if hasattr(job_status, 'status') and job_status.status in ['running', 'pending'] else "red"
|
|
377
|
+
|
|
378
|
+
# Basic job information
|
|
379
|
+
basic_info = f"""[bold cyan]Job Information:[/bold cyan]
|
|
380
|
+
• Job ID: [white]{job_id}[/white]
|
|
381
|
+
• Status: [{status_color}]{status_emoji} {job_status.status if hasattr(job_status, 'status') else 'Unknown'}[/{status_color}]
|
|
382
|
+
• Job Type: [blue]{job_status.job_type if hasattr(job_status, 'job_type') else 'Unknown'}[/blue]
|
|
383
|
+
• Check ID: [white]{job_status.check_id if hasattr(job_status, 'check_id') else 'None (custom check)'}[/white]
|
|
384
|
+
• Created: [white]{job_status.created_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(job_status, 'created_at') and job_status.created_at else 'Unknown'}[/white]
|
|
385
|
+
• Started: [white]{job_status.started_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(job_status, 'started_at') and job_status.started_at else 'Not started'}[/white]
|
|
386
|
+
• Completed: [white]{job_status.completed_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(job_status, 'completed_at') and job_status.completed_at else 'Not completed'}[/white]"""
|
|
387
|
+
|
|
388
|
+
# Check parameters section
|
|
389
|
+
params_info = ""
|
|
390
|
+
if hasattr(job_status, 'check_parameters') and job_status.check_parameters:
|
|
391
|
+
params = job_status.check_parameters
|
|
392
|
+
params_info = f"""
|
|
393
|
+
[bold cyan]Check Parameters:[/bold cyan]
|
|
394
|
+
• Name: [white]{params.get('name', 'Unknown')}[/white]
|
|
395
|
+
• Type: [blue]{params.get('type', 'Unknown')}[/blue]"""
|
|
396
|
+
|
|
397
|
+
# Add type-specific parameters
|
|
398
|
+
if params.get('type') == 'web':
|
|
399
|
+
if 'url' in params:
|
|
400
|
+
params_info += f"\n• URL: [yellow]{params['url']}[/yellow]"
|
|
401
|
+
elif params.get('type') == 'tcp':
|
|
402
|
+
if 'host' in params:
|
|
403
|
+
params_info += f"\n• Host: [yellow]{params['host']}[/yellow]"
|
|
404
|
+
if 'port' in params:
|
|
405
|
+
params_info += f"\n• Port: [white]{params['port']}[/white]"
|
|
406
|
+
elif params.get('type') in ['multistep', 'synthetic']:
|
|
407
|
+
if 'parameters' in params and 'pw_script' in params['parameters']:
|
|
408
|
+
script_preview = params['parameters']['pw_script']
|
|
409
|
+
if len(script_preview) > 100:
|
|
410
|
+
script_preview = script_preview[:100] + "..."
|
|
411
|
+
params_info += f"\n• Script: [dim]{script_preview}[/dim]"
|
|
412
|
+
|
|
413
|
+
if 'timeout' in params:
|
|
414
|
+
params_info += f"\n• Timeout: [white]{params['timeout']}s[/white]"
|
|
415
|
+
|
|
416
|
+
# Show regions if available
|
|
417
|
+
if 'parameters' in params and isinstance(params['parameters'], dict) and 'regions' in params['parameters']:
|
|
418
|
+
regions = params['parameters']['regions']
|
|
419
|
+
params_info += f"\n• Regions: [white]{', '.join(regions)}[/white]"
|
|
420
|
+
|
|
421
|
+
# Error information
|
|
422
|
+
error_info = ""
|
|
423
|
+
if hasattr(job_status, 'error_message') and job_status.error_message:
|
|
424
|
+
error_info = f"""
|
|
425
|
+
[bold red]Error Information:[/bold red]
|
|
426
|
+
• Error: [red]{job_status.error_message}[/red]"""
|
|
427
|
+
|
|
428
|
+
# Result analysis
|
|
429
|
+
result_info = ""
|
|
430
|
+
if hasattr(job_status, 'result') and job_status.result:
|
|
431
|
+
result = job_status.result
|
|
432
|
+
|
|
433
|
+
# Basic result info
|
|
434
|
+
result_status = result.get('status', 'Unknown') if isinstance(result, dict) else 'Unknown'
|
|
435
|
+
result_status_color = "green" if result_status == 'ok' else "red" if result_status == 'failed' else "yellow"
|
|
436
|
+
|
|
437
|
+
result_info = f"""
|
|
438
|
+
[bold cyan]Result Summary:[/bold cyan]
|
|
439
|
+
• Result Status: [{result_status_color}]{result_status}[/{result_status_color}]"""
|
|
440
|
+
|
|
441
|
+
if isinstance(result, dict):
|
|
442
|
+
# Response time
|
|
443
|
+
if 'response_time' in result and result['response_time']:
|
|
444
|
+
result_info += f"\n• Response Time: [yellow]{result['response_time']}ms[/yellow]"
|
|
445
|
+
|
|
446
|
+
# Check server info
|
|
447
|
+
if 'check_server' in result and result['check_server']:
|
|
448
|
+
server = result['check_server']
|
|
449
|
+
result_info += f"\n• Server: [magenta]{server.get('region', 'Unknown')} ({server.get('ip_address', 'Unknown IP')})[/magenta]"
|
|
450
|
+
|
|
451
|
+
# Error message from result
|
|
452
|
+
if 'error_message' in result and result['error_message']:
|
|
453
|
+
result_info += f"\n• Result Error: [red]{result['error_message']}[/red]"
|
|
454
|
+
|
|
455
|
+
# Format check metadata using existing formatters
|
|
456
|
+
if 'check_metadata' in result and result['check_metadata']:
|
|
457
|
+
metadata = result['check_metadata']
|
|
458
|
+
|
|
459
|
+
# Use the same metadata formatting as check results
|
|
460
|
+
try:
|
|
461
|
+
from ..formatters.registry import FormatterRegistry
|
|
462
|
+
registry = FormatterRegistry(verbose=False) # Default to non-verbose for job status
|
|
463
|
+
metadata_formatted = registry.format_metadata(metadata)
|
|
464
|
+
|
|
465
|
+
if metadata_formatted.strip():
|
|
466
|
+
result_info += f"\n{metadata_formatted}"
|
|
467
|
+
except Exception:
|
|
468
|
+
# Fallback to basic metadata display
|
|
469
|
+
if 'logs' in metadata and metadata['logs']:
|
|
470
|
+
logs = metadata['logs']
|
|
471
|
+
result_info += f"\n• Logs: [dim]{len(logs)} log entries[/dim]"
|
|
472
|
+
# Show first log entry if available
|
|
473
|
+
if logs and len(logs) > 0:
|
|
474
|
+
first_log = logs[0]
|
|
475
|
+
log_level = first_log.get('level', 'info')
|
|
476
|
+
log_message = first_log.get('message', 'No message')
|
|
477
|
+
log_color = "red" if log_level == 'error' else "yellow" if log_level == 'warn' else "white"
|
|
478
|
+
result_info += f"\n - [{log_color}]{log_level.upper()}: {log_message}[/{log_color}]"
|
|
479
|
+
|
|
480
|
+
if 'execution_time' in metadata:
|
|
481
|
+
result_info += f"\n• Execution Time: [white]{metadata['execution_time']}ms[/white]"
|
|
482
|
+
|
|
483
|
+
if 'test_summary' in metadata:
|
|
484
|
+
summary = metadata['test_summary']
|
|
485
|
+
total_tests = summary.get('total', 0)
|
|
486
|
+
passed_tests = summary.get('passed', 0)
|
|
487
|
+
failed_tests = summary.get('failed', 0)
|
|
488
|
+
result_info += f"\n• Test Results: [green]{passed_tests} passed[/green], [red]{failed_tests} failed[/red], [white]{total_tests} total[/white]"
|
|
489
|
+
|
|
490
|
+
# Combine all sections
|
|
491
|
+
full_info = basic_info
|
|
492
|
+
if params_info:
|
|
493
|
+
full_info += params_info
|
|
494
|
+
if error_info:
|
|
495
|
+
full_info += error_info
|
|
496
|
+
if result_info:
|
|
497
|
+
full_info += result_info
|
|
498
|
+
|
|
499
|
+
panel = Panel(
|
|
500
|
+
full_info,
|
|
501
|
+
title=f"📋 Job Status: {job_id}",
|
|
502
|
+
border_style="blue",
|
|
503
|
+
padding=(1, 2),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
self.console.print(panel)
|
|
507
|
+
|
|
508
|
+
def _wait_and_show_result(self, job_id: str, initial_details: list):
|
|
509
|
+
"""Wait for job completion and display the result"""
|
|
510
|
+
import time
|
|
511
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
512
|
+
|
|
513
|
+
if self.output_format not in ['json', 'yaml']:
|
|
514
|
+
# Show initial success message
|
|
515
|
+
self.display_success(
|
|
516
|
+
f"On-demand check queued successfully!\n" + "\n".join(initial_details) + f"\n\nWaiting for result...",
|
|
517
|
+
"✅ Check Queued"
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Polling logic with progress indicator
|
|
521
|
+
max_wait_time = 300 # 5 minutes maximum wait
|
|
522
|
+
poll_interval = 2 # Poll every 2 seconds
|
|
523
|
+
elapsed_time = 0
|
|
524
|
+
|
|
525
|
+
with Progress(
|
|
526
|
+
SpinnerColumn(),
|
|
527
|
+
TextColumn("[progress.description]{task.description}"),
|
|
528
|
+
console=self.console,
|
|
529
|
+
transient=True
|
|
530
|
+
) as progress:
|
|
531
|
+
if self.output_format not in ['json', 'yaml']:
|
|
532
|
+
task = progress.add_task("⏳ Waiting for job completion...", total=None)
|
|
533
|
+
|
|
534
|
+
while elapsed_time < max_wait_time:
|
|
535
|
+
try:
|
|
536
|
+
checks_api = self.get_client()
|
|
537
|
+
job_status = checks_api.v1_checks_jobs_job_id_get(job_id=job_id)
|
|
538
|
+
|
|
539
|
+
if hasattr(job_status, 'status'):
|
|
540
|
+
if job_status.status in ['completed', 'failed', 'error']:
|
|
541
|
+
# Job is finished, show the result
|
|
542
|
+
if self.output_format not in ['json', 'yaml']:
|
|
543
|
+
progress.update(task, description=f"✅ Job {job_status.status}!")
|
|
544
|
+
time.sleep(0.5) # Brief pause to show completion
|
|
545
|
+
|
|
546
|
+
# Display the result using existing method
|
|
547
|
+
self.get_job_status(job_id)
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
elif job_status.status == 'running':
|
|
551
|
+
if self.output_format not in ['json', 'yaml']:
|
|
552
|
+
progress.update(task, description=f"🏃 Job running... ({elapsed_time}s elapsed)")
|
|
553
|
+
|
|
554
|
+
else: # pending, queued, etc.
|
|
555
|
+
if self.output_format not in ['json', 'yaml']:
|
|
556
|
+
progress.update(task, description=f"⏳ Job {job_status.status}... ({elapsed_time}s elapsed)")
|
|
557
|
+
|
|
558
|
+
time.sleep(poll_interval)
|
|
559
|
+
elapsed_time += poll_interval
|
|
560
|
+
|
|
561
|
+
except Exception as e:
|
|
562
|
+
if self.output_format in ['json', 'yaml']:
|
|
563
|
+
self.output_data({
|
|
564
|
+
"error": f"Failed to poll job status: {str(e)}",
|
|
565
|
+
"job_id": job_id,
|
|
566
|
+
"elapsed_time": elapsed_time
|
|
567
|
+
})
|
|
568
|
+
else:
|
|
569
|
+
self.display_error(f"Error polling job status: {str(e)}")
|
|
570
|
+
self.display_info(f"You can manually check status with: pngr checks jobs result {job_id}")
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
# Timeout reached
|
|
574
|
+
if self.output_format in ['json', 'yaml']:
|
|
575
|
+
self.output_data({
|
|
576
|
+
"timeout": True,
|
|
577
|
+
"message": f"Job did not complete within {max_wait_time} seconds",
|
|
578
|
+
"job_id": job_id,
|
|
579
|
+
"elapsed_time": elapsed_time
|
|
580
|
+
})
|
|
581
|
+
else:
|
|
582
|
+
self.display_warning(
|
|
583
|
+
f"Job did not complete within {max_wait_time} seconds.\n"
|
|
584
|
+
f"The job may still be running. Use 'pngr checks jobs result {job_id}' to check status manually."
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# Create Typer app for on-demand checks commands
|
|
589
|
+
run_app = typer.Typer(name="run", help="🚀 Execute checks on demand")
|
|
590
|
+
jobs_app = typer.Typer(name="jobs", help="📋 Manage check jobs")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def get_output_format():
|
|
594
|
+
"""Get output format from config"""
|
|
595
|
+
from ..utils.config import get_config
|
|
596
|
+
return get_config().get('output_format', 'table')
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@run_app.command("custom")
|
|
600
|
+
def run_custom_check(
|
|
601
|
+
url: Optional[str] = typer.Option(None, "--url", "-u", help="URL to monitor (required for web, api, ssl checks)"),
|
|
602
|
+
check_type: str = typer.Option("web", "--type", "-t", help="Check type (web, api, tcp, ssl, synthetic, multistep)"),
|
|
603
|
+
host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL checks (max 255 characters)"),
|
|
604
|
+
port: Optional[int] = typer.Option(None, "--port", help="Port number for TCP checks (1-65535)"),
|
|
605
|
+
timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"),
|
|
606
|
+
name: str = typer.Option("On-demand check", "--name", "-n", help="Check name"),
|
|
607
|
+
parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"regions\": [\"US\", \"EU\"]}')"),
|
|
608
|
+
pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks"),
|
|
609
|
+
wait_for_result: bool = typer.Option(False, "--wait-for-result", help="Wait for job completion and show result immediately (max 5 minutes)"),
|
|
610
|
+
):
|
|
611
|
+
"""Execute custom on-demand check. Parameters vary by check type:
|
|
612
|
+
- web/api: --url required
|
|
613
|
+
- tcp: --host required, --port optional
|
|
614
|
+
- ssl: --url or --host required
|
|
615
|
+
- synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
|
|
616
|
+
on_demand_cmd = OnDemandChecksCommand(get_output_format())
|
|
617
|
+
on_demand_cmd.execute_custom_check(url, check_type, host, port, timeout, name, parameters, pw_script_file, wait_for_result)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@run_app.command("existing")
|
|
621
|
+
def run_existing_check(
|
|
622
|
+
check_id: str = typer.Argument(..., help="Existing check ID to execute"),
|
|
623
|
+
wait_for_result: bool = typer.Option(False, "--wait-for-result", help="Wait for job completion and show result immediately (max 5 minutes)"),
|
|
624
|
+
):
|
|
625
|
+
"""Execute existing check on demand"""
|
|
626
|
+
on_demand_cmd = OnDemandChecksCommand(get_output_format())
|
|
627
|
+
on_demand_cmd.execute_existing_check(check_id, wait_for_result)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@jobs_app.command("list")
|
|
631
|
+
def list_jobs(
|
|
632
|
+
page: int = typer.Option(1, "--page", "-p", help="Page number"),
|
|
633
|
+
page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page"),
|
|
634
|
+
):
|
|
635
|
+
"""List check jobs"""
|
|
636
|
+
on_demand_cmd = OnDemandChecksCommand(get_output_format())
|
|
637
|
+
on_demand_cmd.list_jobs(page, page_size)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@jobs_app.command("result")
|
|
641
|
+
def get_job_result(
|
|
642
|
+
job_id: str = typer.Argument(..., help="Job ID to get result for"),
|
|
643
|
+
):
|
|
644
|
+
"""Get job result"""
|
|
645
|
+
on_demand_cmd = OnDemandChecksCommand(get_output_format())
|
|
646
|
+
on_demand_cmd.get_job_status(job_id)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Formatters for different check result types
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .ssl_formatter import SSLFormatter
|
|
7
|
+
from .synthetic_formatter import SyntheticFormatter
|
|
8
|
+
from .multistep_formatter import MultistepFormatter
|
|
9
|
+
from .web_formatter import WebFormatter
|
|
10
|
+
from .generic_formatter import GenericFormatter
|
|
11
|
+
from .registry import FormatterRegistry
|
|
12
|
+
|
|
13
|
+
__all__ = ['SSLFormatter', 'SyntheticFormatter', 'MultistepFormatter', 'WebFormatter', 'GenericFormatter', 'FormatterRegistry']
|