mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: expose user seat limits as Prometheus metrics (#10169)
This commit is contained in:
@@ -375,6 +375,11 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
api.AGPL.WorkspaceProxyHostsFn.Store(&f)
|
||||
}
|
||||
|
||||
err = api.PrometheusRegistry.Register(&api.licenseMetricsCollector)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("unable to register license metrics collector")
|
||||
}
|
||||
|
||||
err = api.updateEntitlements(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("update entitlements: %w", err)
|
||||
@@ -434,6 +439,8 @@ type API struct {
|
||||
entitlements codersdk.Entitlements
|
||||
|
||||
provisionerDaemonAuth *provisionerDaemonAuth
|
||||
|
||||
licenseMetricsCollector license.MetricsCollector
|
||||
}
|
||||
|
||||
func (api *API) Close() error {
|
||||
@@ -660,8 +667,8 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
api.entitlementsMu.Lock()
|
||||
defer api.entitlementsMu.Unlock()
|
||||
api.entitlements = entitlements
|
||||
api.licenseMetricsCollector.Entitlements.Store(&entitlements)
|
||||
api.AGPL.SiteHandler.Entitlements.Store(&entitlements)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
var (
|
||||
activeUsersDesc = prometheus.NewDesc("coderd_license_active_users", "The number of active users.", nil, nil)
|
||||
limitUsersDesc = prometheus.NewDesc("coderd_license_limit_users", "The user seats limit based on the active Coder license.", nil, nil)
|
||||
userLimitEnabledDesc = prometheus.NewDesc("coderd_license_user_limit_enabled", "Returns 1 if the current license enforces the user limit.", nil, nil)
|
||||
)
|
||||
|
||||
type MetricsCollector struct {
|
||||
Entitlements atomic.Pointer[codersdk.Entitlements]
|
||||
}
|
||||
|
||||
var _ prometheus.Collector = new(MetricsCollector)
|
||||
|
||||
func (*MetricsCollector) Describe(descCh chan<- *prometheus.Desc) {
|
||||
descCh <- activeUsersDesc
|
||||
descCh <- limitUsersDesc
|
||||
descCh <- userLimitEnabledDesc
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
|
||||
entitlements := mc.Entitlements.Load()
|
||||
if entitlements == nil || entitlements.Features == nil {
|
||||
return
|
||||
}
|
||||
|
||||
userLimitEntitlement, ok := entitlements.Features[codersdk.FeatureUserLimit]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var enabled float64
|
||||
if userLimitEntitlement.Enabled {
|
||||
enabled = 1
|
||||
}
|
||||
metricsCh <- prometheus.MustNewConstMetric(userLimitEnabledDesc, prometheus.GaugeValue, enabled)
|
||||
|
||||
if userLimitEntitlement.Actual != nil {
|
||||
metricsCh <- prometheus.MustNewConstMetric(activeUsersDesc, prometheus.GaugeValue, float64(*userLimitEntitlement.Actual))
|
||||
}
|
||||
|
||||
if userLimitEntitlement.Limit != nil {
|
||||
metricsCh <- prometheus.MustNewConstMetric(limitUsersDesc, prometheus.GaugeValue, float64(*userLimitEntitlement.Limit))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package license_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
)
|
||||
|
||||
func TestCollectLicenseMetrics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given
|
||||
registry := prometheus.NewRegistry()
|
||||
|
||||
var sut license.MetricsCollector
|
||||
|
||||
const (
|
||||
actualUsers = 4
|
||||
userLimit = 7
|
||||
)
|
||||
sut.Entitlements.Store(&codersdk.Entitlements{
|
||||
Features: map[codersdk.FeatureName]codersdk.Feature{
|
||||
codersdk.FeatureUserLimit: {
|
||||
Enabled: true,
|
||||
Actual: ptr.Int64(actualUsers),
|
||||
Limit: ptr.Int64(userLimit),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
registry.Register(&sut)
|
||||
|
||||
// When
|
||||
metrics, err := registry.Gather()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then
|
||||
goldenFile, err := os.ReadFile("testdata/license-metrics.json")
|
||||
require.NoError(t, err)
|
||||
golden := map[string]int{}
|
||||
err = json.Unmarshal(goldenFile, &golden)
|
||||
require.NoError(t, err)
|
||||
|
||||
collected := map[string]int{}
|
||||
for _, metric := range metrics {
|
||||
switch metric.GetName() {
|
||||
case "coderd_license_active_users", "coderd_license_limit_users", "coderd_license_user_limit_enabled":
|
||||
for _, m := range metric.Metric {
|
||||
collected[metric.GetName()] = int(m.Gauge.GetValue())
|
||||
}
|
||||
default:
|
||||
require.FailNowf(t, "unexpected metric collected", "metric: %s", metric.GetName())
|
||||
}
|
||||
}
|
||||
require.EqualValues(t, golden, collected)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"coderd_license_active_users": 4,
|
||||
"coderd_license_limit_users": 7,
|
||||
"coderd_license_user_limit_enabled": 1
|
||||
}
|
||||
Reference in New Issue
Block a user