Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions experimental/ssh/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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"
Expand Down Expand Up @@ -203,6 +205,52 @@ func Run(ctx context.Context, client *databricks.WorkspaceClient, opts ClientOpt
cancel()
}()

event := BuildTelemetryEvent(opts)

runErr := runConnect(ctx, client, opts, event)
if runErr != nil {
event.IsSuccess = false
} else {
event.IsSuccess = true
}

telemetry.Log(ctx, protos.DatabricksCliLog{
SshTunnelEvent: event,
})

return runErr
}

// BuildTelemetryEvent creates an SshTunnelEvent pre-populated with data from client options.
func BuildTelemetryEvent(opts ClientOptions) *protos.SshTunnelEvent {
event := &protos.SshTunnelEvent{
AcceleratorType: opts.Accelerator,
IdeType: opts.IDE,
AutoStartCluster: opts.AutoStartCluster,
}

if opts.IsServerlessMode() {
event.ComputeType = protos.SshTunnelComputeTypeServerless
} else {
event.ComputeType = protos.SshTunnelComputeTypeDedicated
}

switch {
case opts.ProxyMode:
event.ClientMode = protos.SshTunnelClientModeProxy
case opts.IDE != "":
event.ClientMode = protos.SshTunnelClientModeIDE
default:
event.ClientMode = protos.SshTunnelClientModeSSH
}

// If metadata is provided, the server is already running — this is a reconnect from ProxyCommand.
event.IsReconnect = opts.ServerMetadata != ""

return event
}

func runConnect(ctx context.Context, client *databricks.WorkspaceClient, opts ClientOptions, event *protos.SshTunnelEvent) error {
// For serverless without explicit --name: auto-generate or reconnect to existing session.
if opts.IsServerlessMode() && opts.ConnectionName == "" && !opts.ProxyMode {
err := resolveServerlessSession(ctx, client, &opts)
Expand Down Expand Up @@ -296,10 +344,13 @@ func Run(ctx context.Context, client *databricks.WorkspaceClient, opts ClientOpt
return fmt.Errorf("failed to upload ssh-tunnel binaries: %w", err)
}
sp.Close()

serverStartTime := time.Now()
userName, serverPort, clusterID, err = ensureSSHServerIsRunning(ctx, client, version, secretScopeName, opts)
if err != nil {
return fmt.Errorf("failed to ensure that ssh server is running: %w", err)
}
event.ServerStartTimeMs = time.Since(serverStartTime).Milliseconds()
} else {
// Metadata format: "<user_name>,<port>,<cluster_id>"
metadata := strings.Split(opts.ServerMetadata, ",")
Expand Down
69 changes: 69 additions & 0 deletions experimental/ssh/internal/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/databricks/cli/experimental/ssh/internal/client"
"github.com/databricks/cli/libs/telemetry/protos"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -169,3 +170,71 @@ func TestToProxyCommand(t *testing.T) {
})
}
}

func TestBuildTelemetryEvent(t *testing.T) {
tests := []struct {
name string
opts client.ClientOptions
want *protos.SshTunnelEvent
}{
{
name: "dedicated cluster with SSH client",
opts: client.ClientOptions{
ClusterID: "abc-123",
AutoStartCluster: true,
},
want: &protos.SshTunnelEvent{
ComputeType: protos.SshTunnelComputeTypeDedicated,
ClientMode: protos.SshTunnelClientModeSSH,
AutoStartCluster: true,
},
},
{
name: "serverless with IDE",
opts: client.ClientOptions{
ConnectionName: "my-conn",
Accelerator: "GPU_1xA10",
IDE: "vscode",
},
want: &protos.SshTunnelEvent{
ComputeType: protos.SshTunnelComputeTypeServerless,
ClientMode: protos.SshTunnelClientModeIDE,
AcceleratorType: "GPU_1xA10",
IdeType: "vscode",
},
},
{
name: "proxy mode with metadata (reconnect)",
opts: client.ClientOptions{
ClusterID: "abc-123",
ProxyMode: true,
ServerMetadata: "user,2222,abc-123",
},
want: &protos.SshTunnelEvent{
ComputeType: protos.SshTunnelComputeTypeDedicated,
ClientMode: protos.SshTunnelClientModeProxy,
IsReconnect: true,
},
},
{
name: "serverless proxy mode",
opts: client.ClientOptions{
ConnectionName: "my-conn",
Accelerator: "GPU_8xH100",
ProxyMode: true,
},
want: &protos.SshTunnelEvent{
ComputeType: protos.SshTunnelComputeTypeServerless,
ClientMode: protos.SshTunnelClientModeProxy,
AcceleratorType: "GPU_8xH100",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := client.BuildTelemetryEvent(tt.opts)
assert.Equal(t, tt.want, got)
})
}
}
1 change: 1 addition & 0 deletions libs/telemetry/protos/frontend_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
48 changes: 48 additions & 0 deletions libs/telemetry/protos/ssh_tunnel_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package protos

// SshTunnelComputeType represents the type of compute used for SSH tunnel.
type SshTunnelComputeType string

const (
SshTunnelComputeTypeUnspecified SshTunnelComputeType = "TYPE_UNSPECIFIED"
SshTunnelComputeTypeDedicated SshTunnelComputeType = "DEDICATED"
SshTunnelComputeTypeServerless SshTunnelComputeType = "SERVERLESS"
)

// SshTunnelClientMode represents how the SSH tunnel client is used.
type SshTunnelClientMode string

const (
SshTunnelClientModeUnspecified SshTunnelClientMode = "TYPE_UNSPECIFIED"
SshTunnelClientModeSSH SshTunnelClientMode = "SSH_CLIENT"
SshTunnelClientModeProxy SshTunnelClientMode = "PROXY"
SshTunnelClientModeIDE SshTunnelClientMode = "IDE"
)

// SshTunnelEvent tracks SSH tunnel connection lifecycle and usage.
type SshTunnelEvent struct {
// Type of compute: dedicated cluster or serverless.
ComputeType SshTunnelComputeType `json:"compute_type,omitempty"`

// GPU accelerator type for serverless compute (e.g., "GPU_1xA10", "GPU_8xH100").
AcceleratorType string `json:"accelerator_type,omitempty"`

// IDE used for the connection (e.g., "vscode", "cursor"), empty if none.
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 (including job submission
// and waiting for the server to become ready). Zero if server was already running.
ServerStartTimeMs int64 `json:"server_start_time_ms"`

// Flag to indicate if the connection was successful
IsSuccess bool `json:"is_success,omitempty"`
}
Loading