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
38// NewMetrics creates and registers all Gordon metric instruments.
39// Returns a noop-safe Metrics struct — all fields are always initialized
40// (OTel returns noop instruments when no MeterProvider is set).
41func NewMetrics() (*Metrics, error) {
42 meter := otel.Meter("gordon")
43 m := &Metrics{meter: meter}
44 var err error
45
46 if m.DeployTotal, err = meter.Int64Counter("gordon.deploy.total",
47 metric.WithDescription("Total number of deployments")); err != nil {
48 return nil, err
49 }
50 if m.DeployDuration, err = meter.Float64Histogram("gordon.deploy.duration_seconds",
51 metric.WithDescription("Deploy duration in seconds"),
52 metric.WithUnit("s"),
53 metric.WithExplicitBucketBoundaries(1, 5, 10, 30, 60, 120, 300)); err != nil {
54 return nil, err
55 }
56 if m.DeployErrors, err = meter.Int64Counter("gordon.deploy.errors",
57 metric.WithDescription("Total deploy errors")); err != nil {
58 return nil, err
59 }
60 if m.ContainerRestarts, err = meter.Int64Counter("gordon.container.restarts",
61 metric.WithDescription("Total container restarts")); err != nil {
62 return nil, err
63 }
64 if m.ContainerCrashLoops, err = meter.Int64Counter("gordon.container.crash_loops",
65 metric.WithDescription("Total crash loop detections")); err != nil {
66 return nil, err
67 }
68 if m.ImagePushTotal, err = meter.Int64Counter("gordon.registry.push.total",
69 metric.WithDescription("Total image pushes")); err != nil {
70 return nil, err
71 }
72 if m.ImagePushSize, err = meter.Int64Counter("gordon.registry.push.bytes",
73 metric.WithDescription("Total bytes pushed to registry"),
74 metric.WithUnit("By")); err != nil {
75 return nil, err
76 }
77 if m.EventsProcessed, err = meter.Int64Counter("gordon.events.processed",
78 metric.WithDescription("Total events processed")); err != nil {
79 return nil, err
80 }
81 if m.EventsDropped, err = meter.Int64Counter("gordon.events.dropped",
82 metric.WithDescription("Total events dropped")); err != nil {
83 return nil, err
84 }
85
86 return m, nil
87}
88
89// ObserveManagedContainers registers the gordon.container.managed gauge.
90// count is called at each collection, so the exported value always reflects
91// current state instead of drifting like an incremental counter. A count
92// error skips that observation rather than exporting a wrong value.
93func (m *Metrics) ObserveManagedContainers(count func(context.Context) (int64, error)) error {
94 _, err := m.meter.Int64ObservableGauge("gordon.container.managed",
95 metric.WithDescription("Currently managed containers"),
96 metric.WithInt64Callback(func(ctx context.Context, o metric.Int64Observer) error {
97 n, err := count(ctx)
98 if err != nil {
99 return err
100 }
101 o.Observe(n)
102 return nil
103 }))
104 return err
105}
106
107// RecordImagePush implements out.Metrics.
108func (m *Metrics) RecordImagePush(ctx context.Context, name, reference string, sizeBytes int64) {
109 attrs := metric.WithAttributes(
110 attribute.String("name", name),
111 attribute.String("reference", reference),
112 )
113 m.ImagePushTotal.Add(ctx, 1, attrs)
114 m.ImagePushSize.Add(ctx, sizeBytes, attrs)
115}
116
117// RecordEventProcessed implements out.Metrics.
118func (m *Metrics) RecordEventProcessed(ctx context.Context, eventType domain.EventType) {
119 m.EventsProcessed.Add(ctx, 1, metric.WithAttributes(attribute.String("event_type", string(eventType))))
120}
121
122// RecordEventDropped implements out.Metrics.
123func (m *Metrics) RecordEventDropped(ctx context.Context, eventType domain.EventType) {
124 m.EventsDropped.Add(ctx, 1, metric.WithAttributes(attribute.String("event_type", string(eventType))))
125}