Files
ProxyAuto/services/traffic_monitor.py

339 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""流量监控服务 - 获取 EC2/Lightsail 流量数据"""
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from typing import Any
from zoneinfo import ZoneInfo
import boto3
logger = logging.getLogger(__name__)
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
def create_cloudwatch_client(region: str, aws_access_key: str, aws_secret_key: str):
"""创建 CloudWatch 客户端"""
return boto3.client(
"cloudwatch",
region_name=region,
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
)
def get_ec2_traffic(
region: str,
instance_id: str,
aws_access_key: str,
aws_secret_key: str,
start_time: datetime,
end_time: datetime,
period: int = 3600,
) -> dict[str, Any]:
"""
获取 EC2 实例的流量数据
Args:
region: AWS 区域
instance_id: EC2 实例 ID
aws_access_key: AWS Access Key
aws_secret_key: AWS Secret Key
start_time: 开始时间
end_time: 结束时间
period: 数据点间隔默认1小时
Returns:
{
"network_in": 下载流量(字节),
"network_out": 上传流量(字节),
"data_points": 详细数据点列表
}
"""
try:
cloudwatch = create_cloudwatch_client(region, aws_access_key, aws_secret_key)
# 获取 NetworkIn下载
network_in_response = cloudwatch.get_metric_statistics(
Namespace="AWS/EC2",
MetricName="NetworkIn",
Dimensions=[{"Name": "InstanceId", "Value": instance_id}],
StartTime=start_time,
EndTime=end_time,
Period=period,
Statistics=["Sum"],
)
# 获取 NetworkOut上传
network_out_response = cloudwatch.get_metric_statistics(
Namespace="AWS/EC2",
MetricName="NetworkOut",
Dimensions=[{"Name": "InstanceId", "Value": instance_id}],
StartTime=start_time,
EndTime=end_time,
Period=period,
Statistics=["Sum"],
)
# 计算总流量
network_in_total = sum(dp["Sum"] for dp in network_in_response.get("Datapoints", []))
network_out_total = sum(dp["Sum"] for dp in network_out_response.get("Datapoints", []))
# 合并数据点用于图表
data_points = []
in_points = {dp["Timestamp"]: dp["Sum"] for dp in network_in_response.get("Datapoints", [])}
out_points = {dp["Timestamp"]: dp["Sum"] for dp in network_out_response.get("Datapoints", [])}
all_timestamps = sorted(set(in_points.keys()) | set(out_points.keys()))
for ts in all_timestamps:
data_points.append({
"timestamp": ts.astimezone(SHANGHAI_TZ).isoformat(),
"network_in": in_points.get(ts, 0),
"network_out": out_points.get(ts, 0),
})
return {
"ok": True,
"network_in": network_in_total,
"network_out": network_out_total,
"total": network_in_total + network_out_total,
"data_points": data_points,
}
except Exception as e:
logger.exception("Failed to get EC2 traffic for %s", instance_id)
return {"ok": False, "message": str(e), "network_in": 0, "network_out": 0, "total": 0, "data_points": []}
def get_lightsail_traffic(
region: str,
instance_name: str,
aws_access_key: str,
aws_secret_key: str,
start_time: datetime,
end_time: datetime,
period: int = 3600,
) -> dict[str, Any]:
"""
获取 Lightsail 实例的流量数据
Args:
region: AWS 区域
instance_name: Lightsail 实例名称
aws_access_key: AWS Access Key
aws_secret_key: AWS Secret Key
start_time: 开始时间
end_time: 结束时间
period: 数据点间隔默认1小时
Returns:
{
"network_in": 下载流量(字节),
"network_out": 上传流量(字节),
"data_points": 详细数据点列表
}
"""
try:
lightsail = boto3.client(
"lightsail",
region_name=region,
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
)
# 获取 NetworkIn下载
network_in_response = lightsail.get_instance_metric_data(
instanceName=instance_name,
metricName="NetworkIn",
period=period,
startTime=start_time,
endTime=end_time,
unit="Bytes",
statistics=["Sum"],
)
# 获取 NetworkOut上传
network_out_response = lightsail.get_instance_metric_data(
instanceName=instance_name,
metricName="NetworkOut",
period=period,
startTime=start_time,
endTime=end_time,
unit="Bytes",
statistics=["Sum"],
)
# 计算总流量
in_data = network_in_response.get("metricData", [])
out_data = network_out_response.get("metricData", [])
network_in_total = sum(dp.get("sum", 0) for dp in in_data)
network_out_total = sum(dp.get("sum", 0) for dp in out_data)
# 合并数据点
data_points = []
in_points = {dp["timestamp"]: dp.get("sum", 0) for dp in in_data}
out_points = {dp["timestamp"]: dp.get("sum", 0) for dp in out_data}
all_timestamps = sorted(set(in_points.keys()) | set(out_points.keys()))
for ts in all_timestamps:
data_points.append({
"timestamp": ts.astimezone(SHANGHAI_TZ).isoformat(),
"network_in": in_points.get(ts, 0),
"network_out": out_points.get(ts, 0),
})
return {
"ok": True,
"network_in": network_in_total,
"network_out": network_out_total,
"total": network_in_total + network_out_total,
"data_points": data_points,
}
except Exception as e:
logger.exception("Failed to get Lightsail traffic for %s", instance_name)
return {"ok": False, "message": str(e), "network_in": 0, "network_out": 0, "total": 0, "data_points": []}
def get_machine_traffic(
aws_service: str,
region: str,
instance_id: str,
aws_access_key: str,
aws_secret_key: str,
start_time: datetime,
end_time: datetime,
period: int = 3600,
) -> dict[str, Any]:
"""
统一接口:根据服务类型获取流量数据
"""
if aws_service == "lightsail":
return get_lightsail_traffic(
region=region,
instance_name=instance_id,
aws_access_key=aws_access_key,
aws_secret_key=aws_secret_key,
start_time=start_time,
end_time=end_time,
period=period,
)
else:
return get_ec2_traffic(
region=region,
instance_id=instance_id,
aws_access_key=aws_access_key,
aws_secret_key=aws_secret_key,
start_time=start_time,
end_time=end_time,
period=period,
)
def get_current_month_traffic(
aws_service: str,
region: str,
instance_id: str,
aws_access_key: str,
aws_secret_key: str,
) -> dict[str, Any]:
"""获取当月流量数据"""
now = datetime.now(SHANGHAI_TZ)
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
return get_machine_traffic(
aws_service=aws_service,
region=region,
instance_id=instance_id,
aws_access_key=aws_access_key,
aws_secret_key=aws_secret_key,
start_time=start_of_month,
end_time=now,
period=3600, # 每小时一个数据点
)
def get_current_day_traffic(
aws_service: str,
region: str,
instance_id: str,
aws_access_key: str,
aws_secret_key: str,
) -> dict[str, Any]:
"""获取当日流量数据"""
now = datetime.now(SHANGHAI_TZ)
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
return get_machine_traffic(
aws_service=aws_service,
region=region,
instance_id=instance_id,
aws_access_key=aws_access_key,
aws_secret_key=aws_secret_key,
start_time=start_of_day,
end_time=now,
period=300, # 每5分钟一个数据点
)
def get_all_time_traffic(
aws_service: str,
region: str,
instance_id: str,
aws_access_key: str,
aws_secret_key: str,
created_at: datetime = None,
) -> dict[str, Any]:
"""
获取建站至今的总流量数据
注意: CloudWatch 数据保留期限有限:
- 小于60秒的数据点保留3小时
- 60秒(1分钟)的数据点保留15天
- 300秒(5分钟)的数据点保留63天
- 3600秒(1小时)的数据点保留455天(约15个月)
因此这里只能获取最近约15个月的数据
"""
now = datetime.now(SHANGHAI_TZ)
# 如果提供了创建时间使用它否则使用15个月前
if created_at:
# 确保时区正确
if created_at.tzinfo is None:
start_time = created_at.replace(tzinfo=SHANGHAI_TZ)
else:
start_time = created_at.astimezone(SHANGHAI_TZ)
else:
# CloudWatch 最多保留约15个月的小时级数据
start_time = now - timedelta(days=455)
return get_machine_traffic(
aws_service=aws_service,
region=region,
instance_id=instance_id,
aws_access_key=aws_access_key,
aws_secret_key=aws_secret_key,
start_time=start_time,
end_time=now,
period=86400, # 每天一个数据点
)
def format_bytes(bytes_value: float) -> str:
"""格式化字节数为可读格式"""
if bytes_value < 0:
return "0 B"
units = ["B", "KB", "MB", "GB", "TB"]
unit_index = 0
value = float(bytes_value)
while value >= 1024 and unit_index < len(units) - 1:
value /= 1024
unit_index += 1
if unit_index == 0:
return f"{int(value)} {units[unit_index]}"
return f"{value:.2f} {units[unit_index]}"