Skip to content

Commit dae11a4

Browse files
committed
port test
1 parent 167d4eb commit dae11a4

File tree

1 file changed

+217
-0
lines changed

1 file changed

+217
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package insights_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/uuid"
14+
"github.com/prometheus/client_golang/prometheus"
15+
io_prometheus_client "github.com/prometheus/client_model/go"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
19+
"cdr.dev/slog"
20+
"cdr.dev/slog/sloggers/slogtest"
21+
agentproto "github.com/coder/coder/v2/agent/proto"
22+
"github.com/coder/coder/v2/coderd/coderdtest"
23+
"github.com/coder/coder/v2/coderd/database"
24+
"github.com/coder/coder/v2/coderd/database/dbauthz"
25+
"github.com/coder/coder/v2/coderd/database/dbgen"
26+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
27+
"github.com/coder/coder/v2/coderd/prometheusmetrics/insights"
28+
"github.com/coder/coder/v2/coderd/workspaceapps"
29+
"github.com/coder/coder/v2/coderd/workspacestats"
30+
"github.com/coder/coder/v2/codersdk/agentsdk"
31+
"github.com/coder/coder/v2/testutil"
32+
)
33+
34+
func TestCollectInsights(t *testing.T) {
35+
t.Parallel()
36+
37+
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
38+
db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
39+
40+
options := &coderdtest.Options{
41+
IncludeProvisionerDaemon: true,
42+
AgentStatsRefreshInterval: time.Millisecond * 100,
43+
Database: db,
44+
Pubsub: ps,
45+
}
46+
ownerClient := coderdtest.New(t, options)
47+
ownerClient.SetLogger(logger.Named("ownerClient").Leveled(slog.LevelDebug))
48+
owner := coderdtest.CreateFirstUser(t, ownerClient)
49+
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
50+
51+
// Given
52+
// Initialize metrics collector
53+
mc, err := insights.NewMetricsCollector(db, logger, 0, time.Second)
54+
require.NoError(t, err)
55+
56+
registry := prometheus.NewRegistry()
57+
registry.Register(mc)
58+
59+
var (
60+
orgID = owner.OrganizationID
61+
tpl = dbgen.Template(t, db, database.Template{OrganizationID: orgID, CreatedBy: user.ID, Name: "golden-template"})
62+
ver = dbgen.TemplateVersion(t, db, database.TemplateVersion{OrganizationID: orgID, CreatedBy: user.ID, TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}})
63+
param1 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "first_parameter"})
64+
param2 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "second_parameter", Type: "bool"})
65+
param3 = dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{TemplateVersionID: ver.ID, Name: "third_parameter", Type: "number"})
66+
workspace1 = dbgen.Workspace(t, db, database.Workspace{OrganizationID: orgID, TemplateID: tpl.ID, OwnerID: user.ID})
67+
workspace2 = dbgen.Workspace(t, db, database.Workspace{OrganizationID: orgID, TemplateID: tpl.ID, OwnerID: user.ID})
68+
job1 = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: orgID})
69+
job2 = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: orgID})
70+
build1 = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{TemplateVersionID: ver.ID, WorkspaceID: workspace1.ID, JobID: job1.ID})
71+
build2 = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{TemplateVersionID: ver.ID, WorkspaceID: workspace2.ID, JobID: job2.ID})
72+
res1 = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: build1.JobID})
73+
res2 = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: build2.JobID})
74+
agent1 = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res1.ID})
75+
agent2 = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res2.ID})
76+
app1 = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agent1.ID, Slug: "golden-slug", DisplayName: "Golden Slug"})
77+
app2 = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agent2.ID, Slug: "golden-slug", DisplayName: "Golden Slug"})
78+
_ = dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{
79+
{WorkspaceBuildID: build1.ID, Name: param1.Name, Value: "Foobar"},
80+
{WorkspaceBuildID: build1.ID, Name: param2.Name, Value: "true"},
81+
{WorkspaceBuildID: build1.ID, Name: param3.Name, Value: "789"},
82+
})
83+
_ = dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{
84+
{WorkspaceBuildID: build2.ID, Name: param1.Name, Value: "Baz"},
85+
{WorkspaceBuildID: build2.ID, Name: param2.Name, Value: "true"},
86+
{WorkspaceBuildID: build2.ID, Name: param3.Name, Value: "999"},
87+
})
88+
)
89+
90+
// Start an agent so that we can generate stats.
91+
var agentClients []agentproto.DRPCAgentClient
92+
for i, agent := range []database.WorkspaceAgent{agent1, agent2} {
93+
agentClient := agentsdk.New(client.URL)
94+
agentClient.SetSessionToken(agent.AuthToken.String())
95+
agentClient.SDK.SetLogger(logger.Leveled(slog.LevelDebug).Named(fmt.Sprintf("agent%d", i+1)))
96+
conn, err := agentClient.ConnectRPC(context.Background())
97+
require.NoError(t, err)
98+
agentAPI := agentproto.NewDRPCAgentClient(conn)
99+
agentClients = append(agentClients, agentAPI)
100+
}
101+
102+
// Fake app stats
103+
_, err = agentClients[0].UpdateStats(context.Background(), &agentproto.UpdateStatsRequest{
104+
Stats: &agentproto.Stats{
105+
// ConnectionCount must be positive as database query ignores stats with no active connections at the time frame
106+
ConnectionsByProto: map[string]int64{"TCP": 1},
107+
ConnectionCount: 1,
108+
ConnectionMedianLatencyMs: 15,
109+
// Session counts must be positive, but the exact value is ignored.
110+
// Database query approximates it to 60s of usage.
111+
SessionCountSsh: 99,
112+
SessionCountJetbrains: 47,
113+
SessionCountVscode: 34,
114+
},
115+
})
116+
require.NoError(t, err, "unable to post fake stats")
117+
118+
// Fake app usage
119+
reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{
120+
Database: db,
121+
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
122+
})
123+
refTime := time.Now().Add(-3 * time.Minute).Truncate(time.Minute)
124+
//nolint:gocritic // This is a test.
125+
err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(context.Background()), []workspaceapps.StatsReport{
126+
{
127+
UserID: user.ID,
128+
WorkspaceID: workspace1.ID,
129+
AgentID: agent1.ID,
130+
AccessMethod: "path",
131+
SlugOrPort: app1.Slug,
132+
SessionID: uuid.New(),
133+
SessionStartedAt: refTime,
134+
SessionEndedAt: refTime.Add(2 * time.Minute).Add(-time.Second),
135+
Requests: 1,
136+
},
137+
// Same usage on differrent workspace/agent in same template,
138+
// should not be counted as extra.
139+
{
140+
UserID: user.ID,
141+
WorkspaceID: workspace2.ID,
142+
AgentID: agent2.ID,
143+
AccessMethod: "path",
144+
SlugOrPort: app2.Slug,
145+
SessionID: uuid.New(),
146+
SessionStartedAt: refTime,
147+
SessionEndedAt: refTime.Add(2 * time.Minute).Add(-time.Second),
148+
Requests: 1,
149+
},
150+
{
151+
UserID: user.ID,
152+
WorkspaceID: workspace2.ID,
153+
AgentID: agent2.ID,
154+
AccessMethod: "path",
155+
SlugOrPort: app2.Slug,
156+
SessionID: uuid.New(),
157+
SessionStartedAt: refTime.Add(2 * time.Minute),
158+
SessionEndedAt: refTime.Add(2 * time.Minute).Add(30 * time.Second),
159+
Requests: 1,
160+
},
161+
})
162+
require.NoError(t, err, "want no error inserting app stats")
163+
164+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
165+
defer cancel()
166+
167+
// Run metrics collector
168+
closeFunc, err := mc.Run(ctx)
169+
require.NoError(t, err)
170+
defer closeFunc()
171+
172+
goldenFile, err := os.ReadFile("testdata/insights-metrics.json")
173+
require.NoError(t, err)
174+
golden := map[string]int{}
175+
err = json.Unmarshal(goldenFile, &golden)
176+
require.NoError(t, err)
177+
178+
collected := map[string]int{}
179+
ok := assert.Eventuallyf(t, func() bool {
180+
// When
181+
metrics, err := registry.Gather()
182+
if !assert.NoError(t, err) {
183+
return false
184+
}
185+
186+
// Then
187+
for _, metric := range metrics {
188+
t.Logf("metric: %s: %#v", metric.GetName(), metric)
189+
switch metric.GetName() {
190+
case "coderd_insights_applications_usage_seconds", "coderd_insights_templates_active_users", "coderd_insights_parameters":
191+
for _, m := range metric.Metric {
192+
key := metric.GetName()
193+
if len(m.Label) > 0 {
194+
key = key + "[" + metricLabelAsString(m) + "]"
195+
}
196+
collected[key] = int(m.Gauge.GetValue())
197+
}
198+
default:
199+
assert.Failf(t, "unexpected metric collected", "metric: %s", metric.GetName())
200+
}
201+
}
202+
203+
return assert.ObjectsAreEqualValues(golden, collected)
204+
}, testutil.WaitMedium, testutil.IntervalFast, "template insights are inconsistent with golden files")
205+
if !ok {
206+
diff := cmp.Diff(golden, collected)
207+
assert.Empty(t, diff, "template insights are inconsistent with golden files (-golden +collected)")
208+
}
209+
}
210+
211+
func metricLabelAsString(m *io_prometheus_client.Metric) string {
212+
var labels []string
213+
for _, labelPair := range m.Label {
214+
labels = append(labels, labelPair.GetName()+"="+labelPair.GetValue())
215+
}
216+
return strings.Join(labels, ",")
217+
}

0 commit comments

Comments
 (0)