From d9a4986a586058acbeb6b81a96dbc734161379b0 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Fri, 10 Jul 2026 21:32:45 +0000 Subject: [PATCH 1/4] feat: normalize workspace agent session counts into a child table Adding a new IDE session type currently requires touching 15+ files across the stack. This replaces the fixed session_count_* columns on the ephemeral workspace_agent_stats table with a normalized workspace_agent_session_counts child table keyed by raw app name, and makes the agent report session counts via a dynamic map so new IDEs are tracked without schema or code changes. - Migration copies the ~1 day ephemeral buffer, drops the four columns, and trims the insights covering index (clean cut; no reporting gap). - Proto Stats gains map session_counts (field 13); fields 8-11 are deprecated but still converted server-side for old agents. - Agent counts sessions per raw type via sync.Map; JetBrains keeps its channel-watcher special case via family lookup; session metrics are labeled by family to bound cardinality. - App names are stored raw (casing preserved, null bytes stripped, truncated to 64 runes); well-known names and legacy spellings are canonicalized by the new coderd/idemetadata package. - The workspace usage API and coder ssh --usage-app accept arbitrary app names; codersdk.AllowedAppNames is removed. - All read queries keep their Row shapes, so the deployment stats API, Prometheus gauge names, and telemetry snapshots are unchanged. Part of the Scalable Approach for Adding New IDE Session Types RFC (phase 1 of 5). Later phases add the API map field and labeled metrics, migrate connection_logs off its type enum, and merge template_usage_stats *_mins into app_usage_mins. --- agent/agent.go | 25 +- agent/agent_test.go | 18 +- agent/agentssh/agentssh.go | 77 +- agent/agentssh/metrics.go | 17 +- agent/agentssh/sessiontype_internal_test.go | 67 + agent/proto/agent.pb.go | 1708 +++++++++-------- agent/proto/agent.proto | 22 +- cli/ssh.go | 13 +- cli/ssh_test.go | 70 +- cli/vscodessh_test.go | 2 +- coderd/agentapi/stats.go | 5 +- coderd/agentapi/stats_test.go | 2 + coderd/database/dbgen/dbgen.go | 84 +- coderd/database/dbpurge/dbpurge_test.go | 9 +- coderd/database/dbrollup/dbrollup_test.go | 40 +- coderd/database/dump.sql | 22 +- coderd/database/foreign_key_constraint.go | 1 + ...43_workspace_agent_session_counts.down.sql | 33 + ...0543_workspace_agent_session_counts.up.sql | 32 + ...0543_workspace_agent_session_counts.up.sql | 28 + coderd/database/models.go | 40 +- coderd/database/querier_test.go | 238 +-- coderd/database/queries.sql.go | 416 ++-- coderd/database/queries/insights.sql | 102 +- .../database/queries/workspaceagentstats.sql | 270 ++- coderd/database/unique_constraint.go | 1 + coderd/idemetadata/idemetadata.go | 113 ++ coderd/idemetadata/idemetadata_test.go | 70 + coderd/metricscache/metricscache_test.go | 24 +- coderd/workspaces.go | 32 +- coderd/workspaces_test.go | 6 +- coderd/workspacestats/batcher.go | 62 +- coderd/workspacestats/reporter.go | 5 +- coderd/workspacestats/sessioncounts.go | 45 + coderd/workspacestats/sessioncounts_test.go | 75 + codersdk/workspaces.go | 9 +- tailnet/proto/version.go | 8 +- 37 files changed, 2208 insertions(+), 1583 deletions(-) create mode 100644 agent/agentssh/sessiontype_internal_test.go create mode 100644 coderd/database/migrations/000543_workspace_agent_session_counts.down.sql create mode 100644 coderd/database/migrations/000543_workspace_agent_session_counts.up.sql create mode 100644 coderd/database/migrations/testdata/fixtures/000543_workspace_agent_session_counts.up.sql create mode 100644 coderd/idemetadata/idemetadata.go create mode 100644 coderd/idemetadata/idemetadata_test.go create mode 100644 coderd/workspacestats/sessioncounts.go create mode 100644 coderd/workspacestats/sessioncounts_test.go diff --git a/agent/agent.go b/agent/agent.go index 550392802df5a..3c2a351097610 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -58,6 +58,7 @@ import ( "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli/gitauth" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/idemetadata" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -426,17 +427,16 @@ func (a *agent) init() { BlockLocalPortForwarding: a.blockLocalPortForwarding, ReportConnection: func(id uuid.UUID, magicType agentssh.MagicSessionType, ip string) func(code int, reason string) { var connectionType proto.Connection_Type - switch magicType { - case agentssh.MagicSessionTypeSSH: + // The proto enum cannot represent arbitrary session types, so + // map by family. + switch idemetadata.Lookup(string(magicType)).Family { + case idemetadata.FamilySSH: connectionType = proto.Connection_SSH - case agentssh.MagicSessionTypeVSCode: + case idemetadata.FamilyVSCode: connectionType = proto.Connection_VSCODE - case agentssh.MagicSessionTypeJetBrains: + case idemetadata.FamilyJetBrains: connectionType = proto.Connection_JETBRAINS - case agentssh.MagicSessionTypeUnknown: - connectionType = proto.Connection_TYPE_UNSPECIFIED default: - a.logger.Error(a.hardCtx, "unhandled magic session type when reporting connection", slog.F("magic_type", magicType)) connectionType = proto.Connection_TYPE_UNSPECIFIED } @@ -2163,13 +2163,10 @@ func (a *agent) Collect(ctx context.Context, networkStats map[netlogtype.Connect stats.TxPackets += int64(counts.TxPackets) } - // The count of active sessions. - sshStats := a.sshServer.ConnStats() - stats.SessionCountSsh = sshStats.Sessions - stats.SessionCountVscode = sshStats.VSCode - stats.SessionCountJetbrains = sshStats.JetBrains - - stats.SessionCountReconnectingPty = a.reconnectingPTYServer.ConnCount() + // The count of active sessions. The deprecated session_count_* fields + // are only populated by older agents. + stats.SessionCounts = a.sshServer.ConnStats() + stats.SessionCounts[idemetadata.AppNameReconnectingPTY] = a.reconnectingPTYServer.ConnCount() // Compute the median connection latency! a.logger.Debug(ctx, "starting peer latency measurement for stats") diff --git a/agent/agent_test.go b/agent/agent_test.go index 9fbe263fafd49..747977fc3befa 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -229,7 +229,7 @@ func assertSSHStats(t *testing.T, stats <-chan *proto.Stats) { return false } t.Logf("got stats: ConnectionCount=%d, RxBytes=%d, TxBytes=%d, SessionCountSsh=%d", - s.ConnectionCount, s.RxBytes, s.TxBytes, s.SessionCountSsh) + s.ConnectionCount, s.RxBytes, s.TxBytes, s.SessionCounts["ssh"]) if s.ConnectionCount > 0 { connectionCountSeen = true } @@ -239,7 +239,7 @@ func assertSSHStats(t *testing.T, stats <-chan *proto.Stats) { if s.TxBytes > 0 { txBytesSeen = true } - if s.SessionCountSsh == 1 { + if s.SessionCounts["ssh"] == 1 { sessionCountSSHSeen = true } return connectionCountSeen && rxBytesSeen && txBytesSeen && sessionCountSSHSeen @@ -287,7 +287,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) { if s.TxBytes > 0 { txBytesSeen = true } - if s.SessionCountReconnectingPty == 1 { + if s.SessionCounts["reconnecting_pty"] == 1 { sessionCountReconnectingPTYSeen = true } return connectionCountSeen && rxBytesSeen && txBytesSeen && sessionCountReconnectingPTYSeen @@ -347,11 +347,11 @@ func TestAgent_Stats_Magic(t *testing.T) { require.Eventuallyf(t, func() bool { s, ok := <-stats t.Logf("got stats: ok=%t, ConnectionCount=%d, RxBytes=%d, TxBytes=%d, SessionCountVSCode=%d, ConnectionMedianLatencyMS=%f", - ok, s.ConnectionCount, s.RxBytes, s.TxBytes, s.SessionCountVscode, s.ConnectionMedianLatencyMs) + ok, s.ConnectionCount, s.RxBytes, s.TxBytes, s.SessionCounts["vscode"], s.ConnectionMedianLatencyMs) return ok && // 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 && + s.SessionCounts["vscode"] == 1 && // Ensure that connection latency is being counted! // If it isn't, it's set to -1. s.ConnectionMedianLatencyMs >= 0 @@ -417,8 +417,8 @@ func TestAgent_Stats_Magic(t *testing.T) { require.Eventuallyf(t, func() bool { s, ok := <-stats t.Logf("got stats with conn open: ok=%t, ConnectionCount=%d, SessionCountJetBrains=%d", - ok, s.ConnectionCount, s.SessionCountJetbrains) - return ok && s.SessionCountJetbrains == 1 + ok, s.ConnectionCount, s.SessionCounts["jetbrains"]) + return ok && s.SessionCounts["jetbrains"] == 1 }, testutil.WaitLong, testutil.IntervalFast, "never saw stats with conn open", ) @@ -431,9 +431,9 @@ func TestAgent_Stats_Magic(t *testing.T) { require.Eventuallyf(t, func() bool { s, ok := <-stats t.Logf("got stats after disconnect %t, %d", - ok, s.SessionCountJetbrains) + ok, s.SessionCounts["jetbrains"]) return ok && - s.SessionCountJetbrains == 0 + s.SessionCounts["jetbrains"] == 0 }, testutil.WaitLong, testutil.IntervalFast, "never saw stats after conn closes", ) diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index eb2e9ebb6bf0d..fa401a8aabce9 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -31,6 +31,7 @@ import ( "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/agent/agentrsa" "github.com/coder/coder/v2/agent/usershell" + "github.com/coder/coder/v2/coderd/idemetadata" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/pty" ) @@ -155,9 +156,10 @@ type Server struct { config *Config - connCountVSCode atomic.Int64 - connCountJetBrains atomic.Int64 - connCountSSHSession atomic.Int64 + // connCounts tracks active sessions per session type + // (map[string]*atomic.Int64). Counters are created on demand, so any + // session type reported by a client is counted. + connCounts sync.Map metrics *sshServerMetrics } @@ -232,7 +234,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom ChannelHandlers: map[string]ssh.ChannelHandler{ "direct-tcpip": func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) { // Wrapper is designed to find and track JetBrains Gateway connections. - wrapped := NewJetbrainsChannelWatcher(ctx, s.logger, s.config.ReportConnection, newChan, &s.connCountJetBrains) + wrapped := NewJetbrainsChannelWatcher(ctx, s.logger, s.config.ReportConnection, newChan, s.getOrCreateConnCounter(MagicSessionTypeJetBrains)) ssh.DirectTCPIPHandler(srv, conn, wrapped, ctx) }, "direct-streamlocal@openssh.com": func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) { @@ -324,21 +326,27 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom return s, nil } -type ConnStats struct { - Sessions int64 - VSCode int64 - JetBrains int64 +// getOrCreateConnCounter returns the active session counter for the given +// session type, creating it on first use. +func (s *Server) getOrCreateConnCounter(sessionType MagicSessionType) *atomic.Int64 { + counter, _ := s.connCounts.LoadOrStore(string(sessionType), &atomic.Int64{}) + //nolint:forcetypeassert // Only *atomic.Int64 values are stored. + return counter.(*atomic.Int64) } -func (s *Server) ConnStats() ConnStats { - return ConnStats{ - Sessions: s.connCountSSHSession.Load(), - VSCode: s.connCountVSCode.Load(), - JetBrains: s.connCountJetBrains.Load(), - } +// ConnStats returns a snapshot of active sessions per session type. +func (s *Server) ConnStats() map[string]int64 { + stats := make(map[string]int64) + s.connCounts.Range(func(key, value any) bool { + //nolint:forcetypeassert // Only string keys and *atomic.Int64 values are stored. + stats[key.(string)] = value.(*atomic.Int64).Load() + return true + }) + return stats } -func extractMagicSessionType(env []string) (magicType MagicSessionType, rawType string, filteredEnv []string) { +func extractMagicSessionType(env []string) (sessionType MagicSessionType, filteredEnv []string) { + var rawType string for _, kv := range env { if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) { continue @@ -348,19 +356,16 @@ func extractMagicSessionType(env []string) (magicType MagicSessionType, rawType // Keep going, we'll use the last instance of the env. } - // Always force lowercase checking to be case-insensitive. - switch MagicSessionType(strings.ToLower(rawType)) { - case MagicSessionTypeVSCode: - magicType = MagicSessionTypeVSCode - case MagicSessionTypeJetBrains: - magicType = MagicSessionTypeJetBrains - case "", MagicSessionTypeSSH: - magicType = MagicSessionTypeSSH - default: - magicType = MagicSessionTypeUnknown + if rawType == "" { + // No magic session type set: this is a plain SSH session. + sessionType = MagicSessionTypeSSH + } else { + // Clean, do not classify: the raw string is preserved (well-known + // names are canonicalized) so new IDEs flow through unchanged. + sessionType = MagicSessionType(idemetadata.Normalize(rawType)) } - return magicType, rawType, slices.DeleteFunc(env, func(kv string) bool { + return sessionType, slices.DeleteFunc(env, func(kv string) bool { return strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable+"=") }) } @@ -423,7 +428,7 @@ func (s *Server) sessionHandler(session ssh.Session) { logger.Info(ctx, "handling ssh session") env := session.Environ() - magicType, magicTypeRaw, env := extractMagicSessionType(env) + magicType, env := extractMagicSessionType(env) // It's not safe to assume RemoteAddr() returns a non-nil value. slog.F usage is fine because it correctly // handles nil. @@ -449,19 +454,15 @@ func (s *Server) sessionHandler(session ssh.Session) { reportSession := true - switch magicType { - case MagicSessionTypeVSCode: - s.connCountVSCode.Add(1) - defer s.connCountVSCode.Add(-1) - case MagicSessionTypeJetBrains: + if idemetadata.Lookup(string(magicType)).Family == idemetadata.FamilyJetBrains { // Do nothing here because JetBrains launches hundreds of ssh sessions. - // We instead track JetBrains in the single persistent tcp forwarding channel. + // We instead track JetBrains in the single persistent tcp forwarding + // channel, matched by family to include IDE-specific identifiers. reportSession = false - case MagicSessionTypeSSH: - s.connCountSSHSession.Add(1) - defer s.connCountSSHSession.Add(-1) - case MagicSessionTypeUnknown: - logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("raw_type", magicTypeRaw)) + } else { + counter := s.getOrCreateConnCounter(magicType) + counter.Add(1) + defer counter.Add(-1) } closeCause := func(_ string) {} diff --git a/agent/agentssh/metrics.go b/agent/agentssh/metrics.go index 22bbf1fd80743..1474e1a5dee1e 100644 --- a/agent/agentssh/metrics.go +++ b/agent/agentssh/metrics.go @@ -1,9 +1,9 @@ package agentssh import ( - "strings" - "github.com/prometheus/client_golang/prometheus" + + "github.com/coder/coder/v2/coderd/idemetadata" ) type sshServerMetrics struct { @@ -72,14 +72,7 @@ func newSSHServerMetrics(registerer prometheus.Registerer) *sshServerMetrics { } func magicTypeMetricLabel(magicType MagicSessionType) string { - switch magicType { - case MagicSessionTypeVSCode: - case MagicSessionTypeJetBrains: - case MagicSessionTypeSSH: - case MagicSessionTypeUnknown: - default: - magicType = MagicSessionTypeUnknown - } - // Always be case insensitive - return strings.ToLower(string(magicType)) + // Label by family to keep metric cardinality bounded while arbitrary + // session types flow through. + return idemetadata.Lookup(string(magicType)).Family } diff --git a/agent/agentssh/sessiontype_internal_test.go b/agent/agentssh/sessiontype_internal_test.go new file mode 100644 index 0000000000000..e91fa1fe2c1ae --- /dev/null +++ b/agent/agentssh/sessiontype_internal_test.go @@ -0,0 +1,67 @@ +package agentssh + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExtractMagicSessionType(t *testing.T) { + t.Parallel() + + envWith := func(value string) []string { + return []string{ + "FOO=bar", + fmt.Sprintf("%s=%s", MagicSessionTypeEnvironmentVariable, value), + "BAZ=qux", + } + } + + for _, tc := range []struct { + name string + env []string + want MagicSessionType + }{ + {name: "NoEnvDefaultsToSSH", env: []string{"FOO=bar"}, want: MagicSessionTypeSSH}, + {name: "EmptyValueDefaultsToSSH", env: envWith(""), want: MagicSessionTypeSSH}, + {name: "VSCode", env: envWith("vscode"), want: MagicSessionTypeVSCode}, + {name: "JetBrainsLegacyCasing", env: envWith("JetBrains"), want: MagicSessionTypeJetBrains}, + {name: "UnknownPreservedRaw", env: envWith("Cursor Nightly"), want: MagicSessionType("Cursor Nightly")}, + {name: "LastInstanceWins", env: append(envWith("vscode"), MagicSessionTypeEnvironmentVariable+"=cursor"), want: MagicSessionType("cursor")}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + sessionType, filteredEnv := extractMagicSessionType(tc.env) + require.Equal(t, tc.want, sessionType) + for _, kv := range filteredEnv { + require.NotContains(t, kv, MagicSessionTypeEnvironmentVariable+"=", "env should be stripped") + } + }) + } +} + +func TestConnCounts(t *testing.T) { + t.Parallel() + + s := &Server{} + + // Counters are created dynamically per session type, including types + // unknown to this build of the agent. + s.getOrCreateConnCounter(MagicSessionTypeSSH).Add(1) + s.getOrCreateConnCounter(MagicSessionTypeVSCode).Add(1) + s.getOrCreateConnCounter(MagicSessionType("Cursor")).Add(2) + require.Equal(t, map[string]int64{ + "ssh": 1, + "vscode": 1, + "Cursor": 2, + }, s.ConnStats()) + + // The same counter instance is reused per type. + s.getOrCreateConnCounter(MagicSessionType("Cursor")).Add(-2) + require.Equal(t, map[string]int64{ + "ssh": 1, + "vscode": 1, + "Cursor": 0, + }, s.ConnStats()) +} diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 774504cda22a2..84b387d3848c8 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -1705,17 +1705,38 @@ type Stats struct { TxBytes int64 `protobuf:"varint,7,opt,name=tx_bytes,json=txBytes,proto3" json:"tx_bytes,omitempty"` // SessionCountVSCode is the number of connections received by an agent // that are from our VS Code extension. + // + // Deprecated: Use session_counts instead. Only set by old agents; the + // server converts these fields into the map when the map is empty. + // + // Deprecated: Marked as deprecated in agent/proto/agent.proto. SessionCountVscode int64 `protobuf:"varint,8,opt,name=session_count_vscode,json=sessionCountVscode,proto3" json:"session_count_vscode,omitempty"` // SessionCountJetBrains is the number of connections received by an agent // that are from our JetBrains extension. + // + // Deprecated: Use session_counts instead. + // + // Deprecated: Marked as deprecated in agent/proto/agent.proto. SessionCountJetbrains int64 `protobuf:"varint,9,opt,name=session_count_jetbrains,json=sessionCountJetbrains,proto3" json:"session_count_jetbrains,omitempty"` // SessionCountReconnectingPTY is the number of connections received by an agent // that are from the reconnecting web terminal. + // + // Deprecated: Use session_counts instead. + // + // Deprecated: Marked as deprecated in agent/proto/agent.proto. SessionCountReconnectingPty int64 `protobuf:"varint,10,opt,name=session_count_reconnecting_pty,json=sessionCountReconnectingPty,proto3" json:"session_count_reconnecting_pty,omitempty"` // SessionCountSSH is the number of connections received by an agent // that are normal, non-tagged SSH sessions. + // + // Deprecated: Use session_counts instead. + // + // Deprecated: Marked as deprecated in agent/proto/agent.proto. SessionCountSsh int64 `protobuf:"varint,11,opt,name=session_count_ssh,json=sessionCountSsh,proto3" json:"session_count_ssh,omitempty"` Metrics []*Stats_Metric `protobuf:"bytes,12,rep,name=metrics,proto3" json:"metrics,omitempty"` + // SessionCounts is the number of sessions per app, keyed by the raw app + // identifier reported by the client (e.g. "vscode", "jetbrains", "ssh", + // "reconnecting_pty"). Replaces the deprecated session_count_* fields. + SessionCounts map[string]int64 `protobuf:"bytes,13,rep,name=session_counts,json=sessionCounts,proto3" json:"session_counts,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } func (x *Stats) Reset() { @@ -1799,6 +1820,7 @@ func (x *Stats) GetTxBytes() int64 { return 0 } +// Deprecated: Marked as deprecated in agent/proto/agent.proto. func (x *Stats) GetSessionCountVscode() int64 { if x != nil { return x.SessionCountVscode @@ -1806,6 +1828,7 @@ func (x *Stats) GetSessionCountVscode() int64 { return 0 } +// Deprecated: Marked as deprecated in agent/proto/agent.proto. func (x *Stats) GetSessionCountJetbrains() int64 { if x != nil { return x.SessionCountJetbrains @@ -1813,6 +1836,7 @@ func (x *Stats) GetSessionCountJetbrains() int64 { return 0 } +// Deprecated: Marked as deprecated in agent/proto/agent.proto. func (x *Stats) GetSessionCountReconnectingPty() int64 { if x != nil { return x.SessionCountReconnectingPty @@ -1820,6 +1844,7 @@ func (x *Stats) GetSessionCountReconnectingPty() int64 { return 0 } +// Deprecated: Marked as deprecated in agent/proto/agent.proto. func (x *Stats) GetSessionCountSsh() int64 { if x != nil { return x.SessionCountSsh @@ -1834,6 +1859,13 @@ func (x *Stats) GetMetrics() []*Stats_Metric { return nil } +func (x *Stats) GetSessionCounts() map[string]int64 { + if x != nil { + return x.SessionCounts + } + return nil +} + type UpdateStatsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -4774,7 +4806,7 @@ type Stats_Metric_Label struct { func (x *Stats_Metric_Label) Reset() { *x = Stats_Metric_Label{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[62] + mi := &file_agent_proto_agent_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4787,7 +4819,7 @@ func (x *Stats_Metric_Label) String() string { func (*Stats_Metric_Label) ProtoMessage() {} func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[62] + mi := &file_agent_proto_agent_proto_msgTypes[63] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4829,7 +4861,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct { func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() { *x = BatchUpdateAppHealthRequest_HealthUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[63] + mi := &file_agent_proto_agent_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4842,7 +4874,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string { func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {} func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[63] + mi := &file_agent_proto_agent_proto_msgTypes[64] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4884,7 +4916,7 @@ type GetResourcesMonitoringConfigurationResponse_Config struct { func (x *GetResourcesMonitoringConfigurationResponse_Config) Reset() { *x = GetResourcesMonitoringConfigurationResponse_Config{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[64] + mi := &file_agent_proto_agent_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4897,7 +4929,7 @@ func (x *GetResourcesMonitoringConfigurationResponse_Config) String() string { func (*GetResourcesMonitoringConfigurationResponse_Config) ProtoMessage() {} func (x *GetResourcesMonitoringConfigurationResponse_Config) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[64] + mi := &file_agent_proto_agent_proto_msgTypes[65] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4938,7 +4970,7 @@ type GetResourcesMonitoringConfigurationResponse_Memory struct { func (x *GetResourcesMonitoringConfigurationResponse_Memory) Reset() { *x = GetResourcesMonitoringConfigurationResponse_Memory{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[65] + mi := &file_agent_proto_agent_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4951,7 +4983,7 @@ func (x *GetResourcesMonitoringConfigurationResponse_Memory) String() string { func (*GetResourcesMonitoringConfigurationResponse_Memory) ProtoMessage() {} func (x *GetResourcesMonitoringConfigurationResponse_Memory) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[65] + mi := &file_agent_proto_agent_proto_msgTypes[66] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4986,7 +5018,7 @@ type GetResourcesMonitoringConfigurationResponse_Volume struct { func (x *GetResourcesMonitoringConfigurationResponse_Volume) Reset() { *x = GetResourcesMonitoringConfigurationResponse_Volume{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[66] + mi := &file_agent_proto_agent_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4999,7 +5031,7 @@ func (x *GetResourcesMonitoringConfigurationResponse_Volume) String() string { func (*GetResourcesMonitoringConfigurationResponse_Volume) ProtoMessage() {} func (x *GetResourcesMonitoringConfigurationResponse_Volume) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[66] + mi := &file_agent_proto_agent_proto_msgTypes[67] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5042,7 +5074,7 @@ type PushResourcesMonitoringUsageRequest_Datapoint struct { func (x *PushResourcesMonitoringUsageRequest_Datapoint) Reset() { *x = PushResourcesMonitoringUsageRequest_Datapoint{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[67] + mi := &file_agent_proto_agent_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5055,7 +5087,7 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint) String() string { func (*PushResourcesMonitoringUsageRequest_Datapoint) ProtoMessage() {} func (x *PushResourcesMonitoringUsageRequest_Datapoint) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[67] + mi := &file_agent_proto_agent_proto_msgTypes[68] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5104,7 +5136,7 @@ type PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage struct { func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Reset() { *x = PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[68] + mi := &file_agent_proto_agent_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5117,7 +5149,7 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) String() str func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoMessage() {} func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[68] + mi := &file_agent_proto_agent_proto_msgTypes[69] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5160,7 +5192,7 @@ type PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage struct { func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Reset() { *x = PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[69] + mi := &file_agent_proto_agent_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5173,7 +5205,7 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) String() str func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoMessage() {} func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[69] + mi := &file_agent_proto_agent_proto_msgTypes[70] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5233,7 +5265,7 @@ type CreateSubAgentRequest_App struct { func (x *CreateSubAgentRequest_App) Reset() { *x = CreateSubAgentRequest_App{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[70] + mi := &file_agent_proto_agent_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5246,7 +5278,7 @@ func (x *CreateSubAgentRequest_App) String() string { func (*CreateSubAgentRequest_App) ProtoMessage() {} func (x *CreateSubAgentRequest_App) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[70] + mi := &file_agent_proto_agent_proto_msgTypes[71] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5366,7 +5398,7 @@ type CreateSubAgentRequest_App_Healthcheck struct { func (x *CreateSubAgentRequest_App_Healthcheck) Reset() { *x = CreateSubAgentRequest_App_Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[71] + mi := &file_agent_proto_agent_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5379,7 +5411,7 @@ func (x *CreateSubAgentRequest_App_Healthcheck) String() string { func (*CreateSubAgentRequest_App_Healthcheck) ProtoMessage() {} func (x *CreateSubAgentRequest_App_Healthcheck) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[71] + mi := &file_agent_proto_agent_proto_msgTypes[72] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5429,7 +5461,7 @@ type CreateSubAgentResponse_AppCreationError struct { func (x *CreateSubAgentResponse_AppCreationError) Reset() { *x = CreateSubAgentResponse_AppCreationError{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[72] + mi := &file_agent_proto_agent_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5442,7 +5474,7 @@ func (x *CreateSubAgentResponse_AppCreationError) String() string { func (*CreateSubAgentResponse_AppCreationError) ProtoMessage() {} func (x *CreateSubAgentResponse_AppCreationError) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[72] + mi := &file_agent_proto_agent_proto_msgTypes[73] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5495,7 +5527,7 @@ type BoundaryLog_HttpRequest struct { func (x *BoundaryLog_HttpRequest) Reset() { *x = BoundaryLog_HttpRequest{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[73] + mi := &file_agent_proto_agent_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5508,7 +5540,7 @@ func (x *BoundaryLog_HttpRequest) String() string { func (*BoundaryLog_HttpRequest) ProtoMessage() {} func (x *BoundaryLog_HttpRequest) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[73] + mi := &file_agent_proto_agent_proto_msgTypes[74] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5760,7 +5792,7 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0xb3, 0x07, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x14, 0x63, + 0x74, 0x22, 0xd6, 0x08, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x14, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x62, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, @@ -5780,692 +5812,702 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x78, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, + 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x14, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x76, 0x73, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x56, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x36, - 0x0a, 0x17, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, - 0x6a, 0x65, 0x74, 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x15, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x65, 0x74, - 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x73, 0x68, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x12, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x56, 0x73, 0x63, 0x6f, + 0x64, 0x65, 0x12, 0x3a, 0x0a, 0x17, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x5f, 0x6a, 0x65, 0x74, 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x15, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x65, 0x74, 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x47, + 0x0a, 0x1e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x74, 0x79, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x1b, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6e, 0x67, 0x50, 0x74, 0x79, 0x12, 0x2e, 0x0a, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x73, 0x68, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x73, 0x68, 0x12, 0x36, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x1a, - 0x45, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x8e, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, + 0x4f, 0x0a, 0x0e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x1a, 0x45, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, + 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x8e, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, + 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, + 0x69, 0x63, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x31, - 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0x34, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, - 0x47, 0x41, 0x55, 0x47, 0x45, 0x10, 0x02, 0x22, 0x41, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, + 0x63, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, + 0x31, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x34, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x01, 0x12, 0x09, 0x0a, + 0x05, 0x47, 0x41, 0x55, 0x47, 0x45, 0x10, 0x02, 0x1a, 0x40, 0x0a, 0x12, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x41, 0x0a, 0x12, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x59, 0x0a, + 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x22, 0xae, 0x02, 0x0a, 0x09, 0x4c, 0x69, 0x66, + 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, + 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x41, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, + 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, + 0x4e, 0x47, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x49, + 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x52, 0x54, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, + 0x59, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x48, 0x55, 0x54, 0x54, 0x49, 0x4e, 0x47, 0x5f, + 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x06, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, + 0x57, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, + 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x08, + 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x46, 0x46, 0x10, 0x09, 0x22, 0x51, 0x0a, 0x16, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, + 0x65, 0x52, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x22, 0xc4, 0x01, 0x0a, + 0x1b, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x07, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, + 0x1a, 0x51, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x31, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x22, 0x1e, 0x0a, 0x1c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0xe8, 0x01, 0x0a, 0x07, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x78, 0x70, + 0x61, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x44, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x73, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x13, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x42, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x76, 0x61, 0x6c, 0x22, 0xae, 0x02, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, - 0x63, 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x64, 0x41, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, - 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, - 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, - 0x55, 0x54, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x05, - 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x48, 0x55, 0x54, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x4f, 0x57, - 0x4e, 0x10, 0x06, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, - 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x48, 0x55, - 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x08, 0x12, 0x07, 0x0a, - 0x03, 0x4f, 0x46, 0x46, 0x10, 0x09, 0x22, 0x51, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, - 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x22, 0xc4, 0x01, 0x0a, 0x1b, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x07, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x1a, 0x51, 0x0a, - 0x0c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, - 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x22, 0x1e, 0x0a, 0x1c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0xe8, 0x01, 0x0a, 0x07, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, - 0x65, 0x64, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x11, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x44, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x75, 0x70, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x73, 0x75, - 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x51, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x55, 0x42, 0x53, 0x59, 0x53, 0x54, - 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, 0x56, 0x42, 0x4f, 0x58, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, - 0x45, 0x4e, 0x56, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, - 0x45, 0x58, 0x45, 0x43, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x03, 0x22, 0x49, 0x0a, 0x14, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x07, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x22, 0x63, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x52, 0x0a, 0x1a, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, - 0x1d, 0x0a, 0x1b, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xde, - 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x05, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x53, 0x0a, 0x05, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x43, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x02, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, - 0x52, 0x4e, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, - 0x65, 0x0a, 0x16, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, - 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x0b, 0x6c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, - 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, + 0x61, 0x72, 0x74, 0x75, 0x70, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, + 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x51, 0x0a, 0x09, 0x53, + 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x55, 0x42, 0x53, + 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, 0x56, 0x42, 0x4f, 0x58, 0x10, 0x01, 0x12, + 0x0e, 0x0a, 0x0a, 0x45, 0x4e, 0x56, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x45, 0x52, 0x10, 0x02, 0x12, + 0x0d, 0x0a, 0x09, 0x45, 0x58, 0x45, 0x43, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x03, 0x22, 0x49, + 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, + 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, + 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x22, 0x63, 0x0a, 0x08, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, + 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x52, + 0x0a, 0x1a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x22, 0x1d, 0x0a, 0x1b, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0xde, 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x05, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, - 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x22, 0x47, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x65, - 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, - 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x22, - 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x13, - 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, - 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, 0x67, - 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6c, - 0x6f, 0x72, 0x22, 0x56, 0x0a, 0x24, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, 0x74, 0x69, - 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, + 0x2e, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x53, 0x0a, + 0x05, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, + 0x47, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x03, 0x12, 0x08, 0x0a, + 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x05, 0x22, 0x65, 0x0a, 0x16, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, + 0x6c, 0x6f, 0x67, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x12, 0x27, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x22, 0x47, 0x0a, 0x17, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x5f, 0x65, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, + 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, + 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x13, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, + 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, + 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, + 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, 0x56, 0x0a, 0x24, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, + 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x22, 0x27, 0x0a, + 0x25, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xfd, 0x02, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x30, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x1b, + 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, - 0x6e, 0x67, 0x52, 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x22, 0x27, 0x0a, 0x25, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0xfd, 0x02, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1b, - 0x0a, 0x09, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x08, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, - 0x03, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, - 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, - 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x2e, - 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x35, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, - 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x26, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x09, 0x0a, 0x05, - 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x52, 0x4f, 0x4e, 0x10, 0x02, 0x22, 0x46, 0x0a, 0x06, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x10, 0x0a, - 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, - 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, - 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, - 0x4e, 0x10, 0x03, 0x22, 0x2c, 0x0a, 0x2a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0xa0, 0x04, 0x0a, 0x2b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, + 0x35, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x26, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, + 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, + 0x4f, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x52, 0x4f, 0x4e, 0x10, 0x02, 0x22, 0x46, + 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, + 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, + 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, + 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, + 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x03, 0x22, 0x2c, 0x0a, 0x2a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0xa0, 0x04, 0x0a, 0x2b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x5a, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5f, 0x0a, - 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, - 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, - 0x79, 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x5c, - 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, - 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, 0x6f, 0x0a, 0x06, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, 0x5f, 0x64, 0x61, - 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, - 0x6e, 0x75, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x3e, 0x0a, - 0x1b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x19, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x1a, 0x22, 0x0a, - 0x06, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x1a, 0x36, 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6d, 0x65, - 0x6d, 0x6f, 0x72, 0x79, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0a, - 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x3d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, - 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0xac, 0x03, 0x0a, 0x09, - 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x66, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, - 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, - 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, - 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, - 0x12, 0x63, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x4f, - 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, - 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, - 0x09, 0x0a, 0x07, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, - 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, - 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0xb6, 0x03, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x39, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x06, - 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, - 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x22, 0x3d, 0x0a, 0x06, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, - 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x49, 0x53, 0x43, - 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x02, 0x22, 0x56, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x53, 0x48, 0x10, 0x01, 0x12, - 0x0a, 0x0a, 0x06, 0x56, 0x53, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x4a, - 0x45, 0x54, 0x42, 0x52, 0x41, 0x49, 0x4e, 0x53, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, - 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x54, 0x59, 0x10, 0x04, - 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x55, 0x0a, 0x17, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x4d, 0x0a, 0x08, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x12, + 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x5f, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, + 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, + 0x01, 0x12, 0x5c, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, + 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, + 0x6f, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, + 0x5f, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0d, 0x6e, 0x75, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, + 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x19, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, + 0x1a, 0x22, 0x0a, 0x06, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x1a, 0x36, 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, + 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x5d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0xac, + 0x03, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x66, 0x0a, 0x06, 0x6d, + 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, + 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, + 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, + 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x22, 0x26, 0x0a, + 0x24, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, + 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb6, 0x03, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x33, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, + 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x22, 0x3d, 0x0a, 0x06, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, + 0x0a, 0x07, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x44, + 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x02, 0x22, 0x56, 0x0a, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x53, 0x48, + 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x56, 0x53, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x12, 0x0d, + 0x0a, 0x09, 0x4a, 0x45, 0x54, 0x42, 0x52, 0x41, 0x49, 0x4e, 0x53, 0x10, 0x03, 0x12, 0x14, 0x0a, + 0x10, 0x52, 0x45, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x54, + 0x59, 0x10, 0x04, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x55, + 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4d, 0x0a, 0x08, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xb9, 0x0a, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, + 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x22, 0xb9, 0x0a, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x22, 0x0a, - 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, - 0x65, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x3d, 0x0a, 0x04, - 0x61, 0x70, 0x70, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x41, 0x70, 0x70, 0x52, 0x04, 0x61, 0x70, 0x70, 0x73, 0x12, 0x53, 0x0a, 0x0c, 0x64, - 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x70, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, - 0x0e, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x41, 0x70, 0x70, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x73, - 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x02, - 0x69, 0x64, 0x88, 0x01, 0x01, 0x1a, 0x81, 0x07, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, - 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, - 0x67, 0x12, 0x1d, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x88, 0x01, 0x01, - 0x12, 0x26, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x02, 0x52, 0x08, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x88, 0x01, 0x01, 0x12, 0x5c, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, - 0x65, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, - 0x48, 0x04, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x88, - 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x05, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x88, 0x01, 0x01, 0x12, - 0x17, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x06, 0x52, - 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x4e, 0x0a, 0x07, 0x6f, 0x70, 0x65, 0x6e, - 0x5f, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x48, 0x07, 0x52, 0x06, 0x6f, - 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, - 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x48, 0x08, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, - 0x88, 0x01, 0x01, 0x12, 0x51, 0x0a, 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x68, - 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x48, 0x09, 0x52, 0x05, 0x73, 0x68, - 0x61, 0x72, 0x65, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0a, 0x52, 0x09, 0x73, 0x75, 0x62, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x75, 0x72, 0x6c, - 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x48, 0x0b, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x88, 0x01, 0x01, - 0x1a, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, - 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, - 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, - 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x22, 0x0a, 0x06, 0x4f, - 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x4c, 0x49, 0x4d, 0x5f, 0x57, 0x49, - 0x4e, 0x44, 0x4f, 0x57, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x42, 0x10, 0x01, 0x22, - 0x4a, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, - 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, - 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, - 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4f, 0x52, 0x47, - 0x41, 0x4e, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, 0x5f, - 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x64, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x42, - 0x0e, 0x0a, 0x0c, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x42, - 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x69, - 0x63, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x69, 0x6e, 0x42, - 0x08, 0x0a, 0x06, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x68, - 0x61, 0x72, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x75, 0x72, 0x6c, 0x22, 0x6b, 0x0a, 0x0a, 0x44, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x12, 0x0a, 0x0a, 0x06, 0x56, 0x53, 0x43, 0x4f, 0x44, - 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x53, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x49, 0x4e, - 0x53, 0x49, 0x44, 0x45, 0x52, 0x53, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x57, 0x45, 0x42, 0x5f, - 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x53, - 0x48, 0x5f, 0x48, 0x45, 0x4c, 0x50, 0x45, 0x52, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x4f, - 0x52, 0x54, 0x5f, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x48, 0x45, - 0x4c, 0x50, 0x45, 0x52, 0x10, 0x04, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x69, 0x64, 0x22, 0x96, 0x02, - 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x52, 0x05, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x67, 0x0a, 0x13, 0x61, 0x70, 0x70, 0x5f, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, + 0x74, 0x75, 0x72, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, + 0x67, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, + 0x3d, 0x0a, 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x70, 0x70, 0x52, 0x04, 0x61, 0x70, 0x70, 0x73, 0x12, 0x53, + 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x70, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, + 0x70, 0x70, 0x73, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x48, + 0x00, 0x52, 0x02, 0x69, 0x64, 0x88, 0x01, 0x01, 0x1a, 0x81, 0x07, 0x0a, 0x03, 0x41, 0x70, 0x70, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x73, 0x6c, 0x75, 0x67, 0x12, 0x1d, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x02, 0x52, + 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x05, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x88, 0x01, 0x01, 0x12, 0x5c, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x48, 0x04, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, + 0x63, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x88, + 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x06, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x4e, 0x0a, 0x07, 0x6f, + 0x70, 0x65, 0x6e, 0x5f, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x48, 0x07, + 0x52, 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x6f, + 0x72, 0x64, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x48, 0x08, 0x52, 0x05, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x51, 0x0a, 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, - 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x70, - 0x70, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x11, - 0x61, 0x70, 0x70, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x73, 0x1a, 0x63, 0x0a, 0x10, 0x41, 0x70, 0x70, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x19, 0x0a, 0x05, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x66, 0x69, - 0x65, 0x6c, 0x64, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x42, 0x08, 0x0a, 0x06, - 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x22, 0x27, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x22, - 0x18, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x49, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x64, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x70, 0x70, + 0x2e, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x48, 0x09, 0x52, + 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x73, 0x75, 0x62, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0a, 0x52, 0x09, + 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, + 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x48, 0x0b, 0x52, 0x03, 0x75, 0x72, 0x6c, + 0x88, 0x01, 0x01, 0x1a, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, + 0x63, 0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, + 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x22, + 0x0a, 0x06, 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x4c, 0x49, 0x4d, + 0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x42, + 0x10, 0x01, 0x22, 0x4a, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, + 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, + 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, + 0x4f, 0x52, 0x47, 0x41, 0x4e, 0x49, 0x5a, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x42, 0x0a, + 0x0a, 0x08, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, + 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, + 0x63, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x42, 0x07, 0x0a, + 0x05, 0x5f, 0x69, 0x63, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6f, 0x70, 0x65, 0x6e, 0x5f, + 0x69, 0x6e, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x08, 0x0a, 0x06, + 0x5f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x73, 0x75, 0x62, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x75, 0x72, 0x6c, 0x22, 0x6b, 0x0a, 0x0a, + 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x12, 0x0a, 0x0a, 0x06, 0x56, 0x53, + 0x43, 0x4f, 0x44, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x53, 0x43, 0x4f, 0x44, 0x45, + 0x5f, 0x49, 0x4e, 0x53, 0x49, 0x44, 0x45, 0x52, 0x53, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x57, + 0x45, 0x42, 0x5f, 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x0e, 0x0a, + 0x0a, 0x53, 0x53, 0x48, 0x5f, 0x48, 0x45, 0x4c, 0x50, 0x45, 0x52, 0x10, 0x03, 0x12, 0x1a, 0x0a, + 0x16, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x49, 0x4e, 0x47, + 0x5f, 0x48, 0x45, 0x4c, 0x50, 0x45, 0x52, 0x10, 0x04, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x69, 0x64, + 0x22, 0x96, 0x02, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x75, 0x62, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xb6, 0x02, 0x0a, - 0x0b, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x12, 0x18, 0x0a, 0x07, - 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, - 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x4c, 0x0a, 0x0c, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x6f, - 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, - 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x73, - 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x1a, 0x5a, 0x0a, - 0x0b, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, - 0x64, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, - 0x74, 0x63, 0x68, 0x65, 0x64, 0x52, 0x75, 0x6c, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x52, 0x04, - 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x5f, - 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x63, - 0x65, 0x73, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x1c, 0x0a, 0x1a, 0x52, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe9, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x73, 0x6c, 0x75, 0x67, 0x12, 0x4b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x70, 0x70, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, - 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x22, 0x42, 0x0a, - 0x0e, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x0b, 0x0a, 0x07, 0x57, 0x4f, 0x52, 0x4b, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, - 0x49, 0x44, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, - 0x54, 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, - 0x03, 0x22, 0x19, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8f, 0x05, 0x0a, - 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, - 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x21, - 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, - 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, - 0x12, 0x3e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x50, 0x0a, 0x10, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, - 0x65, 0x42, 0x6f, 0x64, 0x79, 0x48, 0x00, 0x52, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x73, 0x6b, 0x69, 0x6c, - 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x6b, 0x69, 0x6c, 0x6c, 0x4d, 0x65, - 0x74, 0x61, 0x42, 0x6f, 0x64, 0x79, 0x48, 0x00, 0x52, 0x05, 0x73, 0x6b, 0x69, 0x6c, 0x6c, 0x12, - 0x3e, 0x0a, 0x0a, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x6f, - 0x64, 0x79, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x3e, 0x0a, 0x0a, 0x6d, 0x63, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x0d, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x6f, - 0x64, 0x79, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, - 0x61, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x56, 0x45, - 0x52, 0x53, 0x49, 0x5a, 0x45, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x55, 0x4e, 0x52, 0x45, 0x41, - 0x44, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, - 0x49, 0x44, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x58, 0x43, 0x4c, 0x55, 0x44, 0x45, 0x44, - 0x10, 0x05, 0x42, 0x06, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, - 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x4a, 0x04, 0x08, 0x0e, - 0x10, 0x0f, 0x4a, 0x04, 0x08, 0x0f, 0x10, 0x10, 0x4a, 0x04, 0x08, 0x10, 0x10, 0x11, 0x22, 0x2f, - 0x0a, 0x13, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, - 0x65, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, - 0x59, 0x0a, 0x0d, 0x53, 0x6b, 0x69, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x42, 0x6f, 0x64, 0x79, - 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, - 0x6d, 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x0f, 0x0a, 0x0d, 0x4d, 0x43, - 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x6f, 0x64, 0x79, 0x22, 0x81, 0x01, 0x0a, 0x0d, - 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, - 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, - 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x2d, 0x0a, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x4d, 0x43, 0x50, 0x54, 0x6f, 0x6f, 0x6c, 0x52, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x22, - 0x7b, 0x0a, 0x07, 0x4d, 0x43, 0x50, 0x54, 0x6f, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, - 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x3a, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, - 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x22, 0xe0, 0x01, 0x0a, - 0x17, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x5f, - 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x61, 0x67, 0x67, 0x72, - 0x65, 0x67, 0x61, 0x74, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x3d, 0x0a, 0x09, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x69, 0x74, - 0x69, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x6e, 0x69, 0x74, 0x69, - 0x61, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, - 0x36, 0x0a, 0x18, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, - 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, - 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, - 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, - 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, - 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, - 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xc9, 0x0f, 0x0a, - 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, - 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, - 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, - 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x67, 0x0a, 0x13, 0x61, + 0x70, 0x70, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x41, 0x70, 0x70, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x52, 0x11, 0x61, 0x70, 0x70, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x63, 0x0a, 0x10, 0x41, 0x70, 0x70, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, + 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x19, + 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x42, + 0x08, 0x0a, 0x06, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x22, 0x27, 0x0a, 0x15, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, + 0x69, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x0a, 0x14, + 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x49, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, + 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, + 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, + 0xb6, 0x02, 0x0a, 0x0b, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x12, + 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x4c, 0x0a, 0x0c, 0x68, 0x74, 0x74, + 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x2e, 0x48, 0x74, 0x74, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x68, 0x74, 0x74, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x71, 0x75, 0x65, + 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0e, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 0x1a, 0x5a, 0x0a, 0x0b, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x64, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x52, 0x75, 0x6c, 0x65, 0x42, 0x0a, 0x0a, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, + 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x6e, + 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x50, + 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x1c, 0x0a, 0x1a, 0x52, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe9, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x4b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, + 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x41, + 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, + 0x22, 0x42, 0x0a, 0x0e, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x4f, 0x52, 0x4b, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, + 0x08, 0x0a, 0x04, 0x49, 0x44, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x4d, + 0x50, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x41, 0x49, 0x4c, 0x55, + 0x52, 0x45, 0x10, 0x03, 0x22, 0x19, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, + 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x8f, 0x05, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0b, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x88, 0x01, + 0x01, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, + 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x50, 0x0a, 0x10, 0x69, 0x6e, 0x73, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x46, 0x69, 0x6c, 0x65, 0x42, 0x6f, 0x64, 0x79, 0x48, 0x00, 0x52, 0x0f, 0x69, 0x6e, 0x73, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x73, + 0x6b, 0x69, 0x6c, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x6b, 0x69, 0x6c, + 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x42, 0x6f, 0x64, 0x79, 0x48, 0x00, 0x52, 0x05, 0x73, 0x6b, 0x69, + 0x6c, 0x6c, 0x12, 0x3e, 0x0a, 0x0a, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x42, 0x6f, 0x64, 0x79, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0a, 0x6d, 0x63, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x42, 0x6f, 0x64, 0x79, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x22, 0x61, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, + 0x4f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x5a, 0x45, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x55, 0x4e, + 0x52, 0x45, 0x41, 0x44, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, + 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x58, 0x43, 0x4c, 0x55, + 0x44, 0x45, 0x44, 0x10, 0x05, 0x42, 0x06, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x42, 0x0e, 0x0a, + 0x0c, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x4a, 0x04, 0x08, + 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x4a, + 0x04, 0x08, 0x0e, 0x10, 0x0f, 0x4a, 0x04, 0x08, 0x0f, 0x10, 0x10, 0x4a, 0x04, 0x08, 0x10, 0x10, + 0x11, 0x22, 0x2f, 0x0a, 0x13, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x46, 0x69, 0x6c, 0x65, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x22, 0x59, 0x0a, 0x0d, 0x53, 0x6b, 0x69, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x42, + 0x6f, 0x64, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x0f, 0x0a, + 0x0d, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x6f, 0x64, 0x79, 0x22, 0x81, + 0x01, 0x0a, 0x0d, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x6f, 0x64, 0x79, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x05, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x43, 0x50, 0x54, 0x6f, 0x6f, 0x6c, 0x52, 0x05, 0x74, 0x6f, 0x6f, + 0x6c, 0x73, 0x22, 0x7b, 0x0a, 0x07, 0x4d, 0x43, 0x50, 0x54, 0x6f, 0x6f, 0x6c, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x73, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x22, + 0xe0, 0x01, 0x0a, 0x17, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, + 0x74, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x61, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x3d, 0x0a, 0x09, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x69, + 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x6e, + 0x69, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x05, + 0x10, 0x06, 0x22, 0x36, 0x0a, 0x18, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, + 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, + 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, + 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, + 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, + 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, + 0xc9, 0x0f, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, + 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, + 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, + 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, + 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, + 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, - 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, - 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, - 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, - 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, - 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9e, - 0x01, 0x0a, 0x23, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, - 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x5f, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x5f, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x6b, 0x0a, 0x12, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, - 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x29, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x6f, - 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, - 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, - 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x65, 0x0a, 0x10, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, + 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, + 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x23, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, + 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, + 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, + 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x5f, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, + 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, + 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x75, 0x62, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x12, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x6f, 0x75, + 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x29, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x6f, 0x75, 0x6e, + 0x64, 0x61, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x62, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x10, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -6481,7 +6523,7 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 16) -var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 74) +var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 75) var file_agent_proto_agent_proto_goTypes = []interface{}{ (AppHealth)(0), // 0: coder.agent.v2.AppHealth (WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel @@ -6561,33 +6603,34 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ nil, // 75: coder.agent.v2.Manifest.EnvironmentVariablesEntry nil, // 76: coder.agent.v2.Stats.ConnectionsByProtoEntry (*Stats_Metric)(nil), // 77: coder.agent.v2.Stats.Metric - (*Stats_Metric_Label)(nil), // 78: coder.agent.v2.Stats.Metric.Label - (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 79: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - (*GetResourcesMonitoringConfigurationResponse_Config)(nil), // 80: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config - (*GetResourcesMonitoringConfigurationResponse_Memory)(nil), // 81: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory - (*GetResourcesMonitoringConfigurationResponse_Volume)(nil), // 82: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume - (*PushResourcesMonitoringUsageRequest_Datapoint)(nil), // 83: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint - (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 84: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage - (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 85: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage - (*CreateSubAgentRequest_App)(nil), // 86: coder.agent.v2.CreateSubAgentRequest.App - (*CreateSubAgentRequest_App_Healthcheck)(nil), // 87: coder.agent.v2.CreateSubAgentRequest.App.Healthcheck - (*CreateSubAgentResponse_AppCreationError)(nil), // 88: coder.agent.v2.CreateSubAgentResponse.AppCreationError - (*BoundaryLog_HttpRequest)(nil), // 89: coder.agent.v2.BoundaryLog.HttpRequest - (*durationpb.Duration)(nil), // 90: google.protobuf.Duration - (*proto.DERPMap)(nil), // 91: coder.tailnet.v2.DERPMap - (*timestamppb.Timestamp)(nil), // 92: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 93: google.protobuf.Struct - (*emptypb.Empty)(nil), // 94: google.protobuf.Empty + nil, // 78: coder.agent.v2.Stats.SessionCountsEntry + (*Stats_Metric_Label)(nil), // 79: coder.agent.v2.Stats.Metric.Label + (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 80: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + (*GetResourcesMonitoringConfigurationResponse_Config)(nil), // 81: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config + (*GetResourcesMonitoringConfigurationResponse_Memory)(nil), // 82: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory + (*GetResourcesMonitoringConfigurationResponse_Volume)(nil), // 83: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume + (*PushResourcesMonitoringUsageRequest_Datapoint)(nil), // 84: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint + (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 85: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 86: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + (*CreateSubAgentRequest_App)(nil), // 87: coder.agent.v2.CreateSubAgentRequest.App + (*CreateSubAgentRequest_App_Healthcheck)(nil), // 88: coder.agent.v2.CreateSubAgentRequest.App.Healthcheck + (*CreateSubAgentResponse_AppCreationError)(nil), // 89: coder.agent.v2.CreateSubAgentResponse.AppCreationError + (*BoundaryLog_HttpRequest)(nil), // 90: coder.agent.v2.BoundaryLog.HttpRequest + (*durationpb.Duration)(nil), // 91: google.protobuf.Duration + (*proto.DERPMap)(nil), // 92: coder.tailnet.v2.DERPMap + (*timestamppb.Timestamp)(nil), // 93: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 94: google.protobuf.Struct + (*emptypb.Empty)(nil), // 95: google.protobuf.Empty } var file_agent_proto_agent_proto_depIdxs = []int32{ 1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel 72, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck 2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health - 90, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration + 91, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration 73, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result 74, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description 75, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry - 91, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap + 92, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap 17, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript 16, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp 74, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description @@ -6595,106 +6638,107 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 20, // 12: coder.agent.v2.Manifest.secrets:type_name -> coder.agent.v2.WorkspaceSecret 76, // 13: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry 77, // 14: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric - 25, // 15: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats - 90, // 16: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration - 4, // 17: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State - 92, // 18: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp - 28, // 19: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle - 79, // 20: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - 5, // 21: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem - 32, // 22: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup - 73, // 23: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 34, // 24: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata - 92, // 25: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp - 6, // 26: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level - 37, // 27: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log - 42, // 28: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig - 45, // 29: coder.agent.v2.WorkspaceAgentScriptCompletedRequest.timing:type_name -> coder.agent.v2.Timing - 92, // 30: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp - 92, // 31: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp - 7, // 32: coder.agent.v2.Timing.stage:type_name -> coder.agent.v2.Timing.Stage - 8, // 33: coder.agent.v2.Timing.status:type_name -> coder.agent.v2.Timing.Status - 80, // 34: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.config:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config - 81, // 35: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.memory:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory - 82, // 36: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.volumes:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume - 83, // 37: coder.agent.v2.PushResourcesMonitoringUsageRequest.datapoints:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint - 9, // 38: coder.agent.v2.Connection.action:type_name -> coder.agent.v2.Connection.Action - 10, // 39: coder.agent.v2.Connection.type:type_name -> coder.agent.v2.Connection.Type - 92, // 40: coder.agent.v2.Connection.timestamp:type_name -> google.protobuf.Timestamp - 50, // 41: coder.agent.v2.ReportConnectionRequest.connection:type_name -> coder.agent.v2.Connection - 86, // 42: coder.agent.v2.CreateSubAgentRequest.apps:type_name -> coder.agent.v2.CreateSubAgentRequest.App - 11, // 43: coder.agent.v2.CreateSubAgentRequest.display_apps:type_name -> coder.agent.v2.CreateSubAgentRequest.DisplayApp - 52, // 44: coder.agent.v2.CreateSubAgentResponse.agent:type_name -> coder.agent.v2.SubAgent - 88, // 45: coder.agent.v2.CreateSubAgentResponse.app_creation_errors:type_name -> coder.agent.v2.CreateSubAgentResponse.AppCreationError - 52, // 46: coder.agent.v2.ListSubAgentsResponse.agents:type_name -> coder.agent.v2.SubAgent - 92, // 47: coder.agent.v2.BoundaryLog.time:type_name -> google.protobuf.Timestamp - 89, // 48: coder.agent.v2.BoundaryLog.http_request:type_name -> coder.agent.v2.BoundaryLog.HttpRequest - 59, // 49: coder.agent.v2.ReportBoundaryLogsRequest.logs:type_name -> coder.agent.v2.BoundaryLog - 14, // 50: coder.agent.v2.UpdateAppStatusRequest.state:type_name -> coder.agent.v2.UpdateAppStatusRequest.AppStatusState - 15, // 51: coder.agent.v2.ContextResource.status:type_name -> coder.agent.v2.ContextResource.Status - 65, // 52: coder.agent.v2.ContextResource.instruction_file:type_name -> coder.agent.v2.InstructionFileBody - 66, // 53: coder.agent.v2.ContextResource.skill:type_name -> coder.agent.v2.SkillMetaBody - 67, // 54: coder.agent.v2.ContextResource.mcp_config:type_name -> coder.agent.v2.MCPConfigBody - 68, // 55: coder.agent.v2.ContextResource.mcp_server:type_name -> coder.agent.v2.MCPServerBody - 69, // 56: coder.agent.v2.MCPServerBody.tools:type_name -> coder.agent.v2.MCPTool - 93, // 57: coder.agent.v2.MCPTool.input_schema:type_name -> google.protobuf.Struct - 64, // 58: coder.agent.v2.PushContextStateRequest.resources:type_name -> coder.agent.v2.ContextResource - 90, // 59: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration - 92, // 60: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp - 90, // 61: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration - 90, // 62: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration - 3, // 63: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type - 78, // 64: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label - 0, // 65: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth - 92, // 66: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp - 84, // 67: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage - 85, // 68: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volumes:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage - 87, // 69: coder.agent.v2.CreateSubAgentRequest.App.healthcheck:type_name -> coder.agent.v2.CreateSubAgentRequest.App.Healthcheck - 12, // 70: coder.agent.v2.CreateSubAgentRequest.App.open_in:type_name -> coder.agent.v2.CreateSubAgentRequest.App.OpenIn - 13, // 71: coder.agent.v2.CreateSubAgentRequest.App.share:type_name -> coder.agent.v2.CreateSubAgentRequest.App.SharingLevel - 22, // 72: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest - 24, // 73: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest - 26, // 74: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest - 29, // 75: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest - 30, // 76: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest - 33, // 77: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest - 35, // 78: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest - 38, // 79: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest - 40, // 80: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest - 43, // 81: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest - 46, // 82: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:input_type -> coder.agent.v2.GetResourcesMonitoringConfigurationRequest - 48, // 83: coder.agent.v2.Agent.PushResourcesMonitoringUsage:input_type -> coder.agent.v2.PushResourcesMonitoringUsageRequest - 51, // 84: coder.agent.v2.Agent.ReportConnection:input_type -> coder.agent.v2.ReportConnectionRequest - 53, // 85: coder.agent.v2.Agent.CreateSubAgent:input_type -> coder.agent.v2.CreateSubAgentRequest - 55, // 86: coder.agent.v2.Agent.DeleteSubAgent:input_type -> coder.agent.v2.DeleteSubAgentRequest - 57, // 87: coder.agent.v2.Agent.ListSubAgents:input_type -> coder.agent.v2.ListSubAgentsRequest - 60, // 88: coder.agent.v2.Agent.ReportBoundaryLogs:input_type -> coder.agent.v2.ReportBoundaryLogsRequest - 62, // 89: coder.agent.v2.Agent.UpdateAppStatus:input_type -> coder.agent.v2.UpdateAppStatusRequest - 70, // 90: coder.agent.v2.Agent.PushContextState:input_type -> coder.agent.v2.PushContextStateRequest - 19, // 91: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest - 23, // 92: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner - 27, // 93: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse - 28, // 94: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle - 31, // 95: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse - 32, // 96: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup - 36, // 97: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse - 39, // 98: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse - 41, // 99: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse - 44, // 100: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse - 47, // 101: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:output_type -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse - 49, // 102: coder.agent.v2.Agent.PushResourcesMonitoringUsage:output_type -> coder.agent.v2.PushResourcesMonitoringUsageResponse - 94, // 103: coder.agent.v2.Agent.ReportConnection:output_type -> google.protobuf.Empty - 54, // 104: coder.agent.v2.Agent.CreateSubAgent:output_type -> coder.agent.v2.CreateSubAgentResponse - 56, // 105: coder.agent.v2.Agent.DeleteSubAgent:output_type -> coder.agent.v2.DeleteSubAgentResponse - 58, // 106: coder.agent.v2.Agent.ListSubAgents:output_type -> coder.agent.v2.ListSubAgentsResponse - 61, // 107: coder.agent.v2.Agent.ReportBoundaryLogs:output_type -> coder.agent.v2.ReportBoundaryLogsResponse - 63, // 108: coder.agent.v2.Agent.UpdateAppStatus:output_type -> coder.agent.v2.UpdateAppStatusResponse - 71, // 109: coder.agent.v2.Agent.PushContextState:output_type -> coder.agent.v2.PushContextStateResponse - 91, // [91:110] is the sub-list for method output_type - 72, // [72:91] is the sub-list for method input_type - 72, // [72:72] is the sub-list for extension type_name - 72, // [72:72] is the sub-list for extension extendee - 0, // [0:72] is the sub-list for field type_name + 78, // 15: coder.agent.v2.Stats.session_counts:type_name -> coder.agent.v2.Stats.SessionCountsEntry + 25, // 16: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats + 91, // 17: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration + 4, // 18: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State + 93, // 19: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp + 28, // 20: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle + 80, // 21: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + 5, // 22: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem + 32, // 23: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup + 73, // 24: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 34, // 25: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata + 93, // 26: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp + 6, // 27: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level + 37, // 28: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log + 42, // 29: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig + 45, // 30: coder.agent.v2.WorkspaceAgentScriptCompletedRequest.timing:type_name -> coder.agent.v2.Timing + 93, // 31: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp + 93, // 32: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp + 7, // 33: coder.agent.v2.Timing.stage:type_name -> coder.agent.v2.Timing.Stage + 8, // 34: coder.agent.v2.Timing.status:type_name -> coder.agent.v2.Timing.Status + 81, // 35: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.config:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config + 82, // 36: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.memory:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory + 83, // 37: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.volumes:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume + 84, // 38: coder.agent.v2.PushResourcesMonitoringUsageRequest.datapoints:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint + 9, // 39: coder.agent.v2.Connection.action:type_name -> coder.agent.v2.Connection.Action + 10, // 40: coder.agent.v2.Connection.type:type_name -> coder.agent.v2.Connection.Type + 93, // 41: coder.agent.v2.Connection.timestamp:type_name -> google.protobuf.Timestamp + 50, // 42: coder.agent.v2.ReportConnectionRequest.connection:type_name -> coder.agent.v2.Connection + 87, // 43: coder.agent.v2.CreateSubAgentRequest.apps:type_name -> coder.agent.v2.CreateSubAgentRequest.App + 11, // 44: coder.agent.v2.CreateSubAgentRequest.display_apps:type_name -> coder.agent.v2.CreateSubAgentRequest.DisplayApp + 52, // 45: coder.agent.v2.CreateSubAgentResponse.agent:type_name -> coder.agent.v2.SubAgent + 89, // 46: coder.agent.v2.CreateSubAgentResponse.app_creation_errors:type_name -> coder.agent.v2.CreateSubAgentResponse.AppCreationError + 52, // 47: coder.agent.v2.ListSubAgentsResponse.agents:type_name -> coder.agent.v2.SubAgent + 93, // 48: coder.agent.v2.BoundaryLog.time:type_name -> google.protobuf.Timestamp + 90, // 49: coder.agent.v2.BoundaryLog.http_request:type_name -> coder.agent.v2.BoundaryLog.HttpRequest + 59, // 50: coder.agent.v2.ReportBoundaryLogsRequest.logs:type_name -> coder.agent.v2.BoundaryLog + 14, // 51: coder.agent.v2.UpdateAppStatusRequest.state:type_name -> coder.agent.v2.UpdateAppStatusRequest.AppStatusState + 15, // 52: coder.agent.v2.ContextResource.status:type_name -> coder.agent.v2.ContextResource.Status + 65, // 53: coder.agent.v2.ContextResource.instruction_file:type_name -> coder.agent.v2.InstructionFileBody + 66, // 54: coder.agent.v2.ContextResource.skill:type_name -> coder.agent.v2.SkillMetaBody + 67, // 55: coder.agent.v2.ContextResource.mcp_config:type_name -> coder.agent.v2.MCPConfigBody + 68, // 56: coder.agent.v2.ContextResource.mcp_server:type_name -> coder.agent.v2.MCPServerBody + 69, // 57: coder.agent.v2.MCPServerBody.tools:type_name -> coder.agent.v2.MCPTool + 94, // 58: coder.agent.v2.MCPTool.input_schema:type_name -> google.protobuf.Struct + 64, // 59: coder.agent.v2.PushContextStateRequest.resources:type_name -> coder.agent.v2.ContextResource + 91, // 60: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration + 93, // 61: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp + 91, // 62: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration + 91, // 63: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration + 3, // 64: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type + 79, // 65: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label + 0, // 66: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth + 93, // 67: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp + 85, // 68: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + 86, // 69: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volumes:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + 88, // 70: coder.agent.v2.CreateSubAgentRequest.App.healthcheck:type_name -> coder.agent.v2.CreateSubAgentRequest.App.Healthcheck + 12, // 71: coder.agent.v2.CreateSubAgentRequest.App.open_in:type_name -> coder.agent.v2.CreateSubAgentRequest.App.OpenIn + 13, // 72: coder.agent.v2.CreateSubAgentRequest.App.share:type_name -> coder.agent.v2.CreateSubAgentRequest.App.SharingLevel + 22, // 73: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest + 24, // 74: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest + 26, // 75: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest + 29, // 76: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest + 30, // 77: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest + 33, // 78: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest + 35, // 79: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest + 38, // 80: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest + 40, // 81: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest + 43, // 82: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest + 46, // 83: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:input_type -> coder.agent.v2.GetResourcesMonitoringConfigurationRequest + 48, // 84: coder.agent.v2.Agent.PushResourcesMonitoringUsage:input_type -> coder.agent.v2.PushResourcesMonitoringUsageRequest + 51, // 85: coder.agent.v2.Agent.ReportConnection:input_type -> coder.agent.v2.ReportConnectionRequest + 53, // 86: coder.agent.v2.Agent.CreateSubAgent:input_type -> coder.agent.v2.CreateSubAgentRequest + 55, // 87: coder.agent.v2.Agent.DeleteSubAgent:input_type -> coder.agent.v2.DeleteSubAgentRequest + 57, // 88: coder.agent.v2.Agent.ListSubAgents:input_type -> coder.agent.v2.ListSubAgentsRequest + 60, // 89: coder.agent.v2.Agent.ReportBoundaryLogs:input_type -> coder.agent.v2.ReportBoundaryLogsRequest + 62, // 90: coder.agent.v2.Agent.UpdateAppStatus:input_type -> coder.agent.v2.UpdateAppStatusRequest + 70, // 91: coder.agent.v2.Agent.PushContextState:input_type -> coder.agent.v2.PushContextStateRequest + 19, // 92: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest + 23, // 93: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner + 27, // 94: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse + 28, // 95: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle + 31, // 96: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse + 32, // 97: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup + 36, // 98: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse + 39, // 99: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse + 41, // 100: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse + 44, // 101: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse + 47, // 102: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:output_type -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse + 49, // 103: coder.agent.v2.Agent.PushResourcesMonitoringUsage:output_type -> coder.agent.v2.PushResourcesMonitoringUsageResponse + 95, // 104: coder.agent.v2.Agent.ReportConnection:output_type -> google.protobuf.Empty + 54, // 105: coder.agent.v2.Agent.CreateSubAgent:output_type -> coder.agent.v2.CreateSubAgentResponse + 56, // 106: coder.agent.v2.Agent.DeleteSubAgent:output_type -> coder.agent.v2.DeleteSubAgentResponse + 58, // 107: coder.agent.v2.Agent.ListSubAgents:output_type -> coder.agent.v2.ListSubAgentsResponse + 61, // 108: coder.agent.v2.Agent.ReportBoundaryLogs:output_type -> coder.agent.v2.ReportBoundaryLogsResponse + 63, // 109: coder.agent.v2.Agent.UpdateAppStatus:output_type -> coder.agent.v2.UpdateAppStatusResponse + 71, // 110: coder.agent.v2.Agent.PushContextState:output_type -> coder.agent.v2.PushContextStateResponse + 92, // [92:111] is the sub-list for method output_type + 73, // [73:92] is the sub-list for method input_type + 73, // [73:73] is the sub-list for extension type_name + 73, // [73:73] is the sub-list for extension extendee + 0, // [0:73] is the sub-list for field type_name } func init() { file_agent_proto_agent_proto_init() } @@ -7423,7 +7467,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[62].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[63].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric_Label); i { case 0: return &v.state @@ -7435,7 +7479,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[63].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[64].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i { case 0: return &v.state @@ -7447,7 +7491,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[64].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[65].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetResourcesMonitoringConfigurationResponse_Config); i { case 0: return &v.state @@ -7459,7 +7503,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[65].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[66].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetResourcesMonitoringConfigurationResponse_Memory); i { case 0: return &v.state @@ -7471,7 +7515,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[66].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[67].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetResourcesMonitoringConfigurationResponse_Volume); i { case 0: return &v.state @@ -7483,7 +7527,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[67].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[68].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint); i { case 0: return &v.state @@ -7495,7 +7539,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[68].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[69].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage); i { case 0: return &v.state @@ -7507,7 +7551,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[69].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[70].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage); i { case 0: return &v.state @@ -7519,7 +7563,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[70].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[71].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateSubAgentRequest_App); i { case 0: return &v.state @@ -7531,7 +7575,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[71].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[72].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateSubAgentRequest_App_Healthcheck); i { case 0: return &v.state @@ -7543,7 +7587,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[72].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[73].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateSubAgentResponse_AppCreationError); i { case 0: return &v.state @@ -7555,7 +7599,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[73].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[74].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BoundaryLog_HttpRequest); i { case 0: return &v.state @@ -7582,16 +7626,16 @@ func file_agent_proto_agent_proto_init() { (*ContextResource_McpConfig)(nil), (*ContextResource_McpServer)(nil), } - file_agent_proto_agent_proto_msgTypes[67].OneofWrappers = []interface{}{} - file_agent_proto_agent_proto_msgTypes[70].OneofWrappers = []interface{}{} - file_agent_proto_agent_proto_msgTypes[72].OneofWrappers = []interface{}{} + file_agent_proto_agent_proto_msgTypes[68].OneofWrappers = []interface{}{} + file_agent_proto_agent_proto_msgTypes[71].OneofWrappers = []interface{}{} + file_agent_proto_agent_proto_msgTypes[73].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_agent_proto_agent_proto_rawDesc, NumEnums: 16, - NumMessages: 74, + NumMessages: 75, NumExtensions: 0, NumServices: 1, }, diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index f11c9a0f28dd0..92690887ab3ca 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -152,16 +152,25 @@ message Stats { // SessionCountVSCode is the number of connections received by an agent // that are from our VS Code extension. - int64 session_count_vscode = 8; + // + // Deprecated: Use session_counts instead. Only set by old agents; the + // server converts these fields into the map when the map is empty. + int64 session_count_vscode = 8 [deprecated = true]; // SessionCountJetBrains is the number of connections received by an agent // that are from our JetBrains extension. - int64 session_count_jetbrains = 9; + // + // Deprecated: Use session_counts instead. + int64 session_count_jetbrains = 9 [deprecated = true]; // SessionCountReconnectingPTY is the number of connections received by an agent // that are from the reconnecting web terminal. - int64 session_count_reconnecting_pty = 10; + // + // Deprecated: Use session_counts instead. + int64 session_count_reconnecting_pty = 10 [deprecated = true]; // SessionCountSSH is the number of connections received by an agent // that are normal, non-tagged SSH sessions. - int64 session_count_ssh = 11; + // + // Deprecated: Use session_counts instead. + int64 session_count_ssh = 11 [deprecated = true]; message Metric { string name = 1; @@ -182,6 +191,11 @@ message Stats { repeated Label labels = 4; } repeated Metric metrics = 12; + + // SessionCounts is the number of sessions per app, keyed by the raw app + // identifier reported by the client (e.g. "vscode", "jetbrains", "ssh", + // "reconnecting_pty"). Replaces the deprecated session_count_* fields. + map session_counts = 13; } message UpdateStatsRequest{ diff --git a/cli/ssh.go b/cli/ssh.go index d18ac8909f575..e72acfbecfaea 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -1532,17 +1532,12 @@ func getUsageAppName(usageApp string) codersdk.UsageAppName { if usageApp == disableUsageApp { return "" } - - allowedUsageApps := []string{ - string(codersdk.UsageAppNameSSH), - string(codersdk.UsageAppNameVscode), - string(codersdk.UsageAppNameJetbrains), - } - if slices.Contains(allowedUsageApps, usageApp) { - return codersdk.UsageAppName(usageApp) + if usageApp == "" { + return codersdk.UsageAppNameSSH } - return codersdk.UsageAppNameSSH + // Pass through as-is; the server accepts arbitrary app names. + return codersdk.UsageAppName(usageApp) } func setStatsCallback( diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 2221a23e7bf9c..05552ad75fbcb 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -1630,51 +1630,51 @@ func TestSSH(t *testing.T) { t.Parallel() type testCase struct { - name string - experiment bool - usageAppName string - expectedCalls int - expectedCountSSH int - expectedCountJetbrains int - expectedCountVscode int + name string + experiment bool + usageAppName string + expectedCalls int + expectedCounts map[string]int64 } tcs := []testCase{ { name: "NoExperiment", }, { - name: "Empty", - experiment: true, - expectedCalls: 1, - expectedCountSSH: 1, + name: "Empty", + experiment: true, + expectedCalls: 1, + expectedCounts: map[string]int64{"ssh": 1}, }, { - name: "SSH", - experiment: true, - usageAppName: "ssh", - expectedCalls: 1, - expectedCountSSH: 1, + name: "SSH", + experiment: true, + usageAppName: "ssh", + expectedCalls: 1, + expectedCounts: map[string]int64{"ssh": 1}, }, { - name: "Jetbrains", - experiment: true, - usageAppName: "jetbrains", - expectedCalls: 1, - expectedCountJetbrains: 1, + name: "Jetbrains", + experiment: true, + usageAppName: "jetbrains", + expectedCalls: 1, + expectedCounts: map[string]int64{"jetbrains": 1}, }, { - name: "Vscode", - experiment: true, - usageAppName: "vscode", - expectedCalls: 1, - expectedCountVscode: 1, + name: "Vscode", + experiment: true, + usageAppName: "vscode", + expectedCalls: 1, + expectedCounts: map[string]int64{"vscode": 1}, }, { - name: "InvalidDefaultsToSSH", - experiment: true, - usageAppName: "invalid", - expectedCalls: 1, - expectedCountSSH: 1, + // Arbitrary app names pass through to the server and are + // stored raw, so new IDEs are tracked without CLI changes. + name: "ArbitraryNamePassthrough", + experiment: true, + usageAppName: "SomeFutureIDE", + expectedCalls: 1, + expectedCounts: map[string]int64{"SomeFutureIDE": 1}, }, { name: "Disable", @@ -1730,9 +1730,11 @@ func TestSSH(t *testing.T) { <-cmdDone require.EqualValues(t, tc.expectedCalls, batcher.Called) - require.EqualValues(t, tc.expectedCountSSH, batcher.LastStats.SessionCountSsh) - require.EqualValues(t, tc.expectedCountJetbrains, batcher.LastStats.SessionCountJetbrains) - require.EqualValues(t, tc.expectedCountVscode, batcher.LastStats.SessionCountVscode) + if len(tc.expectedCounts) == 0 { + require.Empty(t, batcher.LastStats.GetSessionCounts()) + } else { + require.EqualValues(t, tc.expectedCounts, batcher.LastStats.GetSessionCounts()) + } }) } }) diff --git a/cli/vscodessh_test.go b/cli/vscodessh_test.go index 32afb52ca1da2..beb08e9cc841d 100644 --- a/cli/vscodessh_test.go +++ b/cli/vscodessh_test.go @@ -87,5 +87,5 @@ func TestVSCodeSSH(t *testing.T) { } require.EqualValues(t, 1, batcher.Called) - require.EqualValues(t, 1, batcher.LastStats.SessionCountVscode) + require.EqualValues(t, 1, batcher.LastStats.GetSessionCounts()["vscode"]) } diff --git a/coderd/agentapi/stats.go b/coderd/agentapi/stats.go index d6a698b55081a..d5a63ac6c5309 100644 --- a/coderd/agentapi/stats.go +++ b/coderd/agentapi/stats.go @@ -66,10 +66,7 @@ func (a *StatsAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsR // 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. - req.Stats.SessionCountSsh = 0 - req.Stats.SessionCountJetbrains = 0 - req.Stats.SessionCountVscode = 0 - req.Stats.SessionCountReconnectingPty = 0 + workspacestats.ClearSessionCounts(req.Stats) } err := a.StatsReporter.ReportAgentStats( diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index bf6c41e550c54..6a027e5f6cd06 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -525,6 +525,8 @@ func TestUpdateStats(t *testing.T) { batcher.Mu.Lock() defer batcher.Mu.Unlock() require.EqualValues(t, 1, batcher.Called) + require.Empty(t, batcher.LastStats.GetSessionCounts()) + //nolint:staticcheck // Deprecated fields must also be cleared for old-agent reports. require.EqualValues(t, 0, batcher.LastStats.SessionCountSsh) require.EqualValues(t, 0, batcher.LastStats.SessionCountJetbrains) require.EqualValues(t, 0, batcher.LastStats.SessionCountVscode) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index fc071c5f11add..b9365dc9749f7 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1675,54 +1675,60 @@ func TemplateVersionTerraformValues(t testing.TB, db database.Store, orig databa return v } -func WorkspaceAgentStat(t testing.TB, db database.Store, orig database.WorkspaceAgentStat) database.WorkspaceAgentStat { +// WorkspaceAgentStat inserts a workspace agent stat row. Pass at most one +// map to seed the workspace_agent_session_counts child table. +func WorkspaceAgentStat(t testing.TB, db database.Store, orig database.WorkspaceAgentStat, sessionCounts ...map[string]int64) database.WorkspaceAgentStat { if orig.ConnectionsByProto == nil { orig.ConnectionsByProto = json.RawMessage([]byte("{}")) } jsonProto := []byte(fmt.Sprintf("[%s]", orig.ConnectionsByProto)) + counts := map[string]int64{} + switch len(sessionCounts) { + case 0: + case 1: + counts = sessionCounts[0] + default: + t.Fatalf("expected at most one session counts map, got %d", len(sessionCounts)) + } + jsonSessionCounts, err := json.Marshal([]map[string]int64{counts}) + require.NoError(t, err, "marshal session counts") + params := database.InsertWorkspaceAgentStatsParams{ - ID: []uuid.UUID{takeFirst(orig.ID, uuid.New())}, - CreatedAt: []time.Time{takeFirst(orig.CreatedAt, dbtime.Now())}, - UserID: []uuid.UUID{takeFirst(orig.UserID, uuid.New())}, - TemplateID: []uuid.UUID{takeFirst(orig.TemplateID, uuid.New())}, - WorkspaceID: []uuid.UUID{takeFirst(orig.WorkspaceID, uuid.New())}, - AgentID: []uuid.UUID{takeFirst(orig.AgentID, uuid.New())}, - ConnectionsByProto: jsonProto, - ConnectionCount: []int64{takeFirst(orig.ConnectionCount, 0)}, - RxPackets: []int64{takeFirst(orig.RxPackets, 0)}, - RxBytes: []int64{takeFirst(orig.RxBytes, 0)}, - TxPackets: []int64{takeFirst(orig.TxPackets, 0)}, - TxBytes: []int64{takeFirst(orig.TxBytes, 0)}, - SessionCountVSCode: []int64{takeFirst(orig.SessionCountVSCode, 0)}, - SessionCountJetBrains: []int64{takeFirst(orig.SessionCountJetBrains, 0)}, - SessionCountReconnectingPTY: []int64{takeFirst(orig.SessionCountReconnectingPTY, 0)}, - SessionCountSSH: []int64{takeFirst(orig.SessionCountSSH, 0)}, - ConnectionMedianLatencyMS: []float64{takeFirst(orig.ConnectionMedianLatencyMS, 0)}, - Usage: []bool{takeFirst(orig.Usage, false)}, - } - err := db.InsertWorkspaceAgentStats(genCtx, params) + ID: []uuid.UUID{takeFirst(orig.ID, uuid.New())}, + CreatedAt: []time.Time{takeFirst(orig.CreatedAt, dbtime.Now())}, + UserID: []uuid.UUID{takeFirst(orig.UserID, uuid.New())}, + TemplateID: []uuid.UUID{takeFirst(orig.TemplateID, uuid.New())}, + WorkspaceID: []uuid.UUID{takeFirst(orig.WorkspaceID, uuid.New())}, + AgentID: []uuid.UUID{takeFirst(orig.AgentID, uuid.New())}, + ConnectionsByProto: jsonProto, + ConnectionCount: []int64{takeFirst(orig.ConnectionCount, 0)}, + RxPackets: []int64{takeFirst(orig.RxPackets, 0)}, + RxBytes: []int64{takeFirst(orig.RxBytes, 0)}, + TxPackets: []int64{takeFirst(orig.TxPackets, 0)}, + TxBytes: []int64{takeFirst(orig.TxBytes, 0)}, + SessionCounts: jsonSessionCounts, + ConnectionMedianLatencyMS: []float64{takeFirst(orig.ConnectionMedianLatencyMS, 0)}, + Usage: []bool{takeFirst(orig.Usage, false)}, + } + err = db.InsertWorkspaceAgentStats(genCtx, params) require.NoError(t, err, "insert workspace agent stat") return database.WorkspaceAgentStat{ - ID: params.ID[0], - CreatedAt: params.CreatedAt[0], - UserID: params.UserID[0], - AgentID: params.AgentID[0], - WorkspaceID: params.WorkspaceID[0], - TemplateID: params.TemplateID[0], - ConnectionsByProto: orig.ConnectionsByProto, - ConnectionCount: params.ConnectionCount[0], - RxPackets: params.RxPackets[0], - RxBytes: params.RxBytes[0], - TxPackets: params.TxPackets[0], - TxBytes: params.TxBytes[0], - ConnectionMedianLatencyMS: params.ConnectionMedianLatencyMS[0], - SessionCountVSCode: params.SessionCountVSCode[0], - SessionCountJetBrains: params.SessionCountJetBrains[0], - SessionCountReconnectingPTY: params.SessionCountReconnectingPTY[0], - SessionCountSSH: params.SessionCountSSH[0], - Usage: params.Usage[0], + ID: params.ID[0], + CreatedAt: params.CreatedAt[0], + UserID: params.UserID[0], + AgentID: params.AgentID[0], + WorkspaceID: params.WorkspaceID[0], + TemplateID: params.TemplateID[0], + ConnectionsByProto: orig.ConnectionsByProto, + ConnectionCount: params.ConnectionCount[0], + RxPackets: params.RxPackets[0], + RxBytes: params.RxBytes[0], + TxPackets: params.TxPackets[0], + TxBytes: params.TxBytes[0], + ConnectionMedianLatencyMS: params.ConnectionMedianLatencyMS[0], + Usage: params.Usage[0], } } diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index e73583075da94..2a6898e18576a 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -384,8 +384,7 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) { ConnectionCount: 1, ConnectionMedianLatencyMS: 1, RxBytes: 1111, - SessionCountSSH: 1, - }) + }, map[string]int64{"ssh": 1}) // Stat inserted 180 days - 2 hour ago, should not be deleted before rollup. second := dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ @@ -393,8 +392,7 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) { ConnectionCount: 1, ConnectionMedianLatencyMS: 1, RxBytes: 2222, - SessionCountSSH: 1, - }) + }, map[string]int64{"ssh": 1}) // Stat inserted 179 days - 4 hour ago, should not be deleted at all. third := dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ @@ -402,8 +400,7 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) { ConnectionCount: 1, ConnectionMedianLatencyMS: 1, RxBytes: 3333, - SessionCountSSH: 1, - }) + }, map[string]int64{"ssh": 1}) // when closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, prometheus.NewRegistry(), dbpurge.WithClock(clk)) diff --git a/coderd/database/dbrollup/dbrollup_test.go b/coderd/database/dbrollup/dbrollup_test.go index ebcb6852a3fcb..4be76c89279c5 100644 --- a/coderd/database/dbrollup/dbrollup_test.go +++ b/coderd/database/dbrollup/dbrollup_test.go @@ -75,8 +75,7 @@ func TestRollup_TwoInstancesUseLocking(t *testing.T) { CreatedAt: refTime.Add(-time.Minute), ConnectionMedianLatencyMS: 1, ConnectionCount: 1, - SessionCountSSH: 1, - }) + }, map[string]int64{"ssh": 1}) closeRolluper := func(rolluper *dbrollup.Rolluper, resume chan struct{}) { close(resume) @@ -163,8 +162,7 @@ func TestRollupTemplateUsageStats(t *testing.T) { CreatedAt: anHourAndSixMonthsAgo.AddDate(0, 0, -1), ConnectionMedianLatencyMS: 1, ConnectionCount: 1, - SessionCountSSH: 1, - }) + }, map[string]int64{"ssh": 1}) _ = dbgen.WorkspaceAppStat(t, db, database.WorkspaceAppStat{ UserID: user.ID, WorkspaceID: ws.ID, @@ -176,25 +174,23 @@ func TestRollupTemplateUsageStats(t *testing.T) { // Stats inserted 6 months - 1 day ago, should be rolled up. wags1 := dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - TemplateID: tpl.ID, - WorkspaceID: ws.ID, - AgentID: agent.ID, - UserID: user.ID, - CreatedAt: anHourAndSixMonthsAgo.AddDate(0, 0, 1), - ConnectionMedianLatencyMS: 1, - ConnectionCount: 1, - SessionCountReconnectingPTY: 1, - }) + TemplateID: tpl.ID, + WorkspaceID: ws.ID, + AgentID: agent.ID, + UserID: user.ID, + CreatedAt: anHourAndSixMonthsAgo.AddDate(0, 0, 1), + ConnectionMedianLatencyMS: 1, + ConnectionCount: 1, + }, map[string]int64{"reconnecting_pty": 1}) wags2 := dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - TemplateID: tpl.ID, - WorkspaceID: ws.ID, - AgentID: agent.ID, - UserID: user.ID, - CreatedAt: wags1.CreatedAt.Add(time.Minute), - ConnectionMedianLatencyMS: 1, - ConnectionCount: 1, - SessionCountReconnectingPTY: 1, - }) + TemplateID: tpl.ID, + WorkspaceID: ws.ID, + AgentID: agent.ID, + UserID: user.ID, + CreatedAt: wags1.CreatedAt.Add(time.Minute), + ConnectionMedianLatencyMS: 1, + ConnectionCount: 1, + }, map[string]int64{"reconnecting_pty": 1}) // wags2 and waps1 overlap, so total usage is 4 - 1. waps1 := dbgen.WorkspaceAppStat(t, db, database.WorkspaceAppStat{ UserID: user.ID, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index fae68a1e3e55b..5762eb4f29d8f 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3780,6 +3780,16 @@ CREATE TABLE workspace_agent_scripts ( id uuid DEFAULT gen_random_uuid() NOT NULL ); +CREATE TABLE workspace_agent_session_counts ( + workspace_agent_stats_id uuid NOT NULL, + app_name text NOT NULL, + count bigint DEFAULT 0 NOT NULL +); + +COMMENT ON TABLE workspace_agent_session_counts IS 'Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row.'; + +COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty). Stored raw; display names and family grouping are applied at the API boundary.'; + CREATE SEQUENCE workspace_agent_startup_logs_id_seq START WITH 1 INCREMENT BY 1 @@ -3803,10 +3813,6 @@ CREATE TABLE workspace_agent_stats ( tx_packets bigint DEFAULT 0 NOT NULL, tx_bytes bigint DEFAULT 0 NOT NULL, connection_median_latency_ms double precision 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, usage boolean DEFAULT false NOT NULL ); @@ -4551,6 +4557,9 @@ ALTER TABLE ONLY workspace_agent_script_timings ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_id_key UNIQUE (id); +ALTER TABLE ONLY workspace_agent_session_counts + ADD CONSTRAINT workspace_agent_session_counts_pkey PRIMARY KEY (workspace_agent_stats_id, app_name); + ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id); @@ -4905,7 +4914,7 @@ COMMENT ON INDEX workspace_agent_scripts_workspace_agent_id_idx IS 'Foreign key CREATE INDEX workspace_agent_startup_logs_id_agent_id_idx ON workspace_agent_logs USING btree (agent_id, id); -CREATE INDEX workspace_agent_stats_template_id_created_at_user_id_idx ON workspace_agent_stats USING btree (template_id, created_at, user_id) INCLUDE (session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, connection_median_latency_ms) WHERE (connection_count > 0); +CREATE INDEX workspace_agent_stats_template_id_created_at_user_id_idx ON workspace_agent_stats USING btree (template_id, created_at, user_id) INCLUDE (connection_median_latency_ms) WHERE (connection_count > 0); COMMENT ON INDEX workspace_agent_stats_template_id_created_at_user_id_idx IS 'Support index for template insights endpoint to build interval reports faster.'; @@ -5436,6 +5445,9 @@ ALTER TABLE ONLY workspace_agent_script_timings ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_agent_session_counts + ADD CONSTRAINT workspace_agent_session_counts_workspace_agent_stats_id_fkey FOREIGN KEY (workspace_agent_stats_id) REFERENCES workspace_agent_stats(id) ON DELETE CASCADE; + ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 4b40dcf679e03..79acdd5f6ca76 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -133,6 +133,7 @@ const ( ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; ForeignKeyWorkspaceAgentScriptTimingsScriptID ForeignKeyConstraint = "workspace_agent_script_timings_script_id_fkey" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_fkey FOREIGN KEY (script_id) REFERENCES workspace_agent_scripts(id) ON DELETE CASCADE; ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentSessionCountsWorkspaceAgentStatsID ForeignKeyConstraint = "workspace_agent_session_counts_workspace_agent_stats_id_fkey" // ALTER TABLE ONLY workspace_agent_session_counts ADD CONSTRAINT workspace_agent_session_counts_workspace_agent_stats_id_fkey FOREIGN KEY (workspace_agent_stats_id) REFERENCES workspace_agent_stats(id) ON DELETE CASCADE; ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; ForeignKeyWorkspaceAgentVolumeResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_volume_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; ForeignKeyWorkspaceAgentsParentID ForeignKeyConstraint = "workspace_agents_parent_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000543_workspace_agent_session_counts.down.sql b/coderd/database/migrations/000543_workspace_agent_session_counts.down.sql new file mode 100644 index 0000000000000..3625a36e98713 --- /dev/null +++ b/coderd/database/migrations/000543_workspace_agent_session_counts.down.sql @@ -0,0 +1,33 @@ +ALTER TABLE workspace_agent_stats + ADD COLUMN session_count_vscode bigint DEFAULT 0 NOT NULL, + ADD COLUMN session_count_jetbrains bigint DEFAULT 0 NOT NULL, + ADD COLUMN session_count_reconnecting_pty bigint DEFAULT 0 NOT NULL, + ADD COLUMN session_count_ssh bigint DEFAULT 0 NOT NULL; + +-- Restore the four well-known session counts. App names outside the four +-- legacy columns cannot be represented in the old schema and are dropped. +UPDATE workspace_agent_stats was +SET + session_count_vscode = COALESCE(sc.vscode, 0), + session_count_jetbrains = COALESCE(sc.jetbrains, 0), + session_count_reconnecting_pty = COALESCE(sc.reconnecting_pty, 0), + session_count_ssh = COALESCE(sc.ssh, 0) +FROM ( + SELECT + workspace_agent_stats_id, + SUM(count) FILTER (WHERE app_name = 'vscode') AS vscode, + SUM(count) FILTER (WHERE app_name = 'jetbrains') AS jetbrains, + SUM(count) FILTER (WHERE app_name = 'reconnecting_pty') AS reconnecting_pty, + SUM(count) FILTER (WHERE app_name = 'ssh') AS ssh + FROM workspace_agent_session_counts + GROUP BY workspace_agent_stats_id +) sc +WHERE sc.workspace_agent_stats_id = was.id; + +DROP TABLE workspace_agent_session_counts; + +DROP INDEX workspace_agent_stats_template_id_created_at_user_id_idx; + +CREATE INDEX workspace_agent_stats_template_id_created_at_user_id_idx ON workspace_agent_stats USING btree (template_id, created_at, user_id) INCLUDE (session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, connection_median_latency_ms) WHERE (connection_count > 0); + +COMMENT ON INDEX workspace_agent_stats_template_id_created_at_user_id_idx IS 'Support index for template insights endpoint to build interval reports faster.'; diff --git a/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql b/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql new file mode 100644 index 0000000000000..cc55d919585c1 --- /dev/null +++ b/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE workspace_agent_session_counts ( + workspace_agent_stats_id uuid NOT NULL REFERENCES workspace_agent_stats (id) ON DELETE CASCADE, + app_name text NOT NULL, + count bigint NOT NULL DEFAULT 0, + PRIMARY KEY (workspace_agent_stats_id, app_name) +); + +COMMENT ON TABLE workspace_agent_session_counts IS 'Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row.'; +COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty). Stored raw; display names and family grouping are applied at the API boundary.'; + +-- Copy the ephemeral buffer (~1 day of rows) into the new table so that +-- template usage rollup and deployment stats see no gap during the upgrade. +INSERT INTO workspace_agent_session_counts (workspace_agent_stats_id, app_name, count) +SELECT id, 'vscode', session_count_vscode FROM workspace_agent_stats WHERE session_count_vscode > 0 +UNION ALL +SELECT id, 'jetbrains', session_count_jetbrains FROM workspace_agent_stats WHERE session_count_jetbrains > 0 +UNION ALL +SELECT id, 'reconnecting_pty', session_count_reconnecting_pty FROM workspace_agent_stats WHERE session_count_reconnecting_pty > 0 +UNION ALL +SELECT id, 'ssh', session_count_ssh FROM workspace_agent_stats WHERE session_count_ssh > 0; + +DROP INDEX workspace_agent_stats_template_id_created_at_user_id_idx; + +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; + +CREATE INDEX workspace_agent_stats_template_id_created_at_user_id_idx ON workspace_agent_stats USING btree (template_id, created_at, user_id) INCLUDE (connection_median_latency_ms) WHERE (connection_count > 0); + +COMMENT ON INDEX workspace_agent_stats_template_id_created_at_user_id_idx IS 'Support index for template insights endpoint to build interval reports faster.'; diff --git a/coderd/database/migrations/testdata/fixtures/000543_workspace_agent_session_counts.up.sql b/coderd/database/migrations/testdata/fixtures/000543_workspace_agent_session_counts.up.sql new file mode 100644 index 0000000000000..57a03c3c0a36b --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000543_workspace_agent_session_counts.up.sql @@ -0,0 +1,28 @@ +INSERT INTO workspace_agent_stats ( + id, + created_at, + user_id, + agent_id, + workspace_id, + template_id, + connection_count, + connection_median_latency_ms +) VALUES ( + '4a382ba5-6e57-4a58-991e-d4ac4f6c1012', + NOW(), + gen_random_uuid(), + gen_random_uuid(), + gen_random_uuid(), + gen_random_uuid(), + 1::bigint, + 1::bigint +); + +INSERT INTO workspace_agent_session_counts ( + workspace_agent_stats_id, + app_name, + count +) VALUES + ('4a382ba5-6e57-4a58-991e-d4ac4f6c1012', 'vscode', 2), + ('4a382ba5-6e57-4a58-991e-d4ac4f6c1012', 'ssh', 1), + ('4a382ba5-6e57-4a58-991e-d4ac4f6c1012', 'SomeFutureIDE', 1); diff --git a/coderd/database/models.go b/coderd/database/models.go index 0018fe1efebb8..d2401a90d9c49 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -6493,25 +6493,29 @@ type WorkspaceAgentScriptTiming struct { Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"` } +// Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row. +type WorkspaceAgentSessionCount struct { + WorkspaceAgentStatsID uuid.UUID `db:"workspace_agent_stats_id" json:"workspace_agent_stats_id"` + // App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty). Stored raw; display names and family grouping are applied at the API boundary. + AppName string `db:"app_name" json:"app_name"` + Count int64 `db:"count" json:"count"` +} + 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"` - ConnectionMedianLatencyMS float64 `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"` - Usage bool `db:"usage" json:"usage"` + 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 float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` + Usage bool `db:"usage" json:"usage"` } type WorkspaceAgentVolumeResourceMonitor struct { diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 008b6ef4dbb36..5c531a8903f73 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -57,14 +57,12 @@ func TestGetDeploymentWorkspaceAgentStats(t *testing.T) { TxBytes: 1, RxBytes: 1, ConnectionMedianLatencyMS: 1, - SessionCountVSCode: 1, - }) + }, map[string]int64{"vscode": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ TxBytes: 1, RxBytes: 1, ConnectionMedianLatencyMS: 2, - SessionCountVSCode: 1, - }) + }, map[string]int64{"vscode": 1}) stats, err := db.GetDeploymentWorkspaceAgentStats(ctx, dbtime.Now().Add(-time.Hour)) require.NoError(t, err) @@ -91,8 +89,7 @@ func TestGetDeploymentWorkspaceAgentStats(t *testing.T) { TxBytes: 1, RxBytes: 1, ConnectionMedianLatencyMS: 1, - SessionCountVSCode: 1, - }) + }, map[string]int64{"vscode": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ // Ensure this stat is newer! CreatedAt: insertTime, @@ -100,8 +97,7 @@ func TestGetDeploymentWorkspaceAgentStats(t *testing.T) { TxBytes: 1, RxBytes: 1, ConnectionMedianLatencyMS: 2, - SessionCountVSCode: 1, - }) + }, map[string]int64{"vscode": 1}) stats, err := db.GetDeploymentWorkspaceAgentStats(ctx, dbtime.Now().Add(-time.Hour)) require.NoError(t, err) @@ -135,21 +131,17 @@ func TestGetDeploymentWorkspaceAgentUsageStats(t *testing.T) { RxBytes: 1, ConnectionMedianLatencyMS: 1, // Should be ignored - SessionCountSSH: 4, - SessionCountVSCode: 3, - }) + }, map[string]int64{"ssh": 4, "vscode": 3}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime.Add(-time.Minute), - AgentID: agentID, - SessionCountVSCode: 1, - Usage: true, - }) + CreatedAt: insertTime.Add(-time.Minute), + AgentID: agentID, + Usage: true, + }, map[string]int64{"vscode": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime.Add(-time.Minute), - AgentID: agentID, - SessionCountReconnectingPTY: 1, - Usage: true, - }) + CreatedAt: insertTime.Add(-time.Minute), + AgentID: agentID, + Usage: true, + }, map[string]int64{"reconnecting_pty": 1}) // Latest stats dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ @@ -159,21 +151,17 @@ func TestGetDeploymentWorkspaceAgentUsageStats(t *testing.T) { RxBytes: 1, ConnectionMedianLatencyMS: 2, // Should be ignored - SessionCountSSH: 3, - SessionCountVSCode: 1, - }) + }, map[string]int64{"ssh": 3, "vscode": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agentID, - SessionCountVSCode: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agentID, + Usage: true, + }, map[string]int64{"vscode": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agentID, - SessionCountSSH: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agentID, + Usage: true, + }, map[string]int64{"ssh": 1}) stats, err := db.GetDeploymentWorkspaceAgentUsageStats(ctx, dbtime.Now().Add(-time.Hour)) require.NoError(t, err) @@ -206,9 +194,7 @@ func TestGetDeploymentWorkspaceAgentUsageStats(t *testing.T) { RxBytes: 4, ConnectionMedianLatencyMS: 2, // Should be ignored - SessionCountSSH: 3, - SessionCountVSCode: 1, - }) + }, map[string]int64{"ssh": 3, "vscode": 1}) stats, err := db.GetDeploymentWorkspaceAgentUsageStats(ctx, dbtime.Now().Add(-time.Hour)) require.NoError(t, err) @@ -741,18 +727,15 @@ func TestGetWorkspaceAgentUsageStats(t *testing.T) { RxBytes: 1, ConnectionMedianLatencyMS: 1, // Should be ignored - SessionCountVSCode: 3, - SessionCountSSH: 1, - }) + }, map[string]int64{"vscode": 3, "ssh": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime.Add(-time.Minute), - AgentID: agentID1, - WorkspaceID: workspaceID1, - TemplateID: templateID1, - UserID: userID1, - SessionCountVSCode: 1, - Usage: true, - }) + CreatedAt: insertTime.Add(-time.Minute), + AgentID: agentID1, + WorkspaceID: workspaceID1, + TemplateID: templateID1, + UserID: userID1, + Usage: true, + }, map[string]int64{"vscode": 1}) // Latest workspace 1 stats dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ @@ -765,27 +748,23 @@ func TestGetWorkspaceAgentUsageStats(t *testing.T) { RxBytes: 2, ConnectionMedianLatencyMS: 1, // Should be ignored - SessionCountVSCode: 3, - SessionCountSSH: 4, - }) + }, map[string]int64{"vscode": 3, "ssh": 4}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agentID1, - WorkspaceID: workspaceID1, - TemplateID: templateID1, - UserID: userID1, - SessionCountVSCode: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agentID1, + WorkspaceID: workspaceID1, + TemplateID: templateID1, + UserID: userID1, + Usage: true, + }, map[string]int64{"vscode": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agentID1, - WorkspaceID: workspaceID1, - TemplateID: templateID1, - UserID: userID1, - SessionCountJetBrains: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agentID1, + WorkspaceID: workspaceID1, + TemplateID: templateID1, + UserID: userID1, + Usage: true, + }, map[string]int64{"jetbrains": 1}) // Latest workspace 2 stats dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ @@ -808,27 +787,23 @@ func TestGetWorkspaceAgentUsageStats(t *testing.T) { RxBytes: 3, ConnectionMedianLatencyMS: 1, // Should be ignored - SessionCountVSCode: 3, - SessionCountSSH: 4, - }) + }, map[string]int64{"vscode": 3, "ssh": 4}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agentID2, - WorkspaceID: workspaceID2, - TemplateID: templateID2, - UserID: userID2, - SessionCountSSH: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agentID2, + WorkspaceID: workspaceID2, + TemplateID: templateID2, + UserID: userID2, + Usage: true, + }, map[string]int64{"ssh": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agentID2, - WorkspaceID: workspaceID2, - TemplateID: templateID2, - UserID: userID2, - SessionCountJetBrains: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agentID2, + WorkspaceID: workspaceID2, + TemplateID: templateID2, + UserID: userID2, + Usage: true, + }, map[string]int64{"jetbrains": 1}) reqTime := dbtime.Now().Add(-time.Hour) stats, err := db.GetWorkspaceAgentUsageStats(ctx, reqTime) @@ -872,9 +847,7 @@ func TestGetWorkspaceAgentUsageStats(t *testing.T) { RxBytes: 4, ConnectionMedianLatencyMS: 2, // Should be ignored - SessionCountSSH: 3, - SessionCountVSCode: 1, - }) + }, map[string]int64{"ssh": 3, "vscode": 1}) stats, err := db.GetWorkspaceAgentUsageStats(ctx, dbtime.Now().Add(-time.Hour)) require.NoError(t, err) @@ -951,18 +924,15 @@ func TestGetWorkspaceAgentUsageStatsAndLabels(t *testing.T) { RxBytes: 1, ConnectionMedianLatencyMS: 1, // Should be ignored - SessionCountVSCode: 3, - SessionCountSSH: 1, - }) + }, map[string]int64{"vscode": 3, "ssh": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime.Add(-time.Minute), - AgentID: agent1.ID, - WorkspaceID: workspace1.ID, - TemplateID: template1.ID, - UserID: user1.ID, - SessionCountVSCode: 1, - Usage: true, - }) + CreatedAt: insertTime.Add(-time.Minute), + AgentID: agent1.ID, + WorkspaceID: workspace1.ID, + TemplateID: template1.ID, + UserID: user1.ID, + Usage: true, + }, map[string]int64{"vscode": 1}) // Latest workspace 1 stats dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ @@ -975,27 +945,23 @@ func TestGetWorkspaceAgentUsageStatsAndLabels(t *testing.T) { RxBytes: 2, ConnectionMedianLatencyMS: 1, // Should be ignored - SessionCountVSCode: 4, - SessionCountSSH: 3, - }) + }, map[string]int64{"vscode": 4, "ssh": 3}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agent1.ID, - WorkspaceID: workspace1.ID, - TemplateID: template1.ID, - UserID: user1.ID, - SessionCountJetBrains: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agent1.ID, + WorkspaceID: workspace1.ID, + TemplateID: template1.ID, + UserID: user1.ID, + Usage: true, + }, map[string]int64{"jetbrains": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agent1.ID, - WorkspaceID: workspace1.ID, - TemplateID: template1.ID, - UserID: user1.ID, - SessionCountReconnectingPTY: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agent1.ID, + WorkspaceID: workspace1.ID, + TemplateID: template1.ID, + UserID: user1.ID, + Usage: true, + }, map[string]int64{"reconnecting_pty": 1}) // Latest workspace 2 stats dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ @@ -1009,23 +975,21 @@ func TestGetWorkspaceAgentUsageStatsAndLabels(t *testing.T) { ConnectionMedianLatencyMS: 1, }) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agent2.ID, - WorkspaceID: workspace2.ID, - TemplateID: template2.ID, - UserID: user2.ID, - SessionCountVSCode: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agent2.ID, + WorkspaceID: workspace2.ID, + TemplateID: template2.ID, + UserID: user2.ID, + Usage: true, + }, map[string]int64{"vscode": 1}) dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ - CreatedAt: insertTime, - AgentID: agent2.ID, - WorkspaceID: workspace2.ID, - TemplateID: template2.ID, - UserID: user2.ID, - SessionCountSSH: 1, - Usage: true, - }) + CreatedAt: insertTime, + AgentID: agent2.ID, + WorkspaceID: workspace2.ID, + TemplateID: template2.ID, + UserID: user2.ID, + Usage: true, + }, map[string]int64{"ssh": 1}) stats, err := db.GetWorkspaceAgentUsageStatsAndLabels(ctx, insertTime.Add(-time.Hour)) require.NoError(t, err) @@ -1092,9 +1056,7 @@ func TestGetWorkspaceAgentUsageStatsAndLabels(t *testing.T) { TxBytes: 5, ConnectionMedianLatencyMS: 1, // Should be ignored - SessionCountVSCode: 3, - SessionCountSSH: 1, - }) + }, map[string]int64{"vscode": 3, "ssh": 1}) stats, err := db.GetWorkspaceAgentUsageStatsAndLabels(ctx, insertTime.Add(-time.Hour)) require.NoError(t, err) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index cb2663638c7f3..b5953487bdc02 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15616,35 +15616,41 @@ WITH -- every minute (per user). insights AS ( SELECT - template_id, - user_id, - COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins, - -- TODO(mafredri): Enable when we have the column. - -- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins, - COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins, + was.template_id, + was.user_id, + COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection -- within this bucket". A better solution here would be preferable. - MAX(connection_count) > 0 AS has_connection + MAX(was.connection_count) > 0 AS has_connection FROM - workspace_agent_stats + workspace_agent_stats was + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE - created_at >= $1::timestamptz - AND created_at < $2::timestamptz + was.created_at >= $1::timestamptz + AND was.created_at < $2::timestamptz -- Inclusion criteria to filter out empty results. AND ( - session_count_ssh > 0 - -- TODO(mafredri): Enable when we have the column. - -- OR session_count_sftp > 0 - OR session_count_reconnecting_pty > 0 - OR session_count_vscode > 0 - OR session_count_jetbrains > 0 + sc.ssh > 0 + OR sc.reconnecting_pty > 0 + OR sc.vscode > 0 + OR sc.jetbrains > 0 ) GROUP BY - template_id, user_id + was.template_id, was.user_id ) SELECT @@ -16248,54 +16254,58 @@ WITH SELECT -- Truncate the minute to the nearest half hour, this is the bucket size -- for the data. - date_trunc('hour', created_at) + trunc(date_part('minute', created_at) / 30) * 30 * '1 minute'::interval AS time_bucket, - template_id, - user_id, + date_trunc('hour', was.created_at) + trunc(date_part('minute', was.created_at) / 30) * 30 * '1 minute'::interval AS time_bucket, + was.template_id, + was.user_id, -- Store each unique minute bucket for later merge between datasets. array_agg( DISTINCT CASE WHEN - session_count_ssh > 0 - -- TODO(mafredri): Enable when we have the column. - -- OR session_count_sftp > 0 - OR session_count_reconnecting_pty > 0 - OR session_count_vscode > 0 - OR session_count_jetbrains > 0 + sc.ssh > 0 + OR sc.reconnecting_pty > 0 + OR sc.vscode > 0 + OR sc.jetbrains > 0 THEN - date_trunc('minute', created_at) + date_trunc('minute', was.created_at) ELSE NULL END ) AS minute_buckets, - COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins, - -- TODO(mafredri): Enable when we have the column. - -- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins, - COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins, + COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection -- during this half-hour". A better solution here would be preferable. - MAX(connection_count) > 0 AS has_connection + MAX(was.connection_count) > 0 AS has_connection FROM - workspace_agent_stats + workspace_agent_stats was + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE -- created_at >= @start_time::timestamptz -- AND created_at < @end_time::timestamptz - created_at >= (SELECT t FROM latest_start) - AND created_at < NOW() + was.created_at >= (SELECT t FROM latest_start) + AND was.created_at < NOW() -- Inclusion criteria to filter out empty results. AND ( - session_count_ssh > 0 - -- TODO(mafredri): Enable when we have the column. - -- OR session_count_sftp > 0 - OR session_count_reconnecting_pty > 0 - OR session_count_vscode > 0 - OR session_count_jetbrains > 0 + sc.ssh > 0 + OR sc.reconnecting_pty > 0 + OR sc.vscode > 0 + OR sc.jetbrains > 0 ) GROUP BY - time_bucket, template_id, user_id + time_bucket, was.template_id, was.user_id ), stats AS ( SELECT @@ -33954,30 +33964,43 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { const getDeploymentWorkspaceAgentStats = `-- name: GetDeploymentWorkspaceAgentStats :one WITH stats AS ( SELECT + id, agent_id, created_at, rx_bytes, tx_bytes, connection_median_latency_ms, - session_count_vscode, - session_count_ssh, - session_count_jetbrains, - session_count_reconnecting_pty, ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats WHERE created_at > $1 +), byte_stats AS ( + SELECT + coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes, + coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes, + -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. + coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50, + coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95 + FROM stats +), session_stats AS ( + -- Session counts from the latest stats row per agent. + SELECT + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + FROM stats + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = stats.id + ) sc ON TRUE + WHERE stats.rn = 1 ) -SELECT - coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes, - coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes, - -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. - coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50, - coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95, - coalesce(SUM(session_count_vscode) FILTER (WHERE rn = 1), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh) FILTER (WHERE rn = 1), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains) FILTER (WHERE rn = 1), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty) FILTER (WHERE rn = 1), 0)::bigint AS session_count_reconnecting_pty -FROM stats +SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM byte_stats, session_stats ` type GetDeploymentWorkspaceAgentStatsRow struct { @@ -34020,20 +34043,29 @@ WITH agent_stats AS ( ), minute_buckets AS ( SELECT - agent_id, - date_trunc('minute', created_at) AS minute_bucket, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + was.agent_id, + date_trunc('minute', was.created_at) AS minute_bucket, + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty FROM - workspace_agent_stats + workspace_agent_stats was + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE - created_at >= $1 - AND created_at < date_trunc('minute', now()) -- Exclude current partial minute - AND usage = true + was.created_at >= $1 + AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute + AND was.usage = true GROUP BY - agent_id, + was.agent_id, minute_bucket ), latest_buckets AS ( @@ -34108,14 +34140,24 @@ WITH agent_stats AS ( ), latest_agent_stats AS ( SELECT a.agent_id, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty FROM ( - SELECT 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, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn + SELECT 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, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats WHERE created_at > $1 - ) AS a WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id + ) AS a + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = a.id + ) sc ON TRUE + WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id ) SELECT user_id, agent_stats.agent_id, workspace_id, template_id, aggregated_from, workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, latest_agent_stats.agent_id, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id ` @@ -34189,18 +34231,28 @@ WITH agent_stats AS ( ), latest_agent_stats AS ( SELECT a.agent_id, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, - coalesce(SUM(connection_count), 0)::bigint AS connection_count, - coalesce(MAX(connection_median_latency_ms), 0)::float AS connection_median_latency_ms + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, + coalesce(SUM(a.connection_count), 0)::bigint AS connection_count, + coalesce(MAX(a.connection_median_latency_ms), 0)::float AS connection_median_latency_ms FROM ( - SELECT 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, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn + SELECT 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, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. WHERE created_at > $1 AND connection_median_latency_ms > 0 ) AS a + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = a.id + ) sc ON TRUE WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id ) @@ -34296,25 +34348,34 @@ WITH agent_stats AS ( ), minute_buckets AS ( SELECT - agent_id, - date_trunc('minute', created_at) AS minute_bucket, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + was.agent_id, + date_trunc('minute', was.created_at) AS minute_bucket, + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty FROM - workspace_agent_stats + workspace_agent_stats was + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE - created_at >= $1 - AND created_at < date_trunc('minute', now()) -- Exclude current partial minute - AND usage = true + was.created_at >= $1 + AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute + AND was.usage = true GROUP BY - agent_id, + was.agent_id, minute_bucket, - user_id, - agent_id, - workspace_id, - template_id + was.user_id, + was.agent_id, + was.workspace_id, + was.template_id ), latest_buckets AS ( SELECT DISTINCT ON (agent_id) @@ -34417,17 +34478,27 @@ WITH agent_stats AS ( GROUP BY user_id, agent_id, workspace_id ), latest_agent_stats AS ( SELECT - agent_id, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, - coalesce(SUM(connection_count), 0)::bigint AS connection_count - FROM workspace_agent_stats + was.agent_id, + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, + coalesce(SUM(was.connection_count), 0)::bigint AS connection_count + FROM workspace_agent_stats was + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE -- We only want the latest stats, but those stats might be -- spread across multiple rows. - WHERE usage = true AND created_at > now() - '1 minute'::interval - GROUP BY user_id, agent_id, workspace_id + WHERE was.usage = true AND was.created_at > now() - '1 minute'::interval + GROUP BY was.user_id, was.agent_id, was.workspace_id ) SELECT users.username, workspace_agents.name AS agent_name, workspaces.name AS workspace_name, rx_bytes, tx_bytes, @@ -34507,67 +34578,73 @@ func (q *sqlQuerier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, c } const insertWorkspaceAgentStats = `-- name: InsertWorkspaceAgentStats :exec -INSERT INTO - workspace_agent_stats ( - id, - created_at, - user_id, - workspace_id, - template_id, - agent_id, - connections_by_proto, - connection_count, - rx_packets, - rx_bytes, - tx_packets, - tx_bytes, - session_count_vscode, - session_count_jetbrains, - session_count_reconnecting_pty, - session_count_ssh, - connection_median_latency_ms, - usage - ) -SELECT - unnest($1 :: uuid[]) AS id, - unnest($2 :: timestamptz[]) AS created_at, - unnest($3 :: uuid[]) AS user_id, - unnest($4 :: uuid[]) AS workspace_id, - unnest($5 :: uuid[]) AS template_id, - unnest($6 :: uuid[]) AS agent_id, - jsonb_array_elements($7 :: jsonb) AS connections_by_proto, - unnest($8 :: bigint[]) AS connection_count, - unnest($9 :: bigint[]) AS rx_packets, - unnest($10 :: bigint[]) AS rx_bytes, - unnest($11 :: bigint[]) AS tx_packets, - unnest($12 :: bigint[]) AS tx_bytes, - unnest($13 :: bigint[]) AS session_count_vscode, - unnest($14 :: bigint[]) AS session_count_jetbrains, - unnest($15 :: bigint[]) AS session_count_reconnecting_pty, - unnest($16 :: bigint[]) AS session_count_ssh, - unnest($17 :: double precision[]) AS connection_median_latency_ms, - unnest($18 :: boolean[]) AS usage +WITH stats AS ( + INSERT INTO + workspace_agent_stats ( + id, + created_at, + user_id, + workspace_id, + template_id, + agent_id, + connections_by_proto, + connection_count, + rx_packets, + rx_bytes, + tx_packets, + tx_bytes, + connection_median_latency_ms, + usage + ) + SELECT + unnest($1 :: uuid[]) AS id, + unnest($2 :: timestamptz[]) AS created_at, + unnest($3 :: uuid[]) AS user_id, + unnest($4 :: uuid[]) AS workspace_id, + unnest($5 :: uuid[]) AS template_id, + unnest($6 :: uuid[]) AS agent_id, + jsonb_array_elements($7 :: jsonb) AS connections_by_proto, + unnest($8 :: bigint[]) AS connection_count, + unnest($9 :: bigint[]) AS rx_packets, + unnest($10 :: bigint[]) AS rx_bytes, + unnest($11 :: bigint[]) AS tx_packets, + unnest($12 :: bigint[]) AS tx_bytes, + unnest($13 :: double precision[]) AS connection_median_latency_ms, + unnest($14 :: boolean[]) AS usage +), +session_data AS ( + -- @session_counts is a JSONB array with one object per stats row (zipped + -- with @id by position, same contract as @connections_by_proto), each + -- object mapping app name to session count. + SELECT + unnest($1 :: uuid[]) AS stats_id, + jsonb_array_elements($15 :: jsonb) AS counts +) +INSERT INTO workspace_agent_session_counts (workspace_agent_stats_id, app_name, count) +SELECT + sd.stats_id, + kv.key, + (kv.value)::bigint +FROM session_data sd, jsonb_each_text(sd.counts) kv +WHERE (kv.value)::bigint > 0 ` type InsertWorkspaceAgentStatsParams 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"` - 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 []float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` - Usage []bool `db:"usage" json:"usage"` + 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"` + ConnectionMedianLatencyMS []float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` + Usage []bool `db:"usage" json:"usage"` + SessionCounts json.RawMessage `db:"session_counts" json:"session_counts"` } func (q *sqlQuerier) InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error { @@ -34584,12 +34661,9 @@ func (q *sqlQuerier) InsertWorkspaceAgentStats(ctx context.Context, arg InsertWo pq.Array(arg.RxBytes), pq.Array(arg.TxPackets), pq.Array(arg.TxBytes), - pq.Array(arg.SessionCountVSCode), - pq.Array(arg.SessionCountJetBrains), - pq.Array(arg.SessionCountReconnectingPTY), - pq.Array(arg.SessionCountSSH), pq.Array(arg.ConnectionMedianLatencyMS), pq.Array(arg.Usage), + arg.SessionCounts, ) return err } diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index b589ce4e9a6fe..43bf30f7f05f8 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -154,35 +154,41 @@ WITH -- every minute (per user). insights AS ( SELECT - template_id, - user_id, - COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins, - -- TODO(mafredri): Enable when we have the column. - -- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins, - COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins, + was.template_id, + was.user_id, + COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection -- within this bucket". A better solution here would be preferable. - MAX(connection_count) > 0 AS has_connection + MAX(was.connection_count) > 0 AS has_connection FROM - workspace_agent_stats + workspace_agent_stats was + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE - created_at >= @start_time::timestamptz - AND created_at < @end_time::timestamptz + was.created_at >= @start_time::timestamptz + AND was.created_at < @end_time::timestamptz -- Inclusion criteria to filter out empty results. AND ( - session_count_ssh > 0 - -- TODO(mafredri): Enable when we have the column. - -- OR session_count_sftp > 0 - OR session_count_reconnecting_pty > 0 - OR session_count_vscode > 0 - OR session_count_jetbrains > 0 + sc.ssh > 0 + OR sc.reconnecting_pty > 0 + OR sc.vscode > 0 + OR sc.jetbrains > 0 ) GROUP BY - template_id, user_id + was.template_id, was.user_id ) SELECT @@ -555,54 +561,58 @@ WITH SELECT -- Truncate the minute to the nearest half hour, this is the bucket size -- for the data. - date_trunc('hour', created_at) + trunc(date_part('minute', created_at) / 30) * 30 * '1 minute'::interval AS time_bucket, - template_id, - user_id, + date_trunc('hour', was.created_at) + trunc(date_part('minute', was.created_at) / 30) * 30 * '1 minute'::interval AS time_bucket, + was.template_id, + was.user_id, -- Store each unique minute bucket for later merge between datasets. array_agg( DISTINCT CASE WHEN - session_count_ssh > 0 - -- TODO(mafredri): Enable when we have the column. - -- OR session_count_sftp > 0 - OR session_count_reconnecting_pty > 0 - OR session_count_vscode > 0 - OR session_count_jetbrains > 0 + sc.ssh > 0 + OR sc.reconnecting_pty > 0 + OR sc.vscode > 0 + OR sc.jetbrains > 0 THEN - date_trunc('minute', created_at) + date_trunc('minute', was.created_at) ELSE NULL END ) AS minute_buckets, - COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins, - -- TODO(mafredri): Enable when we have the column. - -- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins, - COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins, + COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection -- during this half-hour". A better solution here would be preferable. - MAX(connection_count) > 0 AS has_connection + MAX(was.connection_count) > 0 AS has_connection FROM - workspace_agent_stats + workspace_agent_stats was + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE -- created_at >= @start_time::timestamptz -- AND created_at < @end_time::timestamptz - created_at >= (SELECT t FROM latest_start) - AND created_at < NOW() + was.created_at >= (SELECT t FROM latest_start) + AND was.created_at < NOW() -- Inclusion criteria to filter out empty results. AND ( - session_count_ssh > 0 - -- TODO(mafredri): Enable when we have the column. - -- OR session_count_sftp > 0 - OR session_count_reconnecting_pty > 0 - OR session_count_vscode > 0 - OR session_count_jetbrains > 0 + sc.ssh > 0 + OR sc.reconnecting_pty > 0 + OR sc.vscode > 0 + OR sc.jetbrains > 0 ) GROUP BY - time_bucket, template_id, user_id + time_bucket, was.template_id, was.user_id ), stats AS ( SELECT diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index 28c17d8271e8d..675316e594775 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -1,44 +1,53 @@ -- name: InsertWorkspaceAgentStats :exec -INSERT INTO - workspace_agent_stats ( - id, - created_at, - user_id, - workspace_id, - template_id, - agent_id, - connections_by_proto, - connection_count, - rx_packets, - rx_bytes, - tx_packets, - tx_bytes, - session_count_vscode, - session_count_jetbrains, - session_count_reconnecting_pty, - session_count_ssh, - connection_median_latency_ms, - usage - ) +WITH stats AS ( + INSERT INTO + workspace_agent_stats ( + id, + created_at, + user_id, + workspace_id, + template_id, + agent_id, + connections_by_proto, + connection_count, + rx_packets, + rx_bytes, + tx_packets, + tx_bytes, + connection_median_latency_ms, + usage + ) + SELECT + unnest(@id :: uuid[]) AS id, + unnest(@created_at :: timestamptz[]) AS created_at, + unnest(@user_id :: uuid[]) AS user_id, + unnest(@workspace_id :: uuid[]) AS workspace_id, + unnest(@template_id :: uuid[]) AS template_id, + unnest(@agent_id :: uuid[]) AS agent_id, + jsonb_array_elements(@connections_by_proto :: jsonb) AS connections_by_proto, + unnest(@connection_count :: bigint[]) AS connection_count, + unnest(@rx_packets :: bigint[]) AS rx_packets, + unnest(@rx_bytes :: bigint[]) AS rx_bytes, + unnest(@tx_packets :: bigint[]) AS tx_packets, + unnest(@tx_bytes :: bigint[]) AS tx_bytes, + unnest(@connection_median_latency_ms :: double precision[]) AS connection_median_latency_ms, + unnest(@usage :: boolean[]) AS usage +), +session_data AS ( + -- @session_counts is a JSONB array with one object per stats row (zipped + -- with @id by position, same contract as @connections_by_proto), each + -- object mapping app name to session count. + SELECT + unnest(@id :: uuid[]) AS stats_id, + jsonb_array_elements(@session_counts :: jsonb) AS counts +) +INSERT INTO workspace_agent_session_counts (workspace_agent_stats_id, app_name, count) SELECT - unnest(@id :: uuid[]) AS id, - unnest(@created_at :: timestamptz[]) AS created_at, - unnest(@user_id :: uuid[]) AS user_id, - unnest(@workspace_id :: uuid[]) AS workspace_id, - unnest(@template_id :: uuid[]) AS template_id, - unnest(@agent_id :: uuid[]) AS agent_id, - jsonb_array_elements(@connections_by_proto :: jsonb) AS connections_by_proto, - unnest(@connection_count :: bigint[]) AS connection_count, - unnest(@rx_packets :: bigint[]) AS rx_packets, - unnest(@rx_bytes :: bigint[]) AS rx_bytes, - unnest(@tx_packets :: bigint[]) AS tx_packets, - unnest(@tx_bytes :: bigint[]) AS tx_bytes, - unnest(@session_count_vscode :: bigint[]) AS session_count_vscode, - unnest(@session_count_jetbrains :: bigint[]) AS session_count_jetbrains, - unnest(@session_count_reconnecting_pty :: bigint[]) AS session_count_reconnecting_pty, - unnest(@session_count_ssh :: bigint[]) AS session_count_ssh, - unnest(@connection_median_latency_ms :: double precision[]) AS connection_median_latency_ms, - unnest(@usage :: boolean[]) AS usage; + sd.stats_id, + kv.key, + (kv.value)::bigint +FROM session_data sd, jsonb_each_text(sd.counts) kv +WHERE (kv.value)::bigint > 0; -- name: DeleteOldWorkspaceAgentStats :exec DELETE FROM @@ -74,30 +83,43 @@ WHERE -- name: GetDeploymentWorkspaceAgentStats :one WITH stats AS ( SELECT + id, agent_id, created_at, rx_bytes, tx_bytes, connection_median_latency_ms, - session_count_vscode, - session_count_ssh, - session_count_jetbrains, - session_count_reconnecting_pty, ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats WHERE created_at > $1 +), byte_stats AS ( + SELECT + coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes, + coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes, + -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. + coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50, + coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95 + FROM stats +), session_stats AS ( + -- Session counts from the latest stats row per agent. + SELECT + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + FROM stats + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = stats.id + ) sc ON TRUE + WHERE stats.rn = 1 ) -SELECT - coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes, - coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes, - -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. - coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50, - coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95, - coalesce(SUM(session_count_vscode) FILTER (WHERE rn = 1), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh) FILTER (WHERE rn = 1), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains) FILTER (WHERE rn = 1), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty) FILTER (WHERE rn = 1), 0)::bigint AS session_count_reconnecting_pty -FROM stats; +SELECT * FROM byte_stats, session_stats; -- name: GetDeploymentWorkspaceAgentUsageStats :one WITH agent_stats AS ( @@ -112,20 +134,29 @@ WITH agent_stats AS ( ), minute_buckets AS ( SELECT - agent_id, - date_trunc('minute', created_at) AS minute_bucket, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + was.agent_id, + date_trunc('minute', was.created_at) AS minute_bucket, + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty FROM - workspace_agent_stats + workspace_agent_stats was + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE - created_at >= $1 - AND created_at < date_trunc('minute', now()) -- Exclude current partial minute - AND usage = true + was.created_at >= $1 + AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute + AND was.usage = true GROUP BY - agent_id, + was.agent_id, minute_bucket ), latest_buckets AS ( @@ -172,14 +203,24 @@ WITH agent_stats AS ( ), latest_agent_stats AS ( SELECT a.agent_id, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats WHERE created_at > $1 - ) AS a WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id + ) AS a + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = a.id + ) sc ON TRUE + WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id ) SELECT * FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id; @@ -202,25 +243,34 @@ WITH agent_stats AS ( ), minute_buckets AS ( SELECT - agent_id, - date_trunc('minute', created_at) AS minute_bucket, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + was.agent_id, + date_trunc('minute', was.created_at) AS minute_bucket, + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty FROM - workspace_agent_stats + workspace_agent_stats was + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE WHERE - created_at >= $1 - AND created_at < date_trunc('minute', now()) -- Exclude current partial minute - AND usage = true + was.created_at >= $1 + AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute + AND was.usage = true GROUP BY - agent_id, + was.agent_id, minute_bucket, - user_id, - agent_id, - workspace_id, - template_id + was.user_id, + was.agent_id, + was.workspace_id, + was.template_id ), latest_buckets AS ( SELECT DISTINCT ON (agent_id) @@ -266,18 +316,28 @@ WITH agent_stats AS ( ), latest_agent_stats AS ( SELECT a.agent_id, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, - coalesce(SUM(connection_count), 0)::bigint AS connection_count, - coalesce(MAX(connection_median_latency_ms), 0)::float AS connection_median_latency_ms + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, + coalesce(SUM(a.connection_count), 0)::bigint AS connection_count, + coalesce(MAX(a.connection_median_latency_ms), 0)::float AS connection_median_latency_ms FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats -- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms. WHERE created_at > $1 AND connection_median_latency_ms > 0 ) AS a + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = a.id + ) sc ON TRUE WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id ) @@ -319,17 +379,27 @@ WITH agent_stats AS ( GROUP BY user_id, agent_id, workspace_id ), latest_agent_stats AS ( SELECT - agent_id, - coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, - coalesce(SUM(connection_count), 0)::bigint AS connection_count - FROM workspace_agent_stats + was.agent_id, + coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, + coalesce(SUM(was.connection_count), 0)::bigint AS connection_count + FROM workspace_agent_stats was + -- One lateral row per stats row, so the aggregates above don't fan out. + LEFT JOIN LATERAL ( + SELECT + coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, + coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, + coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, + coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty + FROM workspace_agent_session_counts + WHERE workspace_agent_stats_id = was.id + ) sc ON TRUE -- We only want the latest stats, but those stats might be -- spread across multiple rows. - WHERE usage = true AND created_at > now() - '1 minute'::interval - GROUP BY user_id, agent_id, workspace_id + WHERE was.usage = true AND was.created_at > now() - '1 minute'::interval + GROUP BY was.user_id, was.agent_id, was.workspace_id ) SELECT users.username, workspace_agents.name AS agent_name, workspaces.name AS workspace_name, rx_bytes, tx_bytes, diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 4b1a4376f2db4..d7be38f17312d 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -122,6 +122,7 @@ const ( UniqueWorkspaceAgentPortSharePkey UniqueConstraint = "workspace_agent_port_share_pkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_pkey PRIMARY KEY (workspace_id, agent_name, port); UniqueWorkspaceAgentScriptTimingsScriptIDStartedAtKey UniqueConstraint = "workspace_agent_script_timings_script_id_started_at_key" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_started_at_key UNIQUE (script_id, started_at); UniqueWorkspaceAgentScriptsIDKey UniqueConstraint = "workspace_agent_scripts_id_key" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_id_key UNIQUE (id); + UniqueWorkspaceAgentSessionCountsPkey UniqueConstraint = "workspace_agent_session_counts_pkey" // ALTER TABLE ONLY workspace_agent_session_counts ADD CONSTRAINT workspace_agent_session_counts_pkey PRIMARY KEY (workspace_agent_stats_id, app_name); UniqueWorkspaceAgentStartupLogsPkey UniqueConstraint = "workspace_agent_startup_logs_pkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id); UniqueWorkspaceAgentVolumeResourceMonitorsPkey UniqueConstraint = "workspace_agent_volume_resource_monitors_pkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_pkey PRIMARY KEY (agent_id, path); UniqueWorkspaceAgentsPkey UniqueConstraint = "workspace_agents_pkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id); diff --git a/coderd/idemetadata/idemetadata.go b/coderd/idemetadata/idemetadata.go new file mode 100644 index 0000000000000..610dc0e2a15c2 --- /dev/null +++ b/coderd/idemetadata/idemetadata.go @@ -0,0 +1,113 @@ +// Package idemetadata is the single source of truth for metadata about IDEs +// and other session types that report workspace usage. App names are stored +// raw throughout the stats pipeline; this package canonicalizes well-known +// legacy spellings at ingestion and supplies the family and display-name +// grouping applied at API and metrics boundaries. It is a leaf package so +// both agent and coderd code can import it. +package idemetadata + +import "strings" + +// MaxAppNameLength is the maximum length of an app name in runes. Longer +// names are truncated before storage. +const MaxAppNameLength = 64 + +// Canonical app names for Coder's built-in session types. These are the +// keys used by the agent and by the workspace usage API, and the values +// the template usage rollup aggregates into dedicated columns. +const ( + AppNameVSCode = "vscode" + AppNameJetBrains = "jetbrains" + AppNameSSH = "ssh" + AppNameReconnectingPTY = "reconnecting_pty" + AppNameUnknown = "unknown" +) + +// Families group related session types for display and for metric labels, +// keeping label cardinality bounded while arbitrary app names flow through +// the pipeline. +const ( + FamilyVSCode = "vscode" + FamilyJetBrains = "jetbrains" + FamilySSH = "ssh" + FamilyReconnectingPTY = "reconnecting_pty" + FamilyUnknown = "unknown" +) + +// IDEInfo describes how a session type is grouped and displayed. +type IDEInfo struct { + Family string + DisplayName string +} + +// known maps canonical (lowercase) app names to their metadata. +var known = map[string]IDEInfo{ + AppNameVSCode: {Family: FamilyVSCode, DisplayName: "VS Code"}, + "vscode_insiders": {Family: FamilyVSCode, DisplayName: "VS Code Insiders"}, + "cursor": {Family: FamilyVSCode, DisplayName: "Cursor"}, + "windsurf": {Family: FamilyVSCode, DisplayName: "Windsurf"}, + "positron": {Family: FamilyVSCode, DisplayName: "Positron"}, + "vscodium": {Family: FamilyVSCode, DisplayName: "VSCodium"}, + AppNameJetBrains: {Family: FamilyJetBrains, DisplayName: "JetBrains"}, + AppNameSSH: {Family: FamilySSH, DisplayName: "SSH"}, + AppNameReconnectingPTY: {Family: FamilyReconnectingPTY, DisplayName: "Web Terminal"}, + AppNameUnknown: {Family: FamilyUnknown, DisplayName: "Unknown"}, +} + +// aliases maps well-known legacy spellings (lowercase) to canonical app +// names so historical client values keep aggregating with their canonical +// counterparts. +var aliases = map[string]string{ + // codersdk.UsageAppNameReconnectingPty uses a hyphen. + "reconnecting-pty": AppNameReconnectingPTY, +} + +// Lookup returns metadata for the given app name. Matching is +// case-insensitive and alias-aware; unknown names fall back to family +// "unknown" with the raw name as the display name. +func Lookup(appName string) IDEInfo { + if info, ok := known[canonicalKey(appName)]; ok { + return info + } + return IDEInfo{Family: FamilyUnknown, DisplayName: appName} +} + +// Normalize prepares a client-supplied app name for storage: it strips null +// bytes (which Postgres TEXT rejects), truncates to MaxAppNameLength runes, +// and canonicalizes well-known names and legacy spellings +// case-insensitively. Unrecognized names are preserved as-is, including +// casing; they are grouped at display time via Lookup. An empty result +// becomes "unknown" so a bad name never invalidates the surrounding report. +func Normalize(appName string) string { + appName = strings.ReplaceAll(appName, "\x00", "") + if runes := []rune(appName); len(runes) > MaxAppNameLength { + appName = string(runes[:MaxAppNameLength]) + } + if appName == "" { + return AppNameUnknown + } + if canonical, ok := canonicalName(appName); ok { + return canonical + } + return appName +} + +// canonicalName resolves well-known names and aliases to their canonical +// form, reporting whether the name was recognized. +func canonicalName(appName string) (string, bool) { + key := canonicalKey(appName) + if _, ok := known[key]; ok { + return key, true + } + return appName, false +} + +// canonicalKey lowercases and resolves aliases to produce the map key for +// a raw app name. +func canonicalKey(appName string) string { + key := strings.ToLower(appName) + if alias, ok := aliases[key]; ok { + return alias + } + return key +} diff --git a/coderd/idemetadata/idemetadata_test.go b/coderd/idemetadata/idemetadata_test.go new file mode 100644 index 0000000000000..af509ed9bb4fe --- /dev/null +++ b/coderd/idemetadata/idemetadata_test.go @@ -0,0 +1,70 @@ +package idemetadata_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/idemetadata" +) + +func TestNormalize(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + input string + want string + }{ + {name: "CanonicalPassthrough", input: "vscode", want: "vscode"}, + {name: "KnownNameCaseInsensitive", input: "JetBrains", want: "jetbrains"}, + {name: "LegacyAlias", input: "reconnecting-pty", want: "reconnecting_pty"}, + {name: "LegacyAliasCaseInsensitive", input: "Reconnecting-PTY", want: "reconnecting_pty"}, + {name: "UnknownPreservesCasing", input: "Cursor Nightly", want: "Cursor Nightly"}, + {name: "UnknownPreservesUnicode", input: "エディタ", want: "エディタ"}, + {name: "StripsNullBytes", input: "cur\x00sor", want: "cursor"}, + {name: "Empty", input: "", want: "unknown"}, + {name: "OnlyNullBytes", input: "\x00\x00", want: "unknown"}, + { + name: "TruncatesToMaxRunes", + input: strings.Repeat("a", idemetadata.MaxAppNameLength+10), + want: strings.Repeat("a", idemetadata.MaxAppNameLength), + }, + { + // Rune-safe truncation must not split multibyte characters. + name: "TruncatesMultibyteSafely", + input: strings.Repeat("あ", idemetadata.MaxAppNameLength+1), + want: strings.Repeat("あ", idemetadata.MaxAppNameLength), + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.want, idemetadata.Normalize(tc.input)) + }) + } +} + +func TestLookup(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + input string + wantFamily string + wantDisplayName string + }{ + {name: "VSCode", input: "vscode", wantFamily: idemetadata.FamilyVSCode, wantDisplayName: "VS Code"}, + {name: "VSCodeFork", input: "cursor", wantFamily: idemetadata.FamilyVSCode, wantDisplayName: "Cursor"}, + {name: "CaseInsensitive", input: "JetBrains", wantFamily: idemetadata.FamilyJetBrains, wantDisplayName: "JetBrains"}, + {name: "Alias", input: "reconnecting-pty", wantFamily: idemetadata.FamilyReconnectingPTY, wantDisplayName: "Web Terminal"}, + {name: "UnknownFallsBackToRawName", input: "SomeFutureIDE", wantFamily: idemetadata.FamilyUnknown, wantDisplayName: "SomeFutureIDE"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + info := idemetadata.Lookup(tc.input) + require.Equal(t, tc.wantFamily, info.Family) + require.Equal(t, tc.wantDisplayName, info.DisplayName) + }) + } +} diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index f730dcc240058..0f9d32a5b85e1 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -308,7 +308,10 @@ func TestCache_DeploymentStats(t *testing.T) { DeploymentStats: time.Minute, }, false) - err := db.InsertWorkspaceAgentStats(context.Background(), database.InsertWorkspaceAgentStatsParams{ + sessionCounts, err := json.Marshal([]map[string]int64{{"vscode": 1}}) + require.NoError(t, err) + + err = db.InsertWorkspaceAgentStats(context.Background(), database.InsertWorkspaceAgentStatsParams{ ID: []uuid.UUID{uuid.New()}, CreatedAt: []time.Time{clock.Now()}, WorkspaceID: []uuid.UUID{uuid.New()}, @@ -317,17 +320,14 @@ func TestCache_DeploymentStats(t *testing.T) { AgentID: []uuid.UUID{uuid.New()}, ConnectionsByProto: json.RawMessage(`[{}]`), - RxPackets: []int64{0}, - RxBytes: []int64{1}, - TxPackets: []int64{0}, - TxBytes: []int64{1}, - ConnectionCount: []int64{1}, - SessionCountVSCode: []int64{1}, - SessionCountJetBrains: []int64{0}, - SessionCountReconnectingPTY: []int64{0}, - SessionCountSSH: []int64{0}, - ConnectionMedianLatencyMS: []float64{10}, - Usage: []bool{false}, + RxPackets: []int64{0}, + RxBytes: []int64{1}, + TxPackets: []int64{0}, + TxBytes: []int64{1}, + ConnectionCount: []int64{1}, + SessionCounts: sessionCounts, + ConnectionMedianLatencyMS: []float64{10}, + Usage: []bool{false}, }) require.NoError(t, err) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 3e68cdcc43d33..bdc956bb4cd7a 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -29,6 +29,7 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpapi/httperror" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/idemetadata" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" @@ -1786,34 +1787,15 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { }) return } - if !slices.Contains(codersdk.AllowedAppNames, req.AppName) { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid request", - Validations: []codersdk.ValidationError{{ - Field: "app_name", - Detail: fmt.Sprintf("must be one of %v", codersdk.AllowedAppNames), - }}, - }) - return - } + + // Any app name is accepted and stored raw; well-known legacy spellings + // (e.g. "reconnecting-pty") are canonicalized so they aggregate with + // agent-reported session types. + appName := idemetadata.Normalize(string(req.AppName)) stat := &proto.Stats{ ConnectionCount: 1, - } - switch req.AppName { - case codersdk.UsageAppNameVscode: - stat.SessionCountVscode = 1 - case codersdk.UsageAppNameJetbrains: - stat.SessionCountJetbrains = 1 - case codersdk.UsageAppNameReconnectingPty: - stat.SessionCountReconnectingPty = 1 - case codersdk.UsageAppNameSSH: - stat.SessionCountSsh = 1 - default: - // This means the app_name is in the codersdk.AllowedAppNames but not being - // handled by this switch statement. - httpapi.InternalServerError(rw, xerrors.Errorf("unknown app_name %q", req.AppName)) - return + SessionCounts: map[string]int64{appName: 1}, } agent, err := api.Database.GetWorkspaceAgentByID(ctx, req.AgentID) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 2c4627d3662ff..8da748ab8f1f9 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -5084,12 +5084,12 @@ func TestWorkspaceUsageTracking(t *testing.T) { AppName: "ssh", }) require.ErrorContains(t, err, "app_name") - // unknown app name fails + // unknown app names are accepted and stored raw err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, - AppName: "unknown", + AppName: "SomeFutureIDE", }) - require.ErrorContains(t, err, "app_name") + require.NoError(t, err) // vscode works err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ diff --git a/coderd/workspacestats/batcher.go b/coderd/workspacestats/batcher.go index 847ef562fbb1c..56a37232ceb3f 100644 --- a/coderd/workspacestats/batcher.go +++ b/coderd/workspacestats/batcher.go @@ -37,9 +37,10 @@ type DBBatcher struct { mu sync.Mutex // TODO: make this a buffered chan instead? buf *database.InsertWorkspaceAgentStatsParams - // NOTE: we batch this separately as it's a jsonb field and + // NOTE: we batch these separately as they're jsonb fields and // pq.Array + unnest doesn't play nicely with this. connectionsByProto []map[string]int64 + sessionCounts []map[string]int64 batchSize int // tickCh is used to periodically flush the buffer. @@ -152,19 +153,16 @@ func (b *DBBatcher) Add( b.buf.TemplateID = append(b.buf.TemplateID, templateID) b.buf.WorkspaceID = append(b.buf.WorkspaceID, workspaceID) - // Store the connections by proto separately as it's a jsonb field. We marshal on flush. - // b.buf.ConnectionsByProto = append(b.buf.ConnectionsByProto, st.ConnectionsByProto) + // Store the connections by proto and session counts separately as + // they're jsonb fields. We marshal on flush. b.connectionsByProto = append(b.connectionsByProto, st.ConnectionsByProto) + b.sessionCounts = append(b.sessionCounts, SessionCountsFromProto(st)) b.buf.ConnectionCount = append(b.buf.ConnectionCount, st.ConnectionCount) b.buf.RxPackets = append(b.buf.RxPackets, st.RxPackets) b.buf.RxBytes = append(b.buf.RxBytes, st.RxBytes) b.buf.TxPackets = append(b.buf.TxPackets, st.TxPackets) b.buf.TxBytes = append(b.buf.TxBytes, st.TxBytes) - b.buf.SessionCountVSCode = append(b.buf.SessionCountVSCode, st.SessionCountVscode) - b.buf.SessionCountJetBrains = append(b.buf.SessionCountJetBrains, st.SessionCountJetbrains) - b.buf.SessionCountReconnectingPTY = append(b.buf.SessionCountReconnectingPTY, st.SessionCountReconnectingPty) - b.buf.SessionCountSSH = append(b.buf.SessionCountSSH, st.SessionCountSsh) b.buf.ConnectionMedianLatencyMS = append(b.buf.ConnectionMedianLatencyMS, st.ConnectionMedianLatencyMs) b.buf.Usage = append(b.buf.Usage, usage) @@ -245,6 +243,16 @@ func (b *DBBatcher) flush(ctx context.Context, forced bool, reason string) { b.buf.ConnectionsByProto = payload } + // marshal session counts. The JSONB array is zipped with the ID array + // by position, so it must have one element per stats row. + sessionCountsPayload, err := json.Marshal(b.sessionCounts) + if err != nil { + b.log.Error(ctx, "unable to marshal agent session counts, dropping data", slog.Error(err)) + b.buf.SessionCounts = json.RawMessage(`[]`) + } else { + b.buf.SessionCounts = sessionCountsPayload + } + // nolint:gocritic // (#13146) Will be moved soon as part of refactor. err = b.store.InsertWorkspaceAgentStats(ctx, *b.buf) elapsed := time.Since(start) @@ -263,27 +271,25 @@ func (b *DBBatcher) flush(ctx context.Context, forced bool, reason string) { // initBuf resets the buffer. b MUST be locked. func (b *DBBatcher) initBuf(size int) { b.buf = &database.InsertWorkspaceAgentStatsParams{ - ID: make([]uuid.UUID, 0, b.batchSize), - CreatedAt: make([]time.Time, 0, b.batchSize), - UserID: make([]uuid.UUID, 0, b.batchSize), - WorkspaceID: make([]uuid.UUID, 0, b.batchSize), - TemplateID: make([]uuid.UUID, 0, b.batchSize), - AgentID: make([]uuid.UUID, 0, b.batchSize), - ConnectionsByProto: json.RawMessage("[]"), - ConnectionCount: make([]int64, 0, b.batchSize), - RxPackets: make([]int64, 0, b.batchSize), - RxBytes: make([]int64, 0, b.batchSize), - TxPackets: make([]int64, 0, b.batchSize), - TxBytes: make([]int64, 0, b.batchSize), - SessionCountVSCode: make([]int64, 0, b.batchSize), - SessionCountJetBrains: make([]int64, 0, b.batchSize), - SessionCountReconnectingPTY: make([]int64, 0, b.batchSize), - SessionCountSSH: make([]int64, 0, b.batchSize), - ConnectionMedianLatencyMS: make([]float64, 0, b.batchSize), - Usage: make([]bool, 0, b.batchSize), + ID: make([]uuid.UUID, 0, b.batchSize), + CreatedAt: make([]time.Time, 0, b.batchSize), + UserID: make([]uuid.UUID, 0, b.batchSize), + WorkspaceID: make([]uuid.UUID, 0, b.batchSize), + TemplateID: make([]uuid.UUID, 0, b.batchSize), + AgentID: make([]uuid.UUID, 0, b.batchSize), + ConnectionsByProto: json.RawMessage("[]"), + ConnectionCount: make([]int64, 0, b.batchSize), + RxPackets: make([]int64, 0, b.batchSize), + RxBytes: make([]int64, 0, b.batchSize), + TxPackets: make([]int64, 0, b.batchSize), + TxBytes: make([]int64, 0, b.batchSize), + SessionCounts: json.RawMessage("[]"), + ConnectionMedianLatencyMS: make([]float64, 0, b.batchSize), + Usage: make([]bool, 0, b.batchSize), } b.connectionsByProto = make([]map[string]int64, 0, size) + b.sessionCounts = make([]map[string]int64, 0, size) } func (b *DBBatcher) resetBuf() { @@ -299,11 +305,9 @@ func (b *DBBatcher) resetBuf() { b.buf.RxBytes = b.buf.RxBytes[:0] b.buf.TxPackets = b.buf.TxPackets[:0] b.buf.TxBytes = b.buf.TxBytes[:0] - b.buf.SessionCountVSCode = b.buf.SessionCountVSCode[:0] - b.buf.SessionCountJetBrains = b.buf.SessionCountJetBrains[:0] - b.buf.SessionCountReconnectingPTY = b.buf.SessionCountReconnectingPTY[:0] - b.buf.SessionCountSSH = b.buf.SessionCountSSH[:0] + b.buf.SessionCounts = json.RawMessage(`[]`) b.buf.ConnectionMedianLatencyMS = b.buf.ConnectionMedianLatencyMS[:0] b.buf.Usage = b.buf.Usage[:0] b.connectionsByProto = b.connectionsByProto[:0] + b.sessionCounts = b.sessionCounts[:0] } diff --git a/coderd/workspacestats/reporter.go b/coderd/workspacestats/reporter.go index c5b8f9f70adf6..4a35e978b04f4 100644 --- a/coderd/workspacestats/reporter.go +++ b/coderd/workspacestats/reporter.go @@ -154,10 +154,7 @@ func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspac } // workspace activity: if no sessions we do not bump activity - if usage && stats.SessionCountVscode == 0 && - stats.SessionCountJetbrains == 0 && - stats.SessionCountReconnectingPty == 0 && - stats.SessionCountSsh == 0 { + if usage && len(SessionCountsFromProto(stats)) == 0 { return nil } diff --git a/coderd/workspacestats/sessioncounts.go b/coderd/workspacestats/sessioncounts.go new file mode 100644 index 0000000000000..b26d35e2b3140 --- /dev/null +++ b/coderd/workspacestats/sessioncounts.go @@ -0,0 +1,45 @@ +package workspacestats + +import ( + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/idemetadata" +) + +// SessionCountsFromProto returns the per-app session counts reported by an +// agent. The deprecated fixed fields are only populated by agents from +// before the session_counts map was introduced and are converted here. +// Entries with non-positive counts are dropped. +func SessionCountsFromProto(st *agentproto.Stats) map[string]int64 { + counts := make(map[string]int64, len(st.GetSessionCounts())) + for app, count := range st.GetSessionCounts() { + if count <= 0 { + continue + } + counts[app] = count + } + if len(counts) > 0 { + return counts + } + // Old-agent fallback: convert the deprecated fixed fields. + //nolint:staticcheck // Deprecated fields are read for old-agent compatibility. + for app, count := range map[string]int64{ + idemetadata.AppNameVSCode: st.SessionCountVscode, + idemetadata.AppNameJetBrains: st.SessionCountJetbrains, + idemetadata.AppNameReconnectingPTY: st.SessionCountReconnectingPty, + idemetadata.AppNameSSH: st.SessionCountSsh, + } { + if count <= 0 { + continue + } + counts[app] = count + } + return counts +} + +// ClearSessionCounts zeroes all session counts on the given stats, including +// the deprecated fixed fields sent by older agents. +func ClearSessionCounts(st *agentproto.Stats) { + st.SessionCounts = nil + //nolint:staticcheck // Deprecated fields are cleared for old-agent compatibility. + st.SessionCountSsh, st.SessionCountJetbrains, st.SessionCountVscode, st.SessionCountReconnectingPty = 0, 0, 0, 0 +} diff --git a/coderd/workspacestats/sessioncounts_test.go b/coderd/workspacestats/sessioncounts_test.go new file mode 100644 index 0000000000000..8fac20d98cf19 --- /dev/null +++ b/coderd/workspacestats/sessioncounts_test.go @@ -0,0 +1,75 @@ +package workspacestats_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/workspacestats" +) + +func TestSessionCountsFromProto(t *testing.T) { + t.Parallel() + + t.Run("MapTakesPrecedence", func(t *testing.T) { + t.Parallel() + //nolint:staticcheck // Deprecated fields are set to verify precedence. + st := &agentproto.Stats{ + SessionCounts: map[string]int64{"Cursor": 2, "ssh": 1}, + SessionCountVscode: 9, + } + require.Equal(t, map[string]int64{"Cursor": 2, "ssh": 1}, workspacestats.SessionCountsFromProto(st)) + }) + + t.Run("DropsNonPositiveEntries", func(t *testing.T) { + t.Parallel() + st := &agentproto.Stats{ + SessionCounts: map[string]int64{"vscode": 1, "reconnecting_pty": 0, "bogus": -1}, + } + require.Equal(t, map[string]int64{"vscode": 1}, workspacestats.SessionCountsFromProto(st)) + }) + + t.Run("OldAgentFallback", func(t *testing.T) { + t.Parallel() + //nolint:staticcheck // Deprecated fields simulate an old agent. + st := &agentproto.Stats{ + SessionCountVscode: 3, + SessionCountJetbrains: 1, + SessionCountSsh: 2, + } + require.Equal(t, map[string]int64{ + "vscode": 3, + "jetbrains": 1, + "ssh": 2, + }, workspacestats.SessionCountsFromProto(st)) + }) + + t.Run("AllZeroMapFallsBackToLegacyFields", func(t *testing.T) { + t.Parallel() + // A new agent with no active sessions sends zero-valued map entries. + // The legacy fields are zero too, so the result is empty either way. + st := &agentproto.Stats{ + SessionCounts: map[string]int64{"ssh": 0, "reconnecting_pty": 0}, + } + require.Empty(t, workspacestats.SessionCountsFromProto(st)) + }) + + t.Run("Empty", func(t *testing.T) { + t.Parallel() + require.Empty(t, workspacestats.SessionCountsFromProto(&agentproto.Stats{})) + }) +} + +func TestClearSessionCounts(t *testing.T) { + t.Parallel() + + //nolint:staticcheck // Deprecated fields simulate an old agent. + st := &agentproto.Stats{ + SessionCounts: map[string]int64{"vscode": 1}, + SessionCountVscode: 1, + SessionCountSsh: 2, + } + workspacestats.ClearSessionCounts(st) + require.Empty(t, workspacestats.SessionCountsFromProto(st)) +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 617dc2d15bf1b..0a13a80820d88 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -383,6 +383,8 @@ type PostWorkspaceUsageRequest struct { type UsageAppName string +// Well-known usage app names. The workspace usage API accepts any app +// name; unrecognized names are stored raw. const ( UsageAppNameVscode UsageAppName = "vscode" UsageAppNameJetbrains UsageAppName = "jetbrains" @@ -390,13 +392,6 @@ const ( UsageAppNameSSH UsageAppName = "ssh" ) -var AllowedAppNames = []UsageAppName{ - UsageAppNameVscode, - UsageAppNameJetbrains, - UsageAppNameReconnectingPty, - UsageAppNameSSH, -} - // PostWorkspaceUsage marks the workspace as having been used recently and records an app stat. func (c *Client) PostWorkspaceUsageWithBody(ctx context.Context, id uuid.UUID, req PostWorkspaceUsageRequest) error { path := fmt.Sprintf("/api/v2/workspaces/%s/usage", id.String()) diff --git a/tailnet/proto/version.go b/tailnet/proto/version.go index 2cd8c987f195b..ec6730b00f1a3 100644 --- a/tailnet/proto/version.go +++ b/tailnet/proto/version.go @@ -81,9 +81,15 @@ import ( // deployments remain interoperable. Real persistence, // KindMCPServer provider, and chatd hydration land in // CODAGT-569. +// +// API v2.11: +// - Added the session_counts map field to Stats on the Agent API, +// deprecating the fixed session_count_* fields. New agents populate the +// map exclusively; the server converts the deprecated fields sent by +// older agents. const ( CurrentMajor = 2 - CurrentMinor = 10 + CurrentMinor = 11 ) var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor) From be57bffea383f1b7e2a1c72185c957bf705eafa0 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Sun, 12 Jul 2026 11:12:19 +0000 Subject: [PATCH 2/4] fix(coderd): harden session count ingestion and insights query plans - Cap session count reports at 64 distinct app names, aggregating the overflow under "other", so a malicious or buggy agent cannot fan out child-table rows. - Guard the batcher's positional JSONB contract at flush: on a length mismatch, drop the session counts for the batch (parent stats still insert) instead of misattributing them. - Replace the per-row lateral probes in UpsertTemplateUsageStats and GetTemplateInsightsByTemplate with a direct hash-joinable join; the inclusion filter becomes the join itself and the DISTINCT/MAX aggregates tolerate the fan-out. At 1.7M stats rows this takes the rollup from 292ms to 99ms and the insights query from 3.0s to 0.66s, with byte-identical results. --- coderd/database/queries.sql.go | 84 +++++++-------------- coderd/database/queries/insights.sql | 84 +++++++-------------- coderd/idemetadata/idemetadata.go | 3 + coderd/workspacestats/batcher.go | 11 ++- coderd/workspacestats/sessioncounts.go | 48 +++++++++++- coderd/workspacestats/sessioncounts_test.go | 17 +++++ 6 files changed, 134 insertions(+), 113 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b5953487bdc02..a92b14a7d01e2 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15618,10 +15618,10 @@ WITH SELECT was.template_id, was.user_id, - COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, - COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'ssh' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'reconnecting_pty' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'vscode' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'jetbrains' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection @@ -15629,26 +15629,18 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- One lateral row per stats row, so the aggregates above don't fan out. - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + -- The join supplies the session counts and filters out rows without + -- session activity in one hash-joinable pass; the DISTINCT/MAX + -- aggregates above tolerate the row fan-out. + JOIN + workspace_agent_session_counts sc + ON + sc.workspace_agent_stats_id = was.id + AND sc.count > 0 + AND sc.app_name IN ('ssh', 'reconnecting_pty', 'vscode', 'jetbrains') WHERE was.created_at >= $1::timestamptz AND was.created_at < $2::timestamptz - -- Inclusion criteria to filter out empty results. - AND ( - sc.ssh > 0 - OR sc.reconnecting_pty > 0 - OR sc.vscode > 0 - OR sc.jetbrains > 0 - ) GROUP BY was.template_id, was.user_id ) @@ -16258,23 +16250,13 @@ WITH was.template_id, was.user_id, -- Store each unique minute bucket for later merge between datasets. - array_agg( - DISTINCT CASE - WHEN - sc.ssh > 0 - OR sc.reconnecting_pty > 0 - OR sc.vscode > 0 - OR sc.jetbrains > 0 - THEN - date_trunc('minute', was.created_at) - ELSE - NULL - END - ) AS minute_buckets, - COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, - COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, + -- The join filters out rows without session activity, so every + -- remaining row contributes its minute. + array_agg(DISTINCT date_trunc('minute', was.created_at)) AS minute_buckets, + COUNT(DISTINCT CASE WHEN sc.app_name = 'ssh' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'reconnecting_pty' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'vscode' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'jetbrains' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection @@ -16282,28 +16264,20 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- One lateral row per stats row, so the aggregates above don't fan out. - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + -- The join supplies the session counts and filters out rows without + -- session activity in one hash-joinable pass; the DISTINCT/MAX + -- aggregates above tolerate the row fan-out. + JOIN + workspace_agent_session_counts sc + ON + sc.workspace_agent_stats_id = was.id + AND sc.count > 0 + AND sc.app_name IN ('ssh', 'reconnecting_pty', 'vscode', 'jetbrains') WHERE -- created_at >= @start_time::timestamptz -- AND created_at < @end_time::timestamptz was.created_at >= (SELECT t FROM latest_start) AND was.created_at < NOW() - -- Inclusion criteria to filter out empty results. - AND ( - sc.ssh > 0 - OR sc.reconnecting_pty > 0 - OR sc.vscode > 0 - OR sc.jetbrains > 0 - ) GROUP BY time_bucket, was.template_id, was.user_id ), diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index 43bf30f7f05f8..78b5d6ce920be 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -156,10 +156,10 @@ WITH SELECT was.template_id, was.user_id, - COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, - COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'ssh' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'reconnecting_pty' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'vscode' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'jetbrains' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection @@ -167,26 +167,18 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- One lateral row per stats row, so the aggregates above don't fan out. - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + -- The join supplies the session counts and filters out rows without + -- session activity in one hash-joinable pass; the DISTINCT/MAX + -- aggregates above tolerate the row fan-out. + JOIN + workspace_agent_session_counts sc + ON + sc.workspace_agent_stats_id = was.id + AND sc.count > 0 + AND sc.app_name IN ('ssh', 'reconnecting_pty', 'vscode', 'jetbrains') WHERE was.created_at >= @start_time::timestamptz AND was.created_at < @end_time::timestamptz - -- Inclusion criteria to filter out empty results. - AND ( - sc.ssh > 0 - OR sc.reconnecting_pty > 0 - OR sc.vscode > 0 - OR sc.jetbrains > 0 - ) GROUP BY was.template_id, was.user_id ) @@ -565,23 +557,13 @@ WITH was.template_id, was.user_id, -- Store each unique minute bucket for later merge between datasets. - array_agg( - DISTINCT CASE - WHEN - sc.ssh > 0 - OR sc.reconnecting_pty > 0 - OR sc.vscode > 0 - OR sc.jetbrains > 0 - THEN - date_trunc('minute', was.created_at) - ELSE - NULL - END - ) AS minute_buckets, - COUNT(DISTINCT CASE WHEN sc.ssh > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, - COUNT(DISTINCT CASE WHEN sc.reconnecting_pty > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, - COUNT(DISTINCT CASE WHEN sc.vscode > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, - COUNT(DISTINCT CASE WHEN sc.jetbrains > 0 THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, + -- The join filters out rows without session activity, so every + -- remaining row contributes its minute. + array_agg(DISTINCT date_trunc('minute', was.created_at)) AS minute_buckets, + COUNT(DISTINCT CASE WHEN sc.app_name = 'ssh' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS ssh_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'reconnecting_pty' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS reconnecting_pty_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'vscode' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS vscode_mins, + COUNT(DISTINCT CASE WHEN sc.app_name = 'jetbrains' THEN date_trunc('minute', was.created_at) ELSE NULL END) AS jetbrains_mins, -- NOTE(mafredri): The agent stats are currently very unreliable, and -- sometimes the connections are missing, even during active sessions. -- Since we can't fully rely on this, we check for "any connection @@ -589,28 +571,20 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- One lateral row per stats row, so the aggregates above don't fan out. - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty, - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + -- The join supplies the session counts and filters out rows without + -- session activity in one hash-joinable pass; the DISTINCT/MAX + -- aggregates above tolerate the row fan-out. + JOIN + workspace_agent_session_counts sc + ON + sc.workspace_agent_stats_id = was.id + AND sc.count > 0 + AND sc.app_name IN ('ssh', 'reconnecting_pty', 'vscode', 'jetbrains') WHERE -- created_at >= @start_time::timestamptz -- AND created_at < @end_time::timestamptz was.created_at >= (SELECT t FROM latest_start) AND was.created_at < NOW() - -- Inclusion criteria to filter out empty results. - AND ( - sc.ssh > 0 - OR sc.reconnecting_pty > 0 - OR sc.vscode > 0 - OR sc.jetbrains > 0 - ) GROUP BY time_bucket, was.template_id, was.user_id ), diff --git a/coderd/idemetadata/idemetadata.go b/coderd/idemetadata/idemetadata.go index 610dc0e2a15c2..af4ae07383540 100644 --- a/coderd/idemetadata/idemetadata.go +++ b/coderd/idemetadata/idemetadata.go @@ -21,6 +21,8 @@ const ( AppNameSSH = "ssh" AppNameReconnectingPTY = "reconnecting_pty" AppNameUnknown = "unknown" + // AppNameOther aggregates session counts beyond the per-report entry cap. + AppNameOther = "other" ) // Families group related session types for display and for metric labels, @@ -52,6 +54,7 @@ var known = map[string]IDEInfo{ AppNameSSH: {Family: FamilySSH, DisplayName: "SSH"}, AppNameReconnectingPTY: {Family: FamilyReconnectingPTY, DisplayName: "Web Terminal"}, AppNameUnknown: {Family: FamilyUnknown, DisplayName: "Unknown"}, + AppNameOther: {Family: FamilyUnknown, DisplayName: "Other"}, } // aliases maps well-known legacy spellings (lowercase) to canonical app diff --git a/coderd/workspacestats/batcher.go b/coderd/workspacestats/batcher.go index 56a37232ceb3f..23ebae6980015 100644 --- a/coderd/workspacestats/batcher.go +++ b/coderd/workspacestats/batcher.go @@ -244,7 +244,16 @@ func (b *DBBatcher) flush(ctx context.Context, forced bool, reason string) { } // marshal session counts. The JSONB array is zipped with the ID array - // by position, so it must have one element per stats row. + // by position, so it must have one element per stats row. On mismatch, + // drop the session counts (parent stats still insert) rather than + // misattribute them. + if len(b.sessionCounts) != len(b.buf.ID) { + b.log.Error(ctx, "session counts buffer out of sync with stats buffer, dropping session counts", + slog.F("stats", len(b.buf.ID)), + slog.F("session_counts", len(b.sessionCounts)), + ) + b.sessionCounts = b.sessionCounts[:0] + } sessionCountsPayload, err := json.Marshal(b.sessionCounts) if err != nil { b.log.Error(ctx, "unable to marshal agent session counts, dropping data", slog.Error(err)) diff --git a/coderd/workspacestats/sessioncounts.go b/coderd/workspacestats/sessioncounts.go index b26d35e2b3140..86d1ce79b8d7e 100644 --- a/coderd/workspacestats/sessioncounts.go +++ b/coderd/workspacestats/sessioncounts.go @@ -1,14 +1,24 @@ package workspacestats import ( + "sort" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/idemetadata" ) +// maxSessionCountEntries bounds the number of distinct app names accepted +// per stats report. Counts beyond the cap are aggregated under +// idemetadata.AppNameOther so a malicious or buggy agent cannot fan out +// child-table rows. Legitimate agents report a handful of entries. +const maxSessionCountEntries = 64 + // SessionCountsFromProto returns the per-app session counts reported by an // agent. The deprecated fixed fields are only populated by agents from // before the session_counts map was introduced and are converted here. -// Entries with non-positive counts are dropped. +// Entries with non-positive counts are dropped, and reports with more than +// maxSessionCountEntries distinct names have the overflow aggregated under +// idemetadata.AppNameOther. func SessionCountsFromProto(st *agentproto.Stats) map[string]int64 { counts := make(map[string]int64, len(st.GetSessionCounts())) for app, count := range st.GetSessionCounts() { @@ -18,7 +28,7 @@ func SessionCountsFromProto(st *agentproto.Stats) map[string]int64 { counts[app] = count } if len(counts) > 0 { - return counts + return capSessionCounts(counts) } // Old-agent fallback: convert the deprecated fixed fields. //nolint:staticcheck // Deprecated fields are read for old-agent compatibility. @@ -36,6 +46,40 @@ func SessionCountsFromProto(st *agentproto.Stats) map[string]int64 { return counts } +// capSessionCounts keeps at most maxSessionCountEntries named entries, +// preferring well-known names and then lexicographic order for determinism, +// and sums the remainder into idemetadata.AppNameOther. +func capSessionCounts(counts map[string]int64) map[string]int64 { + if len(counts) <= maxSessionCountEntries { + return counts + } + names := make([]string, 0, len(counts)) + for name := range counts { + names = append(names, name) + } + sort.Slice(names, func(i, j int) bool { + iKnown := idemetadata.Lookup(names[i]).Family != idemetadata.FamilyUnknown + jKnown := idemetadata.Lookup(names[j]).Family != idemetadata.FamilyUnknown + if iKnown != jKnown { + return iKnown + } + return names[i] < names[j] + }) + capped := make(map[string]int64, maxSessionCountEntries+1) + var other int64 + for i, name := range names { + if i < maxSessionCountEntries { + capped[name] = counts[name] + continue + } + other += counts[name] + } + if other > 0 { + capped[idemetadata.AppNameOther] += other + } + return capped +} + // ClearSessionCounts zeroes all session counts on the given stats, including // the deprecated fixed fields sent by older agents. func ClearSessionCounts(st *agentproto.Stats) { diff --git a/coderd/workspacestats/sessioncounts_test.go b/coderd/workspacestats/sessioncounts_test.go index 8fac20d98cf19..c7a10254bd992 100644 --- a/coderd/workspacestats/sessioncounts_test.go +++ b/coderd/workspacestats/sessioncounts_test.go @@ -1,6 +1,7 @@ package workspacestats_test import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -55,6 +56,22 @@ func TestSessionCountsFromProto(t *testing.T) { require.Empty(t, workspacestats.SessionCountsFromProto(st)) }) + t.Run("CapsEntriesAggregatingIntoOther", func(t *testing.T) { + t.Parallel() + sessionCounts := map[string]int64{"vscode": 5} + for i := range 200 { + sessionCounts[fmt.Sprintf("zz-ide-%03d", i)] = 1 + } + got := workspacestats.SessionCountsFromProto(&agentproto.Stats{SessionCounts: sessionCounts}) + // 64 named entries (well-known first, then lexicographic) plus + // "other" holding the 137 overflow counts. + require.Len(t, got, 65) + require.EqualValues(t, 5, got["vscode"]) + require.EqualValues(t, 1, got["zz-ide-000"]) + require.EqualValues(t, 137, got["other"]) + require.NotContains(t, got, "zz-ide-199") + }) + t.Run("Empty", func(t *testing.T) { t.Parallel() require.Empty(t, workspacestats.SessionCountsFromProto(&agentproto.Stats{})) From 6f013bab0f89f936e64612874852309ab973f5f6 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Sun, 12 Jul 2026 15:30:39 +0300 Subject: [PATCH 3/4] fix: dial agent API v2.11 and simplify session count handling Complete the v2.11 plumbing so agents dial the version they report (fixes TestWorkspaceAgent_Startup), normalize well-known app names once at stats ingestion, and fold the idemetadata display/family layering into a single family map that groups stored names at read time, so new IDEs (Antigravity, Trae, Kiro, Devin) need no migration. Replace the per-row lateral pivots with plain joins where no fan-out is possible and backfill the child table in one pass. --- agent/agent.go | 33 ++--- agent/agentssh/agentssh.go | 37 +++--- agent/agentssh/metrics.go | 2 +- agent/agentssh/sessiontype_internal_test.go | 4 +- agent/agenttest/client.go | 14 +- agent/proto/agent_drpc_old.go | 7 + coderd/database/dbgen/dbgen.go | 10 +- coderd/database/dump.sql | 2 +- ...0543_workspace_agent_session_counts.up.sql | 19 +-- coderd/database/models.go | 2 +- coderd/database/queries.sql.go | 82 ++++-------- coderd/database/queries/insights.sql | 10 +- .../database/queries/workspaceagentstats.sql | 72 +++------- coderd/idemetadata/idemetadata.go | 124 +++++++----------- .../idemetadata/idemetadata_internal_test.go | 16 +++ coderd/idemetadata/idemetadata_test.go | 28 ++-- coderd/workspaceagents_test.go | 2 +- coderd/workspaces.go | 9 +- coderd/workspacestats/batcher.go | 13 +- coderd/workspacestats/reporter.go | 2 +- coderd/workspacestats/sessioncounts.go | 86 +++++++----- coderd/workspacestats/sessioncounts_test.go | 29 +++- codersdk/agentsdk/agentsdk.go | 21 ++- 23 files changed, 293 insertions(+), 331 deletions(-) create mode 100644 coderd/idemetadata/idemetadata_internal_test.go diff --git a/agent/agent.go b/agent/agent.go index 3c2a351097610..706e3a34a8fc8 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -128,23 +128,14 @@ type Options struct { } type Client interface { - ConnectRPC29(ctx context.Context) ( - proto.DRPCAgentClient29, tailnetproto.DRPCTailnetClient28, error, + ConnectRPC211(ctx context.Context) ( + proto.DRPCAgentClient211, tailnetproto.DRPCTailnetClient28, error, ) - // ConnectRPC29WithRole is like ConnectRPC29 but sends an explicit + // ConnectRPC211WithRole is like ConnectRPC211 but sends an explicit // role query parameter to the server. The workspace agent should // use role "agent" to enable connection monitoring. - ConnectRPC29WithRole(ctx context.Context, role string) ( - proto.DRPCAgentClient29, tailnetproto.DRPCTailnetClient28, error, - ) - ConnectRPC210(ctx context.Context) ( - proto.DRPCAgentClient210, tailnetproto.DRPCTailnetClient28, error, - ) - // ConnectRPC210WithRole is like ConnectRPC210 but sends an explicit - // role query parameter to the server. The workspace agent should - // use role "agent" to enable connection monitoring. - ConnectRPC210WithRole(ctx context.Context, role string) ( - proto.DRPCAgentClient210, tailnetproto.DRPCTailnetClient28, error, + ConnectRPC211WithRole(ctx context.Context, role string) ( + proto.DRPCAgentClient211, tailnetproto.DRPCTailnetClient28, error, ) tailnet.DERPMapRewriter agentsdk.RefreshableSessionTokenProvider @@ -429,12 +420,12 @@ func (a *agent) init() { var connectionType proto.Connection_Type // The proto enum cannot represent arbitrary session types, so // map by family. - switch idemetadata.Lookup(string(magicType)).Family { - case idemetadata.FamilySSH: + switch idemetadata.Family(string(magicType)) { + case idemetadata.AppNameSSH: connectionType = proto.Connection_SSH - case idemetadata.FamilyVSCode: + case idemetadata.AppNameVSCode: connectionType = proto.Connection_VSCODE - case idemetadata.FamilyJetBrains: + case idemetadata.AppNameJetBrains: connectionType = proto.Connection_JETBRAINS default: connectionType = proto.Connection_TYPE_UNSPECIFIED @@ -1177,7 +1168,7 @@ func (a *agent) run() (retErr error) { // ConnectRPC returns the dRPC connection we use for the Agent and Tailnet v2+ APIs. // We pass role "agent" to enable connection monitoring on the server, which tracks // the agent's connectivity state (first_connected_at, last_connected_at, disconnected_at). - aAPI, tAPI, err := a.client.ConnectRPC210WithRole(a.hardCtx, "agent") + aAPI, tAPI, err := a.client.ConnectRPC211WithRole(a.hardCtx, "agent") if err != nil { return err } @@ -2166,7 +2157,9 @@ func (a *agent) Collect(ctx context.Context, networkStats map[netlogtype.Connect // The count of active sessions. The deprecated session_count_* fields // are only populated by older agents. stats.SessionCounts = a.sshServer.ConnStats() - stats.SessionCounts[idemetadata.AppNameReconnectingPTY] = a.reconnectingPTYServer.ConnCount() + if count := a.reconnectingPTYServer.ConnCount(); count > 0 { + stats.SessionCounts[idemetadata.AppNameReconnectingPTY] = count + } // Compute the median connection latency! a.logger.Debug(ctx, "starting peer latency measurement for stats") diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index fa401a8aabce9..1cddb9d069e1a 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -32,6 +32,7 @@ import ( "github.com/coder/coder/v2/agent/agentrsa" "github.com/coder/coder/v2/agent/usershell" "github.com/coder/coder/v2/coderd/idemetadata" + "github.com/coder/coder/v2/coderd/util/syncmap" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/pty" ) @@ -72,17 +73,16 @@ const ( ContainerUserEnvironmentVariable = "CODER_CONTAINER_USER" ) -// MagicSessionType enums. +// Well-known magic session types, defined in terms of the canonical app +// names so the agent and server vocabularies cannot drift. const ( - // MagicSessionTypeUnknown means the session type could not be determined. - MagicSessionTypeUnknown MagicSessionType = "unknown" // MagicSessionTypeSSH is the default session type. - MagicSessionTypeSSH MagicSessionType = "ssh" + MagicSessionTypeSSH MagicSessionType = idemetadata.AppNameSSH // MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself. - MagicSessionTypeVSCode MagicSessionType = "vscode" + MagicSessionTypeVSCode MagicSessionType = idemetadata.AppNameVSCode // MagicSessionTypeJetBrains is set in the SSH config by the JetBrains // extension to identify itself. - MagicSessionTypeJetBrains MagicSessionType = "jetbrains" + MagicSessionTypeJetBrains MagicSessionType = idemetadata.AppNameJetBrains ) // BlockedFileTransferCommands contains a list of restricted file transfer commands. @@ -156,10 +156,9 @@ type Server struct { config *Config - // connCounts tracks active sessions per session type - // (map[string]*atomic.Int64). Counters are created on demand, so any - // session type reported by a client is counted. - connCounts sync.Map + // connCounts tracks active sessions per session type, with counters + // created on demand so any session type reported by a client is counted. + connCounts syncmap.Map[string, *atomic.Int64] metrics *sshServerMetrics } @@ -329,17 +328,21 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom // getOrCreateConnCounter returns the active session counter for the given // session type, creating it on first use. func (s *Server) getOrCreateConnCounter(sessionType MagicSessionType) *atomic.Int64 { + if counter, ok := s.connCounts.Load(string(sessionType)); ok { + return counter + } counter, _ := s.connCounts.LoadOrStore(string(sessionType), &atomic.Int64{}) - //nolint:forcetypeassert // Only *atomic.Int64 values are stored. - return counter.(*atomic.Int64) + return counter } -// ConnStats returns a snapshot of active sessions per session type. +// ConnStats returns a snapshot of active sessions per session type, +// omitting idle types so reports don't grow with every type ever seen. func (s *Server) ConnStats() map[string]int64 { stats := make(map[string]int64) - s.connCounts.Range(func(key, value any) bool { - //nolint:forcetypeassert // Only string keys and *atomic.Int64 values are stored. - stats[key.(string)] = value.(*atomic.Int64).Load() + s.connCounts.Range(func(key string, value *atomic.Int64) bool { + if count := value.Load(); count > 0 { + stats[key] = count + } return true }) return stats @@ -454,7 +457,7 @@ func (s *Server) sessionHandler(session ssh.Session) { reportSession := true - if idemetadata.Lookup(string(magicType)).Family == idemetadata.FamilyJetBrains { + if idemetadata.Family(string(magicType)) == idemetadata.AppNameJetBrains { // Do nothing here because JetBrains launches hundreds of ssh sessions. // We instead track JetBrains in the single persistent tcp forwarding // channel, matched by family to include IDE-specific identifiers. diff --git a/agent/agentssh/metrics.go b/agent/agentssh/metrics.go index 1474e1a5dee1e..b4c444392a8fb 100644 --- a/agent/agentssh/metrics.go +++ b/agent/agentssh/metrics.go @@ -74,5 +74,5 @@ func newSSHServerMetrics(registerer prometheus.Registerer) *sshServerMetrics { func magicTypeMetricLabel(magicType MagicSessionType) string { // Label by family to keep metric cardinality bounded while arbitrary // session types flow through. - return idemetadata.Lookup(string(magicType)).Family + return idemetadata.Family(string(magicType)) } diff --git a/agent/agentssh/sessiontype_internal_test.go b/agent/agentssh/sessiontype_internal_test.go index e91fa1fe2c1ae..3973be1922b96 100644 --- a/agent/agentssh/sessiontype_internal_test.go +++ b/agent/agentssh/sessiontype_internal_test.go @@ -57,11 +57,11 @@ func TestConnCounts(t *testing.T) { "Cursor": 2, }, s.ConnStats()) - // The same counter instance is reused per type. + // The same counter instance is reused per type, and idle types are + // omitted from the snapshot. s.getOrCreateConnCounter(MagicSessionType("Cursor")).Add(-2) require.Equal(t, map[string]int64{ "ssh": 1, "vscode": 1, - "Cursor": 0, }, s.ConnStats()) } diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 0f5d83a98f982..9f86b0540e4f9 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -158,8 +158,8 @@ func (c *Client) ConnectRPC29WithRole(ctx context.Context, _ string) ( return c.ConnectRPC29(ctx) } -func (c *Client) ConnectRPC210(ctx context.Context) ( - agentproto.DRPCAgentClient210, proto.DRPCTailnetClient28, error, +func (c *Client) ConnectRPC211(ctx context.Context) ( + agentproto.DRPCAgentClient211, proto.DRPCTailnetClient28, error, ) { aAPI, tAPI, err := c.ConnectRPC29(ctx) if err != nil { @@ -169,17 +169,17 @@ func (c *Client) ConnectRPC210(ctx context.Context) ( // the generated DRPCAgentClient interface, including // PushContextState, so the assertion always succeeds for // the fixture's own connections. - client, ok := aAPI.(agentproto.DRPCAgentClient210) + client, ok := aAPI.(agentproto.DRPCAgentClient211) if !ok { - return nil, nil, xerrors.Errorf("agenttest: connection does not implement DRPCAgentClient210; got %T", aAPI) + return nil, nil, xerrors.Errorf("agenttest: connection does not implement DRPCAgentClient211; got %T", aAPI) } return client, tAPI, nil } -func (c *Client) ConnectRPC210WithRole(ctx context.Context, _ string) ( - agentproto.DRPCAgentClient210, proto.DRPCTailnetClient28, error, +func (c *Client) ConnectRPC211WithRole(ctx context.Context, _ string) ( + agentproto.DRPCAgentClient211, proto.DRPCTailnetClient28, error, ) { - return c.ConnectRPC210(ctx) + return c.ConnectRPC211(ctx) } func (c *Client) ConnectRPC29(ctx context.Context) ( diff --git a/agent/proto/agent_drpc_old.go b/agent/proto/agent_drpc_old.go index f83c52c01ec76..886fef9552c59 100644 --- a/agent/proto/agent_drpc_old.go +++ b/agent/proto/agent_drpc_old.go @@ -99,3 +99,10 @@ type DRPCAgentClient210 interface { DRPCAgentClient29 PushContextState(ctx context.Context, in *PushContextStateRequest) (*PushContextStateResponse, error) } + +// DRPCAgentClient211 is the Agent API at v2.11. It adds the +// session_counts map field to Stats, deprecating the fixed +// session_count_* fields. No new RPCs. +type DRPCAgentClient211 interface { + DRPCAgentClient210 +} diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index b9365dc9749f7..c21407e80dc23 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1675,8 +1675,8 @@ func TemplateVersionTerraformValues(t testing.TB, db database.Store, orig databa return v } -// WorkspaceAgentStat inserts a workspace agent stat row. Pass at most one -// map to seed the workspace_agent_session_counts child table. +// WorkspaceAgentStat inserts a workspace agent stat row. An optional map +// seeds the workspace_agent_session_counts child table. func WorkspaceAgentStat(t testing.TB, db database.Store, orig database.WorkspaceAgentStat, sessionCounts ...map[string]int64) database.WorkspaceAgentStat { if orig.ConnectionsByProto == nil { orig.ConnectionsByProto = json.RawMessage([]byte("{}")) @@ -1684,12 +1684,8 @@ func WorkspaceAgentStat(t testing.TB, db database.Store, orig database.Workspace jsonProto := []byte(fmt.Sprintf("[%s]", orig.ConnectionsByProto)) counts := map[string]int64{} - switch len(sessionCounts) { - case 0: - case 1: + if len(sessionCounts) > 0 { counts = sessionCounts[0] - default: - t.Fatalf("expected at most one session counts map, got %d", len(sessionCounts)) } jsonSessionCounts, err := json.Marshal([]map[string]int64{counts}) require.NoError(t, err, "marshal session counts") diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 5762eb4f29d8f..5a32b5e5ae340 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3788,7 +3788,7 @@ CREATE TABLE workspace_agent_session_counts ( COMMENT ON TABLE workspace_agent_session_counts IS 'Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row.'; -COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty). Stored raw; display names and family grouping are applied at the API boundary.'; +COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion. Family grouping is applied at read time.'; CREATE SEQUENCE workspace_agent_startup_logs_id_seq START WITH 1 diff --git a/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql b/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql index cc55d919585c1..1d7c282868b9e 100644 --- a/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql +++ b/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql @@ -6,18 +6,21 @@ CREATE TABLE workspace_agent_session_counts ( ); COMMENT ON TABLE workspace_agent_session_counts IS 'Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row.'; -COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty). Stored raw; display names and family grouping are applied at the API boundary.'; +COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion. Family grouping is applied at read time.'; -- Copy the ephemeral buffer (~1 day of rows) into the new table so that -- template usage rollup and deployment stats see no gap during the upgrade. INSERT INTO workspace_agent_session_counts (workspace_agent_stats_id, app_name, count) -SELECT id, 'vscode', session_count_vscode FROM workspace_agent_stats WHERE session_count_vscode > 0 -UNION ALL -SELECT id, 'jetbrains', session_count_jetbrains FROM workspace_agent_stats WHERE session_count_jetbrains > 0 -UNION ALL -SELECT id, 'reconnecting_pty', session_count_reconnecting_pty FROM workspace_agent_stats WHERE session_count_reconnecting_pty > 0 -UNION ALL -SELECT id, 'ssh', session_count_ssh FROM workspace_agent_stats WHERE session_count_ssh > 0; +SELECT s.id, v.app_name, v.count +FROM workspace_agent_stats s +CROSS JOIN LATERAL ( + VALUES + ('vscode', s.session_count_vscode), + ('jetbrains', s.session_count_jetbrains), + ('reconnecting_pty', s.session_count_reconnecting_pty), + ('ssh', s.session_count_ssh) +) v(app_name, count) +WHERE v.count > 0; DROP INDEX workspace_agent_stats_template_id_created_at_user_id_idx; diff --git a/coderd/database/models.go b/coderd/database/models.go index d2401a90d9c49..5eab4d70e0e25 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -6496,7 +6496,7 @@ type WorkspaceAgentScriptTiming struct { // Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row. type WorkspaceAgentSessionCount struct { WorkspaceAgentStatsID uuid.UUID `db:"workspace_agent_stats_id" json:"workspace_agent_stats_id"` - // App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty). Stored raw; display names and family grouping are applied at the API boundary. + // App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion. Family grouping is applied at read time. AppName string `db:"app_name" json:"app_name"` Count int64 `db:"count" json:"count"` } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index a92b14a7d01e2..bc293eedfd973 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15629,9 +15629,8 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- The join supplies the session counts and filters out rows without - -- session activity in one hash-joinable pass; the DISTINCT/MAX - -- aggregates above tolerate the row fan-out. + -- The join filters out rows without session activity; the + -- DISTINCT/MAX aggregates above tolerate the row fan-out. JOIN workspace_agent_session_counts sc ON @@ -16264,9 +16263,8 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- The join supplies the session counts and filters out rows without - -- session activity in one hash-joinable pass; the DISTINCT/MAX - -- aggregates above tolerate the row fan-out. + -- The join filters out rows without session activity; the + -- DISTINCT/MAX aggregates above tolerate the row fan-out. JOIN workspace_agent_session_counts sc ON @@ -33958,20 +33956,12 @@ WITH stats AS ( ), session_stats AS ( -- Session counts from the latest stats row per agent. SELECT - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM stats - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = stats.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = stats.id WHERE stats.rn = 1 ) SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM byte_stats, session_stats @@ -34019,21 +34009,13 @@ minute_buckets AS ( SELECT was.agent_id, date_trunc('minute', was.created_at) AS minute_bucket, - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM workspace_agent_stats was - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = was.id WHERE was.created_at >= $1 AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute @@ -34114,23 +34096,15 @@ WITH agent_stats AS ( ), latest_agent_stats AS ( SELECT a.agent_id, - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM ( SELECT 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, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats WHERE created_at > $1 ) AS a - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = a.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = a.id WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id ) SELECT user_id, agent_stats.agent_id, workspace_id, template_id, aggregated_from, workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, latest_agent_stats.agent_id, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id @@ -34324,21 +34298,13 @@ minute_buckets AS ( SELECT was.agent_id, date_trunc('minute', was.created_at) AS minute_bucket, - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM workspace_agent_stats was - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = was.id WHERE was.created_at >= $1 AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index 78b5d6ce920be..67f9d09ce43f3 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -167,9 +167,8 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- The join supplies the session counts and filters out rows without - -- session activity in one hash-joinable pass; the DISTINCT/MAX - -- aggregates above tolerate the row fan-out. + -- The join filters out rows without session activity; the + -- DISTINCT/MAX aggregates above tolerate the row fan-out. JOIN workspace_agent_session_counts sc ON @@ -571,9 +570,8 @@ WITH MAX(was.connection_count) > 0 AS has_connection FROM workspace_agent_stats was - -- The join supplies the session counts and filters out rows without - -- session activity in one hash-joinable pass; the DISTINCT/MAX - -- aggregates above tolerate the row fan-out. + -- The join filters out rows without session activity; the + -- DISTINCT/MAX aggregates above tolerate the row fan-out. JOIN workspace_agent_session_counts sc ON diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index 675316e594775..94711223884f2 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -103,20 +103,12 @@ WITH stats AS ( ), session_stats AS ( -- Session counts from the latest stats row per agent. SELECT - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM stats - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = stats.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = stats.id WHERE stats.rn = 1 ) SELECT * FROM byte_stats, session_stats; @@ -136,21 +128,13 @@ minute_buckets AS ( SELECT was.agent_id, date_trunc('minute', was.created_at) AS minute_bucket, - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM workspace_agent_stats was - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = was.id WHERE was.created_at >= $1 AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute @@ -203,23 +187,15 @@ WITH agent_stats AS ( ), latest_agent_stats AS ( SELECT a.agent_id, - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn FROM workspace_agent_stats WHERE created_at > $1 ) AS a - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = a.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = a.id WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id ) SELECT * FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id; @@ -245,21 +221,13 @@ minute_buckets AS ( SELECT was.agent_id, date_trunc('minute', was.created_at) AS minute_bucket, - coalesce(SUM(sc.vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(sc.ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(sc.jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(sc.reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'vscode'), 0)::bigint AS session_count_vscode, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'ssh'), 0)::bigint AS session_count_ssh, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'jetbrains'), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(sc.count) FILTER (WHERE sc.app_name = 'reconnecting_pty'), 0)::bigint AS session_count_reconnecting_pty FROM workspace_agent_stats was - LEFT JOIN LATERAL ( - SELECT - coalesce(SUM(count) FILTER (WHERE app_name = 'vscode'), 0)::bigint AS vscode, - coalesce(SUM(count) FILTER (WHERE app_name = 'ssh'), 0)::bigint AS ssh, - coalesce(SUM(count) FILTER (WHERE app_name = 'jetbrains'), 0)::bigint AS jetbrains, - coalesce(SUM(count) FILTER (WHERE app_name = 'reconnecting_pty'), 0)::bigint AS reconnecting_pty - FROM workspace_agent_session_counts - WHERE workspace_agent_stats_id = was.id - ) sc ON TRUE + LEFT JOIN workspace_agent_session_counts sc ON sc.workspace_agent_stats_id = was.id WHERE was.created_at >= $1 AND was.created_at < date_trunc('minute', now()) -- Exclude current partial minute diff --git a/coderd/idemetadata/idemetadata.go b/coderd/idemetadata/idemetadata.go index af4ae07383540..e314352b115d3 100644 --- a/coderd/idemetadata/idemetadata.go +++ b/coderd/idemetadata/idemetadata.go @@ -1,12 +1,16 @@ // Package idemetadata is the single source of truth for metadata about IDEs // and other session types that report workspace usage. App names are stored -// raw throughout the stats pipeline; this package canonicalizes well-known -// legacy spellings at ingestion and supplies the family and display-name -// grouping applied at API and metrics boundaries. It is a leaf package so -// both agent and coderd code can import it. +// as reported throughout the stats pipeline; well-known spellings are +// canonicalized at ingestion and grouped into families at API and metrics +// boundaries. It is a leaf package so both agent and coderd code can import +// it. package idemetadata -import "strings" +import ( + "strings" + + utilstrings "github.com/coder/coder/v2/coderd/util/strings" +) // MaxAppNameLength is the maximum length of an app name in runes. Longer // names are truncated before storage. @@ -25,92 +29,60 @@ const ( AppNameOther = "other" ) -// Families group related session types for display and for metric labels, -// keeping label cardinality bounded while arbitrary app names flow through -// the pipeline. -const ( - FamilyVSCode = "vscode" - FamilyJetBrains = "jetbrains" - FamilySSH = "ssh" - FamilyReconnectingPTY = "reconnecting_pty" - FamilyUnknown = "unknown" -) - -// IDEInfo describes how a session type is grouped and displayed. -type IDEInfo struct { - Family string - DisplayName string -} - -// known maps canonical (lowercase) app names to their metadata. -var known = map[string]IDEInfo{ - AppNameVSCode: {Family: FamilyVSCode, DisplayName: "VS Code"}, - "vscode_insiders": {Family: FamilyVSCode, DisplayName: "VS Code Insiders"}, - "cursor": {Family: FamilyVSCode, DisplayName: "Cursor"}, - "windsurf": {Family: FamilyVSCode, DisplayName: "Windsurf"}, - "positron": {Family: FamilyVSCode, DisplayName: "Positron"}, - "vscodium": {Family: FamilyVSCode, DisplayName: "VSCodium"}, - AppNameJetBrains: {Family: FamilyJetBrains, DisplayName: "JetBrains"}, - AppNameSSH: {Family: FamilySSH, DisplayName: "SSH"}, - AppNameReconnectingPTY: {Family: FamilyReconnectingPTY, DisplayName: "Web Terminal"}, - AppNameUnknown: {Family: FamilyUnknown, DisplayName: "Unknown"}, - AppNameOther: {Family: FamilyUnknown, DisplayName: "Other"}, +// families maps canonical (lowercase) app names to their family, keeping +// metric-label cardinality bounded while arbitrary app names flow through +// the pipeline. A family is named after its canonical app name. Names are +// stored as reported and grouped at read time, so extending this map needs +// no migration and applies retroactively to stored data. +var families = map[string]string{ + AppNameVSCode: AppNameVSCode, + "vscode_insiders": AppNameVSCode, + "cursor": AppNameVSCode, + "windsurf": AppNameVSCode, + "positron": AppNameVSCode, + "vscodium": AppNameVSCode, + "codium": AppNameVSCode, + "antigravity": AppNameVSCode, + "trae": AppNameVSCode, + "kiro": AppNameVSCode, + "devin": AppNameVSCode, + AppNameJetBrains: AppNameJetBrains, + AppNameSSH: AppNameSSH, + AppNameReconnectingPTY: AppNameReconnectingPTY, + AppNameUnknown: AppNameUnknown, + AppNameOther: AppNameUnknown, } -// aliases maps well-known legacy spellings (lowercase) to canonical app -// names so historical client values keep aggregating with their canonical -// counterparts. -var aliases = map[string]string{ - // codersdk.UsageAppNameReconnectingPty uses a hyphen. - "reconnecting-pty": AppNameReconnectingPTY, -} - -// Lookup returns metadata for the given app name. Matching is -// case-insensitive and alias-aware; unknown names fall back to family -// "unknown" with the raw name as the display name. -func Lookup(appName string) IDEInfo { - if info, ok := known[canonicalKey(appName)]; ok { - return info +// Family returns the family for the given app name. Matching is +// case-insensitive and alias-aware; unknown names map to AppNameUnknown. +func Family(appName string) string { + if family, ok := families[canonicalKey(appName)]; ok { + return family } - return IDEInfo{Family: FamilyUnknown, DisplayName: appName} + return AppNameUnknown } // Normalize prepares a client-supplied app name for storage: it strips null // bytes (which Postgres TEXT rejects), truncates to MaxAppNameLength runes, -// and canonicalizes well-known names and legacy spellings -// case-insensitively. Unrecognized names are preserved as-is, including -// casing; they are grouped at display time via Lookup. An empty result -// becomes "unknown" so a bad name never invalidates the surrounding report. +// and canonicalizes well-known names case-insensitively. Unrecognized names +// are preserved as-is, including casing, and grouped at display time via +// Family. An empty result becomes AppNameUnknown so a bad name never +// invalidates the surrounding report. func Normalize(appName string) string { appName = strings.ReplaceAll(appName, "\x00", "") - if runes := []rune(appName); len(runes) > MaxAppNameLength { - appName = string(runes[:MaxAppNameLength]) - } + appName = utilstrings.Truncate(appName, MaxAppNameLength) if appName == "" { return AppNameUnknown } - if canonical, ok := canonicalName(appName); ok { - return canonical + if key := canonicalKey(appName); families[key] != "" { + return key } return appName } -// canonicalName resolves well-known names and aliases to their canonical -// form, reporting whether the name was recognized. -func canonicalName(appName string) (string, bool) { - key := canonicalKey(appName) - if _, ok := known[key]; ok { - return key, true - } - return appName, false -} - -// canonicalKey lowercases and resolves aliases to produce the map key for -// a raw app name. +// canonicalKey lowercases the app name and folds hyphens to underscores so +// spellings like "reconnecting-pty" (codersdk.UsageAppNameReconnectingPty) +// and "vscode-insiders" match their canonical form. func canonicalKey(appName string) string { - key := strings.ToLower(appName) - if alias, ok := aliases[key]; ok { - return alias - } - return key + return strings.ReplaceAll(strings.ToLower(appName), "-", "_") } diff --git a/coderd/idemetadata/idemetadata_internal_test.go b/coderd/idemetadata/idemetadata_internal_test.go new file mode 100644 index 0000000000000..ea391492c7779 --- /dev/null +++ b/coderd/idemetadata/idemetadata_internal_test.go @@ -0,0 +1,16 @@ +package idemetadata + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// Lookups fold input through canonicalKey before probing, so map keys that +// are not already canonical can never match. +func TestFamilyKeysAreCanonical(t *testing.T) { + t.Parallel() + for name := range families { + require.Equal(t, canonicalKey(name), name) + } +} diff --git a/coderd/idemetadata/idemetadata_test.go b/coderd/idemetadata/idemetadata_test.go index af509ed9bb4fe..31178f5cb8cca 100644 --- a/coderd/idemetadata/idemetadata_test.go +++ b/coderd/idemetadata/idemetadata_test.go @@ -21,6 +21,8 @@ func TestNormalize(t *testing.T) { {name: "KnownNameCaseInsensitive", input: "JetBrains", want: "jetbrains"}, {name: "LegacyAlias", input: "reconnecting-pty", want: "reconnecting_pty"}, {name: "LegacyAliasCaseInsensitive", input: "Reconnecting-PTY", want: "reconnecting_pty"}, + {name: "HyphenatedKnownName", input: "vscode-insiders", want: "vscode_insiders"}, + {name: "UnknownHyphenatedPreserved", input: "my-future-ide", want: "my-future-ide"}, {name: "UnknownPreservesCasing", input: "Cursor Nightly", want: "Cursor Nightly"}, {name: "UnknownPreservesUnicode", input: "エディタ", want: "エディタ"}, {name: "StripsNullBytes", input: "cur\x00sor", want: "cursor"}, @@ -45,26 +47,26 @@ func TestNormalize(t *testing.T) { } } -func TestLookup(t *testing.T) { +func TestFamily(t *testing.T) { t.Parallel() for _, tc := range []struct { - name string - input string - wantFamily string - wantDisplayName string + name string + input string + want string }{ - {name: "VSCode", input: "vscode", wantFamily: idemetadata.FamilyVSCode, wantDisplayName: "VS Code"}, - {name: "VSCodeFork", input: "cursor", wantFamily: idemetadata.FamilyVSCode, wantDisplayName: "Cursor"}, - {name: "CaseInsensitive", input: "JetBrains", wantFamily: idemetadata.FamilyJetBrains, wantDisplayName: "JetBrains"}, - {name: "Alias", input: "reconnecting-pty", wantFamily: idemetadata.FamilyReconnectingPTY, wantDisplayName: "Web Terminal"}, - {name: "UnknownFallsBackToRawName", input: "SomeFutureIDE", wantFamily: idemetadata.FamilyUnknown, wantDisplayName: "SomeFutureIDE"}, + {name: "VSCode", input: "vscode", want: idemetadata.AppNameVSCode}, + {name: "VSCodeFork", input: "cursor", want: idemetadata.AppNameVSCode}, + {name: "VSCodeInsidersHyphenated", input: "vscode-insiders", want: idemetadata.AppNameVSCode}, + {name: "VSCodium", input: "codium", want: idemetadata.AppNameVSCode}, + {name: "CaseInsensitive", input: "JetBrains", want: idemetadata.AppNameJetBrains}, + {name: "Alias", input: "reconnecting-pty", want: idemetadata.AppNameReconnectingPTY}, + {name: "Other", input: "other", want: idemetadata.AppNameUnknown}, + {name: "UnknownName", input: "SomeFutureIDE", want: idemetadata.AppNameUnknown}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() - info := idemetadata.Lookup(tc.input) - require.Equal(t, tc.wantFamily, info.Family) - require.Equal(t, tc.wantDisplayName, info.DisplayName) + require.Equal(t, tc.want, idemetadata.Family(tc.input)) }) } } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 65c7eb1dbdf6d..4489cde706e75 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -3251,7 +3251,7 @@ func requireGetManifest(ctx context.Context, t testing.TB, aAPI agentproto.DRPCA } func postStartup(ctx context.Context, t testing.TB, client agent.Client, startup *agentproto.Startup) error { - aAPI, _, err := client.ConnectRPC210(ctx) + aAPI, _, err := client.ConnectRPC211(ctx) require.NoError(t, err) defer func() { cErr := aAPI.DRPCConn().Close() diff --git a/coderd/workspaces.go b/coderd/workspaces.go index bdc956bb4cd7a..965f1bee90208 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -29,7 +29,6 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpapi/httperror" "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/coderd/idemetadata" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" @@ -1788,14 +1787,10 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { return } - // Any app name is accepted and stored raw; well-known legacy spellings - // (e.g. "reconnecting-pty") are canonicalized so they aggregate with - // agent-reported session types. - appName := idemetadata.Normalize(string(req.AppName)) - + // Any app name is accepted; it is normalized at stats ingestion. stat := &proto.Stats{ ConnectionCount: 1, - SessionCounts: map[string]int64{appName: 1}, + SessionCounts: map[string]int64{string(req.AppName): 1}, } agent, err := api.Database.GetWorkspaceAgentByID(ctx, req.AgentID) diff --git a/coderd/workspacestats/batcher.go b/coderd/workspacestats/batcher.go index 23ebae6980015..8b1c7ee39be16 100644 --- a/coderd/workspacestats/batcher.go +++ b/coderd/workspacestats/batcher.go @@ -243,17 +243,8 @@ func (b *DBBatcher) flush(ctx context.Context, forced bool, reason string) { b.buf.ConnectionsByProto = payload } - // marshal session counts. The JSONB array is zipped with the ID array - // by position, so it must have one element per stats row. On mismatch, - // drop the session counts (parent stats still insert) rather than - // misattribute them. - if len(b.sessionCounts) != len(b.buf.ID) { - b.log.Error(ctx, "session counts buffer out of sync with stats buffer, dropping session counts", - slog.F("stats", len(b.buf.ID)), - slog.F("session_counts", len(b.sessionCounts)), - ) - b.sessionCounts = b.sessionCounts[:0] - } + // marshal session counts; the JSONB array is zipped with the ID array + // by position on insert sessionCountsPayload, err := json.Marshal(b.sessionCounts) if err != nil { b.log.Error(ctx, "unable to marshal agent session counts, dropping data", slog.Error(err)) diff --git a/coderd/workspacestats/reporter.go b/coderd/workspacestats/reporter.go index 4a35e978b04f4..7705f50c74253 100644 --- a/coderd/workspacestats/reporter.go +++ b/coderd/workspacestats/reporter.go @@ -154,7 +154,7 @@ func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspac } // workspace activity: if no sessions we do not bump activity - if usage && len(SessionCountsFromProto(stats)) == 0 { + if usage && !HasSessionCounts(stats) { return nil } diff --git a/coderd/workspacestats/sessioncounts.go b/coderd/workspacestats/sessioncounts.go index 86d1ce79b8d7e..e81cc6dbabdce 100644 --- a/coderd/workspacestats/sessioncounts.go +++ b/coderd/workspacestats/sessioncounts.go @@ -1,51 +1,64 @@ package workspacestats import ( - "sort" + "maps" + "slices" + "strings" agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/idemetadata" ) // maxSessionCountEntries bounds the number of distinct app names accepted -// per stats report. Counts beyond the cap are aggregated under -// idemetadata.AppNameOther so a malicious or buggy agent cannot fan out -// child-table rows. Legitimate agents report a handful of entries. +// per stats report so a malicious or buggy agent cannot fan out child-table +// rows. Overflow is aggregated under idemetadata.AppNameOther. const maxSessionCountEntries = 64 // SessionCountsFromProto returns the per-app session counts reported by an -// agent. The deprecated fixed fields are only populated by agents from -// before the session_counts map was introduced and are converted here. -// Entries with non-positive counts are dropped, and reports with more than -// maxSessionCountEntries distinct names have the overflow aggregated under -// idemetadata.AppNameOther. +// agent, with names normalized and non-positive counts dropped. The +// deprecated fixed fields are converted for agents from before the +// session_counts map (API < 2.11). func SessionCountsFromProto(st *agentproto.Stats) map[string]int64 { counts := make(map[string]int64, len(st.GetSessionCounts())) for app, count := range st.GetSessionCounts() { - if count <= 0 { - continue + if count > 0 { + counts[idemetadata.Normalize(app)] += count } - counts[app] = count } if len(counts) > 0 { return capSessionCounts(counts) } - // Old-agent fallback: convert the deprecated fixed fields. //nolint:staticcheck // Deprecated fields are read for old-agent compatibility. - for app, count := range map[string]int64{ - idemetadata.AppNameVSCode: st.SessionCountVscode, - idemetadata.AppNameJetBrains: st.SessionCountJetbrains, - idemetadata.AppNameReconnectingPTY: st.SessionCountReconnectingPty, - idemetadata.AppNameSSH: st.SessionCountSsh, - } { - if count <= 0 { - continue + legacy := [...]struct { + app string + count int64 + }{ + {idemetadata.AppNameVSCode, st.SessionCountVscode}, + {idemetadata.AppNameJetBrains, st.SessionCountJetbrains}, + {idemetadata.AppNameReconnectingPTY, st.SessionCountReconnectingPty}, + {idemetadata.AppNameSSH, st.SessionCountSsh}, + } + for _, l := range legacy { + if l.count > 0 { + counts[l.app] = l.count } - counts[app] = count } return counts } +// HasSessionCounts reports whether the stats contain any active session, +// without building the counts map. +func HasSessionCounts(st *agentproto.Stats) bool { + for _, count := range st.GetSessionCounts() { + if count > 0 { + return true + } + } + //nolint:staticcheck // Deprecated fields are read for old-agent compatibility. + return st.SessionCountVscode > 0 || st.SessionCountJetbrains > 0 || + st.SessionCountReconnectingPty > 0 || st.SessionCountSsh > 0 +} + // capSessionCounts keeps at most maxSessionCountEntries named entries, // preferring well-known names and then lexicographic order for determinism, // and sums the remainder into idemetadata.AppNameOther. @@ -53,25 +66,26 @@ func capSessionCounts(counts map[string]int64) map[string]int64 { if len(counts) <= maxSessionCountEntries { return counts } - names := make([]string, 0, len(counts)) - for name := range counts { - names = append(names, name) + names := slices.Collect(maps.Keys(counts)) + known := make(map[string]bool, len(names)) + for _, name := range names { + known[name] = idemetadata.Family(name) != idemetadata.AppNameUnknown } - sort.Slice(names, func(i, j int) bool { - iKnown := idemetadata.Lookup(names[i]).Family != idemetadata.FamilyUnknown - jKnown := idemetadata.Lookup(names[j]).Family != idemetadata.FamilyUnknown - if iKnown != jKnown { - return iKnown + slices.SortFunc(names, func(a, b string) int { + if known[a] != known[b] { + if known[a] { + return -1 + } + return 1 } - return names[i] < names[j] + return strings.Compare(a, b) }) capped := make(map[string]int64, maxSessionCountEntries+1) + for _, name := range names[:maxSessionCountEntries] { + capped[name] = counts[name] + } var other int64 - for i, name := range names { - if i < maxSessionCountEntries { - capped[name] = counts[name] - continue - } + for _, name := range names[maxSessionCountEntries:] { other += counts[name] } if other > 0 { diff --git a/coderd/workspacestats/sessioncounts_test.go b/coderd/workspacestats/sessioncounts_test.go index c7a10254bd992..31fcf182ab13b 100644 --- a/coderd/workspacestats/sessioncounts_test.go +++ b/coderd/workspacestats/sessioncounts_test.go @@ -17,10 +17,21 @@ func TestSessionCountsFromProto(t *testing.T) { t.Parallel() //nolint:staticcheck // Deprecated fields are set to verify precedence. st := &agentproto.Stats{ - SessionCounts: map[string]int64{"Cursor": 2, "ssh": 1}, + SessionCounts: map[string]int64{"cursor": 2, "ssh": 1}, SessionCountVscode: 9, } - require.Equal(t, map[string]int64{"Cursor": 2, "ssh": 1}, workspacestats.SessionCountsFromProto(st)) + require.Equal(t, map[string]int64{"cursor": 2, "ssh": 1}, workspacestats.SessionCountsFromProto(st)) + }) + + t.Run("NormalizesAndMergesNames", func(t *testing.T) { + t.Parallel() + st := &agentproto.Stats{ + SessionCounts: map[string]int64{"VSCode": 1, "vscode": 2, "Reconnecting-PTY": 1}, + } + require.Equal(t, map[string]int64{ + "vscode": 3, + "reconnecting_pty": 1, + }, workspacestats.SessionCountsFromProto(st)) }) t.Run("DropsNonPositiveEntries", func(t *testing.T) { @@ -78,6 +89,20 @@ func TestSessionCountsFromProto(t *testing.T) { }) } +func TestHasSessionCounts(t *testing.T) { + t.Parallel() + + require.False(t, workspacestats.HasSessionCounts(&agentproto.Stats{})) + require.False(t, workspacestats.HasSessionCounts(&agentproto.Stats{ + SessionCounts: map[string]int64{"ssh": 0}, + })) + require.True(t, workspacestats.HasSessionCounts(&agentproto.Stats{ + SessionCounts: map[string]int64{"ssh": 1}, + })) + //nolint:staticcheck // Deprecated fields simulate an old agent. + require.True(t, workspacestats.HasSessionCounts(&agentproto.Stats{SessionCountJetbrains: 1})) +} + func TestClearSessionCounts(t *testing.T) { t.Parallel() diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index c199b1c873b88..e6070235324c9 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -349,13 +349,26 @@ func (c *Client) ConnectRPC210(ctx context.Context) ( return proto.NewDRPCAgentClient(conn), tailnetproto.NewDRPCTailnetClient(conn), nil } -// ConnectRPC210WithRole is like ConnectRPC210 but sends an explicit role +// ConnectRPC211 returns a dRPC client to the Agent API v2.11. It is useful +// when you want to report per-app session counts via the session_counts map +// on Stats instead of the deprecated fixed fields. +func (c *Client) ConnectRPC211(ctx context.Context) ( + proto.DRPCAgentClient211, tailnetproto.DRPCTailnetClient28, error, +) { + conn, err := c.connectRPCVersion(ctx, apiversion.New(2, 11), "") + if err != nil { + return nil, nil, err + } + return proto.NewDRPCAgentClient(conn), tailnetproto.NewDRPCTailnetClient(conn), nil +} + +// ConnectRPC211WithRole is like ConnectRPC211 but sends an explicit role // query parameter to the server. Use "agent" for workspace agents to // enable connection monitoring. -func (c *Client) ConnectRPC210WithRole(ctx context.Context, role string) ( - proto.DRPCAgentClient210, tailnetproto.DRPCTailnetClient28, error, +func (c *Client) ConnectRPC211WithRole(ctx context.Context, role string) ( + proto.DRPCAgentClient211, tailnetproto.DRPCTailnetClient28, error, ) { - conn, err := c.connectRPCVersion(ctx, apiversion.New(2, 10), role) + conn, err := c.connectRPCVersion(ctx, apiversion.New(2, 11), role) if err != nil { return nil, nil, err } From 95cf6ef199218d364601272ee9b271263b16495f Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Sun, 12 Jul 2026 15:56:17 +0300 Subject: [PATCH 4/4] fix(coderd/idemetadata): store app names in canonical form Fold every app name to lowercase with hyphens as underscores at ingestion instead of only rewriting well-known spellings. Once the rollup starts persisting arbitrary names durably, raw storage would split aggregation keys across spellings forever; canonical storage removes the read-side folding obligation. Display casing returns via the phase-2 metadata map. --- agent/agentssh/agentssh.go | 4 +-- agent/agentssh/sessiontype_internal_test.go | 2 +- coderd/database/dump.sql | 2 +- ...0543_workspace_agent_session_counts.up.sql | 2 +- coderd/database/models.go | 2 +- coderd/idemetadata/idemetadata.go | 31 +++++++++---------- coderd/idemetadata/idemetadata_test.go | 4 +-- coderd/workspacestats/sessioncounts_test.go | 7 +++-- 8 files changed, 26 insertions(+), 28 deletions(-) diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index 1cddb9d069e1a..1d845fa0473bd 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -363,8 +363,8 @@ func extractMagicSessionType(env []string) (sessionType MagicSessionType, filter // No magic session type set: this is a plain SSH session. sessionType = MagicSessionTypeSSH } else { - // Clean, do not classify: the raw string is preserved (well-known - // names are canonicalized) so new IDEs flow through unchanged. + // Canonicalize, do not classify: unknown names flow through in + // canonical form so new IDEs need no code changes. sessionType = MagicSessionType(idemetadata.Normalize(rawType)) } diff --git a/agent/agentssh/sessiontype_internal_test.go b/agent/agentssh/sessiontype_internal_test.go index 3973be1922b96..c8aafd651101b 100644 --- a/agent/agentssh/sessiontype_internal_test.go +++ b/agent/agentssh/sessiontype_internal_test.go @@ -27,7 +27,7 @@ func TestExtractMagicSessionType(t *testing.T) { {name: "EmptyValueDefaultsToSSH", env: envWith(""), want: MagicSessionTypeSSH}, {name: "VSCode", env: envWith("vscode"), want: MagicSessionTypeVSCode}, {name: "JetBrainsLegacyCasing", env: envWith("JetBrains"), want: MagicSessionTypeJetBrains}, - {name: "UnknownPreservedRaw", env: envWith("Cursor Nightly"), want: MagicSessionType("Cursor Nightly")}, + {name: "UnknownFoldedToCanonicalForm", env: envWith("Cursor Nightly"), want: MagicSessionType("cursor nightly")}, {name: "LastInstanceWins", env: append(envWith("vscode"), MagicSessionTypeEnvironmentVariable+"=cursor"), want: MagicSessionType("cursor")}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 5a32b5e5ae340..07735ac035239 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -3788,7 +3788,7 @@ CREATE TABLE workspace_agent_session_counts ( COMMENT ON TABLE workspace_agent_session_counts IS 'Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row.'; -COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion. Family grouping is applied at read time.'; +COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion: lowercased with hyphens folded to underscores. Family grouping is applied at read time.'; CREATE SEQUENCE workspace_agent_startup_logs_id_seq START WITH 1 diff --git a/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql b/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql index 1d7c282868b9e..d66730e8ebe87 100644 --- a/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql +++ b/coderd/database/migrations/000543_workspace_agent_session_counts.up.sql @@ -6,7 +6,7 @@ CREATE TABLE workspace_agent_session_counts ( ); COMMENT ON TABLE workspace_agent_session_counts IS 'Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row.'; -COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion. Family grouping is applied at read time.'; +COMMENT ON COLUMN workspace_agent_session_counts.app_name IS 'App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion: lowercased with hyphens folded to underscores. Family grouping is applied at read time.'; -- Copy the ephemeral buffer (~1 day of rows) into the new table so that -- template usage rollup and deployment stats see no gap during the upgrade. diff --git a/coderd/database/models.go b/coderd/database/models.go index 5eab4d70e0e25..6f8cd80376a7e 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -6496,7 +6496,7 @@ type WorkspaceAgentScriptTiming struct { // Session counts per app for each workspace agent stats row. Rows are removed together with their parent workspace_agent_stats row. type WorkspaceAgentSessionCount struct { WorkspaceAgentStatsID uuid.UUID `db:"workspace_agent_stats_id" json:"workspace_agent_stats_id"` - // App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion. Family grouping is applied at read time. + // App identifier as reported by the client (e.g. vscode, jetbrains, ssh, reconnecting_pty), canonicalized at ingestion: lowercased with hyphens folded to underscores. Family grouping is applied at read time. AppName string `db:"app_name" json:"app_name"` Count int64 `db:"count" json:"count"` } diff --git a/coderd/idemetadata/idemetadata.go b/coderd/idemetadata/idemetadata.go index e314352b115d3..d613e6e5a5e21 100644 --- a/coderd/idemetadata/idemetadata.go +++ b/coderd/idemetadata/idemetadata.go @@ -1,9 +1,8 @@ // Package idemetadata is the single source of truth for metadata about IDEs // and other session types that report workspace usage. App names are stored -// as reported throughout the stats pipeline; well-known spellings are -// canonicalized at ingestion and grouped into families at API and metrics -// boundaries. It is a leaf package so both agent and coderd code can import -// it. +// in canonical form (see Normalize) and grouped into families at API and +// metrics boundaries. It is a leaf package so both agent and coderd code can +// import it. package idemetadata import ( @@ -29,11 +28,11 @@ const ( AppNameOther = "other" ) -// families maps canonical (lowercase) app names to their family, keeping -// metric-label cardinality bounded while arbitrary app names flow through -// the pipeline. A family is named after its canonical app name. Names are -// stored as reported and grouped at read time, so extending this map needs -// no migration and applies retroactively to stored data. +// families maps canonical app names to their family, keeping metric-label +// cardinality bounded while arbitrary app names flow through the pipeline. +// A family is named after its canonical app name. Grouping happens at read +// time, so extending this map needs no migration and applies retroactively +// to stored data. var families = map[string]string{ AppNameVSCode: AppNameVSCode, "vscode_insiders": AppNameVSCode, @@ -64,20 +63,18 @@ func Family(appName string) string { // Normalize prepares a client-supplied app name for storage: it strips null // bytes (which Postgres TEXT rejects), truncates to MaxAppNameLength runes, -// and canonicalizes well-known names case-insensitively. Unrecognized names -// are preserved as-is, including casing, and grouped at display time via -// Family. An empty result becomes AppNameUnknown so a bad name never -// invalidates the surrounding report. +// lowercases, and folds hyphens to underscores. Every name is stored in this +// canonical form so aggregation keys never split across spellings once they +// reach the durable rollup; display casing for well-known names comes from +// IDE metadata at the API boundary. An empty result becomes AppNameUnknown +// so a bad name never invalidates the surrounding report. func Normalize(appName string) string { appName = strings.ReplaceAll(appName, "\x00", "") appName = utilstrings.Truncate(appName, MaxAppNameLength) if appName == "" { return AppNameUnknown } - if key := canonicalKey(appName); families[key] != "" { - return key - } - return appName + return canonicalKey(appName) } // canonicalKey lowercases the app name and folds hyphens to underscores so diff --git a/coderd/idemetadata/idemetadata_test.go b/coderd/idemetadata/idemetadata_test.go index 31178f5cb8cca..9134bfe5b0138 100644 --- a/coderd/idemetadata/idemetadata_test.go +++ b/coderd/idemetadata/idemetadata_test.go @@ -22,8 +22,8 @@ func TestNormalize(t *testing.T) { {name: "LegacyAlias", input: "reconnecting-pty", want: "reconnecting_pty"}, {name: "LegacyAliasCaseInsensitive", input: "Reconnecting-PTY", want: "reconnecting_pty"}, {name: "HyphenatedKnownName", input: "vscode-insiders", want: "vscode_insiders"}, - {name: "UnknownHyphenatedPreserved", input: "my-future-ide", want: "my-future-ide"}, - {name: "UnknownPreservesCasing", input: "Cursor Nightly", want: "Cursor Nightly"}, + {name: "UnknownHyphensFolded", input: "my-future-ide", want: "my_future_ide"}, + {name: "UnknownLowercased", input: "Cursor Nightly", want: "cursor nightly"}, {name: "UnknownPreservesUnicode", input: "エディタ", want: "エディタ"}, {name: "StripsNullBytes", input: "cur\x00sor", want: "cursor"}, {name: "Empty", input: "", want: "unknown"}, diff --git a/coderd/workspacestats/sessioncounts_test.go b/coderd/workspacestats/sessioncounts_test.go index 31fcf182ab13b..868d532d86263 100644 --- a/coderd/workspacestats/sessioncounts_test.go +++ b/coderd/workspacestats/sessioncounts_test.go @@ -75,12 +75,13 @@ func TestSessionCountsFromProto(t *testing.T) { } got := workspacestats.SessionCountsFromProto(&agentproto.Stats{SessionCounts: sessionCounts}) // 64 named entries (well-known first, then lexicographic) plus - // "other" holding the 137 overflow counts. + // "other" holding the 137 overflow counts. Names are stored in + // canonical form, so the hyphens fold to underscores. require.Len(t, got, 65) require.EqualValues(t, 5, got["vscode"]) - require.EqualValues(t, 1, got["zz-ide-000"]) + require.EqualValues(t, 1, got["zz_ide_000"]) require.EqualValues(t, 137, got["other"]) - require.NotContains(t, got, "zz-ide-199") + require.NotContains(t, got, "zz_ide_199") }) t.Run("Empty", func(t *testing.T) {