from flask import Blueprint, jsonify, request from server.database import Session from server.permissions import admin_or_higher from models.models import ClientLog, Client, LogLevel from sqlalchemy import desc, func from datetime import datetime, timedelta, timezone import json client_logs_bp = Blueprint("client_logs", __name__, url_prefix="/api/client-logs") @client_logs_bp.route("/test", methods=["GET"]) def test_client_logs(): """Test endpoint to verify logging infrastructure (no auth required)""" session = Session() try: # Count total logs total_logs = session.query(func.count(ClientLog.id)).scalar() # Count by level error_count = session.query(func.count(ClientLog.id)).filter_by(level=LogLevel.ERROR).scalar() warn_count = session.query(func.count(ClientLog.id)).filter_by(level=LogLevel.WARN).scalar() info_count = session.query(func.count(ClientLog.id)).filter_by(level=LogLevel.INFO).scalar() # Get last 5 logs recent_logs = session.query(ClientLog).order_by(desc(ClientLog.timestamp)).limit(5).all() recent = [] for log in recent_logs: recent.append({ "client_uuid": log.client_uuid, "level": log.level.value if log.level else None, "message": log.message, "timestamp": log.timestamp.isoformat() if log.timestamp else None }) session.close() return jsonify({ "status": "ok", "infrastructure": "working", "total_logs": total_logs, "counts": { "ERROR": error_count, "WARN": warn_count, "INFO": info_count }, "recent_5": recent }) except Exception as e: session.close() return jsonify({"status": "error", "message": str(e)}), 500 @client_logs_bp.route("//logs", methods=["GET"]) @admin_or_higher def get_client_logs(uuid): """ Get logs for a specific client Query params: - level: ERROR, WARN, INFO, DEBUG (optional) - limit: number of entries (default 50, max 500) - since: ISO timestamp (optional) Example: /api/client-logs/abc-123/logs?level=ERROR&limit=100 """ session = Session() try: # Verify client exists client = session.query(Client).filter_by(uuid=uuid).first() if not client: session.close() return jsonify({"error": "Client not found"}), 404 # Parse query parameters level_param = request.args.get('level') limit = min(int(request.args.get('limit', 50)), 500) since_param = request.args.get('since') # Build query query = session.query(ClientLog).filter_by(client_uuid=uuid) # Filter by log level if level_param: try: level_enum = LogLevel[level_param.upper()] query = query.filter_by(level=level_enum) except KeyError: session.close() return jsonify({"error": f"Invalid level: {level_param}. Must be ERROR, WARN, INFO, or DEBUG"}), 400 # Filter by timestamp if since_param: try: # Handle both with and without 'Z' suffix since_str = since_param.replace('Z', '+00:00') since_dt = datetime.fromisoformat(since_str) if since_dt.tzinfo is None: since_dt = since_dt.replace(tzinfo=timezone.utc) query = query.filter(ClientLog.timestamp >= since_dt) except ValueError: session.close() return jsonify({"error": "Invalid timestamp format. Use ISO 8601"}), 400 # Execute query logs = query.order_by(desc(ClientLog.timestamp)).limit(limit).all() # Format results result = [] for log in logs: entry = { "id": log.id, "timestamp": log.timestamp.isoformat() if log.timestamp else None, "level": log.level.value if log.level else None, "message": log.message, "context": {} } # Parse context JSON if log.context: try: entry["context"] = json.loads(log.context) except json.JSONDecodeError: entry["context"] = {"raw": log.context} result.append(entry) session.close() return jsonify({ "client_uuid": uuid, "logs": result, "count": len(result), "limit": limit }) except Exception as e: session.close() return jsonify({"error": f"Server error: {str(e)}"}), 500 @client_logs_bp.route("/summary", methods=["GET"]) @admin_or_higher def get_logs_summary(): """ Get summary of errors/warnings across all clients in last 24 hours Returns count of ERROR, WARN, INFO logs per client Example response: { "summary": { "client-uuid-1": {"ERROR": 5, "WARN": 12, "INFO": 45}, "client-uuid-2": {"ERROR": 0, "WARN": 3, "INFO": 20} }, "period_hours": 24, "timestamp": "2026-03-09T21:00:00Z" } """ session = Session() try: # Get hours parameter (default 24, max 168 = 1 week) hours = min(int(request.args.get('hours', 24)), 168) since = datetime.now(timezone.utc) - timedelta(hours=hours) # Query log counts grouped by client and level stats = session.query( ClientLog.client_uuid, ClientLog.level, func.count(ClientLog.id).label('count') ).filter( ClientLog.timestamp >= since ).group_by( ClientLog.client_uuid, ClientLog.level ).all() # Build summary dictionary summary = {} for stat in stats: uuid = stat.client_uuid if uuid not in summary: # Initialize all levels to 0 summary[uuid] = { "ERROR": 0, "WARN": 0, "INFO": 0, "DEBUG": 0 } summary[uuid][stat.level.value] = stat.count # Get client info for enrichment clients = session.query(Client.uuid, Client.hostname, Client.description).all() client_info = {c.uuid: {"hostname": c.hostname, "description": c.description} for c in clients} # Enrich summary with client info enriched_summary = {} for uuid, counts in summary.items(): enriched_summary[uuid] = { "counts": counts, "info": client_info.get(uuid, {}) } session.close() return jsonify({ "summary": enriched_summary, "period_hours": hours, "since": since.isoformat(), "timestamp": datetime.now(timezone.utc).isoformat() }) except Exception as e: session.close() return jsonify({"error": f"Server error: {str(e)}"}), 500 @client_logs_bp.route("/recent-errors", methods=["GET"]) @admin_or_higher def get_recent_errors(): """ Get recent ERROR logs across all clients Query params: - limit: number of entries (default 20, max 100) Useful for system-wide error monitoring """ session = Session() try: limit = min(int(request.args.get('limit', 20)), 100) # Get recent errors from all clients logs = session.query(ClientLog).filter_by( level=LogLevel.ERROR ).order_by( desc(ClientLog.timestamp) ).limit(limit).all() result = [] for log in logs: entry = { "id": log.id, "client_uuid": log.client_uuid, "timestamp": log.timestamp.isoformat() if log.timestamp else None, "message": log.message, "context": json.loads(log.context) if log.context else {} } result.append(entry) session.close() return jsonify({ "errors": result, "count": len(result) }) except Exception as e: session.close() return jsonify({"error": f"Server error: {str(e)}"}), 500