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:
@@ -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")
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user