Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5704.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-exporter-zipkin-json`: warn and fall back to the default instead of raising `ValueError` when `OTEL_EXPORTER_ZIPKIN_TIMEOUT` holds a non-integer value; the same fix is applied to `opentelemetry-exporter-zipkin-proto-http`.
1 change: 1 addition & 0 deletions .changelog/5879.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-test-utils`: document every hand-written module, class and callable
4 changes: 0 additions & 4 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ persistent=yes
# Specify a configuration file.
#rcfile=

# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes

# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
Expand Down
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pylint==3.3.4
pylint==4.0.9
griffe==1.15.0
httpretty==1.1.4
inline-snapshot==0.34.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,27 @@

DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans"
REQUESTS_SUCCESS_STATUS_CODES = (200, 202)
_DEFAULT_TIMEOUT = 10

logger = logging.getLogger(__name__)


def _timeout_from_env() -> int:
value = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT)
if value is None or not value.strip():
return _DEFAULT_TIMEOUT
try:
return int(value)
except ValueError:
logger.warning(
"Invalid value %r for %s, using default of %s seconds",
value,
OTEL_EXPORTER_ZIPKIN_TIMEOUT,
_DEFAULT_TIMEOUT,
)
return _DEFAULT_TIMEOUT


class ZipkinExporter(SpanExporter):
def __init__(
self,
Expand Down Expand Up @@ -128,7 +145,7 @@ def __init__(
self.session = session or requests.Session()
self.session.headers.update({"Content-Type": self.encoder.content_type()})
self._closed = False
self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10))
self.timeout = timeout or _timeout_from_env()

def export(self, spans: Sequence[Span]) -> SpanExportResult:
# After the call to Shutdown subsequent calls to Export are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,52 @@ def test_constructor_env_vars(self):
self.assertEqual(exporter.local_node.port, None)
self.assertEqual(exporter.timeout, 15)

def test_constructor_invalid_timeout_env_var_falls_back(self):
with patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: "10s"}):
with self.assertLogs("opentelemetry.exporter.zipkin.json", level="WARNING") as cm:
exporter = ZipkinExporter()

self.assertEqual(exporter.timeout, 10)
self.assertEqual(len(cm.records), 1)
self.assertTrue(
any("Invalid value" in message for message in cm.output),
cm.output,
)

def test_constructor_empty_timeout_env_var_falls_back(self):
for value in ("", " ", "\t\n"):
with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"):
exporter = ZipkinExporter()
self.assertEqual(exporter.timeout, 10)

def test_constructor_unset_timeout_env_var_uses_default(self):
with patch.dict(os.environ):
os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None)
with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"):
exporter = ZipkinExporter()
self.assertEqual(exporter.timeout, 10)

def test_constructor_numeric_timeout_env_var_preserves_values(self):
for value, expected in (("15", 15), (" 15 ", 15), ("0", 0), ("-1", -1)):
with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"):
exporter = ZipkinExporter()
self.assertEqual(exporter.timeout, expected)

def test_constructor_explicit_timeout_ignores_invalid_env_var(self):
for value in ("10s", "", "\t\n"):
with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"):
exporter = ZipkinExporter(timeout=7)
self.assertEqual(exporter.timeout, 7)

def test_constructor_explicit_timeout_preserves_truthiness(self):
with patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: "15"}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.json", level="WARNING"):
self.assertEqual(ZipkinExporter(timeout=0).timeout, 15)
self.assertEqual(ZipkinExporter(timeout=-1).timeout, -1)

def test_constructor_protocol_endpoint(self):
"""Test the constructor for the common usage of providing the
protocol and endpoint arguments."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,27 @@

DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans"
REQUESTS_SUCCESS_STATUS_CODES = (200, 202)
_DEFAULT_TIMEOUT = 10

logger = logging.getLogger(__name__)


def _timeout_from_env() -> int:
value = environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT)
if value is None or not value.strip():
return _DEFAULT_TIMEOUT
try:
return int(value)
except ValueError:
logger.warning(
"Invalid value %r for %s, using default of %s seconds",
value,
OTEL_EXPORTER_ZIPKIN_TIMEOUT,
_DEFAULT_TIMEOUT,
)
return _DEFAULT_TIMEOUT


class ZipkinExporter(SpanExporter):
def __init__(
self,
Expand Down Expand Up @@ -121,7 +138,7 @@ def __init__(
self.session = session or requests.Session()
self.session.headers.update({"Content-Type": self.encoder.content_type()})
self._closed = False
self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10))
self.timeout = timeout or _timeout_from_env()

def export(self, spans: Sequence[Span]) -> SpanExportResult:
# After the call to Shutdown subsequent calls to Export are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,52 @@ def test_constructor_env_vars(self):
self.assertEqual(exporter.local_node.port, None)
self.assertEqual(exporter.timeout, 15)

def test_constructor_invalid_timeout_env_var_falls_back(self):
with patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: "10s"}):
with self.assertLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING") as cm:
exporter = ZipkinExporter()

self.assertEqual(exporter.timeout, 10)
self.assertEqual(len(cm.records), 1)
self.assertTrue(
any("Invalid value" in message for message in cm.output),
cm.output,
)

def test_constructor_empty_timeout_env_var_falls_back(self):
for value in ("", " ", "\t\n"):
with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"):
exporter = ZipkinExporter()
self.assertEqual(exporter.timeout, 10)

def test_constructor_unset_timeout_env_var_uses_default(self):
with patch.dict(os.environ):
os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None)
with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"):
exporter = ZipkinExporter()
self.assertEqual(exporter.timeout, 10)

def test_constructor_numeric_timeout_env_var_preserves_values(self):
for value, expected in (("15", 15), (" 15 ", 15), ("0", 0), ("-1", -1)):
with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"):
exporter = ZipkinExporter()
self.assertEqual(exporter.timeout, expected)

def test_constructor_explicit_timeout_ignores_invalid_env_var(self):
for value in ("10s", "", "\t\n"):
with self.subTest(value=value), patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: value}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"):
exporter = ZipkinExporter(timeout=7)
self.assertEqual(exporter.timeout, 7)

def test_constructor_explicit_timeout_preserves_truthiness(self):
with patch.dict(os.environ, {OTEL_EXPORTER_ZIPKIN_TIMEOUT: "15"}):
with self.assertNoLogs("opentelemetry.exporter.zipkin.proto.http", level="WARNING"):
self.assertEqual(ZipkinExporter(timeout=0).timeout, 15)
self.assertEqual(ZipkinExporter(timeout=-1).timeout, -1)

def test_constructor_protocol_endpoint(self):
"""Test the constructor for the common usage of providing the
protocol and endpoint arguments."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from collections.abc import Mapping
from dataclasses import MISSING, fields, is_dataclass
from enum import Enum
from types import UnionType
from types import NoneType, UnionType
from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints

_T = TypeVar("_T")
Expand All @@ -27,7 +27,7 @@ def _unwrap_optional(type_hint: Any) -> Any:
"""
origin = get_origin(type_hint)
if origin is UnionType or origin is Union:
non_none = [t for t in get_args(type_hint) if t is not type(None)]
non_none = [t for t in get_args(type_hint) if t is not NoneType]
if len(non_none) == 1:
return non_none[0]
return type_hint
Expand Down
4 changes: 0 additions & 4 deletions opentelemetry-semantic-conventions/.pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ persistent=yes
# Specify a configuration file.
#rcfile=

# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes

# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
Expand Down
108 changes: 107 additions & 1 deletion tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,140 @@

# type: ignore

"""Shared test helpers for the OpenTelemetry Python packages.

This package is published as ``opentelemetry-test-utils``. It is a test
dependency of the other distributions in this repository and of the
instrumentation repositories, never a runtime dependency of library or
application code.

The module itself provides ``TestCase``, a ``unittest.TestCase`` extended with
an ``assertNotRaises`` assertion. The submodules provide the rest: base classes
that reset the API globals and capture spans and metrics in memory
(``opentelemetry.test.test_base``, ``opentelemetry.test.globals_test``), WSGI,
ASGI and HTTP server fixtures (``opentelemetry.test.wsgitestutil``,
``opentelemetry.test.asgitestutil``, ``opentelemetry.test.httptest``), builders
for spans, metrics and samplers (``opentelemetry.test.spantestutil``,
``opentelemetry.test.metrictestutil``, ``opentelemetry.test.samplertestutil``),
propagator doubles (``opentelemetry.test.mock_textmap``), an entry point double
(``opentelemetry.test.mock_test_classes``), helpers for provoking a thread race
(``opentelemetry.test.concurrency_test``), an in-process OTLP/HTTP receiver
(``opentelemetry.test._otlp_test_server``), a semantic convention check against
weaver (``opentelemetry.test.weaver_live_check``), and the version of the
distribution (``opentelemetry.test.version``).
"""

from traceback import format_tb
from unittest import TestCase


class _AssertNotRaisesMixin:
"""Mixin that adds an ``assertNotRaises`` assertion to a test case.

``unittest`` has ``assertRaises`` but no inverse, because an unexpected
exception already fails a test by propagating out of it. ``assertNotRaises``
exists so that a test can state the expectation explicitly and fail with the
traceback of the unwanted exception formatted into the failure message,
rather than with the raw exception.

Mix it in before ``unittest.TestCase`` in the base class list, so that its
``__init__`` runs first and installs the assertion. The ``TestCase`` defined
in this module already does that.
"""

class _AssertNotRaises:
"""Callable context manager that fails its test case on given exceptions.

An instance is bound to ``self.assertNotRaises`` on the test case. It is
both the callable and the context manager: calling it records the
exception types to watch for and returns the same instance, which is
then entered. Entering it without calling it first succeeds, because
``__enter__`` does not read the exception types; the omission surfaces
as an ``AttributeError`` out of ``__exit__``, and only when the block
raised something.

The instance is shared across every use within one test case, so nesting
two ``assertNotRaises`` blocks does not work: the inner call overwrites
the exception types of the outer one.

Args:
test_case: The test case to call ``fail`` on. Stored, not copied, so
the failure is reported against the running test.
"""

def __init__(self, test_case):
"""Parameters are documented on the class docstring."""
self._test_case = test_case

def __enter__(self):
"""Return this instance so the ``with`` body can run.

Returns:
The context manager itself. The value is of no use to the body
and is returned only because entering a context manager binds a
value.
"""
return self

def __exit__(self, type_, value, tb): # pylint: disable=invalid-name
"""Fail the test case when one of the watched exceptions was raised.

The exception type is matched by identity against the types passed
to ``__call__``, not with ``issubclass``, so a subclass of a watched
type does not match.

Args:
type_: Class of the exception raised in the ``with`` body, or
``None`` when the body completed normally.
value: The exception instance, or ``None``.
tb: The traceback of the exception, or ``None``. It is formatted
into the failure message.

Returns:
Always ``True``, which suppresses whatever exception the ``with``
body raised, including an exception of a type that was not
watched.
"""
if value is not None and type_ in self._exception_types:
self._test_case.fail("Unexpected exception was raised:\n{}".format("\n".join(format_tb(tb))))

return True

def __call__(self, exception, *exceptions):
"""Record the exception types to watch for and return this instance.

Args:
exception: An exception class that must not be raised. At least
one is required, so that an empty watch list cannot be set
by accident.
*exceptions: Further exception classes that must not be raised.

Returns:
The context manager itself, ready to be entered. The types
recorded here replace any recorded by an earlier call.
"""
# pylint: disable=attribute-defined-outside-init
self._exception_types = (exception, *exceptions)
return self

def __init__(self, *args, **kwargs):
"""Install ``assertNotRaises`` on this test case.

Args:
*args: Forwarded unchanged to the next class in the method
resolution order, which is the ``unittest.TestCase`` this mixin
is combined with.
**kwargs: Forwarded unchanged for the same reason.
"""
super().__init__(*args, **kwargs)
# pylint: disable=invalid-name
self.assertNotRaises = self._AssertNotRaises(self)


class TestCase(_AssertNotRaisesMixin, TestCase): # pylint: disable=function-redefined
pass
"""A ``unittest.TestCase`` that also provides ``assertNotRaises``.

This class deliberately shadows the ``unittest.TestCase`` imported in this
module, so that a test module can switch to it by changing only its import
and keep writing ``class MyTest(TestCase)``.
"""
Loading
Loading