Skip to content

Commit 2f5a948

Browse files
committed
Added tests for workspace detail metrics
Moderate refactoring Signed-off-by: Danny Kopping <danny@coder.com>
1 parent c31b498 commit 2f5a948

File tree

4 files changed

+273
-20
lines changed

4 files changed

+273
-20
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,16 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac
403403
break
404404
}
405405
}
406+
407+
if pj, err := q.GetProvisionerJobByID(ctx, build.JobID); err == nil {
408+
wr.LatestBuildStatus = database.NullProvisionerJobStatus{ProvisionerJobStatus: pj.JobStatus, Valid: true}
409+
}
410+
411+
wr.LatestBuildTransition = build.Transition
412+
}
413+
414+
if u, err := q.GetUserByID(ctx, w.OwnerID); err == nil {
415+
wr.Username = u.Username
406416
}
407417

408418
rows = append(rows, wr)

coderd/prometheusmetrics/prometheusmetrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func Workspaces(ctx context.Context, logger slog.Logger, registerer prometheus.R
160160
}
161161
}
162162

163-
workspacesDetail.WithLabelValues(buildStatus, w.TemplateName, w.TemplateVersionName.String, w.Name, w.Username, string(w.LatestBuildTransition)).Set(1)
163+
workspaceDetails.WithLabelValues(buildStatus, w.TemplateName, w.TemplateVersionName.String, w.Name, w.Username, string(w.LatestBuildTransition)).Set(1)
164164
}
165165
}
166166

coderd/prometheusmetrics/prometheusmetrics_test.go

Lines changed: 257 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"database/sql"
66
"encoding/json"
77
"fmt"
8+
"math/rand"
89
"os"
910
"reflect"
1011
"sync/atomic"
1112
"testing"
1213
"time"
1314

15+
"github.com/coder/coder/v2/cryptorand"
1416
"github.com/google/uuid"
1517
"github.com/prometheus/client_golang/prometheus"
1618
"github.com/stretchr/testify/assert"
@@ -110,7 +112,7 @@ func TestActiveUsers(t *testing.T) {
110112
}
111113
}
112114

113-
func TestWorkspaces(t *testing.T) {
115+
func TestWorkspaceStatuses(t *testing.T) {
114116
t.Parallel()
115117

116118
insertRunning := func(db database.Store) database.ProvisionerJob {
@@ -229,33 +231,273 @@ func TestWorkspaces(t *testing.T) {
229231
t.Run(tc.Name, func(t *testing.T) {
230232
t.Parallel()
231233
registry := prometheus.NewRegistry()
232-
closeFunc, err := prometheusmetrics.Workspaces(context.Background(), slogtest.Make(t, nil), registry, tc.Database(), time.Millisecond)
234+
closeFunc, err := prometheusmetrics.Workspaces(context.Background(), slogtest.Make(t, nil), registry, tc.Database(), testutil.IntervalFast)
233235
require.NoError(t, err)
234236
t.Cleanup(closeFunc)
235237

236238
require.Eventually(t, func() bool {
237239
metrics, err := registry.Gather()
238240
assert.NoError(t, err)
239-
if len(metrics) < 1 {
240-
return false
241-
}
242241
sum := 0
243-
for _, metric := range metrics[0].Metric {
244-
count, ok := tc.Status[codersdk.ProvisionerJobStatus(metric.Label[0].GetValue())]
245-
if metric.Gauge.GetValue() == 0 {
242+
for _, m := range metrics {
243+
if m.GetName() != "coderd_api_workspace_latest_build_total" {
246244
continue
247245
}
248-
if !ok {
249-
t.Fail()
250-
}
251-
if metric.Gauge.GetValue() != float64(count) {
252-
return false
246+
247+
for _, metric := range m.Metric {
248+
count, ok := tc.Status[codersdk.ProvisionerJobStatus(metric.Label[0].GetValue())]
249+
if metric.Gauge.GetValue() == 0 {
250+
continue
251+
}
252+
if !ok {
253+
t.Fail()
254+
}
255+
if metric.Gauge.GetValue() != float64(count) {
256+
return false
257+
}
258+
sum += int(metric.Gauge.GetValue())
253259
}
254-
sum += int(metric.Gauge.GetValue())
255260
}
256261
t.Logf("sum %d == total %d", sum, tc.Total)
257262
return sum == tc.Total
258-
}, testutil.WaitShort, testutil.IntervalFast)
263+
}, testutil.WaitSuperShort, testutil.IntervalFast)
264+
})
265+
}
266+
}
267+
268+
func TestWorkspaceDetails(t *testing.T) {
269+
t.Parallel()
270+
271+
templateA := uuid.New()
272+
templateVersionA := uuid.New()
273+
templateB := uuid.New()
274+
templateVersionB := uuid.New()
275+
276+
insertTemplates := func(db database.Store) {
277+
require.NoError(t, db.InsertTemplate(context.Background(), database.InsertTemplateParams{
278+
ID: templateA,
279+
Name: "template-a",
280+
Provisioner: database.ProvisionerTypeTerraform,
281+
MaxPortSharingLevel: database.AppSharingLevelAuthenticated,
282+
}))
283+
284+
require.NoError(t, db.InsertTemplateVersion(context.Background(), database.InsertTemplateVersionParams{
285+
ID: templateVersionA,
286+
TemplateID: uuid.NullUUID{UUID: templateA},
287+
Name: "version-1a",
288+
}))
289+
290+
require.NoError(t, db.InsertTemplate(context.Background(), database.InsertTemplateParams{
291+
ID: templateB,
292+
Name: "template-b",
293+
Provisioner: database.ProvisionerTypeTerraform,
294+
MaxPortSharingLevel: database.AppSharingLevelAuthenticated,
295+
}))
296+
297+
require.NoError(t, db.InsertTemplateVersion(context.Background(), database.InsertTemplateVersionParams{
298+
ID: templateVersionB,
299+
TemplateID: uuid.NullUUID{UUID: templateB},
300+
Name: "version-1b",
301+
}))
302+
}
303+
304+
insertUser := func(db database.Store) database.User {
305+
username, err := cryptorand.String(8)
306+
require.NoError(t, err)
307+
308+
user, err := db.InsertUser(context.Background(), database.InsertUserParams{
309+
ID: uuid.New(),
310+
Username: username,
311+
LoginType: database.LoginTypeNone,
312+
})
313+
require.NoError(t, err)
314+
315+
return user
316+
}
317+
318+
insertRunning := func(db database.Store) database.ProvisionerJob {
319+
var template, templateVersion uuid.UUID
320+
if rand.Intn(10) > 5 {
321+
template = templateB
322+
templateVersion = templateVersionB
323+
} else {
324+
template = templateA
325+
templateVersion = templateVersionA
326+
}
327+
328+
workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{
329+
ID: uuid.New(),
330+
OwnerID: insertUser(db).ID,
331+
Name: uuid.NewString(),
332+
TemplateID: template,
333+
AutomaticUpdates: database.AutomaticUpdatesNever,
334+
})
335+
require.NoError(t, err)
336+
337+
job, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
338+
ID: uuid.New(),
339+
CreatedAt: dbtime.Now(),
340+
UpdatedAt: dbtime.Now(),
341+
Provisioner: database.ProvisionerTypeEcho,
342+
StorageMethod: database.ProvisionerStorageMethodFile,
343+
Type: database.ProvisionerJobTypeWorkspaceBuild,
344+
})
345+
require.NoError(t, err)
346+
err = db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{
347+
ID: uuid.New(),
348+
WorkspaceID: workspace.ID,
349+
JobID: job.ID,
350+
BuildNumber: 1,
351+
Transition: database.WorkspaceTransitionStart,
352+
Reason: database.BuildReasonInitiator,
353+
TemplateVersionID: templateVersion,
354+
})
355+
require.NoError(t, err)
356+
// This marks the job as started.
357+
_, err = db.AcquireProvisionerJob(context.Background(), database.AcquireProvisionerJobParams{
358+
OrganizationID: job.OrganizationID,
359+
StartedAt: sql.NullTime{
360+
Time: dbtime.Now(),
361+
Valid: true,
362+
},
363+
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
364+
})
365+
require.NoError(t, err)
366+
return job
367+
}
368+
369+
insertCanceled := func(db database.Store) {
370+
job := insertRunning(db)
371+
err := db.UpdateProvisionerJobWithCancelByID(context.Background(), database.UpdateProvisionerJobWithCancelByIDParams{
372+
ID: job.ID,
373+
CanceledAt: sql.NullTime{
374+
Time: dbtime.Now(),
375+
Valid: true,
376+
},
377+
})
378+
require.NoError(t, err)
379+
err = db.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
380+
ID: job.ID,
381+
CompletedAt: sql.NullTime{
382+
Time: dbtime.Now(),
383+
Valid: true,
384+
},
385+
})
386+
require.NoError(t, err)
387+
}
388+
389+
insertFailed := func(db database.Store) {
390+
job := insertRunning(db)
391+
err := db.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
392+
ID: job.ID,
393+
CompletedAt: sql.NullTime{
394+
Time: dbtime.Now(),
395+
Valid: true,
396+
},
397+
Error: sql.NullString{
398+
String: "failed",
399+
Valid: true,
400+
},
401+
})
402+
require.NoError(t, err)
403+
}
404+
405+
insertSuccess := func(db database.Store) {
406+
job := insertRunning(db)
407+
err := db.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
408+
ID: job.ID,
409+
CompletedAt: sql.NullTime{
410+
Time: dbtime.Now(),
411+
Valid: true,
412+
},
413+
})
414+
require.NoError(t, err)
415+
}
416+
417+
for _, tc := range []struct {
418+
Name string
419+
Database func() database.Store
420+
ExpectedSeries int
421+
ExpectedStatuses map[codersdk.ProvisionerJobStatus]int
422+
ExpectedWorkspaces int
423+
}{{
424+
Name: "None",
425+
Database: func() database.Store {
426+
return dbmem.New()
427+
},
428+
ExpectedSeries: 0,
429+
ExpectedWorkspaces: 0,
430+
}, {
431+
Name: "Multiple",
432+
Database: func() database.Store {
433+
db := dbmem.New()
434+
insertTemplates(db)
435+
insertCanceled(db)
436+
insertFailed(db)
437+
insertFailed(db)
438+
insertSuccess(db)
439+
insertSuccess(db)
440+
insertSuccess(db)
441+
insertRunning(db)
442+
return db
443+
},
444+
ExpectedSeries: 7,
445+
ExpectedWorkspaces: 7,
446+
ExpectedStatuses: map[codersdk.ProvisionerJobStatus]int{
447+
codersdk.ProvisionerJobCanceled: 1,
448+
codersdk.ProvisionerJobFailed: 2,
449+
codersdk.ProvisionerJobSucceeded: 3,
450+
codersdk.ProvisionerJobRunning: 1,
451+
},
452+
}} {
453+
tc := tc
454+
t.Run(tc.Name, func(t *testing.T) {
455+
t.Parallel()
456+
registry := prometheus.NewRegistry()
457+
closeFunc, err := prometheusmetrics.Workspaces(context.Background(), slogtest.Make(t, nil), registry, tc.Database(), testutil.IntervalFast)
458+
require.NoError(t, err)
459+
t.Cleanup(closeFunc)
460+
461+
require.Eventually(t, func() bool {
462+
metrics, err := registry.Gather()
463+
assert.NoError(t, err)
464+
stMap := map[codersdk.ProvisionerJobStatus]int{}
465+
wMap := map[string]struct{}{}
466+
for _, m := range metrics {
467+
if m.GetName() != "coderd_api_workspace_detail" {
468+
continue
469+
}
470+
471+
for _, metric := range m.Metric {
472+
for _, l := range metric.Label {
473+
if l == nil {
474+
continue
475+
}
476+
477+
switch l.GetName() {
478+
case "status":
479+
status := codersdk.ProvisionerJobStatus(l.GetValue())
480+
stMap[status] += int(metric.Gauge.GetValue())
481+
case "workspace_name":
482+
wMap[l.GetValue()] = struct{}{}
483+
}
484+
}
485+
}
486+
}
487+
488+
stSum := 0
489+
for st, count := range stMap {
490+
if tc.ExpectedStatuses[st] != count {
491+
return false
492+
}
493+
494+
stSum += count
495+
}
496+
497+
t.Logf("status series = %d, expected == %d", stSum, tc.ExpectedSeries)
498+
t.Logf("workspace series = %d, expected == %d", len(wMap), tc.ExpectedWorkspaces)
499+
return stSum == tc.ExpectedSeries && len(wMap) == tc.ExpectedWorkspaces
500+
}, testutil.WaitSuperShort, testutil.IntervalFast)
259501
})
260502
}
261503
}

testutil/duration.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99
// Constants for timing out operations, usable for creating contexts
1010
// that timeout or in require.Eventually.
1111
const (
12-
WaitShort = 10 * time.Second
13-
WaitMedium = 15 * time.Second
14-
WaitLong = 25 * time.Second
15-
WaitSuperLong = 60 * time.Second
12+
WaitSuperShort = time.Second
13+
WaitShort = 10 * time.Second
14+
WaitMedium = 15 * time.Second
15+
WaitLong = 25 * time.Second
16+
WaitSuperLong = 60 * time.Second
1617
)
1718

1819
// Constants for delaying repeated operations, e.g. in

0 commit comments

Comments
 (0)