Skip to content

Commit b31ec33

Browse files
committed
feat(coderd): implement user latency insights logic
1 parent 8a1d3ee commit b31ec33

File tree

17 files changed

+223
-40
lines changed

17 files changed

+223
-40
lines changed

coderd/apidoc/docs.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbauthz/dbauthz.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,13 @@ func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateD
11731173
return q.db.GetTemplateDAUs(ctx, arg)
11741174
}
11751175

1176+
func (q *querier) GetTemplateUserLatencyStats(ctx context.Context, arg database.GetTemplateUserLatencyStatsParams) ([]database.GetTemplateUserLatencyStatsRow, error) {
1177+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
1178+
return nil, err
1179+
}
1180+
return q.db.GetTemplateUserLatencyStats(ctx, arg)
1181+
}
1182+
11761183
func (q *querier) GetTemplateVersionByID(ctx context.Context, tvid uuid.UUID) (database.TemplateVersion, error) {
11771184
tv, err := q.db.GetTemplateVersionByID(ctx, tvid)
11781185
if err != nil {

coderd/database/dbfake/dbfake.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,6 +1917,15 @@ func (q *FakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplat
19171917
return rs, nil
19181918
}
19191919

1920+
func (q *FakeQuerier) GetTemplateUserLatencyStats(ctx context.Context, arg database.GetTemplateUserLatencyStatsParams) ([]database.GetTemplateUserLatencyStatsRow, error) {
1921+
err := validateDatabaseType(arg)
1922+
if err != nil {
1923+
return nil, err
1924+
}
1925+
1926+
panic("not implemented")
1927+
}
1928+
19201929
func (q *FakeQuerier) GetTemplateVersionByID(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) {
19211930
q.mutex.RLock()
19221931
defer q.mutex.RUnlock()
@@ -5263,9 +5272,9 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
52635272
}
52645273
}
52655274

5266-
if len(arg.TemplateIds) > 0 {
5275+
if len(arg.TemplateIDs) > 0 {
52675276
match := false
5268-
for _, id := range arg.TemplateIds {
5277+
for _, id := range arg.TemplateIDs {
52695278
if workspace.TemplateID == id {
52705279
match = true
52715280
break

coderd/database/dbmetrics/dbmetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/modelqueries.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
209209
arg.OwnerID,
210210
arg.OwnerUsername,
211211
arg.TemplateName,
212-
pq.Array(arg.TemplateIds),
212+
pq.Array(arg.TemplateIDs),
213213
arg.Name,
214214
arg.HasAgent,
215215
arg.AgentInactiveDisconnectTimeoutSeconds,

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 63 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaceagentstats.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,21 @@ JOIN
155155
workspaces
156156
ON
157157
workspaces.id = agent_stats.workspace_id;
158+
159+
160+
-- name: GetTemplateUserLatencyStats :many
161+
SELECT
162+
workspace_agent_stats.user_id,
163+
users.username,
164+
array_agg(DISTINCT template_id)::uuid[] AS template_ids,
165+
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
166+
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
167+
FROM workspace_agent_stats
168+
JOIN users ON (users.id = workspace_agent_stats.user_id)
169+
WHERE
170+
workspace_agent_stats.created_at >= @start_time
171+
AND workspace_agent_stats.created_at < @end_time
172+
AND workspace_agent_stats.connection_median_latency_ms > 0
173+
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
174+
GROUP BY workspace_agent_stats.user_id, users.username
175+
ORDER BY user_id ASC;

coderd/database/sqlc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ overrides:
5757
inactivity_ttl: InactivityTTL
5858
eof: EOF
5959
locked_ttl: LockedTTL
60+
template_ids: TemplateIDs
6061

6162
sql:
6263
- schema: "./dump.sql"

coderd/insights.go

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"time"
77

88
"github.com/google/uuid"
9+
"golang.org/x/exp/slices"
910

11+
"github.com/coder/coder/coderd/database"
1012
"github.com/coder/coder/coderd/httpapi"
1113
"github.com/coder/coder/coderd/rbac"
1214
"github.com/coder/coder/codersdk"
@@ -62,6 +64,12 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
6264
return
6365
}
6466

67+
// TODO(mafredri): Client or deployment timezone?
68+
// Example:
69+
// - I want data from Monday - Friday
70+
// - I'm UTC+3 and the deployment is UTC+0
71+
// - Do we select Monday - Friday in UTC+0 or UTC+3?
72+
// - Considering users can be in different timezones, perhaps this should be per-user (but we don't keep track of user timezones).
6573
p := httpapi.NewQueryParamParser().
6674
Required("start_time").
6775
Required("end_time")
@@ -80,24 +88,80 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
8088
return
8189
}
8290

83-
// TODO(mafredri) Verify template IDs.
84-
_ = templateIDs
91+
// Should we verify all template IDs exist, or just return no rows?
92+
// _, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
93+
// IDs: templateIDs,
94+
// })
95+
96+
rows, err := api.Database.GetTemplateUserLatencyStats(ctx, database.GetTemplateUserLatencyStatsParams{
97+
StartTime: startTime,
98+
EndTime: endTime,
99+
TemplateIDs: templateIDs,
100+
})
101+
if err != nil {
102+
httpapi.InternalServerError(rw, err)
103+
return
104+
}
105+
106+
// Fetch all users so that we can still include users that have no
107+
// latency data.
108+
users, err := api.Database.GetUsers(ctx, database.GetUsersParams{})
109+
if err != nil {
110+
httpapi.InternalServerError(rw, err)
111+
return
112+
}
113+
114+
templateIDSet := make(map[uuid.UUID]struct{})
115+
usersWithLatencyByID := make(map[uuid.UUID]codersdk.UserLatency)
116+
for _, row := range rows {
117+
for _, templateID := range row.TemplateIDs {
118+
templateIDSet[templateID] = struct{}{}
119+
}
120+
usersWithLatencyByID[row.UserID] = codersdk.UserLatency{
121+
TemplateIDs: row.TemplateIDs,
122+
UserID: row.UserID,
123+
Username: row.Username,
124+
LatencyMS: &codersdk.ConnectionLatency{
125+
P50: row.WorkspaceConnectionLatency50,
126+
P95: row.WorkspaceConnectionLatency95,
127+
},
128+
}
129+
}
130+
userLatencies := []codersdk.UserLatency{}
131+
for _, user := range users {
132+
userLatency, ok := usersWithLatencyByID[user.ID]
133+
if !ok {
134+
// TODO(mafredri): Other cases?
135+
// We only include deleted/inactive users if they were
136+
// active as part of the requested timeframe.
137+
if user.Deleted || user.Status != database.UserStatusActive {
138+
continue
139+
}
140+
141+
userLatency = codersdk.UserLatency{
142+
TemplateIDs: []uuid.UUID{},
143+
UserID: user.ID,
144+
Username: user.Username,
145+
}
146+
}
147+
userLatencies = append(userLatencies, userLatency)
148+
}
149+
150+
// TemplateIDs that contributed to the data.
151+
seenTemplateIDs := make([]uuid.UUID, 0, len(templateIDSet))
152+
for templateID := range templateIDSet {
153+
seenTemplateIDs = append(seenTemplateIDs, templateID)
154+
}
155+
slices.SortFunc(seenTemplateIDs, func(a, b uuid.UUID) bool {
156+
return a.String() < b.String()
157+
})
85158

86159
resp := codersdk.UserLatencyInsightsResponse{
87160
Report: codersdk.UserLatencyInsightsReport{
88161
StartTime: startTime,
89162
EndTime: endTime,
90-
TemplateIDs: []uuid.UUID{},
91-
Users: []codersdk.UserLatency{
92-
{
93-
UserID: uuid.New(),
94-
Name: "Some User",
95-
LatencyMS: codersdk.ConnectionLatency{
96-
P50: 14.45,
97-
P95: 32.16,
98-
},
99-
},
100-
},
163+
TemplateIDs: seenTemplateIDs,
164+
Users: userLatencies,
101165
},
102166
}
103167
httpapi.Write(ctx, rw, http.StatusOK, resp)

coderd/templates.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
7979
// return ALL workspaces. Not just workspaces the user can view.
8080
// nolint:gocritic
8181
workspaces, err := api.Database.GetWorkspaces(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{
82-
TemplateIds: []uuid.UUID{template.ID},
82+
TemplateIDs: []uuid.UUID{template.ID},
8383
})
8484
if err != nil && !errors.Is(err, sql.ErrNoRows) {
8585
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{

codersdk/insights.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ type UserLatencyInsightsReport struct {
3838

3939
// UserLatency shows the connection latency for a user.
4040
type UserLatency struct {
41-
TemplateID []uuid.UUID `json:"template_ids" format:"uuid"`
42-
UserID uuid.UUID `json:"user_id" format:"uuid"`
43-
Name string `json:"name"`
44-
LatencyMS ConnectionLatency `json:"latency_ms"`
41+
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
42+
UserID uuid.UUID `json:"user_id" format:"uuid"`
43+
Username string `json:"username"`
44+
LatencyMS *ConnectionLatency `json:"latency_ms"`
4545
}
4646

4747
// ConnectionLatency shows the latency for a connection.

0 commit comments

Comments
 (0)