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

Merged
merged 40 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
40 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
b444cf2
add -> increment; move snowpark counter check to e2e
sfc-gh-mchok Sep 20, 2024
0608844
remove snowpark processor test from e2e
sfc-gh-mchok Sep 23, 2024
f57f5a7
make sql template test name not confusing
sfc-gh-mchok Sep 23, 2024
28912ab
fix test local changes
sfc-gh-mchok Sep 23, 2024
618dc43
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 23, 2024
2326701
remove fixture to simplify test utils now that e2e is no longer needed
sfc-gh-mchok Sep 23, 2024
4b26b6f
fix post deploy scripts not being picked up properly due to extraneou…
sfc-gh-mchok Sep 23, 2024
b268e4e
add back post deploy check and move instrumentation to fix failing test
sfc-gh-mchok Sep 23, 2024
8d0c4a4
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 23, 2024
65f1d9f
fix pre-commit errors
sfc-gh-mchok Sep 23, 2024
dde4337
Merge remote-tracking branch 'origin/main' into mchok-metrics-counter…
sfc-gh-mchok Sep 24, 2024
a796c11
adjust counter naming and add comment for convention
sfc-gh-mchok Sep 24, 2024
712b46a
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 24, 2024
23ccfac
adjust constant names and docstring
sfc-gh-mchok Sep 24, 2024
fbb155c
move processors instrumentation, centralize post deploy hook instrume…
sfc-gh-mchok Sep 24, 2024
5304633
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 25, 2024
dd2f34a
remove wrong comment
sfc-gh-mchok Sep 25, 2024
8af47f9
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 25, 2024
b6a272b
Merge branch 'main' into mchok-metrics-counter-feature-tracking
sfc-gh-mchok Sep 25, 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)
sfc-gh-mchok marked this conversation as resolved.
Show resolved Hide resolved

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
52 changes: 52 additions & 0 deletions src/snowflake/cli/api/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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?

Copy link
Contributor

@sfc-gh-melnacouzi sfc-gh-melnacouzi Sep 20, 2024

Choose a reason for hiding this comment

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

I was thinking, automated graphs will pick up anything under features. Do we want area-specific commands to also be part of that main graph, or do we want to adjust the hierarchy so that app features for instance, are tracked in separate graphs? In that case, 'app.features.snowpark_process' would make more sense:

  • Main features graph can look for any metrics starting with features. and display them.
  • App specific features graph can look for any metrics starting with app.features. and display them.

Copy link
Contributor

Choose a reason for hiding this comment

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

@sfc-gh-mchok discussed with Bruno, and I think we can keep features first, but let's add something like 'features.global.' for the global features to keep the structure consistent, and be able to extract global features easily.

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 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:
sfc-gh-mchok marked this conversation as resolved.
Show resolved Hide resolved
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)
14 changes: 14 additions & 0 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 @@ -340,6 +342,18 @@ def render_definition_template(
project_context[CONTEXT_KEY]["env"] = environment_overrides
return ProjectProperties(project_definition, project_context)

has_user_referenced_vars = (
Copy link
Contributor

Choose a reason for hiding this comment

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

can we put this whole new logic into a function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I also refactored the usage here with this function, let me know if this works for you!

len(_get_referenced_vars_in_definition(template_env, definition)) > 0
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a risk/side effect from calling this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I went through the called functions and i do not believe there should be any, unless self._jinja_env.parse has side effects?

Copy link
Contributor

Choose a reason for hiding this comment

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

it could throw error, but I guess it will throw them later anyways

)
metrics = get_cli_context().metrics

# this function is invoked multiple times both by the user and by us,
# so we should only overwrite if templates were not found at any point
if metrics.get_counter(CLICounterField.PDF_TEMPLATES) != 1:
sfc-gh-mchok marked this conversation as resolved.
Show resolved Hide resolved
metrics.set_counter(
CLICounterField.PDF_TEMPLATES, int(has_user_referenced_vars)
)

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

Expand Down
83 changes: 83 additions & 0 deletions tests/api/test_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 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
expected = {}
metrics = CLIMetrics()

# when

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


def test_metrics_set_one_counter():
# given
expected = {"counter1": 1}
metrics = CLIMetrics()

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

# then
assert metrics.counters == expected
assert metrics.get_counter("counter1") == expected["counter1"]


def test_metrics_add_new_counter():
# given
expected = {"counter1": 2}
sfc-gh-mchok marked this conversation as resolved.
Show resolved Hide resolved
metrics = CLIMetrics()

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

# then
assert metrics.counters == expected
assert metrics.get_counter("counter1") == expected["counter1"]


def test_metrics_add_existing_counter():
# given
expected = {"counter1": 3}
metrics = CLIMetrics()

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

# then
assert metrics.counters == expected
assert metrics.get_counter("counter1") == expected["counter1"]


def test_metrics_set_multiple_counters():
# given
expected = {"counter1": 1, "counter2": 2}
metrics = CLIMetrics()

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

# then
assert metrics.counters == expected
assert metrics.get_counter("counter1") == expected["counter1"]
assert metrics.get_counter("counter2") == expected["counter2"]
Loading
Loading