@@ -2,6 +2,9 @@ package prebuilds
2
2
3
3
import (
4
4
"context"
5
+ "fmt"
6
+ "sync"
7
+ "sync/atomic"
5
8
"time"
6
9
7
10
"cdr.dev/slog"
35
38
labels ,
36
39
nil ,
37
40
)
41
+ resourceReplacementsDesc = prometheus .NewDesc (
42
+ "coderd_prebuilt_workspaces_resource_replacements_total" ,
43
+ "Total number of prebuilt workspaces whose resource(s) got replaced upon being claimed. " +
44
+ "In Terraform, drift on immutable attributes results in resource replacement. " +
45
+ "This represents a worst-case scenario for prebuilt workspaces because the pre-provisioned resource " +
46
+ "would have been recreated when claiming, thus obviating the point of pre-provisioning. " +
47
+ "See https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces.md#preventing-resource-replacement" ,
48
+ labels ,
49
+ nil ,
50
+ )
38
51
desiredPrebuildsDesc = prometheus .NewDesc (
39
52
"coderd_prebuilt_workspaces_desired" ,
40
53
"Target number of prebuilt workspaces that should be available for each template preset." ,
@@ -61,22 +74,27 @@ type MetricsCollector struct {
61
74
database database.Store
62
75
logger slog.Logger
63
76
snapshotter prebuilds.StateSnapshotter
77
+
78
+ replacementsCounter map [replacementKey ]* atomic.Int64
79
+ replacementsCounterMu sync.Mutex
64
80
}
65
81
66
82
var _ prometheus.Collector = new (MetricsCollector )
67
83
68
84
func NewMetricsCollector (db database.Store ,logger slog.Logger ,snapshotter prebuilds.StateSnapshotter )* MetricsCollector {
69
85
return & MetricsCollector {
70
- database :db ,
71
- logger :logger .Named ("prebuilds_metrics_collector" ),
72
- snapshotter :snapshotter ,
86
+ database :db ,
87
+ logger :logger .Named ("prebuilds_metrics_collector" ),
88
+ snapshotter :snapshotter ,
89
+ replacementsCounter :make (map [replacementKey ]* atomic.Int64 ),
73
90
}
74
91
}
75
92
76
93
func (* MetricsCollector )Describe (descCh chan <- * prometheus.Desc ) {
77
94
descCh <- createdPrebuildsDesc
78
95
descCh <- failedPrebuildsDesc
79
96
descCh <- claimedPrebuildsDesc
97
+ descCh <- resourceReplacementsDesc
80
98
descCh <- desiredPrebuildsDesc
81
99
descCh <- runningPrebuildsDesc
82
100
descCh <- eligiblePrebuildsDesc
@@ -98,6 +116,12 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
98
116
metricsCh <- prometheus .MustNewConstMetric (claimedPrebuildsDesc ,prometheus .CounterValue ,float64 (metric .ClaimedCount ),metric .TemplateName ,metric .PresetName ,metric .OrganizationName )
99
117
}
100
118
119
+ mc .replacementsCounterMu .Lock ()
120
+ for key ,val := range mc .replacementsCounter {
121
+ metricsCh <- prometheus .MustNewConstMetric (resourceReplacementsDesc ,prometheus .CounterValue ,float64 (val .Load ()),key .templateName ,key .presetName ,key .orgName )
122
+ }
123
+ mc .replacementsCounterMu .Unlock ()
124
+
101
125
snapshot ,err := mc .snapshotter .SnapshotState (ctx ,mc .database )
102
126
if err != nil {
103
127
mc .logger .Error (ctx ,"failed to get latest prebuild state" ,slog .Error (err ))
@@ -121,3 +145,22 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
121
145
metricsCh <- prometheus .MustNewConstMetric (eligiblePrebuildsDesc ,prometheus .GaugeValue ,float64 (state .Eligible ),preset .TemplateName ,preset .Name ,preset .OrganizationName )
122
146
}
123
147
}
148
+
149
+ type replacementKey struct {
150
+ orgName ,templateName ,presetName string
151
+ }
152
+
153
+ func (k replacementKey )String ()string {
154
+ return fmt .Sprintf ("%s:%s:%s" ,k .orgName ,k .templateName ,k .presetName )
155
+ }
156
+
157
+ func (mc * MetricsCollector )trackResourceReplacement (orgName ,templateName ,presetName string ) {
158
+ mc .replacementsCounterMu .Lock ()
159
+ defer mc .replacementsCounterMu .Unlock ()
160
+
161
+ key := replacementKey {orgName :orgName ,templateName :templateName ,presetName :presetName }
162
+ if _ ,ok := mc .replacementsCounter [key ];! ok {
163
+ mc .replacementsCounter [key ]= & atomic.Int64 {}
164
+ }
165
+ mc .replacementsCounter [key ].Add (1 )
166
+ }