Skip to content

Commit 1753bbf

Browse files
authored
Adds contract tests for mysqlclient with current behavior (#219)
*Issue #, if available:* N/A *Description of changes:* Adding contract tests for mysqlclient adapter. Similar to the [psycopg2](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/main/contract-tests/tests/test/amazon/psycopg2/psycopg2_test.py) and [pymysql](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/main/contract-tests/tests/test/amazon/pymysql/pymysql_test.py) adapters. Mysqlclient adapter is not fully supported in the upstream repository for OTEL, so we are adding assertions for the current behavior. Contract tests running successfully: ``` $ ./scripts/build_and_install_distro.sh ... $ ./scripts/set-up-contract-tests.sh ... $ pytest contract-tests/tests 2024-06-26 16:57:13 [ INFO] Waiting to be ready... (waiting_utils.py:46) ============================= test session starts ============================== platform linux -- Python 3.8.11, pytest-7.1.3, pluggy-1.5.0 rootdir: /local/home/gbochile/aws-otel-python-instrumentation/contract-tests/tests, configfile: pyproject.toml plugins: cov-4.1.0, flaky-3.7.0 collected 42 items ... contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py::MysqlClientTest::test_create_database_succeeds -------------------------------- live log setup -------------------------------- 2024-06-26 16:56:02 [ INFO] Pulling image aws-application-signals-mock-collector-python (container.py:53) 2024-06-26 16:56:03 [ INFO] Container started: 452136cdf3a2 (container.py:64) 2024-06-26 16:56:04 [ INFO] Pulling image mysql:latest (container.py:53) 2024-06-26 16:56:04 [ INFO] Container started: 68a6851341d0 (container.py:64) 2024-06-26 16:56:04 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:04 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:05 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:06 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:07 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:08 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:09 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:10 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:11 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:12 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:13 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:14 [ INFO] Waiting to be ready... (waiting_utils.py:46) -------------------------------- live log call --------------------------------- 2024-06-26 16:56:14 [ INFO] Pulling image aws-application-signals-tests-mysqlclient-app (container.py:53) 2024-06-26 16:56:15 [ INFO] Container started: 75a9b9c3d8db (container.py:64) 2024-06-26 16:56:21 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:21 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:21 [ INFO] Application stdout (contract_test_base.py:118) 2024-06-26 16:56:21 [ INFO] Ready (contract_test_base.py:119) 2024-06-26 16:56:21 [ INFO] Application stderr (contract_test_base.py:120) 2024-06-26 16:56:21 [ INFO] 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' AwsEcsResourceDetector failed: Missing ECS_CONTAINER_METADATA_URI therefore process is not on ECS. Exception in detector <opentelemetry.sdk.extension.aws.resource.ec2.AwsEc2ResourceDetector object at 0x7f7b60d8fa90>, ignoring AwsEc2ResourceDetector failed: timed out Configuration of configurator not loaded, aws_configurator already loaded (contract_test_base.py:121) PASSED contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py::MysqlClientTest::test_drop_table_succeeds -------------------------------- live log call --------------------------------- 2024-06-26 16:56:22 [ INFO] Pulling image aws-application-signals-tests-mysqlclient-app (container.py:53) 2024-06-26 16:56:22 [ INFO] Container started: 2ecce860cd73 (container.py:64) 2024-06-26 16:56:28 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:29 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:29 [ INFO] Application stdout (contract_test_base.py:118) 2024-06-26 16:56:29 [ INFO] Ready (contract_test_base.py:119) 2024-06-26 16:56:29 [ INFO] Application stderr (contract_test_base.py:120) 2024-06-26 16:56:29 [ 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 0x7f7d566d7a00>, ignoring AwsEc2ResourceDetector failed: timed out Configuration of configurator not loaded, aws_configurator already loaded (contract_test_base.py:121) PASSED [ 69%] contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py::MysqlClientTest::test_fault -------------------------------- live log call --------------------------------- 2024-06-26 16:56:29 [ INFO] Pulling image aws-application-signals-tests-mysqlclient-app (container.py:53) 2024-06-26 16:56:30 [ INFO] Container started: 20446879f214 (container.py:64) 2024-06-26 16:56:36 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:36 [ INFO] Waiting to be ready... (waiting_utils.py:46) 2024-06-26 16:56:36 [ INFO] Application stdout (contract_test_base.py:118) 2024-06-26 16:56:36 [ INFO] Ready Expected Exception with Invalid SQL occurred: (1146, "Table 'testdb.invalid_table' doesn't exist") (contract_test_base.py:119) 2024-06-26 16:56:36 [ INFO] Application stderr (contract_test_base.py:120) 2024-06-26 16:56:36 [ 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 0x7efc2e775450>, ignoring AwsEc2ResourceDetector failed: timed out Configuration of configurator not loaded, aws_configurator already loaded (contract_test_base.py:121) PASSED ... PASSED [100%] ------------------------------ live log teardown ------------------------------- 2024-06-26 16:58:30 [ INFO] MockCollector stdout (contract_test_base.py:71) 2024-06-26 16:58:30 [ INFO] Ready (contract_test_base.py:72) 2024-06-26 16:58:30 [ INFO] MockCollector stderr (contract_test_base.py:73) 2024-06-26 16:58:30 [ INFO] (contract_test_base.py:74) ======================== 42 passed in 407.01s (0:06:47) ======================== ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent eac2299 commit 1753bbf

File tree

12 files changed

+223
-30
lines changed

12 files changed

+223
-30
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,6 @@ def _build_db_connection(address: str, port: int) -> Optional[str]:
465465

466466

467467
def _build_db_connection_string(connection_string: str) -> Optional[str]:
468-
469468
uri = urlparse(connection_string)
470469
address = uri.hostname
471470
try:

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ def apply_instrumentation_patches() -> None:
3535
from gevent import monkey
3636

3737
if gevent_patch_module == "all":
38-
3938
monkey.patch_all()
4039
else:
4140
module_list = [module.strip() for module in gevent_patch_module.split(",")]

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/sampler/_rate_limiter.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ def try_spend(self, cost: float) -> bool:
3131
with self.__lock:
3232
wallet_ceiling_millis = Decimal(self._clock.now().timestamp() * 1000.0)
3333
current_balance_millis = wallet_ceiling_millis - self.__wallet_floor_millis
34-
if current_balance_millis > self.MAX_BALANCE_MILLIS:
35-
current_balance_millis = self.MAX_BALANCE_MILLIS
36-
34+
current_balance_millis = min(current_balance_millis, self.MAX_BALANCE_MILLIS)
3735
pending_remaining_balance_millis = current_balance_millis - cost_in_millis
3836
if pending_remaining_balance_millis >= 0:
3937
self.__wallet_floor_millis = wallet_ceiling_millis - pending_remaining_balance_millis
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 /mysqlclient
6+
COPY ./dist/$DISTRO /mysqlclient
7+
COPY ./contract-tests/images/applications/mysqlclient /mysqlclient
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", "./mysqlclient_server.py"]
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 MySQLdb
10+
from MySQLdb import ProgrammingError
11+
from typing_extensions import override
12+
13+
_PORT: int = 8080
14+
_DROP_TABLE: str = "drop_table"
15+
_ERROR: str = "error"
16+
_FAULT: str = "fault"
17+
_CREATE_DATABASE: str = "create_database"
18+
19+
_DB_HOST = os.getenv("DB_HOST")
20+
_DB_USER = os.getenv("DB_USER")
21+
_DB_PASS = os.getenv("DB_PASS")
22+
_DB_NAME = os.getenv("DB_NAME")
23+
24+
25+
class RequestHandler(BaseHTTPRequestHandler):
26+
@override
27+
# pylint: disable=invalid-name
28+
def do_GET(self):
29+
status_code: int = 200
30+
conn = MySQLdb.connect(database=_DB_NAME, user=_DB_USER, password=_DB_PASS, host=_DB_HOST)
31+
conn.autocommit = True # CREATE DATABASE cannot run in a transaction block
32+
if self.in_path(_DROP_TABLE):
33+
cur = conn.cursor()
34+
cur.execute("DROP TABLE IF EXISTS test_table")
35+
cur.close()
36+
status_code = 200
37+
elif self.in_path(_CREATE_DATABASE):
38+
cur = conn.cursor()
39+
cur.execute("CREATE DATABASE test_database")
40+
cur.close()
41+
status_code = 200
42+
elif self.in_path(_FAULT):
43+
cur = conn.cursor()
44+
try:
45+
cur.execute("SELECT DISTINCT id, name FROM invalid_table")
46+
except ProgrammingError as exception:
47+
print("Expected Exception with Invalid SQL occurred:", exception)
48+
status_code = 500
49+
except Exception as exception: # pylint: disable=broad-except
50+
print("Exception Occurred:", exception)
51+
else:
52+
status_code = 200
53+
finally:
54+
cur.close()
55+
else:
56+
status_code = 404
57+
conn.close()
58+
self.send_response_only(status_code)
59+
self.end_headers()
60+
61+
def in_path(self, sub_path: str):
62+
return sub_path in self.path
63+
64+
65+
def main() -> None:
66+
server_address: Tuple[str, int] = ("0.0.0.0", _PORT)
67+
request_handler_class: type = RequestHandler
68+
requests_server: ThreadingHTTPServer = ThreadingHTTPServer(server_address, request_handler_class)
69+
atexit.register(requests_server.shutdown)
70+
server_thread: Thread = Thread(target=requests_server.serve_forever)
71+
server_thread.start()
72+
print("Ready")
73+
server_thread.join()
74+
75+
76+
if __name__ == "__main__":
77+
main()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
name = "mysqlclient-server"
3+
description = "Simple server that relies on mysqlclient 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.46b0
2+
opentelemetry-exporter-otlp-proto-grpc==1.25.0
3+
typing-extensions==4.9.0
4+
mysqlclient==2.2.4

contract-tests/tests/test/amazon/base/database_contract_test_base.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
from opentelemetry.trace import StatusCode
2323

2424
DATABASE_HOST: str = "mydb"
25-
DATABASE_USER: str = "root"
26-
DATABASE_PASSWORD: str = "example"
2725
DATABASE_NAME: str = "testdb"
26+
DATABASE_PASSWORD: str = "example"
27+
DATABASE_USER: str = "root"
28+
SPAN_KIND_CLIENT: str = "CLIENT"
29+
SPAN_KIND_LOCAL_ROOT: str = "LOCAL_ROOT"
2830

2931

3032
class DatabaseContractTestBase(ContractTestBase):
@@ -68,7 +70,9 @@ def _assert_aws_span_attributes(self, resource_scope_spans: List[ResourceScopeSp
6870
if resource_scope_span.span.kind == Span.SPAN_KIND_CLIENT:
6971
target_spans.append(resource_scope_span.span)
7072

71-
self.assertEqual(len(target_spans), 1)
73+
self.assertEqual(
74+
len(target_spans), 1, f"target_spans is {str(target_spans)}, although only one walue was expected"
75+
)
7276
self._assert_aws_attributes(target_spans[0].attributes, **kwargs)
7377

7478
@override
@@ -101,7 +105,9 @@ def _assert_semantic_conventions_attributes(self, attributes_list: List[KeyValue
101105
self.assertTrue("db.operation" not in attributes_dict)
102106

103107
@override
104-
def _assert_aws_attributes(self, attributes_list: List[KeyValue], **kwargs) -> None:
108+
def _assert_aws_attributes(
109+
self, attributes_list: List[KeyValue], expected_span_kind: str = SPAN_KIND_LOCAL_ROOT, **kwargs
110+
) -> None:
105111
attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list)
106112
self._assert_str_attribute(attributes_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name())
107113
# InternalOperation as OTEL does not instrument the basic server we are using, so the client span is a local
@@ -115,7 +121,7 @@ def _assert_aws_attributes(self, attributes_list: List[KeyValue], **kwargs) -> N
115121
attributes_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, self.get_remote_resource_identifier()
116122
)
117123
# See comment above AWS_LOCAL_OPERATION
118-
self._assert_str_attribute(attributes_dict, AWS_SPAN_KIND, "LOCAL_ROOT")
124+
self._assert_str_attribute(attributes_dict, AWS_SPAN_KIND, expected_span_kind)
119125

120126
@override
121127
def _assert_metric_attributes(
@@ -125,32 +131,25 @@ def _assert_metric_attributes(
125131
for resource_scope_metric in resource_scope_metrics:
126132
if resource_scope_metric.metric.name.lower() == metric_name.lower():
127133
target_metrics.append(resource_scope_metric.metric)
128-
129-
self.assertEqual(len(target_metrics), 1)
130-
target_metric: Metric = target_metrics[0]
131-
dp_list: List[ExponentialHistogramDataPoint] = target_metric.exponential_histogram.data_points
132-
134+
self.assertLessEqual(
135+
len(target_metrics),
136+
2,
137+
f"target_metrics is {str(target_metrics)}, although we expect less than or equal to 2 metrics",
138+
)
139+
dp_list: List[ExponentialHistogramDataPoint] = [
140+
dp for target_metric in target_metrics for dp in target_metric.exponential_histogram.data_points
141+
]
133142
self.assertEqual(len(dp_list), 2)
134143
dependency_dp: ExponentialHistogramDataPoint = dp_list[0]
135144
service_dp: ExponentialHistogramDataPoint = dp_list[1]
136145
if len(dp_list[1].attributes) > len(dp_list[0].attributes):
137146
dependency_dp = dp_list[1]
138147
service_dp = dp_list[0]
139-
attribute_dict: Dict[str, AnyValue] = self._get_attributes_dict(dependency_dp.attributes)
140-
self._assert_str_attribute(attribute_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name())
141-
# See comment on AWS_LOCAL_OPERATION in _assert_aws_attributes
142-
self._assert_str_attribute(attribute_dict, AWS_LOCAL_OPERATION, "InternalOperation")
143-
self._assert_str_attribute(attribute_dict, AWS_REMOTE_SERVICE, self.get_remote_service())
144-
self._assert_str_attribute(attribute_dict, AWS_REMOTE_OPERATION, kwargs.get("sql_command"))
145-
self._assert_str_attribute(attribute_dict, AWS_REMOTE_RESOURCE_TYPE, "DB::Connection")
146-
self._assert_str_attribute(
147-
attribute_dict, AWS_REMOTE_RESOURCE_IDENTIFIER, self.get_remote_resource_identifier()
148-
)
149-
self._assert_str_attribute(attribute_dict, AWS_SPAN_KIND, "CLIENT")
148+
self._assert_aws_attributes(dependency_dp.attributes, SPAN_KIND_CLIENT, **kwargs)
150149
self.check_sum(metric_name, dependency_dp.sum, expected_sum)
151150

152151
attribute_dict: Dict[str, AnyValue] = self._get_attributes_dict(service_dp.attributes)
153152
# See comment on AWS_LOCAL_OPERATION in _assert_aws_attributes
154153
self._assert_str_attribute(attribute_dict, AWS_LOCAL_OPERATION, "InternalOperation")
155-
self._assert_str_attribute(attribute_dict, AWS_SPAN_KIND, "LOCAL_ROOT")
154+
self._assert_str_attribute(attribute_dict, AWS_SPAN_KIND, SPAN_KIND_LOCAL_ROOT)
156155
self.check_sum(metric_name, service_dp.sum, expected_sum)

contract-tests/tests/test/amazon/misc/service_name_in_env_var_test.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88

99
class ServiceNameInEnvVarTest(ResourceAttributesTest):
10-
1110
@override
1211
# pylint: disable=no-self-use
1312
def get_application_extra_environment_variables(self) -> str:

contract-tests/tests/test/amazon/misc/service_name_in_resource_attributes_test.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88

99
class ServiceNameInResourceAttributesTest(ResourceAttributesTest):
10-
1110
@override
1211
# pylint: disable=no-self-use
1312
def get_application_otel_resource_attributes(self) -> str:

0 commit comments

Comments
 (0)