mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
Daily Active User Metrics (#3735)
* agent: add StatsReporter * Stabilize protoc
This commit is contained in:
@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -18,6 +19,7 @@ import (
|
||||
"golang.org/x/mod/semver"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@ -745,6 +747,130 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator *tailnet.Coordi
|
||||
|
||||
return workspaceAgent, nil
|
||||
}
|
||||
func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) {
|
||||
api.websocketWaitMutex.Lock()
|
||||
api.websocketWaitGroup.Add(1)
|
||||
api.websocketWaitMutex.Unlock()
|
||||
defer api.websocketWaitGroup.Done()
|
||||
|
||||
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), workspaceAgent.ResourceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to get workspace resource.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
build, err := api.Database.GetWorkspaceBuildByJobID(r.Context(), resource.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to get build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), build.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to get workspace.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to accept websocket.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer conn.Close(websocket.StatusAbnormalClosure, "")
|
||||
|
||||
// Allow overriding the stat interval for debugging and testing purposes.
|
||||
ctx := r.Context()
|
||||
timer := time.NewTicker(api.AgentStatsRefreshInterval)
|
||||
var lastReport codersdk.AgentStatsReportResponse
|
||||
for {
|
||||
err := wsjson.Write(ctx, conn, codersdk.AgentStatsReportRequest{})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to write report request.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
var rep codersdk.AgentStatsReportResponse
|
||||
|
||||
err = wsjson.Read(ctx, conn, &rep)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to read report response.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
repJSON, err := json.Marshal(rep)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to marshal stat json.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Avoid inserting duplicate rows to preserve DB space.
|
||||
// We will see duplicate reports when on idle connections
|
||||
// (e.g. web terminal left open) or when there are no connections at
|
||||
// all.
|
||||
var insert = !reflect.DeepEqual(lastReport, rep)
|
||||
|
||||
api.Logger.Debug(ctx, "read stats report",
|
||||
slog.F("interval", api.AgentStatsRefreshInterval),
|
||||
slog.F("agent", workspaceAgent.ID),
|
||||
slog.F("resource", resource.ID),
|
||||
slog.F("workspace", workspace.ID),
|
||||
slog.F("insert", insert),
|
||||
slog.F("payload", rep),
|
||||
)
|
||||
|
||||
if insert {
|
||||
lastReport = rep
|
||||
|
||||
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: time.Now(),
|
||||
AgentID: workspaceAgent.ID,
|
||||
WorkspaceID: build.WorkspaceID,
|
||||
UserID: workspace.OwnerID,
|
||||
TemplateID: workspace.TemplateID,
|
||||
Payload: json.RawMessage(repJSON),
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to insert agent stat.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
conn.Close(websocket.StatusNormalClosure, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func
|
||||
// is called if a read or write error is encountered.
|
||||
|
Reference in New Issue
Block a user