Skip to content

Commit 9322358

Browse files
committed
Add backend endpoint
1 parent 9a600e2 commit 9322358

File tree

16 files changed

+340
-117
lines changed

16 files changed

+340
-117
lines changed

Makefile

+27-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ bin: $(shell find . -not -path './vendor/*' -type f -name '*.go') go.mod go.sum
3030
darwin:amd64,arm64
3131
.PHONY: bin
3232

33-
build: site/out/index.html $(shell find . -not -path './vendor/*' -type f -name '*.go') go.mod go.sum $(shell find ./examples/templates)
33+
GO_FILES=$(shell find . -not -path './vendor/*' -type f -name '*.go') go.mod go.sum $(shell find ./examples/templates)
34+
35+
build: site/out/index.html $(GO_FILES)
3436
rm -rf ./dist
3537
mkdir -p ./dist
3638
rm -f ./site/out/bin/coder*
@@ -55,6 +57,30 @@ build: site/out/index.html $(shell find . -not -path './vendor/*' -type f -name
5557
darwin:amd64,arm64
5658
.PHONY: build
5759

60+
# Builds a test binary for just Linux
61+
build-linux-test: site/out/index.html $(GO_FILES)
62+
rm -rf ./dist
63+
mkdir -p ./dist
64+
rm -f ./site/out/bin/coder*
65+
66+
# build slim artifacts and copy them to the site output directory
67+
./scripts/build_go_slim.sh \
68+
--version "$(VERSION)" \
69+
--compress 6 \
70+
--output ./dist/ \
71+
linux:amd64,armv7,arm64 \
72+
windows:amd64,arm64 \
73+
darwin:amd64,arm64
74+
75+
# build not-so-slim artifacts with the default name format
76+
./scripts/build_go_matrix.sh \
77+
--version "$(VERSION)" \
78+
--output ./dist/ \
79+
--archive \
80+
--package-linux \
81+
linux:amd64
82+
.PHONY: build-linux-test
83+
5884
# Runs migrations to output a dump of the database.
5985
coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/database/migrations/*.sql)
6086
go run coderd/database/gen/dump/main.go

agent/stats.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ func (c *ConnStats) Write(b []byte) (n int, err error) {
3131
}
3232

3333
type ProtocolStats struct {
34-
NumConns int64 `json:"num_comms,omitempty"`
34+
NumConns int64 `json:"num_comms"`
3535

3636
// RxBytes must be read with atomic.
37-
RxBytes int64 `json:"rx_bytes,omitempty"`
37+
RxBytes int64 `json:"rx_bytes"`
3838

3939
// TxBytes must be read with atomic.
40-
TxBytes int64 `json:"tx_bytes,omitempty"`
40+
TxBytes int64 `json:"tx_bytes"`
4141
}
4242

4343
var _ net.Conn = new(ConnStats)

cli/agent.go

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func workspaceAgent() *cobra.Command {
182182
EnableWireguard: wireguard,
183183
UploadWireguardKeys: client.UploadWorkspaceAgentKeys,
184184
ListenWireguardPeers: client.WireguardPeerListener,
185+
StatsReporter: client.AgentReportStats,
185186
})
186187
<-cmd.Context().Done()
187188
return closer.Close()

coderd/coderd.go

+6
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,12 @@ func New(options *Options) *API {
338338
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
339339
r.Get("/report-agent-stats", api.workspaceAgentReportStats)
340340
})
341+
r.Group(func(r chi.Router) {
342+
r.Use(
343+
apiKeyMiddleware,
344+
)
345+
r.Get("/daus", api.getDAUs)
346+
})
341347
})
342348
r.Route("/workspaceagents", func(r chi.Router) {
343349
r.Post("/azure-instance-identity", api.postWorkspaceAuthAzureInstanceIdentity)

coderd/database/databasefake/databasefake.go

+34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/google/uuid"
1212
"github.com/lib/pq"
13+
"golang.org/x/exp/maps"
1314
"golang.org/x/exp/slices"
1415

1516
"github.com/coder/coder/coderd/database"
@@ -153,6 +154,39 @@ func (q *fakeQuerier) InsertAgentStat(_ context.Context, p database.InsertAgentS
153154
return stat, nil
154155
}
155156

157+
func (q *fakeQuerier) GetDAUsFromAgentStats(_ context.Context) ([]database.GetDAUsFromAgentStatsRow, error) {
158+
q.mutex.Lock()
159+
defer q.mutex.Unlock()
160+
161+
counts := make(map[time.Time]map[string]struct{})
162+
163+
for _, as := range q.agentStats {
164+
date := as.CreatedAt.Truncate(time.Hour * 24)
165+
dateEntry := counts[date]
166+
if dateEntry == nil {
167+
dateEntry = make(map[string]struct{})
168+
}
169+
counts[date] = dateEntry
170+
171+
dateEntry[as.UserID.String()] = struct{}{}
172+
}
173+
174+
countKeys := maps.Keys(counts)
175+
sort.Slice(countKeys, func(i, j int) bool {
176+
return countKeys[i].Before(countKeys[j])
177+
})
178+
179+
var rs []database.GetDAUsFromAgentStatsRow
180+
for _, key := range countKeys {
181+
rs = append(rs, database.GetDAUsFromAgentStatsRow{
182+
Date: key,
183+
Daus: int64(len(counts[key])),
184+
})
185+
}
186+
187+
return rs, nil
188+
}
189+
156190
func (q *fakeQuerier) ParameterValue(_ context.Context, id uuid.UUID) (database.ParameterValue, error) {
157191
q.mutex.Lock()
158192
defer q.mutex.Unlock()

coderd/database/dump.sql

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

coderd/database/migrations/000039_agent_stats.up.sql

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
CREATE TABLE agent_stats (
22
id text NOT NULL,
3+
PRIMARY KEY (id),
34
created_at timestamptz NOT NULL,
45
user_id uuid NOT NULL,
56
agent_id uuid NOT NULL,

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

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

coderd/database/queries/agentstats.sql

+13
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,16 @@ INSERT INTO
1010
)
1111
VALUES
1212
($1, $2, $3, $4, $5, $6) RETURNING *;
13+
14+
-- name: GetDAUsFromAgentStats :many
15+
select
16+
created_at::date as date,
17+
count(distinct(user_id)) as daus
18+
from
19+
agent_stats
20+
where
21+
cast(payload->>'num_comms' as integer) > 0
22+
group by
23+
date
24+
order by
25+
date asc;

coderd/metrics.go

+46-15
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,37 @@ import (
1515
"github.com/coder/coder/coderd/database"
1616
"github.com/coder/coder/coderd/httpapi"
1717
"github.com/coder/coder/coderd/httpmw"
18+
"github.com/coder/coder/coderd/rbac"
1819
"github.com/coder/coder/codersdk"
1920
)
2021

21-
const AgentStatIntervalEnv = "AGENT_STAT_INTERVAL"
22+
const AgentStatIntervalEnv = "CODER_AGENT_STAT_INTERVAL"
23+
24+
func (api *API) getDAUs(rw http.ResponseWriter, r *http.Request) {
25+
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceMetrics) {
26+
httpapi.Forbidden(rw)
27+
return
28+
}
29+
30+
daus, err := api.Database.GetDAUsFromAgentStats(r.Context())
31+
if err != nil {
32+
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
33+
Message: "Failed to get DAUs.",
34+
Detail: err.Error(),
35+
})
36+
return
37+
}
38+
39+
var resp codersdk.GetDAUsResponse
40+
for _, ent := range daus {
41+
resp.Entries = append(resp.Entries, codersdk.DAUEntry{
42+
Date: ent.Date,
43+
DAUs: int(ent.Daus),
44+
})
45+
}
46+
47+
json.NewEncoder(rw).Encode(resp)
48+
}
2249

2350
func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) {
2451
api.websocketWaitMutex.Lock()
@@ -117,22 +144,26 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
117144
slog.F("agent", workspaceAgent.ID),
118145
slog.F("resource", resource.ID),
119146
slog.F("workspace", workspace.ID),
120-
slog.F("conns", rep.ProtocolStats),
147+
slog.F("payload", rep),
121148
)
122-
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
123-
ID: uuid.NewString(),
124-
CreatedAt: time.Now(),
125-
AgentID: workspaceAgent.ID,
126-
WorkspaceID: build.WorkspaceID,
127-
UserID: workspace.OwnerID,
128-
Payload: json.RawMessage(repJSON),
129-
})
130-
if err != nil {
131-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
132-
Message: "Failed to insert agent stat.",
133-
Detail: err.Error(),
149+
150+
// Avoid inserting empty rows to preserve DB space.
151+
if len(rep.ProtocolStats) > 0 {
152+
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
153+
ID: uuid.NewString(),
154+
CreatedAt: time.Now(),
155+
AgentID: workspaceAgent.ID,
156+
WorkspaceID: build.WorkspaceID,
157+
UserID: workspace.OwnerID,
158+
Payload: json.RawMessage(repJSON),
134159
})
135-
return
160+
if err != nil {
161+
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
162+
Message: "Failed to insert agent stat.",
163+
Detail: err.Error(),
164+
})
165+
return
166+
}
136167
}
137168

138169
select {

coderd/metrics_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ func TestWorkspaceReportStats(t *testing.T) {
8989
_, err = session.Output("echo hello")
9090
require.NoError(t, err)
9191

92+
// Give enough time for stats to hit DB
9293
time.Sleep(time.Second * 1)
94+
95+
daus, err := client.GetDAUsFromAgentStats(context.Background())
9396
require.NoError(t, err)
97+
98+
require.Equal(t, &codersdk.GetDAUsResponse{
99+
Entries: []codersdk.DAUEntry{
100+
{
101+
102+
Date: time.Now().UTC().Truncate(time.Hour * 24),
103+
DAUs: 1,
104+
},
105+
},
106+
}, daus)
94107
}

coderd/rbac/object.go

+4
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ var (
124124
ResourceLicense = Object{
125125
Type: "license",
126126
}
127+
128+
ResourceMetrics = Object{
129+
Type: "metrics",
130+
}
127131
)
128132

129133
// Object is used to create objects for authz checks when you have none in

0 commit comments

Comments
 (0)