1package telemetry
2
3import (
4 "context"
5
6 "go.opentelemetry.io/otel"
7 "go.opentelemetry.io/otel/attribute"
8 "go.opentelemetry.io/otel/metric"
9
10 "github.com/bnema/gordon/internal/boundaries/out"
11 "github.com/bnema/gordon/internal/domain"
12)
13
14var _ out.Metrics = (*Metrics)(nil)
15
16// Metrics holds Gordon-specific OTel metrics instruments.
17type Metrics struct {
18 meter metric.Meter
19
20 // Deployments
21 DeployTotal metric.Int64Counter
22 DeployDuration metric.Float64Histogram
23 DeployErrors metric.Int64Counter
24
25 // Container lifecycle
26 ContainerRestarts metric.Int64Counter
27 ContainerCrashLoops metric.Int64Counter
28
29 // Registry
30 ImagePushTotal metric.Int64Counter
31 ImagePushSize metric.Int64Counter // bytes
32
33 // Events
34 EventsProcessed metric.Int64Counter
35 EventsDropped metric.Int64Counter
36
37 // Backups
38 BackupRuns metric.Int64Counter
39 BackupDuration metric.Float64Histogram
40 backups *backupState
41}
42
43// NewMetrics creates and registers all Gordon metric instruments.
44// Returns a noop-safe Metrics struct — all fields are always initialized
45// (OTel returns noop instruments when no MeterProvider is set).
46func NewMetrics() (*Metrics, error) {
47 meter := otel.Meter("gordon")
48 m := &Metrics{meter: meter, backups: newBackupState()}
49 var err error
50
51 if m.DeployTotal, err = meter.Int64Counter("gordon.deploy.total",
52 metric.WithDescription("Total number of deployments")); err != nil {
53 return nil, err
54 }
55 if m.DeployDuration, err = meter.Float64Histogram("gordon.deploy.duration_seconds",
56 metric.WithDescription("Deploy duration in seconds"),
57 metric.WithUnit("s"),
58 metric.WithExplicitBucketBoundaries(1, 5, 10, 30, 60, 120, 300)); err != nil {
59 return nil, err
60 }
61 if m.DeployErrors, err = meter.Int64Counter("gordon.deploy.errors",
62 metric.WithDescription("Total deploy errors")); err != nil {
63 return nil, err
64 }
65 if m.ContainerRestarts, err = meter.Int64Counter("gordon.container.restarts",
66 metric.WithDescription("Total container restarts")); err != nil {
67 return nil, err
68 }
69 if m.ContainerCrashLoops, err = meter.Int64Counter("gordon.container.crash_loops",
70 metric.WithDescription("Total crash loop detections")); err != nil {
71 return nil, err
72 }
73 if m.ImagePushTotal, err = meter.Int64Counter("gordon.registry.push.total",
74 metric.WithDescription("Total image pushes")); err != nil {
75 return nil, err
76 }
77 if m.ImagePushSize, err = meter.Int64Counter("gordon.registry.push.bytes",
78 metric.WithDescription("Total bytes pushed to registry"),
79 metric.WithUnit("By")); err != nil {
80 return nil, err
81 }
82 if m.EventsProcessed, err = meter.Int64Counter("gordon.events.processed",
83 metric.WithDescription("Total events processed")); err != nil {
84 return nil, err
85 }
86 if m.EventsDropped, err = meter.Int64Counter("gordon.events.dropped",
87 metric.WithDescription("Total events dropped")); err != nil {
88 return nil, err
89 }
90 if err := m.registerBackupInstruments(); err != nil {
91 return nil, err
92 }
93
94 return m, nil
95}
96
97// ObserveManagedContainers registers the gordon.container.managed gauge.
98// count is called at each collection, so the exported value always reflects
99// current state instead of drifting like an incremental counter. A count
100// error skips that observation rather than exporting a wrong value.
101func (m *Metrics) ObserveManagedContainers(count func(context.Context) (int64, error)) error {
102 _, err := m.meter.Int64ObservableGauge("gordon.container.managed",
103 metric.WithDescription("Currently managed containers"),
104 metric.WithInt64Callback(func(ctx context.Context, o metric.Int64Observer) error {
105 n, err := count(ctx)
106 if err != nil {
107 return err
108 }
109 o.Observe(n)
110 return nil
111 }))
112 return err
113}
114
115// RecordImagePush implements out.Metrics.
116func (m *Metrics) RecordImagePush(ctx context.Context, name, reference string, sizeBytes int64) {
117 attrs := metric.WithAttributes(
118 attribute.String("name", name),
119 attribute.String("reference", reference),
120 )
121 m.ImagePushTotal.Add(ctx, 1, attrs)
122 m.ImagePushSize.Add(ctx, sizeBytes, attrs)
123}
124
125// RecordEventProcessed implements out.Metrics.
126func (m *Metrics) RecordEventProcessed(ctx context.Context, eventType domain.EventType) {
127 m.EventsProcessed.Add(ctx, 1, metric.WithAttributes(attribute.String("event_type", string(eventType))))
128}
129
130// RecordEventDropped implements out.Metrics.
131func (m *Metrics) RecordEventDropped(ctx context.Context, eventType domain.EventType) {
132 m.EventsDropped.Add(ctx, 1, metric.WithAttributes(attribute.String("event_type", string(eventType))))
133}