Skip to content

Commit b1542c8

Browse files
committed
SNOW-2306184: config refactor - telemetry
1 parent 9a34293 commit b1542c8

File tree

7 files changed

+521
-42
lines changed

7 files changed

+521
-42
lines changed

src/snowflake/cli/_app/telemetry.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ class CLITelemetryField(Enum):
6161
COMMAND_CI_ENVIRONMENT = "command_ci_environment"
6262
# Configuration
6363
CONFIG_FEATURE_FLAGS = "config_feature_flags"
64+
CONFIG_PROVIDER_TYPE = "config_provider_type"
65+
CONFIG_SOURCES_USED = "config_sources_used"
66+
CONFIG_SOURCE_WINS = "config_source_wins"
67+
CONFIG_TOTAL_KEYS_RESOLVED = "config_total_keys_resolved"
68+
CONFIG_KEYS_WITH_OVERRIDES = "config_keys_with_overrides"
6469
# Metrics
6570
COUNTERS = "counters"
6671
SPANS = "spans"
@@ -219,6 +224,55 @@ def python_version() -> str:
219224
return f"{py_ver.major}.{py_ver.minor}.{py_ver.micro}"
220225

221226

227+
def _get_config_telemetry() -> TelemetryDict:
228+
"""Get configuration resolution telemetry data."""
229+
try:
230+
from snowflake.cli.api.config_ng.telemetry_integration import (
231+
get_config_telemetry_payload,
232+
)
233+
from snowflake.cli.api.config_provider import (
234+
AlternativeConfigProvider,
235+
get_config_provider_singleton,
236+
)
237+
238+
provider = get_config_provider_singleton()
239+
240+
# Identify which config provider is being used
241+
provider_type = (
242+
"ng" if isinstance(provider, AlternativeConfigProvider) else "legacy"
243+
)
244+
245+
result: TelemetryDict = {CLITelemetryField.CONFIG_PROVIDER_TYPE: provider_type}
246+
247+
# Get detailed telemetry if using ng config
248+
if isinstance(provider, AlternativeConfigProvider):
249+
provider._ensure_initialized() # noqa: SLF001
250+
payload = get_config_telemetry_payload(provider._resolver) # noqa: SLF001
251+
252+
# Map payload keys to telemetry fields
253+
if payload:
254+
if "config_sources_used" in payload:
255+
result[CLITelemetryField.CONFIG_SOURCES_USED] = payload[
256+
"config_sources_used"
257+
]
258+
if "config_source_wins" in payload:
259+
result[CLITelemetryField.CONFIG_SOURCE_WINS] = payload[
260+
"config_source_wins"
261+
]
262+
if "config_total_keys_resolved" in payload:
263+
result[CLITelemetryField.CONFIG_TOTAL_KEYS_RESOLVED] = payload[
264+
"config_total_keys_resolved"
265+
]
266+
if "config_keys_with_overrides" in payload:
267+
result[CLITelemetryField.CONFIG_KEYS_WITH_OVERRIDES] = payload[
268+
"config_keys_with_overrides"
269+
]
270+
271+
return result
272+
except Exception:
273+
return {}
274+
275+
222276
class CLITelemetryClient:
223277
@property
224278
def _ctx(self) -> _CliGlobalContextAccess:
@@ -239,6 +293,7 @@ def generate_telemetry_data_dict(
239293
k: str(v) for k, v in get_feature_flags_section().items()
240294
},
241295
**_find_command_info(),
296+
**_get_config_telemetry(),
242297
**telemetry_payload,
243298
}
244299
# To map Enum to string, so we don't have to use .value every time

src/snowflake/cli/api/config_ng/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272
SnowSQLSection,
7373
get_merged_variables,
7474
)
75+
from snowflake.cli.api.config_ng.telemetry_integration import (
76+
get_config_telemetry_payload,
77+
record_config_source_usage,
78+
)
7579

7680
__all__ = [
7781
"check_value_source",
@@ -91,12 +95,14 @@
9195
"extract_root_level_connection_params",
9296
"FILE_SOURCE_NAMES",
9397
"format_summary_for_display",
98+
"get_config_telemetry_payload",
9499
"get_merged_variables",
95100
"get_resolution_summary",
96101
"get_resolver",
97102
"INTERNAL_CLI_PARAMETERS",
98103
"is_resolution_logging_available",
99104
"merge_params_into_connections",
105+
"record_config_source_usage",
100106
"ResolutionEntry",
101107
"ResolutionHistory",
102108
"ResolutionHistoryTracker",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Copyright (c) 2024 Snowflake Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Telemetry integration for config_ng system.
17+
18+
This module provides functions to track configuration source usage
19+
and integrate with the CLI's telemetry system.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
from typing import TYPE_CHECKING, Any, Dict, Optional
25+
26+
if TYPE_CHECKING:
27+
from snowflake.cli.api.config_ng.resolver import ConfigurationResolver
28+
29+
30+
# Map source names to counter field names
31+
SOURCE_TO_COUNTER = {
32+
"snowsql_config": "config_source_snowsql",
33+
"cli_config_toml": "config_source_cli_toml",
34+
"connections_toml": "config_source_connections_toml",
35+
"snowsql_env": "config_source_snowsql_env",
36+
"connection_specific_env": "config_source_connection_env",
37+
"cli_env": "config_source_cli_env",
38+
"cli_arguments": "config_source_cli_args",
39+
}
40+
41+
42+
def record_config_source_usage(resolver: ConfigurationResolver) -> None:
43+
"""
44+
Record configuration source usage to CLI metrics.
45+
46+
This should be called after configuration resolution completes.
47+
Sets counters to 1 for sources that provided winning values, 0 otherwise.
48+
49+
Args:
50+
resolver: The ConfigurationResolver instance
51+
"""
52+
try:
53+
from snowflake.cli.api.cli_global_context import get_cli_context
54+
from snowflake.cli.api.metrics import CLICounterField
55+
56+
cli_context = get_cli_context()
57+
summary = resolver.get_tracker().get_summary()
58+
59+
# Track which sources won (provided final values)
60+
source_wins = summary.get("source_wins", {})
61+
62+
# Set counters for each source
63+
for source_name, counter_name in SOURCE_TO_COUNTER.items():
64+
# Set to 1 if this source provided any winning values, 0 otherwise
65+
value = 1 if source_wins.get(source_name, 0) > 0 else 0
66+
counter_field = getattr(CLICounterField, counter_name.upper(), None)
67+
if counter_field:
68+
cli_context.metrics.set_counter(counter_field, value)
69+
70+
except Exception:
71+
# Don't break execution if telemetry fails
72+
pass
73+
74+
75+
def get_config_telemetry_payload(
76+
resolver: Optional[ConfigurationResolver],
77+
) -> Dict[str, Any]:
78+
"""
79+
Get configuration telemetry payload for inclusion in command telemetry.
80+
81+
Args:
82+
resolver: Optional ConfigurationResolver instance
83+
84+
Returns:
85+
Dictionary with config telemetry data
86+
"""
87+
if resolver is None:
88+
return {}
89+
90+
try:
91+
summary = resolver.get_tracker().get_summary()
92+
93+
return {
94+
"config_sources_used": list(summary.get("source_usage", {}).keys()),
95+
"config_source_wins": summary.get("source_wins", {}),
96+
"config_total_keys_resolved": summary.get("total_keys_resolved", 0),
97+
"config_keys_with_overrides": summary.get("keys_with_overrides", 0),
98+
}
99+
except Exception:
100+
return {}

src/snowflake/cli/api/config_provider.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,26 @@ def _ensure_initialized(self) -> None:
293293
if not self._config_cache:
294294
self._config_cache = self._resolver.resolve()
295295

296+
# Record telemetry about config sources used
297+
self._record_config_telemetry()
298+
296299
self._initialized = True
297300

301+
def _record_config_telemetry(self) -> None:
302+
"""Record configuration source usage to telemetry system."""
303+
if self._resolver is None:
304+
return
305+
306+
try:
307+
from snowflake.cli.api.config_ng.telemetry_integration import (
308+
record_config_source_usage,
309+
)
310+
311+
record_config_source_usage(self._resolver)
312+
except Exception:
313+
# Don't break initialization if telemetry fails
314+
pass
315+
298316
def read_config(self) -> None:
299317
"""
300318
Load configuration from all sources.

src/snowflake/cli/api/metrics.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@ class CLICounterField:
7676
EVENT_SHARING_ERROR = (
7777
f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.event_sharing_error"
7878
)
79+
# Config source usage tracking
80+
CONFIG_SOURCE_SNOWSQL = (
81+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.config_source_snowsql"
82+
)
83+
CONFIG_SOURCE_CLI_TOML = (
84+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.config_source_cli_toml"
85+
)
86+
CONFIG_SOURCE_CONNECTIONS_TOML = (
87+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.config_source_connections_toml"
88+
)
89+
CONFIG_SOURCE_SNOWSQL_ENV = (
90+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.config_source_snowsql_env"
91+
)
92+
CONFIG_SOURCE_CONNECTION_ENV = (
93+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.config_source_connection_env"
94+
)
95+
CONFIG_SOURCE_CLI_ENV = (
96+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.config_source_cli_env"
97+
)
98+
CONFIG_SOURCE_CLI_ARGS = (
99+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.config_source_cli_args"
100+
)
79101

80102

81103
@dataclass

0 commit comments

Comments
 (0)