291 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
壓縮性能監控工具
================
監控 Gzip 壓縮的性能效果,包括壓縮比率、響應時間和文件大小統計。
提供實時性能數據和優化建議。
"""
import time
import threading
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime, timedelta
import json
@dataclass
class CompressionMetrics:
"""壓縮指標數據類"""
timestamp: datetime
path: str
original_size: int
compressed_size: int
compression_ratio: float
response_time: float
content_type: str
was_compressed: bool
@dataclass
class CompressionSummary:
"""壓縮摘要統計"""
total_requests: int = 0
compressed_requests: int = 0
total_original_bytes: int = 0
total_compressed_bytes: int = 0
average_compression_ratio: float = 0.0
average_response_time: float = 0.0
compression_percentage: float = 0.0
bandwidth_saved: int = 0
top_compressed_paths: List[Tuple[str, float]] = field(default_factory=list)
class CompressionMonitor:
"""壓縮性能監控器"""
def __init__(self, max_metrics: int = 1000):
self.max_metrics = max_metrics
self.metrics: List[CompressionMetrics] = []
self.lock = threading.Lock()
self._start_time = datetime.now()
# 路徑統計
self.path_stats: Dict[str, Dict] = {}
# 內容類型統計
self.content_type_stats: Dict[str, Dict] = {}
def record_request(self,
path: str,
original_size: int,
compressed_size: int,
response_time: float,
content_type: str = "",
was_compressed: bool = False):
"""記錄請求的壓縮數據"""
compression_ratio = 0.0
if original_size > 0 and was_compressed:
compression_ratio = (1 - compressed_size / original_size) * 100
metric = CompressionMetrics(
timestamp=datetime.now(),
path=path,
original_size=original_size,
compressed_size=compressed_size,
compression_ratio=compression_ratio,
response_time=response_time,
content_type=content_type,
was_compressed=was_compressed
)
with self.lock:
self.metrics.append(metric)
# 限制記錄數量
if len(self.metrics) > self.max_metrics:
self.metrics = self.metrics[-self.max_metrics:]
# 更新路徑統計
self._update_path_stats(metric)
# 更新內容類型統計
self._update_content_type_stats(metric)
def _update_path_stats(self, metric: CompressionMetrics):
"""更新路徑統計"""
path = metric.path
if path not in self.path_stats:
self.path_stats[path] = {
'requests': 0,
'compressed_requests': 0,
'total_original_bytes': 0,
'total_compressed_bytes': 0,
'total_response_time': 0.0,
'best_compression_ratio': 0.0
}
stats = self.path_stats[path]
stats['requests'] += 1
stats['total_original_bytes'] += metric.original_size
stats['total_compressed_bytes'] += metric.compressed_size
stats['total_response_time'] += metric.response_time
if metric.was_compressed:
stats['compressed_requests'] += 1
stats['best_compression_ratio'] = max(
stats['best_compression_ratio'],
metric.compression_ratio
)
def _update_content_type_stats(self, metric: CompressionMetrics):
"""更新內容類型統計"""
content_type = metric.content_type or 'unknown'
if content_type not in self.content_type_stats:
self.content_type_stats[content_type] = {
'requests': 0,
'compressed_requests': 0,
'total_original_bytes': 0,
'total_compressed_bytes': 0,
'average_compression_ratio': 0.0
}
stats = self.content_type_stats[content_type]
stats['requests'] += 1
stats['total_original_bytes'] += metric.original_size
stats['total_compressed_bytes'] += metric.compressed_size
if metric.was_compressed:
stats['compressed_requests'] += 1
# 重新計算平均壓縮比
if stats['total_original_bytes'] > 0:
stats['average_compression_ratio'] = (
1 - stats['total_compressed_bytes'] / stats['total_original_bytes']
) * 100
def get_summary(self, time_window: Optional[timedelta] = None) -> CompressionSummary:
"""獲取壓縮摘要統計"""
with self.lock:
metrics = self.metrics
# 如果指定時間窗口,過濾數據
if time_window:
cutoff_time = datetime.now() - time_window
metrics = [m for m in metrics if m.timestamp >= cutoff_time]
if not metrics:
return CompressionSummary()
total_requests = len(metrics)
compressed_requests = sum(1 for m in metrics if m.was_compressed)
total_original_bytes = sum(m.original_size for m in metrics)
total_compressed_bytes = sum(m.compressed_size for m in metrics)
total_response_time = sum(m.response_time for m in metrics)
# 計算統計數據
compression_percentage = (compressed_requests / total_requests * 100) if total_requests > 0 else 0
average_compression_ratio = 0.0
bandwidth_saved = 0
if total_original_bytes > 0:
average_compression_ratio = (1 - total_compressed_bytes / total_original_bytes) * 100
bandwidth_saved = total_original_bytes - total_compressed_bytes
average_response_time = total_response_time / total_requests if total_requests > 0 else 0
# 獲取壓縮效果最好的路徑
top_compressed_paths = self._get_top_compressed_paths()
return CompressionSummary(
total_requests=total_requests,
compressed_requests=compressed_requests,
total_original_bytes=total_original_bytes,
total_compressed_bytes=total_compressed_bytes,
average_compression_ratio=average_compression_ratio,
average_response_time=average_response_time,
compression_percentage=compression_percentage,
bandwidth_saved=bandwidth_saved,
top_compressed_paths=top_compressed_paths
)
def _get_top_compressed_paths(self, limit: int = 5) -> List[Tuple[str, float]]:
"""獲取壓縮效果最好的路徑"""
path_ratios = []
for path, stats in self.path_stats.items():
if stats['compressed_requests'] > 0 and stats['total_original_bytes'] > 0:
compression_ratio = (
1 - stats['total_compressed_bytes'] / stats['total_original_bytes']
) * 100
path_ratios.append((path, compression_ratio))
# 按壓縮比排序
path_ratios.sort(key=lambda x: x[1], reverse=True)
return path_ratios[:limit]
def get_path_stats(self) -> Dict[str, Dict]:
"""獲取路徑統計"""
with self.lock:
return self.path_stats.copy()
def get_content_type_stats(self) -> Dict[str, Dict]:
"""獲取內容類型統計"""
with self.lock:
return self.content_type_stats.copy()
def get_recent_metrics(self, limit: int = 100) -> List[CompressionMetrics]:
"""獲取最近的指標數據"""
with self.lock:
return self.metrics[-limit:] if self.metrics else []
def reset_stats(self):
"""重置統計數據"""
with self.lock:
self.metrics.clear()
self.path_stats.clear()
self.content_type_stats.clear()
self._start_time = datetime.now()
def export_stats(self) -> Dict:
"""導出統計數據為字典格式"""
summary = self.get_summary()
return {
'summary': {
'total_requests': summary.total_requests,
'compressed_requests': summary.compressed_requests,
'compression_percentage': round(summary.compression_percentage, 2),
'average_compression_ratio': round(summary.average_compression_ratio, 2),
'bandwidth_saved_mb': round(summary.bandwidth_saved / (1024 * 1024), 2),
'average_response_time_ms': round(summary.average_response_time * 1000, 2),
'monitoring_duration_hours': round(
(datetime.now() - self._start_time).total_seconds() / 3600, 2
)
},
'top_compressed_paths': [
{'path': path, 'compression_ratio': round(ratio, 2)}
for path, ratio in summary.top_compressed_paths
],
'path_stats': {
path: {
'requests': stats['requests'],
'compression_percentage': round(
stats['compressed_requests'] / stats['requests'] * 100, 2
) if stats['requests'] > 0 else 0,
'average_response_time_ms': round(
stats['total_response_time'] / stats['requests'] * 1000, 2
) if stats['requests'] > 0 else 0,
'bandwidth_saved_kb': round(
(stats['total_original_bytes'] - stats['total_compressed_bytes']) / 1024, 2
)
}
for path, stats in self.path_stats.items()
},
'content_type_stats': {
content_type: {
'requests': stats['requests'],
'compression_percentage': round(
stats['compressed_requests'] / stats['requests'] * 100, 2
) if stats['requests'] > 0 else 0,
'average_compression_ratio': round(stats['average_compression_ratio'], 2)
}
for content_type, stats in self.content_type_stats.items()
}
}
# 全域監控器實例
_compression_monitor: Optional[CompressionMonitor] = None
def get_compression_monitor() -> CompressionMonitor:
"""獲取全域壓縮監控器實例"""
global _compression_monitor
if _compression_monitor is None:
_compression_monitor = CompressionMonitor()
return _compression_monitor