main 0857610

backup_metrics.go RawHistoryFolder

1package telemetry
2
3import (
4	"context"
5	"sync"
6	"time"
7
8	"go.opentelemetry.io/otel/attribute"
9	"go.opentelemetry.io/otel/metric"
10
11	"github.com/bnema/gordon/internal/boundaries/out"
12	"github.com/bnema/gordon/internal/domain"
13)
14
15var _ out.BackupMetrics = (*Metrics)(nil)
16
17func targetAttrs(ref domain.BackupTargetRef) []attribute.KeyValue {
18	return []attribute.KeyValue{
19		attribute.String("kind", string(ref.Kind)),
20		attribute.String("app", ref.App),
21		attribute.String("service", ref.Service),
22		attribute.String("target", ref.Target),
23	}
24}
25
26type backupLastSuccess struct {
27	at      time.Time
28	size    int64
29	updated time.Time // when this entry was written, for retain
30}
31
32// backupState holds the last success per target, exported by observable
33// gauges at each collection.
34type backupState struct {
35	now     func() time.Time
36	mu      sync.Mutex
37	last    map[domain.BackupTargetRef]backupLastSuccess
38	nextRun map[domain.BackupKind]func() time.Time
39}
40
41func newBackupState() *backupState {
42	return &backupState{
43		now:     time.Now,
44		last:    map[domain.BackupTargetRef]backupLastSuccess{},
45		nextRun: map[domain.BackupKind]func() time.Time{},
46	}
47}
48
49// remember stores a success unless a newer one is already known.
50func (s *backupState) remember(run domain.BackupRunRecord) {
51	if !run.Succeeded || run.StartedAt.IsZero() {
52		return
53	}
54	key := run.BackupTargetRef
55	s.mu.Lock()
56	defer s.mu.Unlock()
57	if prev, ok := s.last[key]; ok && !run.StartedAt.After(prev.at) {
58		return
59	}
60	s.last[key] = backupLastSuccess{at: run.StartedAt, size: run.SizeBytes, updated: s.now()}
61}
62
63// retain drops the last success of every target of kind not in targets,
64// unless it was updated after asOf.
65func (s *backupState) retain(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
66	keep := make(map[domain.BackupTargetRef]struct{}, len(targets))
67	for _, ref := range targets {
68		keep[ref] = struct{}{}
69	}
70	s.mu.Lock()
71	defer s.mu.Unlock()
72	for key, last := range s.last {
73		if _, ok := keep[key]; key.Kind != kind || ok || last.updated.After(asOf) {
74			continue
75		}
76		delete(s.last, key)
77	}
78}
79
80// registerBackupInstruments creates the backup counters, histogram, and
81// observable gauges.
82func (m *Metrics) registerBackupInstruments() error {
83	var err error
84	if m.BackupRuns, err = m.meter.Int64Counter("gordon.backup.runs",
85		metric.WithDescription("Finished backup runs by result")); err != nil {
86		return err
87	}
88	if m.BackupDuration, err = m.meter.Float64Histogram("gordon.backup.duration_seconds",
89		metric.WithDescription("Backup run duration in seconds"),
90		metric.WithUnit("s"),
91		metric.WithExplicitBucketBoundaries(1, 5, 15, 30, 60, 120, 300, 600, 1800, 3600)); err != nil {
92		return err
93	}
94	lastSuccess, err := m.meter.Float64ObservableGauge("gordon.backup.last_success_timestamp_seconds",
95		metric.WithDescription("Start time (Unix) of the last successful backup per target"),
96		metric.WithUnit("s"))
97	if err != nil {
98		return err
99	}
100	lastSize, err := m.meter.Float64ObservableGauge("gordon.backup.last_size_bytes",
101		metric.WithDescription("Size of the last successful backup per target"),
102		metric.WithUnit("By"))
103	if err != nil {
104		return err
105	}
106	nextRun, err := m.meter.Float64ObservableGauge("gordon.backup.next_run_timestamp_seconds",
107		metric.WithDescription("Unix time of the next scheduled backup run"),
108		metric.WithUnit("s"))
109	if err != nil {
110		return err
111	}
112	_, err = m.meter.RegisterCallback(func(_ context.Context, o metric.Observer) error {
113		m.backups.mu.Lock()
114		defer m.backups.mu.Unlock()
115		for key, last := range m.backups.last {
116			attrs := metric.WithAttributes(targetAttrs(key)...)
117			o.ObserveFloat64(lastSuccess, float64(last.at.Unix()), attrs)
118			o.ObserveFloat64(lastSize, float64(last.size), attrs)
119		}
120		for kind, next := range m.backups.nextRun {
121			if at := next(); !at.IsZero() {
122				o.ObserveFloat64(nextRun, float64(at.Unix()), metric.WithAttributes(attribute.String("kind", string(kind))))
123			}
124		}
125		return nil
126	}, lastSuccess, lastSize, nextRun)
127	return err
128}
129
130// RecordBackupRun implements out.BackupMetrics.
131func (m *Metrics) RecordBackupRun(ctx context.Context, run domain.BackupRunRecord) {
132	result := "failure"
133	if run.Succeeded {
134		result = "success"
135	}
136	attrs := targetAttrs(run.BackupTargetRef)
137	m.BackupRuns.Add(ctx, 1, metric.WithAttributes(append(attrs, attribute.String("result", result))...))
138	m.BackupDuration.Record(ctx, run.Duration.Seconds(), metric.WithAttributes(attrs...))
139	m.backups.remember(run)
140}
141
142// ObserveStoredBackup implements out.BackupMetrics.
143func (m *Metrics) ObserveStoredBackup(run domain.BackupRunRecord) {
144	m.backups.remember(run)
145}
146
147// RetainBackupTargets implements out.BackupMetrics.
148func (m *Metrics) RetainBackupTargets(kind domain.BackupKind, targets []domain.BackupTargetRef, asOf time.Time) {
149	m.backups.retain(kind, targets, asOf)
150}
151
152// ObserveBackupNextRun exports the next planned run of one backup kind.
153// next is called at each collection; a zero time is not exported.
154func (m *Metrics) ObserveBackupNextRun(kind domain.BackupKind, next func() time.Time) {
155	m.backups.mu.Lock()
156	defer m.backups.mu.Unlock()
157	m.backups.nextRun[kind] = next
158}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close