Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement metrics counter feature tracking for NADE #1597

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7f2618a
wip: implement metrics and implementation, tests failing
sfc-gh-mchok Sep 18, 2024
bd93cc3
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 18, 2024
4470cd6
fix missing counter field causing test fails
sfc-gh-mchok Sep 18, 2024
b6101ab
add pdf template instrumentation
sfc-gh-mchok Sep 19, 2024
945bc3c
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 19, 2024
5ae5bc7
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 19, 2024
fe6926d
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 19, 2024
61dd177
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 19, 2024
9145b67
remove validate option callback for failing tests
sfc-gh-mchok Sep 19, 2024
9dccb93
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 19, 2024
949edf0
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 19, 2024
e8a3956
remove duplicate tracked counters, fix bug with carryover metrics bet…
sfc-gh-mchok Sep 19, 2024
1316163
add integration tests for feature tracking metrics
sfc-gh-mchok Sep 19, 2024
95ae58c
fix improper typing for None
sfc-gh-mchok Sep 19, 2024
d24aedb
implement metrics custom equality comparison for failing test
sfc-gh-mchok Sep 19, 2024
674decd
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 20, 2024
c41044e
review fixes
sfc-gh-mchok Sep 20, 2024
f2def23
extract metrics update from render_definition_template
sfc-gh-mchok Sep 20, 2024
ef26a09
fix key being used for metrics
sfc-gh-mchok Sep 20, 2024
a17a0d4
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 20, 2024
6cc8c5f
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 20, 2024
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
14 changes: 14 additions & 0 deletions src/snowflake/cli/_app/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class CLITelemetryField(Enum):
COMMAND_EXECUTION_TIME = "command_execution_time"
# Configuration
CONFIG_FEATURE_FLAGS = "config_feature_flags"
# Metrics
COUNTERS = "counters"
# Information
EVENT = "event"
ERROR_MSG = "error_msg"
Expand All @@ -72,6 +74,16 @@ class TelemetryEvent(Enum):
TelemetryDict = Dict[Union[CLITelemetryField, TelemetryField], Any]


def _get_command_metrics() -> TelemetryDict:
cli_context = get_cli_context()

return {
CLITelemetryField.COUNTERS: {
**cli_context.metrics.counters,
}
}


def _find_command_info() -> TelemetryDict:
ctx = click.get_current_context()
command_path = ctx.command_path.split(" ")[1:]
Expand Down Expand Up @@ -168,6 +180,7 @@ def log_command_result(execution: ExecutionMetadata):
CLITelemetryField.COMMAND_EXECUTION_ID: execution.execution_id,
CLITelemetryField.COMMAND_RESULT_STATUS: execution.status.value,
CLITelemetryField.COMMAND_EXECUTION_TIME: execution.get_duration(),
**_get_command_metrics(),
}
)

Expand All @@ -183,6 +196,7 @@ def log_command_execution_error(exception: Exception, execution: ExecutionMetada
CLITelemetryField.ERROR_TYPE: exception_type,
CLITelemetryField.IS_CLI_EXCEPTION: is_cli_exception,
CLITelemetryField.COMMAND_EXECUTION_TIME: execution.get_duration(),
**_get_command_metrics(),
}
)

Expand Down
6 changes: 6 additions & 0 deletions src/snowflake/cli/_plugins/nativeapp/codegen/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
TemplatesProcessor,
)
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.metrics import CLICounterField
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
ProcessorMapping,
)
Expand Down Expand Up @@ -76,6 +78,10 @@ def compile_artifacts(self):
if not self._should_invoke_processors():
return

metrics = get_cli_context().metrics
metrics.set_counter(CLICounterField.TEMPLATES_PROCESSOR, 0)
metrics.set_counter(CLICounterField.SNOWPARK_PROCESSOR, 0)

with cc.phase("Invoking artifact processors"):
if self._bundle_ctx.generated_root.exists():
raise ClickException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
NativeAppExtensionFunction,
)
from snowflake.cli._plugins.stage.diff import to_stage_path
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.metrics import CLICounterField
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
PathMapping,
ProcessorMapping,
Expand Down Expand Up @@ -176,6 +178,8 @@ def process(
setup script with generated SQL that registers these functions.
"""

get_cli_context().metrics.set_counter(CLICounterField.SNOWPARK_PROCESSOR, 1)

bundle_map = BundleMap(
project_root=self._bundle_ctx.project_root,
deploy_root=self._bundle_ctx.deploy_root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from snowflake.cli._plugins.nativeapp.exceptions import InvalidTemplateInFileError
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.metrics import CLICounterField
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
PathMapping,
ProcessorMapping,
Expand Down Expand Up @@ -98,6 +99,8 @@ def process(
Process the artifact by executing the template expansion logic on it.
"""

get_cli_context().metrics.set_counter(CLICounterField.TEMPLATES_PROCESSOR, 1)

bundle_map = BundleMap(
project_root=self._bundle_ctx.project_root,
deploy_root=self._bundle_ctx.deploy_root,
Expand Down
7 changes: 7 additions & 0 deletions src/snowflake/cli/api/cli_global_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from snowflake.cli.api.connections import ConnectionContext, OpenConnectionCache
from snowflake.cli.api.exceptions import MissingConfiguration
from snowflake.cli.api.metrics import CLIMetrics
from snowflake.cli.api.output.formats import OutputFormat
from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
from snowflake.connector import SnowflakeConnection
Expand All @@ -46,6 +47,8 @@ class _CliGlobalContextManager:
experimental: bool = False
enable_tracebacks: bool = True

metrics: CLIMetrics = field(default_factory=CLIMetrics)

project_path_arg: str | None = None
project_is_optional: bool = True
project_env_overrides_args: dict[str, str] = field(default_factory=dict)
Expand Down Expand Up @@ -152,6 +155,10 @@ def connection_context(self) -> ConnectionContext:
def enable_tracebacks(self) -> bool:
return self._manager.enable_tracebacks

@property
def metrics(self):
return self._manager.metrics

@property
def output_format(self) -> OutputFormat:
return self._manager.output_format
Expand Down
6 changes: 6 additions & 0 deletions src/snowflake/cli/api/entities/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
NO_WAREHOUSE_SELECTED_IN_SESSION,
)
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.api.metrics import CLICounterField
from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
from snowflake.cli.api.rendering.sql_templates import (
choose_sql_jinja_env_based_on_template_syntax,
Expand Down Expand Up @@ -246,9 +247,14 @@ def execute_post_deploy_hooks(
While executing SQL post deploy hooks, it first switches to the database provided in the input.
All post deploy scripts templates will first be expanded using the global template context.
"""
metrics = get_cli_context().metrics
metrics.set_counter(CLICounterField.POST_DEPLOY_SCRIPTS, 0)

if not post_deploy_hooks:
return

metrics.set_counter(CLICounterField.POST_DEPLOY_SCRIPTS, 1)

with console.phase(f"Executing {deployed_object_type} post-deploy actions"):
sql_scripts_paths = []
for hook in post_deploy_hooks:
Expand Down
57 changes: 57 additions & 0 deletions src/snowflake/cli/api/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Dict, Optional

_FEATURES_PREFIX = "features"
_APP_PREFIX = "app"


class CLICounterField:
TEMPLATES_PROCESSOR = f"{_FEATURES_PREFIX}.templates_processor"
SQL_TEMPLATES = f"{_FEATURES_PREFIX}.sql_templates"
PDF_TEMPLATES = f"{_FEATURES_PREFIX}.pdf_templates"
SNOWPARK_PROCESSOR = f"{_FEATURES_PREFIX}.{_APP_PREFIX}.snowpark_processor"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we should switch these, so app comes first, so we can separate these easily? @sfc-gh-bdufour , @sfc-gh-turbaszek wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's here makes sense to me, I don't think I understand the suggestion. Can you explain?

POST_DEPLOY_SCRIPTS = f"{_FEATURES_PREFIX}.{_APP_PREFIX}.post_deploy_scripts"


class CLIMetrics:
"""
Class to track various metrics across the execution of a command
"""

def __init__(self):
self._counters: Dict[str, int] = {}

def __eq__(self, other):
if isinstance(other, CLIMetrics):
return self._counters == other._counters
return False
Comment on lines +37 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this?


def get_counter(self, name: str) -> Optional[int]:
return self._counters.get(name)

def set_counter(self, name: str, value: int) -> None:
self._counters[name] = value

def add_counter(self, name: str, value: int) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: increment_counter
nit: optional value = 1

if name not in self._counters:
self.set_counter(name, value)
else:
self._counters[name] += value

@property
def counters(self) -> Dict[str, int]:
# return a copy of the original dict to avoid mutating the original
return self._counters.copy()
6 changes: 6 additions & 0 deletions src/snowflake/cli/api/rendering/sql_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.console.console import cli_console
from snowflake.cli.api.exceptions import InvalidTemplate
from snowflake.cli.api.metrics import CLICounterField
from snowflake.cli.api.rendering.jinja import (
CONTEXT_KEY,
FUNCTION_KEY,
Expand Down Expand Up @@ -96,4 +97,9 @@ def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
context_data = get_cli_context().template_context
context_data.update(data)
env = choose_sql_jinja_env_based_on_template_syntax(content)

get_cli_context().metrics.set_counter(
CLICounterField.SQL_TEMPLATES, int(has_sql_templates(content))
)

return env.from_string(content).render(context_data)
28 changes: 24 additions & 4 deletions src/snowflake/cli/api/utils/definition_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@

from jinja2 import Environment, TemplateSyntaxError, nodes
from packaging.version import Version
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.exceptions import CycleDetectedError, InvalidTemplate
from snowflake.cli.api.metrics import CLICounterField
from snowflake.cli.api.project.schemas.project_definition import (
ProjectProperties,
build_project_definition,
Expand Down Expand Up @@ -266,6 +268,12 @@ def find_any_template_vars(element):
return referenced_vars


def _has_referenced_vars_in_definition(
template_env: TemplatedEnvironment, definition: Definition
) -> bool:
return len(_get_referenced_vars_in_definition(template_env, definition)) > 0


def _template_version_warning():
cc.warning(
"Ignoring template pattern in project definition file. "
Expand All @@ -291,6 +299,17 @@ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
return definition_with_defaults


def _update_metrics(template_env: TemplatedEnvironment, definition: Definition):
metrics = get_cli_context().metrics

# render_definition_template is invoked multiple times both by the user
# and by us so we should make sure we don't overwrite a 1 with a 0 here
metrics.add_counter(CLICounterField.PDF_TEMPLATES, 0)

if _has_referenced_vars_in_definition(template_env, definition):
metrics.set_counter(CLICounterField.PDF_TEMPLATES, 1)


def render_definition_template(
original_definition: Optional[Definition], context_overrides: Context
) -> ProjectProperties:
Expand Down Expand Up @@ -326,10 +345,7 @@ def render_definition_template(
definition["definition_version"]
) < Version("1.1"):
try:
referenced_vars = _get_referenced_vars_in_definition(
template_env, definition
)
if referenced_vars:
if _has_referenced_vars_in_definition(template_env, definition):
_template_version_warning()
except Exception:
# also warn on Exception, as it means the user is incorrectly attempting to use templating
Expand All @@ -340,6 +356,10 @@ def render_definition_template(
project_context[CONTEXT_KEY]["env"] = environment_overrides
return ProjectProperties(project_definition, project_context)

# need to have the metrics added here since we add defaults to the
# definition that the user might not have added themselves later
_update_metrics(template_env, definition)

definition = _add_defaults_to_definition(definition)
project_context = {CONTEXT_KEY: definition}

Expand Down
78 changes: 78 additions & 0 deletions tests/api/test_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from snowflake.cli.api.metrics import CLIMetrics


def test_metrics_no_counters():
# given
metrics = CLIMetrics()

# when

# then
assert metrics.counters == {}
assert metrics.get_counter("counter1") is None


def test_metrics_set_one_counter():
# given
metrics = CLIMetrics()

# when
metrics.set_counter("counter1", 1)

# then
assert metrics.counters == {"counter1": 1}
assert metrics.get_counter("counter1") == 1


def test_metrics_add_new_counter():
# given
metrics = CLIMetrics()

# when
metrics.add_counter("counter1", 2)

# then
assert metrics.counters == {"counter1": 2}
assert metrics.get_counter("counter1") == 2


def test_metrics_add_existing_counter():
# given
metrics = CLIMetrics()

# when
metrics.set_counter("counter1", 2)
metrics.add_counter(name="counter1", value=1)

# then
assert metrics.counters == {"counter1": 3}
assert metrics.get_counter("counter1") == 3


def test_metrics_set_multiple_counters():
# given
metrics = CLIMetrics()

# when
metrics.set_counter("counter1", 1)
metrics.set_counter("counter2", 0)
metrics.set_counter(name="counter2", value=2)

# then
assert metrics.counters == {"counter1": 1, "counter2": 2}
assert metrics.get_counter("counter1") == 1
assert metrics.get_counter("counter2") == 2
Loading
Loading