Skip to content

Commit 2a4ac2a

Browse files
authored
feat: expose user seat limits as Prometheus metrics (#10169)
1 parent 570f963 commit 2a4ac2a

File tree

4 files changed

+129
-1
lines changed

4 files changed

+129
-1
lines changed

enterprise/coderd/coderd.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
375375
api.AGPL.WorkspaceProxyHostsFn.Store(&f)
376376
}
377377

378+
err = api.PrometheusRegistry.Register(&api.licenseMetricsCollector)
379+
if err != nil {
380+
return nil, xerrors.Errorf("unable to register license metrics collector")
381+
}
382+
378383
err = api.updateEntitlements(ctx)
379384
if err != nil {
380385
return nil, xerrors.Errorf("update entitlements: %w", err)
@@ -434,6 +439,8 @@ type API struct {
434439
entitlements codersdk.Entitlements
435440

436441
provisionerDaemonAuth *provisionerDaemonAuth
442+
443+
licenseMetricsCollector license.MetricsCollector
437444
}
438445

439446
func (api *API) Close() error {
@@ -660,8 +667,8 @@ func (api *API) updateEntitlements(ctx context.Context) error {
660667
api.entitlementsMu.Lock()
661668
defer api.entitlementsMu.Unlock()
662669
api.entitlements = entitlements
670+
api.licenseMetricsCollector.Entitlements.Store(&entitlements)
663671
api.AGPL.SiteHandler.Entitlements.Store(&entitlements)
664-
665672
return nil
666673
}
667674

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package license
2+
3+
import (
4+
"sync/atomic"
5+
6+
"github.com/prometheus/client_golang/prometheus"
7+
8+
"github.com/coder/coder/v2/codersdk"
9+
)
10+
11+
var (
12+
activeUsersDesc = prometheus.NewDesc("coderd_license_active_users", "The number of active users.", nil, nil)
13+
limitUsersDesc = prometheus.NewDesc("coderd_license_limit_users", "The user seats limit based on the active Coder license.", nil, nil)
14+
userLimitEnabledDesc = prometheus.NewDesc("coderd_license_user_limit_enabled", "Returns 1 if the current license enforces the user limit.", nil, nil)
15+
)
16+
17+
type MetricsCollector struct {
18+
Entitlements atomic.Pointer[codersdk.Entitlements]
19+
}
20+
21+
var _ prometheus.Collector = new(MetricsCollector)
22+
23+
func (*MetricsCollector) Describe(descCh chan<- *prometheus.Desc) {
24+
descCh <- activeUsersDesc
25+
descCh <- limitUsersDesc
26+
descCh <- userLimitEnabledDesc
27+
}
28+
29+
func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
30+
entitlements := mc.Entitlements.Load()
31+
if entitlements == nil || entitlements.Features == nil {
32+
return
33+
}
34+
35+
userLimitEntitlement, ok := entitlements.Features[codersdk.FeatureUserLimit]
36+
if !ok {
37+
return
38+
}
39+
40+
var enabled float64
41+
if userLimitEntitlement.Enabled {
42+
enabled = 1
43+
}
44+
metricsCh <- prometheus.MustNewConstMetric(userLimitEnabledDesc, prometheus.GaugeValue, enabled)
45+
46+
if userLimitEntitlement.Actual != nil {
47+
metricsCh <- prometheus.MustNewConstMetric(activeUsersDesc, prometheus.GaugeValue, float64(*userLimitEntitlement.Actual))
48+
}
49+
50+
if userLimitEntitlement.Limit != nil {
51+
metricsCh <- prometheus.MustNewConstMetric(limitUsersDesc, prometheus.GaugeValue, float64(*userLimitEntitlement.Limit))
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package license_test
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"testing"
7+
8+
"github.com/aws/smithy-go/ptr"
9+
"github.com/prometheus/client_golang/prometheus"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/coder/v2/enterprise/coderd/license"
14+
)
15+
16+
func TestCollectLicenseMetrics(t *testing.T) {
17+
t.Parallel()
18+
19+
// Given
20+
registry := prometheus.NewRegistry()
21+
22+
var sut license.MetricsCollector
23+
24+
const (
25+
actualUsers = 4
26+
userLimit = 7
27+
)
28+
sut.Entitlements.Store(&codersdk.Entitlements{
29+
Features: map[codersdk.FeatureName]codersdk.Feature{
30+
codersdk.FeatureUserLimit: {
31+
Enabled: true,
32+
Actual: ptr.Int64(actualUsers),
33+
Limit: ptr.Int64(userLimit),
34+
},
35+
},
36+
})
37+
38+
registry.Register(&sut)
39+
40+
// When
41+
metrics, err := registry.Gather()
42+
require.NoError(t, err)
43+
44+
// Then
45+
goldenFile, err := os.ReadFile("testdata/license-metrics.json")
46+
require.NoError(t, err)
47+
golden := map[string]int{}
48+
err = json.Unmarshal(goldenFile, &golden)
49+
require.NoError(t, err)
50+
51+
collected := map[string]int{}
52+
for _, metric := range metrics {
53+
switch metric.GetName() {
54+
case "coderd_license_active_users", "coderd_license_limit_users", "coderd_license_user_limit_enabled":
55+
for _, m := range metric.Metric {
56+
collected[metric.GetName()] = int(m.Gauge.GetValue())
57+
}
58+
default:
59+
require.FailNowf(t, "unexpected metric collected", "metric: %s", metric.GetName())
60+
}
61+
}
62+
require.EqualValues(t, golden, collected)
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"coderd_license_active_users": 4,
3+
"coderd_license_limit_users": 7,
4+
"coderd_license_user_limit_enabled": 1
5+
}

0 commit comments

Comments
 (0)