Files
mmw-agent/internal/collector/metrics.go
T
2026-04-10 15:25:21 +08:00

178 lines
4.6 KiB
Go

package collector
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"mmw-agent/internal/constants"
)
// XrayMetrics 表示 Xray /debug/vars 的响应结构。
type XrayMetrics struct {
Stats *XrayStats `json:"stats,omitempty"`
}
// XrayStats 包含入站、出站和用户维度的流量统计。
type XrayStats struct {
Inbound map[string]TrafficData `json:"inbound,omitempty"`
Outbound map[string]TrafficData `json:"outbound,omitempty"`
User map[string]TrafficData `json:"user,omitempty"`
}
// TrafficData 表示上下行流量(字节)。
type TrafficData struct {
Uplink int64 `json:"uplink"`
Downlink int64 `json:"downlink"`
}
// XrayConfig 用于读取 xray config.json 中的 metrics 监听配置。
type XrayConfig struct {
Log json.RawMessage `json:"log,omitempty"`
DNS json.RawMessage `json:"dns,omitempty"`
API json.RawMessage `json:"api,omitempty"`
Stats json.RawMessage `json:"stats,omitempty"`
Policy json.RawMessage `json:"policy,omitempty"`
Routing json.RawMessage `json:"routing,omitempty"`
Inbounds json.RawMessage `json:"inbounds,omitempty"`
Outbounds json.RawMessage `json:"outbounds,omitempty"`
Metrics *MetricsConfig `json:"metrics,omitempty"`
}
// MetricsConfig 对应 xray 配置中的 metrics 段。
type MetricsConfig struct {
Tag string `json:"tag,omitempty"`
Listen string `json:"listen,omitempty"` // 格式示例: "127.0.0.1:38889"
}
// Collector 负责采集 Xray 流量指标。
type Collector struct {
httpClient *http.Client
defaultMetricsPort int
defaultMetricsHost string
}
// 创建指标采集器。
func NewCollector() *Collector {
return &Collector{
httpClient: &http.Client{Timeout: constants.DefaultHTTPClientTimeout},
defaultMetricsPort: constants.DefaultMetricsPort,
defaultMetricsHost: constants.DefaultMetricsHost,
}
}
// 从 xray 配置中读取 metrics 监听地址和端口。
func (c *Collector) GetMetricsPortFromConfig(configPath string) (string, int, error) {
if configPath == "" {
return constants.DefaultMetricsHost, c.defaultMetricsPort, nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return constants.DefaultMetricsHost, c.defaultMetricsPort, fmt.Errorf("read config file: %w", err)
}
var config XrayConfig
if err := json.Unmarshal(data, &config); err != nil {
return constants.DefaultMetricsHost, c.defaultMetricsPort, fmt.Errorf("parse config file: %w", err)
}
if config.Metrics == nil || config.Metrics.Listen == "" {
return "", 0, fmt.Errorf("metrics not configured in xray config")
}
// 解析监听地址,支持 "127.0.0.1:38889" 或 ":38889"
listen := config.Metrics.Listen
host := constants.DefaultMetricsHost
var port int
if strings.Contains(listen, ":") {
parts := strings.Split(listen, ":")
if len(parts) == 2 {
if parts[0] != "" {
host = parts[0]
}
p, err := strconv.Atoi(parts[1])
if err != nil {
return "", 0, fmt.Errorf("invalid metrics port: %s", parts[1])
}
port = p
}
} else {
// 兼容仅填写端口的写法
p, err := strconv.Atoi(listen)
if err != nil {
return "", 0, fmt.Errorf("invalid metrics listen format: %s", listen)
}
port = p
}
if port <= 0 || port > 65535 {
return "", 0, fmt.Errorf("invalid metrics port: %d", port)
}
return host, port, nil
}
// 从 Xray 的 /debug/vars 拉取指标。
func (c *Collector) FetchMetrics(host string, port int) (*XrayMetrics, error) {
url := fmt.Sprintf("http://%s:%d/debug/vars", host, port)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
var metrics XrayMetrics
if err := json.Unmarshal(body, &metrics); err != nil {
return nil, fmt.Errorf("unmarshal metrics: %w", err)
}
return &metrics, nil
}
// 将 source 的统计合并到 dest。
func MergeStats(dest, source *XrayStats) {
if source == nil {
return
}
if dest.Inbound == nil {
dest.Inbound = make(map[string]TrafficData)
}
if dest.Outbound == nil {
dest.Outbound = make(map[string]TrafficData)
}
if dest.User == nil {
dest.User = make(map[string]TrafficData)
}
for k, v := range source.Inbound {
dest.Inbound[k] = v
}
for k, v := range source.Outbound {
dest.Outbound[k] = v
}
for k, v := range source.User {
dest.User[k] = v
}
}