Skip to content

Commit c30fef7

Browse files
worstellampagent
andcommitted
feat(git): instrument readiness-gate prewarm with metrics
Adds per-repo duration histogram (cachew.git.prewarm_repo_duration_seconds, labelled by path=fetch|restore and status=success|error), total-pass duration histogram (cachew.git.prewarm_pass_duration_seconds), and pass-completion counter (cachew.git.prewarm_passes_total), both labelled by outcome (complete|empty_histogram|no_store). Lets us see prewarm tail latency per deploy and distinguish 'no top-N' configs from genuinely-warm pods on the dashboard. Amp-Thread-ID: https://ampcode.com/threads/T-019e4193-0c35-732e-96f0-b9c97aed2fae Co-authored-by: Amp <amp@ampcode.com>
1 parent 09acad1 commit c30fef7

2 files changed

Lines changed: 55 additions & 21 deletions

File tree

internal/strategy/git/git.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ func (s *Strategy) SetMetadataStore(store *metadatadb.Store) {
227227
if s.config.PrewarmMostClonedRepos > 0 {
228228
logger.WarnContext(s.ctx, "prewarm-most-cloned-repos is set but no metadata store was provided; "+
229229
"reporting ready without prewarm (misconfiguration)")
230+
s.metrics.recordPrewarmPass(s.ctx, "no_store", 0)
230231
s.prewarmComplete.Store(true)
231232
}
232233
return
@@ -261,6 +262,7 @@ func (s *Strategy) SetMetadataStore(store *metadatadb.Store) {
261262
// against cold upstreams can extend startup considerably.
262263
func (s *Strategy) prewarmMostCloned(ctx context.Context) {
263264
logger := logging.FromContext(ctx)
265+
passStart := time.Now()
264266
select {
265267
case <-ctx.Done():
266268
return
@@ -269,6 +271,7 @@ func (s *Strategy) prewarmMostCloned(ctx context.Context) {
269271
top := s.repoCounts.TopRepos(prewarmPopularityWindowDays, s.config.PrewarmMostClonedRepos)
270272
if len(top) == 0 {
271273
logger.WarnContext(ctx, "prewarm-most-cloned-repos is set but the clone histogram is empty; reporting ready")
274+
s.metrics.recordPrewarmPass(ctx, "empty_histogram", time.Since(passStart))
272275
return
273276
}
274277
logger.InfoContext(ctx, "Prewarming most-cloned repositories for readiness gate", "count", len(top))
@@ -277,34 +280,41 @@ func (s *Strategy) prewarmMostCloned(ctx context.Context) {
277280
return
278281
}
279282
start := time.Now()
280-
if err := s.prewarmRepo(ctx, rc.Repo); err != nil {
283+
path, err := s.prewarmRepo(ctx, rc.Repo)
284+
dur := time.Since(start)
285+
status := "success"
286+
if err != nil {
287+
status = "error"
281288
logger.ErrorContext(ctx, "Failed to prewarm repo for readiness gate",
282-
"upstream", rc.Repo, "clone_count", rc.Count, "duration", time.Since(start), "error", err)
283-
continue
289+
"upstream", rc.Repo, "clone_count", rc.Count, "path", path, "duration", dur, "error", err)
290+
} else {
291+
logger.InfoContext(ctx, "Prewarmed repo for readiness gate",
292+
"upstream", rc.Repo, "clone_count", rc.Count, "path", path, "duration", dur)
284293
}
285-
logger.InfoContext(ctx, "Prewarmed repo for readiness gate",
286-
"upstream", rc.Repo, "clone_count", rc.Count, "duration", time.Since(start))
294+
s.metrics.recordPrewarmRepo(ctx, path, status, dur)
287295
}
296+
s.metrics.recordPrewarmPass(ctx, "complete", time.Since(passStart))
288297
}
289298

290299
// prewarmRepo ensures the mirror for upstreamURL exists and has been freshened
291300
// once. Uses FetchTimeout (not CloneTimeout) on the fetch path so a slow
292301
// upstream on an already-cloned mirror cannot extend startup by an hour.
293-
func (s *Strategy) prewarmRepo(ctx context.Context, upstreamURL string) error {
302+
// Returns the path taken ("fetch" or "restore") for metrics labelling.
303+
func (s *Strategy) prewarmRepo(ctx context.Context, upstreamURL string) (string, error) {
294304
repo, err := s.cloneManager.GetOrCreate(ctx, upstreamURL)
295305
if err != nil {
296-
return errors.Wrapf(err, "resolve clone for %s", upstreamURL)
306+
return "restore", errors.Wrapf(err, "resolve clone for %s", upstreamURL)
297307
}
298308
if repo.State() != gitclone.StateReady {
299309
if err := s.ensureCloneReady(ctx, repo); err != nil {
300-
return errors.Wrapf(err, "clone %s", upstreamURL)
310+
return "restore", errors.Wrapf(err, "clone %s", upstreamURL)
301311
}
302-
return nil
312+
return "restore", nil
303313
}
304314
if err := repo.FetchLenient(ctx, s.cloneManager.Config().FetchTimeout); err != nil {
305-
return errors.Wrapf(err, "fetch %s", upstreamURL)
315+
return "fetch", errors.Wrapf(err, "fetch %s", upstreamURL)
306316
}
307-
return nil
317+
return "fetch", nil
308318
}
309319

310320
func (s *Strategy) warmExistingRepos(ctx context.Context) error {

internal/strategy/git/metrics.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,27 @@ import (
1212
)
1313

1414
type gitMetrics struct {
15-
operationDuration metric.Float64Histogram
16-
operationTotal metric.Int64Counter
17-
requestTotal metric.Int64Counter
18-
snapshotServeTotal metric.Int64Counter
19-
snapshotServeSize metric.Float64Histogram
15+
operationDuration metric.Float64Histogram
16+
operationTotal metric.Int64Counter
17+
requestTotal metric.Int64Counter
18+
snapshotServeTotal metric.Int64Counter
19+
snapshotServeSize metric.Float64Histogram
20+
prewarmRepoDuration metric.Float64Histogram
21+
prewarmPassDuration metric.Float64Histogram
22+
prewarmPassTotal metric.Int64Counter
2023
}
2124

2225
func newGitMetrics() *gitMetrics {
2326
meter := otel.Meter("cachew.git")
2427
return &gitMetrics{
25-
operationDuration: metrics.NewMetric[metric.Float64Histogram](meter, "cachew.git.operation_duration_seconds", "s", "Duration of git operations (clone, fetch, repack, snapshot)"),
26-
operationTotal: metrics.NewMetric[metric.Int64Counter](meter, "cachew.git.operations_total", "{operations}", "Total number of git operations"),
27-
requestTotal: metrics.NewMetric[metric.Int64Counter](meter, "cachew.git.requests_total", "{requests}", "Total number of git HTTP requests by type"),
28-
snapshotServeTotal: metrics.NewMetric[metric.Int64Counter](meter, "cachew.git.snapshot_serves_total", "{serves}", "Snapshot serve events by source (cache, spool, cold_cache) and repository"),
29-
snapshotServeSize: metrics.NewMetric[metric.Float64Histogram](meter, "cachew.git.snapshot_serve_bytes", "By", "Size of served snapshots in bytes"),
28+
operationDuration: metrics.NewMetric[metric.Float64Histogram](meter, "cachew.git.operation_duration_seconds", "s", "Duration of git operations (clone, fetch, repack, snapshot)"),
29+
operationTotal: metrics.NewMetric[metric.Int64Counter](meter, "cachew.git.operations_total", "{operations}", "Total number of git operations"),
30+
requestTotal: metrics.NewMetric[metric.Int64Counter](meter, "cachew.git.requests_total", "{requests}", "Total number of git HTTP requests by type"),
31+
snapshotServeTotal: metrics.NewMetric[metric.Int64Counter](meter, "cachew.git.snapshot_serves_total", "{serves}", "Snapshot serve events by source (cache, spool, cold_cache) and repository"),
32+
snapshotServeSize: metrics.NewMetric[metric.Float64Histogram](meter, "cachew.git.snapshot_serve_bytes", "By", "Size of served snapshots in bytes"),
33+
prewarmRepoDuration: metrics.NewMetric[metric.Float64Histogram](meter, "cachew.git.prewarm_repo_duration_seconds", "s", "Duration of per-repo prewarm during readiness gate, by entry state and status"),
34+
prewarmPassDuration: metrics.NewMetric[metric.Float64Histogram](meter, "cachew.git.prewarm_pass_duration_seconds", "s", "Total duration of the readiness-gate prewarm pass, by outcome"),
35+
prewarmPassTotal: metrics.NewMetric[metric.Int64Counter](meter, "cachew.git.prewarm_passes_total", "{passes}", "Total readiness-gate prewarm passes completed, by outcome"),
3036
}
3137
}
3238

@@ -56,3 +62,21 @@ func (m *gitMetrics) recordSnapshotServe(ctx context.Context, source, repo strin
5662
m.snapshotServeSize.Record(ctx, float64(sizeBytes), attrs)
5763
}
5864
}
65+
66+
// recordPrewarmRepo records the wall-clock cost of warming a single repo during
67+
// the readiness gate. path is "fetch" (mirror already on disk) or "restore"
68+
// (mirror snapshot pulled from S3 or full clone). status is "success" or "error".
69+
func (m *gitMetrics) recordPrewarmRepo(ctx context.Context, path, status string, duration time.Duration) {
70+
m.prewarmRepoDuration.Record(ctx, duration.Seconds(), metric.WithAttributes(
71+
attribute.String("path", path),
72+
attribute.String("status", status),
73+
))
74+
}
75+
76+
// recordPrewarmPass records the total prewarm-pass wall time and completion
77+
// outcome. outcome is one of: "complete", "empty_histogram", "no_store".
78+
func (m *gitMetrics) recordPrewarmPass(ctx context.Context, outcome string, duration time.Duration) {
79+
attrs := metric.WithAttributes(attribute.String("outcome", outcome))
80+
m.prewarmPassTotal.Add(ctx, 1, attrs)
81+
m.prewarmPassDuration.Record(ctx, duration.Seconds(), attrs)
82+
}

0 commit comments

Comments
 (0)