mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat(coderd): add user latency and template insights endpoints (#8519)
Part of #8514 Refs #8109
This commit is contained in:
committed by
GitHub
parent
539fcf9e6b
commit
30fe153296
@ -2,12 +2,14 @@ package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/agent"
|
||||
@ -100,3 +102,275 @@ func TestDeploymentInsights(t *testing.T) {
|
||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserLatencyInsights(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
AgentStatsRefreshInterval: time.Millisecond * 100,
|
||||
})
|
||||
|
||||
// Create two users, one that will appear in the report and another that
|
||||
// won't (due to not having/using a workspace).
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
|
||||
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Start an agent so that we can generate stats.
|
||||
agentClient := agentsdk.New(client.URL)
|
||||
agentClient.SetSessionToken(authToken)
|
||||
agentCloser := agent.New(agent.Options{
|
||||
Logger: logger.Named("agent"),
|
||||
Client: agentClient,
|
||||
})
|
||||
defer func() {
|
||||
_ = agentCloser.Close()
|
||||
}()
|
||||
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
||||
|
||||
// Start must be at the beginning of the day, initialize it early in case
|
||||
// the day changes so that we get the relevant stats faster.
|
||||
y, m, d := time.Now().UTC().Date()
|
||||
today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Connect to the agent to generate usage/latency stats.
|
||||
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{
|
||||
Logger: logger.Named("client"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
sshConn, err := conn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshConn.Close()
|
||||
|
||||
sess, err := sshConn.NewSession()
|
||||
require.NoError(t, err)
|
||||
defer sess.Close()
|
||||
|
||||
r, w := io.Pipe()
|
||||
defer r.Close()
|
||||
defer w.Close()
|
||||
sess.Stdin = r
|
||||
err = sess.Start("cat")
|
||||
require.NoError(t, err)
|
||||
|
||||
var userLatencies codersdk.UserLatencyInsightsResponse
|
||||
require.Eventuallyf(t, func() bool {
|
||||
userLatencies, err = client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{
|
||||
StartTime: today,
|
||||
EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour.
|
||||
TemplateIDs: []uuid.UUID{template.ID},
|
||||
})
|
||||
if !assert.NoError(t, err) {
|
||||
return false
|
||||
}
|
||||
return len(userLatencies.Report.Users) > 0 && userLatencies.Report.Users[0].LatencyMS.P50 > 0
|
||||
}, testutil.WaitShort, testutil.IntervalFast, "user latency is missing")
|
||||
|
||||
// We got our latency data, close the connection.
|
||||
_ = sess.Close()
|
||||
_ = sshConn.Close()
|
||||
|
||||
require.Len(t, userLatencies.Report.Users, 1, "want only 1 user")
|
||||
require.Equal(t, userLatencies.Report.Users[0].UserID, user.UserID, "want user id to match")
|
||||
assert.Greater(t, userLatencies.Report.Users[0].LatencyMS.P50, float64(0), "want p50 to be greater than 0")
|
||||
assert.Greater(t, userLatencies.Report.Users[0].LatencyMS.P95, float64(0), "want p95 to be greater than 0")
|
||||
}
|
||||
|
||||
func TestUserLatencyInsights_BadRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
y, m, d := time.Now().UTC().Date()
|
||||
today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{
|
||||
StartTime: today,
|
||||
EndTime: today.AddDate(0, 0, -1),
|
||||
})
|
||||
assert.Error(t, err, "want error for end time before start time")
|
||||
|
||||
_, err = client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{
|
||||
StartTime: today.AddDate(0, 0, -7),
|
||||
EndTime: today.Add(-time.Hour),
|
||||
})
|
||||
assert.Error(t, err, "want error for end time partial day when not today")
|
||||
}
|
||||
|
||||
func TestTemplateInsights(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
opts := &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
AgentStatsRefreshInterval: time.Millisecond * 100,
|
||||
}
|
||||
client := coderdtest.New(t, opts)
|
||||
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
|
||||
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Start an agent so that we can generate stats.
|
||||
agentClient := agentsdk.New(client.URL)
|
||||
agentClient.SetSessionToken(authToken)
|
||||
agentCloser := agent.New(agent.Options{
|
||||
Logger: logger.Named("agent"),
|
||||
Client: agentClient,
|
||||
})
|
||||
defer func() {
|
||||
_ = agentCloser.Close()
|
||||
}()
|
||||
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
||||
|
||||
// Start must be at the beginning of the day, initialize it early in case
|
||||
// the day changes so that we get the relevant stats faster.
|
||||
y, m, d := time.Now().UTC().Date()
|
||||
today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Connect to the agent to generate usage/latency stats.
|
||||
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{
|
||||
Logger: logger.Named("client"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
sshConn, err := conn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshConn.Close()
|
||||
|
||||
// Start an SSH session to generate SSH usage stats.
|
||||
sess, err := sshConn.NewSession()
|
||||
require.NoError(t, err)
|
||||
defer sess.Close()
|
||||
|
||||
r, w := io.Pipe()
|
||||
defer r.Close()
|
||||
defer w.Close()
|
||||
sess.Stdin = r
|
||||
err = sess.Start("cat")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start an rpty session to generate rpty usage stats.
|
||||
rpty, err := client.WorkspaceAgentReconnectingPTY(ctx, codersdk.WorkspaceAgentReconnectingPTYOpts{
|
||||
AgentID: resources[0].Agents[0].ID,
|
||||
Reconnect: uuid.New(),
|
||||
Width: 80,
|
||||
Height: 24,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer rpty.Close()
|
||||
|
||||
var resp codersdk.TemplateInsightsResponse
|
||||
var req codersdk.TemplateInsightsRequest
|
||||
waitForAppSeconds := func(slug string) func() bool {
|
||||
return func() bool {
|
||||
req = codersdk.TemplateInsightsRequest{
|
||||
StartTime: today,
|
||||
EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour),
|
||||
Interval: codersdk.InsightsReportIntervalDay,
|
||||
}
|
||||
resp, err = client.TemplateInsights(ctx, req)
|
||||
if !assert.NoError(t, err) {
|
||||
return false
|
||||
}
|
||||
|
||||
if slices.IndexFunc(resp.Report.AppsUsage, func(au codersdk.TemplateAppUsage) bool {
|
||||
return au.Slug == slug && au.Seconds > 0
|
||||
}) != -1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
require.Eventually(t, waitForAppSeconds("reconnecting-pty"), testutil.WaitShort, testutil.IntervalFast, "reconnecting-pty seconds missing")
|
||||
require.Eventually(t, waitForAppSeconds("ssh"), testutil.WaitShort, testutil.IntervalFast, "ssh seconds missing")
|
||||
|
||||
// We got our data, close down sessions and connections.
|
||||
_ = rpty.Close()
|
||||
_ = sess.Close()
|
||||
_ = sshConn.Close()
|
||||
|
||||
assert.WithinDuration(t, req.StartTime, resp.Report.StartTime, 0)
|
||||
assert.WithinDuration(t, req.EndTime, resp.Report.EndTime, 0)
|
||||
assert.Equal(t, resp.Report.ActiveUsers, int64(1), "want one active user")
|
||||
for _, app := range resp.Report.AppsUsage {
|
||||
if slices.Contains([]string{"reconnecting-pty", "ssh"}, app.Slug) {
|
||||
assert.Equal(t, app.Seconds, int64(300), "want app %q to have 5 minutes of usage", app.Slug)
|
||||
} else {
|
||||
assert.Equal(t, app.Seconds, int64(0), "want app %q to have 0 minutes of usage", app.Slug)
|
||||
}
|
||||
}
|
||||
// The full timeframe is <= 24h, so the interval matches exactly.
|
||||
assert.Len(t, resp.IntervalReports, 1, "want one interval report")
|
||||
assert.WithinDuration(t, req.StartTime, resp.IntervalReports[0].StartTime, 0)
|
||||
assert.WithinDuration(t, req.EndTime, resp.IntervalReports[0].EndTime, 0)
|
||||
assert.Equal(t, resp.IntervalReports[0].ActiveUsers, int64(1), "want one active user in the interval report")
|
||||
}
|
||||
|
||||
func TestTemplateInsights_BadRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
y, m, d := time.Now().UTC().Date()
|
||||
today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
|
||||
StartTime: today,
|
||||
EndTime: today.AddDate(0, 0, -1),
|
||||
})
|
||||
assert.Error(t, err, "want error for end time before start time")
|
||||
|
||||
_, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
|
||||
StartTime: today.AddDate(0, 0, -7),
|
||||
EndTime: today.Add(-time.Hour),
|
||||
})
|
||||
assert.Error(t, err, "want error for end time partial day when not today")
|
||||
|
||||
_, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
|
||||
StartTime: today.AddDate(0, 0, -1),
|
||||
EndTime: today,
|
||||
Interval: "invalid",
|
||||
})
|
||||
assert.Error(t, err, "want error for bad interval")
|
||||
}
|
||||
|
Reference in New Issue
Block a user