feat(embedded): add Perses + VictoriaMetrics subprocess management with auto-download

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Alexander
2026-04-14 21:56:32 +02:00
parent be4113e7ef
commit 859640d814
5 changed files with 854 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
package embedded
import (
"embed"
)
//go:embed dashboard/proxy.json
var dashboardFS embed.FS
func DashboardJSON() ([]byte, error) {
return dashboardFS.ReadFile("dashboard/proxy.json")
}
+450
View File
@@ -0,0 +1,450 @@
{
"kind": "Dashboard",
"metadata": {
"name": "proxy",
"createdAt": "2026-04-14T19:47:48.013238204Z",
"updatedAt": "2026-04-14T19:49:30.874125459Z",
"version": 1,
"project": "anthropic-proxy"
},
"spec": {
"display": {
"name": "Anthropic Proxy"
},
"datasources": {
"vm": {
"default": true,
"plugin": {
"kind": "PrometheusDatasource",
"spec": {
"directUrl": "http://localhost:9428"
}
}
}
},
"panels": {
"latency": {
"kind": "Panel",
"spec": {
"display": {
"name": "Latency"
},
"plugin": {
"kind": "TimeSeriesChart",
"spec": {
"legend": {
"position": "bottom"
},
"yAxis": {
"format": {
"unit": "milliseconds"
}
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "histogram_quantile(0.50, rate(proxy_request_duration_ms_milliseconds_bucket[5m]))",
"seriesNameFormat": "p50"
}
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "histogram_quantile(0.95, rate(proxy_request_duration_ms_milliseconds_bucket[5m]))",
"seriesNameFormat": "p95"
}
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "histogram_quantile(0.99, rate(proxy_request_duration_ms_milliseconds_bucket[5m]))",
"seriesNameFormat": "p99"
}
}
}
}
]
}
},
"request_rate": {
"kind": "Panel",
"spec": {
"display": {
"name": "Request Rate"
},
"plugin": {
"kind": "TimeSeriesChart",
"spec": {
"legend": {
"position": "bottom"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "rate(proxy_request_count_total[5m])",
"seriesNameFormat": "req/s"
}
}
}
}
]
}
},
"token_rate": {
"kind": "Panel",
"spec": {
"display": {
"name": "Token Rate"
},
"plugin": {
"kind": "TimeSeriesChart",
"spec": {
"legend": {
"position": "bottom"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "rate(proxy_tokens_input_total[5m]) * 60",
"seriesNameFormat": "input/min"
}
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "rate(proxy_tokens_output_total[5m]) * 60",
"seriesNameFormat": "output/min"
}
}
}
}
]
}
},
"tokens_5h": {
"kind": "Panel",
"spec": {
"display": {
"name": "5h Tokens"
},
"plugin": {
"kind": "StatChart",
"spec": {
"calculation": "last",
"format": {
"unit": "decimal"
},
"sparkline": {}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "increase(proxy_tokens_output_total[3h])"
}
}
}
}
]
}
},
"tokens_7d": {
"kind": "Panel",
"spec": {
"display": {
"name": "7d Tokens"
},
"plugin": {
"kind": "StatChart",
"spec": {
"calculation": "last",
"format": {
"unit": "decimal"
},
"sparkline": {}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "increase(proxy_tokens_output_total[9h])"
}
}
}
}
]
}
},
"util_5h": {
"kind": "Panel",
"spec": {
"display": {
"name": "5h Utilization"
},
"plugin": {
"kind": "GaugeChart",
"spec": {
"calculation": "last",
"format": {
"unit": "percent"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "proxy_usage_utilization{window=\"5h\"}"
}
}
}
}
]
}
},
"util_7d": {
"kind": "Panel",
"spec": {
"display": {
"name": "7d Utilization"
},
"plugin": {
"kind": "GaugeChart",
"spec": {
"calculation": "last",
"format": {
"unit": "percent"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "vm"
},
"query": "proxy_usage_utilization{window=\"7d\"}"
}
}
}
}
]
}
}
},
"layouts": [
{
"kind": "Grid",
"spec": {
"display": {
"title": "Utilization"
},
"items": [
{
"x": 0,
"y": 0,
"width": 6,
"height": 5,
"content": {
"$ref": "#/spec/panels/util_5h"
}
},
{
"x": 6,
"y": 0,
"width": 6,
"height": 5,
"content": {
"$ref": "#/spec/panels/util_7d"
}
},
{
"x": 12,
"y": 0,
"width": 6,
"height": 5,
"content": {
"$ref": "#/spec/panels/tokens_5h"
}
},
{
"x": 18,
"y": 0,
"width": 6,
"height": 5,
"content": {
"$ref": "#/spec/panels/tokens_7d"
}
}
]
}
},
{
"kind": "Grid",
"spec": {
"display": {
"title": "Traffic"
},
"items": [
{
"x": 0,
"y": 0,
"width": 12,
"height": 8,
"content": {
"$ref": "#/spec/panels/request_rate"
}
},
{
"x": 12,
"y": 0,
"width": 12,
"height": 8,
"content": {
"$ref": "#/spec/panels/latency"
}
}
]
}
},
{
"kind": "Grid",
"spec": {
"display": {
"title": "Tokens"
},
"items": [
{
"x": 0,
"y": 0,
"width": 24,
"height": 8,
"content": {
"$ref": "#/spec/panels/token_rate"
}
}
]
}
}
],
"duration": "1h",
"refreshInterval": "10s"
}
}
+155
View File
@@ -0,0 +1,155 @@
package embedded
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/rs/zerolog/log"
)
const cacheDir = ".cache/anthropic-proxy/bin"
var downloads = map[string]struct {
urlTemplate string
version string
extractName string
}{
"victoria-metrics": {
urlTemplate: "https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v%s/victoria-metrics-%s-v%s.tar.gz",
version: "1.118.0",
extractName: "victoria-metrics-prod",
},
"perses": {
urlTemplate: "https://github.com/perses/perses/releases/download/v%s/perses_%s_%s_%s.tar.gz",
version: "0.53.1",
},
}
func ensureBinary(name, configPath, configBinDir string) (string, error) {
if configPath != "" {
if p, err := exec.LookPath(configPath); err == nil {
return p, nil
}
}
if p, err := exec.LookPath(name); err == nil {
return p, nil
}
binDir := configBinDir
if binDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("get home dir: %w", err)
}
binDir = filepath.Join(home, cacheDir)
}
cachedPath := filepath.Join(binDir, name)
if _, err := os.Stat(cachedPath); err == nil {
return cachedPath, nil
}
log.Info().Str("binary", name).Msg("downloading binary (first run)")
if err := os.MkdirAll(binDir, 0o755); err != nil {
return "", fmt.Errorf("create cache dir: %w", err)
}
url, err := downloadURL(name)
if err != nil {
return "", err
}
if err := extractAll(url, binDir); err != nil {
return "", fmt.Errorf("download %s: %w", name, err)
}
d := downloads[name]
if d.extractName != "" {
oldPath := filepath.Join(binDir, d.extractName)
if _, err := os.Stat(oldPath); err == nil {
os.Rename(oldPath, cachedPath)
}
}
if _, err := os.Stat(cachedPath); err != nil {
return "", fmt.Errorf("binary %s not found after extraction", name)
}
log.Info().Str("binary", name).Str("path", cachedPath).Msg("binary downloaded")
return cachedPath, nil
}
func downloadURL(name string) (string, error) {
goarch := runtime.GOARCH
goos := runtime.GOOS
d, ok := downloads[name]
if !ok {
return "", fmt.Errorf("unknown binary: %s", name)
}
switch name {
case "victoria-metrics":
vmOS := fmt.Sprintf("%s-%s", goos, goarch)
return fmt.Sprintf(d.urlTemplate, d.version, vmOS, d.version), nil
case "perses":
return fmt.Sprintf(d.urlTemplate, d.version, d.version, goos, goarch), nil
}
return "", fmt.Errorf("unknown binary: %s", name)
}
func extractAll(url, destDir string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("download failed: HTTP %d from %s", resp.StatusCode, url)
}
gz, err := gzip.NewReader(resp.Body)
if err != nil {
return fmt.Errorf("gzip reader: %w", err)
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
hdr, err := tr.Next()
if err == io.EOF {
return nil
}
if err != nil {
return fmt.Errorf("read tar: %w", err)
}
target := filepath.Join(destDir, hdr.Name)
switch hdr.Typeflag {
case tar.TypeDir:
os.MkdirAll(target, 0o755)
case tar.TypeReg:
os.MkdirAll(filepath.Dir(target), 0o755)
mode := os.FileMode(hdr.Mode)
if mode == 0 {
mode = 0o644
}
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return err
}
io.Copy(out, tr)
out.Close()
}
}
}
+149
View File
@@ -0,0 +1,149 @@
package embedded
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/fujin/anthropic-proxy/internal/config"
"github.com/rs/zerolog/log"
)
type Perses struct {
cfg config.EmbeddedConfig
proxyPort int
cmd *exec.Cmd
tmpDir string
}
func NewPerses(cfg config.EmbeddedConfig, proxyPort int) *Perses {
return &Perses{cfg: cfg, proxyPort: proxyPort}
}
func (p *Perses) Start() error {
bin, err := ensureBinary("perses", p.cfg.PersesBinary, p.cfg.BinDir)
if err != nil {
return fmt.Errorf("perses: %w", err)
}
p.tmpDir, err = os.MkdirTemp("", "perses-*")
if err != nil {
return fmt.Errorf("create temp dir: %w", err)
}
if err := p.writeServerConfig(); err != nil {
return fmt.Errorf("write server config: %w", err)
}
if err := p.writeDatasourceProvision(); err != nil {
return fmt.Errorf("write datasource provision: %w", err)
}
if err := p.writeDashboardProvision(); err != nil {
return fmt.Errorf("write dashboard provision: %w", err)
}
p.cmd = exec.Command(bin,
"--config", filepath.Join(p.tmpDir, "config.yaml"),
"-web.listen-address", fmt.Sprintf(":%d", p.cfg.Port),
)
p.cmd.Dir = filepath.Dir(bin)
p.cmd.Stdout = &logWriter{level: "info", component: "perses"}
p.cmd.Stderr = &logWriter{level: "error", component: "perses"}
if err := p.cmd.Start(); err != nil {
return fmt.Errorf("start perses: %w", err)
}
log.Info().
Str("binary", bin).
Int("port", p.cfg.Port).
Str("config", p.tmpDir).
Msg("perses started")
return nil
}
func (p *Perses) Stop() {
if p.cmd != nil && p.cmd.Process != nil {
_ = p.cmd.Process.Kill()
_ = p.cmd.Wait()
}
if p.tmpDir != "" {
_ = os.RemoveAll(p.tmpDir)
}
}
func (p *Perses) Running() bool {
return p.cmd != nil && p.cmd.Process != nil && p.cmd.ProcessState == nil
}
func (p *Perses) writeServerConfig() error {
provisionDir := filepath.Join(p.tmpDir, "provisions")
if err := os.MkdirAll(filepath.Join(provisionDir, "datasources"), 0o755); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(provisionDir, "dashboards"), 0o755); err != nil {
return err
}
cfg := fmt.Sprintf(`provisioning:
interval: 1m
folders:
- %s
database:
file:
folder: %s/data
extension: json
security:
readonly: false
enable_auth: false
`, provisionDir, p.tmpDir)
return os.WriteFile(filepath.Join(p.tmpDir, "config.yaml"), []byte(cfg), 0o644)
}
func (p *Perses) writeDatasourceProvision() error {
ds := fmt.Sprintf(`kind: Datasource
metadata:
name: victoria-metrics
project: anthropic-proxy
spec:
default: true
plugin:
kind: PrometheusDatasource
spec:
directUrl: http://localhost:%d
`, p.cfg.VMPort)
return os.WriteFile(
filepath.Join(p.tmpDir, "provisions", "datasources", "vm.yaml"),
[]byte(ds), 0o644,
)
}
func (p *Perses) writeDashboardProvision() error {
dashData, err := DashboardJSON()
if err != nil {
return err
}
return os.WriteFile(
filepath.Join(p.tmpDir, "provisions", "dashboards", "proxy.json"),
dashData, 0o644,
)
}
type logWriter struct {
level string
component string
}
func (w *logWriter) Write(p []byte) (n int, err error) {
msg := string(p)
switch w.level {
case "error":
log.Error().Str("component", w.component).Msg(msg)
default:
log.Debug().Str("component", w.component).Msg(msg)
}
return len(p), nil
}
+88
View File
@@ -0,0 +1,88 @@
package embedded
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/fujin/anthropic-proxy/internal/config"
"github.com/rs/zerolog/log"
)
type VM struct {
cfg config.EmbeddedConfig
proxyPort int
cmd *exec.Cmd
tmpDir string
}
func NewVM(cfg config.EmbeddedConfig, proxyPort int) *VM {
return &VM{cfg: cfg, proxyPort: proxyPort}
}
func (v *VM) Start() error {
bin, err := ensureBinary("victoria-metrics", v.cfg.VMBinary, v.cfg.BinDir)
if err != nil {
return fmt.Errorf("victoria-metrics: %w", err)
}
v.tmpDir, err = os.MkdirTemp("", "vm-*")
if err != nil {
return fmt.Errorf("create temp dir: %w", err)
}
scrapeConfig := fmt.Sprintf(`global:
scrape_interval: 15s
scrape_configs:
- job_name: anthropic-proxy
static_configs:
- targets:
- localhost:%d
`, v.proxyPort)
scrapePath := filepath.Join(v.tmpDir, "scrape.yaml")
if err := os.WriteFile(scrapePath, []byte(scrapeConfig), 0o644); err != nil {
return fmt.Errorf("write scrape config: %w", err)
}
dataPath := filepath.Join(v.tmpDir, "data")
if err := os.MkdirAll(dataPath, 0o755); err != nil {
return fmt.Errorf("create data dir: %w", err)
}
v.cmd = exec.Command(bin,
"-storageDataPath", dataPath,
"-retentionPeriod", "7d",
"-httpListenAddr", fmt.Sprintf(":%d", v.cfg.VMPort),
"-promscrape.config", scrapePath,
)
v.cmd.Stdout = &logWriter{level: "info", component: "victoria-metrics"}
v.cmd.Stderr = &logWriter{level: "error", component: "victoria-metrics"}
if err := v.cmd.Start(); err != nil {
return fmt.Errorf("start victoria-metrics: %w", err)
}
log.Info().
Str("binary", bin).
Int("port", v.cfg.VMPort).
Int("scrape_target_port", v.proxyPort).
Msg("victoria-metrics started")
return nil
}
func (v *VM) Stop() {
if v.cmd != nil && v.cmd.Process != nil {
_ = v.cmd.Process.Kill()
_ = v.cmd.Wait()
}
if v.tmpDir != "" {
_ = os.RemoveAll(v.tmpDir)
}
}
func (v *VM) Running() bool {
return v.cmd != nil && v.cmd.Process != nil && v.cmd.ProcessState == nil
}