mirror of https://github.com/coder/coder.git
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:
parent
537547fcc3
commit
2ff1c6d613
184
agent/agent.go
184
agent/agent.go
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue