Skip to content

Commit

Permalink
add feature for collecting parity metrics (localstack#6305)
Browse files Browse the repository at this point in the history
Co-authored-by: Dominik Schubert <[email protected]>
Co-authored-by: Thomas Rausch <[email protected]>
  • Loading branch information
3 people authored Jul 11, 2022
1 parent f1e7efb commit b7ac9ba
Show file tree
Hide file tree
Showing 13 changed files with 1,242 additions and 21 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ docker-create-push-manifests-light: ## Create and push manifests for the light d
docker-run-tests: ## Initializes the test environment and runs the tests in a docker container
# Remove argparse and dataclasses to fix https://github.com/pytest-dev/pytest/issues/5594
# Note: running "install-test-only" below, to avoid pulling in [runtime] extras from transitive dependencies
docker run --entrypoint= -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ \
docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ \
$(IMAGE_NAME_FULL) \
bash -c "make install-test-only && make init-testlibs && pip uninstall -y argparse dataclasses && DEBUG=$(DEBUG) LAMBDA_EXECUTOR=local PYTEST_LOGLEVEL=debug PYTEST_ARGS='$(PYTEST_ARGS)' COVERAGE_FILE='$(COVERAGE_FILE)' TEST_PATH='$(TEST_PATH)' make test-coverage"

Expand All @@ -207,6 +207,7 @@ test: ## Run automated tests
test-coverage: ## Run automated tests and create coverage report
($(VENV_RUN); python -m coverage --version; \
DEBUG=$(DEBUG) \
LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 \
python -m coverage run $(COVERAGE_ARGS) -m \
pytest --durations=10 --log-cli-level=$(PYTEST_LOGLEVEL) -s $(PYTEST_ARGS) $(TEST_PATH))

Expand Down
5 changes: 5 additions & 0 deletions localstack/aws/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from localstack.aws import handlers
from localstack.aws.handlers.metric_handler import MetricHandler
from localstack.aws.handlers.service_plugin import ServiceLoader
from localstack.services.plugins import SERVICE_PLUGINS, ServiceManager, ServicePluginManager

Expand All @@ -21,10 +22,12 @@ def __init__(self, service_manager: ServiceManager = None) -> None:
# lazy-loads services into the router
load_service = ServiceLoader(self.service_manager, self.service_request_router)

metric_collector = MetricHandler()
# the main request handler chain
self.request_handlers.extend(
[
handlers.push_request_context,
metric_collector.create_metric_handler_item,
handlers.parse_service_name, # enforce_cors and content_decoder depend on the service name
handlers.enforce_cors,
handlers.content_decoder,
Expand All @@ -36,6 +39,7 @@ def __init__(self, service_manager: ServiceManager = None) -> None:
handlers.add_region_from_header,
handlers.add_account_id,
handlers.parse_service_request,
metric_collector.record_parsed_request,
handlers.serve_custom_service_request_handlers,
load_service, # once we have the service request we can make sure we load the service
self.service_request_router, # once we know the service is loaded we can route the request
Expand All @@ -62,6 +66,7 @@ def __init__(self, service_manager: ServiceManager = None) -> None:
handlers.log_response,
handlers.count_service_request,
handlers.pop_request_context,
metric_collector.update_metric_collection,
]
)

Expand Down
173 changes: 173 additions & 0 deletions localstack/aws/handlers/metric_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import copy
import logging
from typing import List, Optional

from localstack import config
from localstack.aws.api import RequestContext, ServiceRequest
from localstack.aws.chain import HandlerChain
from localstack.http import Response
from localstack.utils.aws.aws_stack import is_internal_call_context

LOG = logging.getLogger(__name__)


class MetricHandlerItem:
"""
MetricHandlerItem to reference and update requests by the MetricHandler
"""

request_id: str
request_context: RequestContext
request_after_parse: Optional[ServiceRequest]

def __init__(self, request_contex: RequestContext) -> None:
super().__init__()
self.request_id = str(hash(request_contex))
self.request_context = request_contex
self.request_after_parse = None


class Metric:
"""
Data object to store relevant information for a metric entry in the raw-data collection (csv)
"""

service: str
operation: str
headers: str
parameters: str
status_code: int
response_code: Optional[str]
exception: str
origin: str
xfail: bool
aws_validated: bool
snapshot: bool
node_id: str

RAW_DATA_HEADER = [
"service",
"operation",
"request_headers",
"parameters",
"response_code",
"response_data",
"exception",
"origin",
"test_node_id",
"xfail",
"aws_validated",
"snapshot",
]

def __init__(
self,
service: str,
operation: str,
headers: str,
parameters: str,
response_code: int,
response_data: str,
exception: str,
origin: str,
node_id: str = "",
xfail: bool = False,
aws_validated: bool = False,
snapshot: bool = False,
) -> None:
self.service = service
self.operation = operation
self.headers = headers
self.parameters = parameters
self.response_code = response_code
self.response_data = response_data
self.exception = exception
self.origin = origin
self.node_id = node_id
self.xfail = xfail
self.aws_validated = aws_validated
self.snapshot = snapshot

def __iter__(self):
return iter(
[
self.service,
self.operation,
self.headers,
self.parameters,
self.response_code,
self.response_data,
self.exception,
self.origin,
self.node_id,
self.xfail,
self.aws_validated,
self.snapshot,
]
)


class MetricHandler:
metric_data: List[Metric] = []

def __init__(self) -> None:
self.metrics_handler_items = {}

def create_metric_handler_item(
self, chain: HandlerChain, context: RequestContext, response: Response
):
if not config.is_collect_metrics_mode():
return
item = MetricHandlerItem(context)
self.metrics_handler_items[context] = item

def _get_metric_handler_item_for_context(self, context: RequestContext) -> MetricHandlerItem:
return self.metrics_handler_items[context]

def record_parsed_request(
self, chain: HandlerChain, context: RequestContext, response: Response
):
if not config.is_collect_metrics_mode():
return
item = self._get_metric_handler_item_for_context(context)
item.request_after_parse = copy.deepcopy(context.service_request)

def record_exception(
self, chain: HandlerChain, exception: Exception, context: RequestContext, response: Response
):
if not config.is_collect_metrics_mode():
return
item = self._get_metric_handler_item_for_context(context)
item.caught_exception_name = exception.__class__.__name__

def update_metric_collection(
self, chain: HandlerChain, context: RequestContext, response: Response
):
if not config.is_collect_metrics_mode() or not context.service_operation:
return

is_internal = is_internal_call_context(context.request.headers)
item = self._get_metric_handler_item_for_context(context)

# parameters might get changed when dispatched to the service - we use the params stored in request_after_parse
parameters = ",".join(item.request_after_parse or "")

response_data = response.data.decode("utf-8") if response.status_code >= 300 else ""

MetricHandler.metric_data.append(
Metric(
service=context.service_operation.service,
operation=context.service_operation.operation,
headers=context.request.headers,
parameters=parameters,
response_code=response.status_code,
response_data=response_data,
exception=context.service_exception.__class__.__name__
if context.service_exception
else "",
origin="internal" if is_internal else "external",
)
)

# cleanup
del self.metrics_handler_items[context]
Loading

0 comments on commit b7ac9ba

Please sign in to comment.