objgitd currently emits only slog logs; an operator has no quantitative view
of what the server is doing. This adds Prometheus instrumentation across the
three things the user named — S3/Tigris ops, git repo operations, and
authorization decisions — plus a few extra operator-visibility metrics, and
serves them on a dedicated /metrics HTTP listener.
Metrics use github.com/prometheus/client_golang/prometheus/promauto against the
default registry, so Go-runtime and process collectors come for free and
promhttp.Handler() exposes everything.
- No
repolabel. Unbounded cardinality. Repo operations are labeled byprotocol+service+statusonly (the original "per repo" ask is dropped). /metricson by default at:9090(-metrics-bind, empty string disables).- s3fs measured at the S3-API-call level (GetObject, PutObject, …), not the billy-method level.
- Extras: push-hook runs, repos auto-created, in-flight requests gauge, Go/process collectors.
Central definitions so names stay consistent and packages don't each import
promauto. All vectors via promauto.NewCounterVec/NewHistogramVec/NewGauge
(default registerer). Prefix objgit_.
| Metric | Type | Labels |
|---|---|---|
objgit_s3_requests_total |
counter | operation, status (ok/error) |
objgit_s3_request_duration_seconds |
histogram | operation |
objgit_git_requests_total |
counter | protocol, service, status (ok/error/denied) |
objgit_git_request_duration_seconds |
histogram | protocol, service |
objgit_git_requests_in_flight |
gauge | protocol |
objgit_auth_requests_total |
counter | transport, operation (read/write), decision (allow/deny/unauthenticated) |
objgit_auth_request_duration_seconds |
histogram | transport |
objgit_hook_runs_total |
counter | status (ok/error/timeout) |
objgit_hook_run_duration_seconds |
histogram | (none) |
objgit_repos_created_total |
counter | (none) |
Exported helpers (keep call sites tiny):
ObserveS3(operation string, dur time.Duration, err error)— the s3fs observer.ObserveGitOp(protocol, service, status string, start time.Time).TrackInFlight(protocol string) func()— inc gauge, return a dec closure fordefer.ObserveAuth(transport string, op auth.Operation, d auth.Decision, start time.Time)(maps the enums to label strings here so transports stay clean).ObserveHook(status string, dur time.Duration)andReposCreated().
The S3 calls live in standalone constructors (newS3ReadFile, etc.) that hold a
*storage.Client but not the S3FS, so an instance Option can't reach them
without threading a field through every constructor. Instead use a process-level
observer (internal/s3fs/metrics.go) — honest, since the S3 client and the
Prometheus registry it feeds are both process-global:
func SetMetricsObserver(fn func(operation string, dur time.Duration, err error))
func observeS3(operation string, start time.Time, err error) // package-private helperSetMetricsObserver installs the observer once at startup; observeS3 times one
call and reports it (no-op when no observer is set). Wrap each client.* call
site with a start := time.Now() / observeS3("Op", start, err) pair.
Representative sites (from exploration):
file.go— HeadObject (:68), GetObject (:77), PutObject (:249), CreateMultipartUpload (:304), UploadPart (:342), CompleteMultipartUpload (:394).basic.go— HeadObject (:137), PutObject (:186), RenameObject (:201), ListObjectsV2 (:97,:158), DeleteObject (:226).dir.go— ListObjectsV2 (:32), PutObject (:83).
operation label = the S3 API name (string constant per call site). Keeps s3fs
free of any prometheus import — the observer is an opaque callback the project
wires in main.go via s3fs.SetMetricsObserver(metrics.ObserveS3).
Today each transport calls d.authz.Authorize(...) directly
(ssh.go:144, git_protocol.go:123, http.go:151). Add:
func (d *daemon) authorize(ctx context.Context, req auth.Request) auth.Decisionin git_protocol.go (next to operationFor, git_protocol.go:48). It times the
inner d.authz.Authorize, records ObserveAuth(...), and returns the decision.
Replace all three call sites to call d.authorize(...). This is the single auth
seam the metrics hook lives on.
Each handler knows (protocol, service) early. Pattern at each entry point:
done := metrics.TrackInFlight(protocol); defer done()
start := time.Now()
// ... handle; determine status ...
metrics.ObserveGitOp(protocol, service, status, start)- HTTP (
http.go): wrap inhandleRPC/resolve(http.go:89,:150),protocol="http". - git:// (
git_protocol.go): inhandleafterreq.Decode(git_protocol.go:109),protocol="git". - SSH (
ssh.go): inhandleSSHaftergitServiceFor(ssh.go:130),protocol="ssh".
service = transport.UploadPackService / ReceivePackService /
UploadArchiveService. status = denied when authorize rejects, error on a
non-nil handler error, else ok.
- Push hooks (
hooks.go,runHooks~:117): time each hook run, classifytimeout(context deadline) vserrorvsok, callmetrics.ObserveHook. - Repos auto-created (
git_protocol.goloadOrInit, the create branch ~:185, right by the existingslog.Info("created repository", …)):metrics.ReposCreated(). - In-flight gauge — already handled by
TrackInFlightin seam #3. - Go/process collectors — automatic via default registry; no code beyond serving.
- Add flag after
hookTimeout(main.go:37):metricsBind = flag.String("metrics-bind", ":9090", "TCP address for the Prometheus /metrics endpoint; empty disables it").flagenvauto-maps it toMETRICS_BIND. - Wire the s3fs observer before constructing the filesystem:
s3fs.SetMetricsObserver(metrics.ObserveS3)(main.go, just beforestorage.New). - Add
"metrics_bind", *metricsBindto the startupslog.Info(main.go:84). - Add a metrics listener in the errgroup, cloning the existing HTTP
Serve/Shutdown idiom (
main.go:104-123):if *metricsBind != "" { ln, err := net.Listen("tcp", *metricsBind) // os.Exit(1) on error, like the others mux := http.NewServeMux(); mux.Handle("/metrics", promhttp.Handler()) srv := &http.Server{Handler: mux} g.Go(func() error { /* srv.Serve, ignore http.ErrServerClosed */ }) g.Go(func() error { /* <-gCtx.Done(); 10s Shutdown */ }) }
github.com/prometheus/client_golang is already in go.sum (indirect). After
coding, run go mod tidy to promote it to a direct require and pull
promauto/promhttp. No other new deps.
- New:
internal/metrics/metrics.go internal/s3fs/filesystem.go(Option +observehelper),internal/s3fs/file.go,basic.go,dir.go(wrap call sites)cmd/objgitd/git_protocol.go(authorizehelper,handlewrap,loadOrInitcounter)cmd/objgitd/http.go,cmd/objgitd/ssh.go(auth call-site swap + repo-op wrap)cmd/objgitd/hooks.go(hook metrics)cmd/objgitd/main.go(flag, s3fs wiring, metrics server, startup log)go.mod/go.sum(tidy)CLAUDE.md(document the metrics surface +-metrics-bind, brief)
go build ./...andgo vet ./....go test ./...— existing protocol/SSH tests must still pass (metrics calls are additive and the observer is nil-safe when unset). The metric helpers themselves are not unit-tested.- Manual:
./objgitd -bucket $BUCKET -allow-pushthencurl -s localhost:9090/metrics | grep objgit_after a clone/push; confirm s3, git, auth, hook, and repos-created series plus Go/process collectors.