Skip to content

Commit c4f5905

Browse files
authored
feat: expose template insights as Prometheus metrics (#10325)
1 parent 997493d commit c4f5905

File tree

11 files changed

+555
-0
lines changed

11 files changed

+555
-0
lines changed

cli/server.go

+16
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import (
8080
"github.com/coder/coder/v2/coderd/httpmw"
8181
"github.com/coder/coder/v2/coderd/oauthpki"
8282
"github.com/coder/coder/v2/coderd/prometheusmetrics"
83+
"github.com/coder/coder/v2/coderd/prometheusmetrics/insights"
8384
"github.com/coder/coder/v2/coderd/schedule"
8485
"github.com/coder/coder/v2/coderd/telemetry"
8586
"github.com/coder/coder/v2/coderd/tracing"
@@ -200,6 +201,21 @@ func enablePrometheus(
200201
}
201202
afterCtx(ctx, closeWorkspacesFunc)
202203

204+
insightsMetricsCollector, err := insights.NewMetricsCollector(options.Database, options.Logger, 0, 0)
205+
if err != nil {
206+
return nil, xerrors.Errorf("unable to initialize insights metrics collector: %w", err)
207+
}
208+
err = options.PrometheusRegistry.Register(insightsMetricsCollector)
209+
if err != nil {
210+
return nil, xerrors.Errorf("unable to register insights metrics collector: %w", err)
211+
}
212+
213+
closeInsightsMetricsCollector, err := insightsMetricsCollector.Run(ctx)
214+
if err != nil {
215+
return nil, xerrors.Errorf("unable to run insights metrics collector: %w", err)
216+
}
217+
afterCtx(ctx, closeInsightsMetricsCollector)
218+
203219
if vals.Prometheus.CollectAgentStats {
204220
closeAgentStatsFunc, err := prometheusmetrics.AgentStats(ctx, logger, options.PrometheusRegistry, options.Database, time.Now(), 0)
205221
if err != nil {

coderd/database/dbauthz/dbauthz.go

+7
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,13 @@ func (q *querier) GetTemplateInsightsByInterval(ctx context.Context, arg databas
13271327
return q.db.GetTemplateInsightsByInterval(ctx, arg)
13281328
}
13291329

1330+
func (q *querier) GetTemplateInsightsByTemplate(ctx context.Context, arg database.GetTemplateInsightsByTemplateParams) ([]database.GetTemplateInsightsByTemplateRow, error) {
1331+
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
1332+
return nil, err
1333+
}
1334+
return q.db.GetTemplateInsightsByTemplate(ctx, arg)
1335+
}
1336+
13301337
func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
13311338
for _, templateID := range arg.TemplateIDs {
13321339
template, err := q.db.GetTemplateByID(ctx, templateID)

coderd/database/dbfake/dbfake.go

+99
Original file line numberDiff line numberDiff line change
@@ -2500,6 +2500,10 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem
25002500

25012501
templateIDSet := make(map[uuid.UUID]struct{})
25022502
appUsageIntervalsByUser := make(map[uuid.UUID]map[time.Time]*database.GetTemplateInsightsRow)
2503+
2504+
q.mutex.RLock()
2505+
defer q.mutex.RUnlock()
2506+
25032507
for _, s := range q.workspaceAgentStats {
25042508
if s.CreatedAt.Before(arg.StartTime) || s.CreatedAt.Equal(arg.EndTime) || s.CreatedAt.After(arg.EndTime) {
25052509
continue
@@ -2648,6 +2652,101 @@ func (q *FakeQuerier) GetTemplateInsightsByInterval(ctx context.Context, arg dat
26482652
return result, nil
26492653
}
26502654

2655+
func (q *FakeQuerier) GetTemplateInsightsByTemplate(_ context.Context, arg database.GetTemplateInsightsByTemplateParams) ([]database.GetTemplateInsightsByTemplateRow, error) {
2656+
err := validateDatabaseType(arg)
2657+
if err != nil {
2658+
return nil, err
2659+
}
2660+
2661+
q.mutex.RLock()
2662+
defer q.mutex.RUnlock()
2663+
2664+
// map time.Time x TemplateID x UserID x <usage>
2665+
appUsageByTemplateAndUser := map[time.Time]map[uuid.UUID]map[uuid.UUID]database.GetTemplateInsightsByTemplateRow{}
2666+
2667+
// Review agent stats in terms of usage
2668+
templateIDSet := make(map[uuid.UUID]struct{})
2669+
2670+
for _, s := range q.workspaceAgentStats {
2671+
if s.CreatedAt.Before(arg.StartTime) || s.CreatedAt.Equal(arg.EndTime) || s.CreatedAt.After(arg.EndTime) {
2672+
continue
2673+
}
2674+
if s.ConnectionCount == 0 {
2675+
continue
2676+
}
2677+
2678+
t := s.CreatedAt.Truncate(time.Minute)
2679+
templateIDSet[s.TemplateID] = struct{}{}
2680+
2681+
if _, ok := appUsageByTemplateAndUser[t]; !ok {
2682+
appUsageByTemplateAndUser[t] = make(map[uuid.UUID]map[uuid.UUID]database.GetTemplateInsightsByTemplateRow)
2683+
}
2684+
2685+
if _, ok := appUsageByTemplateAndUser[t][s.TemplateID]; !ok {
2686+
appUsageByTemplateAndUser[t][s.TemplateID] = make(map[uuid.UUID]database.GetTemplateInsightsByTemplateRow)
2687+
}
2688+
2689+
if _, ok := appUsageByTemplateAndUser[t][s.TemplateID][s.UserID]; !ok {
2690+
appUsageByTemplateAndUser[t][s.TemplateID][s.UserID] = database.GetTemplateInsightsByTemplateRow{}
2691+
}
2692+
2693+
u := appUsageByTemplateAndUser[t][s.TemplateID][s.UserID]
2694+
if s.SessionCountJetBrains > 0 {
2695+
u.UsageJetbrainsSeconds = 60
2696+
}
2697+
if s.SessionCountVSCode > 0 {
2698+
u.UsageVscodeSeconds = 60
2699+
}
2700+
if s.SessionCountReconnectingPTY > 0 {
2701+
u.UsageReconnectingPtySeconds = 60
2702+
}
2703+
if s.SessionCountSSH > 0 {
2704+
u.UsageSshSeconds = 60
2705+
}
2706+
appUsageByTemplateAndUser[t][s.TemplateID][s.UserID] = u
2707+
}
2708+
2709+
// Sort used templates
2710+
templateIDs := make([]uuid.UUID, 0, len(templateIDSet))
2711+
for templateID := range templateIDSet {
2712+
templateIDs = append(templateIDs, templateID)
2713+
}
2714+
slices.SortFunc(templateIDs, func(a, b uuid.UUID) int {
2715+
return slice.Ascending(a.String(), b.String())
2716+
})
2717+
2718+
// Build result
2719+
var result []database.GetTemplateInsightsByTemplateRow
2720+
for _, templateID := range templateIDs {
2721+
r := database.GetTemplateInsightsByTemplateRow{
2722+
TemplateID: templateID,
2723+
}
2724+
2725+
uniqueUsers := map[uuid.UUID]struct{}{}
2726+
2727+
for _, mTemplateUserUsage := range appUsageByTemplateAndUser {
2728+
mUserUsage, ok := mTemplateUserUsage[templateID]
2729+
if !ok {
2730+
continue // template was not used in this time window
2731+
}
2732+
2733+
for userID, usage := range mUserUsage {
2734+
uniqueUsers[userID] = struct{}{}
2735+
2736+
r.UsageJetbrainsSeconds += usage.UsageJetbrainsSeconds
2737+
r.UsageVscodeSeconds += usage.UsageVscodeSeconds
2738+
r.UsageReconnectingPtySeconds += usage.UsageReconnectingPtySeconds
2739+
r.UsageSshSeconds += usage.UsageSshSeconds
2740+
}
2741+
}
2742+
2743+
r.ActiveUsers = int64(len(uniqueUsers))
2744+
2745+
result = append(result, r)
2746+
}
2747+
return result, nil
2748+
}
2749+
26512750
func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
26522751
err := validateDatabaseType(arg)
26532752
if err != nil {

coderd/database/dbmetrics/dbmetrics.go

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+73
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/insights.sql

+28
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,34 @@ SELECT
134134
COALESCE(SUM(usage_ssh_seconds), 0)::bigint AS usage_ssh_seconds
135135
FROM agent_stats_by_interval_and_user;
136136

137+
-- name: GetTemplateInsightsByTemplate :many
138+
WITH agent_stats_by_interval_and_user AS (
139+
SELECT
140+
date_trunc('minute', was.created_at) AS created_at_trunc,
141+
was.template_id,
142+
was.user_id,
143+
CASE WHEN SUM(was.session_count_vscode) > 0 THEN 60 ELSE 0 END AS usage_vscode_seconds,
144+
CASE WHEN SUM(was.session_count_jetbrains) > 0 THEN 60 ELSE 0 END AS usage_jetbrains_seconds,
145+
CASE WHEN SUM(was.session_count_reconnecting_pty) > 0 THEN 60 ELSE 0 END AS usage_reconnecting_pty_seconds,
146+
CASE WHEN SUM(was.session_count_ssh) > 0 THEN 60 ELSE 0 END AS usage_ssh_seconds
147+
FROM workspace_agent_stats was
148+
WHERE
149+
was.created_at >= @start_time::timestamptz
150+
AND was.created_at < @end_time::timestamptz
151+
AND was.connection_count > 0
152+
GROUP BY created_at_trunc, was.template_id, was.user_id
153+
)
154+
155+
SELECT
156+
template_id,
157+
COALESCE(COUNT(DISTINCT user_id))::bigint AS active_users,
158+
COALESCE(SUM(usage_vscode_seconds), 0)::bigint AS usage_vscode_seconds,
159+
COALESCE(SUM(usage_jetbrains_seconds), 0)::bigint AS usage_jetbrains_seconds,
160+
COALESCE(SUM(usage_reconnecting_pty_seconds), 0)::bigint AS usage_reconnecting_pty_seconds,
161+
COALESCE(SUM(usage_ssh_seconds), 0)::bigint AS usage_ssh_seconds
162+
FROM agent_stats_by_interval_and_user
163+
GROUP BY template_id;
164+
137165
-- name: GetTemplateAppInsights :many
138166
-- GetTemplateAppInsights returns the aggregate usage of each app in a given
139167
-- timeframe. The result can be filtered on template_ids, meaning only user data

0 commit comments

Comments
 (0)