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}