dirBrowse rendered a single directory level but paid for a full recursive listing of the repo — on CephFS-backed LocalFS that is a walk of the entire storage root plus a sidecar read per object, which took Ceph down on large repos. Storage grows a ListDir primitive (os.ReadDir on localfs, Delimiter on S3) and the UI and the debian dists fallback use it; the recursive List stays for the docker page and now starts its walk at the requested prefix. The per-vrepo storage gauges on /metrics did the same full listing on every scrape; they are removed — the backend's own accounting (du, bucket metrics) covers capacity. Co-Authored-By: Claude Code <noreply@anthropic.com>
139 lines
4.8 KiB
Go
139 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"tiny-proxy/internal/config"
|
|
"tiny-proxy/internal/distro"
|
|
"tiny-proxy/internal/docker"
|
|
"tiny-proxy/internal/github"
|
|
"tiny-proxy/internal/maven"
|
|
"tiny-proxy/internal/metrics"
|
|
"tiny-proxy/internal/proxycache"
|
|
"tiny-proxy/internal/storage"
|
|
"tiny-proxy/internal/tracing"
|
|
"tiny-proxy/internal/ui"
|
|
"tiny-proxy/internal/upstream"
|
|
)
|
|
|
|
func main() {
|
|
configPath := flag.String("config", "config.yaml", "path to config file")
|
|
flag.Parse()
|
|
|
|
cfg, err := config.Load(*configPath)
|
|
if err != nil {
|
|
slog.Error("load config", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
// From here on every log record carries the trace id of the request that
|
|
// caused it, so one grep follows a client request through the upstreams
|
|
// it fanned out to.
|
|
slog.SetDefault(tracing.NewLogger(os.Stderr, cfg.Log.Level, cfg.Log.Format))
|
|
|
|
ctx := context.Background()
|
|
reg := metrics.NewRegistry()
|
|
var store storage.Store
|
|
switch cfg.Storage.Type {
|
|
case "none":
|
|
// No cache: every request is proxied straight to the upstream.
|
|
store = storage.NullStore{}
|
|
case "s3":
|
|
store, err = storage.NewS3(ctx, cfg.Storage.S3)
|
|
default:
|
|
var local *storage.LocalFS
|
|
local, err = storage.NewLocalFS(cfg.Storage.LocalFS.Root)
|
|
if local != nil {
|
|
local.OnCorrupt = func(string) { reg.IncCorruptEntry() }
|
|
store = local
|
|
}
|
|
}
|
|
if err != nil {
|
|
slog.Error("init storage", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// One proxy/auth-aware HTTP client per vrepo upstream (maven, debian and
|
|
// pacman vrepos may have several, tried in order).
|
|
clients := map[string][]*http.Client{}
|
|
for _, v := range cfg.VRepos {
|
|
// Registering up front keeps every configured series present from
|
|
// the first scrape — a mirror that has served nothing yet reads as
|
|
// zero rather than as a gap — and lets each client hold its own
|
|
// counters directly instead of looking them up per request.
|
|
reg.RegisterVRepo(v.Name, string(v.Type))
|
|
for _, u := range v.AllUpstreams() {
|
|
c, err := upstream.NewClient(u, upstream.Options{
|
|
VRepo: v.Name,
|
|
Log: cfg.Log.LogUpstream(),
|
|
Metrics: reg,
|
|
Stats: reg.RegisterUpstream(v.Name, u.URL),
|
|
})
|
|
if err != nil {
|
|
slog.Error("init upstream client", "vrepo", v.Name, "url", u.URL, "err", err)
|
|
os.Exit(1)
|
|
}
|
|
clients[v.Name] = append(clients[v.Name], c)
|
|
}
|
|
}
|
|
// Docker, github and gitea vrepos are single-upstream.
|
|
first := map[string]*http.Client{}
|
|
for name, cs := range clients {
|
|
first[name] = cs[0]
|
|
}
|
|
|
|
cache := proxycache.New(store)
|
|
cache.Metrics = reg
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/v2/", metrics.CountRequests(reg, "docker", docker.NewHandler(cfg.VRepos, first, cache)))
|
|
mux.Handle("/maven/", metrics.CountRequests(reg, "maven", maven.NewHandler(cfg.VRepos, clients, cache)))
|
|
mux.Handle("/github/", metrics.CountRequests(reg, "github", github.NewHandler(cfg.VRepos, first, cache)))
|
|
mux.Handle("/gitea/", metrics.CountRequests(reg, "gitea", github.NewHandler(cfg.VRepos, first, cache)))
|
|
mux.Handle("/debian/", metrics.CountRequests(reg, "debian", distro.NewHandler(distro.Debian, cfg.VRepos, clients, cache)))
|
|
mux.Handle("/pacman/", metrics.CountRequests(reg, "pacman", distro.NewHandler(distro.Pacman, cfg.VRepos, clients, cache)))
|
|
mux.Handle("/ui/", ui.NewHandler(cfg.VRepos, store, clients).Routes())
|
|
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/ui/", http.StatusFound)
|
|
})
|
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("ok\n"))
|
|
})
|
|
mux.Handle("GET /metrics", metrics.NewHandler(reg, cfg.Metrics.Token))
|
|
|
|
srv := &http.Server{
|
|
Addr: cfg.Listen,
|
|
// Wrapping the whole mux, so /ui/, /metrics and /healthz get trace
|
|
// ids and access logs too.
|
|
Handler: tracing.Middleware(cfg.Log.LogRequests(), reg, mux),
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
for _, v := range cfg.VRepos {
|
|
urls := make([]string, 0, len(v.AllUpstreams()))
|
|
for _, u := range v.AllUpstreams() {
|
|
urls = append(urls, u.URL)
|
|
}
|
|
slog.Info("vrepo configured", "name", v.Name, "type", v.Type, "upstreams", urls, "auth", v.Auth.Mode)
|
|
}
|
|
// Serving on our own listener rather than ListenAndServe so client
|
|
// connections can be counted at the socket: the client byte totals then
|
|
// include request lines, headers and chunked framing instead of payload
|
|
// alone. Bound before the ready line, so a taken port reports as a
|
|
// failure to start rather than after announcing success.
|
|
ln, err := net.Listen("tcp", cfg.Listen)
|
|
if err != nil {
|
|
slog.Error("listen", "addr", cfg.Listen, "err", err)
|
|
os.Exit(1)
|
|
}
|
|
slog.Info("listening", "addr", cfg.Listen, "storage", cfg.Storage.Type,
|
|
"log_level", cfg.Log.Level, "log_format", cfg.Log.Format)
|
|
if err := srv.Serve(reg.CountConnections(ln)); err != nil {
|
|
slog.Error("server stopped", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|