diff --git a/experimental/ssh/internal/client/client.go b/experimental/ssh/internal/client/client.go index 1df157a7c6..e20ce8ac62 100644 --- a/experimental/ssh/internal/client/client.go +++ b/experimental/ssh/internal/client/client.go @@ -26,6 +26,8 @@ import ( "github.com/databricks/cli/internal/build" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/telemetry" + "github.com/databricks/cli/libs/telemetry/protos" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/retries" "github.com/databricks/databricks-sdk-go/service/compute" @@ -283,6 +285,9 @@ func Run(ctx context.Context, client *databricks.WorkspaceClient, opts ClientOpt version := build.GetInfo().Version + isReconnect := opts.ServerMetadata != "" + var serverStartTimeMs int64 + if opts.ServerMetadata == "" { cmdio.LogString(ctx, "Uploading binaries...") sp := cmdio.NewSpinner(ctx, cmdio.WithElapsedTime()) @@ -292,10 +297,13 @@ func Run(ctx context.Context, client *databricks.WorkspaceClient, opts ClientOpt if err != nil { return fmt.Errorf("failed to upload ssh-tunnel binaries: %w", err) } + serverStartTime := time.Now() userName, serverPort, clusterID, err = ensureSSHServerIsRunning(ctx, client, version, secretScopeName, opts) if err != nil { + logSshTunnelEvent(ctx, opts, false, isReconnect, 0) return fmt.Errorf("failed to ensure that ssh server is running: %w", err) } + serverStartTimeMs = time.Since(serverStartTime).Milliseconds() } else { // Metadata format: ",," metadata := strings.Split(opts.ServerMetadata, ",") @@ -332,6 +340,8 @@ func Run(ctx context.Context, client *databricks.WorkspaceClient, opts ClientOpt cmdio.LogString(ctx, "Connected!") } + logSshTunnelEvent(ctx, opts, true, isReconnect, serverStartTimeMs) + if opts.ProxyMode { return runSSHProxy(ctx, client, serverPort, clusterID, opts) } else if opts.IDE != "" { @@ -704,3 +714,33 @@ func ensureSSHServerIsRunning(ctx context.Context, client *databricks.WorkspaceC return userName, serverPort, effectiveClusterID, nil } + +func logSshTunnelEvent(ctx context.Context, opts ClientOptions, isSuccess, isReconnect bool, serverStartTimeMs int64) { + computeType := protos.SshTunnelComputeTypeDedicated + if opts.IsServerlessMode() { + computeType = protos.SshTunnelComputeTypeServerless + } + + var clientMode protos.SshTunnelClientMode + switch { + case opts.ProxyMode: + clientMode = protos.SshTunnelClientModeProxy + case opts.IDE != "": + clientMode = protos.SshTunnelClientModeIDE + default: + clientMode = protos.SshTunnelClientModeSSH + } + + telemetry.Log(ctx, protos.DatabricksCliLog{ + SshTunnelEvent: &protos.SshTunnelEvent{ + ComputeType: computeType, + AcceleratorType: opts.Accelerator, + IdeType: opts.IDE, + ClientMode: clientMode, + IsReconnect: isReconnect, + AutoStartCluster: opts.AutoStartCluster, + ServerStartTimeMs: serverStartTimeMs, + IsSuccess: isSuccess, + }, + }) +} diff --git a/libs/telemetry/protos/frontend_log.go b/libs/telemetry/protos/frontend_log.go index 7e6ab1012b..816297a8ee 100644 --- a/libs/telemetry/protos/frontend_log.go +++ b/libs/telemetry/protos/frontend_log.go @@ -19,4 +19,5 @@ type DatabricksCliLog struct { CliTestEvent *CliTestEvent `json:"cli_test_event,omitempty"` BundleInitEvent *BundleInitEvent `json:"bundle_init_event,omitempty"` BundleDeployEvent *BundleDeployEvent `json:"bundle_deploy_event,omitempty"` + SshTunnelEvent *SshTunnelEvent `json:"ssh_tunnel_event,omitempty"` } diff --git a/libs/telemetry/protos/ssh_tunnel.go b/libs/telemetry/protos/ssh_tunnel.go new file mode 100644 index 0000000000..42be8233b7 --- /dev/null +++ b/libs/telemetry/protos/ssh_tunnel.go @@ -0,0 +1,47 @@ +package protos + +type SshTunnelComputeType string + +const ( + SshTunnelComputeTypeUnspecified SshTunnelComputeType = "TYPE_UNSPECIFIED" + SshTunnelComputeTypeDedicated SshTunnelComputeType = "DEDICATED" + SshTunnelComputeTypeServerless SshTunnelComputeType = "SERVERLESS" +) + +type SshTunnelClientMode string + +const ( + SshTunnelClientModeUnspecified SshTunnelClientMode = "TYPE_UNSPECIFIED" + SshTunnelClientModeSSH SshTunnelClientMode = "SSH_CLIENT" + SshTunnelClientModeProxy SshTunnelClientMode = "PROXY" + SshTunnelClientModeIDE SshTunnelClientMode = "IDE" +) + +// SshTunnelEvent is emitted when a user establishes an SSH tunnel connection +// via the Databricks CLI. +type SshTunnelEvent struct { + // Type of compute: dedicated cluster or serverless. + ComputeType SshTunnelComputeType `json:"compute_type,omitempty"` + + // GPU accelerator type for serverless compute. + AcceleratorType string `json:"accelerator_type,omitempty"` + + // IDE that initiated the connection (e.g., "vscode", "cursor"). + IdeType string `json:"ide_type,omitempty"` + + // How the client is used: SSH client, proxy mode, or IDE mode. + ClientMode SshTunnelClientMode `json:"client_mode,omitempty"` + + // Whether this is a reconnection to an existing session. + IsReconnect bool `json:"is_reconnect,omitempty"` + + // Whether the cluster was auto-started by the CLI. + AutoStartCluster bool `json:"auto_start_cluster,omitempty"` + + // Time in milliseconds spent starting the SSH server. + // Zero if server was already running. + ServerStartTimeMs int64 `json:"server_start_time_ms"` + + // Whether the connection was successful. + IsSuccess bool `json:"is_success,omitempty"` +}