feat: add agent stats for different connection types (#6412)

This allows us to track when our extensions are used, when the
web terminal is used, and average connection latency to the agent.
This commit is contained in:
Kyle Carberry 2023-03-02 08:06:00 -06:00 committed by GitHub
parent 537547fcc3
commit 2ff1c6d613
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 412 additions and 131 deletions

View File

@ -18,6 +18,7 @@ import (
"os/user"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
@ -56,6 +57,14 @@ const (
// command just returning a nonzero exit code, and is chosen as an arbitrary, high number
// unlikely to shadow other exit codes, which are typically 1, 2, 3, etc.
MagicSessionErrorCode = 229
// MagicSSHSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection.
// This is stripped from any commands being executed, and is counted towards connection stats.
MagicSSHSessionTypeEnvironmentVariable = "__CODER_SSH_SESSION_TYPE"
// MagicSSHSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself.
MagicSSHSessionTypeVSCode = "vscode"
// MagicSSHSessionTypeJetBrains is set in the SSH config by the JetBrains extension to identify itself.
MagicSSHSessionTypeJetBrains = "jetbrains"
)
type Options struct {
@ -146,6 +155,15 @@ type agent struct {
network *tailnet.Conn
connStatsChan chan *agentsdk.Stats
statRxPackets atomic.Int64
statRxBytes atomic.Int64
statTxPackets atomic.Int64
statTxBytes atomic.Int64
connCountVSCode atomic.Int64
connCountJetBrains atomic.Int64
connCountReconnectingPTY atomic.Int64
connCountSSHSession atomic.Int64
}
// runLoop attempts to start the agent in a retry loop.
@ -350,33 +368,7 @@ func (a *agent) run(ctx context.Context) error {
return xerrors.New("agent is closed")
}
setStatInterval := func(d time.Duration) {
network.SetConnStatsCallback(d, 2048,
func(_, _ time.Time, virtual, _ map[netlogtype.Connection]netlogtype.Counts) {
select {
case a.connStatsChan <- convertAgentStats(virtual):
default:
a.logger.Warn(ctx, "network stat dropped")
}
},
)
}
// Report statistics from the created network.
cl, err := a.client.ReportStats(ctx, a.logger, a.connStatsChan, setStatInterval)
if err != nil {
a.logger.Error(ctx, "report stats", slog.Error(err))
} else {
if err = a.trackConnGoroutine(func() {
// This is OK because the agent never re-creates the tailnet
// and the only shutdown indicator is agent.Close().
<-a.closed
_ = cl.Close()
}); err != nil {
a.logger.Debug(ctx, "report stats goroutine", slog.Error(err))
_ = cl.Close()
}
}
a.startReportingConnectionStats(ctx)
} else {
// Update the DERP map!
network.SetDERPMap(metadata.DERPMap)
@ -765,23 +757,6 @@ func (a *agent) init(ctx context.Context) {
go a.runLoop(ctx)
}
func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *agentsdk.Stats {
stats := &agentsdk.Stats{
ConnectionsByProto: map[string]int64{},
ConnectionCount: int64(len(counts)),
}
for conn, count := range counts {
stats.ConnectionsByProto[conn.Proto.String()]++
stats.RxPackets += int64(count.RxPackets)
stats.RxBytes += int64(count.RxBytes)
stats.TxPackets += int64(count.TxPackets)
stats.TxBytes += int64(count.TxBytes)
}
return stats
}
// createCommand processes raw command input with OpenSSH-like behavior.
// If the rawCommand provided is empty, it will default to the users shell.
// This injects environment variables specified by the user at launch too.
@ -892,7 +867,27 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
ctx := session.Context()
cmd, err := a.createCommand(ctx, session.RawCommand(), session.Environ())
env := session.Environ()
var magicType string
for index, kv := range env {
if !strings.HasPrefix(kv, MagicSSHSessionTypeEnvironmentVariable) {
continue
}
magicType = strings.TrimPrefix(kv, MagicSSHSessionTypeEnvironmentVariable+"=")
env = append(env[:index], env[index+1:]...)
}
switch magicType {
case MagicSSHSessionTypeVSCode:
a.connCountVSCode.Add(1)
case MagicSSHSessionTypeJetBrains:
a.connCountJetBrains.Add(1)
case "":
a.connCountSSHSession.Add(1)
default:
a.logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("type", magicType))
}
cmd, err := a.createCommand(ctx, session.RawCommand(), env)
if err != nil {
return err
}
@ -990,6 +985,8 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, msg codersdk.WorkspaceAgentReconnectingPTYInit, conn net.Conn) (retErr error) {
defer conn.Close()
a.connCountReconnectingPTY.Add(1)
connectionID := uuid.NewString()
logger = logger.With(slog.F("id", msg.ID), slog.F("connection_id", connectionID))
@ -1180,6 +1177,103 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, m
}
}
// startReportingConnectionStats runs the connection stats reporting goroutine.
func (a *agent) startReportingConnectionStats(ctx context.Context) {
reportStats := func(networkStats map[netlogtype.Connection]netlogtype.Counts) {
stats := &agentsdk.Stats{
ConnectionCount: int64(len(networkStats)),
ConnectionsByProto: map[string]int64{},
}
// Tailscale resets counts on every report!
// We'd rather have these compound, like Linux does!
for conn, counts := range networkStats {
stats.ConnectionsByProto[conn.Proto.String()]++
stats.RxBytes = a.statRxBytes.Add(int64(counts.RxBytes))
stats.RxPackets = a.statRxPackets.Add(int64(counts.RxPackets))
stats.TxBytes = a.statTxBytes.Add(int64(counts.TxBytes))
stats.TxPackets = a.statTxPackets.Add(int64(counts.TxPackets))
}
// Tailscale's connection stats are not cumulative, but it makes no sense to make
// ours temporary.
stats.SessionCountSSH = a.connCountSSHSession.Load()
stats.SessionCountVSCode = a.connCountVSCode.Load()
stats.SessionCountJetBrains = a.connCountJetBrains.Load()
stats.SessionCountReconnectingPTY = a.connCountReconnectingPTY.Load()
// Compute the median connection latency!
var wg sync.WaitGroup
var mu sync.Mutex
status := a.network.Status()
durations := []float64{}
ctx, cancelFunc := context.WithTimeout(ctx, 5*time.Second)
defer cancelFunc()
for nodeID, peer := range status.Peer {
if !peer.Active {
continue
}
addresses, found := a.network.NodeAddresses(nodeID)
if !found {
continue
}
if len(addresses) == 0 {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
duration, _, _, err := a.network.Ping(ctx, addresses[0].Addr())
if err != nil {
return
}
mu.Lock()
durations = append(durations, float64(duration.Microseconds()))
mu.Unlock()
}()
}
wg.Wait()
sort.Float64s(durations)
durationsLength := len(durations)
if durationsLength == 0 {
stats.ConnectionMedianLatencyMS = -1
} else if durationsLength%2 == 0 {
stats.ConnectionMedianLatencyMS = (durations[durationsLength/2-1] + durations[durationsLength/2]) / 2
} else {
stats.ConnectionMedianLatencyMS = durations[durationsLength/2]
}
// Convert from microseconds to milliseconds.
stats.ConnectionMedianLatencyMS /= 1000
select {
case a.connStatsChan <- stats:
default:
a.logger.Warn(ctx, "network stat dropped")
}
}
// Report statistics from the created network.
cl, err := a.client.ReportStats(ctx, a.logger, a.connStatsChan, func(d time.Duration) {
a.network.SetConnStatsCallback(d, 2048,
func(_, _ time.Time, virtual, _ map[netlogtype.Connection]netlogtype.Counts) {
reportStats(virtual)
},
)
})
if err != nil {
a.logger.Error(ctx, "report stats", slog.Error(err))
} else {
if err = a.trackConnGoroutine(func() {
// This is OK because the agent never re-creates the tailnet
// and the only shutdown indicator is agent.Close().
<-a.closed
_ = cl.Close()
}); err != nil {
a.logger.Debug(ctx, "report stats goroutine", slog.Error(err))
_ = cl.Close()
}
}
}
// isClosed returns whether the API is closed or not.
func (a *agent) isClosed() bool {
select {

View File

@ -73,7 +73,7 @@ func TestAgent_Stats_SSH(t *testing.T) {
require.Eventuallyf(t, func() bool {
var ok bool
s, ok = <-stats
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && s.SessionCountSSH == 1
}, testutil.WaitLong, testutil.IntervalFast,
"never saw stats: %+v", s,
)
@ -102,7 +102,47 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) {
require.Eventuallyf(t, func() bool {
var ok bool
s, ok = <-stats
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && s.SessionCountReconnectingPTY == 1
}, testutil.WaitLong, testutil.IntervalFast,
"never saw stats: %+v", s,
)
}
func TestAgent_Stats_Magic(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0)
sshClient, err := conn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
session, err := sshClient.NewSession()
require.NoError(t, err)
session.Setenv(agent.MagicSSHSessionTypeEnvironmentVariable, agent.MagicSSHSessionTypeVSCode)
defer session.Close()
command := "sh -c 'echo $" + agent.MagicSSHSessionTypeEnvironmentVariable + "'"
expected := ""
if runtime.GOOS == "windows" {
expected = "%" + agent.MagicSSHSessionTypeEnvironmentVariable + "%"
command = "cmd.exe /c echo " + expected
}
output, err := session.Output(command)
require.NoError(t, err)
require.Equal(t, expected, strings.TrimSpace(string(output)))
var s *agentsdk.Stats
require.Eventuallyf(t, func() bool {
var ok bool
s, ok = <-stats
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 &&
// Ensure that the connection didn't count as a "normal" SSH session.
// This was a special one, so it should be labeled specially in the stats!
s.SessionCountVSCode == 1 &&
// Ensure that connection latency is being counted!
// If it isn't, it's set to -1.
s.ConnectionMedianLatencyMS >= 0
}, testutil.WaitLong, testutil.IntervalFast,
"never saw stats: %+v", s,
)

30
coderd/apidoc/docs.go generated
View File

@ -5201,17 +5201,21 @@ const docTemplate = `{
"agentsdk.Stats": {
"type": "object",
"properties": {
"conns_by_proto": {
"connection_count": {
"description": "ConnectionCount is the number of connections received by an agent.",
"type": "integer"
},
"connection_median_latency_ms": {
"description": "ConnectionMedianLatencyMS is the median latency of all connections in milliseconds.",
"type": "number"
},
"connections_by_proto": {
"description": "ConnectionsByProto is a count of connections by protocol.",
"type": "object",
"additionalProperties": {
"type": "integer"
}
},
"num_comms": {
"description": "ConnectionCount is the number of connections received by an agent.",
"type": "integer"
},
"rx_bytes": {
"description": "RxBytes is the number of received bytes.",
"type": "integer"
@ -5220,6 +5224,22 @@ const docTemplate = `{
"description": "RxPackets is the number of received packets.",
"type": "integer"
},
"session_count_jetbrains": {
"description": "SessionCountJetBrains is the number of connections received by an agent\nthat are from our JetBrains extension.",
"type": "integer"
},
"session_count_reconnecting_pty": {
"description": "SessionCountReconnectingPTY is the number of connections received by an agent\nthat are from the reconnecting web terminal.",
"type": "integer"
},
"session_count_ssh": {
"description": "SessionCountSSH is the number of connections received by an agent\nthat are normal, non-tagged SSH sessions.",
"type": "integer"
},
"session_count_vscode": {
"description": "SessionCountVSCode is the number of connections received by an agent\nthat are from our VS Code extension.",
"type": "integer"
},
"tx_bytes": {
"description": "TxBytes is the number of transmitted bytes.",
"type": "integer"

View File

@ -4594,17 +4594,21 @@
"agentsdk.Stats": {
"type": "object",
"properties": {
"conns_by_proto": {
"connection_count": {
"description": "ConnectionCount is the number of connections received by an agent.",
"type": "integer"
},
"connection_median_latency_ms": {
"description": "ConnectionMedianLatencyMS is the median latency of all connections in milliseconds.",
"type": "number"
},
"connections_by_proto": {
"description": "ConnectionsByProto is a count of connections by protocol.",
"type": "object",
"additionalProperties": {
"type": "integer"
}
},
"num_comms": {
"description": "ConnectionCount is the number of connections received by an agent.",
"type": "integer"
},
"rx_bytes": {
"description": "RxBytes is the number of received bytes.",
"type": "integer"
@ -4613,6 +4617,22 @@
"description": "RxPackets is the number of received packets.",
"type": "integer"
},
"session_count_jetbrains": {
"description": "SessionCountJetBrains is the number of connections received by an agent\nthat are from our JetBrains extension.",
"type": "integer"
},
"session_count_reconnecting_pty": {
"description": "SessionCountReconnectingPTY is the number of connections received by an agent\nthat are from the reconnecting web terminal.",
"type": "integer"
},
"session_count_ssh": {
"description": "SessionCountSSH is the number of connections received by an agent\nthat are normal, non-tagged SSH sessions.",
"type": "integer"
},
"session_count_vscode": {
"description": "SessionCountVSCode is the number of connections received by an agent\nthat are from our VS Code extension.",
"type": "integer"
},
"tx_bytes": {
"description": "TxBytes is the number of transmitted bytes.",
"type": "integer"

View File

@ -272,18 +272,23 @@ func (q *fakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.Ins
defer q.mutex.Unlock()
stat := database.WorkspaceAgentStat{
ID: p.ID,
CreatedAt: p.CreatedAt,
WorkspaceID: p.WorkspaceID,
AgentID: p.AgentID,
UserID: p.UserID,
ConnectionsByProto: p.ConnectionsByProto,
ConnectionCount: p.ConnectionCount,
RxPackets: p.RxPackets,
RxBytes: p.RxBytes,
TxPackets: p.TxPackets,
TxBytes: p.TxBytes,
TemplateID: p.TemplateID,
ID: p.ID,
CreatedAt: p.CreatedAt,
WorkspaceID: p.WorkspaceID,
AgentID: p.AgentID,
UserID: p.UserID,
ConnectionsByProto: p.ConnectionsByProto,
ConnectionCount: p.ConnectionCount,
RxPackets: p.RxPackets,
RxBytes: p.RxBytes,
TxPackets: p.TxPackets,
TxBytes: p.TxBytes,
TemplateID: p.TemplateID,
SessionCountVSCode: p.SessionCountVSCode,
SessionCountJetBrains: p.SessionCountJetBrains,
SessionCountReconnectingPTY: p.SessionCountReconnectingPTY,
SessionCountSSH: p.SessionCountSSH,
ConnectionMedianLatencyMS: p.ConnectionMedianLatencyMS,
}
q.workspaceAgentStats = append(q.workspaceAgentStats, stat)
return stat, nil

View File

@ -474,7 +474,12 @@ CREATE TABLE workspace_agent_stats (
rx_packets bigint DEFAULT 0 NOT NULL,
rx_bytes bigint DEFAULT 0 NOT NULL,
tx_packets bigint DEFAULT 0 NOT NULL,
tx_bytes bigint DEFAULT 0 NOT NULL
tx_bytes bigint DEFAULT 0 NOT NULL,
connection_median_latency_ms bigint DEFAULT '-1'::integer NOT NULL,
session_count_vscode bigint DEFAULT 0 NOT NULL,
session_count_jetbrains bigint DEFAULT 0 NOT NULL,
session_count_reconnecting_pty bigint DEFAULT 0 NOT NULL,
session_count_ssh bigint DEFAULT 0 NOT NULL
);
CREATE TABLE workspace_agents (

View File

@ -0,0 +1,5 @@
ALTER TABLE workspace_agent_stats DROP COLUMN session_count_vscode,
DROP COLUMN session_count_jetbrains,
DROP COLUMN session_count_reconnecting_pty,
DROP COLUMN session_count_ssh,
DROP COLUMN connection_median_latency_ms;

View File

@ -0,0 +1,5 @@
ALTER TABLE workspace_agent_stats ADD COLUMN connection_median_latency_ms bigint DEFAULT -1 NOT NULL;
ALTER TABLE workspace_agent_stats ADD COLUMN session_count_vscode bigint DEFAULT 0 NOT NULL;
ALTER TABLE workspace_agent_stats ADD COLUMN session_count_jetbrains bigint DEFAULT 0 NOT NULL;
ALTER TABLE workspace_agent_stats ADD COLUMN session_count_reconnecting_pty bigint DEFAULT 0 NOT NULL;
ALTER TABLE workspace_agent_stats ADD COLUMN session_count_ssh bigint DEFAULT 0 NOT NULL;

View File

@ -1549,18 +1549,23 @@ type WorkspaceAgent struct {
}
type WorkspaceAgentStat struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
ConnectionsByProto json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"`
ConnectionCount int64 `db:"connection_count" json:"connection_count"`
RxPackets int64 `db:"rx_packets" json:"rx_packets"`
RxBytes int64 `db:"rx_bytes" json:"rx_bytes"`
TxPackets int64 `db:"tx_packets" json:"tx_packets"`
TxBytes int64 `db:"tx_bytes" json:"tx_bytes"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
ConnectionsByProto json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"`
ConnectionCount int64 `db:"connection_count" json:"connection_count"`
RxPackets int64 `db:"rx_packets" json:"rx_packets"`
RxBytes int64 `db:"rx_bytes" json:"rx_bytes"`
TxPackets int64 `db:"tx_packets" json:"tx_packets"`
TxBytes int64 `db:"tx_bytes" json:"tx_bytes"`
ConnectionMedianLatencyMS int64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
}
type WorkspaceApp struct {

View File

@ -5416,25 +5416,35 @@ INSERT INTO
rx_packets,
rx_bytes,
tx_packets,
tx_bytes
tx_bytes,
session_count_vscode,
session_count_jetbrains,
session_count_reconnecting_pty,
session_count_ssh,
connection_median_latency_ms
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh
`
type InsertWorkspaceAgentStatParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
ConnectionsByProto json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"`
ConnectionCount int64 `db:"connection_count" json:"connection_count"`
RxPackets int64 `db:"rx_packets" json:"rx_packets"`
RxBytes int64 `db:"rx_bytes" json:"rx_bytes"`
TxPackets int64 `db:"tx_packets" json:"tx_packets"`
TxBytes int64 `db:"tx_bytes" json:"tx_bytes"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
ConnectionsByProto json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"`
ConnectionCount int64 `db:"connection_count" json:"connection_count"`
RxPackets int64 `db:"rx_packets" json:"rx_packets"`
RxBytes int64 `db:"rx_bytes" json:"rx_bytes"`
TxPackets int64 `db:"tx_packets" json:"tx_packets"`
TxBytes int64 `db:"tx_bytes" json:"tx_bytes"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
ConnectionMedianLatencyMS int64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
}
func (q *sqlQuerier) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) {
@ -5451,6 +5461,11 @@ func (q *sqlQuerier) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWor
arg.RxBytes,
arg.TxPackets,
arg.TxBytes,
arg.SessionCountVSCode,
arg.SessionCountJetBrains,
arg.SessionCountReconnectingPTY,
arg.SessionCountSSH,
arg.ConnectionMedianLatencyMS,
)
var i WorkspaceAgentStat
err := row.Scan(
@ -5466,6 +5481,11 @@ func (q *sqlQuerier) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWor
&i.RxBytes,
&i.TxPackets,
&i.TxBytes,
&i.ConnectionMedianLatencyMS,
&i.SessionCountVSCode,
&i.SessionCountJetBrains,
&i.SessionCountReconnectingPTY,
&i.SessionCountSSH,
)
return i, err
}

View File

@ -12,10 +12,15 @@ INSERT INTO
rx_packets,
rx_bytes,
tx_packets,
tx_bytes
tx_bytes,
session_count_vscode,
session_count_jetbrains,
session_count_reconnecting_pty,
session_count_ssh,
connection_median_latency_ms
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING *;
-- name: GetTemplateDAUs :many
SELECT

View File

@ -25,6 +25,11 @@ overrides:
api_key_scope_all: APIKeyScopeAll
api_key_scope_application_connect: APIKeyScopeApplicationConnect
avatar_url: AvatarURL
session_count_vscode: SessionCountVSCode
session_count_jetbrains: SessionCountJetBrains
session_count_reconnecting_pty: SessionCountReconnectingPTY
session_count_ssh: SessionCountSSH
connection_median_latency_ms: ConnectionMedianLatencyMS
login_type_oidc: LoginTypeOIDC
oauth_access_token: OAuthAccessToken
oauth_expiry: OAuthExpiry

View File

@ -941,18 +941,23 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
now := database.Now()
_, err = api.Database.InsertWorkspaceAgentStat(ctx, database.InsertWorkspaceAgentStatParams{
ID: uuid.New(),
CreatedAt: now,
AgentID: workspaceAgent.ID,
WorkspaceID: workspace.ID,
UserID: workspace.OwnerID,
TemplateID: workspace.TemplateID,
ConnectionsByProto: payload,
ConnectionCount: req.ConnectionCount,
RxPackets: req.RxPackets,
RxBytes: req.RxBytes,
TxPackets: req.TxPackets,
TxBytes: req.TxBytes,
ID: uuid.New(),
CreatedAt: now,
AgentID: workspaceAgent.ID,
WorkspaceID: workspace.ID,
UserID: workspace.OwnerID,
TemplateID: workspace.TemplateID,
ConnectionsByProto: payload,
ConnectionCount: req.ConnectionCount,
RxPackets: req.RxPackets,
RxBytes: req.RxBytes,
TxPackets: req.TxPackets,
TxBytes: req.TxBytes,
SessionCountVSCode: req.SessionCountVSCode,
SessionCountJetBrains: req.SessionCountJetBrains,
SessionCountReconnectingPTY: req.SessionCountReconnectingPTY,
SessionCountSSH: req.SessionCountSSH,
ConnectionMedianLatencyMS: int64(req.ConnectionMedianLatencyMS),
})
if err != nil {
httpapi.InternalServerError(rw, err)

View File

@ -1178,12 +1178,17 @@ func TestWorkspaceAgentReportStats(t *testing.T) {
agentClient.SetSessionToken(authToken)
_, err := agentClient.PostStats(context.Background(), &agentsdk.Stats{
ConnectionsByProto: map[string]int64{"TCP": 1},
ConnectionCount: 1,
RxPackets: 1,
RxBytes: 1,
TxPackets: 1,
TxBytes: 1,
ConnectionsByProto: map[string]int64{"TCP": 1},
ConnectionCount: 1,
RxPackets: 1,
RxBytes: 1,
TxPackets: 1,
TxBytes: 1,
SessionCountVSCode: 1,
SessionCountJetBrains: 1,
SessionCountReconnectingPTY: 1,
SessionCountSSH: 1,
ConnectionMedianLatencyMS: 10,
})
require.NoError(t, err)

View File

@ -427,9 +427,11 @@ func (c *Client) ReportStats(ctx context.Context, log slog.Logger, statsChan <-c
// user-facing metrics and debugging.
type Stats struct {
// ConnectionsByProto is a count of connections by protocol.
ConnectionsByProto map[string]int64 `json:"conns_by_proto"`
ConnectionsByProto map[string]int64 `json:"connections_by_proto"`
// ConnectionCount is the number of connections received by an agent.
ConnectionCount int64 `json:"num_comms"`
ConnectionCount int64 `json:"connection_count"`
// ConnectionMedianLatencyMS is the median latency of all connections in milliseconds.
ConnectionMedianLatencyMS float64 `json:"connection_median_latency_ms"`
// RxPackets is the number of received packets.
RxPackets int64 `json:"rx_packets"`
// RxBytes is the number of received bytes.
@ -438,6 +440,19 @@ type Stats struct {
TxPackets int64 `json:"tx_packets"`
// TxBytes is the number of transmitted bytes.
TxBytes int64 `json:"tx_bytes"`
// SessionCountVSCode is the number of connections received by an agent
// that are from our VS Code extension.
SessionCountVSCode int64 `json:"session_count_vscode"`
// SessionCountJetBrains is the number of connections received by an agent
// that are from our JetBrains extension.
SessionCountJetBrains int64 `json:"session_count_jetbrains"`
// SessionCountReconnectingPTY is the number of connections received by an agent
// that are from the reconnecting web terminal.
SessionCountReconnectingPTY int64 `json:"session_count_reconnecting_pty"`
// SessionCountSSH is the number of connections received by an agent
// that are normal, non-tagged SSH sessions.
SessionCountSSH int64 `json:"session_count_ssh"`
}
type StatsResponse struct {

View File

@ -401,13 +401,18 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/report-stats \
```json
{
"conns_by_proto": {
"connection_count": 0,
"connection_median_latency_ms": 0,
"connections_by_proto": {
"property1": 0,
"property2": 0
},
"num_comms": 0,
"rx_bytes": 0,
"rx_packets": 0,
"session_count_jetbrains": 0,
"session_count_reconnecting_pty": 0,
"session_count_ssh": 0,
"session_count_vscode": 0,
"tx_bytes": 0,
"tx_packets": 0
}

View File

@ -248,13 +248,18 @@
```json
{
"conns_by_proto": {
"connection_count": 0,
"connection_median_latency_ms": 0,
"connections_by_proto": {
"property1": 0,
"property2": 0
},
"num_comms": 0,
"rx_bytes": 0,
"rx_packets": 0,
"session_count_jetbrains": 0,
"session_count_reconnecting_pty": 0,
"session_count_ssh": 0,
"session_count_vscode": 0,
"tx_bytes": 0,
"tx_packets": 0
}
@ -262,15 +267,20 @@
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------------ | ------- | -------- | ------------ | ------------------------------------------------------------ |
| `conns_by_proto` | object | false | | Conns by proto is a count of connections by protocol. |
| » `[any property]` | integer | false | | |
| `num_comms` | integer | false | | Num comms is the number of connections received by an agent. |
| `rx_bytes` | integer | false | | Rx bytes is the number of received bytes. |
| `rx_packets` | integer | false | | Rx packets is the number of received packets. |
| `tx_bytes` | integer | false | | Tx bytes is the number of transmitted bytes. |
| `tx_packets` | integer | false | | Tx packets is the number of transmitted bytes. |
| Name | Type | Required | Restrictions | Description |
| -------------------------------- | ------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `connection_count` | integer | false | | Connection count is the number of connections received by an agent. |
| `connection_median_latency_ms` | number | false | | Connection median latency ms is the median latency of all connections in milliseconds. |
| `connections_by_proto` | object | false | | Connections by proto is a count of connections by protocol. |
| » `[any property]` | integer | false | | |
| `rx_bytes` | integer | false | | Rx bytes is the number of received bytes. |
| `rx_packets` | integer | false | | Rx packets is the number of received packets. |
| `session_count_jetbrains` | integer | false | | Session count jetbrains is the number of connections received by an agent that are from our JetBrains extension. |
| `session_count_reconnecting_pty` | integer | false | | Session count reconnecting pty is the number of connections received by an agent that are from the reconnecting web terminal. |
| `session_count_ssh` | integer | false | | Session count ssh is the number of connections received by an agent that are normal, non-tagged SSH sessions. |
| `session_count_vscode` | integer | false | | Session count vscode is the number of connections received by an agent that are from our VS Code extension. |
| `tx_bytes` | integer | false | | Tx bytes is the number of transmitted bytes. |
| `tx_packets` | integer | false | | Tx packets is the number of transmitted bytes. |
## agentsdk.StatsResponse

View File

@ -460,6 +460,18 @@ func (c *Conn) UpdateNodes(nodes []*Node, replacePeers bool) error {
return nil
}
// NodeAddresses returns the addresses of a node from the NetworkMap.
func (c *Conn) NodeAddresses(publicKey key.NodePublic) ([]netip.Prefix, bool) {
c.mutex.Lock()
defer c.mutex.Unlock()
for _, node := range c.netMap.Peers {
if node.Key == publicKey {
return node.Addresses, true
}
}
return nil, false
}
// Status returns the current ipnstate of a connection.
func (c *Conn) Status() *ipnstate.Status {
sb := &ipnstate.StatusBuilder{WantPeers: true}