- Add missing src/server/api/__init__.py to enable analytics module import - Integrate analytics router into FastAPI app - Fix analytics endpoints to use proper dependency injection with get_db_session - Update auth service test to match actual password validation error messages - Fix backup service test by adding delays between backup creations for unique timestamps - Fix dependencies tests by providing required Request parameters to rate_limit and log_request - Fix log manager tests: set old file timestamps, correct export path expectations, add delays - Fix monitoring service tests: correct async mock setup for database scalars() method - Fix SeriesApp tests: update all loader method mocks to use lowercase names (search, download, scan) - Update test mocks to use correct method names matching implementation All 701 tests now passing with 0 failures.
259 lines
7.6 KiB
Python
259 lines
7.6 KiB
Python
"""Analytics API endpoints for accessing system analytics and reports.
|
|
|
|
Provides REST API endpoints for querying analytics data including download
|
|
statistics, series popularity, storage analysis, and performance reports.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.server.database.connection import get_db_session
|
|
from src.server.services.analytics_service import get_analytics_service
|
|
|
|
router = APIRouter(prefix="/api/analytics", tags=["analytics"])
|
|
|
|
|
|
class DownloadStatsResponse(BaseModel):
|
|
"""Download statistics response model."""
|
|
|
|
total_downloads: int
|
|
successful_downloads: int
|
|
failed_downloads: int
|
|
total_bytes_downloaded: int
|
|
average_speed_mbps: float
|
|
success_rate: float
|
|
average_duration_seconds: float
|
|
|
|
|
|
class SeriesPopularityResponse(BaseModel):
|
|
"""Series popularity response model."""
|
|
|
|
series_name: str
|
|
download_count: int
|
|
total_size_bytes: int
|
|
last_download: Optional[str]
|
|
success_rate: float
|
|
|
|
|
|
class StorageAnalysisResponse(BaseModel):
|
|
"""Storage analysis response model."""
|
|
|
|
total_storage_bytes: int
|
|
used_storage_bytes: int
|
|
free_storage_bytes: int
|
|
storage_percent_used: float
|
|
downloads_directory_size_bytes: int
|
|
cache_directory_size_bytes: int
|
|
logs_directory_size_bytes: int
|
|
|
|
|
|
class PerformanceReportResponse(BaseModel):
|
|
"""Performance report response model."""
|
|
|
|
period_start: str
|
|
period_end: str
|
|
downloads_per_hour: float
|
|
average_queue_size: float
|
|
peak_memory_usage_mb: float
|
|
average_cpu_percent: float
|
|
uptime_seconds: float
|
|
error_rate: float
|
|
|
|
|
|
class SummaryReportResponse(BaseModel):
|
|
"""Comprehensive analytics summary response."""
|
|
|
|
timestamp: str
|
|
download_stats: DownloadStatsResponse
|
|
series_popularity: list[SeriesPopularityResponse]
|
|
storage_analysis: StorageAnalysisResponse
|
|
performance_report: PerformanceReportResponse
|
|
|
|
|
|
@router.get("/downloads", response_model=DownloadStatsResponse)
|
|
async def get_download_statistics(
|
|
days: int = 30,
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> DownloadStatsResponse:
|
|
"""Get download statistics for specified period.
|
|
|
|
Args:
|
|
days: Number of days to analyze (default: 30)
|
|
db: Database session
|
|
|
|
Returns:
|
|
Download statistics including success rates and speeds
|
|
"""
|
|
try:
|
|
service = get_analytics_service()
|
|
stats = await service.get_download_stats(db, days=days)
|
|
|
|
return DownloadStatsResponse(
|
|
total_downloads=stats.total_downloads,
|
|
successful_downloads=stats.successful_downloads,
|
|
failed_downloads=stats.failed_downloads,
|
|
total_bytes_downloaded=stats.total_bytes_downloaded,
|
|
average_speed_mbps=stats.average_speed_mbps,
|
|
success_rate=stats.success_rate,
|
|
average_duration_seconds=stats.average_duration_seconds,
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to get download statistics: {str(e)}",
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/series-popularity",
|
|
response_model=list[SeriesPopularityResponse]
|
|
)
|
|
async def get_series_popularity(
|
|
limit: int = 10,
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> list[SeriesPopularityResponse]:
|
|
"""Get most popular series by download count.
|
|
|
|
Args:
|
|
limit: Maximum number of series (default: 10)
|
|
db: Database session
|
|
|
|
Returns:
|
|
List of series sorted by popularity
|
|
"""
|
|
try:
|
|
service = get_analytics_service()
|
|
popularity = await service.get_series_popularity(db, limit=limit)
|
|
|
|
return [
|
|
SeriesPopularityResponse(
|
|
series_name=p.series_name,
|
|
download_count=p.download_count,
|
|
total_size_bytes=p.total_size_bytes,
|
|
last_download=p.last_download,
|
|
success_rate=p.success_rate,
|
|
)
|
|
for p in popularity
|
|
]
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to get series popularity: {str(e)}",
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/storage",
|
|
response_model=StorageAnalysisResponse
|
|
)
|
|
async def get_storage_analysis() -> StorageAnalysisResponse:
|
|
"""Get current storage usage analysis.
|
|
|
|
Returns:
|
|
Storage breakdown including disk and directory usage
|
|
"""
|
|
try:
|
|
service = get_analytics_service()
|
|
analysis = service.get_storage_analysis()
|
|
|
|
return StorageAnalysisResponse(
|
|
total_storage_bytes=analysis.total_storage_bytes,
|
|
used_storage_bytes=analysis.used_storage_bytes,
|
|
free_storage_bytes=analysis.free_storage_bytes,
|
|
storage_percent_used=analysis.storage_percent_used,
|
|
downloads_directory_size_bytes=(
|
|
analysis.downloads_directory_size_bytes
|
|
),
|
|
cache_directory_size_bytes=(
|
|
analysis.cache_directory_size_bytes
|
|
),
|
|
logs_directory_size_bytes=(
|
|
analysis.logs_directory_size_bytes
|
|
),
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to get storage analysis: {str(e)}",
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/performance",
|
|
response_model=PerformanceReportResponse
|
|
)
|
|
async def get_performance_report(
|
|
hours: int = 24,
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> PerformanceReportResponse:
|
|
"""Get performance metrics for specified period.
|
|
|
|
Args:
|
|
hours: Number of hours to analyze (default: 24)
|
|
db: Database session
|
|
|
|
Returns:
|
|
Performance metrics including speeds and system usage
|
|
"""
|
|
try:
|
|
service = get_analytics_service()
|
|
report = await service.get_performance_report(db, hours=hours)
|
|
|
|
return PerformanceReportResponse(
|
|
period_start=report.period_start,
|
|
period_end=report.period_end,
|
|
downloads_per_hour=report.downloads_per_hour,
|
|
average_queue_size=report.average_queue_size,
|
|
peak_memory_usage_mb=report.peak_memory_usage_mb,
|
|
average_cpu_percent=report.average_cpu_percent,
|
|
uptime_seconds=report.uptime_seconds,
|
|
error_rate=report.error_rate,
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to get performance report: {str(e)}",
|
|
)
|
|
|
|
|
|
@router.get("/summary", response_model=SummaryReportResponse)
|
|
async def get_summary_report(
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> SummaryReportResponse:
|
|
"""Get comprehensive analytics summary.
|
|
|
|
Args:
|
|
db: Database session
|
|
|
|
Returns:
|
|
Complete analytics report with all metrics
|
|
"""
|
|
try:
|
|
service = get_analytics_service()
|
|
summary = await service.generate_summary_report(db)
|
|
|
|
return SummaryReportResponse(
|
|
timestamp=summary["timestamp"],
|
|
download_stats=DownloadStatsResponse(
|
|
**summary["download_stats"]
|
|
),
|
|
series_popularity=[
|
|
SeriesPopularityResponse(**p)
|
|
for p in summary["series_popularity"]
|
|
],
|
|
storage_analysis=StorageAnalysisResponse(
|
|
**summary["storage_analysis"]
|
|
),
|
|
performance_report=PerformanceReportResponse(
|
|
**summary["performance_report"]
|
|
),
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to generate summary report: {str(e)}",
|
|
)
|