try another fix
Build and Push Docker Images / build-and-push (push) Successful in 18s

This commit is contained in:
2025-07-01 09:54:11 +02:00
parent bde19374c0
commit 96eb344191
3 changed files with 562 additions and 128 deletions
+154
View File
@@ -0,0 +1,154 @@
# Puppeteer API Server
A FastAPI-based server that uses Puppeteer (via pyppeteer) to scrape websites and extract various types of data.
## Recent Fixes (v2.0)
### Issues Fixed
- **Resource temporarily unavailable (Errno 11)** - Fixed by implementing proper browser cleanup and resource limits
- **100% CPU usage** - Fixed by reducing browser pool size, adding operation limits, and improving cleanup
- **Server hanging** - Fixed by adding timeouts, emergency cleanup, and better error handling
### Key Improvements
#### 1. Resource Management
- **Reduced browser pool size**: From 5 to 3 browsers maximum
- **Shorter browser TTL**: From 1 hour to 30 minutes
- **Concurrent operation limits**: Maximum 5 concurrent operations
- **Queue size limits**: Prevent unbounded growth
#### 2. Better Cleanup
- **Automatic browser recycling**: Browsers are recycled every 30 minutes
- **Emergency cleanup**: Force cleanup all browsers when needed
- **Proper page cleanup**: Ensure pages are closed after each operation
- **Signal handlers**: Graceful shutdown on SIGTERM/SIGINT
#### 3. Monitoring & Debugging
- **System status endpoint**: `/status` - Monitor CPU, memory, file descriptors
- **Browser pool monitoring**: Track active browsers and their ages
- **Emergency cleanup endpoint**: `/emergency-cleanup` - Force cleanup when needed
- **Monitoring script**: `monitor.py` - Real-time system monitoring
#### 4. Error Handling
- **Timeout handling**: 30s timeout for getting browsers from pool
- **Emergency recovery**: Auto-cleanup when timeouts occur
- **Better exception handling**: More detailed error logging
## Environment Variables
```bash
# Required
API_KEY=your-api-key-here
# Optional (with defaults)
MAX_BROWSERS=3 # Maximum browser instances (default: 3)
BROWSER_TTL=1800 # Browser time-to-live in seconds (default: 1800 = 30min)
MAX_CONCURRENT_OPERATIONS=5 # Max concurrent operations (default: 5)
CACHE_EXPIRY_HOURS=36 # Cache expiry in hours (default: 36)
CLEANUP_CRON=0 3 * * * # Cache cleanup schedule (default: daily at 3 AM)
RATE_LIMIT_MINUTE=60 # Requests per minute (default: 60)
```
## API Endpoints
### Core Endpoints
- `GET /` - Visit URL and get HTML content
- `GET /seo` - Extract SEO information
- `GET /meta` - Extract meta tags and Open Graph data
### Management Endpoints
- `HEAD /` - Health check
- `GET /status` - System status and browser pool information
- `POST /emergency-cleanup` - Force cleanup all browsers
- `GET /cache/stats` - Cache statistics
- `GET /cache/clear` - Clear all cache
## Usage Examples
### Basic Usage
```bash
# Visit a URL
curl -H "X-API-Key: your-api-key" "http://localhost:8000/?url=https://example.com"
# Extract SEO data
curl -H "X-API-Key: your-api-key" "http://localhost:8000/seo?url=https://example.com"
# Get system status
curl -H "X-API-Key: your-api-key" "http://localhost:8000/status"
```
### Monitoring
```bash
# Monitor system every 30 seconds
python monitor.py your-api-key 30
# Monitor with custom URL
python monitor.py your-api-key 30 http://your-server:8000
# Trigger emergency cleanup
python monitor.py your-api-key 30 http://localhost:8000 cleanup
```
## Docker Usage
```bash
# Build the image
docker build -t puppeteer-api .
# Run with environment variables
docker run -d \
--name puppeteer-api \
-p 8000:8000 \
-e API_KEY=your-api-key \
-e MAX_BROWSERS=3 \
-e BROWSER_TTL=1800 \
-v /path/to/cache:/db \
puppeteer-api
```
## Troubleshooting
### High CPU Usage
1. Check system status: `GET /status`
2. If browser pool is at capacity, trigger cleanup: `POST /emergency-cleanup`
3. Consider reducing `MAX_BROWSERS` or `MAX_CONCURRENT_OPERATIONS`
### Resource Errors (Errno 11)
1. The system now automatically handles this with better cleanup
2. Monitor with `python monitor.py` to track resource usage
3. If persistent, trigger emergency cleanup
### Server Hanging
1. Check for stuck operations with monitoring script
2. Trigger emergency cleanup
3. Restart the container if needed
## Performance Tips
1. **Use caching**: The API caches results for 36 hours by default
2. **Monitor resources**: Use the monitoring script to track usage
3. **Adjust limits**: Tune `MAX_BROWSERS` and `MAX_CONCURRENT_OPERATIONS` based on your server capacity
4. **Regular cleanup**: The system automatically recycles browsers every 30 minutes
## Logs
The server provides detailed logging for:
- Browser creation and cleanup
- Resource usage warnings
- Error conditions
- Cache operations
Monitor logs to identify patterns and adjust configuration accordingly.
+268 -128
View File
@@ -6,6 +6,8 @@ import json
import re import re
import sqlite3 import sqlite3
import time import time
import signal
import psutil
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
from urllib.parse import unquote from urllib.parse import unquote
@@ -14,7 +16,7 @@ from apscheduler.triggers.cron import CronTrigger
from fastapi.middleware.base import BaseHTTPMiddleware from fastapi.middleware.base import BaseHTTPMiddleware
# Add imports for browser pool # Add imports for browser pool
from asyncio import Queue, Lock from asyncio import Queue, Lock, Semaphore
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
app = FastAPI() app = FastAPI()
@@ -33,12 +35,17 @@ CLEANUP_CRON = os.getenv('CLEANUP_CRON', '0 3 * * *')
# Define custom user agent # Define custom user agent
CUSTOM_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36' CUSTOM_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
# Browser pool configuration # Browser pool configuration - Reduced for better resource management
MAX_BROWSERS = int(os.getenv('MAX_BROWSERS', '5')) # Maximum number of browser instances MAX_BROWSERS = int(os.getenv('MAX_BROWSERS', '3')) # Reduced from 5 to 3
BROWSER_TTL = int(os.getenv('BROWSER_TTL', '3600')) # Time to live for browser instances in seconds BROWSER_TTL = int(os.getenv('BROWSER_TTL', '1800')) # Reduced from 3600 to 1800 seconds (30 minutes)
browser_pool = Queue() MAX_CONCURRENT_OPERATIONS = int(os.getenv('MAX_CONCURRENT_OPERATIONS', '5')) # New: limit concurrent operations
# Browser pool management
browser_pool = Queue(maxsize=MAX_BROWSERS) # Add maxsize to prevent unbounded growth
browser_lock = Lock() browser_lock = Lock()
browser_creation_times = {} browser_creation_times = {}
active_browsers = set() # Track active browsers
operation_semaphore = Semaphore(MAX_CONCURRENT_OPERATIONS) # Limit concurrent operations
# Rate limiting configuration # Rate limiting configuration
RATE_LIMIT_MINUTE = int(os.getenv('RATE_LIMIT_MINUTE', '60')) # requests per minute RATE_LIMIT_MINUTE = int(os.getenv('RATE_LIMIT_MINUTE', '60')) # requests per minute
@@ -87,105 +94,164 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
app.add_middleware(RateLimitMiddleware) app.add_middleware(RateLimitMiddleware)
async def create_browser(): async def create_browser():
"""Create a new browser instance""" """Create a new browser instance with improved resource management"""
browser = await launch( try:
headless=True, browser = await launch(
executablePath='/usr/bin/google-chrome', headless=True,
args=[ executablePath='/usr/bin/google-chrome',
'--no-sandbox', args=[
'--disable-setuid-sandbox', '--no-sandbox',
'--disable-dev-shm-usage', '--disable-setuid-sandbox',
'--disable-accelerated-2d-canvas', '--disable-dev-shm-usage',
'--disable-gpu', '--disable-accelerated-2d-canvas',
'--disable-extensions', '--disable-gpu',
'--disable-sync', '--disable-extensions',
'--disable-background-networking', '--disable-sync',
'--disable-default-apps', '--disable-background-networking',
'--disable-translate', '--disable-default-apps',
'--disable-background-timer-throttling', '--disable-translate',
'--disable-backgrounding-occluded-windows', '--disable-background-timer-throttling',
'--disable-client-side-phishing-detection', '--disable-backgrounding-occluded-windows',
'--disable-features=site-per-process', '--disable-client-side-phishing-detection',
'--disable-hang-monitor', '--disable-features=site-per-process',
'--disable-ipc-flooding-protection', '--disable-hang-monitor',
'--disable-popup-blocking', '--disable-ipc-flooding-protection',
'--disable-prompt-on-repost', '--disable-popup-blocking',
'--disable-renderer-backgrounding', '--disable-prompt-on-repost',
'--memory-pressure-off', '--disable-renderer-backgrounding',
'--no-first-run', '--memory-pressure-off',
'--safebrowsing-disable-auto-update', '--no-first-run',
], '--safebrowsing-disable-auto-update',
handleSIGINT=False, '--max_old_space_size=512', # Limit memory usage
handleSIGTERM=False, '--single-process', # Use single process to reduce resource usage
handleSIGHUP=False, '--disable-web-security',
ignoreHTTPSErrors=True '--disable-features=VizDisplayCompositor',
) ],
browser_creation_times[browser] = time.time() handleSIGINT=False,
return browser handleSIGTERM=False,
handleSIGHUP=False,
ignoreHTTPSErrors=True,
autoClose=True, # Ensure browser closes automatically
)
browser_creation_times[browser] = time.time()
active_browsers.add(browser)
print(f"Created new browser instance. Total active browsers: {len(active_browsers)}")
return browser
except Exception as e:
print(f"Error creating browser: {e}")
raise
async def cleanup_browser(browser):
"""Safely cleanup a browser instance"""
try:
if browser in active_browsers:
active_browsers.remove(browser)
if browser in browser_creation_times:
del browser_creation_times[browser]
# Close all pages first
pages = await browser.pages()
for page in pages:
try:
await page.close()
except Exception as e:
print(f"Error closing page: {e}")
# Close browser
await browser.close()
print(f"Browser cleaned up. Total active browsers: {len(active_browsers)}")
except Exception as e:
print(f"Error during browser cleanup: {e}")
async def check_browser_health(): async def check_browser_health():
"""Check browser health and recycle if needed""" """Check browser health and recycle if needed"""
while True: while True:
try: try:
# Sleep for 5 minutes between checks # Sleep for 2 minutes between checks (reduced from 5 minutes)
await asyncio.sleep(300) await asyncio.sleep(120)
async with browser_lock: async with browser_lock:
# Get all browsers from the pool # Get all browsers from the pool
browsers = [] browsers = []
while not browser_pool.empty(): while not browser_pool.empty():
browsers.append(await browser_pool.get()) try:
browsers.append(await browser_pool.get_nowait())
except asyncio.QueueEmpty:
break
# Check each browser # Check each browser
for browser in browsers: for browser in browsers:
try: try:
# Check if browser is too old # Check if browser is too old
if time.time() - browser_creation_times.get(browser, 0) > BROWSER_TTL: if time.time() - browser_creation_times.get(browser, 0) > BROWSER_TTL:
await browser.close() print(f"Recycling old browser (age: {time.time() - browser_creation_times.get(browser, 0):.0f}s)")
del browser_creation_times[browser] await cleanup_browser(browser)
browser = await create_browser() browser = await create_browser()
else: else:
# Quick health check # Quick health check
await browser.pages() await browser.pages()
# Put back in pool if healthy # Put back in pool if healthy
await browser_pool.put(browser) if not browser_pool.full():
except Exception: await browser_pool.put(browser)
else:
# Pool is full, cleanup this browser
await cleanup_browser(browser)
except Exception as e:
print(f"Browser health check failed: {e}")
# If unhealthy, close and create new # If unhealthy, close and create new
try: await cleanup_browser(browser)
await browser.close() if not browser_pool.full():
except: new_browser = await create_browser()
pass await browser_pool.put(new_browser)
if browser in browser_creation_times:
del browser_creation_times[browser]
new_browser = await create_browser()
await browser_pool.put(new_browser)
except Exception as e: except Exception as e:
print(f"Error in browser health check: {str(e)}") print(f"Error in browser health check: {str(e)}")
async def force_cleanup_all_browsers():
"""Force cleanup all browsers in emergency situations"""
print("Force cleaning up all browsers...")
# Clean up pool
while not browser_pool.empty():
try:
browser = await browser_pool.get_nowait()
await cleanup_browser(browser)
except asyncio.QueueEmpty:
break
# Clean up active browsers
for browser in list(active_browsers):
await cleanup_browser(browser)
# Initialize browser pool # Initialize browser pool
@app.on_event("startup") @app.on_event("startup")
async def init_browser_pool(): async def init_browser_pool():
"""Initialize the browser pool with some browsers""" """Initialize the browser pool with some browsers"""
for _ in range(min(3, MAX_BROWSERS)): # Start with 3 browsers or MAX_BROWSERS, whichever is smaller try:
browser = await create_browser() for _ in range(min(2, MAX_BROWSERS)): # Start with 2 browsers instead of 3
await browser_pool.put(browser) browser = await create_browser()
print(f"Browser pool initialized with {browser_pool.qsize()} browsers") await browser_pool.put(browser)
print(f"Browser pool initialized with {browser_pool.qsize()} browsers")
# Start browser health check task # Start browser health check task
asyncio.create_task(check_browser_health()) asyncio.create_task(check_browser_health())
except Exception as e:
print(f"Error initializing browser pool: {e}")
@app.on_event("shutdown") @app.on_event("shutdown")
async def cleanup_browser_pool(): async def cleanup_browser_pool():
"""Clean up all browsers in the pool""" """Clean up all browsers in the pool"""
while not browser_pool.empty(): await force_cleanup_all_browsers()
try:
browser = await browser_pool.get_nowait() # Signal handlers for graceful shutdown
await browser.close() def signal_handler(signum, frame):
if browser in browser_creation_times: print(f"Received signal {signum}, shutting down gracefully...")
del browser_creation_times[browser] asyncio.create_task(force_cleanup_all_browsers())
except:
pass signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
# Initialize SQLite database # Initialize SQLite database
def init_db(): def init_db():
@@ -371,47 +437,49 @@ async def health_check():
return {"status": "ok"} return {"status": "ok"}
async def safe_browser_operation(url, operation_func): async def safe_browser_operation(url, operation_func):
"""Safely perform a browser operation with proper cleanup""" """Safely perform a browser operation with proper cleanup and resource limits"""
async with get_browser() as browser: async with operation_semaphore: # Limit concurrent operations
try: async with get_browser() as browser:
# Create a new page page = None
page = await browser.newPage()
# Set reasonable viewport
await page.setViewport({'width': 1280, 'height': 800})
# Set user agent
await page.setUserAgent(CUSTOM_USER_AGENT)
# Set reasonable timeout
page.setDefaultNavigationTimeout(30000)
# Enable request interception to block unnecessary resources
await page.setRequestInterception(True)
async def intercept(request):
# Block unnecessary resource types
if request.resourceType in ['image', 'media', 'font', 'stylesheet']:
await request.abort()
else:
await request.continue_()
page.on('request', lambda req: asyncio.ensure_future(intercept(req)))
# Perform the operation
result = await operation_func(page)
return result
except Exception as e:
print(f"Error during browser operation: {str(e)}")
raise
finally:
try: try:
# Ensure page is properly closed # Create a new page
if 'page' in locals(): page = await browser.newPage()
await page.close()
# Set reasonable viewport
await page.setViewport({'width': 1280, 'height': 800})
# Set user agent
await page.setUserAgent(CUSTOM_USER_AGENT)
# Set reasonable timeout
page.setDefaultNavigationTimeout(30000)
# Enable request interception to block unnecessary resources
await page.setRequestInterception(True)
async def intercept(request):
# Block unnecessary resource types
if request.resourceType in ['image', 'media', 'font', 'stylesheet']:
await request.abort()
else:
await request.continue_()
page.on('request', lambda req: asyncio.ensure_future(intercept(req)))
# Perform the operation
result = await operation_func(page)
return result
except Exception as e: except Exception as e:
print(f"Error closing page: {str(e)}") print(f"Error during browser operation: {str(e)}")
raise
finally:
try:
# Ensure page is properly closed
if page:
await page.close()
except Exception as e:
print(f"Error closing page: {str(e)}")
@app.get("/") @app.get("/")
async def visit_url(url: str, x_api_key: Optional[str] = Header(None)): async def visit_url(url: str, x_api_key: Optional[str] = Header(None)):
@@ -682,23 +750,93 @@ async def cache_stats(x_api_key: Optional[str] = Header(None)):
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.get("/status")
async def system_status(x_api_key: Optional[str] = Header(None)):
"""Get system status and browser pool information"""
# Validate API key
if not x_api_key or x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
try:
# Get system information
process = psutil.Process()
memory_info = process.memory_info()
# Get browser pool information
pool_size = browser_pool.qsize()
active_browser_count = len(active_browsers)
# Calculate browser ages
browser_ages = []
for browser, creation_time in browser_creation_times.items():
age = time.time() - creation_time
browser_ages.append(age)
return {
"system": {
"cpu_percent": process.cpu_percent(),
"memory_mb": memory_info.rss / 1024 / 1024,
"memory_percent": process.memory_percent(),
"open_files": len(process.open_files()),
"connections": len(process.connections()),
"threads": process.num_threads()
},
"browser_pool": {
"pool_size": pool_size,
"active_browsers": active_browser_count,
"max_browsers": MAX_BROWSERS,
"browser_ttl_seconds": BROWSER_TTL,
"browser_ages_seconds": browser_ages,
"concurrent_operations_limit": MAX_CONCURRENT_OPERATIONS
},
"timestamp": time.time()
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/emergency-cleanup")
async def emergency_cleanup(x_api_key: Optional[str] = Header(None)):
"""Force emergency cleanup of all browsers"""
# Validate API key
if not x_api_key or x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
try:
await force_cleanup_all_browsers()
return {"status": "success", "message": "Emergency cleanup completed"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@asynccontextmanager @asynccontextmanager
async def get_browser(): async def get_browser():
"""Get a browser from the pool or create a new one if needed""" """Get a browser from the pool or create a new one if needed"""
browser = None browser = None
try: try:
# Try to get a browser from the pool # Try to get a browser from the pool with timeout
try: try:
browser = await browser_pool.get_nowait() browser = await asyncio.wait_for(browser_pool.get(), timeout=30.0)
except asyncio.QueueEmpty: except (asyncio.QueueEmpty, asyncio.TimeoutError):
# If pool is empty, create a new browser if under the limit # If pool is empty or timeout, create a new browser if under the limit
async with browser_lock: async with browser_lock:
if browser_pool.qsize() + 1 <= MAX_BROWSERS: current_browser_count = len(active_browsers)
if current_browser_count < MAX_BROWSERS:
browser = await create_browser() browser = await create_browser()
else: else:
# If at limit, wait for a browser to become available # If at limit, wait for a browser to become available with timeout
browser = await browser_pool.get() try:
browser = await asyncio.wait_for(browser_pool.get(), timeout=60.0)
except asyncio.TimeoutError:
# Emergency cleanup and create new browser
print("Emergency: Timeout waiting for browser, forcing cleanup")
await force_cleanup_all_browsers()
browser = await create_browser()
yield browser
except Exception as e:
print(f"Error in get_browser: {e}")
# Emergency cleanup if we can't get a browser
await force_cleanup_all_browsers()
browser = await create_browser()
yield browser yield browser
finally: finally:
# Return browser to pool if it's still viable # Return browser to pool if it's still viable
@@ -708,21 +846,23 @@ async def get_browser():
await browser.pages() await browser.pages()
# Check if browser is too old # Check if browser is too old
if time.time() - browser_creation_times.get(browser, 0) > BROWSER_TTL: if time.time() - browser_creation_times.get(browser, 0) > BROWSER_TTL:
await browser.close() print(f"Recycling old browser in get_browser (age: {time.time() - browser_creation_times.get(browser, 0):.0f}s)")
if browser in browser_creation_times: await cleanup_browser(browser)
del browser_creation_times[browser]
browser = await create_browser() browser = await create_browser()
await browser_pool.put(browser)
except Exception: # Only put back if pool is not full
if not browser_pool.full():
await browser_pool.put(browser)
else:
# Pool is full, cleanup this browser
await cleanup_browser(browser)
except Exception as e:
print(f"Browser health check failed in get_browser: {e}")
# If browser is not usable, close it and create a new one # If browser is not usable, close it and create a new one
try: await cleanup_browser(browser)
await browser.close() if not browser_pool.full():
except: new_browser = await create_browser()
pass await browser_pool.put(new_browser)
if browser in browser_creation_times:
del browser_creation_times[browser]
browser = await create_browser()
await browser_pool.put(browser)
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""
Monitoring script for the Puppeteer API server.
This script helps track system resources and browser pool status.
"""
import asyncio
import aiohttp
import json
import time
import sys
from datetime import datetime
async def get_status(api_key, base_url="http://localhost:8000"):
"""Get system status from the API"""
async with aiohttp.ClientSession() as session:
headers = {"X-API-Key": api_key}
try:
async with session.get(f"{base_url}/status", headers=headers) as response:
if response.status == 200:
return await response.json()
else:
print(f"Error getting status: {response.status}")
return None
except Exception as e:
print(f"Exception getting status: {e}")
return None
async def emergency_cleanup(api_key, base_url="http://localhost:8000"):
"""Trigger emergency cleanup"""
async with aiohttp.ClientSession() as session:
headers = {"X-API-Key": api_key}
try:
async with session.post(f"{base_url}/emergency-cleanup", headers=headers) as response:
if response.status == 200:
result = await response.json()
print(f"Emergency cleanup: {result}")
return True
else:
print(f"Error triggering cleanup: {response.status}")
return False
except Exception as e:
print(f"Exception triggering cleanup: {e}")
return False
def print_status(status_data):
"""Print formatted status information"""
if not status_data:
print("No status data available")
return
print(f"\n{'='*60}")
print(f"Status Report - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'='*60}")
# System information
system = status_data.get('system', {})
print(f"\n📊 SYSTEM STATUS:")
print(f" CPU Usage: {system.get('cpu_percent', 'N/A')}%")
print(f" Memory: {system.get('memory_mb', 'N/A'):.1f} MB ({system.get('memory_percent', 'N/A'):.1f}%)")
print(f" Open Files: {system.get('open_files', 'N/A')}")
print(f" Network Connections: {system.get('connections', 'N/A')}")
print(f" Threads: {system.get('threads', 'N/A')}")
# Browser pool information
pool = status_data.get('browser_pool', {})
print(f"\n🌐 BROWSER POOL:")
print(f" Pool Size: {pool.get('pool_size', 'N/A')}")
print(f" Active Browsers: {pool.get('active_browsers', 'N/A')}")
print(f" Max Browsers: {pool.get('max_browsers', 'N/A')}")
print(f" Browser TTL: {pool.get('browser_ttl_seconds', 'N/A')}s")
print(f" Concurrent Operations Limit: {pool.get('concurrent_operations_limit', 'N/A')}")
# Browser ages
ages = pool.get('browser_ages_seconds', [])
if ages:
print(f" Browser Ages: {[f'{age:.0f}s' for age in ages]}")
# Warnings
warnings = []
if system.get('cpu_percent', 0) > 80:
warnings.append("⚠️ High CPU usage")
if system.get('memory_percent', 0) > 80:
warnings.append("⚠️ High memory usage")
if system.get('open_files', 0) > 1000:
warnings.append("⚠️ Many open files")
if pool.get('active_browsers', 0) >= pool.get('max_browsers', 0):
warnings.append("⚠️ Browser pool at capacity")
if warnings:
print(f"\n🚨 WARNINGS:")
for warning in warnings:
print(f" {warning}")
else:
print(f"\n✅ All systems normal")
async def monitor_loop(api_key, interval=30, base_url="http://localhost:8000"):
"""Continuous monitoring loop"""
print(f"Starting monitoring with {interval}s intervals...")
print(f"Press Ctrl+C to stop")
try:
while True:
status = await get_status(api_key, base_url)
print_status(status)
# Wait for next check
await asyncio.sleep(interval)
except KeyboardInterrupt:
print("\nMonitoring stopped by user")
async def main():
"""Main function"""
if len(sys.argv) < 2:
print("Usage: python monitor.py <api_key> [interval_seconds] [base_url]")
print("Example: python monitor.py my-api-key 30 http://localhost:8000")
sys.exit(1)
api_key = sys.argv[1]
interval = int(sys.argv[2]) if len(sys.argv) > 2 else 30
base_url = sys.argv[3] if len(sys.argv) > 3 else "http://localhost:8000"
if len(sys.argv) > 4 and sys.argv[4] == "cleanup":
# Emergency cleanup mode
print("Triggering emergency cleanup...")
success = await emergency_cleanup(api_key, base_url)
if success:
print("Emergency cleanup completed successfully")
else:
print("Emergency cleanup failed")
return
# Start monitoring
await monitor_loop(api_key, interval, base_url)
if __name__ == "__main__":
asyncio.run(main())