Skip to content

Commit e042cfe

Browse files
authored
Implemented Misc Contract Tests (#163)
*Description of changes:* Implement resource and configuration contract test comparable to [java tests](https://github.com/aws-observability/aws-otel-java-instrumentation/tree/main/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc) 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 61542d4 commit e042cfe

5 files changed

+231
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import time
4+
from typing import List
5+
6+
from mock_collector_client import ResourceScopeMetric, ResourceScopeSpan
7+
from requests import Response, request
8+
from typing_extensions import override
9+
10+
from amazon.base.contract_test_base import ContractTestBase
11+
from amazon.utils.application_signals_constants import ERROR_METRIC, FAULT_METRIC, LATENCY_METRIC
12+
from opentelemetry.sdk.metrics.export import AggregationTemporality
13+
14+
# Tests in this class are supposed to validate that the SDK was configured in the correct way: It
15+
# uses the X-Ray ID format. Metrics are deltaPreferred. Type of the metrics are exponentialHistogram
16+
17+
18+
class ConfigurationTest(ContractTestBase):
19+
@override
20+
def get_application_image_name(self) -> str:
21+
return "aws-application-signals-tests-django-app"
22+
23+
@override
24+
def get_application_wait_pattern(self) -> str:
25+
return "Quit the server with CONTROL-C."
26+
27+
@override
28+
def get_application_extra_environment_variables(self):
29+
return {"DJANGO_SETTINGS_MODULE": "django_server.settings"}
30+
31+
def test_configuration_metrics(self):
32+
address: str = self.application.get_container_host_ip()
33+
port: str = self.application.get_exposed_port(self.get_application_port())
34+
url: str = f"http://{address}:{port}/success"
35+
response: Response = request("GET", url, timeout=20)
36+
self.assertEqual(200, response.status_code)
37+
metrics: List[ResourceScopeMetric] = self.mock_collector_client.get_metrics(
38+
{LATENCY_METRIC, ERROR_METRIC, FAULT_METRIC}
39+
)
40+
41+
self.assertEqual(len(metrics), 3)
42+
for metric in metrics:
43+
self.assertIsNotNone(metric.metric.exponential_histogram)
44+
self.assertEqual(metric.metric.exponential_histogram.aggregation_temporality, AggregationTemporality.DELTA)
45+
46+
def test_xray_id_format(self):
47+
"""
48+
We are testing here that the X-Ray id format is always used by inspecting the traceid that
49+
was in the span received by the collector, which should be consistent across multiple spans.
50+
We are testing the following properties:
51+
1. Traceid is random
52+
2. First 32 bits of traceid is a timestamp
53+
It is important to remember that the X-Ray traceId format had to be adapted to fit into the
54+
definition of the OpenTelemetry traceid:
55+
https://opentelemetry.io/docs/specs/otel/trace/api/#retrieving-the-traceid-and-spanid
56+
Specifically for an X-Ray traceid to be a valid Otel traceId, the version digit had to be
57+
dropped. Reference:
58+
https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_id_generator.py
59+
"""
60+
61+
seen: List[str] = []
62+
for _ in range(100):
63+
address: str = self.application.get_container_host_ip()
64+
port: str = self.application.get_exposed_port(self.get_application_port())
65+
url: str = f"http://{address}:{port}/success"
66+
response: Response = request("GET", url, timeout=20)
67+
self.assertEqual(200, response.status_code)
68+
69+
# Since we just made the request, the time in epoch registered in the traceid should be
70+
# approximate equal to the current time in the test, since both run on the same host.
71+
start_time_sec: int = int(time.time())
72+
73+
resource_scope_spans: List[ResourceScopeSpan] = self.mock_collector_client.get_traces()
74+
target_span: ResourceScopeSpan = resource_scope_spans[0]
75+
self.assertEqual(target_span.span.name, "GET success")
76+
77+
self.assertTrue(target_span.span.trace_id.hex() not in seen)
78+
seen.append(target_span.span.trace_id.hex())
79+
80+
# trace_id is bytes, so we convert it to hex string and pick the first 8 byte
81+
# that represent the timestamp, then convert it to int for timestamp in second
82+
trace_id_time_stamp_int: int = int(target_span.span.trace_id.hex()[:8], 16)
83+
84+
# Give 2 minutes time range of tolerance for the trace timestamp
85+
self.assertGreater(trace_id_time_stamp_int, start_time_sec - 60)
86+
self.assertGreater(start_time_sec + 60, trace_id_time_stamp_int)
87+
self.mock_collector_client.clear_signals()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 requests import Response, request
7+
from typing_extensions import override
8+
9+
from amazon.base.contract_test_base import ContractTestBase
10+
from amazon.utils.application_signals_constants import ERROR_METRIC, FAULT_METRIC, LATENCY_METRIC
11+
from opentelemetry.proto.common.v1.common_pb2 import AnyValue
12+
from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric
13+
from opentelemetry.proto.trace.v1.trace_pb2 import Span
14+
15+
16+
def _get_k8s_attributes():
17+
return {
18+
"k8s.namespace.name": "namespace-name",
19+
"k8s.pod.name": "pod-name",
20+
"k8s.deployment.name": "deployment-name",
21+
}
22+
23+
24+
# Tests consuming this class are supposed to validate that the agent is able to get the resource
25+
# attributes through the environment variables OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME
26+
#
27+
# These tests are structured with nested classes since it is only possible to change the
28+
# resource attributes during the initialization of the OpenTelemetry SDK.
29+
30+
31+
class ResourceAttributesTest(ContractTestBase):
32+
@override
33+
def get_application_image_name(self) -> str:
34+
return "aws-application-signals-tests-django-app"
35+
36+
@override
37+
def get_application_wait_pattern(self) -> str:
38+
return "Quit the server with CONTROL-C."
39+
40+
@override
41+
def get_application_extra_environment_variables(self):
42+
return {"DJANGO_SETTINGS_MODULE": "django_server.settings"}
43+
44+
def do_test_resource_attributes(self, service_name):
45+
address: str = self.application.get_container_host_ip()
46+
port: str = self.application.get_exposed_port(self.get_application_port())
47+
url: str = f"http://{address}:{port}/success"
48+
response: Response = request("GET", url, timeout=20)
49+
self.assertEqual(200, response.status_code)
50+
self.assert_resource_attributes(service_name)
51+
52+
def assert_resource_attributes(self, service_name):
53+
resource_scope_spans: List[ResourceScopeSpan] = self.mock_collector_client.get_traces()
54+
metrics: List[ResourceScopeMetric] = self.mock_collector_client.get_metrics(
55+
{LATENCY_METRIC, ERROR_METRIC, FAULT_METRIC}
56+
)
57+
target_spans: List[Span] = []
58+
for resource_scope_span in resource_scope_spans:
59+
# pylint: disable=no-member
60+
if resource_scope_span.span.name == "GET success":
61+
target_spans.append(resource_scope_span.resource_spans)
62+
63+
self.assertEqual(len(target_spans), 1)
64+
attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(target_spans[0].resource.attributes)
65+
for key, value in _get_k8s_attributes().items():
66+
self._assert_str_attribute(attributes_dict, key, value)
67+
self._assert_str_attribute(attributes_dict, "service.name", service_name)
68+
69+
target_metrics: List[Metric] = []
70+
for resource_scope_metric in metrics:
71+
if resource_scope_metric.metric.name in ["Error", "Fault", "Latency"]:
72+
target_metrics.append(resource_scope_metric.resource_metrics)
73+
self.assertEqual(len(target_metrics), 3)
74+
for target_metric in target_metrics:
75+
metric_attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(target_metric.resource.attributes)
76+
for key, value in _get_k8s_attributes().items():
77+
self._assert_str_attribute(metric_attributes_dict, key, value)
78+
self._assert_str_attribute(metric_attributes_dict, "service.name", service_name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import List
4+
5+
from resource_attributes_test_base import ResourceAttributesTest, _get_k8s_attributes
6+
from typing_extensions import override
7+
8+
9+
class ServiceNameInEnvVarTest(ResourceAttributesTest):
10+
11+
@override
12+
# pylint: disable=no-self-use
13+
def get_application_extra_environment_variables(self) -> str:
14+
return {"DJANGO_SETTINGS_MODULE": "django_server.settings", "OTEL_SERVICE_NAME": "service-name-test"}
15+
16+
@override
17+
# pylint: disable=no-self-use
18+
def get_application_otel_resource_attributes(self) -> str:
19+
pairlist: List[str] = []
20+
for key, value in _get_k8s_attributes().items():
21+
pairlist.append(key + "=" + value)
22+
return ",".join(pairlist)
23+
24+
def test_service(self) -> None:
25+
self.do_test_resource_attributes("service-name-test")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import List
4+
5+
from resource_attributes_test_base import ResourceAttributesTest, _get_k8s_attributes
6+
from typing_extensions import override
7+
8+
9+
class ServiceNameInResourceAttributesTest(ResourceAttributesTest):
10+
11+
@override
12+
# pylint: disable=no-self-use
13+
def get_application_otel_resource_attributes(self) -> str:
14+
pairlist: List[str] = []
15+
for key, value in _get_k8s_attributes().items():
16+
pairlist.append(key + "=" + value)
17+
pairlist.append("service.name=service-name")
18+
return ",".join(pairlist)
19+
20+
def test_service(self) -> None:
21+
self.do_test_resource_attributes("service-name")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import List
4+
5+
from resource_attributes_test_base import ResourceAttributesTest, _get_k8s_attributes
6+
from typing_extensions import override
7+
8+
9+
class UnknownServiceNameTest(ResourceAttributesTest):
10+
11+
@override
12+
# pylint: disable=no-self-use
13+
def get_application_otel_resource_attributes(self) -> str:
14+
pairlist: List[str] = []
15+
for key, value in _get_k8s_attributes().items():
16+
pairlist.append(key + "=" + value)
17+
return ",".join(pairlist)
18+
19+
def test_service(self) -> None:
20+
self.do_test_resource_attributes("unknown_service")

0 commit comments

Comments
 (0)