#!/usr/bin/env python3 """ Upload Log Analyzer Analyzes upload logs from beta testing to identify patterns, common errors, and performance bottlenecks. Usage: python3 scripts/analyze_upload_logs.py --date 2026-01-15 python3 scripts/analyze_upload_logs.py --last 7 # Last 7 days python3 scripts/analyze_upload_logs.py --upload-id upload_20260115_123456 """ import argparse import json import re from pathlib import Path from datetime import datetime, timedelta from collections import Counter, defaultdict from typing import List, Dict, Any import statistics class UploadLogAnalyzer: """Analyze upload logs and generate reports""" def __init__(self, log_dir: str = "logs/uploads"): self.log_dir = Path(log_dir) def analyze_date(self, date: str) -> Dict[str, Any]: """Analyze logs for a specific date (YYYY-MM-DD)""" # Read main upload log log_file = self.log_dir / f"uploads_{date}.log" error_log = self.log_dir / f"upload_errors_{date}.log" structured_log = self.log_dir / f"uploads_structured_{date}.jsonl" if not log_file.exists(): return {"error": f"Log file not found: {log_file}"} stats = { "date": date, "total_uploads": 0, "total_files": 0, "successful_files": 0, "failed_files": 0, "duplicate_files": 0, "errors_by_type": Counter(), "errors_by_stage": Counter(), "parse_errors": [], "gps_errors": [], "performance": { "parse_times": [], "matching_times": [], "total_times": [] }, "top_failures": [], "user_stats": defaultdict(int) } # Parse structured JSON log if structured_log.exists(): with open(structured_log, 'r') as f: for line in f: try: entry = json.loads(line) self._process_log_entry(entry, stats) except json.JSONDecodeError: continue # Parse error log for details if error_log.exists(): with open(error_log, 'r') as f: for line in f: self._process_error_line(line, stats) # Calculate summary statistics self._calculate_summary(stats) return stats def _process_log_entry(self, entry: Dict[str, Any], stats: Dict[str, Any]): """Process a single structured log entry""" event = entry.get("event") if event == "upload_attempt": stats["total_uploads"] += 1 stats["total_files"] += entry.get("file_count", 0) if entry.get("user_id"): stats["user_stats"]["authenticated"] += 1 else: stats["user_stats"]["anonymous"] += 1 elif event == "file_processing": stage = entry.get("stage") success = entry.get("success") if stage == "complete" and success: stats["successful_files"] += 1 elif not success: if stage == "validation" and entry.get("metadata", {}).get("status") == "duplicate": stats["duplicate_files"] += 1 else: stats["failed_files"] += 1 stats["errors_by_stage"][stage] += 1 # Track performance duration = entry.get("duration_ms") if duration and success: if stage == "parsing": stats["performance"]["parse_times"].append(duration) elif stage == "complete": stats["performance"]["total_times"].append(duration) elif event == "parse_error": stats["parse_errors"].append({ "filename": entry.get("filename"), "error_type": entry.get("error_type"), "error_message": entry.get("error_message"), "upload_id": entry.get("upload_id") }) stats["errors_by_type"][entry.get("error_type", "Unknown")] += 1 elif event == "gps_validation_error": stats["gps_errors"].append({ "filename": entry.get("filename"), "latitude": entry.get("latitude"), "longitude": entry.get("longitude"), "reason": entry.get("reason") }) stats["errors_by_type"]["GPS Validation"] += 1 elif event == "matching_results": duration = entry.get("duration_ms") if duration: stats["performance"]["matching_times"].append(duration) elif event == "performance_metrics": # Aggregate performance metrics for key, value in entry.items(): if key.endswith("_ms") and isinstance(value, (int, float)): perf_key = key.replace("_ms", "_times") if perf_key not in stats["performance"]: stats["performance"][perf_key] = [] stats["performance"][perf_key].append(value) def _process_error_line(self, line: str, stats: Dict[str, Any]): """Process error log line""" # Extract error type from log line error_match = re.search(r'(Exception|Error): (.+)', line) if error_match: error_type = error_match.group(1) stats["errors_by_type"][error_type] += 1 def _calculate_summary(self, stats: Dict[str, Any]): """Calculate summary statistics""" # Success rate total = stats["successful_files"] + stats["failed_files"] if total > 0: stats["success_rate"] = round(stats["successful_files"] / total * 100, 1) else: stats["success_rate"] = 0.0 # Performance statistics for key, times in stats["performance"].items(): if times: stats["performance"][f"{key}_avg"] = round(statistics.mean(times), 2) stats["performance"][f"{key}_median"] = round(statistics.median(times), 2) stats["performance"][f"{key}_p95"] = round(statistics.quantiles(times, n=20)[18], 2) if len(times) > 20 else None # Top failure reasons stats["top_failures"] = stats["errors_by_type"].most_common(10) def print_report(self, stats: Dict[str, Any]): """Print formatted report""" print("=" * 80) print(f"Upload Log Analysis - {stats['date']}") print("=" * 80) # Overview print("\nšŸ“Š OVERVIEW") print(f" Total Uploads: {stats['total_uploads']}") print(f" Total Files: {stats['total_files']}") print(f" Successful: {stats['successful_files']} ({stats['success_rate']}%)") print(f" Failed: {stats['failed_files']}") print(f" Duplicates: {stats['duplicate_files']}") # User statistics print("\nšŸ‘„ USERS") print(f" Authenticated: {stats['user_stats']['authenticated']}") print(f" Anonymous: {stats['user_stats']['anonymous']}") # Performance print("\n⚔ PERFORMANCE") perf = stats["performance"] if perf.get("parse_times_avg"): print(f" Parse Time: {perf['parse_times_avg']:.0f}ms avg, {perf['parse_times_median']:.0f}ms median") if perf.get("matching_times_avg"): print(f" Matching Time: {perf['matching_times_avg']:.0f}ms avg, {perf['matching_times_median']:.0f}ms median") if perf.get("total_times_avg"): print(f" Total Time: {perf['total_times_avg']:.0f}ms avg, {perf['total_times_median']:.0f}ms median") # Errors if stats["top_failures"]: print("\nāŒ TOP ERRORS") for error_type, count in stats["top_failures"]: print(f" {error_type:30} {count:5} occurrences") # Parse errors detail if stats["parse_errors"]: print(f"\nšŸ” PARSE ERRORS ({len(stats['parse_errors'])})") for error in stats["parse_errors"][:5]: # Show first 5 print(f" {error['filename']:30} {error['error_type']}: {error['error_message'][:50]}") # GPS errors detail if stats["gps_errors"]: print(f"\nšŸ“ GPS ERRORS ({len(stats['gps_errors'])})") for error in stats["gps_errors"][:5]: # Show first 5 print(f" {error['filename']:30} ({error['latitude']}, {error['longitude']}) - {error['reason']}") print("\n" + "=" * 80) def analyze_upload_id(self, upload_id: str) -> Dict[str, Any]: """Analyze a specific upload""" # Search all structured logs results = { "upload_id": upload_id, "events": [], "files": [], "errors": [], "timeline": [] } for log_file in self.log_dir.glob("uploads_structured_*.jsonl"): with open(log_file, 'r') as f: for line in f: try: entry = json.loads(line) if entry.get("upload_id") == upload_id: results["events"].append(entry) results["timeline"].append({ "timestamp": entry.get("timestamp"), "event": entry.get("event"), "details": entry }) except json.JSONDecodeError: continue # Sort timeline results["timeline"].sort(key=lambda x: x["timestamp"]) return results def print_upload_details(self, details: Dict[str, Any]): """Print detailed upload report""" print("=" * 80) print(f"Upload Details: {details['upload_id']}") print("=" * 80) print(f"\nTotal Events: {len(details['events'])}") print("\nšŸ“… TIMELINE:") for event in details["timeline"]: timestamp = event["timestamp"].split('T')[1][:12] # HH:MM:SS.mmm print(f" {timestamp} | {event['event']:20} | {self._format_event_details(event['details'])}") def _format_event_details(self, event: Dict[str, Any]) -> str: """Format event details for display""" event_type = event.get("event") if event_type == "upload_attempt": return f"{event.get('file_count')} files, {event.get('total_size_mb')} MB" elif event_type == "file_processing": success = "āœ“" if event.get("success") else "āœ—" return f"{success} {event.get('filename')} - {event.get('stage')} ({event.get('duration_ms', 0):.0f}ms)" elif event_type == "parse_error": return f"āŒ {event.get('filename')} - {event.get('error_type')}" elif event_type == "matching_results": return f"šŸ” {event.get('match_count')} matches ({event.get('duration_ms', 0):.0f}ms)" elif event_type == "upload_complete": return f"āœ… {event.get('successful_count')} success, {event.get('failed_count')} failed" else: return json.dumps(event, indent=2)[:100] def main(): parser = argparse.ArgumentParser(description="Analyze upload logs from beta testing") parser.add_argument("--date", help="Date to analyze (YYYY-MM-DD)") parser.add_argument("--last", type=int, help="Analyze last N days") parser.add_argument("--upload-id", help="Analyze specific upload ID") parser.add_argument("--log-dir", default="logs/uploads", help="Log directory") args = parser.parse_args() analyzer = UploadLogAnalyzer(log_dir=args.log_dir) if args.upload_id: # Analyze specific upload details = analyzer.analyze_upload_id(args.upload_id) analyzer.print_upload_details(details) elif args.date: # Analyze specific date stats = analyzer.analyze_date(args.date) analyzer.print_report(stats) elif args.last: # Analyze last N days for i in range(args.last): date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d") print(f"\n{'='*80}") print(f"Analyzing {date}") print('='*80) stats = analyzer.analyze_date(date) analyzer.print_report(stats) else: # Default: analyze today today = datetime.now().strftime("%Y-%m-%d") stats = analyzer.analyze_date(today) analyzer.print_report(stats) if __name__ == "__main__": main()