mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
Report session counts as a dynamic app-name map from the agent, the SSH CLI, and the workspace usage API, so a new IDE flows end to end without a schema, proto, or server change. - Report through `Stats.session_counts` on Agent API v2.11. Older agents keep reporting through the deprecated count fields. - Accept arbitrary names from `coder ssh --usage-app` and the workspace usage API. A name the registry does not recognize keeps its identity and is attributed to the `unknown` family. - Add reconnecting PTY counts to the map without overwriting the counts SSH reported. - Normalize and merge names before applying the deterministic 64-name cap, ordered by known apps, then count, then name. Overflow sums into `unknown`, and non-positive counts cannot cancel valid activity. - Strip control characters, treat whitespace-only HTTP app names as absent, and require the exact environment variable prefix when you extract an SSH session type. - Count overflow entries in `coderd_agentstats_session_counts_overflow_total` and keep raw payloads out of logs. - Change the usage API's `app_name` to a string, and update the SDK types and API reference. closes #27410
91 lines
2.6 KiB
Go
91 lines
2.6 KiB
Go
package agentapi
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
|
|
"cdr.dev/slog/v3"
|
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/workspacestats"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
type StatsAPI struct {
|
|
AgentID uuid.UUID
|
|
AgentName string
|
|
Workspace *CachedWorkspaceFields
|
|
Database database.Store
|
|
Log slog.Logger
|
|
StatsReporter *workspacestats.Reporter
|
|
AgentStatsRefreshInterval time.Duration
|
|
Experiments codersdk.Experiments
|
|
|
|
TimeNowFn func() time.Time // defaults to dbtime.Now()
|
|
}
|
|
|
|
func (a *StatsAPI) now() time.Time {
|
|
if a.TimeNowFn != nil {
|
|
return a.TimeNowFn()
|
|
}
|
|
return dbtime.Now()
|
|
}
|
|
|
|
func (a *StatsAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsRequest) (*agentproto.UpdateStatsResponse, error) {
|
|
res := &agentproto.UpdateStatsResponse{
|
|
ReportInterval: durationpb.New(a.AgentStatsRefreshInterval),
|
|
}
|
|
// An empty stat means it's just looking for the report interval.
|
|
if req.Stats == nil {
|
|
return res, nil
|
|
}
|
|
|
|
// If cache is empty (prebuild or invalid), fall back to DB
|
|
var ws database.WorkspaceIdentity
|
|
var ok bool
|
|
if ws, ok = a.Workspace.AsWorkspaceIdentity(); !ok {
|
|
w, err := a.Database.GetWorkspaceByAgentID(ctx, a.AgentID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get workspace by agent ID %q: %w", a.AgentID, err)
|
|
}
|
|
ws = database.WorkspaceIdentityFromWorkspace(w)
|
|
}
|
|
|
|
// The payload is unnormalized and unbounded until the batcher caps it, so
|
|
// log scalars rather than its contents.
|
|
a.Log.Debug(ctx, "read stats report",
|
|
slog.F("interval", a.AgentStatsRefreshInterval),
|
|
slog.F("workspace_id", ws.ID),
|
|
slog.F("connection_count", req.Stats.GetConnectionCount()),
|
|
slog.F("session_count_keys", len(req.Stats.GetSessionCounts())),
|
|
slog.F("connections_by_proto_keys", len(req.Stats.GetConnectionsByProto())),
|
|
)
|
|
|
|
if a.Experiments.Enabled(codersdk.ExperimentWorkspaceUsage) {
|
|
// while the experiment is enabled we will not report
|
|
// session stats from the agent. This is because it is
|
|
// being handled by the CLI and the postWorkspaceUsage route.
|
|
workspacestats.ClearSessionCounts(req.Stats)
|
|
}
|
|
|
|
err := a.StatsReporter.ReportAgentStats(
|
|
ctx,
|
|
a.now(),
|
|
ws,
|
|
a.AgentID,
|
|
a.AgentName,
|
|
req.Stats,
|
|
false,
|
|
)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("report agent stats: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|