Skip to content

Commit 14eb20f

Browse files
committed
feat: add prometheus metric for tracking user statuses
1 parent bbd2dd8 commit 14eb20f

File tree

3 files changed

+134
-3
lines changed

3 files changed

+134
-3
lines changed

cli/server.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,13 @@ func enablePrometheus(
212212
options.PrometheusRegistry.MustRegister(collectors.NewGoCollector())
213213
options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
214214

215-
closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0)
215+
closeActiveUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.Logger.Named("active_user_metrics"), options.PrometheusRegistry, options.Database, 0)
216+
if err != nil {
217+
return nil, xerrors.Errorf("register active users prometheus metric: %w", err)
218+
}
219+
afterCtx(ctx, closeActiveUsersFunc)
220+
221+
closeUsersFunc, err := prometheusmetrics.Users(ctx, options.Logger.Named("user_metrics"), options.PrometheusRegistry, options.Database, 0)
216222
if err != nil {
217223
return nil, xerrors.Errorf("register active users prometheus metric: %w", err)
218224
}

coderd/prometheusmetrics/prometheusmetrics.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
const defaultRefreshRate = time.Minute
2828

2929
// ActiveUsers tracks the number of users that have authenticated within the past hour.
30-
func ActiveUsers(ctx context.Context, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) {
30+
func ActiveUsers(ctx context.Context, logger slog.Logger, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) {
3131
if duration == 0 {
3232
duration = defaultRefreshRate
3333
}
@@ -58,6 +58,7 @@ func ActiveUsers(ctx context.Context, registerer prometheus.Registerer, db datab
5858

5959
apiKeys, err := db.GetAPIKeysLastUsedAfter(ctx, dbtime.Now().Add(-1*time.Hour))
6060
if err != nil {
61+
logger.Error(ctx, "get api keys", slog.Error(err))
6162
continue
6263
}
6364
distinctUsers := map[uuid.UUID]struct{}{}
@@ -73,6 +74,55 @@ func ActiveUsers(ctx context.Context, registerer prometheus.Registerer, db datab
7374
}, nil
7475
}
7576

77+
// Users tracks the total user count, broken out by status..
78+
func Users(ctx context.Context, logger slog.Logger, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) {
79+
if duration == 0 {
80+
// It's not super important this tracks real-time.
81+
duration = defaultRefreshRate * 5
82+
}
83+
84+
gauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
85+
Namespace: "coderd",
86+
Subsystem: "api",
87+
Name: "total_user_count",
88+
Help: "The total number of users, broken out by status.",
89+
}, []string{"status"})
90+
err := registerer.Register(gauge)
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
ctx, cancelFunc := context.WithCancel(ctx)
96+
done := make(chan struct{})
97+
ticker := time.NewTicker(duration)
98+
go func() {
99+
defer close(done)
100+
defer ticker.Stop()
101+
for {
102+
select {
103+
case <-ctx.Done():
104+
return
105+
case <-ticker.C:
106+
}
107+
108+
gauge.Reset()
109+
users, err := db.GetUsers(dbauthz.AsSystemRestricted(ctx), database.GetUsersParams{})
110+
if err != nil {
111+
logger.Error(ctx, "get users", slog.Error(err))
112+
continue
113+
}
114+
115+
for _, user := range users {
116+
gauge.WithLabelValues(string(user.Status)).Inc()
117+
}
118+
}
119+
}()
120+
return func() {
121+
cancelFunc()
122+
<-done
123+
}, nil
124+
}
125+
76126
// Workspaces tracks the total number of workspaces with labels on status.
77127
func Workspaces(ctx context.Context, logger slog.Logger, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) {
78128
if duration == 0 {

coderd/prometheusmetrics/prometheusmetrics_test.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func TestActiveUsers(t *testing.T) {
9898
t.Run(tc.Name, func(t *testing.T) {
9999
t.Parallel()
100100
registry := prometheus.NewRegistry()
101-
closeFunc, err := prometheusmetrics.ActiveUsers(context.Background(), registry, tc.Database(t), time.Millisecond)
101+
closeFunc, err := prometheusmetrics.ActiveUsers(context.Background(), slogtest.Make(t, nil), registry, tc.Database(t), time.Millisecond)
102102
require.NoError(t, err)
103103
t.Cleanup(closeFunc)
104104

@@ -112,6 +112,81 @@ func TestActiveUsers(t *testing.T) {
112112
}
113113
}
114114

115+
func TestUsers(t *testing.T) {
116+
t.Parallel()
117+
118+
for _, tc := range []struct {
119+
Name string
120+
Database func(t *testing.T) database.Store
121+
Count map[string]int
122+
}{{
123+
Name: "None",
124+
Database: func(t *testing.T) database.Store {
125+
return dbmem.New()
126+
},
127+
Count: map[string]int{},
128+
}, {
129+
Name: "One",
130+
Database: func(t *testing.T) database.Store {
131+
db := dbmem.New()
132+
dbgen.User(t, db, database.User{Status: database.UserStatusActive})
133+
return db
134+
},
135+
Count: map[string]int{"active": 1},
136+
}, {
137+
Name: "MultipleStatuses",
138+
Database: func(t *testing.T) database.Store {
139+
db := dbmem.New()
140+
141+
dbgen.User(t, db, database.User{Status: database.UserStatusActive})
142+
dbgen.User(t, db, database.User{Status: database.UserStatusDormant})
143+
144+
return db
145+
},
146+
Count: map[string]int{"active": 1, "dormant": 1},
147+
}, {
148+
Name: "MultipleActive",
149+
Database: func(t *testing.T) database.Store {
150+
db := dbmem.New()
151+
dbgen.User(t, db, database.User{Status: database.UserStatusActive})
152+
dbgen.User(t, db, database.User{Status: database.UserStatusActive})
153+
dbgen.User(t, db, database.User{Status: database.UserStatusActive})
154+
return db
155+
},
156+
Count: map[string]int{"active": 3},
157+
}} {
158+
tc := tc
159+
t.Run(tc.Name, func(t *testing.T) {
160+
t.Parallel()
161+
registry := prometheus.NewRegistry()
162+
closeFunc, err := prometheusmetrics.Users(context.Background(), slogtest.Make(t, nil), registry, tc.Database(t), time.Millisecond)
163+
require.NoError(t, err)
164+
t.Cleanup(closeFunc)
165+
166+
require.Eventually(t, func() bool {
167+
metrics, err := registry.Gather()
168+
if err != nil {
169+
return false
170+
}
171+
172+
// If we get no metrics and we know none should exist, bail
173+
// early. If we get no metrics but we expect some, retry.
174+
if len(metrics) == 0 {
175+
return len(tc.Count) == 0
176+
}
177+
178+
for _, metric := range metrics[0].Metric {
179+
if tc.Count[*metric.Label[0].Value] != int(metric.Gauge.GetValue()) {
180+
return false
181+
}
182+
}
183+
184+
return true
185+
}, testutil.WaitShort, testutil.IntervalSlow)
186+
})
187+
}
188+
}
189+
115190
func TestWorkspaceLatestBuildTotals(t *testing.T) {
116191
t.Parallel()
117192

0 commit comments

Comments
 (0)