Skip to content

Commit f838f5d

Browse files
ektabjthpierce
andauthored
Creating contract-tests for basic SELECT query with MySQL (#208)
*Issue #, if available:* N/A *Description of changes:* Creating contract-tests for basic SELECT query with MySQL. Testing: ``` <whatever>: pytest contract-tests/tests ================================================================ test session starts ================================================================= platform linux -- Python 3.12.3, pytest-8.2.2, pluggy-1.5.0 rootdir: /home/ektabj/aws-otel-python-instrumentation/contract-tests/tests configfile: pyproject.toml collected 2 items ../../../../home/ektabj/aws-otel-python-instrumentation/contract-tests/tests/test/amazon/pymysql/pymysql_test.py::PyMysqlTest::test_fault ------------------------------------------------------------------- live log setup ------------------------------------------------------------------- 2024-06-12 19:13:37 [ INFO] Pulling image aws-application-signals-mock-collector-python (container.py:53) 2024-06-12 19:13:37 [ INFO] Container started: d7238d399968 (container.py:64) 2024-06-12 19:13:38 [ INFO] Pulling image mysql:latest (container.py:53) 2024-06-12 19:13:39 [ INFO] Container started: 18c0eaa21b11 (container.py:64) 2024-06-12 19:13:39 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:39 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:40 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:41 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:42 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:43 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:44 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:45 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:46 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:47 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:48 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:49 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:50 [ INFO] Waiting to be ready... (waiting_utils.py:46) ------------------------------------------------------------------- live log call -------------------------------------------------------------------- 2024-06-12 19:13:50 [ INFO] Pulling image aws-application-signals-tests-pymysql-app (container.py:53) 2024-06-12 19:13:50 [ INFO] Container started: 253735cdb4d2 (container.py:64) 2024-06-12 19:13:56 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:57 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:13:57 [ INFO] Application stdout (contract_test_base.py:118) 2024-06-12 19:13:57 [ INFO] Ready Expected Exception with Invalid SQL occurred: (1146, "Table 'testdb.invalid_table' doesn't exist") (contract_test_base.py:119) 2024-06-12 19:13:57 [ INFO] Application stderr (contract_test_base.py:120) 2024-06-12 19:13:57 [ INFO] Failed to get k8s token: [Errno 2] No such file or directory: '/var/run/secrets/kubernetes.io/serviceaccount/token' AwsEcsResourceDetector failed: Missing ECS_CONTAINER_METADATA_URI therefore process is not on ECS. AwsEksResourceDetector failed: [Errno 2] No such file or directory: '/var/run/secrets/kubernetes.io/serviceaccount/token' Exception in detector <opentelemetry.sdk.extension.aws.resource.ec2.AwsEc2ResourceDetector object at 0x7fc36a868df0>, ignoring AwsEc2ResourceDetector failed: timed out Configuration of configurator not loaded, aws_configurator already loaded (contract_test_base.py:121) PASSED [ 50%] ../../../../home/ektabj/aws-otel-python-instrumentation/contract-tests/tests/test/amazon/pymysql/pymysql_test.py::PyMysqlTest::test_success ------------------------------------------------------------------- live log call -------------------------------------------------------------------- 2024-06-12 19:13:57 [ INFO] Pulling image aws-application-signals-tests-pymysql-app (container.py:53) 2024-06-12 19:13:58 [ INFO] Container started: 57ab71887ac6 (container.py:64) 2024-06-12 19:14:04 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:14:04 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-12 19:14:04 [ INFO] Application stdout (contract_test_base.py:118) 2024-06-12 19:14:04 [ INFO] Ready (contract_test_base.py:119) 2024-06-12 19:14:04 [ INFO] Application stderr (contract_test_base.py:120) 2024-06-12 19:14:04 [ INFO] AwsEcsResourceDetector failed: Missing ECS_CONTAINER_METADATA_URI therefore process is not on ECS. Failed to get k8s token: [Errno 2] No such file or directory: '/var/run/secrets/kubernetes.io/serviceaccount/token' AwsEksResourceDetector failed: [Errno 2] No such file or directory: '/var/run/secrets/kubernetes.io/serviceaccount/token' Exception in detector <opentelemetry.sdk.extension.aws.resource.ec2.AwsEc2ResourceDetector object at 0x7f21390ecd00>, ignoring AwsEc2ResourceDetector failed: timed out Configuration of configurator not loaded, aws_configurator already loaded (contract_test_base.py:121) PASSED [100%] ----------------------------------------------------------------- live log teardown ------------------------------------------------------------------ 2024-06-12 19:14:05 [ INFO] MockCollector stdout (contract_test_base.py:71) 2024-06-12 19:14:05 [ INFO] Ready (contract_test_base.py:72) 2024-06-12 19:14:05 [ INFO] MockCollector stderr (contract_test_base.py:73) 2024-06-12 19:14:05 [ INFO] (contract_test_base.py:74) ================================================================== warnings summary ================================================================== <frozen importlib._bootstrap>:488 <frozen importlib._bootstrap>:488: DeprecationWarning: Type google._upb._message.MessageMapContainer uses PyType_Spec with a metaclass that has custom tp_new. This is deprecated and will no longer be allowed in Python 3.14. <frozen importlib._bootstrap>:488 <frozen importlib._bootstrap>:488: DeprecationWarning: Type google._upb._message.ScalarMapContainer uses PyType_Spec with a metaclass that has custom tp_new. This is deprecated and will no longer be allowed in Python 3.14. -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html =========================================================== 2 passed, 2 warnings in 30.13s =========================================================== ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Thomas Pierce <[email protected]>
1 parent bbd0e8d commit f838f5d

File tree

6 files changed

+248
-0
lines changed

6 files changed

+248
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Meant to be run from aws-otel-python-instrumentation/contract-tests.
2+
# Assumes existence of dist/aws_opentelemetry_distro-<pkg_version>-py3-none-any.whl.
3+
# Assumes filename of aws_opentelemetry_distro-<pkg_version>-py3-none-any.whl is passed in as "DISTRO" arg.
4+
FROM python:3.10
5+
WORKDIR /pymysql
6+
COPY ./dist/$DISTRO /pymysql
7+
COPY ./contract-tests/images/applications/pymysql /pymysql
8+
9+
ENV PIP_ROOT_USER_ACTION=ignore
10+
ARG DISTRO
11+
RUN pip install --upgrade pip && pip install -r requirements.txt && pip install ${DISTRO} --force-reinstall
12+
RUN opentelemetry-bootstrap -a install
13+
14+
# Without `-u`, logs will be buffered and `wait_for_logs` will never return.
15+
CMD ["opentelemetry-instrument", "python", "-u", "./pymysql_server.py"]
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import atexit
4+
import os
5+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
6+
from threading import Thread
7+
from typing import Tuple
8+
9+
import pymysql
10+
from typing_extensions import override
11+
12+
_PORT: int = 8080
13+
_SUCCESS: str = "success"
14+
_ERROR: str = "error"
15+
_FAULT: str = "fault"
16+
17+
_DB_HOST = os.getenv("DB_HOST")
18+
_DB_USER = os.getenv("DB_USER")
19+
_DB_PASS = os.getenv("DB_PASS")
20+
_DB_NAME = os.getenv("DB_NAME")
21+
22+
23+
class RequestHandler(BaseHTTPRequestHandler):
24+
@override
25+
# pylint: disable=invalid-name
26+
def do_GET(self):
27+
status_code: int = 200
28+
conn = pymysql.connect(host=_DB_HOST, user=_DB_USER, password=_DB_PASS, database=_DB_NAME)
29+
if self.in_path(_SUCCESS):
30+
cur = conn.cursor()
31+
cur.execute("DROP TABLE IF EXISTS test_table")
32+
cur.close()
33+
status_code = 200
34+
elif self.in_path(_FAULT):
35+
cur = conn.cursor()
36+
try:
37+
cur.execute("SELECT DISTINCT id, name FROM invalid_table")
38+
except pymysql.MySQLError as exception:
39+
print("Expected Exception with Invalid SQL occurred:", exception)
40+
status_code = 500
41+
except Exception as exception: # pylint: disable=broad-except
42+
print("Exception Occurred:", exception)
43+
else:
44+
status_code = 200
45+
finally:
46+
cur.close()
47+
else:
48+
status_code = 404
49+
conn.close()
50+
self.send_response_only(status_code)
51+
self.end_headers()
52+
53+
def in_path(self, sub_path: str):
54+
return sub_path in self.path
55+
56+
57+
def main() -> None:
58+
server_address: Tuple[str, int] = ("0.0.0.0", _PORT)
59+
request_handler_class: type = RequestHandler
60+
requests_server: ThreadingHTTPServer = ThreadingHTTPServer(server_address, request_handler_class)
61+
atexit.register(requests_server.shutdown)
62+
server_thread: Thread = Thread(target=requests_server.serve_forever)
63+
server_thread.start()
64+
print("Ready")
65+
server_thread.join()
66+
67+
68+
if __name__ == "__main__":
69+
main()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
name = "pymysql-server"
3+
description = "Simple server that relies on pymysql library"
4+
version = "1.0.0"
5+
license = "Apache-2.0"
6+
requires-python = ">=3.8"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
opentelemetry-distro==0.43b0
2+
opentelemetry-exporter-otlp-proto-grpc==1.22.0
3+
typing-extensions==4.9.0
4+
pymysql==1.1.1
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Dict, List
4+
5+
from mock_collector_client import ResourceScopeMetric, ResourceScopeSpan
6+
from testcontainers.mysql import MySqlContainer
7+
from typing_extensions import override
8+
9+
from amazon.base.contract_test_base import NETWORK_NAME, ContractTestBase
10+
from amazon.utils.application_signals_constants import (
11+
AWS_LOCAL_OPERATION,
12+
AWS_LOCAL_SERVICE,
13+
AWS_REMOTE_OPERATION,
14+
AWS_REMOTE_RESOURCE_IDENTIFIER,
15+
AWS_REMOTE_RESOURCE_TYPE,
16+
AWS_REMOTE_SERVICE,
17+
AWS_SPAN_KIND,
18+
)
19+
from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue
20+
from opentelemetry.proto.metrics.v1.metrics_pb2 import ExponentialHistogramDataPoint, Metric
21+
from opentelemetry.proto.trace.v1.trace_pb2 import Span
22+
from opentelemetry.trace import StatusCode
23+
24+
25+
class PyMysqlTest(ContractTestBase):
26+
@override
27+
@classmethod
28+
def set_up_dependency_container(cls) -> None:
29+
cls.container = (
30+
MySqlContainer(MYSQL_USER="dbuser", MYSQL_PASSWORD="example", MYSQL_DATABASE="testdb")
31+
.with_kwargs(network=NETWORK_NAME)
32+
.with_name("mydb")
33+
)
34+
cls.container.start()
35+
36+
@override
37+
@classmethod
38+
def tear_down_dependency_container(cls) -> None:
39+
cls.container.stop()
40+
41+
@override
42+
def get_application_extra_environment_variables(self) -> Dict[str, str]:
43+
return {
44+
"DB_HOST": "mydb",
45+
"DB_USER": "dbuser",
46+
"DB_PASS": "example",
47+
"DB_NAME": "testdb",
48+
}
49+
50+
@override
51+
def get_application_image_name(self) -> str:
52+
return "aws-application-signals-tests-pymysql-app"
53+
54+
def test_success(self) -> None:
55+
self.mock_collector_client.clear_signals()
56+
self.do_test_requests("success", "GET", 200, 0, 0, sql_command="DROP TABLE")
57+
58+
def test_fault(self) -> None:
59+
self.mock_collector_client.clear_signals()
60+
self.do_test_requests("fault", "GET", 500, 0, 1, sql_command="SELECT DISTINCT")
61+
62+
@override
63+
def _assert_aws_span_attributes(self, resource_scope_spans: List[ResourceScopeSpan], path: str, **kwargs) -> None:
64+
target_spans: List[Span] = []
65+
for resource_scope_span in resource_scope_spans:
66+
# pylint: disable=no-member
67+
if resource_scope_span.span.kind == Span.SPAN_KIND_CLIENT:
68+
target_spans.append(resource_scope_span.span)
69+
70+
self.assertEqual(len(target_spans), 1)
71+
self._assert_aws_attributes(target_spans[0].attributes, **kwargs)
72+
73+
@override
74+
def _assert_aws_attributes(self, attributes_list: List[KeyValue], **kwargs) -> None:
75+
attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list)
76+
self._assert_str_attribute(attributes_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name())
77+
# InternalOperation as OTEL does not instrument the basic server we are using, so the client span is a local
78+
# root.
79+
self._assert_str_attribute(attributes_dict, AWS_LOCAL_OPERATION, "InternalOperation")
80+
self._assert_str_attribute(attributes_dict, AWS_REMOTE_SERVICE, "mysql")
81+
command: str = kwargs.get("sql_command")
82+
self._assert_str_attribute(attributes_dict, AWS_REMOTE_OPERATION, f"{command}")
83+
self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_TYPE, "DB::Connection")
84+
self._assert_str_attribute(attributes_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, "testdb|mydb|3306")
85+
# See comment above AWS_LOCAL_OPERATION
86+
self._assert_str_attribute(attributes_dict, AWS_SPAN_KIND, "LOCAL_ROOT")
87+
88+
@override
89+
def _assert_semantic_conventions_span_attributes(
90+
self, resource_scope_spans: List[ResourceScopeSpan], method: str, path: str, status_code: int, **kwargs
91+
) -> None:
92+
target_spans: List[Span] = []
93+
for resource_scope_span in resource_scope_spans:
94+
# pylint: disable=no-member
95+
if resource_scope_span.span.kind == Span.SPAN_KIND_CLIENT:
96+
target_spans.append(resource_scope_span.span)
97+
98+
self.assertEqual(target_spans[0].name, kwargs.get("sql_command").split()[0])
99+
if status_code == 200:
100+
self.assertEqual(target_spans[0].status.code, StatusCode.UNSET.value)
101+
else:
102+
self.assertEqual(target_spans[0].status.code, StatusCode.ERROR.value)
103+
104+
self._assert_semantic_conventions_attributes(target_spans[0].attributes, kwargs.get("sql_command"))
105+
106+
def _assert_semantic_conventions_attributes(self, attributes_list: List[KeyValue], command: str) -> None:
107+
attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list)
108+
self.assertTrue(attributes_dict.get("db.statement").string_value.startswith(command))
109+
self._assert_str_attribute(attributes_dict, "db.system", "mysql")
110+
self._assert_str_attribute(attributes_dict, "db.name", "testdb")
111+
self._assert_str_attribute(attributes_dict, "net.peer.name", "mydb")
112+
self._assert_int_attribute(attributes_dict, "net.peer.port", 3306)
113+
self.assertTrue("server.address" not in attributes_dict)
114+
self.assertTrue("server.port" not in attributes_dict)
115+
self.assertTrue("db.operation" not in attributes_dict)
116+
117+
@override
118+
def _assert_metric_attributes(
119+
self, resource_scope_metrics: List[ResourceScopeMetric], metric_name: str, expected_sum: int, **kwargs
120+
) -> None:
121+
target_metrics: List[Metric] = []
122+
for resource_scope_metric in resource_scope_metrics:
123+
if resource_scope_metric.metric.name.lower() == metric_name.lower():
124+
target_metrics.append(resource_scope_metric.metric)
125+
126+
self.assertEqual(len(target_metrics), 1)
127+
target_metric: Metric = target_metrics[0]
128+
dp_list: List[ExponentialHistogramDataPoint] = target_metric.exponential_histogram.data_points
129+
130+
self.assertEqual(len(dp_list), 2)
131+
dependency_dp: ExponentialHistogramDataPoint = dp_list[0]
132+
service_dp: ExponentialHistogramDataPoint = dp_list[1]
133+
if len(dp_list[1].attributes) > len(dp_list[0].attributes):
134+
dependency_dp = dp_list[1]
135+
service_dp = dp_list[0]
136+
attribute_dict: Dict[str, AnyValue] = self._get_attributes_dict(dependency_dp.attributes)
137+
self._assert_str_attribute(attribute_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name())
138+
# See comment on AWS_LOCAL_OPERATION in _assert_aws_attributes
139+
self._assert_str_attribute(attribute_dict, AWS_LOCAL_OPERATION, "InternalOperation")
140+
self._assert_str_attribute(attribute_dict, AWS_REMOTE_SERVICE, "mysql")
141+
self._assert_str_attribute(attribute_dict, AWS_REMOTE_OPERATION, kwargs.get("sql_command"))
142+
self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_TYPE, "DB::Connection")
143+
self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, "testdb|mydb|3306")
144+
self._assert_str_attribute(attribute_dict, AWS_SPAN_KIND, "CLIENT")
145+
self.check_sum(metric_name, dependency_dp.sum, expected_sum)
146+
147+
attribute_dict: Dict[str, AnyValue] = self._get_attributes_dict(service_dp.attributes)
148+
# See comment on AWS_LOCAL_OPERATION in _assert_aws_attributes
149+
self._assert_str_attribute(attribute_dict, AWS_LOCAL_OPERATION, "InternalOperation")
150+
self._assert_str_attribute(attribute_dict, AWS_SPAN_KIND, "LOCAL_ROOT")
151+
self.check_sum(metric_name, service_dp.sum, expected_sum)

scripts/set-up-contract-tests.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ rm -rf dist/mock_collector*
1818
rm -rf dist/contract_tests*
1919

2020
# Install python dependency for contract-test
21+
pip install pymysql
22+
pip install cryptography
23+
2124
# To be clear, install binary for psycopg2 have no negative influence on otel here
2225
# since Otel-Instrumentation running in container that install psycopg2 from source
2326
pip install sqlalchemy psycopg2-binary

0 commit comments

Comments
 (0)