-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds contract tests for mysqlclient with current behavior
- Loading branch information
Showing
5 changed files
with
201 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Meant to be run from aws-otel-python-instrumentation/contract-tests. | ||
# Assumes existence of dist/aws_opentelemetry_distro-<pkg_version>-py3-none-any.whl. | ||
# Assumes filename of aws_opentelemetry_distro-<pkg_version>-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"] |
77 changes: 77 additions & 0 deletions
77
contract-tests/images/applications/mysqlclient/mysqlclient_server.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
6 changes: 6 additions & 0 deletions
6
contract-tests/images/applications/mysqlclient/pyproject.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" |
4 changes: 4 additions & 0 deletions
4
contract-tests/images/applications/mysqlclient/requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
99 changes: 99 additions & 0 deletions
99
contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |