mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Daily Active User Metrics (#3735)
* agent: add StatsReporter * Stabilize protoc
This commit is contained in:
@@ -218,3 +218,9 @@ func (e *Error) Error() string {
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type closeFunc func() error
|
||||
|
||||
func (c closeFunc) Close() error {
|
||||
return c()
|
||||
}
|
||||
|
||||
@@ -133,3 +133,43 @@ func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID,
|
||||
var templateVersion TemplateVersion
|
||||
return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion)
|
||||
}
|
||||
|
||||
type DAUEntry struct {
|
||||
Date time.Time `json:"date"`
|
||||
Amount int `json:"amount"`
|
||||
}
|
||||
|
||||
type TemplateDAUsResponse struct {
|
||||
Entries []DAUEntry `json:"entries"`
|
||||
}
|
||||
|
||||
func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*TemplateDAUsResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var resp TemplateDAUsResponse
|
||||
return &resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// AgentStatsReportRequest is a WebSocket request by coderd
|
||||
// to the agent for stats.
|
||||
// @typescript-ignore AgentStatsReportRequest
|
||||
type AgentStatsReportRequest struct {
|
||||
}
|
||||
|
||||
// AgentStatsReportResponse is returned for each report
|
||||
// request by the agent.
|
||||
type AgentStatsReportResponse struct {
|
||||
NumConns int64 `json:"num_comms"`
|
||||
// RxBytes is the number of received bytes.
|
||||
RxBytes int64 `json:"rx_bytes"`
|
||||
// TxBytes is the number of received bytes.
|
||||
TxBytes int64 `json:"tx_bytes"`
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"golang.org/x/net/proxy"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -528,3 +529,87 @@ func (c *Client) turnProxyDialer(ctx context.Context, httpClient *http.Client, p
|
||||
return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil
|
||||
})
|
||||
}
|
||||
|
||||
// AgentReportStats begins a stat streaming connection with the Coder server.
|
||||
// It is resilient to network failures and intermittent coderd issues.
|
||||
func (c *Client) AgentReportStats(
|
||||
ctx context.Context,
|
||||
log slog.Logger,
|
||||
stats func() *agent.Stats,
|
||||
) (io.Closer, error) {
|
||||
serverURL, err := c.URL.Parse("/api/v2/workspaceagents/me/report-stats")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse url: %w", err)
|
||||
}
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create cookie jar: %w", err)
|
||||
}
|
||||
|
||||
jar.SetCookies(serverURL, []*http.Cookie{{
|
||||
Name: SessionTokenKey,
|
||||
Value: c.SessionToken,
|
||||
}})
|
||||
|
||||
httpClient := &http.Client{
|
||||
Jar: jar,
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
|
||||
// If the agent connection succeeds for a while, then fails, then succeeds
|
||||
// for a while (etc.) the retry may hit the maximum. This is a normal
|
||||
// case for long-running agents that experience coderd upgrades, so
|
||||
// we use a short maximum retry limit.
|
||||
for r := retry.New(time.Second, time.Minute); r.Wait(ctx); {
|
||||
err = func() error {
|
||||
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
|
||||
HTTPClient: httpClient,
|
||||
// Need to disable compression to avoid a data-race.
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
if res == nil {
|
||||
return err
|
||||
}
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
|
||||
for {
|
||||
var req AgentStatsReportRequest
|
||||
err := wsjson.Read(ctx, conn, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := stats()
|
||||
|
||||
resp := AgentStatsReportResponse{
|
||||
NumConns: s.NumConns,
|
||||
RxBytes: s.RxBytes,
|
||||
TxBytes: s.TxBytes,
|
||||
}
|
||||
|
||||
err = wsjson.Write(ctx, conn, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil && ctx.Err() == nil {
|
||||
log.Error(ctx, "report stats", slog.Error(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return closeFunc(func() error {
|
||||
cancel()
|
||||
<-doneCh
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user