Skip to content

Commit 3fa3258

Browse files
committed
WIP
1 parent c6cf719 commit 3fa3258

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed

coderd/coderd.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
// Used for swagger docs.
3939
_ "github.com/coder/coder/v2/coderd/apidoc"
4040
"github.com/coder/coder/v2/coderd/externalauth"
41+
"github.com/coder/coder/v2/coderd/prometheusmetrics"
4142

4243
"cdr.dev/slog"
4344
"github.com/coder/coder/v2/buildinfo"
@@ -1033,6 +1034,8 @@ type API struct {
10331034
statsBatcher *batchstats.Batcher
10341035

10351036
Acquirer *provisionerdserver.Acquirer
1037+
1038+
LicenseMetrics *prometheusmetrics.LicenseMetrics
10361039
}
10371040

10381041
// Close waits for all WebSocket connections to drain before returning.

coderd/prometheusmetrics/license.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package prometheusmetrics
2+
3+
import (
4+
"context"
5+
"sync/atomic"
6+
"time"
7+
8+
"cdr.dev/slog"
9+
"github.com/google/uuid"
10+
"github.com/prometheus/client_golang/prometheus"
11+
"golang.org/x/xerrors"
12+
13+
"github.com/coder/coder/v2/coderd/database"
14+
"github.com/coder/coder/v2/coderd/database/dbtime"
15+
"github.com/coder/coder/v2/codersdk"
16+
)
17+
18+
type LicenseMetrics struct {
19+
db database.Store
20+
interval time.Duration
21+
logger slog.Logger
22+
registry *prometheus.Registry
23+
24+
Entitlements atomic.Pointer[codersdk.Entitlements]
25+
}
26+
27+
type LicenseMetricsOptions struct {
28+
Interval time.Duration
29+
Database database.Store
30+
Logger slog.Logger
31+
Registry *prometheus.Registry
32+
}
33+
34+
func NewLicenseMetrics(opts *LicenseMetricsOptions) (*LicenseMetrics, error) {
35+
if opts.Interval == 0 {
36+
opts.Interval = 1 * time.Minute
37+
}
38+
if opts.Database == nil {
39+
return nil, xerrors.Errorf("database is required")
40+
}
41+
if opts.Registry == nil {
42+
opts.Registry = prometheus.NewRegistry()
43+
}
44+
45+
return &LicenseMetrics{
46+
db: opts.Database,
47+
interval: opts.Interval,
48+
logger: opts.Logger,
49+
registry: opts.Registry,
50+
}, nil
51+
}
52+
53+
func (lm *LicenseMetrics) Collect(ctx context.Context) (func(), error) {
54+
55+
licenseLimitGauge := prometheus.NewGauge(prometheus.GaugeOpts{
56+
Namespace: "coderd",
57+
Subsystem: "license",
58+
Name: "user_limit",
59+
Help: `The user seats limit based on the current license. "Zero" means unlimited or a disabled feature.`,
60+
})
61+
err := registerer.Register(licenseLimitGauge)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
activeUsersGauge := prometheus.NewGauge(prometheus.GaugeOpts{
67+
Namespace: "coderd",
68+
Subsystem: "license",
69+
Name: "active_users",
70+
Help: "The number of active users.",
71+
})
72+
err = registerer.Register(activeUsersGauge)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
ctx, cancelFunc := context.WithCancel(ctx)
78+
done := make(chan struct{})
79+
ticker := time.NewTicker(duration)
80+
go func() {
81+
defer close(done)
82+
defer ticker.Stop()
83+
for {
84+
select {
85+
case <-ctx.Done():
86+
return
87+
case <-ticker.C:
88+
}
89+
90+
apiKeys, err := db.GetAPIKeysLastUsedAfter(ctx, dbtime.Now().Add(-1*time.Hour))
91+
if err != nil {
92+
continue
93+
}
94+
distinctUsers := map[uuid.UUID]struct{}{}
95+
for _, apiKey := range apiKeys {
96+
distinctUsers[apiKey.UserID] = struct{}{}
97+
}
98+
gauge.Set(float64(len(distinctUsers)))
99+
}
100+
}()
101+
return func() {
102+
cancelFunc()
103+
<-done
104+
}, nil
105+
}

coderd/prometheusmetrics/prometheusmetrics.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,61 @@ func AgentStats(ctx context.Context, logger slog.Logger, registerer prometheus.R
502502
<-done
503503
}, nil
504504
}
505+
506+
// License function tracks the number of occupied license seats, based on the entitlements.
507+
func License(ctx context.Context, registerer prometheus.Registerer, db database.Store, entitlementsFetcher func() codersdk.Entitlements, duration time.Duration) (func(), error) {
508+
if duration == 0 {
509+
duration = 1 * time.Minute
510+
}
511+
512+
licenseLimitGauge := prometheus.NewGauge(prometheus.GaugeOpts{
513+
Namespace: "coderd",
514+
Subsystem: "license",
515+
Name: "user_limit",
516+
Help: `The user seats limit based on the current license. "Zero" means unlimited or a disabled feature.`,
517+
})
518+
err := registerer.Register(licenseLimitGauge)
519+
if err != nil {
520+
return nil, err
521+
}
522+
523+
activeUsersGauge := prometheus.NewGauge(prometheus.GaugeOpts{
524+
Namespace: "coderd",
525+
Subsystem: "license",
526+
Name: "active_users",
527+
Help: "The number of active users.",
528+
})
529+
err = registerer.Register(activeUsersGauge)
530+
if err != nil {
531+
return nil, err
532+
}
533+
534+
ctx, cancelFunc := context.WithCancel(ctx)
535+
done := make(chan struct{})
536+
ticker := time.NewTicker(duration)
537+
go func() {
538+
defer close(done)
539+
defer ticker.Stop()
540+
for {
541+
select {
542+
case <-ctx.Done():
543+
return
544+
case <-ticker.C:
545+
}
546+
547+
apiKeys, err := db.GetAPIKeysLastUsedAfter(ctx, dbtime.Now().Add(-1*time.Hour))
548+
if err != nil {
549+
continue
550+
}
551+
distinctUsers := map[uuid.UUID]struct{}{}
552+
for _, apiKey := range apiKeys {
553+
distinctUsers[apiKey.UserID] = struct{}{}
554+
}
555+
gauge.Set(float64(len(distinctUsers)))
556+
}
557+
}()
558+
return func() {
559+
cancelFunc()
560+
<-done
561+
}, nil
562+
}

enterprise/coderd/coderd.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,3 +923,9 @@ func (api *API) runEntitlementsLoop(ctx context.Context) {
923923
func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
924924
return api.AGPL.HTTPAuth.Authorize(r, action, object)
925925
}
926+
927+
func (api *API) fetchEntitlements() codersdk.Entitlements {
928+
api.entitlementsMu.RLock()
929+
defer api.entitlementsMu.RUnlock()
930+
return api.entitlements
931+
}

0 commit comments

Comments
 (0)