Skip to content

Commit

Permalink
Adds contract tests for mysqlclient with current behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
georgeboc committed Jun 26, 2024
1 parent f9355f2 commit 99152c3
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 0 deletions.
15 changes: 15 additions & 0 deletions contract-tests/images/applications/mysqlclient/Dockerfile
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"]
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 contract-tests/images/applications/mysqlclient/pyproject.toml
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"
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 contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py
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)

0 comments on commit 99152c3

Please sign in to comment.