mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
## Summary Implements DEVEX-664: the coder CLI commands that open a workspace connection now generate a client session ID and attach it to their requests, logs, and existing network telemetry, per the connection-log RFC. Commands opt in explicitly with a `client_session_id` annotation. The opted-in set is `coder ssh`, `coder start`, `coder stop`, and `coder update`. Long-running daemon commands (`coder server`, `agent`, `provisionerd`, and so on) deliberately stay out, so they never carry a meaningless session ID in their logs, request baggage, or telemetry. - **Generate / resolve**: a 16-byte lowercase-hex session ID is generated per opted-in invocation. When `CODER_TRACE_SESSION_ID` is set (so Toolbox and the VS Code plugin can correlate the CLI they spawn), that value is used verbatim; a warning is logged if it is not the canonical 32-char lowercase hex form, since coderd/agent middleware drop non-canonical values. - **Requests**: the ID is attached to every request as `client_session_id` W3C baggage via a transport wrapper, independent of whether tracing is enabled. The already-merged coderd tracing middleware (#27671) reads it. - **Coordinate websocket**: the ID is also set explicitly as baggage on the tailnet coordinate handshake in `workspacesdk.DialAgent`, so coderd's `/api/v2/workspaceagents/{id}/coordinate` endpoint logs it (it does not rely on the CLI transport for that dial). - **Logs**: the ID is added to the command's log context. - **Existing telemetry**: the ID is added to tailnet `TelemetryEvent` (`client_session_id`, new field 20; proto minor version bumped) and plumbed from the CLI through `workspacesdk.DialAgent`. Scope is the coder CLI client only (RFC glossary + requirement 5), and within the CLI only the opted-in workspace-connecting commands above. This PR is independent of #28039 (the agent middleware that consumes the baggage); it depends only on `tracing.SessionIDBaggageKey`, which is already on `main` via #27671. ## Out of scope RFC requirements 7-16 (connection-state change logging, `connection_logs` columns, log buffering, routing/disconnect-reason logs, coordination-protocol plumbing, `agentssh`/`reconnectingpty` command logging, support-bundle log capture) and requirements 4.2/4.3 (VS Code telemetry, Prometheus stats) are separate tickets. IDE plugins, Toolbox, and the daemon commands are also out of scope. ## Testing - `go test ./coderd/tracing/... ./cli/... ./tailnet/... ./codersdk/workspacesdk/...` (unit tests for the generator, the env/generate resolver, per-command opt-in, the baggage transport wrapper, the coordinate-dial baggage helper, and the telemetry field). - `go build ./...`, `go vet`, and `golangci-lint` pass on the touched packages. - `make tailnet/proto/tailnet.pb.go` regenerated the proto; the proto minor version was bumped and `postStartup` now connects at the current agent API version so `TestWorkspaceAgent_Startup/OK` tracks it. > Note: the pre-commit `enterprise/cli/.gen-golden` step could not run in this environment (no Postgres: `open postgres connection: EOF`). `cli/testdata/.gen-golden` passed; this change adds no serpent flags, so there is no CLI help/golden drift. <details> <summary>Implementation plan</summary> ### Scope mapping (RFC -> DEVEX-664) In scope (the CLI workspace-connecting subset of RFC phase 2): - Req 1: generate a 16-byte session ID as 32-char lowercase hex. - Req 2: add the session ID to every log the command makes for the session. - Req 3: attach the session ID to every API request via `client_session_id` baggage, including the coordinate websocket handshake. (The query-param fallback is browser-only; not applicable to the CLI.) - Req 4.1: attach the session ID to CLI network telemetry. - Req 5 / 5.3: support `CODER_TRACE_SESSION_ID`; when set, use it instead of generating. ### Resolved decisions - Scope: opt-in per command via a `client_session_id` annotation; opted-in set is `coder ssh`, `coder start`, `coder stop`, `coder update`. Daemon commands are excluded. - `CODER_TRACE_SESSION_ID`: when set, use the value verbatim; warn if it is not canonical 32-char lowercase hex. When unset, generate a fresh ID. - Existing telemetry = CLI network telemetry (tailnet `TelemetryEvent`); requires a new proto field + plumbing and a proto minor version bump. - Coordinate endpoint: set the session ID as baggage on the coordinate handshake explicitly in `DialAgent` (header, not query param) so it is logged server-side without relying on the CLI transport. - Shared helper: add `tracing.NewSessionID()` and export the validator as `tracing.ValidSessionID` for a single source of truth. ### Implementation 1. `coderd/tracing`: add `NewSessionID()` (16 random bytes, hex-encoded); export `ValidSessionID`. 2. `cli`: resolve the ID from `CODER_TRACE_SESSION_ID` or generate; attach `client_session_id` baggage to every request via `wrapTransportWithSessionIDHeader`, gated on the `annotationClientSessionID` opt-in; add it to the command log context. 3. `tailnet` + `codersdk/workspacesdk`: add `client_session_id` to `TelemetryEvent`, an `Options.ClientSessionID`/`Conn.clientSessionID` set in `newTelemetryEvent`, a `DialAgentOptions.ClientSessionID` plumbed from the opted-in commands, and the coordinate-handshake baggage in `DialAgent`. </details> --- _This PR was created by Coder Agents on behalf of @aqandrew._
250 lines
5.4 KiB
Protocol Buffer
250 lines
5.4 KiB
Protocol Buffer
syntax = "proto3";
|
|
option go_package = "github.com/coder/coder/v2/tailnet/proto";
|
|
|
|
package coder.tailnet.v2;
|
|
|
|
import "google/protobuf/timestamp.proto"; // Importing for time.Time
|
|
import "google/protobuf/duration.proto"; // Importing for time.Duration
|
|
import "google/protobuf/wrappers.proto"; // Importing for nullable types
|
|
|
|
|
|
message DERPMap {
|
|
message HomeParams {
|
|
map<int64, double> region_score = 1;
|
|
}
|
|
HomeParams home_params = 1;
|
|
|
|
message Region {
|
|
int64 region_id = 1;
|
|
bool embedded_relay = 2;
|
|
string region_code = 3;
|
|
string region_name = 4;
|
|
bool avoid = 5;
|
|
|
|
message Node {
|
|
string name = 1;
|
|
int64 region_id = 2;
|
|
string host_name = 3;
|
|
string cert_name = 4;
|
|
string ipv4 = 5;
|
|
string ipv6 = 6;
|
|
int32 stun_port = 7;
|
|
bool stun_only = 8;
|
|
int32 derp_port = 9;
|
|
bool insecure_for_tests = 10;
|
|
bool force_http = 11;
|
|
string stun_test_ip = 12;
|
|
bool can_port_80 = 13;
|
|
}
|
|
repeated Node nodes = 6;
|
|
}
|
|
map<int64, Region> regions = 2;
|
|
}
|
|
|
|
message StreamDERPMapsRequest {}
|
|
|
|
// defined in tailnet/coordinator.go
|
|
message Node {
|
|
int64 id = 1;
|
|
google.protobuf.Timestamp as_of = 2;
|
|
bytes key = 3;
|
|
string disco = 4;
|
|
int32 preferred_derp = 5;
|
|
map<string, double> derp_latency = 6;
|
|
map<int32, string> derp_forced_websocket = 7;
|
|
repeated string addresses = 8;
|
|
repeated string allowed_ips = 9;
|
|
repeated string endpoints = 10;
|
|
}
|
|
|
|
message RefreshResumeTokenRequest {}
|
|
|
|
message RefreshResumeTokenResponse {
|
|
string token = 1;
|
|
google.protobuf.Duration refresh_in = 2;
|
|
google.protobuf.Timestamp expires_at = 3;
|
|
}
|
|
|
|
message CoordinateRequest {
|
|
message UpdateSelf {
|
|
Node node = 1;
|
|
}
|
|
UpdateSelf update_self = 1;
|
|
|
|
message Disconnect {}
|
|
Disconnect disconnect = 2;
|
|
|
|
message Tunnel {
|
|
bytes id = 1;
|
|
}
|
|
Tunnel add_tunnel = 3;
|
|
Tunnel remove_tunnel = 4;
|
|
|
|
// ReadyForHandskales are sent from destinations back to the source,
|
|
// acknowledging receipt of the source's node. If the source starts pinging
|
|
// before a ReadyForHandshake, the Wireguard handshake will likely be
|
|
// dropped.
|
|
message ReadyForHandshake {
|
|
bytes id = 1;
|
|
}
|
|
repeated ReadyForHandshake ready_for_handshake = 5;
|
|
}
|
|
|
|
message CoordinateResponse {
|
|
message PeerUpdate {
|
|
bytes id = 1;
|
|
Node node = 2;
|
|
|
|
enum Kind {
|
|
KIND_UNSPECIFIED = 0;
|
|
NODE = 1;
|
|
DISCONNECTED = 2;
|
|
LOST = 3;
|
|
READY_FOR_HANDSHAKE = 4;
|
|
}
|
|
Kind kind = 3;
|
|
|
|
string reason = 4;
|
|
}
|
|
repeated PeerUpdate peer_updates = 1;
|
|
string error = 2;
|
|
}
|
|
|
|
message IPFields {
|
|
int32 version = 1;
|
|
enum IPClass {
|
|
PUBLIC = 0;
|
|
PRIVATE = 1;
|
|
LINK_LOCAL = 2;
|
|
LOOPBACK = 3;
|
|
}
|
|
IPClass class = 2;
|
|
}
|
|
|
|
message Netcheck {
|
|
bool UDP = 1;
|
|
bool IPv6 = 2;
|
|
bool IPv4 = 3;
|
|
bool IPv6CanSend = 4;
|
|
bool IPv4CanSend = 5;
|
|
bool ICMPv4 = 6;
|
|
|
|
google.protobuf.BoolValue OSHasIPv6 = 7;
|
|
google.protobuf.BoolValue MappingVariesByDestIP = 8;
|
|
google.protobuf.BoolValue HairPinning = 9;
|
|
google.protobuf.BoolValue UPnP = 10;
|
|
google.protobuf.BoolValue PMP = 11;
|
|
google.protobuf.BoolValue PCP = 12;
|
|
|
|
int64 PreferredDERP = 13; // 0 for unknown
|
|
|
|
map<int64, google.protobuf.Duration> RegionV4Latency = 14;
|
|
map<int64, google.protobuf.Duration> RegionV6Latency = 15;
|
|
|
|
message NetcheckIP {
|
|
string hash = 1;
|
|
IPFields fields = 2;
|
|
}
|
|
NetcheckIP GlobalV4 = 17;
|
|
NetcheckIP GlobalV6 = 18;
|
|
}
|
|
|
|
message TelemetryEvent {
|
|
enum Status {
|
|
CONNECTED = 0;
|
|
DISCONNECTED = 1;
|
|
}
|
|
|
|
enum ClientType {
|
|
CLI = 0;
|
|
AGENT = 1;
|
|
CODERD = 2;
|
|
WSPROXY = 3;
|
|
}
|
|
|
|
message P2PEndpoint {
|
|
string hash = 1;
|
|
int32 port = 2;
|
|
IPFields fields = 3;
|
|
}
|
|
|
|
bytes id = 1;
|
|
google.protobuf.Timestamp time = 2;
|
|
string application = 3;
|
|
Status status = 4;
|
|
// Previously the unused DisconnectionReason,
|
|
// reserved to ensure it's not re-used.
|
|
reserved 5;
|
|
ClientType client_type = 6;
|
|
string client_version = 19;
|
|
uint64 node_id_self = 7;
|
|
uint64 node_id_remote = 8;
|
|
P2PEndpoint p2p_endpoint = 9;
|
|
int32 home_derp = 10;
|
|
DERPMap derp_map = 11;
|
|
Netcheck latest_netcheck = 12;
|
|
|
|
google.protobuf.Duration connection_age = 13;
|
|
google.protobuf.Duration connection_setup = 14;
|
|
google.protobuf.Duration p2p_setup = 15;
|
|
google.protobuf.Duration derp_latency = 16;
|
|
google.protobuf.Duration p2p_latency = 17;
|
|
google.protobuf.FloatValue throughput_mbits = 18;
|
|
|
|
// client_session_id correlates this event with the client's logs,
|
|
// requests, and other telemetry for a single session. It is a
|
|
// 32-character lowercase hexadecimal string, or empty when unset.
|
|
string client_session_id = 20;
|
|
}
|
|
|
|
message TelemetryRequest {
|
|
repeated TelemetryEvent events = 1;
|
|
}
|
|
|
|
message TelemetryResponse {}
|
|
|
|
message WorkspaceUpdatesRequest {
|
|
bytes workspace_owner_id = 1; // UUID
|
|
}
|
|
|
|
message WorkspaceUpdate {
|
|
repeated Workspace upserted_workspaces = 1;
|
|
repeated Agent upserted_agents = 2;
|
|
repeated Workspace deleted_workspaces = 3;
|
|
repeated Agent deleted_agents = 4;
|
|
}
|
|
|
|
message Workspace {
|
|
bytes id = 1; // UUID
|
|
string name = 2;
|
|
|
|
enum Status {
|
|
UNKNOWN = 0;
|
|
PENDING = 1;
|
|
STARTING = 2;
|
|
RUNNING = 3;
|
|
STOPPING = 4;
|
|
STOPPED = 5;
|
|
FAILED = 6;
|
|
CANCELING = 7;
|
|
CANCELED = 8;
|
|
DELETING = 9;
|
|
DELETED = 10;
|
|
}
|
|
Status status = 3;
|
|
}
|
|
|
|
message Agent {
|
|
bytes id = 1; // UUID
|
|
string name = 2;
|
|
bytes workspace_id = 3; // UUID
|
|
}
|
|
|
|
service Tailnet {
|
|
rpc PostTelemetry(TelemetryRequest) returns (TelemetryResponse);
|
|
rpc StreamDERPMaps(StreamDERPMapsRequest) returns (stream DERPMap);
|
|
rpc RefreshResumeToken(RefreshResumeTokenRequest) returns (RefreshResumeTokenResponse);
|
|
rpc Coordinate(stream CoordinateRequest) returns (stream CoordinateResponse);
|
|
rpc WorkspaceUpdates(WorkspaceUpdatesRequest) returns (stream WorkspaceUpdate);
|
|
}
|