metrics.go

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

Source Files