feat(coderd): add coder_app usage stats (#9001)

Updates #8658
This commit is contained in:
Mathias Fredriksson
2023-08-16 15:22:00 +03:00
committed by GitHub
parent f3c707648c
commit 6fd9975aed
31 changed files with 1730 additions and 62 deletions
+1
View File
@@ -167,6 +167,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
)
r.Get("/coordinate", api.workspaceProxyCoordinate)
r.Post("/issue-signed-app-token", api.workspaceProxyIssueSignedAppToken)
r.Post("/app-stats", api.workspaceProxyReportAppStats)
r.Post("/register", api.workspaceProxyRegister)
r.Post("/deregister", api.workspaceProxyDeregister)
})
@@ -110,6 +110,10 @@ func NewWorkspaceProxy(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Clie
})
require.NoError(t, err, "failed to create workspace proxy")
// Inherit collector options from coderd, but keep the wsproxy reporter.
statsCollectorOptions := coderdAPI.Options.WorkspaceAppsStatsCollectorOptions
statsCollectorOptions.Reporter = nil
wssrv, err := wsproxy.New(ctx, &wsproxy.Options{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
Experiments: options.Experiments,
@@ -129,6 +133,7 @@ func NewWorkspaceProxy(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Clie
DERPEnabled: !options.DerpDisabled,
DERPOnly: options.DerpOnly,
DERPServerRelayAddress: accessURL.String(),
StatsCollectorOptions: statsCollectorOptions,
})
require.NoError(t, err)
t.Cleanup(func() {
+30
View File
@@ -497,6 +497,36 @@ func (api *API) workspaceProxyIssueSignedAppToken(rw http.ResponseWriter, r *htt
})
}
// @Summary Report workspace app stats
// @ID report-workspace-app-stats
// @Security CoderSessionToken
// @Accept json
// @Tags Enterprise
// @Param request body wsproxysdk.ReportAppStatsRequest true "Report app stats request"
// @Success 204
// @Router /workspaceproxies/me/app-stats [post]
// @x-apidocgen {"skip": true}
func (api *API) workspaceProxyReportAppStats(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = httpmw.WorkspaceProxy(r) // Ensure the proxy is authenticated.
var req wsproxysdk.ReportAppStatsRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
api.Logger.Debug(ctx, "report app stats", slog.F("stats", req.Stats))
reporter := api.WorkspaceAppsStatsCollectorOptions.Reporter
if err := reporter.Report(ctx, req.Stats); err != nil {
api.Logger.Error(ctx, "report app stats failed", slog.Error(err))
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
}
// workspaceProxyRegister is used to register a new workspace proxy. When a proxy
// comes online, it will announce itself to this endpoint. This updates its values
// in the database and returns a signed token that can be used to authenticate
+21
View File
@@ -0,0 +1,21 @@
package wsproxy
import (
"context"
"github.com/coder/coder/coderd/workspaceapps"
"github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
)
var _ workspaceapps.StatsReporter = (*appStatsReporter)(nil)
type appStatsReporter struct {
Client *wsproxysdk.Client
}
func (r *appStatsReporter) Report(ctx context.Context, stats []workspaceapps.StatsReport) error {
err := r.Client.ReportAppStats(ctx, wsproxysdk.ReportAppStatsRequest{
Stats: stats,
})
return err
}
+15 -2
View File
@@ -79,6 +79,8 @@ type Options struct {
// By default, CORs is set to accept external requests
// from the dashboardURL. This should only be used in development.
AllowAllCors bool
StatsCollectorOptions workspaceapps.StatsCollectorOptions
}
func (o *Options) Validate() error {
@@ -262,8 +264,17 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
}
}
workspaceAppsLogger := opts.Logger.Named("workspaceapps")
if opts.StatsCollectorOptions.Logger == nil {
named := workspaceAppsLogger.Named("stats_collector")
opts.StatsCollectorOptions.Logger = &named
}
if opts.StatsCollectorOptions.Reporter == nil {
opts.StatsCollectorOptions.Reporter = &appStatsReporter{Client: client}
}
s.AppServer = &workspaceapps.Server{
Logger: opts.Logger.Named("workspaceapps"),
Logger: workspaceAppsLogger,
DashboardURL: opts.DashboardURL,
AccessURL: opts.AccessURL,
Hostname: opts.AppHostname,
@@ -279,9 +290,11 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
},
AppSecurityKey: secKey,
AgentProvider: agentProvider,
DisablePathApps: opts.DisablePathApps,
SecureAuthCookie: opts.SecureAuthCookie,
AgentProvider: agentProvider,
StatsCollector: workspaceapps.NewStatsCollector(opts.StatsCollectorOptions),
}
derpHandler := derphttp.Handler(derpServer)
+2
View File
@@ -478,6 +478,7 @@ func TestWorkspaceProxyWorkspaceApps_Wsconncache(t *testing.T) {
"CF-Connecting-IP",
},
},
WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
@@ -536,6 +537,7 @@ func TestWorkspaceProxyWorkspaceApps_SingleTailnet(t *testing.T) {
"CF-Connecting-IP",
},
},
WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
@@ -152,6 +152,25 @@ func (c *Client) IssueSignedAppTokenHTML(ctx context.Context, rw http.ResponseWr
return res, true
}
type ReportAppStatsRequest struct {
Stats []workspaceapps.StatsReport `json:"stats"`
}
// ReportAppStats reports the given app stats to the primary coder server.
func (c *Client) ReportAppStats(ctx context.Context, req ReportAppStatsRequest) error {
resp, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceproxies/me/app-stats", req)
if err != nil {
return xerrors.Errorf("make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return codersdk.ReadBodyAsError(resp)
}
return nil
}
type RegisterWorkspaceProxyRequest struct {
// AccessURL that hits the workspace proxy api.
AccessURL string `json:"access_url"`