mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
chore: add CLI invokation telemetry (#7589)
This commit is contained in:
@ -465,6 +465,7 @@ func New(options *Options) *API {
|
||||
// Specific routes can specify different limits, but every rate
|
||||
// limit must be configurable by the admin.
|
||||
apiRateLimiter,
|
||||
httpmw.ReportCLITelemetry(api.Logger, options.Telemetry),
|
||||
)
|
||||
r.Get("/", apiRoot)
|
||||
// All CSP errors will be logged
|
||||
|
80
coderd/httpmw/clitelemetry.go
Normal file
80
coderd/httpmw/clitelemetry.go
Normal file
@ -0,0 +1,80 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstime/rate"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func ReportCLITelemetry(log slog.Logger, rep telemetry.Reporter) func(http.Handler) http.Handler {
|
||||
var (
|
||||
mu sync.Mutex
|
||||
|
||||
// We send telemetry at most once per minute.
|
||||
limiter = rate.NewLimiter(rate.Every(time.Minute), 1)
|
||||
queue []telemetry.CLIInvocation
|
||||
)
|
||||
|
||||
log = log.Named("cli-telemetry")
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// No matter what, we proceed with the request.
|
||||
defer next.ServeHTTP(rw, r)
|
||||
|
||||
payload := r.Header.Get(codersdk.CLITelemetryHeader)
|
||||
if payload == "" {
|
||||
return
|
||||
}
|
||||
|
||||
byt, err := base64.StdEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
log.Error(
|
||||
r.Context(),
|
||||
"base64 decode",
|
||||
slog.F("error", err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var inv telemetry.CLIInvocation
|
||||
err = json.Unmarshal(byt, &inv)
|
||||
if err != nil {
|
||||
log.Error(
|
||||
r.Context(),
|
||||
"unmarshal header",
|
||||
slog.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// We do expensive work in a goroutine so we don't block the
|
||||
// request.
|
||||
go func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
queue = append(queue, inv)
|
||||
if !limiter.Allow() && len(queue) < 1024 {
|
||||
return
|
||||
}
|
||||
rep.Report(&telemetry.Snapshot{
|
||||
CLIInvocations: queue,
|
||||
})
|
||||
log.Debug(
|
||||
r.Context(),
|
||||
"report sent", slog.F("count", len(queue)),
|
||||
)
|
||||
queue = queue[:0]
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
@ -701,6 +701,7 @@ type Snapshot struct {
|
||||
WorkspaceBuilds []WorkspaceBuild `json:"workspace_build"`
|
||||
WorkspaceResources []WorkspaceResource `json:"workspace_resources"`
|
||||
WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"`
|
||||
CLIInvocations []CLIInvocation `json:"cli_invocations"`
|
||||
}
|
||||
|
||||
// Deployment contains information about the host running Coder.
|
||||
@ -876,6 +877,18 @@ type License struct {
|
||||
UUID uuid.UUID `json:"uuid"`
|
||||
}
|
||||
|
||||
type CLIOption struct {
|
||||
Name string `json:"name"`
|
||||
ValueSource string `json:"value_source"`
|
||||
}
|
||||
|
||||
type CLIInvocation struct {
|
||||
Command string `json:"command"`
|
||||
Options []CLIOption `json:"options"`
|
||||
// InvokedAt is provided for deduplication purposes.
|
||||
InvokedAt time.Time `json:"invoked_at"`
|
||||
}
|
||||
|
||||
type noopReporter struct{}
|
||||
|
||||
func (*noopReporter) Report(_ *Snapshot) {}
|
||||
|
Reference in New Issue
Block a user