From a2d0f1f242c182e78bac1d8f4e1be14baad5f809 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 13 Mar 2025 18:23:31 +0100 Subject: [PATCH 1/4] metrics migration and improve query language transparency --- .../usage_metrics_static_analyser.py | 49 +++++++++++++------ .../services/stepfunctions/usage.py | 20 ++------ 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py b/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py index 113abc2bc8de4..3735373ee188b 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py +++ b/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import logging +from typing import Final import localstack.services.stepfunctions.usage as UsageMetrics from localstack.services.stepfunctions.asl.antlr.runtime.ASLParser import ASLParser @@ -10,20 +13,36 @@ LOG = logging.getLogger(__name__) +class QueryLanguage(str): + JSONPath = QueryLanguageMode.JSONPath.name + JSONata = QueryLanguageMode.JSONata.name + Both = "JSONPath+JSONata" + + class UsageMetricsStaticAnalyser(StaticAnalyser): @staticmethod - def process(definition: str) -> "UsageMetricsStaticAnalyser": + def process(definition: str) -> UsageMetricsStaticAnalyser: analyser = UsageMetricsStaticAnalyser() try: + # Run the static analyser. analyser.analyse(definition=definition) - if analyser.has_jsonata: - UsageMetrics.jsonata_create_counter.increment() + # Determine which query language is being used in this state machine. + query_modes = analyser._query_language_modes + if len(query_modes) == 2: + language_used = QueryLanguage.Both + elif QueryLanguageMode.JSONata in query_modes: + language_used = QueryLanguage.JSONata else: - UsageMetrics.jsonpath_create_counter.increment() + language_used = QueryLanguage.JSONPath - if analyser.has_variable_sampling: - UsageMetrics.variables_create_counter.increment() + # Determine is the state machine uses the variables feature. + uses_variables = analyser._uses_variables + + # Count. + UsageMetrics.language_features_counter.labels( + query_language=language_used, variables=uses_variables + ).increment() except Exception as e: LOG.warning( "Failed to record Step Functions metrics from static analysis", @@ -31,26 +50,28 @@ def process(definition: str) -> "UsageMetricsStaticAnalyser": ) return analyser + _query_language_modes: Final[set[QueryLanguageMode]] + _uses_variables: bool + def __init__(self): super().__init__() - self.has_jsonata: bool = False - self.has_variable_sampling = False + self._query_language_modes = set() + self._uses_variables = False def visitQuery_language_decl(self, ctx: ASLParser.Query_language_declContext): - if self.has_jsonata: + if len(self._query_language_modes) == 2: + # Both query language modes have been confirmed to be in use. return - query_language_mode_int = ctx.children[-1].getSymbol().type query_language_mode = QueryLanguageMode(value=query_language_mode_int) - if query_language_mode == QueryLanguageMode.JSONata: - self.has_jsonata = True + self._query_language_modes.add(query_language_mode) def visitString_literal(self, ctx: ASLParser.String_literalContext): # Prune everything parsed as a string literal. return def visitString_variable_sample(self, ctx: ASLParser.String_variable_sampleContext): - self.has_variable_sampling = True + self._uses_variables = True def visitAssign_decl(self, ctx: ASLParser.Assign_declContext): - self.has_variable_sampling = True + self._uses_variables = True diff --git a/localstack-core/localstack/services/stepfunctions/usage.py b/localstack-core/localstack/services/stepfunctions/usage.py index 8d5e9391f3147..98b403b8b0879 100644 --- a/localstack-core/localstack/services/stepfunctions/usage.py +++ b/localstack-core/localstack/services/stepfunctions/usage.py @@ -2,19 +2,9 @@ Usage reporting for StepFunctions service """ -from localstack.utils.analytics.usage import UsageCounter +from localstack.utils.analytics.metrics import Counter -# Count of StepFunctions being created with JSONata QueryLanguage -jsonata_create_counter = UsageCounter("stepfunctions:jsonata:create") - -# Count of StepFunctions being created with JSONPath QueryLanguage -jsonpath_create_counter = UsageCounter("stepfunctions:jsonpath:create") - -# Count of StepFunctions being created that use Variable Sampling or the Assign block -variables_create_counter = UsageCounter("stepfunctions:variables:create") - -# Successful invocations (also including expected error cases in line with AWS behaviour) -invocation_counter = UsageCounter("stepfunctions:invocation") - -# Unexpected errors that we do not account for -error_counter = UsageCounter("stepfunctions:error") +# Initialize a counter to record the usage of language features for each state machine. +language_features_counter = Counter( + namespace="stepfunctions", name="language_features_used", labels=["query_language", "variables"] +) From 55c628d58358afa43b285f26a15e865b35492dd8 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:04:09 +0100 Subject: [PATCH 2/4] fix unit tests, refine query language computation --- .../usage_metrics_static_analyser.py | 27 +++++++++------ .../test_usage_metrics_static_analyser.py | 34 ++++++++++++++----- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py b/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py index 3735373ee188b..01f36b9a3f5f1 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py +++ b/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py @@ -28,7 +28,7 @@ def process(definition: str) -> UsageMetricsStaticAnalyser: analyser.analyse(definition=definition) # Determine which query language is being used in this state machine. - query_modes = analyser._query_language_modes + query_modes = analyser.query_language_modes if len(query_modes) == 2: language_used = QueryLanguage.Both elif QueryLanguageMode.JSONata in query_modes: @@ -37,7 +37,7 @@ def process(definition: str) -> UsageMetricsStaticAnalyser: language_used = QueryLanguage.JSONPath # Determine is the state machine uses the variables feature. - uses_variables = analyser._uses_variables + uses_variables = analyser.uses_variables # Count. UsageMetrics.language_features_counter.labels( @@ -50,28 +50,35 @@ def process(definition: str) -> UsageMetricsStaticAnalyser: ) return analyser - _query_language_modes: Final[set[QueryLanguageMode]] - _uses_variables: bool + query_language_modes: Final[set[QueryLanguageMode]] + uses_variables: bool def __init__(self): super().__init__() - self._query_language_modes = set() - self._uses_variables = False + self.query_language_modes = set() + self.uses_variables = False def visitQuery_language_decl(self, ctx: ASLParser.Query_language_declContext): - if len(self._query_language_modes) == 2: + if len(self.query_language_modes) == 2: # Both query language modes have been confirmed to be in use. return query_language_mode_int = ctx.children[-1].getSymbol().type query_language_mode = QueryLanguageMode(value=query_language_mode_int) - self._query_language_modes.add(query_language_mode) + self.query_language_modes.add(query_language_mode) + + def visitState_decl(self, ctx: ASLParser.State_declContext): + # If before entering a state, no query language was explicitly enforced, then we know + # this is the first state operating under the default mode (JSONPath) + if not self.query_language_modes: + self.query_language_modes.add(QueryLanguageMode.JSONPath) + super().visitState_decl(ctx=ctx) def visitString_literal(self, ctx: ASLParser.String_literalContext): # Prune everything parsed as a string literal. return def visitString_variable_sample(self, ctx: ASLParser.String_variable_sampleContext): - self._uses_variables = True + self.uses_variables = True def visitAssign_decl(self, ctx: ASLParser.Assign_declContext): - self._uses_variables = True + self.uses_variables = True diff --git a/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py b/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py index 86779bebb1096..51e98f1c60638 100644 --- a/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py +++ b/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py @@ -2,6 +2,7 @@ import pytest +from localstack.services.stepfunctions.asl.component.common.query_language import QueryLanguageMode from localstack.services.stepfunctions.asl.static_analyser.usage_metrics_static_analyser import ( UsageMetricsStaticAnalyser, ) @@ -104,22 +105,39 @@ class TestUsageMetricsStaticAnalyser: ) def test_jsonata(self, definition): analyser = UsageMetricsStaticAnalyser.process(definition) - assert analyser.has_jsonata - assert not analyser.has_variable_sampling + assert not analyser.uses_variables + assert QueryLanguageMode.JSONata in analyser.query_language_modes + + @pytest.mark.parametrize( + "definition", + [ + BASE_PASS_JSONATA_OVERRIDE, + BASE_PASS_JSONATA_OVERRIDE_DEFAULT, + ], + ids=[ + "BASE_PASS_JSONATA_OVERRIDE", + "BASE_PASS_JSONATA_OVERRIDE_DEFAULT", + ], + ) + def test_both_query_languages(self, definition): + analyser = UsageMetricsStaticAnalyser.process(definition) + assert not analyser.uses_variables + assert QueryLanguageMode.JSONata in analyser.query_language_modes + assert QueryLanguageMode.JSONPath in analyser.query_language_modes @pytest.mark.parametrize("definition", [BASE_PASS_JSONPATH], ids=["BASE_PASS_JSONPATH"]) def test_jsonpath(self, definition): analyser = UsageMetricsStaticAnalyser.process(definition) - assert not analyser.has_jsonata - assert not analyser.has_variable_sampling + assert QueryLanguageMode.JSONata not in analyser.query_language_modes + assert not analyser.uses_variables @pytest.mark.parametrize( "definition", [JSONPATH_TO_JSONATA_DATAFLOW], ids=["JSONPATH_TO_JSONATA_DATAFLOW"] ) def test_jsonata_and_variable_sampling(self, definition): analyser = UsageMetricsStaticAnalyser.process(definition) - assert analyser.has_jsonata - assert analyser.has_variable_sampling + assert QueryLanguageMode.JSONata in analyser.query_language_modes + assert analyser.uses_variables @pytest.mark.parametrize( "definition", @@ -134,5 +152,5 @@ def test_jsonata_and_variable_sampling(self, definition): ) def test_jsonpath_and_variable_sampling(self, definition): analyser = UsageMetricsStaticAnalyser.process(definition) - assert not analyser.has_jsonata - assert analyser.has_variable_sampling + assert QueryLanguageMode.JSONata not in analyser.query_language_modes + assert analyser.uses_variables From bc475b6b6ee7360376e560bebde1759ce9d82bca Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:58:57 +0100 Subject: [PATCH 3/4] rename variables label --- .../asl/static_analyser/usage_metrics_static_analyser.py | 2 +- localstack-core/localstack/services/stepfunctions/usage.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py b/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py index 01f36b9a3f5f1..b19fd0d4bf420 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py +++ b/localstack-core/localstack/services/stepfunctions/asl/static_analyser/usage_metrics_static_analyser.py @@ -41,7 +41,7 @@ def process(definition: str) -> UsageMetricsStaticAnalyser: # Count. UsageMetrics.language_features_counter.labels( - query_language=language_used, variables=uses_variables + query_language=language_used, uses_variables=uses_variables ).increment() except Exception as e: LOG.warning( diff --git a/localstack-core/localstack/services/stepfunctions/usage.py b/localstack-core/localstack/services/stepfunctions/usage.py index 98b403b8b0879..63c5c90411b40 100644 --- a/localstack-core/localstack/services/stepfunctions/usage.py +++ b/localstack-core/localstack/services/stepfunctions/usage.py @@ -6,5 +6,7 @@ # Initialize a counter to record the usage of language features for each state machine. language_features_counter = Counter( - namespace="stepfunctions", name="language_features_used", labels=["query_language", "variables"] + namespace="stepfunctions", + name="language_features_used", + labels=["query_language", "uses_variables"], ) From 27aace50e59c26edc426ef2f0aed5906ee611ddb Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:06:33 +0100 Subject: [PATCH 4/4] minor --- .../services/stepfunctions/test_usage_metrics_static_analyser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py b/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py index 51e98f1c60638..2d2d8aa6b4931 100644 --- a/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py +++ b/tests/unit/services/stepfunctions/test_usage_metrics_static_analyser.py @@ -136,6 +136,7 @@ def test_jsonpath(self, definition): ) def test_jsonata_and_variable_sampling(self, definition): analyser = UsageMetricsStaticAnalyser.process(definition) + assert QueryLanguageMode.JSONPath in analyser.query_language_modes assert QueryLanguageMode.JSONata in analyser.query_language_modes assert analyser.uses_variables