-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NH-11358 Add integration tests #67
Merged
Merged
Changes from 64 commits
Commits
Show all changes
69 commits
Select commit
Hold shift + click to select a range
ead50fc
WIP mess
tammy-baylis-swi 52f3b26
Add unittest mock of get_current_span() and get_span_context() as cal…
tammy-baylis-swi cec6d5e
Add some scenario 4 assertions
tammy-baylis-swi 9c8626a
Change mock values for less confusion
tammy-baylis-swi a9f78ef
Add experimental manual_propagation test
tammy-baylis-swi 2a8a26b
Add bare-bones Flask app, some S4 asserts
tammy-baylis-swi 7e0cb2e
Simplify scenario4 test to compare inject to span contents and rename
tammy-baylis-swi 8da6daf
Clean up TestFunctionalSpanAttributes experiment
tammy-baylis-swi 3c004b0
Rm unknown (un)instrument calls in test
tammy-baylis-swi 4eba91a
Merge branch 'main' into NH-11358-functional-tests
tammy-baylis-swi 744de2c
Move functional tests to own dir
tammy-baylis-swi 9ebd0d9
Rename two tests
tammy-baylis-swi aae43f5
Fix InMemorySpanExporter setup and use
tammy-baylis-swi 36d94cd
Add test_non_root_attrs_only_do_sample
tammy-baylis-swi 79bf019
Add test_child_span_attrs
tammy-baylis-swi bb64e5b
Add test_root_span_attrs_with_traceparent_and_tracestate
tammy-baylis-swi f681b91
Rm confusing test_non_root_attrs_only_do_sample and test_internal_spa…
tammy-baylis-swi 9f09b2b
Add stub todo
tammy-baylis-swi 431bd12
Bugfix: typo attributeerror
tammy-baylis-swi 54aafb3
Add test_root_span_attrs_with_signed_trigger_trace
tammy-baylis-swi bf9e6c8
Rename test class, update comment
tammy-baylis-swi f0dc9ea
TestHeaderPropagation redesigned. Added test_injection_with_existing_…
tammy-baylis-swi 77d18ba
Add test_injection_with_existing_traceparent_tracestate_not_sampled
tammy-baylis-swi 5f85adc
Add test_injection_new_decision
tammy-baylis-swi ba5f997
Add test_injection_signed_tt
tammy-baylis-swi af1c9b5
Rm unused imports, update comment
tammy-baylis-swi b73ac49
Add SolarWindsDistroTestBase and refactor
tammy-baylis-swi 9beb8d2
Fix mock decision retvals, add test_root_span_attrs_not_sampled
tammy-baylis-swi c801858
Add helper_test_root_span_attrs and refactor
tammy-baylis-swi 94a8754
Update comments
tammy-baylis-swi ad485d1
Merge branch 'main' into NH-11358-functional-tests
tammy-baylis-swi 59cc1b0
Rename folder. Update readme
tammy-baylis-swi 8b1b7d5
Merge branch 'main' into NH-11358-functional-tests
tammy-baylis-swi da545ea
WIPPP
tammy-baylis-swi d5d02c4
WIP - Flask exports to InMemory
tammy-baylis-swi dd9d4d6
Add (wip) combined scenario 1 test
tammy-baylis-swi e7be4fe
(WIP) This uses SwSampler but doesn't export spans to memory
tammy-baylis-swi 8d8b611
Merge branch 'main' into redesign-integration-tests
tammy-baylis-swi 7c64c6d
Merge branch 'NH-25812-otel-1-14-0' into redesign-integration-tests
tammy-baylis-swi 74501e5
(WIP) Both sampler and propagators used with InMemExporter
tammy-baylis-swi 73086b7
Add scenario 1 tracestate header and span id check
tammy-baylis-swi c467c8e
Reorganize setup/teardown
tammy-baylis-swi cfe4084
Adjust scenario1 assertions
tammy-baylis-swi 7bf4af3
Add scenario4 sampled/not sampled
tammy-baylis-swi 263817a
Add scenario6
tammy-baylis-swi 76fd9f6
Rm propagation_test_app_v02
tammy-baylis-swi d74ad2a
Mv to TestBaseSwHeadersAndAttributes
tammy-baylis-swi dbe9b82
TestBaseSw class and separate scenario classes
tammy-baylis-swi 471d1a7
Rm old integration tests and httpx inst dependency
tammy-baylis-swi fc3156c
Add span tracestate checks
tammy-baylis-swi 95cd700
assert is not None
tammy-baylis-swi 820e1ae
Update comments
tammy-baylis-swi b387d44
Simplify regex use
tammy-baylis-swi 2964d1a
Mock signature for tt test scenario
tammy-baylis-swi b74eb3a
Scenario6 does not extract traceparent, tt unsigned
tammy-baylis-swi e7688b1
Update comment
tammy-baylis-swi 06da4b4
Add scenario8 tests
tammy-baylis-swi 7518439
Update comment wording for true root spans
tammy-baylis-swi 9d90f20
Add checks for other some-header interference
tammy-baylis-swi 397b877
Add continue decision propagation checks
tammy-baylis-swi aa31cc2
Merge pull request #77 from appoptics/redesign-integration-tests
tammy-baylis-swi 1ed26a6
Update comment
tammy-baylis-swi 3e893bd
Update comment
tammy-baylis-swi 6ccb8f9
Merge branch 'main' into NH-11358-functional-tests
tammy-baylis-swi f327539
Fix scenario_6 docstring on x-trace-options-response
tammy-baylis-swi cce07b4
Update comment
tammy-baylis-swi b55984c
Rm unnecessary todos
tammy-baylis-swi 20a8de4
Update more comment
tammy-baylis-swi ea5d5d8
Rm unnecessary tox service key env vars
tammy-baylis-swi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 |
---|---|---|
@@ -1,5 +1,9 @@ | ||
opentelemetry-test-utils==0.35b0 | ||
opentelemetry-test-utils~=0.35b0 | ||
opentelemetry-instrumentation-flask~=0.35b0 | ||
opentelemetry-instrumentation-requests~=0.35b0 | ||
pytest | ||
pytest-cov | ||
pytest-mock | ||
requests | ||
requests | ||
flask | ||
werkzeug |
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
Empty file.
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,135 @@ | ||
import os | ||
from pkg_resources import ( | ||
iter_entry_points, | ||
load_entry_point | ||
) | ||
import re | ||
|
||
import flask | ||
import requests | ||
from werkzeug.test import Client | ||
from werkzeug.wrappers import Response | ||
|
||
from opentelemetry import trace as trace_api | ||
from opentelemetry.instrumentation.flask import FlaskInstrumentor | ||
from opentelemetry.instrumentation.requests import RequestsInstrumentor | ||
from opentelemetry.propagate import get_global_textmap | ||
from opentelemetry.sdk.trace import TracerProvider, export | ||
from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( | ||
InMemorySpanExporter, | ||
) | ||
from opentelemetry.test.globals_test import reset_trace_globals | ||
from opentelemetry.test.test_base import TestBase | ||
|
||
from solarwinds_apm.apm_config import SolarWindsApmConfig | ||
from solarwinds_apm.configurator import SolarWindsConfigurator | ||
from solarwinds_apm.distro import SolarWindsDistro | ||
from solarwinds_apm.propagator import SolarWindsPropagator | ||
from solarwinds_apm.sampler import ParentBasedSwSampler | ||
|
||
|
||
class TestBaseSwHeadersAndAttributes(TestBase): | ||
""" | ||
Base class for testing SolarWinds custom distro header propagation | ||
and span attributes calculation from decision and headers. | ||
""" | ||
|
||
SW_SETTINGS_KEYS = [ | ||
"BucketCapacity", | ||
"BucketRate", | ||
"SampleRate", | ||
"SampleSource" | ||
] | ||
|
||
@staticmethod | ||
def _test_trace(): | ||
incoming_headers = {} | ||
for k, v in flask.request.headers.items(): | ||
# WSGI capitalizes incoming HTTP headers | ||
incoming_headers.update({k.lower(): v.lower()}) | ||
|
||
resp = requests.get(f"http://postman-echo.com/headers") | ||
|
||
# The return type must be a string, dict, tuple, Response instance, or WSGI callable | ||
# (not CaseInsensitiveDict) | ||
return { | ||
"traceparent": resp.request.headers["traceparent"], | ||
"tracestate": resp.request.headers["tracestate"], | ||
"incoming-headers": incoming_headers, | ||
} | ||
|
||
def _setup_endpoints(self): | ||
# pylint: disable=no-member | ||
self.app.route("/test_trace/")(self._test_trace) | ||
# pylint: disable=attribute-defined-outside-init | ||
self.client = Client(self.app, Response) | ||
|
||
def setUp(self): | ||
"""Set up called before each test scenario""" | ||
# Based on auto_instrumentation run() and sitecustomize.py | ||
# Load OTel env vars entry points | ||
argument_otel_environment_variable = {} | ||
for entry_point in iter_entry_points( | ||
"opentelemetry_environment_variables" | ||
): | ||
environment_variable_module = entry_point.load() | ||
for attribute in dir(environment_variable_module): | ||
if attribute.startswith("OTEL_"): | ||
argument = re.sub(r"OTEL_(PYTHON_)?", "", attribute).lower() | ||
argument_otel_environment_variable[argument] = attribute | ||
|
||
# Set APM service key - not valid, but we mock liboboe anyway | ||
os.environ["SW_APM_SERVICE_KEY"] = "foo:bar" | ||
|
||
# Load Distro | ||
SolarWindsDistro().configure() | ||
assert os.environ["OTEL_PROPAGATORS"] == "tracecontext,baggage,solarwinds_propagator" | ||
|
||
# Load Configurator to Configure SW custom SDK components | ||
# except use TestBase InMemorySpanExporter | ||
apm_config = SolarWindsApmConfig() | ||
configurator = SolarWindsConfigurator() | ||
configurator._initialize_solarwinds_reporter(apm_config) | ||
configurator._configure_propagator() | ||
configurator._configure_response_propagator() | ||
# This is done because set_tracer_provider cannot override the | ||
# current tracer provider. Has to be done here. | ||
reset_trace_globals() | ||
sampler = load_entry_point( | ||
"solarwinds_apm", | ||
"opentelemetry_traces_sampler", | ||
configurator._DEFAULT_SW_TRACES_SAMPLER | ||
)(apm_config) | ||
self.tracer_provider = TracerProvider(sampler=sampler) | ||
# Set InMemorySpanExporter for testing | ||
# We do NOT use SolarWindsSpanExporter | ||
self.memory_exporter = InMemorySpanExporter() | ||
span_processor = export.SimpleSpanProcessor(self.memory_exporter) | ||
self.tracer_provider.add_span_processor(span_processor) | ||
trace_api.set_tracer_provider(self.tracer_provider) | ||
self.tracer = self.tracer_provider.get_tracer(__name__) | ||
|
||
# Make sure SW SDK components were set | ||
propagators = get_global_textmap()._propagators | ||
assert len(propagators) == 3 | ||
assert isinstance(propagators[2], SolarWindsPropagator) | ||
assert isinstance(trace_api.get_tracer_provider().sampler, ParentBasedSwSampler) | ||
|
||
# We need to instrument and create test app for every test | ||
self.requests_inst = RequestsInstrumentor() | ||
self.flask_inst = FlaskInstrumentor() | ||
self.flask_inst.uninstrument() | ||
self.flask_inst.instrument( | ||
tracer_provider=trace_api.get_tracer_provider() | ||
) | ||
self.requests_inst.uninstrument() | ||
self.requests_inst.instrument( | ||
tracer_provider=trace_api.get_tracer_provider() | ||
) | ||
self.app = flask.Flask(__name__) | ||
self._setup_endpoints() | ||
self.client = Client(self.app, Response) | ||
|
||
def tearDown(self): | ||
"""Teardown called after each test scenario""" | ||
self.memory_exporter.clear() |
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,131 @@ | ||
import re | ||
import json | ||
|
||
from opentelemetry import trace as trace_api | ||
from unittest import mock | ||
|
||
from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes | ||
|
||
class TestScenario1(TestBaseSwHeadersAndAttributes): | ||
""" | ||
Test class for starting a new tracing decision with no input headers. | ||
""" | ||
|
||
def test_scenario_1_sampled(self): | ||
""" | ||
Scenario #1, sampled: | ||
1. Decision to sample is made at root/service entry span (mocked). There is no | ||
OTel context extracted from request headers, so this is the root and start | ||
of the trace. | ||
2. Headers in the original request are not altered by the SW propagator. | ||
3. Some traceparent and tracestate are injected into service's outgoing request | ||
(done by OTel TraceContextTextMapPropagator). | ||
4. Sampling-related attributes are set for the root/service entry span. | ||
5. The span_id of the outgoing request span matches the span_id portion in the | ||
tracestate header. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Love this docstring and your point 5 reminded me that this was never made clear in the W3C Trace context spec! I've updated the paragraph about tracestate accordingly. |
||
""" | ||
# Use in-process test app client and mock to propagate context | ||
# and create in-memory trace | ||
resp = None | ||
# liboboe mocked to guarantee return of "do_sample" (2nd arg) | ||
mock_decision = mock.Mock( | ||
return_value=(1, 1, 3, 4, 5.0, 6.0, 1, 0, "ok", "ok", 0) | ||
) | ||
with mock.patch( | ||
target="solarwinds_apm.extension.oboe.Context.getDecisions", | ||
new=mock_decision, | ||
): | ||
# Request to instrumented app, no traceparent/tracestate | ||
resp = self.client.get( | ||
"/test_trace/", | ||
headers={ | ||
"some-header": "some-value" | ||
} | ||
) | ||
resp_json = json.loads(resp.data) | ||
|
||
# Verify some-header was not altered by instrumentation | ||
try: | ||
assert resp_json["incoming-headers"]["some-header"] == "some-value" | ||
except KeyError as e: | ||
self.fail("KeyError was raised at incoming-headers check: {}".format(e)) | ||
|
||
# Verify trace context injected into test app's outgoing postman-echo call | ||
# (added to Flask app's response data) includes: | ||
# - traceparent with a trace_id, span_id, and trace_flags for do_sample | ||
# - tracestate with same span_id and trace_flags for do_sample | ||
assert "traceparent" in resp_json | ||
_TRACEPARENT_HEADER_FORMAT = ( | ||
"^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" | ||
) | ||
_TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) | ||
traceparent_re_result = re.search( | ||
_TRACEPARENT_HEADER_FORMAT_RE, | ||
resp_json["traceparent"], | ||
) | ||
new_trace_id = traceparent_re_result.group(2) | ||
assert new_trace_id is not None | ||
new_span_id = traceparent_re_result.group(3) | ||
assert new_span_id is not None | ||
new_trace_flags = traceparent_re_result.group(4) | ||
assert new_trace_flags == "01" | ||
|
||
assert "tracestate" in resp_json | ||
# In this test we know there is only `sw` in tracestate | ||
# and its value will be new_span_id and new_trace_flags | ||
assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) | ||
|
||
# Verify x-trace response header has same trace_id | ||
# though it will have different span ID because of Flask | ||
# app's outgoing request | ||
assert "x-trace" in resp.headers | ||
assert new_trace_id in resp.headers["x-trace"] | ||
|
||
# Verify spans exported: service entry (root) + outgoing request (child with local parent) | ||
spans = self.memory_exporter.get_finished_spans() | ||
assert len(spans) == 2 | ||
span_server = spans[1] | ||
span_client = spans[0] | ||
assert span_server.name == "/test_trace/" | ||
assert span_server.kind == trace_api.SpanKind.SERVER | ||
assert span_client.name == "HTTP GET" | ||
assert span_client.kind == trace_api.SpanKind.CLIENT | ||
|
||
# Check root span tracestate has `sw` key | ||
# In this test we know its value will have invalid span_id | ||
expected_trace_state = trace_api.TraceState([("sw", "0000000000000000-01")]) | ||
assert span_server.context.trace_state == expected_trace_state | ||
|
||
# Check root span attributes | ||
# :present: | ||
# service entry internal KVs, which are on all entry spans | ||
# :absent: | ||
# sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision | ||
# SWKeys, because no xtraceoptions in otel context | ||
assert all(attr_key in span_server.attributes for attr_key in self.SW_SETTINGS_KEYS) | ||
assert span_server.attributes["BucketCapacity"] == "6.0" | ||
assert span_server.attributes["BucketRate"] == "5.0" | ||
assert span_server.attributes["SampleRate"] == 3 | ||
assert span_server.attributes["SampleSource"] == 4 | ||
assert not "sw.tracestate_parent_id" in span_server.attributes | ||
assert not "SWKeys" in span_server.attributes | ||
|
||
# Check outgoing request tracestate has `sw` key | ||
# In this test we know its value will also have invalid span_id | ||
expected_trace_state = trace_api.TraceState([("sw", "0000000000000000-01")]) | ||
assert span_client.context.trace_state == expected_trace_state | ||
|
||
# Check outgoing request span attributes | ||
# :absent: | ||
# service entry internal KVs, which are only on entry spans | ||
# sw.tracestate_parent_id, because cannot be set without attributes at decision | ||
# SWKeys, because no xtraceoptions in otel context | ||
assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) | ||
assert not "sw.tracestate_parent_id" in span_client.attributes | ||
assert not "SWKeys" in span_client.attributes | ||
|
||
# Check span_id of the outgoing request span (client span) matches | ||
# the span_id portion in the outgoing tracestate header, which | ||
# is stored in the test app's response body (new_span_id). | ||
# Note: context.span_id needs a 16-byte hex conversion first. | ||
assert "{:016x}".format(span_client.context.span_id) == new_span_id |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe, very minor: I gave this a try and was BAD and didn't set any
SW_APM_SERVICE_KEY_TOX_*
env vars, but the tests passed fine. So it seems that the tox-driven unit and integration tests don't actually need valid SW_APM_ env vars (which makes sense given what they're testing).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yess!!! This must have been leftover from a very old setup. Removed in ea5d5d8