-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Add generated tests for azure-core-tracing-opentelemetry #45675
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,296 @@ | ||
| # ------------------------------------ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
| # ------------------------------------ | ||
|
|
||
| import builtins | ||
| import importlib.util | ||
| import sys | ||
| from types import SimpleNamespace | ||
| from unittest import mock | ||
|
|
||
| import pytest | ||
| from opentelemetry import context, trace | ||
| from opentelemetry.trace import NonRecordingSpan | ||
|
|
||
| from azure.core.tracing import Link, SpanKind | ||
| from azure.core.tracing.ext import opentelemetry_span as otel_span_module | ||
| from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan | ||
|
|
||
|
|
||
| class _RecordingTracer: | ||
| def __init__(self, span): | ||
| self._span = span | ||
| self.calls = [] | ||
|
|
||
| def start_span(self, name=None, kind=None, **kwargs): | ||
| self.calls.append({"name": name, "kind": kind, **kwargs}) | ||
| if kind is not None: | ||
| self._span.kind = kind | ||
| return self._span | ||
|
|
||
|
|
||
| class _BasicSpan: | ||
| def __init__(self): | ||
| self.ended = False | ||
| self.end_time = None | ||
| self.attributes = {} | ||
| self._kind = None | ||
|
|
||
| @property | ||
| def kind(self): | ||
| return self._kind | ||
|
|
||
| @kind.setter | ||
| def kind(self, value): | ||
| self._kind = value | ||
|
|
||
| def end(self): | ||
| self.ended = True | ||
| self.end_time = object() | ||
|
|
||
| def set_attribute(self, key, value): | ||
| self.attributes[key] = value | ||
|
|
||
| def set_status(self, status): | ||
| self.status = status | ||
|
|
||
| def get_span_context(self): | ||
| return trace.INVALID_SPAN.get_span_context() | ||
|
|
||
|
|
||
| class _TruthyEmptyLinks: | ||
| def __bool__(self): | ||
| return True | ||
|
|
||
| def __iter__(self): | ||
| return iter(()) | ||
|
|
||
|
|
||
| class _CoreLikeLink: | ||
| def __init__(self, headers, attributes): | ||
| self.headers = headers | ||
| self.attributes = attributes | ||
|
|
||
|
|
||
| class TestOpenTelemetrySpanGapPaths: | ||
| def test_fallback_suppress_http_instrumentation_key_on_import_error(self, monkeypatch): | ||
| original_import = builtins.__import__ | ||
|
|
||
| def _import(name, globals=None, locals=None, fromlist=(), level=0): | ||
| if name == "opentelemetry.context" and fromlist and "_SUPPRESS_HTTP_INSTRUMENTATION_KEY" in fromlist: | ||
| raise ImportError("forced missing private key") | ||
| return original_import(name, globals, locals, fromlist, level) | ||
|
|
||
| monkeypatch.setattr(builtins, "__import__", _import) | ||
|
|
||
| module_name = "azure.core.tracing.ext.opentelemetry_span.__test_fallback__" | ||
| spec = importlib.util.spec_from_file_location(module_name, otel_span_module.__file__) | ||
| module = importlib.util.module_from_spec(spec) | ||
| sys.modules[module_name] = module | ||
|
|
||
| try: | ||
| spec.loader.exec_module(module) | ||
| assert module._SUPPRESS_HTTP_INSTRUMENTATION_KEY == "suppress_http_instrumentation" | ||
| finally: | ||
| sys.modules.pop(module_name, None) | ||
|
|
||
| def test_init_raises_for_unsupported_kind(self): | ||
| with pytest.raises(ValueError): | ||
| OpenTelemetrySpan(name="bad-kind", kind=object()) | ||
|
|
||
| def test_init_converts_truthy_empty_links_to_empty_otel_links(self, monkeypatch): | ||
| span = _BasicSpan() | ||
| tracer = _RecordingTracer(span) | ||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: tracer) | ||
|
|
||
| wrapped = OpenTelemetrySpan(name="empty-links", links=_TruthyEmptyLinks()) | ||
|
|
||
| assert wrapped.span_instance is span | ||
| assert tracer.calls[0]["links"] == [] | ||
|
|
||
| def test_init_converts_core_links_and_parent_context(self, monkeypatch): | ||
| span = _BasicSpan() | ||
| tracer = _RecordingTracer(span) | ||
| sentinel_context = object() | ||
| span_context = trace.INVALID_SPAN.get_span_context() | ||
|
|
||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: tracer) | ||
| monkeypatch.setattr(otel_span_module, "extract", lambda headers: sentinel_context) | ||
| monkeypatch.setattr( | ||
| otel_span_module, | ||
| "get_span_from_context", | ||
| lambda ctx=None: SimpleNamespace(get_span_context=lambda: span_context), | ||
| ) | ||
|
|
||
| wrapped = OpenTelemetrySpan( | ||
| name="with-links", | ||
| links=[_CoreLikeLink({"traceparent": "00-11111111111111111111111111111111-2222222222222222-01"}, {"k": 1})], | ||
| context={"traceparent": "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01"}, | ||
| ) | ||
|
|
||
| assert wrapped.span_instance is span | ||
| assert len(tracer.calls[0]["links"]) == 1 | ||
| assert tracer.calls[0]["context"] is sentinel_context | ||
|
|
||
| def test_init_keeps_user_links_when_link_shape_is_invalid(self, monkeypatch): | ||
| span = _BasicSpan() | ||
| tracer = _RecordingTracer(span) | ||
| bad_links = [object()] | ||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: tracer) | ||
|
|
||
| wrapped = OpenTelemetrySpan(name="bad-links", links=bad_links) | ||
|
|
||
| assert wrapped.span_instance is span | ||
| assert tracer.calls[0]["links"] is bad_links | ||
|
|
||
| def test_kind_property_returns_none_when_span_has_no_kind(self): | ||
| wrapped = OpenTelemetrySpan(span=object()) | ||
| assert wrapped.kind is None | ||
|
|
||
| def test_kind_setter_assigns_kind_on_supported_value(self, monkeypatch): | ||
| span = _BasicSpan() | ||
| tracer = _RecordingTracer(span) | ||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: tracer) | ||
|
|
||
| wrapped = OpenTelemetrySpan(name="set-kind") | ||
| wrapped.kind = SpanKind.CLIENT | ||
| assert wrapped.span_instance.kind.name == "CLIENT" | ||
|
|
||
| def test_kind_setter_raises_for_invalid_kind(self): | ||
| wrapped = OpenTelemetrySpan(name="bad-kind-set") | ||
| with pytest.raises(ValueError): | ||
| wrapped.kind = "invalid-kind" # type: ignore[assignment] | ||
|
|
||
| def test_kind_setter_warns_when_span_does_not_allow_assignment(self): | ||
| wrapped = OpenTelemetrySpan(span=object()) | ||
| with pytest.warns(UserWarning): | ||
| wrapped.kind = SpanKind.CLIENT | ||
|
|
||
| def test_start_is_noop_and_exit_without_enter_finishes_span(self, monkeypatch): | ||
| span = _BasicSpan() | ||
| tracer = _RecordingTracer(span) | ||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: tracer) | ||
|
|
||
| wrapped = OpenTelemetrySpan(name="manual-exit") | ||
| wrapped.start() | ||
| wrapped.__exit__(None, None, None) | ||
| assert wrapped.span_instance.end_time is not None | ||
|
|
||
| def test_to_header_and_get_trace_parent(self, tracing_helper): | ||
| with tracing_helper.tracer.start_as_current_span("root"): | ||
| wrapped = OpenTelemetrySpan(name="headers") | ||
| headers = wrapped.to_header() | ||
| assert wrapped.get_trace_parent() == headers["traceparent"] | ||
|
|
||
| def test_link_delegates_to_link_from_headers(self): | ||
| with mock.patch.object(OpenTelemetrySpan, "link_from_headers") as patched: | ||
| OpenTelemetrySpan.link( | ||
| "00-2578531519ed94423ceae67588eff2c9-231ebdc614cb9ddd-01", | ||
| {"a": 1}, | ||
| ) | ||
|
|
||
| assert patched.call_count == 1 | ||
| assert patched.call_args.args[0] == {"traceparent": "00-2578531519ed94423ceae67588eff2c9-231ebdc614cb9ddd-01"} | ||
| assert patched.call_args.args[1] == {"a": 1} | ||
|
|
||
| def test_link_from_headers_appends_link_to_current_span(self, monkeypatch): | ||
| current_span = SimpleNamespace(_links=[]) | ||
| span_context = trace.INVALID_SPAN.get_span_context() | ||
|
|
||
| monkeypatch.setattr(otel_span_module, "extract", lambda headers: object()) | ||
| monkeypatch.setattr( | ||
| otel_span_module, | ||
| "get_span_from_context", | ||
| lambda ctx=None: SimpleNamespace(get_span_context=lambda: span_context), | ||
| ) | ||
| monkeypatch.setattr(OpenTelemetrySpan, "get_current_span", classmethod(lambda cls: current_span)) | ||
|
|
||
| OpenTelemetrySpan.link_from_headers( | ||
| {"traceparent": "00-2578531519ed94423ceae67588eff2c9-231ebdc614cb9ddd-01"}, | ||
| {"k": "v"}, | ||
| ) | ||
|
|
||
| assert len(current_span._links) == 1 | ||
|
|
||
| def test_link_from_headers_warns_when_current_span_has_no_links(self, monkeypatch): | ||
| span_context = trace.INVALID_SPAN.get_span_context() | ||
|
|
||
| monkeypatch.setattr(otel_span_module, "extract", lambda headers: object()) | ||
| monkeypatch.setattr( | ||
| otel_span_module, | ||
| "get_span_from_context", | ||
| lambda ctx=None: SimpleNamespace(get_span_context=lambda: span_context), | ||
| ) | ||
| monkeypatch.setattr(OpenTelemetrySpan, "get_current_span", classmethod(lambda cls: object())) | ||
|
|
||
| with pytest.warns(UserWarning): | ||
| OpenTelemetrySpan.link_from_headers( | ||
| {"traceparent": "00-2578531519ed94423ceae67588eff2c9-231ebdc614cb9ddd-01"} | ||
| ) | ||
|
|
||
| def test_get_current_span_returns_last_unsuppressed_parent_for_non_recording(self, monkeypatch): | ||
| span = _BasicSpan() | ||
| tracer = _RecordingTracer(span) | ||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: tracer) | ||
|
|
||
| with OpenTelemetrySpan(name="outer", kind=SpanKind.INTERNAL) as outer: | ||
| with OpenTelemetrySpan(name="inner", kind=SpanKind.INTERNAL): | ||
| assert OpenTelemetrySpan.get_current_span() is outer.span_instance | ||
|
|
||
| def test_get_current_tracer_delegates_to_trace_get_tracer(self, monkeypatch): | ||
| sentinel = object() | ||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: sentinel) | ||
| assert OpenTelemetrySpan.get_current_tracer() is sentinel | ||
|
|
||
| def test_change_context_handles_raw_span_and_wrapped_span(self, tracing_helper): | ||
| raw_span = tracing_helper.tracer.start_span("raw") | ||
| with OpenTelemetrySpan.change_context(raw_span): | ||
| assert trace.get_current_span() is raw_span | ||
| raw_span.end() | ||
|
|
||
| wrapped = OpenTelemetrySpan(name="wrapped", kind=SpanKind.INTERNAL) | ||
| with OpenTelemetrySpan.change_context(wrapped): | ||
| assert trace.get_current_span() is wrapped.span_instance | ||
| wrapped.finish() | ||
|
|
||
| def test_set_current_span_raises_not_implemented(self): | ||
| with pytest.raises(NotImplementedError): | ||
| OpenTelemetrySpan.set_current_span(trace.INVALID_SPAN) | ||
|
|
||
| def test_set_current_tracer_is_noop(self, tracing_helper): | ||
| assert OpenTelemetrySpan.set_current_tracer(tracing_helper.tracer) is None | ||
|
|
||
| def test_get_current_span_uses_last_unsuppressed_context_value_when_non_recording(self, monkeypatch): | ||
| parent = OpenTelemetrySpan(name="parent", kind=SpanKind.SERVER) | ||
| non_recording = NonRecordingSpan(context=trace.INVALID_SPAN.get_span_context()) | ||
|
|
||
| monkeypatch.setattr(otel_span_module, "get_span_from_context", lambda *args, **kwargs: non_recording) | ||
|
|
||
| token = context.attach(context.set_value(otel_span_module._LAST_UNSUPPRESSED_SPAN, parent, context.get_current())) | ||
| try: | ||
| assert OpenTelemetrySpan.get_current_span() is parent.span_instance | ||
| finally: | ||
| context.detach(token) | ||
| parent.finish() | ||
|
|
||
| def test_init_converts_public_core_link_type(self, monkeypatch): | ||
|
Member
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. This seems to be testing similar things to test_init_converts_core_links_and_parent_context, just one is using Link and one is using _CoreLikeLink. Perhaps update test_init_converts_core_links_and_parent_context to just use |
||
| span = _BasicSpan() | ||
| tracer = _RecordingTracer(span) | ||
| span_context = trace.INVALID_SPAN.get_span_context() | ||
|
|
||
| monkeypatch.setattr(otel_span_module.trace, "get_tracer", lambda *args, **kwargs: tracer) | ||
| monkeypatch.setattr(otel_span_module, "extract", lambda headers: object()) | ||
| monkeypatch.setattr( | ||
| otel_span_module, | ||
| "get_span_from_context", | ||
| lambda ctx=None: SimpleNamespace(get_span_context=lambda: span_context), | ||
| ) | ||
|
|
||
| wrapped = OpenTelemetrySpan( | ||
| name="core-link", | ||
| links=[Link({"traceparent": "00-11111111111111111111111111111111-2222222222222222-01"}, {"x": 5})], | ||
| ) | ||
|
|
||
| assert wrapped.span_instance is span | ||
| assert len(tracer.calls[0]["links"]) == 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,15 @@ | |
|
|
||
|
|
||
| class TestOpenTelemetrySchema: | ||
| def test_latest_schema_attributes_renamed(self, tracing_helper): | ||
| with tracing_helper.tracer.start_as_current_span("Root", kind=OpenTelemetrySpanKind.CLIENT) as parent: | ||
| wrapped_class = OpenTelemetrySpan(span=parent) | ||
| @staticmethod | ||
| def _get_span_schema_url(span_instance): | ||
|
Member
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. Why was this added? |
||
| scope = getattr(span_instance, "instrumentation_scope", None) | ||
| if scope is None: | ||
| scope = getattr(span_instance, "instrumentation_info", None) | ||
| return getattr(scope, "schema_url", None) | ||
|
|
||
| def test_latest_schema_attributes_renamed(self): | ||
| with OpenTelemetrySpan(name="Root") as wrapped_class: | ||
| schema_version = wrapped_class._schema_version | ||
| attribute_mappings = OpenTelemetrySchema.get_attribute_mappings(schema_version) | ||
| attribute_values = {} | ||
|
|
@@ -30,10 +36,8 @@ def test_latest_schema_attributes_renamed(self, tracing_helper): | |
| # Check that original attribute is not present. | ||
| assert wrapped_class.span_instance.attributes.get(key) is None | ||
|
|
||
| def test_latest_schema_attributes_not_renamed(self, tracing_helper): | ||
| with tracing_helper.tracer.start_as_current_span("Root", kind=OpenTelemetrySpanKind.CLIENT) as parent: | ||
| wrapped_class = OpenTelemetrySpan(span=parent) | ||
|
|
||
| def test_latest_schema_attributes_not_renamed(self): | ||
| with OpenTelemetrySpan(name="Root") as wrapped_class: | ||
| wrapped_class.add_attribute("foo", "bar") | ||
| wrapped_class.add_attribute("baz", "qux") | ||
|
|
||
|
|
@@ -43,17 +47,23 @@ def test_latest_schema_attributes_not_renamed(self, tracing_helper): | |
| def test_schema_url_in_instrumentation_scope(self): | ||
| with OpenTelemetrySpan(name="span") as span: | ||
| schema_url = OpenTelemetrySchema.get_schema_url(span._schema_version) | ||
| assert span.span_instance.instrumentation_scope.schema_url == schema_url | ||
| assert self._get_span_schema_url(span.span_instance) == schema_url | ||
|
|
||
| def test_schema_version_argument(self, tracing_helper): | ||
| def test_schema_version_argument(self): | ||
| with OpenTelemetrySpan(name="span", schema_version="1.0.0") as span: | ||
| assert span._schema_version == "1.0.0" | ||
| assert span._attribute_mappings == {} | ||
| assert span.span_instance.instrumentation_scope.schema_url == "https://opentelemetry.io/schemas/1.0.0" | ||
| assert self._get_span_schema_url(span.span_instance) == "https://opentelemetry.io/schemas/1.0.0" | ||
|
|
||
| def test_schema_version_formats(self, tracing_helper): | ||
| def test_schema_version_formats(self): | ||
| assert OpenTelemetrySchema.get_attribute_mappings(OpenTelemetrySchemaVersion.V1_19_0) | ||
| assert OpenTelemetrySchema.get_attribute_mappings(OpenTelemetrySchemaVersion.V1_23_1) | ||
| assert OpenTelemetrySchema.get_attribute_mappings("1.19.0") | ||
| assert OpenTelemetrySchema.get_attribute_mappings("1.23.1") | ||
| assert not OpenTelemetrySchema.get_attribute_mappings("1.0.0") | ||
|
|
||
| def test_get_latest_version_returns_last_supported_version(self): | ||
| latest = OpenTelemetrySchema.get_latest_version() | ||
|
|
||
| assert latest == OpenTelemetrySchemaVersion.V1_23_1 | ||
| assert latest == OpenTelemetrySchema.SUPPORTED_VERSIONS[-1] | ||
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.
Aren't the inner span and outer span the same regardless since they both come from _RecordingTracer which will always return the same span?
In any case, I don't think this test adds coverage as test_tracing_implementation.py has coverage for this case.