@@ -18,6 +18,7 @@ import (
18
18
"github.com/coder/coder/v2/coderd/database/dbauthz"
19
19
"github.com/coder/coder/v2/coderd/database/dbtime"
20
20
"github.com/coder/coder/v2/coderd/notifications"
21
+ "github.com/coder/coder/v2/coderd/util/slice"
21
22
"github.com/coder/coder/v2/codersdk"
22
23
)
23
24
@@ -102,6 +103,11 @@ const (
102
103
failedWorkspaceBuildsReportFrequencyLabel = "week"
103
104
)
104
105
106
+ type adminReport struct {
107
+ stats database.GetWorkspaceBuildStatsByTemplatesRow
108
+ failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow
109
+ }
110
+
105
111
func reportFailedWorkspaceBuilds (ctx context.Context , logger slog.Logger , db database.Store , enqueuer notifications.Enqueuer , clk quartz.Clock ) error {
106
112
now := clk .Now ()
107
113
since := now .Add (- failedWorkspaceBuildsReportFrequency )
@@ -136,6 +142,8 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
136
142
return xerrors .Errorf ("unable to fetch failed workspace builds: %w" , err )
137
143
}
138
144
145
+ reports := make (map [uuid.UUID ][]adminReport )
146
+
139
147
for _ , stats := range templateStatsRows {
140
148
select {
141
149
case <- ctx .Done ():
@@ -165,33 +173,40 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
165
173
logger .Error (ctx , "unable to fetch failed workspace builds" , slog .F ("template_id" , stats .TemplateID ), slog .Error (err ))
166
174
continue
167
175
}
168
- reportData := buildDataForReportFailedWorkspaceBuilds (stats , failedBuilds )
169
176
170
- // Send reports to template admins
171
- templateDisplayName := stats .TemplateDisplayName
172
- if templateDisplayName == "" {
173
- templateDisplayName = stats .TemplateName
177
+ for _ , templateAdmin := range templateAdmins {
178
+ adminReports := reports [templateAdmin .ID ]
179
+ adminReports = append (adminReports , adminReport {
180
+ failedBuilds : failedBuilds ,
181
+ stats : stats ,
182
+ })
183
+
184
+ reports [templateAdmin .ID ] = adminReports
174
185
}
186
+ }
175
187
176
- for _ , templateAdmin := range templateAdmins {
177
- select {
178
- case <- ctx .Done ():
179
- logger .Debug (ctx , "context is canceled, quitting" , slog .Error (ctx .Err ()))
180
- break
181
- default :
182
- }
188
+ for templateAdmin , reports := range reports {
189
+ select {
190
+ case <- ctx .Done ():
191
+ logger .Debug (ctx , "context is canceled, quitting" , slog .Error (ctx .Err ()))
192
+ break
193
+ default :
194
+ }
183
195
184
- if _ , err := enqueuer .EnqueueWithData (ctx , templateAdmin .ID , notifications .TemplateWorkspaceBuildsFailedReport ,
185
- map [string ]string {
186
- "template_name" : stats .TemplateName ,
187
- "template_display_name" : templateDisplayName ,
188
- },
189
- reportData ,
190
- "report_generator" ,
191
- stats .TemplateID , stats .TemplateOrganizationID ,
192
- ); err != nil {
193
- logger .Warn (ctx , "failed to send a report with failed workspace builds" , slog .Error (err ))
194
- }
196
+ reportData := buildDataForReportFailedWorkspaceBuilds (reports )
197
+
198
+ targets := []uuid.UUID {}
199
+ for _ , report := range reports {
200
+ targets = append (targets , report .stats .TemplateID , report .stats .TemplateOrganizationID )
201
+ }
202
+
203
+ if _ , err := enqueuer .EnqueueWithData (ctx , templateAdmin , notifications .TemplateWorkspaceBuildsFailedReport ,
204
+ map [string ]string {},
205
+ reportData ,
206
+ "report_generator" ,
207
+ slice .Unique (targets )... ,
208
+ ); err != nil {
209
+ logger .Warn (ctx , "failed to send a report with failed workspace builds" , slog .Error (err ))
195
210
}
196
211
}
197
212
@@ -213,54 +228,69 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
213
228
214
229
const workspaceBuildsLimitPerTemplateVersion = 10
215
230
216
- func buildDataForReportFailedWorkspaceBuilds (stats database.GetWorkspaceBuildStatsByTemplatesRow , failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow ) map [string ]any {
217
- // Build notification model for template versions and failed workspace builds.
218
- //
219
- // Failed builds are sorted by template version ascending, workspace build number descending.
220
- // Review builds, group them by template versions, and assign to builds to template versions.
221
- // The map requires `[]map[string]any{}` to be compatible with data passed to `NotificationEnqueuer`.
222
- templateVersions := []map [string ]any {}
223
- for _ , failedBuild := range failedBuilds {
224
- c := len (templateVersions )
225
-
226
- if c == 0 || templateVersions [c - 1 ]["template_version_name" ] != failedBuild .TemplateVersionName {
227
- templateVersions = append (templateVersions , map [string ]any {
228
- "template_version_name" : failedBuild .TemplateVersionName ,
229
- "failed_count" : 1 ,
230
- "failed_builds" : []map [string ]any {
231
- {
232
- "workspace_owner_username" : failedBuild .WorkspaceOwnerUsername ,
233
- "workspace_name" : failedBuild .WorkspaceName ,
234
- "build_number" : failedBuild .WorkspaceBuildNumber ,
231
+ func buildDataForReportFailedWorkspaceBuilds (reports []adminReport ) map [string ]any {
232
+ templates := []map [string ]any {}
233
+
234
+ for _ , report := range reports {
235
+ // Build notification model for template versions and failed workspace builds.
236
+ //
237
+ // Failed builds are sorted by template version ascending, workspace build number descending.
238
+ // Review builds, group them by template versions, and assign to builds to template versions.
239
+ // The map requires `[]map[string]any{}` to be compatible with data passed to `NotificationEnqueuer`.
240
+ templateVersions := []map [string ]any {}
241
+ for _ , failedBuild := range report .failedBuilds {
242
+ c := len (templateVersions )
243
+
244
+ if c == 0 || templateVersions [c - 1 ]["template_version_name" ] != failedBuild .TemplateVersionName {
245
+ templateVersions = append (templateVersions , map [string ]any {
246
+ "template_version_name" : failedBuild .TemplateVersionName ,
247
+ "failed_count" : 1 ,
248
+ "failed_builds" : []map [string ]any {
249
+ {
250
+ "workspace_owner_username" : failedBuild .WorkspaceOwnerUsername ,
251
+ "workspace_name" : failedBuild .WorkspaceName ,
252
+ "build_number" : failedBuild .WorkspaceBuildNumber ,
253
+ },
235
254
},
236
- },
237
- })
238
- continue
255
+ })
256
+ continue
257
+ }
258
+
259
+ tv := templateVersions [c - 1 ]
260
+ //nolint:errorlint,forcetypeassert // only this function prepares the notification model
261
+ tv ["failed_count" ] = tv ["failed_count" ].(int ) + 1
262
+
263
+ //nolint:errorlint,forcetypeassert // only this function prepares the notification model
264
+ builds := tv ["failed_builds" ].([]map [string ]any )
265
+ if len (builds ) < workspaceBuildsLimitPerTemplateVersion {
266
+ // return N last builds to prevent long email reports
267
+ builds = append (builds , map [string ]any {
268
+ "workspace_owner_username" : failedBuild .WorkspaceOwnerUsername ,
269
+ "workspace_name" : failedBuild .WorkspaceName ,
270
+ "build_number" : failedBuild .WorkspaceBuildNumber ,
271
+ })
272
+ tv ["failed_builds" ] = builds
273
+ }
274
+ templateVersions [c - 1 ] = tv
239
275
}
240
276
241
- tv := templateVersions [c - 1 ]
242
- //nolint:errorlint,forcetypeassert // only this function prepares the notification model
243
- tv ["failed_count" ] = tv ["failed_count" ].(int ) + 1
244
-
245
- //nolint:errorlint,forcetypeassert // only this function prepares the notification model
246
- builds := tv ["failed_builds" ].([]map [string ]any )
247
- if len (builds ) < workspaceBuildsLimitPerTemplateVersion {
248
- // return N last builds to prevent long email reports
249
- builds = append (builds , map [string ]any {
250
- "workspace_owner_username" : failedBuild .WorkspaceOwnerUsername ,
251
- "workspace_name" : failedBuild .WorkspaceName ,
252
- "build_number" : failedBuild .WorkspaceBuildNumber ,
253
- })
254
- tv ["failed_builds" ] = builds
277
+ templateDisplayName := report .stats .TemplateDisplayName
278
+ if templateDisplayName == "" {
279
+ templateDisplayName = report .stats .TemplateName
255
280
}
256
- templateVersions [c - 1 ] = tv
281
+
282
+ templates = append (templates , map [string ]any {
283
+ "failed_builds" : report .stats .FailedBuilds ,
284
+ "total_builds" : report .stats .TotalBuilds ,
285
+ "versions" : templateVersions ,
286
+ "name" : report .stats .TemplateName ,
287
+ "display_name" : templateDisplayName ,
288
+ })
257
289
}
258
290
259
291
return map [string ]any {
260
- "failed_builds" : stats .FailedBuilds ,
261
- "total_builds" : stats .TotalBuilds ,
262
- "report_frequency" : failedWorkspaceBuildsReportFrequencyLabel ,
263
- "template_versions" : templateVersions ,
292
+ "report_frequency" : failedWorkspaceBuildsReportFrequencyLabel ,
293
+ "templates" : templates ,
264
294
}
265
295
}
266
296
0 commit comments