metrics.go

v0.6.1
Doc Versions Source
1
package proxy
2
3
import (
4
	"fmt"
5
	"strings"
6
	"sync"
7
	"time"
8
)
9
10
// CallRecord holds metrics for a single proxy call.
11
type CallRecord struct {
12
	Provider       string
13
	UpstreamDomain string
14
	Endpoint       string
15
	Model          string
16
	StatusCode     int
17
	Stream         bool
18
	Duration       time.Duration
19
	InputTokens    int
20
	OutputTokens   int
21
	Error          bool
22
	QuotaProbe     bool
23
	Timestamp      time.Time
24
}
25
26
// Metrics collects proxy call records for the session.
27
type Metrics struct {
28
	mu      sync.Mutex
29
	records []CallRecord
30
}
31
32
// DefaultMetrics is the package-level metrics instance.
33
var DefaultMetrics = &Metrics{}
34
35
// Record appends a call record.
36
func (m *Metrics) Record(r CallRecord) {
37
	if r.Timestamp.IsZero() {
38
		r.Timestamp = time.Now()
39
	}
40
	m.mu.Lock()
41
	m.records = append(m.records, r)
42
	m.mu.Unlock()
43
}
44
45
// Summary returns a formatted metrics summary. Returns empty string if no calls recorded.
46
func (m *Metrics) Summary() string {
47
	m.mu.Lock()
48
	records := make([]CallRecord, len(m.records))
49
	copy(records, m.records)
50
	m.mu.Unlock()
51
52
	if len(records) == 0 {
53
		return ""
54
	}
55
56
	var (
57
		total       int
58
		errors      int
59
		quotaProbes int
60
		totalIn     int
61
		totalOut    int
62
		totalDur    time.Duration
63
64
		byModel    = make(map[string]*modelStats)
65
		byStatus   = make(map[int]int)
66
		byDomain   = make(map[string]int)
67
		byEndpoint = make(map[string]int)
68
	)
69
70
	for _, r := range records {
71
		total++
72
		totalIn += r.InputTokens
73
		totalOut += r.OutputTokens
74
		totalDur += r.Duration
75
76
		if r.Error {
77
			errors++
78
		}
79
		if r.QuotaProbe {
80
			quotaProbes++
81
		}
82
83
		byStatus[r.StatusCode]++
84
		byDomain[r.UpstreamDomain]++
85
		byEndpoint[r.Endpoint]++
86
87
		ms, ok := byModel[r.Model]
88
		if !ok {
89
			ms = &modelStats{}
90
			byModel[r.Model] = ms
91
		}
92
		ms.count++
93
		ms.inputTokens += r.InputTokens
94
		ms.outputTokens += r.OutputTokens
95
		ms.totalDur += r.Duration
96
	}
97
98
	var b strings.Builder
99
	b.WriteString("\n")
100
	b.WriteString(strings.Repeat("─", 60) + "\n")
101
	b.WriteString("  Proxy Metrics Summary\n")
102
	b.WriteString(strings.Repeat("─", 60) + "\n")
103
104
	fmt.Fprintf(&b, "  Total requests:  %d\n", total)
105
	fmt.Fprintf(&b, "  Errors:          %d\n", errors)
106
	fmt.Fprintf(&b, "  Quota probes:    %d\n", quotaProbes)
107
	fmt.Fprintf(&b, "  Total duration:  %s\n", totalDur.Round(time.Millisecond))
108
	fmt.Fprintf(&b, "  Total tokens:    %d in / %d out\n", totalIn, totalOut)
109
110
	if len(byModel) > 0 {
111
		b.WriteString("\n  By Model:\n")
112
		for model, ms := range byModel {
113
			avg := time.Duration(0)
114
			if ms.count > 0 {
115
				avg = ms.totalDur / time.Duration(ms.count)
116
			}
117
			fmt.Fprintf(&b, "    %-30s  %3d req, %6d in / %6d out, avg %s\n",
118
				model, ms.count, ms.inputTokens, ms.outputTokens, avg.Round(time.Millisecond))
119
		}
120
	}
121
122
	if len(byStatus) > 0 {
123
		b.WriteString("\n  By Status Code:\n")
124
		for code, count := range byStatus {
125
			label := fmt.Sprintf("%d", code)
126
			if code == 0 {
127
				label = "N/A (error/intercepted)"
128
			}
129
			fmt.Fprintf(&b, "    %-25s  %d\n", label, count)
130
		}
131
	}
132
133
	if len(byDomain) > 0 {
134
		b.WriteString("\n  By Upstream Domain:\n")
135
		for domain, count := range byDomain {
136
			if domain == "" {
137
				domain = "(local)"
138
			}
139
			fmt.Fprintf(&b, "    %-35s  %d\n", domain, count)
140
		}
141
	}
142
143
	if len(byEndpoint) > 0 {
144
		b.WriteString("\n  By Endpoint:\n")
145
		for ep, count := range byEndpoint {
146
			fmt.Fprintf(&b, "    %-35s  %d\n", ep, count)
147
		}
148
	}
149
150
	b.WriteString(strings.Repeat("─", 60) + "\n")
151
	return b.String()
152
}
153
154
type modelStats struct {
155
	count        int
156
	inputTokens  int
157
	outputTokens int
158
	totalDur     time.Duration
159
}
160

Source Files