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
16 changes: 16 additions & 0 deletions acceptance/bundle/python/telemetry/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
bundle:
name: my_project

sync: {paths: []} # don't need to copy files

# Single line so the script can switch it to 'experimental/python'.
python: {resources: ["resources:load_resources"], mutators: ["mutators:update_job"]}

resources:
jobs:
yaml_job:
name: "YAML Job"
schemas:
yaml_schema:
catalog_name: main
name: yaml_schema
11 changes: 11 additions & 0 deletions acceptance/bundle/python/telemetry/mutators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dataclasses import replace

from databricks.bundles.core import job_mutator
from databricks.bundles.jobs import Job


@job_mutator
def update_job(job: Job) -> Job:
assert isinstance(job.name, str)

return replace(job, name=f"{job.name} (updated)")
3 changes: 3 additions & 0 deletions acceptance/bundle/python/telemetry/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Cloud = false
EnvMatrix.DMS = [""]
EnvMatrix.PYDAB_VERSION = ["current"]
70 changes: 70 additions & 0 deletions acceptance/bundle/python/telemetry/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

=== Deploy with 'python'
>>> uv run [UV_ARGS] -q [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/my_project/default/files...
Created jobs.python_job
Created jobs.yaml_job
Created pipelines.python_pipeline
Created schemas.yaml_schema
Files: 0 uploaded, 0 deleted
Resources: 4 created, 0 changed, 0 deleted, 0 unchanged
{
"pydabs": {
"resource_loaders_count": 1,
"resource_mutators_count": 1,
"added_resources_count": 2,
"updated_resources_count": 2,
"resources": [
{
"resource_type": "jobs",
"added_count": 1,
"updated_count": 2
},
{
"resource_type": "pipelines",
"added_count": 1,
"updated_count": 0
}
],
"config_section": "PYTHON"
},
"all_resource_types": [
"jobs",
"pipelines",
"schemas"
]
}

=== Deploy with 'experimental/python'
>>> update_file.py databricks.yml python: {resources: ["resources:load_resources"], mutators: ["mutators:update_job"]} experimental: {python: {resources: ["resources:load_resources"], mutators: ["mutators:update_job"]}}

>>> uv run [UV_ARGS] -q [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/my_project/default/files...
Files: 0 uploaded, 0 deleted
Resources: 0 created, 0 changed, 0 deleted, 4 unchanged
{
"pydabs": {
"resource_loaders_count": 1,
"resource_mutators_count": 1,
"added_resources_count": 2,
"updated_resources_count": 2,
"resources": [
{
"resource_type": "jobs",
"added_count": 1,
"updated_count": 2
},
{
"resource_type": "pipelines",
"added_count": 1,
"updated_count": 0
}
],
"config_section": "EXPERIMENTAL_PYTHON"
},
"all_resource_types": [
"jobs",
"pipelines",
"schemas"
]
}
9 changes: 9 additions & 0 deletions acceptance/bundle/python/telemetry/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from databricks.bundles.core import Resources


def load_resources() -> Resources:
resources = Resources()
resources.add_job("python_job", {"name": "Python Job"})
resources.add_pipeline("python_pipeline", {"name": "Python Pipeline"})

return resources
20 changes: 20 additions & 0 deletions acceptance/bundle/python/telemetry/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# pydabs covers only resources defined or modified in Python (jobs, pipelines),
# while resources_metadata covers every resource in the bundle (including schemas).
print_pydabs_telemetry() {
jq 'select(has("path") and .path == "/telemetry-ext") | .body.protoLogs[] | fromjson | .entry.databricks_cli_log.bundle_deploy_event | {
pydabs,
all_resource_types: [.resources_metadata.resources[].resource_type]
}' "$OUT_REQUESTS"
rm "$OUT_REQUESTS"
}

title "Deploy with 'python'"
trace uv run $UV_ARGS -q $CLI bundle deploy
print_pydabs_telemetry

title "Deploy with 'experimental/python'"
trace update_file.py databricks.yml 'python: {resources: ["resources:load_resources"], mutators: ["mutators:update_job"]}' 'experimental: {python: {resources: ["resources:load_resources"], mutators: ["mutators:update_job"]}}'
trace uv run $UV_ARGS -q $CLI bundle deploy
print_pydabs_telemetry

rm -fr .databricks __pycache__
5 changes: 5 additions & 0 deletions acceptance/bundle/python/telemetry/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RecordRequests = true

EnvMatrix.DMS = [""]
# Telemetry doesn't depend on the databricks-bundles version.
EnvMatrix.PYDAB_VERSION = ["current"]
21 changes: 13 additions & 8 deletions bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,19 @@ const terraformStateFilename = "terraform.tfstate"
// This struct is used as a communication channel to collect metrics
// from all over the bundle codebase to finally be emitted as telemetry.
type Metrics struct {
ConfigurationFileCount int64
TargetCount int64
DeploymentId uuid.UUID
BoolValues []protos.BoolMapEntry
PythonAddedResourcesCount int64
PythonUpdatedResourcesCount int64
ExecutionTimes []protos.IntMapEntry
LocalCacheMeasurementsMs []protos.IntMapEntry // Local cache measurements stored as milliseconds
ConfigurationFileCount int64
TargetCount int64
DeploymentId uuid.UUID
BoolValues []protos.BoolMapEntry
ExecutionTimes []protos.IntMapEntry
LocalCacheMeasurementsMs []protos.IntMapEntry // Local cache measurements stored as milliseconds

// Number of resources added and updated by the Python mutator, keyed by resource type.
PythonAddedResources map[string]int64
Comment thread
Sankalp-Mittal marked this conversation as resolved.
PythonUpdatedResources map[string]int64

// Configuration section Python support is declared in.
PythonConfigSection protos.PydabsConfigSection

// StateEngine is the engine that ran (or would have run) the deploy. Set to the
// requested engine as soon as it is resolved, then refined to the state's engine
Expand Down
39 changes: 37 additions & 2 deletions bundle/config/mutator/python/python_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/databricks/cli/libs/dyn/convert"
"github.com/databricks/cli/libs/dyn/yamlloader"
"github.com/databricks/cli/libs/process"
"github.com/databricks/cli/libs/telemetry/protos"
)

type phase string
Expand Down Expand Up @@ -107,6 +108,17 @@ type runPythonMutatorOpts struct {
authEnv map[string]string
}

// addResourceCounts adds the number of resources in set to counts, per resource type.
func addResourceCounts(counts map[string]int64, set resourcemutator.ResourceKeySet) map[string]int64 {
for resourceType, names := range set {
if counts == nil {
counts = make(map[string]int64)
}
counts[resourceType] += int64(len(names))
}
return counts
}

// getOpts adapts deprecated PyDABs and upcoming Python configuration
// into a common structure.
func getOpts(b *bundle.Bundle, phase phase) (opts, error) {
Expand Down Expand Up @@ -151,6 +163,23 @@ func getOpts(b *bundle.Bundle, phase phase) (opts, error) {
}
}

// pythonConfigSection reports which section the user declared Python support in.
func pythonConfigSection(b *bundle.Bundle) protos.PydabsConfigSection {
pythonEnabled := !reflect.DeepEqual(b.Config.Python, config.Python{})
experimentalPythonEnabled := b.Config.Experimental != nil && !reflect.DeepEqual(b.Config.Experimental.Python, config.Python{})

switch {
case pythonEnabled && experimentalPythonEnabled:
return protos.PydabsConfigSectionBoth
case pythonEnabled:
return protos.PydabsConfigSectionPython
case experimentalPythonEnabled:
return protos.PydabsConfigSectionExperimentalPython
default:
return protos.PydabsConfigSectionUnspecified
}
}

// applyBackwardsCompatibilityFixes applies fixes to bundle configuration
// so that older version of databricks-bundles Python library continue to see
// 'experimental.python' section even if bundle configuration uses 'python' section.
Expand Down Expand Up @@ -199,6 +228,12 @@ func applyBackwardsCompatibilityFixes(b *bundle.Bundle) error {
}

func (m *pythonMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// Record before applyBackwardsCompatibilityFixes copies 'python' into 'experimental/python',
// which makes the sections indistinguishable. The load_resources phase runs first.
if m.phase == PythonMutatorPhaseLoadResources {
b.Metrics.PythonConfigSection = pythonConfigSection(b)
}

err := applyBackwardsCompatibilityFixes(b)
if err != nil {
return diag.FromErr(fmt.Errorf("failed to apply backwards compatibility fixes: %w", err))
Expand Down Expand Up @@ -290,8 +325,8 @@ func (m *pythonMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagno
})

// we can precisely track resources that are added/updated, so sum doesn't double-count
b.Metrics.PythonUpdatedResourcesCount += int64(result.UpdatedResources.Size())
b.Metrics.PythonAddedResourcesCount += int64(result.AddedResources.Size())
b.Metrics.PythonUpdatedResources = addResourceCounts(b.Metrics.PythonUpdatedResources, result.UpdatedResources)
b.Metrics.PythonAddedResources = addResourceCounts(b.Metrics.PythonAddedResources, result.AddedResources)

if errors.Is(err, mutateDiagsHasError) {
if !mutateDiags.HasError() {
Expand Down
23 changes: 18 additions & 5 deletions bundle/config/mutator/python/python_mutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/databricks/cli/internal/testutil"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/process"
"github.com/databricks/cli/libs/telemetry/protos"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -157,9 +158,6 @@ workspace: { current_user: { userName: test }}`)
})
assert.NoError(t, err)

assert.Equal(t, int64(2), b.Metrics.PythonAddedResourcesCount)
assert.Equal(t, int64(0), b.Metrics.PythonUpdatedResourcesCount)

assert.Len(t, diags, 1)
assert.Equal(t, "job doesn't have any tasks", diags[0].Summary)
assert.Equal(t, []diag.Location{
Expand Down Expand Up @@ -237,9 +235,24 @@ resources:
return v, nil
})
assert.NoError(t, err)
}

assert.Equal(t, int64(0), b.Metrics.PythonAddedResourcesCount)
assert.Equal(t, int64(1), b.Metrics.PythonUpdatedResourcesCount)
func TestPythonConfigSection(t *testing.T) {
tests := []struct {
name string
yaml string
expected protos.PydabsConfigSection
}{
{"none", `bundle: {name: test}`, protos.PydabsConfigSectionUnspecified},
{"both", `
python: {resources: ["resources:load"]}
experimental: {python: {resources: ["resources:load"]}}`, protos.PydabsConfigSectionBoth},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, pythonConfigSection(loadYaml("databricks.yml", tc.yaml)))
})
}
}

func TestPythonMutator_badOutput(t *testing.T) {
Expand Down
49 changes: 42 additions & 7 deletions bundle/phases/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package phases
import (
"cmp"
"context"
"maps"
"math"
"reflect"
"slices"
Expand Down Expand Up @@ -195,6 +196,39 @@ func aiRuntimeTaskMetrics(jobs map[string]*resources.Job) (present, scheduled, m
return present, scheduled, multitask
}

// pydabsMetrics reports usage of Python support. Loaders and mutators are read from
// 'experimental/python' only: the Python mutator copies 'python' there.
func pydabsMetrics(b *bundle.Bundle) *protos.BundleDeployPydabs {
experimentalConfig := b.Config.Experimental
if experimentalConfig == nil {
experimentalConfig = &config.Experimental{}
}

out := &protos.BundleDeployPydabs{
ResourceLoadersCount: int64(len(experimentalConfig.Python.Resources)),
ResourceMutatorsCount: int64(len(experimentalConfig.Python.Mutators)),
ConfigSection: b.Metrics.PythonConfigSection,
}

byType := make(map[string]*protos.PydabsResourceTypeCount)
for t, n := range b.Metrics.PythonAddedResources {
byType[t] = &protos.PydabsResourceTypeCount{ResourceType: t, AddedCount: n}
out.AddedResourcesCount += n
}
for t, n := range b.Metrics.PythonUpdatedResources {
if byType[t] == nil {
byType[t] = &protos.PydabsResourceTypeCount{ResourceType: t}
}
byType[t].UpdatedCount = n
out.UpdatedResourcesCount += n
}

for _, t := range slices.Sorted(maps.Keys(byType)) {
out.Resources = append(out.Resources, *byType[t])
}
return out
}

// LogDeployTelemetry logs a telemetry event for a bundle deploy command.
func LogDeployTelemetry(ctx context.Context, b *bundle.Bundle, errMsg string) {
errMsg = telemetry.ScrubErrorMessage(errMsg)
Expand Down Expand Up @@ -349,9 +383,12 @@ func LogDeployTelemetry(ctx context.Context, b *bundle.Bundle, errMsg string) {
mode = protos.BundleModeProduction
}

experimentalConfig := b.Config.Experimental
if experimentalConfig == nil {
experimentalConfig = &config.Experimental{}
pydabs := pydabsMetrics(b)

// Omit the pydabs section for bundles that don't use Python support.
var pydabsSection *protos.BundleDeployPydabs
if pydabs.ResourceLoadersCount > 0 || pydabs.ResourceMutatorsCount > 0 {
pydabsSection = pydabs
}

uploadFileSizers := make([]sizer, len(b.Files))
Expand Down Expand Up @@ -386,6 +423,8 @@ func LogDeployTelemetry(ctx context.Context, b *bundle.Bundle, errMsg string) {

ResourcesMetadata: collectResourcesMetadata(ctx, b),

Pydabs: pydabsSection,

Experimental: &protos.BundleDeployExperimental{
BundleMode: mode,
ConfigurationFileCount: b.Metrics.ConfigurationFileCount,
Expand All @@ -395,10 +434,6 @@ func LogDeployTelemetry(ctx context.Context, b *bundle.Bundle, errMsg string) {
WorkspaceArtifactPathType: artifactPathType,
BoolValues: b.Metrics.BoolValues,
LocalCacheMeasurementsMs: b.Metrics.LocalCacheMeasurementsMs,
PythonAddedResourcesCount: b.Metrics.PythonAddedResourcesCount,
PythonUpdatedResourcesCount: b.Metrics.PythonUpdatedResourcesCount,
PythonResourceLoadersCount: int64(len(experimentalConfig.Python.Resources)),
PythonResourceMutatorsCount: int64(len(experimentalConfig.Python.Mutators)),
VariableCount: int64(variableCount),
ComplexVariableCount: complexVariableCount,
LookupVariableCount: lookupVariableCount,
Expand Down
Loading
Loading