From 99152c3e65c933b66bb4a949176a84eb0ad42298 Mon Sep 17 00:00:00 2001 From: George Bochileanu Parfenie Date: Wed, 26 Jun 2024 16:50:06 +0000 Subject: [PATCH] Adds contract tests for mysqlclient with current behavior --- .../applications/mysqlclient/Dockerfile | 15 +++ .../mysqlclient/mysqlclient_server.py | 77 +++++++++++++++ .../applications/mysqlclient/pyproject.toml | 6 ++ .../applications/mysqlclient/requirements.txt | 4 + .../amazon/mysqlclient/mysqlclient_test.py | 99 +++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 contract-tests/images/applications/mysqlclient/Dockerfile create mode 100644 contract-tests/images/applications/mysqlclient/mysqlclient_server.py create mode 100644 contract-tests/images/applications/mysqlclient/pyproject.toml create mode 100644 contract-tests/images/applications/mysqlclient/requirements.txt create mode 100644 contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py diff --git a/contract-tests/images/applications/mysqlclient/Dockerfile b/contract-tests/images/applications/mysqlclient/Dockerfile new file mode 100644 index 000000000..b86e1364a --- /dev/null +++ b/contract-tests/images/applications/mysqlclient/Dockerfile @@ -0,0 +1,15 @@ +# Meant to be run from aws-otel-python-instrumentation/contract-tests. +# Assumes existence of dist/aws_opentelemetry_distro--py3-none-any.whl. +# Assumes filename of aws_opentelemetry_distro--py3-none-any.whl is passed in as "DISTRO" arg. +FROM python:3.10 +WORKDIR /mysqlclient +COPY ./dist/$DISTRO /mysqlclient +COPY ./contract-tests/images/applications/mysqlclient /mysqlclient + +ENV PIP_ROOT_USER_ACTION=ignore +ARG DISTRO +RUN pip install --upgrade pip && pip install -r requirements.txt && pip install ${DISTRO} --force-reinstall +RUN opentelemetry-bootstrap -a install + +# Without `-u`, logs will be buffered and `wait_for_logs` will never return. +CMD ["opentelemetry-instrument", "python", "-u", "./mysqlclient_server.py"] diff --git a/contract-tests/images/applications/mysqlclient/mysqlclient_server.py b/contract-tests/images/applications/mysqlclient/mysqlclient_server.py new file mode 100644 index 000000000..ba405518f --- /dev/null +++ b/contract-tests/images/applications/mysqlclient/mysqlclient_server.py @@ -0,0 +1,77 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import atexit +import os +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread +from typing import Tuple + +import MySQLdb +from MySQLdb import ProgrammingError +from typing_extensions import override + +_PORT: int = 8080 +_DROP_TABLE: str = "drop_table" +_ERROR: str = "error" +_FAULT: str = "fault" +_CREATE_DATABASE: str = "create_database" + +_DB_HOST = os.getenv("DB_HOST") +_DB_USER = os.getenv("DB_USER") +_DB_PASS = os.getenv("DB_PASS") +_DB_NAME = os.getenv("DB_NAME") + + +class RequestHandler(BaseHTTPRequestHandler): + @override + # pylint: disable=invalid-name + def do_GET(self): + status_code: int = 200 + conn = MySQLdb.connect(database=_DB_NAME, user=_DB_USER, password=_DB_PASS, host=_DB_HOST) + conn.autocommit = True # CREATE DATABASE cannot run in a transaction block + if self.in_path(_DROP_TABLE): + cur = conn.cursor() + cur.execute("DROP TABLE IF EXISTS test_table") + cur.close() + status_code = 200 + elif self.in_path(_CREATE_DATABASE): + cur = conn.cursor() + cur.execute("CREATE DATABASE test_database") + cur.close() + status_code = 200 + elif self.in_path(_FAULT): + cur = conn.cursor() + try: + cur.execute("SELECT DISTINCT id, name FROM invalid_table") + except ProgrammingError as exception: + print("Expected Exception with Invalid SQL occurred:", exception) + status_code = 500 + except Exception as exception: # pylint: disable=broad-except + print("Exception Occurred:", exception) + else: + status_code = 200 + finally: + cur.close() + else: + status_code = 404 + conn.close() + self.send_response_only(status_code) + self.end_headers() + + def in_path(self, sub_path: str): + return sub_path in self.path + + +def main() -> None: + server_address: Tuple[str, int] = ("0.0.0.0", _PORT) + request_handler_class: type = RequestHandler + requests_server: ThreadingHTTPServer = ThreadingHTTPServer(server_address, request_handler_class) + atexit.register(requests_server.shutdown) + server_thread: Thread = Thread(target=requests_server.serve_forever) + server_thread.start() + print("Ready") + server_thread.join() + + +if __name__ == "__main__": + main() diff --git a/contract-tests/images/applications/mysqlclient/pyproject.toml b/contract-tests/images/applications/mysqlclient/pyproject.toml new file mode 100644 index 000000000..e29cb186a --- /dev/null +++ b/contract-tests/images/applications/mysqlclient/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "mysqlclient-server" +description = "Simple server that relies on mysqlclient library" +version = "1.0.0" +license = "Apache-2.0" +requires-python = ">=3.8" diff --git a/contract-tests/images/applications/mysqlclient/requirements.txt b/contract-tests/images/applications/mysqlclient/requirements.txt new file mode 100644 index 000000000..550093422 --- /dev/null +++ b/contract-tests/images/applications/mysqlclient/requirements.txt @@ -0,0 +1,4 @@ +opentelemetry-distro==0.43b0 +opentelemetry-exporter-otlp-proto-grpc==1.22.0 +typing-extensions==4.9.0 +mysqlclient==2.2.4 diff --git a/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py b/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py new file mode 100644 index 000000000..74ece3be4 --- /dev/null +++ b/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py @@ -0,0 +1,99 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from typing import Dict, List + +from testcontainers.mysql import MySqlContainer +from typing_extensions import override + +from amazon.base.contract_test_base import NETWORK_NAME +from amazon.base.database_contract_test_base import ( + DATABASE_HOST, + DATABASE_NAME, + DATABASE_PASSWORD, + DATABASE_USER, + LOCAL_ROOT, + DatabaseContractTestBase, +) +from amazon.utils.application_signals_constants import ( + AWS_LOCAL_OPERATION, + AWS_LOCAL_SERVICE, + AWS_REMOTE_OPERATION, + AWS_REMOTE_RESOURCE_IDENTIFIER, + AWS_REMOTE_RESOURCE_TYPE, + AWS_REMOTE_SERVICE, + AWS_SPAN_KIND, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue + + +class MysqlClientTest(DatabaseContractTestBase): + @override + @classmethod + def set_up_dependency_container(cls) -> None: + cls.container = ( + MySqlContainer(MYSQL_USER=DATABASE_USER, MYSQL_PASSWORD=DATABASE_PASSWORD, MYSQL_DATABASE=DATABASE_NAME) + .with_kwargs(network=NETWORK_NAME) + .with_name(DATABASE_HOST) + ) + cls.container.start() + + @override + @classmethod + def tear_down_dependency_container(cls) -> None: + cls.container.stop() + + @override + @staticmethod + def get_remote_service() -> str: + return "mysql" + + @override + @staticmethod + def get_database_port() -> int: + return 3306 + + @override + @staticmethod + def get_application_image_name() -> str: + return "aws-application-signals-tests-mysqlclient-app" + + def test_drop_table_succeeds(self) -> None: + self.assert_drop_table_succeeds() + + def test_create_database_succeeds(self) -> None: + self.assert_create_database_succeeds() + + def test_fault(self) -> None: + self.assert_fault() + + # This adapter is not currently fully supported by OTEL + # GitHub issue: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1319 + # Once the adapter is supported, we could remove _assert_aws_attributes and _assert_semantic_conventions_attributes + # methods from this class. + @override + def _assert_aws_attributes( + self, attributes_list: List[KeyValue], expected_span_kind: str = LOCAL_ROOT, **kwargs + ) -> None: + attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) + self._assert_str_attribute(attributes_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name()) + # InternalOperation as OTEL does not instrument the basic server we are using, so the client span is a local + # root. + self._assert_str_attribute(attributes_dict, AWS_LOCAL_OPERATION, "InternalOperation") + self._assert_str_attribute(attributes_dict, AWS_REMOTE_SERVICE, self.get_remote_service()) + self._assert_str_attribute(attributes_dict, AWS_REMOTE_OPERATION, kwargs.get("sql_command")) + self.assertTrue(AWS_REMOTE_RESOURCE_TYPE not in attributes_dict) + self.assertTrue(AWS_REMOTE_RESOURCE_IDENTIFIER not in attributes_dict) + # See comment above AWS_LOCAL_OPERATION + self._assert_str_attribute(attributes_dict, AWS_SPAN_KIND, expected_span_kind) + + @override + def _assert_semantic_conventions_attributes(self, attributes_list: List[KeyValue], command: str) -> None: + attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) + self.assertTrue(attributes_dict.get("db.statement").string_value.startswith(command)) + self._assert_str_attribute(attributes_dict, "db.system", self.get_remote_service()) + self._assert_str_attribute(attributes_dict, "db.name", "") + self.assertTrue("net.peer.name" not in attributes_dict) + self._assert_int_attribute(attributes_dict, "net.peer.port", self.get_database_port()) + self.assertTrue("server.address" not in attributes_dict) + self.assertTrue("server.port" not in attributes_dict) + self.assertTrue("db.operation" not in attributes_dict)