diff --git a/.changelog/5876.added b/.changelog/5876.added new file mode 100644 index 00000000000..de46e4571c6 --- /dev/null +++ b/.changelog/5876.added @@ -0,0 +1 @@ +`opentelemetry-api`: document every hand-written module, class and callable diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 203d0a9314b..ea0429cade1 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -51,6 +51,33 @@ class LogRecord(ABC): LogRecord instances are created and emitted via `Logger` every time something is logged. They contain all the information pertinent to the event being logged. + + Args: + timestamp: When the event occurred, as nanoseconds since the Unix + epoch. + observed_timestamp: When the event was observed by the collection + system, as nanoseconds since the Unix epoch. Defaults to the time + the record is built, whether or not ``timestamp`` was given. + context: The context the record belongs to. Defaults to the current + context, from which the trace id, span id and trace flags are + taken. + trace_id: The id of the trace the record belongs to. Defaults to the + one in ``context``. + span_id: The id of the span the record belongs to. Defaults to the one + in ``context``. + trace_flags: The sampling flags of the span the record belongs to. + Defaults to the ones in ``context``. + severity_text: The severity as the originating system spelled it, for + example ``"WARN"``. + severity_number: The severity on the OpenTelemetry scale. + body: The payload of the event. + attributes: Further name/value pairs describing the event. + event_name: The name identifying the class of event. + exception: An exception to record alongside the event. + + .. deprecated:: 1.35.0 + Passing ``trace_id``, ``span_id`` or ``trace_flags`` is deprecated. + Pass ``context`` instead. """ __slots__ = ( @@ -117,6 +144,11 @@ def __init__( event_name: str | None = None, exception: BaseException | None = None, ) -> None: + """Build the record, filling in the timestamps and trace fields. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ if not context: context = get_current() span_context = get_current_span(context).get_span_context() @@ -137,7 +169,20 @@ def __init__( class Logger(ABC): - """Handles emitting events and logs via `LogRecord`.""" + """Handles emitting events and logs via `LogRecord`. + + A logger is bound to one instrumentation scope, named by the arguments it + is built with. Obtain one from :meth:`LoggerProvider.get_logger` rather + than constructing it directly. + + Args: + name: The name of the instrumenting module, package or class, not the + name of the thing being instrumented. + version: The version string of the instrumenting library. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + """ def __init__( self, @@ -146,6 +191,11 @@ def __init__( schema_url: str | None = None, attributes: Attributes = None, ) -> None: + """Record the instrumentation scope this logger is bound to. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__() self._name = name self._version = version @@ -188,7 +238,36 @@ def emit( event_name: str | None = None, exception: BaseException | None = None, ) -> None: - """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" + """Emits a :class:`LogRecord` representing a log to the processing pipeline. + + The record is supplied either as a built :class:`LogRecord` positional + argument or as the keyword arguments to build one from, never both. + + Args: + record: A fully built :class:`LogRecord` to emit. When it is + given, every keyword argument below is ignored. When it is + ``None``, a record is built from the keyword arguments. + timestamp: When the event occurred, as nanoseconds since the Unix + epoch. Left unset when omitted, so the record carries ``None`` + rather than a time the API invented. + observed_timestamp: When the event was observed by the collection + system, as nanoseconds since the Unix epoch. Defaults to the + time the record is built. + context: The context the record belongs to, used to associate it + with the active span. Defaults to the current context. + severity_number: The severity of the event on the OpenTelemetry + scale. + severity_text: The severity of the event as the originating system + spelled it, for example ``"WARN"``. + body: The payload of the event. A string for an ordinary log line, + or any other ``AnyValue`` for + structured content. + attributes: Further name/value pairs describing the event. + event_name: The name identifying the class of event, for a + structured event rather than a free-form log line. + exception: An exception to record alongside the event. Its type, + message and stack trace are attached to the record. + """ @abstractmethod def enabled( @@ -205,6 +284,16 @@ def enabled( The returned value may change over time and should be checked each time before emitting a log record. + + Args: + context: The context the record would belong to. Defaults to the + current context. + severity_number: The severity the record would carry. + event_name: The name of the event the record would carry. + + Returns: + ``True`` when a record emitted with these arguments would be + processed, ``False`` when it would be discarded. """ @@ -249,7 +338,23 @@ def emit( event_name: str | None = None, exception: BaseException | None = None, ) -> None: - pass + """Discard the call, since this logger builds no record at all. + + Nothing here is read. No :class:`LogRecord` is built from the keyword + arguments, so no timestamp is taken and no context is consulted. + + Args: + record: Ignored. + timestamp: Ignored. + observed_timestamp: Ignored. + context: Ignored. + severity_number: Ignored. + severity_text: Ignored. + body: Ignored. + attributes: Ignored. + event_name: Ignored. + exception: Ignored. + """ def enabled( self, @@ -258,10 +363,39 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: + """Report that nothing is ever emitted. + + The body is ``return False`` and reads no argument, so none of the + three is consulted and no context lookup happens. + + Args: + context: Ignored. + severity_number: Ignored. + event_name: Ignored. + + Returns: + Always ``False``, so instrumentation skips building a record it + would only discard. + """ return False class ProxyLogger(Logger): + """A logger that stands in until a real provider is set globally. + + Every record emitted through it is discarded while no + :class:`LoggerProvider` has been installed. Once one is, the proxy + resolves a real logger for its instrumentation scope, caches it, and + forwards everything from then on. + + Args: + name: The name of the instrumenting module, passed on to + :meth:`LoggerProvider.get_logger`. + version: The version of the instrumenting module. + schema_url: The schema URL the emitted records follow. + attributes: Attributes describing the instrumentation scope. + """ + def __init__( # pylint: disable=super-init-not-called self, name: str, @@ -269,6 +403,11 @@ def __init__( # pylint: disable=super-init-not-called schema_url: str | None = None, attributes: Attributes = None, ): + """Record what to ask the global provider for once it exists. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._name = name self._version = version self._schema_url = schema_url @@ -278,6 +417,14 @@ def __init__( # pylint: disable=super-init-not-called @property def _logger(self) -> Logger: + """Return the logger to delegate to, resolving it on first use. + + Returns: + The logger obtained from the global :class:`LoggerProvider` once + one is set, cached thereafter. A :class:`NoOpLogger` until then, + built once and returned on every call, which the proxy never + adopts as its real logger, so a later provider is still picked up. + """ if self._real_logger: return self._real_logger @@ -326,6 +473,36 @@ def emit( event_name: str | None = None, exception: BaseException | None = None, ) -> None: + """Forward the log record to the real logger once one is available. + + Until a :class:`LoggerProvider` is set globally, the record is handed + to a :class:`NoOpLogger` and discarded. + + Args: + record: A fully built :class:`LogRecord` to emit. When it is + given, every keyword argument below is ignored. When it is + ``None``, a record is built from the keyword arguments. + timestamp: When the event occurred, as nanoseconds since the Unix + epoch. Left unset when omitted, so the record carries ``None`` + rather than a time the API invented. + observed_timestamp: When the event was observed by the collection + system, as nanoseconds since the Unix epoch. Defaults to the + time the record is built. + context: The context the record belongs to, used to associate it + with the active span. Defaults to the current context. + severity_number: The severity of the event on the OpenTelemetry + scale. + severity_text: The severity of the event as the originating system + spelled it, for example ``"WARN"``. + body: The payload of the event. A string for an ordinary log line, + or any other ``AnyValue`` for + structured content. + attributes: Further name/value pairs describing the event. + event_name: The name identifying the class of event, for a + structured event rather than a free-form log line. + exception: An exception to record alongside the event. Its type, + message and stack trace are attached to the record. + """ if record: self._logger.emit(record) else: @@ -348,6 +525,18 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: + """Ask the real logger whether a record would be processed. + + Args: + context: The context the record would belong to. Defaults to the + current context. + severity_number: The severity the record would carry. + event_name: The name of the event the record would carry. + + Returns: + Whatever the real logger reports, or ``False`` while no + :class:`LoggerProvider` has been set globally. + """ return self._logger.enabled( context=context, severity_number=severity_number, @@ -394,6 +583,9 @@ def get_logger( attributes: Optional. Specifies the instrumentation scope attributes to associate with emitted telemetry. + + Returns: + A `Logger` bound to the given instrumentation scope. """ @@ -407,11 +599,35 @@ def get_logger( schema_url: str | None = None, attributes: Attributes = None, ) -> Logger: - """Returns a NoOpLogger.""" + """Returns a NoOpLogger. + + Args: + name: The name of the instrumenting module, package or class, not + the name of the thing being instrumented. + version: The version string of the instrumenting library. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + + Returns: + A :class:`NoOpLogger` carrying the given scope, which discards + every record emitted through it. + """ return NoOpLogger(name, version=version, schema_url=schema_url, attributes=attributes) class ProxyLoggerProvider(LoggerProvider): + """The provider returned before a real one has been set globally. + + It holds no implementation of its own. Every logger this logger provider + hands out before a real logger provider is set is a :class:`ProxyLogger`, + which starts out discarding records and switches to the real logger + provider as soon as :func:`set_logger_provider` installs one. Every logger + handed out after that call comes from the real logger provider directly. + This is what lets a library call :func:`get_logger` at import time, before + the application has configured the SDK. + """ + def get_logger( self, name: str, @@ -419,6 +635,20 @@ def get_logger( schema_url: str | None = None, attributes: Attributes = None, ) -> Logger: + """Return a logger bound to the real provider once one exists. + + Args: + name: The name of the instrumenting module, package or class, not + the name of the thing being instrumented. + version: The version string of the instrumenting library. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + + Returns: + A logger from the global provider when one has been set, and a + :class:`ProxyLogger` carrying the given scope otherwise. + """ if _LOGGER_PROVIDER: return _LOGGER_PROVIDER.get_logger( name, @@ -440,7 +670,20 @@ def get_logger( def get_logger_provider() -> LoggerProvider: - """Gets the current global :class:`~.LoggerProvider` object.""" + """Gets the current global :class:`~.LoggerProvider` object. + + Returns: + The provider set by :func:`set_logger_provider`, or the one named by + the ``OTEL_PYTHON_LOGGER_PROVIDER`` environment variable. When neither + is present, a ``ProxyLoggerProvider`` is returned, which starts + delegating as soon as a provider is set. + + Raises: + Exception: Whatever loading the entry point named by the environment + variable raised, since ``_load_provider`` logs it and re-raises. A + name that matches no installed entry point surfaces as + :exc:`StopIteration`. + """ global _LOGGER_PROVIDER # pylint: disable=global-variable-not-assigned if _LOGGER_PROVIDER is None: if _OTEL_PYTHON_LOGGER_PROVIDER not in environ: @@ -456,7 +699,16 @@ def get_logger_provider() -> LoggerProvider: def _set_logger_provider(logger_provider: LoggerProvider, log: bool) -> None: + """Install the global provider, at most once per process. + + Args: + logger_provider: The provider to install. + log: Whether to log a warning when a provider is already installed and + this call therefore has no effect. + """ + def set_lp() -> None: + """Assign the global provider. Called at most once.""" global _LOGGER_PROVIDER # pylint: disable=global-statement _LOGGER_PROVIDER = logger_provider @@ -471,6 +723,9 @@ def set_logger_provider(logger_provider: LoggerProvider) -> None: This can only be done once, a warning will be logged if any further attempt is made. + + Args: + logger_provider: The provider to install globally. """ _set_logger_provider(logger_provider, log=True) @@ -488,6 +743,20 @@ def get_logger( opentelemetry.sdk._logs.LoggerProvider.get_logger. If logger_provider param is omitted the current configured one is used. + + Args: + instrumenting_module_name: The name of the instrumenting module, not + the name of the thing being instrumented. + instrumenting_library_version: The version string of the instrumenting + library. + logger_provider: The provider to obtain the logger from. Defaults to + the globally configured one. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + + Returns: + A `Logger` bound to the given instrumentation scope. """ if logger_provider is None: logger_provider = get_logger_provider() diff --git a/opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py index 96ffcc6229e..d7d700a112d 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py @@ -1,6 +1,15 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Severity levels of a log record. + +:class:`SeverityNumber` holds the numeric severity scale of the OpenTelemetry +log data model. The scale is ordered, so a larger value is a more severe +event, and it is finer grained than the levels of the standard :mod:`logging` +module: each familiar level has ``2``, ``3`` and ``4`` variants, for example +:attr:`~SeverityNumber.ERROR`, :attr:`~SeverityNumber.ERROR2`. +""" + import enum diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index f589b6b982e..1f1763d8430 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -3,6 +3,24 @@ # pyright: reportUnnecessaryIsInstance=false +"""Validation and size-limiting of telemetry attributes. + +The OpenTelemetry specification restricts which Python values may be used as +an attribute value, and lets an SDK cap both the number of attributes and the +length of each string. This module holds the two pieces that enforce those +rules: ``_clean_attribute_value``, which coerces or drops an invalid +value, and :class:`BoundedAttributes`, the mapping that spans, log records and +metric measurements store their attributes in. + +A value that cannot be represented is coerced to :class:`str` when the object +defines ``__str__`` or ``__repr__``, and is replaced with ``None`` otherwise. +The pair stays in the mapping either way and ``dropped`` is not incremented. A +key is treated differently: a key of a nested mapping that cannot be converted +is dropped along with its value. A warning is logged in every case. Note that +coercion calls ``__str__`` on the object, so a ``__str__`` that raises does +propagate to the caller. +""" + import copy import logging import threading @@ -21,6 +39,19 @@ # If neither is defined it uses the base class's `object.__repr__` method, which returns a string that is hard to understand. # So in that case we drop the key/value pair. def _is_non_custom_str(key: Any) -> bool: + """Report whether converting ``key`` to a string would produce something useful. + + Args: + key: The object about to be converted with :func:`str`. It is an + attribute key or an attribute value. + + Returns: + ``True`` when the type of ``key`` overrides ``__str__`` or + ``__repr__``, so :func:`str` yields a meaningful result. ``False`` when + neither is overridden, in which case :func:`str` would return the + default ```` form and the caller should + drop the object instead. + """ return type(key).__str__ is not object.__str__ or type(key).__repr__ is not object.__repr__ @@ -49,6 +80,17 @@ def _clean_attribute_value( If this fails, the value is replaced with None. Sequence's are converted to tuples and mappings are copied into new dicts. + A mapping key that is empty, or that is not a string and cannot be + converted to one, is dropped from the result and a warning is logged. + + Args: + value: The attribute value to clean. Any Python object is accepted; + one that is not a ``AnyValue`` is + coerced or replaced rather than rejected. + max_string_value_length: Maximum length of a string value, in + characters. A longer string is truncated to this length and a + warning is logged. Pass ``None`` to apply no limit. + Returns: The recursively cleaned AnyValue. """ @@ -144,6 +186,14 @@ def __init__( max_value_len: int | None = None, extended_attributes: bool = False, ) -> None: + """Build the mapping and store ``attributes`` in it. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + + Raises: + ValueError: If ``maxlen`` or ``max_value_len`` is negative. + """ if maxlen is not None and maxlen < 0: raise ValueError("maxlen must be valid int greater or equal to 0") if max_value_len is not None and max_value_len < 0: @@ -163,16 +213,52 @@ def __init__( self._immutable = immutable def __repr__(self) -> str: + """Render the stored attributes as a plain dict literal. + + Returns: + The current attributes formatted as a :class:`dict` would be. The + count of dropped attributes is not included. + """ return f"{dict(self._dict)}" def __getitem__(self, key: str) -> types.AnyValue: + """Return the value stored under ``key``. + + Args: + key: The attribute name to look up. + + Returns: + The cleaned value stored under ``key``. + + Raises: + KeyError: If ``key`` is absent, which includes the case where it + was dropped for exceeding the capacity. + """ return self._dict[key] def _raise_if_immutable(self) -> None: + """Reject a mutation when this instance was created immutable. + + Raises: + TypeError: If this instance is immutable. + """ if self._immutable: raise TypeError("Cannot mutate immutable BoundedAttributes") def __setitem__(self, key: str, value: types.AnyValue) -> None: + """Clean ``value`` and store it under ``key`` if there is capacity. + + An invalid key, or a key that does not fit within the capacity, is + counted in ``dropped`` rather than stored, and no exception is raised. + + Args: + key: The attribute name. It must be a non-empty :class:`str`. + value: The attribute value, cleaned by + ``_clean_attribute_value`` before being stored. + + Raises: + TypeError: If this instance is immutable. + """ self._raise_if_immutable() if self.maxlen is not None and self.maxlen == 0: with self._lock: @@ -191,6 +277,16 @@ def __setitem__(self, key: str, value: types.AnyValue) -> None: self._setitem_locked(key, cleaned) def _set_items(self, attributes: Mapping[str, types.AnyValue]) -> None: + """Clean and store several attributes under a single lock acquisition. + + Args: + attributes: The attributes to store. Each pair is cleaned by + ``_clean_attribute_value``, and any pair dropped during + cleaning is counted in ``dropped``. + + Raises: + TypeError: If this instance is immutable. + """ self._raise_if_immutable() if self.maxlen is not None and self.maxlen == 0: with self._lock: @@ -203,6 +299,18 @@ def _set_items(self, attributes: Mapping[str, types.AnyValue]) -> None: self._setitem_locked(key, value) def _setitem_locked(self, key: str, value: types.AnyValue) -> None: + """Store an already cleaned pair, assuming the lock is held. + + Re-setting a key that is already present moves it to the end and never + counts against the capacity. Adding a new key once the mapping is full + increments ``dropped`` and stores nothing. The full-mapping warning is + logged at most once per instance. + + Args: + key: The attribute name, already validated as a non-empty + :class:`str`. + value: The attribute value, already cleaned. + """ if key in self._dict: del self._dict[key] elif self.maxlen is not None and len(self._dict) >= self.maxlen: @@ -217,19 +325,56 @@ def _setitem_locked(self, key: str, value: types.AnyValue) -> None: self._dict[key] = value def __delitem__(self, key: str) -> None: + """Remove the attribute stored under ``key``. + + Args: + key: The attribute name to remove. + + Raises: + TypeError: If this instance is immutable. + KeyError: If ``key`` is absent. + """ self._raise_if_immutable() del self._dict[key] def __iter__(self): + """Iterate over the attribute names. + + A mutable instance is iterated over a snapshot of the names taken + under the lock, so adding an attribute from another thread during + iteration does not disturb it. + + Returns: + An iterator over the attribute names, in insertion order. + """ if self._immutable: return iter(self._dict) with self._lock: return iter(list(self._dict)) def __len__(self) -> int: + """Return how many attributes are stored. + + Returns: + The number of attributes currently held. Dropped attributes are + not counted; read ``dropped`` for those. + """ return len(self._dict) def __deepcopy__(self, memo: dict[int, Any]) -> "BoundedAttributes": + """Return a deep copy carrying the same limits and dropped count. + + The stored values are copied directly rather than re-cleaned, since + they were cleaned when they were first stored. + + Args: + memo: The memo dict :func:`copy.deepcopy` uses to track objects it + has already copied. + + Returns: + A new instance holding deep copies of the values, with the same + ``maxlen``, ``max_value_len``, immutability and ``dropped`` count. + """ copy_ = BoundedAttributes( maxlen=self.maxlen, immutable=self._immutable, @@ -244,4 +389,12 @@ def __deepcopy__(self, memo: dict[int, Any]) -> "BoundedAttributes": return copy_ def copy(self) -> MutableMapping[str, types.AnyValue]: + """Return the stored attributes as an ordinary mutable mapping. + + The result is a shallow copy and carries no capacity limit, so writing + to it neither affects this instance nor drops anything. + + Returns: + A plain :class:`dict` holding the current attributes. + """ return self._dict.copy() diff --git a/opentelemetry-api/src/opentelemetry/baggage/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/__init__.py index 6ad9067775f..d4137eb4387 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/__init__.py @@ -1,6 +1,23 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Name/value pairs that travel alongside a trace. + +Baggage is a set of name/value pairs carried in the current +:class:`~opentelemetry.context.Context` and propagated across process +boundaries by :class:`~opentelemetry.baggage.propagation.W3CBaggagePropagator`. +It lets a value set early in a request, such as an account identifier, be read +much later without threading it through every call. + +Every function here that changes the baggage returns a new +:class:`~opentelemetry.context.Context` rather than modifying one, matching the +immutability of the context API. The reading functions, :func:`get_all` and +:func:`get_baggage`, return the pairs themselves. + +Baggage is not added to spans automatically, and it is sent to every service +downstream, so a value that is sensitive or large does not belong in it. +""" + from collections.abc import Mapping from logging import getLogger from string import ascii_letters, digits @@ -92,6 +109,18 @@ def clear(context: Context | None = None) -> Context: def _get_baggage_value(context: Context | None = None) -> dict[str, object]: + """Read the raw baggage dict out of a context. + + Args: + context: The context to read from. If ``None``, the current context is + used. + + Returns: + The baggage dict stored in the context, or an empty dict when the + context holds no baggage or holds something that is not a dict. The + returned dict is the stored one, so callers that intend to modify it + must copy it first. + """ baggage = get_value(_BAGGAGE_KEY, context=context) if isinstance(baggage, dict): return baggage @@ -99,19 +128,60 @@ def _get_baggage_value(context: Context | None = None) -> dict[str, object]: def _is_valid_key(name: str) -> bool: + """Report whether ``name`` is usable as a baggage name. + + Args: + name: The candidate baggage name. + + Returns: + ``True`` when ``name`` is non-empty and made up only of RFC 7230 token + characters, which is what the W3C Baggage header format permits. + """ return bool(name) and _KEY_CHARS.issuperset(name) def _is_valid_property(property: str) -> bool: + """Report whether ``property`` is a valid baggage list-member property. + + A property is the optional metadata that may follow a baggage value after + a semicolon. It is either a bare key or a ``key=value`` pair. + + Args: + property: One property, with no leading semicolon. + + Returns: + ``True`` when the key half is a valid baggage name and the value half, + if present, holds only permitted characters. + """ key, separator, value = property.partition("=") return _is_valid_key(key.strip(" \t")) and (not separator or _VALUE_CHARS.issuperset(value.strip(" \t"))) def _is_valid_value(value: object) -> bool: + """Report whether ``value`` is usable as a baggage value. + + Args: + value: The candidate baggage value. It is converted with :func:`str` + before being checked, and any semicolon-delimited property that + follows it is checked too. + + Returns: + ``True`` when the value holds only printable US-ASCII characters other + than the delimiters, and every property following it is valid. + """ value, *properties = str(value).split(";") # W3C Baggage allows optional whitespace between the value and ";" return _VALUE_CHARS.issuperset(value.rstrip(" \t")) and all(map(_is_valid_property, properties)) def _is_valid_pair(key: str, value: str) -> bool: + """Report whether a name/value pair may be put in a baggage header. + + Args: + key: The candidate baggage name. + value: The candidate baggage value. + + Returns: + ``True`` when both halves are valid on their own. + """ return _is_valid_key(key) and _is_valid_value(value) diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 953128a45f6..287b63a2f8c 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -1,6 +1,21 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # +"""Serialization of baggage into the W3C ``baggage`` HTTP header. + +:class:`W3CBaggagePropagator` writes the baggage held in a context into the +``baggage`` header when a request is sent, and reads it back out when one is +received, following https://www.w3.org/TR/baggage/. + +The format caps the size of the header, of each list-member, and the number of +list-members. Nothing here raises, so oversized baggage degrades the telemetry +instead of the request. Which entries go is not uniform: an entry that holds a +non-ASCII character, fails validation or exceeds the per-entry limit is dropped +on its own with a warning, while exceeding the entry count or the total header +length stops processing there and discards every remaining entry, including +entries that would have fitted. +""" + from collections.abc import Iterable, Iterator, Mapping from logging import getLogger from urllib.parse import quote, unquote @@ -22,6 +37,18 @@ def _filter_valid_entries( entries: Iterable[str], max_pair_length: int, ) -> Iterator[str]: + """Drop the baggage entries that cannot appear in a header. + + An empty entry is skipped silently. An entry holding a non-ASCII character + or exceeding ``max_pair_length`` is skipped with a warning naming its key. + + Args: + entries: The candidate entries, each one a ``key=value`` string. + max_pair_length: Maximum length of a single entry, in characters. + + Yields: + Each entry that passes every check, in the order received. + """ for entry in entries: if not entry: continue @@ -51,6 +78,20 @@ def _apply_baggage_limits( Yields entries that fit within the W3C specification limits. Logs warnings when entries are dropped. + + Iteration stops at the first entry that would exceed ``max_pairs`` or + ``max_header_length``, so every later entry is dropped as well even if it + would have fitted on its own. + + Args: + entries: The candidate entries, each one a ``key=value`` string. + max_pairs: Maximum number of entries to yield. + max_pair_length: Maximum length of a single entry, in characters. + max_header_length: Maximum combined length of the entries, counting + the comma that joins them. + + Yields: + Each entry that fits within every limit, in the order received. """ length = 0 for index, entry in enumerate(_filter_valid_entries(entries, max_pair_length)): @@ -83,6 +124,24 @@ def extract( See `opentelemetry.propagators.textmap.TextMapPropagator.extract` + + A list-member that does not parse, that fails validation, that + holds a non-ASCII character or that is longer than the per-entry + limit is skipped on its own, so the rest of the header still + extracts. Exceeding the entry count or the total header length is + different: extraction stops there and every remaining entry is + discarded, including entries that would have fitted. + + Args: + carrier: The object holding the incoming headers, for example the + header mapping of an HTTP request. + context: The context to add the extracted baggage to. If ``None``, + the current context is used. + getter: Reads the ``baggage`` header values out of ``carrier``. + + Returns: + A new context holding the baggage found in ``carrier``. When the + header is absent or empty, the context is returned unchanged. """ if context is None: @@ -134,6 +193,17 @@ def inject( See `opentelemetry.propagators.textmap.TextMapPropagator.inject` + + Nothing is written when the context holds no baggage, and nothing is + written when every entry was dropped, whether for carrying a key that + is not a valid baggage token or for breaking a size limit. + + Args: + carrier: The object to write the outgoing header into, for example + the header mapping of an outbound HTTP request. + context: The context to read the baggage from. If ``None``, the + current context is used. + setter: Writes the ``baggage`` header into ``carrier``. """ baggage_entries = get_all(context=context) if not baggage_entries: @@ -153,12 +223,29 @@ def inject( @property def fields(self) -> set[str]: - """Returns a set with the fields set in `inject`.""" + """Returns a set with the fields set in `inject`. + + Returns: + A set holding the single name ``"baggage"``. A caller clearing a + carrier before injecting into it should remove these names. + """ return {self._BAGGAGE_HEADER_NAME} def _split_baggage_entries(headers: Iterable[str]) -> Iterator[str]: - """Yield the list-members of all baggage headers, combined as a single baggage string.""" + """Yield the list-members of all baggage headers, combined as a single baggage string. + + Several ``baggage`` headers on one message are treated as one list, which + is what the header format requires. Surrounding whitespace is stripped + from each list-member. + + Args: + headers: The raw values of every ``baggage`` header received. + + Yields: + Each comma-separated list-member, stripped of surrounding whitespace. + An empty header yields one empty string, which the caller filters out. + """ for header in headers: start = 0 while True: @@ -173,7 +260,21 @@ def _split_baggage_entries(headers: Iterable[str]) -> Iterator[str]: def _encode_baggage_pairs( baggage_entries: Mapping[str, object], ) -> Iterator[str]: - """Yield 'key=value' pairs with percent-encoded values from baggage entries.""" + """Yield 'key=value' pairs with percent-encoded values from baggage entries. + + A pair whose key is not a valid baggage name is skipped with a warning. + Values are percent-encoded, so a value containing a comma, a semicolon or + an equals sign survives the round trip. + + Args: + baggage_entries: The baggage held in a context, as returned by + :func:`opentelemetry.baggage.get_all`. Each value is converted + with :func:`str` before being encoded. + + Yields: + One ``key=value`` string per valid entry, with the value + percent-encoded. + """ for key, value in baggage_entries.items(): if not _is_valid_key(key): _logger.warning("Baggage key `%s` is not a valid token", key) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 9bd2084d3c4..9020427bb8b 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -1,6 +1,27 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Storage for the values that travel with the current execution. + +The context API lets a cross-cutting concern, such as tracing or baggage, keep +its own state without passing it through every function signature. A value is +stored under a key obtained from :func:`create_key`, read with +:func:`get_value` and replaced with :func:`set_value`, which returns a new +:class:`Context` rather than modifying the existing one. + +:func:`attach` makes a context current and returns a token, and :func:`detach` +restores what was current before. The pair is normally used through a context +manager rather than called directly. + +The implementation backing :func:`get_current` is chosen once at import time. +It is ``ContextVarsRuntimeContext`` +unless the ``OTEL_PYTHON_CONTEXT`` environment variable names a different +``opentelemetry_context`` entry point. A named entry point that cannot be +found or cannot be constructed is logged and falls back to +``ContextVarsRuntimeContext`` as well, so setting the variable is not by itself +proof that the named implementation is in use. +""" + from __future__ import annotations import logging @@ -48,8 +69,10 @@ def create_key(keyname: str) -> str: """To allow cross-cutting concern to control access to their local state, the RuntimeContext API provides a function which takes a keyname as input, and returns a unique key. + Args: keyname: The key name is for debugging purposes and is not required to be unique. + Returns: A unique string representing the newly created key. """ diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 984bf424d84..6b6dc3d6a79 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -1,6 +1,15 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""The immutable context mapping and the runtime that stores it. + +A :class:`Context` carries the values that travel with the current execution, +such as the active span and the current baggage. It is a read-only +:class:`dict`, so adding a value produces a new :class:`Context` rather than +changing the existing one, which is what makes a context safe to share between +concurrent tasks. +""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -8,28 +17,113 @@ class Context(dict[str, object]): + """An immutable mapping from context key to value. + + Every mutating method of :class:`dict` is overridden to raise + :exc:`ValueError`. Build a new :class:`Context` with + :func:`opentelemetry.context.set_value` instead of modifying one in place, + and read from it with :func:`opentelemetry.context.get_value`. + """ + def __setitem__(self, key: str, value: object) -> None: + """Reject assignment of a single key. + + Args: + key: The context key that would have been set. + value: The value that would have been stored. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError def __delitem__(self, key: str) -> None: + """Reject deletion of a single key. + + Args: + key: The context key that would have been removed. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError def setdefault(self, key: str, default: object = None) -> object: + """Reject insertion of a default value. + + Args: + key: The context key that would have been looked up. + default: The value that would have been stored if ``key`` were + absent. + + Returns: + Never returns. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError def pop(self, key: str, *args: object) -> object: + """Reject removal of a key. + + Args: + key: The context key that would have been removed. + *args: An optional default to return when ``key`` is absent, as + accepted by :meth:`dict.pop`. + + Returns: + Never returns. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError def popitem(self) -> tuple[str, object]: + """Reject removal of an arbitrary pair. + + Returns: + Never returns. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError def clear(self) -> None: + """Reject removal of every key. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError def update(self, *args: object, **kwargs: object) -> None: + """Reject a bulk update. + + Args: + *args: A mapping or iterable of pairs, as accepted by + :meth:`dict.update`. + **kwargs: Further pairs that would have been stored. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError def __ior__(self, other: object) -> Context: + """Reject an in-place merge with ``other``. + + Args: + other: The mapping that would have been merged in. + + Returns: + Never returns. + + Raises: + ValueError: Always, because a context is immutable. + """ raise ValueError @@ -47,11 +141,20 @@ def attach(self, context: Context) -> Token[Context]: Args: context: The Context to set. + + Returns: + A token to pass to :meth:`detach` to restore the context that was + current before this call. """ @abstractmethod def get_current(self) -> Context: - """Returns the current `Context` object.""" + """Returns the current `Context` object. + + Returns: + The context attached to the current execution, or an empty + :class:`Context` when nothing has been attached. + """ @abstractmethod def detach(self, token: Token[Context]) -> None: diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 3cbcdd9ccc3..e04f944b14d 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -1,6 +1,16 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Runtime context backed by :mod:`contextvars`. + +This is the only runtime context implementation shipped with the API. Storing +the current :class:`~opentelemetry.context.Context` in a +:class:`~contextvars.ContextVar` gives each thread and each task its own view, +so a span started in one task does not become current in another. A generator +is not included: a generator runs in the context of whoever resumes it, so a +value the generator attaches stays visible to its caller. +""" + from __future__ import annotations from contextvars import ContextVar, Token @@ -16,6 +26,11 @@ class ContextVarsRuntimeContext(_RuntimeContext): _CONTEXT_KEY = "current_context" def __init__(self) -> None: + """Create the backing :class:`~contextvars.ContextVar`. + + The variable starts out holding an empty + :class:`~opentelemetry.context.Context`. + """ self._current_context = ContextVar(self._CONTEXT_KEY, default=Context()) def attach(self, context: Context) -> Token[Context]: @@ -24,11 +39,21 @@ def attach(self, context: Context) -> Token[Context]: Args: context: The Context to set. + + Returns: + A token to pass to :meth:`detach` to restore the context that was + current before this call. """ return self._current_context.set(context) def get_current(self) -> Context: - """Returns the current `Context` object.""" + """Returns the current `Context` object. + + Returns: + The context attached to the current execution, or an empty + :class:`~opentelemetry.context.Context` when nothing has been + attached. + """ return self._current_context.get() def detach(self, token: Token[Context]) -> None: @@ -36,6 +61,13 @@ def detach(self, token: Token[Context]) -> None: Args: token: A reference to a previous Context. + + Raises: + ValueError: When ``token`` came from a different + :class:`ContextVarsRuntimeContext` instance, since each one + owns its own :class:`~contextvars.ContextVar`. + RuntimeError: When ``token`` has already been passed to this + method once. """ self._current_context.reset(token) diff --git a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py index b335c826380..600a83502c9 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py @@ -1,10 +1,43 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Names of the ``OTEL_*`` environment variables the API package defines. + +Each name in this module is bound to a constant so that code refers to the +environment variable by symbol rather than by a repeated string literal. +Defining a name here does not mean the API reads it: ``OTEL_LOGS_EXPORTER``, +``OTEL_METRICS_EXPORTER``, ``OTEL_TRACES_EXPORTER`` and +``OTEL_PYTHON_ID_GENERATOR`` are defined here and read only by +``opentelemetry.sdk._configuration``. The environment variables the SDK both +defines and reads live in :mod:`opentelemetry.sdk.environment_variables`. +""" + OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER" """ .. envvar:: OTEL_LOGS_EXPORTER +Specifies which exporter is used for logs. See `General SDK Configuration +`__. + +**Default value:** ``"otlp"`` + +**Example:** + +``export OTEL_LOGS_EXPORTER="console"`` + +Accepted values for ``OTEL_LOGS_EXPORTER`` are: + +- ``"otlp"`` +- ``"console"`` +- ``"none"``: No automatically configured exporter for logs. + +.. note:: + + Exporter packages may add entry points for group ``opentelemetry_logs_exporter`` which + can then be used with this environment variable by name. The entry point should point to + a `opentelemetry.sdk._logs.export.LogRecordExporter` subclass; it must be constructable + without any required arguments. This mechanism is considered experimental and may change + in the future. """ OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py index 183966b1b7b..acdc97df0a7 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-ancestors +# pylint: disable=too-many-lines """ The OpenTelemetry metrics API describes the classes used to generate @@ -124,6 +125,10 @@ def get_meter( schema_url: Optional. Specifies the Schema URL of the emitted telemetry. attributes: Optional. Attributes that are associated with the emitted telemetry. + + Returns: + A `Meter` that creates instruments attributed to the named + instrumenting module. """ @@ -137,12 +142,37 @@ def get_meter( schema_url: str | None = None, attributes: Attributes = None, ) -> "Meter": - """Returns a NoOpMeter.""" + """Returns a NoOpMeter. + + Args: + name: The name of the instrumenting module. + version: Optional. The version string of the instrumenting + library. + schema_url: Optional. Specifies the Schema URL of the emitted + telemetry. + attributes: Optional. Accepted and discarded: the `NoOpMeter` is + built without them. + + Returns: + A `NoOpMeter`, which discards every measurement made through the + instruments it creates. + """ return NoOpMeter(name, version=version, schema_url=schema_url) class _ProxyMeterProvider(MeterProvider): + """The meter provider in use before a real one is set. + + Every meter this meter provider hands out before a real meter provider is + set is a `_ProxyMeter`. When a real meter provider is finally set, + `on_set_meter_provider` gives each of those proxy meters a real backing + meter, so instruments created before the SDK was installed start recording + from that moment on. Every meter handed out after that call comes from the + real meter provider directly. + """ + def __init__(self) -> None: + """Build the proxy meter provider with no real one behind it yet.""" self._lock = Lock() self._meters: list[_ProxyMeter] = [] self._real_meter_provider: MeterProvider | None = None @@ -154,6 +184,23 @@ def get_meter( schema_url: str | None = None, attributes: Attributes = None, ) -> "Meter": + """Return a meter, proxying until a real meter provider is set. + + Args: + name: The name of the instrumenting module. + version: Optional. The version string of the instrumenting + library. + schema_url: Optional. Specifies the Schema URL of the emitted + telemetry. + attributes: Optional. Accepted and discarded: neither the + `_ProxyMeter` nor the real meter provider is given them, so a + scope attribute supplied here never reaches the telemetry. + + Returns: + A meter from the real meter provider when one has been set, and a + `_ProxyMeter` otherwise. The proxy is remembered so that it can be + given a real backing meter later. + """ with self._lock: if self._real_meter_provider is not None: return self._real_meter_provider.get_meter(name, version, schema_url) @@ -163,6 +210,11 @@ def get_meter( return meter def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + """Give every meter handed out so far a real backing meter. + + Args: + meter_provider: The real meter provider that has just been set. + """ with self._lock: self._real_meter_provider = meter_provider for meter in self._meters: @@ -171,6 +223,19 @@ def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: @dataclass class _InstrumentRegistrationStatus: + """What a meter's instrument registry held for one identity before a call. + + Args: + instrument_id: The identity the instrument was registered under, + built from its name, type, unit and description. + already_registered: Whether an instrument of this identity had been + created on the same meter before. + conflict: Whether that earlier instrument carried a different + advisory from the one now requested. + current_advisory: The advisory the earlier instrument was registered + with, when there is a conflict. + """ + instrument_id: str already_registered: bool conflict: bool @@ -182,6 +247,12 @@ class Meter(ABC): This class provides methods for creating instruments which are then used to produce measurements. + + Args: + name: The name of the instrumenting module. + version: The version string of the instrumenting library, or + ``None`` when it has none. + schema_url: The Schema URL of the emitted telemetry, or ``None``. """ def __init__( @@ -190,6 +261,11 @@ def __init__( version: str | None = None, schema_url: str | None = None, ) -> None: + """Build the meter. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__() self._name = name self._version = version @@ -201,6 +277,9 @@ def __init__( def name(self) -> str: """ The name of the instrumenting module. + + Returns: + The name this meter was created with. """ return self._name @@ -208,6 +287,10 @@ def name(self) -> str: def version(self) -> str | None: """ The version string of the instrumenting library. + + Returns: + The version this meter was created with, or ``None`` when it was + created without one. """ return self._version @@ -215,6 +298,10 @@ def version(self) -> str | None: def schema_url(self) -> str | None: """ Specifies the Schema URL of the emitted telemetry + + Returns: + The Schema URL this meter was created with, or ``None`` when it + was created without one. """ return self._schema_url @@ -230,12 +317,22 @@ def _register_instrument( Register an instrument with the name, type, unit and description as identifying keys and the advisory as value. - Returns a tuple. The first value is the instrument id. - The second value is an `_InstrumentRegistrationStatus` where - `already_registered` is `True` if the instrument has been registered - already. - If `conflict` is set to True the `current_advisory` attribute contains - the registered instrument advisory. + Args: + name: The name of the instrument being created. + type_: The class of the instrument being created. + unit: The unit of the instrument being created. + description: The description of the instrument being created. + advisory: The advisory of the instrument being created, for the + instrument types that take one. + + Returns: + An `_InstrumentRegistrationStatus` describing what the registry + held for this identity before the call. Its ``instrument_id`` + attribute carries the identity the registry keyed on. Its + ``already_registered`` attribute is ``True`` when an instrument + with that identity was registered before. When its ``conflict`` + attribute is ``True``, its ``current_advisory`` attribute carries + the advisory of the instrument that was already registered. """ instrument_id = ",".join([name.strip().lower(), type_.__name__, unit, description]) @@ -268,6 +365,20 @@ def _log_instrument_registration_conflict( description: str, status: _InstrumentRegistrationStatus, ) -> None: + """Warn that an instrument of this identity already exists. + + The instrument that was registered first keeps its advisory, and the + advisory passed to the later call is ignored. + + Args: + name: The name of the instrument being created. + instrumentation_type: The name of the class of the instrument + being created. + unit: The unit of the instrument being created. + description: The description of the instrument being created. + status: The status returned by `_register_instrument` for the + instrument being created. + """ _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already with a " @@ -293,6 +404,10 @@ def create_counter( unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. + Returns: + A :class:`~opentelemetry.metrics.Counter`. Which implementation is + returned depends on the meter: a recording one when an SDK is + installed, a no-op one otherwise. """ @abstractmethod @@ -309,6 +424,10 @@ def create_up_down_counter( unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. + Returns: + A :class:`~opentelemetry.metrics.UpDownCounter`. Which implementation is + returned depends on the meter: a recording one when an SDK is + installed, a no-op one otherwise. """ @abstractmethod @@ -453,6 +572,10 @@ def scrape_http_callback( unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. + Returns: + A :class:`~opentelemetry.metrics.ObservableCounter`. Which implementation is + returned depends on the meter: a recording one when an SDK is + installed, a no-op one otherwise. """ @abstractmethod @@ -471,6 +594,15 @@ def create_histogram( unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. + explicit_bucket_boundaries_advisory: The bucket boundaries this + instrument suggests to the SDK, in ascending order. ``None`` + leaves the choice to the SDK, and a view configured on the SDK + overrides the suggestion either way. + + Returns: + A :class:`~opentelemetry.metrics.Histogram`. Which implementation + is returned depends on the meter: a recording one when an SDK is + installed, a no-op one otherwise. """ def create_gauge( # type: ignore # pylint: disable=no-self-use @@ -486,6 +618,13 @@ def create_gauge( # type: ignore # pylint: disable=no-self-use unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. + + Returns: + A :class:`~opentelemetry.metrics.Gauge` from a meter that + overrides this method. This method is not abstract and its own + body only emits a warning, so a meter that inherits it returns + ``None`` and a caller that goes on to record a measurement raises + :exc:`AttributeError`. """ warnings.warn("create_gauge() is not implemented and will be a no-op") @@ -507,6 +646,10 @@ def create_observable_gauge( unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. + Returns: + A :class:`~opentelemetry.metrics.ObservableGauge`. Which implementation is + returned depends on the meter: a recording one when an SDK is + installed, a no-op one otherwise. """ @abstractmethod @@ -527,16 +670,40 @@ def create_observable_up_down_counter( unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. + Returns: + A :class:`~opentelemetry.metrics.ObservableUpDownCounter`. Which implementation is + returned depends on the meter: a recording one when an SDK is + installed, a no-op one otherwise. """ class _ProxyMeter(Meter): + """The meter a `_ProxyMeterProvider` hands out before a real one is set. + + Every instrument this meter creates before a real meter is set is a proxy + instrument that records nothing. When `on_set_meter_provider` is called, + this meter acquires a real backing meter and each of those proxy + instruments acquires a real backing instrument. Every instrument this meter + creates after that call comes from the real meter directly. + + Args: + name: The name of the instrumenting module. + version: The version string of the instrumenting library, or + ``None`` when it has none. + schema_url: The Schema URL of the emitted telemetry, or ``None``. + """ + def __init__( self, name: str, version: str | None = None, schema_url: str | None = None, ) -> None: + """Build the proxy meter with no real meter behind it yet. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(name, version=version, schema_url=schema_url) self._lock = Lock() self._instruments: list[_ProxyInstrumentT] = [] @@ -547,6 +714,9 @@ def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: Creates a real backing meter for this instance and notifies all created instruments so they can create real backing instruments. + + Args: + meter_provider: The real meter provider that has just been set. """ real_meter = meter_provider.get_meter(self._name, self._version, self._schema_url) @@ -563,6 +733,19 @@ def create_counter( unit: str = "", description: str = "", ) -> Counter: + """Create a counter, proxying until a real meter is set. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A counter from the real meter when one has been set, and a proxy + counter otherwise. The proxy is remembered so that it can be given + a real backing instrument later. + """ with self._lock: if self._real_meter: return self._real_meter.create_counter(name, unit, description) @@ -576,6 +759,19 @@ def create_up_down_counter( unit: str = "", description: str = "", ) -> UpDownCounter: + """Create a up-down counter, proxying until a real meter is set. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A up-down counter from the real meter when one has been set, and a proxy + up-down counter otherwise. The proxy is remembered so that it can be given + a real backing instrument later. + """ with self._lock: if self._real_meter: return self._real_meter.create_up_down_counter(name, unit, description) @@ -590,6 +786,21 @@ def create_observable_counter( unit: str = "", description: str = "", ) -> ObservableCounter: + """Create a observable counter, proxying until a real meter is set. + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A observable counter from the real meter when one has been set, and a proxy + observable counter otherwise. The proxy is remembered so that it can be given + a real backing instrument later. + """ with self._lock: if self._real_meter: return self._real_meter.create_observable_counter(name, callbacks, unit, description) @@ -605,6 +816,21 @@ def create_histogram( *, explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> Histogram: + """Create a histogram, proxying until a real meter is set. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + explicit_bucket_boundaries_advisory: The bucket boundaries this + instrument suggests to the SDK, in ascending order. + + Returns: + A histogram from the real meter when one has been set, and a proxy + histogram otherwise. The proxy is remembered so that it can be given + a real backing instrument later. + """ with self._lock: if self._real_meter: return self._real_meter.create_histogram( @@ -623,6 +849,19 @@ def create_gauge( unit: str = "", description: str = "", ) -> Gauge: + """Create a gauge, proxying until a real meter is set. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A gauge from the real meter when one has been set, and a proxy + gauge otherwise. The proxy is remembered so that it can be given + a real backing instrument later. + """ with self._lock: if self._real_meter: return self._real_meter.create_gauge(name, unit, description) @@ -637,6 +876,21 @@ def create_observable_gauge( unit: str = "", description: str = "", ) -> ObservableGauge: + """Create a observable gauge, proxying until a real meter is set. + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A observable gauge from the real meter when one has been set, and a proxy + observable gauge otherwise. The proxy is remembered so that it can be given + a real backing instrument later. + """ with self._lock: if self._real_meter: return self._real_meter.create_observable_gauge(name, callbacks, unit, description) @@ -651,6 +905,21 @@ def create_observable_up_down_counter( unit: str = "", description: str = "", ) -> ObservableUpDownCounter: + """Create a observable up-down counter, proxying until a real meter is set. + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A observable up-down counter from the real meter when one has been set, and a proxy + observable up-down counter otherwise. The proxy is remembered so that it can be given + a real backing instrument later. + """ with self._lock: if self._real_meter: return self._real_meter.create_observable_up_down_counter( @@ -676,7 +945,18 @@ def create_counter( unit: str = "", description: str = "", ) -> Counter: - """Returns a no-op Counter.""" + """Returns a no-op Counter. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A `NoOpCounter`, which discards every measurement made + through it. + """ status = self._register_instrument(name, NoOpCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( @@ -695,7 +975,18 @@ def create_gauge( unit: str = "", description: str = "", ) -> Gauge: - """Returns a no-op Gauge.""" + """Returns a no-op Gauge. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A `NoOpGauge`, which discards every measurement made + through it. + """ status = self._register_instrument(name, NoOpGauge, unit, description) if status.conflict: self._log_instrument_registration_conflict( @@ -713,7 +1004,18 @@ def create_up_down_counter( unit: str = "", description: str = "", ) -> UpDownCounter: - """Returns a no-op UpDownCounter.""" + """Returns a no-op UpDownCounter. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A `NoOpUpDownCounter`, which discards every measurement made + through it. + """ status = self._register_instrument(name, NoOpUpDownCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( @@ -732,7 +1034,20 @@ def create_observable_counter( unit: str = "", description: str = "", ) -> ObservableCounter: - """Returns a no-op ObservableCounter.""" + """Returns a no-op ObservableCounter. + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A `NoOpObservableCounter`, which discards every measurement made + through it. + """ status = self._register_instrument(name, NoOpObservableCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( @@ -757,7 +1072,20 @@ def create_histogram( *, explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> Histogram: - """Returns a no-op Histogram.""" + """Returns a no-op Histogram. + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + explicit_bucket_boundaries_advisory: The bucket boundaries this + instrument suggests to the SDK, in ascending order. + + Returns: + A `NoOpHistogram`, which discards every measurement made + through it. + """ status = self._register_instrument( name, NoOpHistogram, @@ -787,7 +1115,20 @@ def create_observable_gauge( unit: str = "", description: str = "", ) -> ObservableGauge: - """Returns a no-op ObservableGauge.""" + """Returns a no-op ObservableGauge. + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A `NoOpObservableGauge`, which discards every measurement made + through it. + """ status = self._register_instrument(name, NoOpObservableGauge, unit, description) if status.conflict: self._log_instrument_registration_conflict( @@ -811,7 +1152,20 @@ def create_observable_up_down_counter( unit: str = "", description: str = "", ) -> ObservableUpDownCounter: - """Returns a no-op ObservableUpDownCounter.""" + """Returns a no-op ObservableUpDownCounter. + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + + Returns: + A `NoOpObservableUpDownCounter`, which discards every measurement made + through it. + """ status = self._register_instrument(name, NoOpObservableUpDownCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( @@ -847,6 +1201,22 @@ def get_meter( `opentelemetry.metrics.MeterProvider.get_meter`. If meter_provider is omitted the current configured one is used. + + Args: + name: The name of the instrumenting module. See + `opentelemetry.metrics.MeterProvider.get_meter` for how to choose + it. + version: The version string of the instrumenting library. + meter_provider: The meter provider to ask. If ``None``, the globally + configured one is used. + schema_url: Optional. Specifies the Schema URL of the emitted + telemetry. + attributes: Optional. Attributes that are associated with the emitted + telemetry. + + Returns: + A `Meter` that creates instruments attributed to the named + instrumenting module. """ if meter_provider is None: meter_provider = get_meter_provider() @@ -854,7 +1224,18 @@ def get_meter( def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: + """Set the global meter provider, the first call winning. + + Args: + meter_provider: The meter provider to make global. + log: Whether to log a warning when a provider has already been set, so + that this call has no effect. The call that loads a provider from + :envvar:`OTEL_PYTHON_METER_PROVIDER` passes ``False``, since it + races with an explicit `set_meter_provider` by design. + """ + def set_mp() -> None: + """Store the meter provider and hand every proxy a real backing one.""" global _METER_PROVIDER # pylint: disable=global-statement _METER_PROVIDER = meter_provider @@ -872,12 +1253,32 @@ def set_meter_provider(meter_provider: MeterProvider) -> None: This can only be done once, a warning will be logged if any further attempt is made. + + Args: + meter_provider: The meter provider to make global. """ _set_meter_provider(meter_provider, log=True) def get_meter_provider() -> MeterProvider: - """Gets the current global :class:`~.MeterProvider` object.""" + """Gets the current global :class:`~.MeterProvider` object. + + When none has been set, the provider named by + :envvar:`OTEL_PYTHON_METER_PROVIDER` is loaded and set, if that + environment variable is present. + + Returns: + The global meter provider. When none has been set and the environment + variable is absent, a proxy meter provider is returned instead, so + that instruments created before an SDK is installed start recording + once one is. + + Raises: + Exception: Whatever loading the entry point named by the environment + variable raised, since ``_load_provider`` logs it and re-raises. A + name that matches no installed entry point surfaces as + :exc:`StopIteration`. + """ if _METER_PROVIDER is None: if OTEL_PYTHON_METER_PROVIDER not in environ: diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index 5ebe05e891c..f1ccb0ce568 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -2,7 +2,22 @@ # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-ancestors +# pylint: disable=too-many-lines +"""The instrument types a meter hands out, and their no-op implementations. + +An instrument records measurements of one kind. A synchronous instrument is +called at the moment something happens, for example +:meth:`~opentelemetry.metrics.Counter.add`. An asynchronous instrument is +given callbacks instead, and the SDK invokes them when it collects. + +Each instrument type here has two implementations beside the abstract one. The +``NoOp`` variant discards every measurement and is what a +:class:`~opentelemetry.metrics.NoOpMeter` returns. The ``_Proxy`` variant is +what a meter returns before a real +:class:`~opentelemetry.metrics.MeterProvider` is set, which is the state with +no SDK installed, and it swaps itself for a real instrument once one is set. +""" from abc import ABC, abstractmethod from collections.abc import Callable, Generator, Iterable, Sequence @@ -30,6 +45,17 @@ @dataclass(frozen=True) class _MetricsHistogramAdvisory: + """A hint to the SDK about how a histogram should bucket its values. + + An advisory is a suggestion rather than a setting. A view configured on + the SDK overrides it. + + Args: + explicit_bucket_boundaries: The bucket boundaries the instrument + suggests, in ascending order. ``None`` leaves the choice to the + SDK. + """ + explicit_bucket_boundaries: Sequence[float] | None = None @@ -51,7 +77,16 @@ class CallbackOptions: class Instrument(ABC): - """Abstract class that serves as base for all instruments.""" + """Abstract class that serves as base for all instruments. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ @abstractmethod def __init__( @@ -60,7 +95,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: - pass + """Build the instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ @staticmethod def _check_name_unit_description(name: str, unit: str, description: str) -> dict[str, str | None]: @@ -72,6 +111,19 @@ def _check_name_unit_description(name: str, unit: str, description: str) -> dict corresponding values will be the checked strings or `None` if the value is invalid. If valid, the checked strings should be used instead of the original values. + + Args: + name: The candidate instrument name. + unit: The candidate unit. A ``None`` reaching this method despite + the annotation is treated as the empty string. + description: The candidate description. A ``None`` reaching this + method despite the annotation is treated as the empty string. + + Returns: + A dict with the keys ``"name"``, ``"unit"`` and ``"description"``. + Each value is the checked string, or ``None`` when that part + failed validation. The description is never ``None``, since any + string is accepted. """ result: dict[str, str | None] = {} @@ -97,19 +149,47 @@ def _check_name_unit_description(name: str, unit: str, description: str) -> dict class _ProxyInstrument(ABC, Generic[InstrumentT]): + """An instrument that stands in until a real meter provider is set. + + It records nothing of its own. Once a real provider is installed, the + creating meter calls :meth:`on_meter_set`, and every measurement made after + that call returns is forwarded to a real instrument built from the stored + name, unit and description, together with whatever else the subclass stored: + the callbacks for an asynchronous instrument, the bucket advisory for a + histogram. A measurement made while the real instrument is being built is + discarded, since no lock is held across its creation. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ + def __init__( self, name: str, unit: str = "", description: str = "", ) -> None: + """Record what to build the real instrument from, once one is possible. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._name = name self._unit = unit self._description = description self._real_instrument: InstrumentT | None = None def on_meter_set(self, meter: "metrics.Meter") -> None: - """Called when a real meter is set on the creating _ProxyMeter""" + """Called when a real meter is set on the creating _ProxyMeter + + Args: + meter: The real meter to build the backing instrument from. + """ # We don't need any locking on proxy instruments because it's OK if some # measurements get dropped while a real backing instrument is being @@ -118,10 +198,33 @@ def on_meter_set(self, meter: "metrics.Meter") -> None: @abstractmethod def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: - """Create an instance of the real instrument. Implement this.""" + """Create an instance of the real instrument. Implement this. + + Args: + meter: The real meter to create the instrument from. + + Returns: + A real instrument of the type this proxy stands in for, carrying + the name, unit and description the proxy was built with, and + whatever else the implementing subclass stored. + """ class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): + """A proxy instrument that also carries callbacks to the real instrument. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + callbacks: The callbacks to invoke on each collection. Each one + receives a :class:`CallbackOptions` and returns an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ + def __init__( self, name: str, @@ -129,6 +232,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Record what to build the real instrument from, including callbacks. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(name, unit, description) self._callbacks = callbacks @@ -138,7 +246,19 @@ class Synchronous(Instrument): class Asynchronous(Instrument): - """Base class for all asynchronous instruments""" + """Base class for all asynchronous instruments + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + callbacks: The callbacks to invoke on each collection. Each one + receives a :class:`CallbackOptions` and returns an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ @abstractmethod def __init__( @@ -148,6 +268,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Build the instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(name, unit=unit, description=description) @@ -172,7 +297,16 @@ def add( class NoOpCounter(Counter): - """No-op implementation of `Counter`.""" + """No-op implementation of `Counter`. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ def __init__( self, @@ -180,6 +314,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Build the no-op instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(name, unit=unit, description=description) def add( @@ -188,20 +327,57 @@ def add( attributes: Attributes = None, context: Context | None = None, ) -> None: + """Discard the measurement. + + The call reaches the abstract method of the base class, whose body is + empty, so no argument is read and nothing is recorded anywhere. + + Args: + amount: Ignored. + attributes: Ignored. + context: Ignored. + """ return super().add(amount, attributes=attributes, context=context) class _ProxyCounter(_ProxyInstrument[Counter], Counter): + """A counter that stands in until a real meter provider is set. + + It records nothing of its own until one is. + """ + def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: + """Forward the measurement to the real instrument, if there is one. + + The measurement is discarded while no real + :class:`~opentelemetry.metrics.MeterProvider` has been set. + + Args: + amount: The value to record. + attributes: Optional set of attributes to associate with the + measurement. + context: Optional context to associate with the measurement. If + not provided, the current context is used. + """ if self._real_instrument: self._real_instrument.add(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: + """Create the real counter this proxy stands in for. + + Args: + meter: The real meter to create the instrument from. + + Returns: + The instrument returned by + :meth:`~opentelemetry.metrics.Meter.create_counter`, carrying the name, + unit and description this proxy was built with. + """ return meter.create_counter( self._name, self._unit, @@ -234,7 +410,16 @@ def add( class NoOpUpDownCounter(UpDownCounter): - """No-op implementation of `UpDownCounter`.""" + """No-op implementation of `UpDownCounter`. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ def __init__( self, @@ -242,6 +427,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Build the no-op instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(name, unit=unit, description=description) def add( @@ -250,20 +440,58 @@ def add( attributes: Attributes = None, context: Context | None = None, ) -> None: + """Discard the measurement. + + The call reaches the abstract method of the base class, whose body is + empty, so no argument is read and nothing is recorded anywhere. + + Args: + amount: Ignored. + attributes: Ignored. + context: Ignored. + """ return super().add(amount, attributes=attributes, context=context) class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): + """A up-down counter that stands in until a real meter provider is set. + + It records nothing of its own until one is. + """ + def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: + """Forward the measurement to the real instrument, if there is one. + + The measurement is discarded while no real + :class:`~opentelemetry.metrics.MeterProvider` has been set. + + Args: + amount: The value to record. + attributes: Optional set of attributes to associate with the + measurement. + context: Optional context to associate with the measurement. If + not provided, the current context is used. + """ if self._real_instrument: self._real_instrument.add(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: + """Create the real up-down counter this proxy stands in for. + + Args: + meter: The real meter to create the instrument from. + + Returns: + The instrument returned by + :meth:`~opentelemetry.metrics.Meter.create_up_down_counter`, + carrying the name, unit and description this proxy was built + with. + """ return meter.create_up_down_counter( self._name, self._unit, @@ -278,7 +506,21 @@ class ObservableCounter(Asynchronous): class NoOpObservableCounter(ObservableCounter): - """No-op implementation of `ObservableCounter`.""" + """No-op implementation of `ObservableCounter`. + + The callbacks are never invoked. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + callbacks: The callbacks to invoke on each collection. Each one + receives a :class:`CallbackOptions` and returns an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ def __init__( self, @@ -287,6 +529,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Build the no-op instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__( name, callbacks, @@ -296,7 +543,25 @@ def __init__( class _ProxyObservableCounter(_ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter): + """An observable counter that stands in until a real meter provider is set. + + It records nothing of its own until one is. + """ + def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableCounter: + """Create the real observable counter this proxy stands in for. + + Args: + meter: The real meter to create the instrument from. + + Returns: + The instrument returned by + :meth:`~opentelemetry.metrics.Meter.create_observable_counter`, + carrying the name, unit, description and callbacks this proxy was + built with. The callbacks are handed to the real instrument, so a + counter created before the SDK was installed starts reporting on + the first collection after it is. + """ return meter.create_observable_counter( self._name, self._callbacks, @@ -313,7 +578,21 @@ class ObservableUpDownCounter(Asynchronous): class NoOpObservableUpDownCounter(ObservableUpDownCounter): - """No-op implementation of `ObservableUpDownCounter`.""" + """No-op implementation of `ObservableUpDownCounter`. + + The callbacks are never invoked. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + callbacks: The callbacks to invoke on each collection. Each one + receives a :class:`CallbackOptions` and returns an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ def __init__( self, @@ -322,6 +601,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Build the no-op instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__( name, callbacks, @@ -334,7 +618,25 @@ class _ProxyObservableUpDownCounter( _ProxyAsynchronousInstrument[ObservableUpDownCounter], ObservableUpDownCounter, ): + """An observable up-down counter that stands in until a real meter provider is set. + + It records nothing of its own until one is. + """ + def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableUpDownCounter: + """Create the real observable up-down counter this proxy stands in for. + + Args: + meter: The real meter to create the instrument from. + + Returns: + The instrument returned by + :meth:`~opentelemetry.metrics.Meter.create_observable_up_down_counter`, + carrying the name, unit, description and callbacks this proxy was + built with. The callbacks are handed to the real instrument, so a + counter created before the SDK was installed starts reporting on + the first collection after it is. + """ return meter.create_observable_up_down_counter( self._name, self._callbacks, @@ -347,6 +649,17 @@ class Histogram(Synchronous): """Histogram is a synchronous `Instrument` which can be used to report arbitrary values that are likely to be statistically meaningful. It is intended for statistics such as histograms, summaries, and percentile. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + explicit_bucket_boundaries_advisory: The bucket boundaries the + instrument suggests to the SDK, in ascending order. ``None`` + leaves the choice to the SDK. """ @abstractmethod @@ -357,7 +670,11 @@ def __init__( description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> None: - pass + """Build the instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ @abstractmethod def record( @@ -383,7 +700,19 @@ def record( class NoOpHistogram(Histogram): - """No-op implementation of `Histogram`.""" + """No-op implementation of `Histogram`. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + explicit_bucket_boundaries_advisory: The bucket boundaries the + instrument suggests to the SDK, in ascending order. ``None`` + leaves the choice to the SDK. + """ def __init__( self, @@ -392,6 +721,11 @@ def __init__( description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> None: + """Build the no-op instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__( name, unit=unit, @@ -405,10 +739,36 @@ def record( attributes: Attributes = None, context: Context | None = None, ) -> None: + """Discard the measurement. + + The call reaches the abstract method of the base class, whose body is + empty, so no argument is read and nothing is recorded anywhere. + + Args: + amount: Ignored. + attributes: Ignored. + context: Ignored. + """ return super().record(amount, attributes=attributes, context=context) class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram): + """A histogram that stands in until a real meter provider is set. + + It records nothing of its own until one is. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + explicit_bucket_boundaries_advisory: The bucket boundaries the + instrument suggests to the SDK, in ascending order. ``None`` + leaves the choice to the SDK. + """ + def __init__( self, name: str, @@ -416,6 +776,11 @@ def __init__( description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> None: + """Record what to build the real histogram from, once one is possible. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(name, unit=unit, description=description) self._explicit_bucket_boundaries_advisory = explicit_bucket_boundaries_advisory @@ -425,10 +790,34 @@ def record( attributes: Attributes = None, context: Context | None = None, ) -> None: + """Forward the measurement to the real instrument, if there is one. + + The measurement is discarded while no real + :class:`~opentelemetry.metrics.MeterProvider` has been set. + + Args: + amount: The value to record. + attributes: Optional set of attributes to associate with the + measurement. + context: Optional context to associate with the measurement. If + not provided, the current context is used. + """ if self._real_instrument: self._real_instrument.record(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: + """Create the real histogram this proxy stands in for. + + Args: + meter: The real meter to create the instrument from. + + Returns: + The instrument returned by + :meth:`~opentelemetry.metrics.Meter.create_histogram`, carrying the name, + unit and description this proxy was built with, and the bucket + advisory as well, so a histogram created before the SDK was + installed keeps the boundaries it asked for. + """ return meter.create_histogram( self._name, self._unit, @@ -445,7 +834,21 @@ class ObservableGauge(Asynchronous): class NoOpObservableGauge(ObservableGauge): - """No-op implementation of `ObservableGauge`.""" + """No-op implementation of `ObservableGauge`. + + The callbacks are never invoked. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + callbacks: The callbacks to invoke on each collection. Each one + receives a :class:`CallbackOptions` and returns an iterable of + :class:`~opentelemetry.metrics.Observation`. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ def __init__( self, @@ -454,6 +857,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Build the no-op instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__( name, callbacks, @@ -466,7 +874,25 @@ class _ProxyObservableGauge( _ProxyAsynchronousInstrument[ObservableGauge], ObservableGauge, ): + """An observable gauge that stands in until a real meter provider is set. + + It records nothing of its own until one is. + """ + def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableGauge: + """Create the real observable gauge this proxy stands in for. + + Args: + meter: The real meter to create the instrument from. + + Returns: + The instrument returned by + :meth:`~opentelemetry.metrics.Meter.create_observable_gauge`, + carrying the name, unit, description and callbacks this proxy was + built with. The callbacks are handed to the real instrument, so a + gauge created before the SDK was installed starts reporting on the + first collection after it is. + """ return meter.create_observable_gauge( self._name, self._callbacks, @@ -500,7 +926,16 @@ def set( class NoOpGauge(Gauge): - """No-op implementation of ``Gauge``.""" + """No-op implementation of ``Gauge``. + + Args: + name: The name of the instrument, which must match + ``[a-zA-Z][-_./a-zA-Z0-9]{0,254}``. + unit: The unit of the measurements, as at most 63 ASCII + characters. + description: A human-readable description of what the instrument + measures. + """ def __init__( self, @@ -508,6 +943,11 @@ def __init__( unit: str = "", description: str = "", ) -> None: + """Build the no-op instrument. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(name, unit=unit, description=description) def set( @@ -516,6 +956,16 @@ def set( attributes: Attributes = None, context: Context | None = None, ) -> None: + """Discard the measurement. + + The call reaches the abstract method of the base class, whose body is + empty, so no argument is read and nothing is recorded anywhere. + + Args: + amount: Ignored. + attributes: Ignored. + context: Ignored. + """ return super().set(amount, attributes=attributes, context=context) @@ -523,16 +973,43 @@ class _ProxyGauge( _ProxyInstrument[Gauge], Gauge, ): + """A gauge that stands in until a real meter provider is set. + + It records nothing of its own until one is. + """ + def set( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: + """Forward the measurement to the real instrument, if there is one. + + The measurement is discarded while no real + :class:`~opentelemetry.metrics.MeterProvider` has been set. + + Args: + amount: The value to record. + attributes: Optional set of attributes to associate with the + measurement. + context: Optional context to associate with the measurement. If + not provided, the current context is used. + """ if self._real_instrument: self._real_instrument.set(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> Gauge: + """Create the real gauge this proxy stands in for. + + Args: + meter: The real meter to create the instrument from. + + Returns: + The instrument returned by + :meth:`~opentelemetry.metrics.Meter.create_gauge`, carrying the name, + unit and description this proxy was built with. + """ return meter.create_gauge( self._name, self._unit, diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/observation.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/observation.py index 0ee7a311369..43e71b79ec5 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/observation.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/observation.py @@ -1,6 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""A single measurement reported by an asynchronous instrument.""" from opentelemetry.context import Context from opentelemetry.util.types import Attributes @@ -23,23 +24,54 @@ def __init__( attributes: Attributes = None, context: Context | None = None, ) -> None: + """Record one measurement to be reported by a callback. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._value = value self._attributes = attributes self._context = context @property def value(self) -> float | int: + """The measured value. + + Returns: + The value this observation was built with. + """ return self._value @property def attributes(self) -> Attributes: + """The attributes the measurement is recorded under. + + Returns: + The attributes, or ``None`` when the observation carries none. + Each distinct set of attributes forms its own time series. + """ return self._attributes @property def context(self) -> Context | None: + """The context the measurement was taken in. + + Returns: + The context, or ``None`` when the observation carries none. It is + what lets an exemplar link the measurement back to a span. + """ return self._context def __eq__(self, other: object) -> bool: + """Compare against another observation by value, attributes and context. + + Args: + other: The object to compare with. + + Returns: + ``True`` when ``other`` is an :class:`Observation` whose value, + attributes and context are all equal to this one's. + """ return ( isinstance(other, Observation) and self.value == other.value @@ -48,4 +80,9 @@ def __eq__(self, other: object) -> bool: ) def __repr__(self) -> str: + """Render the observation for debugging. + + Returns: + A string naming the value, the attributes and the context. + """ return f"Observation(value={self.value}, attributes={self.attributes}, context={self.context})" diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 15f876fdbbf..aa01cdf9cad 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -19,31 +19,39 @@ ``opentelemetry_propagator`` entry points in the ``pyproject.toml`` file of ``opentelemetry``. +See the `Propagation API Specification`_ for what a propagator has to do. + Example:: import flask import requests from opentelemetry import propagate + from opentelemetry.propagators.textmap import Getter, Setter PROPAGATOR = propagate.get_global_textmap() - def get_header_from_flask_request(request, key): - return request.headers.get_all(key) + class FlaskGetter(Getter[flask.Request]): + def get(self, carrier: flask.Request, key: str) -> list[str] | None: + return carrier.headers.get_all(key) or None + + def keys(self, carrier: flask.Request) -> list[str]: + return list(carrier.headers.keys()) - def set_header_into_requests_request(request: requests.Request, key: str, value: str): - request.headers[key] = value + class RequestsSetter(Setter[requests.Request]): + def set(self, carrier: requests.Request, key: str, value: str) -> None: + carrier.headers[key] = value - def example_route(): - context = PROPAGATOR.extract(get_header_from_flask_request, flask.request) + def example_route() -> None: + context = PROPAGATOR.extract(flask.request, getter=FlaskGetter()) request_to_downstream = requests.Request("GET", "http://httpbin.org/get") PROPAGATOR.inject( - set_header_into_requests_request, request_to_downstream, context=context, + setter=RequestsSetter(), ) session = requests.Session() session.send(request_to_downstream.prepare()) @@ -80,6 +88,11 @@ def extract( which understands how to extract a value from it. context: an optional Context to use. Defaults to root context if not set. + + Returns: + A Context holding whatever the globally configured propagator found in + the carrier. When the carrier holds nothing the propagator recognises, + ``context`` is returned unchanged. """ return get_global_textmap().extract(carrier, context, getter=getter) @@ -105,6 +118,25 @@ def inject( def _load_propagators() -> textmap.TextMapPropagator: + """Build the global propagator from the ``OTEL_PROPAGATORS`` variable. + + Each comma-separated name is loaded as an ``opentelemetry_propagator`` + entry point. When the variable is unset, W3C trace context and W3C baggage + are used. When a name is ``none``, propagation is disabled entirely and + every name after it is ignored. Every name before it is resolved first, so + a misspelled name ahead of ``none`` still raises :exc:`ValueError`. + + Returns: + A :class:`~opentelemetry.propagators.composite.CompositePropagator` + wrapping the configured propagators, in the order they were named. It + wraps an empty list when propagation is disabled. + + Raises: + ValueError: If a named propagator has no matching entry point, which + means it is misspelled or its package is not installed. + Exception: Re-raises whatever loading an entry point raised, after + logging it. + """ configured = environ.get(OTEL_PROPAGATORS) if not configured: # pylint: disable=import-outside-toplevel,no-name-in-module @@ -161,11 +193,27 @@ def _load_propagators() -> textmap.TextMapPropagator: def get_global_textmap() -> textmap.TextMapPropagator: + """Return the propagator that :func:`extract` and :func:`inject` use. + + Returns: + The globally configured propagator. Unless + :func:`set_global_textmap` has replaced it, this is the one built from + the ``OTEL_PROPAGATORS`` environment variable at import time. + """ return _HTTP_TEXT_FORMAT def set_global_textmap( http_text_format: textmap.TextMapPropagator, ) -> None: + """Replace the propagator that :func:`extract` and :func:`inject` use. + + This overrides whatever the ``OTEL_PROPAGATORS`` environment variable + selected. Call it before any instrumentation starts propagating, since a + propagator already captured by a caller is not replaced. + + Args: + http_text_format: The propagator to install globally. + """ global _HTTP_TEXT_FORMAT # pylint:disable=global-statement _HTTP_TEXT_FORMAT = http_text_format diff --git a/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py b/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py index c474745638a..a517e9a3cc4 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py +++ b/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py @@ -16,6 +16,23 @@ def _normalize_key(key: str) -> str: + """Convert a propagation field name into an environment variable name. + + Every character that is not a letter, a digit or an underscore becomes an + underscore, and the result is uppercased, so ``traceparent`` becomes + ``TRACEPARENT``. A leading digit is prefixed with an underscore, because an + environment variable name may not start with one. An empty name becomes a + single underscore. + + The mapping is not reversible: two different field names can normalize to + the same environment variable name. + + Args: + key: The propagation field name to convert. + + Returns: + The corresponding environment variable name. + """ if not key: return "_" result = re.sub(r"[^A-Za-z0-9_]", "_", key).upper() @@ -25,6 +42,16 @@ def _normalize_key(key: str) -> str: def _is_normalized_key(key: str) -> bool: + """Report whether ``key`` is already a normalized environment variable name. + + Args: + key: The candidate environment variable name. + + Returns: + ``True`` when ``key`` is non-empty, does not start with a digit, and + holds only uppercase letters, digits and underscores, which is exactly + what :func:`_normalize_key` produces. + """ if not key: return False if "0" <= key[0] <= "9": diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index 769e9f93597..723731adb0b 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -1,5 +1,16 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""A propagator that chains several other propagators together. + +An application usually needs more than one wire format at once, for example +W3C trace context together with W3C baggage. +:class:`CompositePropagator` presents several propagators as one. It is what +:func:`opentelemetry.propagate.get_global_textmap` returns in every case, since +``OTEL_PROPAGATORS`` naming a single propagator still produces a +:class:`CompositePropagator` wrapping that one, and naming ``none`` produces +one wrapping no propagator at all. +""" + import collections.abc import logging @@ -20,6 +31,11 @@ class CompositePropagator(textmap.TextMapPropagator): """ def __init__(self, propagators: collections.abc.Sequence[textmap.TextMapPropagator]) -> None: + """Store the propagators to run, in the order given. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._propagators = propagators def extract( @@ -34,6 +50,21 @@ def extract( will override previous propagators. See `opentelemetry.propagators.textmap.TextMapPropagator.extract` + + Args: + carrier: The object holding the incoming values, passed unchanged + to every configured propagator. + context: The context to add the extracted values to. ``None`` is + handed to the first configured propagator only, which decides + what to use in its place. Every later propagator receives the + context the previous propagator returned. + getter: Reads values out of ``carrier``, passed unchanged to every + configured propagator. + + Returns: + The context returned by the last configured propagator, having + been threaded through each one in turn. When no propagator is + configured, ``context`` is returned unchanged. """ for propagator in self._propagators: context = propagator.extract(carrier, context, getter=getter) @@ -51,6 +82,14 @@ def inject( will override previous propagators. See `opentelemetry.propagators.textmap.TextMapPropagator.inject` + + Args: + carrier: The object to write the outgoing values into, passed + unchanged to every configured propagator. + context: The context to read the values from. If ``None``, each + propagator falls back to the current context. + setter: Writes values into ``carrier``, passed unchanged to every + configured propagator. """ for propagator in self._propagators: propagator.inject(carrier, context, setter=setter) @@ -61,6 +100,9 @@ def fields(self) -> set[str]: See `opentelemetry.propagators.textmap.TextMapPropagator.fields` + + Returns: + The union of the fields of every configured propagator. """ composite_fields = set() diff --git a/opentelemetry-api/src/opentelemetry/propagators/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py index 7dc132e5b92..3cbb8cd5854 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/textmap.py +++ b/opentelemetry-api/src/opentelemetry/propagators/textmap.py @@ -1,6 +1,19 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""The interface a cross-process propagator implements. + +A propagator serializes part of a :class:`~opentelemetry.context.Context` into +a carrier when a request leaves the process, and reads it back when one +arrives. The carrier is whatever the transport exposes, most often a mapping +of HTTP headers. + +A propagator never touches the carrier directly. It reads through a +:class:`Getter` and writes through a :class:`Setter`, so one propagator works +with any carrier shape as long as a getter and a setter exist for it. +``default_getter`` and ``default_setter`` cover a plain mapping. +""" + import abc import typing from collections.abc import Iterable, Mapping, MutableMapping @@ -27,8 +40,10 @@ def get(self, carrier: CarrierT, key: str) -> list[str] | None: carrier: An object which contains values that are used to construct a Context. key: key of a field in carrier. - Returns: first value of the propagation key or None if the key doesn't - exist. + + Returns: + Every value stored under ``key``, or ``None`` when the key does not + exist in the carrier. """ @abc.abstractmethod @@ -38,6 +53,7 @@ def keys(self, carrier: CarrierT) -> list[str]: Args: carrier: An object which contains values that are used to construct a Context. + Returns: list of keys from the carrier. """ @@ -50,7 +66,7 @@ class Setter(abc.ABC, typing.Generic[CarrierT]): @abc.abstractmethod def set(self, carrier: CarrierT, key: str, value: str) -> None: - """Function that can set a value into a carrier"" + """Function that can set a value into a carrier. Args: carrier: An object which contains values that are used to @@ -61,14 +77,24 @@ def set(self, carrier: CarrierT, key: str, value: str) -> None: class DefaultGetter(Getter[Mapping[str, CarrierValT]]): + """A :class:`Getter` for a carrier that is a plain mapping. + + This is the getter used when a caller passes HTTP headers as a dict and + supplies no getter of their own. ``default_getter`` is a ready-made + instance. + """ + def get(self, carrier: Mapping[str, CarrierValT], key: str) -> list[str] | None: """Getter implementation to retrieve a value from a dictionary. Args: carrier: dictionary in which to get value key: the key used to get the value + Returns: A list with a single string with the value if it exists, else None. + A value that is itself a non-string iterable is returned as a list + of its items, which is how a repeated header is handled. """ val = carrier.get(key, None) if val is None: @@ -78,7 +104,14 @@ def get(self, carrier: Mapping[str, CarrierValT], key: str) -> list[str] | None: return [val] def keys(self, carrier: Mapping[str, CarrierValT]) -> list[str]: - """Keys implementation that returns all keys from a dictionary.""" + """Keys implementation that returns all keys from a dictionary. + + Args: + carrier: dictionary whose keys to return + + Returns: + Every key in the carrier, in iteration order. + """ return list(carrier.keys()) @@ -86,6 +119,12 @@ def keys(self, carrier: Mapping[str, CarrierValT]) -> list[str]: class DefaultSetter(Setter[MutableMapping[str, CarrierValT]]): + """A :class:`Setter` for a carrier that is a plain mutable mapping. + + Setting a key that is already present replaces it. ``default_setter`` + is a ready-made instance. + """ + def set( self, carrier: MutableMapping[str, CarrierValT], @@ -137,9 +176,9 @@ def extract( which understands how to extract a value from it. context: an optional Context to use. Defaults to root context if not set. + Returns: A Context with configuration found in the carrier. - """ @abc.abstractmethod diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index cb8a5400601..1e8a1177462 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -31,7 +31,7 @@ with tracer.start_as_current_span("parent"): # Attach a new child and update the current span with tracer.start_as_current_span("child"): - do_work(): + do_work() # Close child span, set parent as current # Close parent span, set default span as current @@ -105,17 +105,41 @@ class _LinkBase(ABC): + """The span context half of a link, shared by every link implementation. + + A link associates one span with another that is causally related but is + not its parent, for example the spans of the messages gathered into a + batch. + + Args: + context: The span context of the span to link to. + """ + def __init__(self, context: "SpanContext") -> None: + """Store the context of the span being linked to. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._context = context @property def context(self) -> "SpanContext": + """The context of the span being linked to. + + Returns: + The span context this link was built with. + """ return self._context @property @abstractmethod def attributes(self) -> types.Attributes: - pass + """The attributes describing this link. + + Returns: + The link attributes, or ``None`` when the link carries none. + """ class Link(_LinkBase): @@ -131,15 +155,32 @@ def __init__( context: "SpanContext", attributes: types.Attributes = None, ) -> None: + """Build the link. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ super().__init__(context) self._attributes = attributes @property def attributes(self) -> types.Attributes: + """The attributes describing this link. + + Returns: + The link attributes, or ``None`` when the link carries none. + """ return self._attributes @property def dropped_attributes(self) -> int: + """How many attributes were discarded for exceeding the limits. + + Returns: + The count reported by the underlying + :class:`~opentelemetry.attributes.BoundedAttributes`, or ``0`` + when the attributes are a plain mapping with no limit applied. + """ if isinstance(self._attributes, BoundedAttributes): return self._attributes.dropped return 0 @@ -178,6 +219,13 @@ class SpanKind(Enum): class TracerProvider(ABC): + """The entry point of the tracing API, which hands out tracers. + + An application configures one provider, usually by installing an SDK, and + every library obtains its own :class:`Tracer` from it through + :meth:`get_tracer`. + """ + @abstractmethod def get_tracer( self, @@ -213,6 +261,9 @@ def get_tracer( schema_url: Optional. Specifies the Schema URL of the emitted telemetry. attributes: Optional. Specifies the attributes of the emitted telemetry. + + Returns: + A `Tracer` bound to the given instrumentation scope. """ @@ -229,6 +280,22 @@ def get_tracer( schema_url: str | None = None, attributes: types.Attributes = None, ) -> "Tracer": + """Return a tracer that creates only non-recording spans. + + Args: + instrumenting_module_name: The uniquely identifiable name for the + instrumentation scope, not the name of the module being + instrumented. + instrumenting_library_version: The version string of the + instrumenting library. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + + Returns: + A :class:`NoOpTracer`. The scope arguments are accepted and + ignored. + """ # pylint:disable=no-self-use,unused-argument return NoOpTracer() @@ -242,6 +309,17 @@ class _DefaultTracerProvider(NoOpTracerProvider): class ProxyTracerProvider(TracerProvider): + """The provider returned before a real one has been set globally. + + Every tracer this tracer provider hands out before a real tracer provider + is set is a :class:`ProxyTracer`, which creates non-recording spans and + switches to the real tracer provider as soon as + :func:`set_tracer_provider` installs one. Every tracer handed out after + that call comes from the real tracer provider directly. This is what lets + a library call :func:`get_tracer` at import time, before the application + configures the SDK. + """ + def get_tracer( self, instrumenting_module_name: str, @@ -249,6 +327,22 @@ def get_tracer( schema_url: str | None = None, attributes: types.Attributes = None, ) -> "Tracer": + """Return a tracer bound to the real provider once one exists. + + Args: + instrumenting_module_name: The uniquely identifiable name for the + instrumentation scope, not the name of the module being + instrumented. + instrumenting_library_version: The version string of the + instrumenting library. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + + Returns: + A tracer from the global provider when one has been set, and a + :class:`ProxyTracer` carrying the given scope otherwise. + """ if _TRACER_PROVIDER: return _TRACER_PROVIDER.get_tracer( instrumenting_module_name, @@ -402,6 +496,23 @@ def function(): ... class ProxyTracer(Tracer): + """A tracer that stands in until a real provider is set globally. + + Every span it creates is a :class:`~opentelemetry.trace.NonRecordingSpan` while no + :class:`TracerProvider` has been installed. Once one is, the proxy + resolves a real tracer for its instrumentation scope, caches it, and + forwards everything from then on. + + Args: + instrumenting_module_name: The uniquely identifiable name for the + instrumentation scope. + instrumenting_library_version: The version string of the + instrumenting library. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + """ + # pylint: disable=W0222,signature-differs def __init__( self, @@ -410,6 +521,11 @@ def __init__( schema_url: str | None = None, attributes: types.Attributes = None, ): + """Record what to ask the global provider for once it exists. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._instrumenting_module_name = instrumenting_module_name self._instrumenting_library_version = instrumenting_library_version self._schema_url = schema_url @@ -419,6 +535,14 @@ def __init__( @property def _tracer(self) -> Tracer: + """Return the tracer to delegate to, resolving it on first use. + + Returns: + The tracer obtained from the global :class:`TracerProvider` once + one is set, cached thereafter. A :class:`NoOpTracer` until then, + built once and returned on every call, which the proxy never + adopts as its real tracer, so a later provider is still picked up. + """ if self._real_tracer: return self._real_tracer @@ -433,10 +557,34 @@ def _tracer(self) -> Tracer: return self._noop_tracer def start_span(self, *args, **kwargs) -> Span: # type: ignore + """Start a span through the real tracer once one is available. + + Args: + *args: Positional arguments forwarded to + :meth:`Tracer.start_span`. + **kwargs: Keyword arguments forwarded to + :meth:`Tracer.start_span`. + + Returns: + The span the delegate created, which is a + :class:`~opentelemetry.trace.NonRecordingSpan` while no provider has been set. + """ return self._tracer.start_span(*args, **kwargs) # type: ignore @_agnosticcontextmanager # type: ignore def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]: + """Start a span and make it current, through the real tracer. + + Args: + *args: Positional arguments forwarded to + :meth:`Tracer.start_as_current_span`. + **kwargs: Keyword arguments forwarded to + :meth:`Tracer.start_as_current_span`. + + Yields: + The span the delegate created, which is a + :class:`~opentelemetry.trace.NonRecordingSpan` while no provider has been set. + """ with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore yield span @@ -458,6 +606,31 @@ def start_span( record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": + """Return a non-recording span, creating no telemetry. + + ``context`` is the only argument that has an effect. Every other one + is accepted so that the signature matches :meth:`Tracer.start_span`, + and discarded, because a non-recording span holds nothing but a span + context. + + Args: + name: Ignored. + context: The Context to read the current span from. Defaults to + the current context. + kind: Ignored. + attributes: Ignored. + links: Ignored. + start_time: Ignored. + record_exception: Ignored. + set_status_on_exception: Ignored. + + Returns: + The current span when it is already a + :class:`~opentelemetry.trace.NonRecordingSpan`, so the trace context is preserved. + Otherwise a new :class:`~opentelemetry.trace.NonRecordingSpan` carrying the current + span context, or :data:`INVALID_SPAN` when that context is not + usable. + """ current_span = get_current_span(context) if isinstance(current_span, NonRecordingSpan): return current_span @@ -485,6 +658,33 @@ def start_as_current_span( set_status_on_exception: bool = True, end_on_exit: bool = True, ) -> Iterator["Span"]: + """Make a non-recording span current for the duration of the block. + + ``context`` is the only argument that changes what happens. The rest + are accepted so that the signature matches + :meth:`Tracer.start_as_current_span`, and the three passed on to + :func:`use_span` change nothing there, because a non-recording span is + never recording and discards the call to :meth:`Span.end`. + + Args: + name: Ignored. + context: The Context to read the current span from. Defaults to + the current context. + kind: Ignored. + attributes: Ignored. + links: Ignored. + start_time: Ignored. + record_exception: Passed on to :func:`use_span`, which records + nothing on a non-recording span. + set_status_on_exception: Passed on to :func:`use_span`, which sets + no status on a non-recording span. + end_on_exit: Passed on to :func:`use_span`, whose call to + :meth:`Span.end` a non-recording span discards. + + Yields: + A :class:`~opentelemetry.trace.NonRecordingSpan`, which records nothing set on it but + keeps the trace context current so propagation still works. + """ span = self.start_span( name=name, context=context, @@ -530,6 +730,21 @@ def get_tracer( opentelemetry.trace.TracerProvider.get_tracer. If tracer_provider is omitted the current configured one is used. + + Args: + instrumenting_module_name: The uniquely identifiable name for the + instrumentation scope, not the name of the module being + instrumented. + instrumenting_library_version: The version string of the instrumenting + library. + tracer_provider: The provider to obtain the tracer from. Defaults to + the globally configured one. + schema_url: The Schema URL of the emitted telemetry. + attributes: The instrumentation scope attributes to associate with + emitted telemetry. + + Returns: + A `Tracer` bound to the given instrumentation scope. """ if tracer_provider is None: tracer_provider = get_tracer_provider() @@ -542,7 +757,16 @@ def get_tracer( def _set_tracer_provider(tracer_provider: TracerProvider, log: bool) -> None: + """Install the global provider, at most once per process. + + Args: + tracer_provider: The provider to install. + log: Whether to log a warning when a provider is already installed and + this call therefore has no effect. + """ + def set_tp() -> None: + """Assign the global provider. Called at most once.""" global _TRACER_PROVIDER # pylint: disable=global-statement _TRACER_PROVIDER = tracer_provider @@ -557,12 +781,28 @@ def set_tracer_provider(tracer_provider: TracerProvider) -> None: This can only be done once, a warning will be logged if any further attempt is made. + + Args: + tracer_provider: The provider to install globally. """ _set_tracer_provider(tracer_provider, log=True) def get_tracer_provider() -> TracerProvider: - """Gets the current global :class:`~.TracerProvider` object.""" + """Gets the current global :class:`~.TracerProvider` object. + + Returns: + The provider set by :func:`set_tracer_provider`, or the one named by + the ``OTEL_PYTHON_TRACER_PROVIDER`` environment variable. When neither + is present, a ``ProxyTracerProvider`` is returned, which starts + delegating as soon as a provider is set. + + Raises: + Exception: Whatever loading the entry point named by the environment + variable raised, since ``_load_provider`` logs it and re-raises. A + name that matches no installed entry point surfaces as + :exc:`StopIteration`. + """ if _TRACER_PROVIDER is None: # if a global tracer provider has not been set either via code or env # vars, return a proxy tracer provider @@ -595,6 +835,16 @@ def use_span( be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. + + Yields: + The span passed in, now current for the duration of the block. + + Raises: + Exception: Re-raises whatever the block raised, after recording it on + the span when the span is recording and ``record_exception`` is + true. An exception inheriting directly from :exc:`BaseException`, + such as :exc:`GeneratorExit`, passes through without being + recorded, because it does not signal an error. """ try: token = context_api.attach(context_api.set_value(_SPAN_KEY, span)) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 93a09160a81..4796174790f 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -1,6 +1,14 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Storage of the active span inside a context. + +A span becomes current by being stored in a +:class:`~opentelemetry.context.Context` under a private key. These two +functions are the only supported way to read and write that slot, and they are +re-exported from :mod:`opentelemetry.trace`. +""" + from opentelemetry.context import create_key, get_value, set_value from opentelemetry.context.context import Context from opentelemetry.trace.span import INVALID_SPAN, Span @@ -16,6 +24,10 @@ def set_span_in_context(span: Span, context: Context | None = None) -> Context: span: The Span to set. context: a Context object. if one is not passed, the default current context is used instead. + + Returns: + A new Context holding ``span`` as the current span. The context passed + in is not modified. """ ctx = set_value(_SPAN_KEY, span, context=context) return ctx diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index b17a7748ef7..5dd19d769b9 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -1,6 +1,14 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # +"""Serialization of a span context into the W3C trace context headers. + +:class:`TraceContextTextMapPropagator` writes the ``traceparent`` and +``tracestate`` headers when a request is sent and reads them back when one is +received, following https://www.w3.org/TR/trace-context/. This is the format +that makes a trace continue across a service boundary. +""" + import logging import re @@ -37,6 +45,24 @@ def extract( """Extracts SpanContext from the carrier. See `opentelemetry.propagators.textmap.TextMapPropagator.extract` + + A ``traceparent`` header that is absent, too long, malformed, carries + an all-zero trace or span id, or names version ``ff`` is ignored and + the context is returned unchanged. A ``tracestate`` header that + exceeds the maximum length is dropped while the ``traceparent`` is + still honoured. + + Args: + carrier: The object holding the incoming headers. + context: The context to add the extracted span to. If ``None``, an + empty context is used rather than the current one. + getter: Reads the header values out of ``carrier``. + + Returns: + A context holding a + :class:`~opentelemetry.trace.NonRecordingSpan` built from the + headers, marked as remote. The context is returned unchanged when + no valid ``traceparent`` is present. """ if context is None: context = Context() @@ -105,6 +131,17 @@ def inject( """Injects SpanContext into the carrier. See `opentelemetry.propagators.textmap.TextMapPropagator.inject` + + Nothing is written when the current span context is invalid, so a + request made outside any span carries no trace context header. The + ``tracestate`` header is written only when the span context carries a + non-empty trace state. + + Args: + carrier: The object to write the outgoing headers into. + context: The context to read the current span from. If ``None``, + the current context is used. + setter: Writes the headers into ``carrier``. """ span = trace.get_current_span(context) span_context = span.get_span_context() @@ -122,5 +159,10 @@ def fields(self) -> set[str]: See `opentelemetry.propagators.textmap.TextMapPropagator.fields` + + Returns: + A set holding ``"traceparent"`` and ``"tracestate"``. A caller + clearing a carrier before injecting into it should remove these + names. """ return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME} diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index bf68050a19b..833bde37c68 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -1,6 +1,20 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Spans and the identifiers that place them in a trace. + +A :class:`Span` covers one operation: it has a name, a start and end time, and +may carry attributes, events and links. :class:`SpanContext` is the immutable +identity of a span, the part that is serialized onto the wire so that a span +created in another process joins the same trace. :class:`TraceFlags` and +:class:`TraceState` are the two further fields the W3C trace context format +carries alongside those identifiers. + +:class:`~opentelemetry.trace.NonRecordingSpan` is the span implementation the API uses on its own. +It holds a :class:`SpanContext` so propagation keeps working, and discards +everything else, so instrumentation runs unchanged when no SDK is installed. +""" + from __future__ import annotations import abc @@ -49,6 +63,20 @@ def _is_valid_pair(key: str, value: str) -> bool: + """Report whether a pair may appear in a ``tracestate`` header. + + Args: + key: The candidate trace state key. A key starting with a lowercase + letter needs no ``@``. A key starting with a digit is valid only + in the tenant ``@`` vendor form, whose vendor half must itself + start with a lowercase letter. + value: The candidate trace state value. It must hold only printable + ASCII other than comma and equals sign, and must not end with a + space. + + Returns: + ``True`` when both halves match the W3C trace context grammar. + """ return ( isinstance(key, str) and _KEY_PATTERN.fullmatch(key) is not None @@ -68,6 +96,12 @@ def end(self, end_time: int | None = None) -> None: Only the first call to `end` should modify the span, and implementations are free to ignore or raise on further calls. + + Args: + end_time: When the operation finished, as nanoseconds since the + Unix epoch. Defaults to the time of the call. Pass it + explicitly only when the real end time differs from now, for + example when recording an operation that finished earlier. """ @abc.abstractmethod @@ -91,6 +125,10 @@ def set_attributes(self, attributes: Mapping[str, types.AnyValue]) -> None: strongly discouraged. It is also preferred to set attributes at span creation, instead of calling this method later since samplers can only consider information already present during span creation. + + Args: + attributes: The name/value pairs to set. Each is validated and + may be truncated or dropped by the SDK limits. """ @abc.abstractmethod @@ -103,6 +141,10 @@ def set_attribute(self, key: str, value: types.AnyValue) -> None: strongly discouraged. It is also preferred to set attributes at span creation, instead of calling this method later since samplers can only consider information already present during span creation. + + Args: + key: The attribute name. It must be a non-empty string. + value: The attribute value. """ @abc.abstractmethod @@ -117,6 +159,12 @@ def add_event( Adds a single `Event` with the name and, optionally, a timestamp and attributes passed as arguments. Implementations should generate a timestamp if the `timestamp` argument is omitted. + + Args: + name: The name of the event. + attributes: Name/value pairs describing the event. + timestamp: When the event occurred, as nanoseconds since the Unix + epoch. Defaults to the time of the call. """ def add_link( # pylint: disable=no-self-use @@ -134,6 +182,13 @@ def add_link( # pylint: disable=no-self-use Note: It is preferred to add links at span creation, instead of calling this method later since samplers can only consider information already present during span creation. + + This method is not abstract and its own body only emits a warning, so + a span that inherits it records no link at all. + + Args: + context: The span context to link to. + attributes: Name/value pairs describing the link. """ warnings.warn( "Span.add_link() not implemented and will be a no-op. " @@ -148,6 +203,9 @@ def update_name(self, name: str) -> None: Upon this update, any sampling behavior based on Span name will depend on the implementation. + + Args: + name: The new name of the span. """ @abc.abstractmethod @@ -156,6 +214,12 @@ def is_recording(self) -> bool: Returns true if this Span is active and recording information like events with the add_event operation and attributes using set_attribute. + + Returns: + ``True`` when the span records what is set on it. ``False`` for a + span that has ended, for one that was not sampled, and for a + :class:`~opentelemetry.trace.NonRecordingSpan`. Instrumentation should check this + before doing expensive work to build an attribute value. """ @abc.abstractmethod @@ -166,6 +230,15 @@ def set_status( ) -> None: """Sets the Status of the Span. If used, this will override the default Span status. + + Args: + status: The status to set, either a + :class:`~opentelemetry.trace.Status` or a bare + :class:`~opentelemetry.trace.StatusCode`. + description: A description of the status. It is kept only when + ``status`` is or carries :attr:`~.StatusCode.ERROR`, and must + not be given when ``status`` is already a + :class:`~opentelemetry.trace.Status`. """ @abc.abstractmethod @@ -176,12 +249,29 @@ def record_exception( timestamp: int | None = None, escaped: bool = False, ) -> None: - """Records an exception as a span event.""" + """Records an exception as a span event. + + Recording an exception does not set the span status. Set it with + :meth:`set_status` as well when the exception means the operation + failed. + + Args: + exception: The exception to record. Its type, message and stack + trace are attached to the event. + attributes: Further name/value pairs describing the event. + timestamp: When the exception was recorded, as nanoseconds since + the Unix epoch. Defaults to the time of the call. + escaped: Whether the exception escaped the scope of the span, + meaning it was not caught before the span ended. + """ def __enter__(self) -> Span: """Invoked when `Span` is used as a context manager. Returns the `Span` itself. + + Returns: + This span, so that ``with span as s`` binds the span itself. """ return self @@ -191,7 +281,18 @@ def __exit__( exc_val: BaseException | None, exc_tb: python_types.TracebackType | None, ) -> None: - """Ends context manager and calls `end` on the `Span`.""" + """Ends context manager and calls `end` on the `Span`. + + The span is ended whether or not the block raised. This method does + not set the span status or record the exception; a tracer that creates + the span is responsible for that. + + Args: + exc_type: The type of the exception that left the block, or + ``None`` when it exited normally. + exc_val: The exception that left the block, or ``None``. + exc_tb: The traceback of that exception, or ``None``. + """ self.end() @@ -200,10 +301,11 @@ class TraceFlags(int): """A bitmask that represents options specific to the trace. Supported flags: + - "sampled" (``0x01``): Indicates the trace may have been sampled upstream. - "random-trace-id" (``0x02``): Indicates the trace ID was generated - randomly, with at least the 7 rightmost bytes (56 bits) selected with - uniform distribution. + randomly, with at least the 7 rightmost bytes (56 bits) selected with + uniform distribution. See the `W3C Trace Context - Traceparent`_ spec for details. @@ -217,14 +319,32 @@ class TraceFlags(int): @classmethod def get_default(cls) -> TraceFlags: + """Return the flags with every option off. + + Returns: + Flags equal to :attr:`DEFAULT`, meaning not sampled and no + guarantee about how the trace id was generated. + """ return cls(cls.DEFAULT) @property def sampled(self) -> bool: + """Whether the trace may have been sampled upstream. + + Returns: + ``True`` when the :attr:`SAMPLED` bit is set. + """ return bool(self & TraceFlags.SAMPLED) @property def random_trace_id(self) -> bool: + """Whether the trace id was generated randomly. + + Returns: + ``True`` when the :attr:`RANDOM_TRACE_ID` bit is set, which + promises that at least the rightmost 56 bits of the trace id were + drawn with uniform distribution. + """ return bool(self & TraceFlags.RANDOM_TRACE_ID) @@ -238,6 +358,15 @@ class TraceState(Mapping[str, str]): Implementations should conform to the `W3C Trace Context - Tracestate`_ spec, which describes additional restrictions on valid field values. + Construction drops anything the format forbids. An invalid pair and a + duplicate key are each skipped with a warning. Supplying more than 32 + pairs discards all of them, leaving an empty trace state, because the + format defines no way to choose which to keep. + + Args: + entries: The pairs to store, in order. ``None`` builds an empty + trace state. + .. _W3C Trace Context - Tracestate: https://www.w3.org/TR/trace-context/#tracestate-field """ @@ -246,6 +375,11 @@ def __init__( self, entries: Sequence[tuple[str, str]] | None = None, ) -> None: + """Build the trace state, dropping anything the format forbids. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._dict = {} # type: dict[str, str] if entries is None: return @@ -266,18 +400,55 @@ def __init__( _logger.warning("Invalid key/value pair (%s, %s) found.", key, value) def __contains__(self, item: object) -> bool: + """Report whether a key is present. + + Args: + item: The key to look for. + + Returns: + ``True`` when the key is present. + """ return item in self._dict def __getitem__(self, key: str) -> str: + """Return the value stored under ``key``. + + Args: + key: The trace state key to look up. + + Returns: + The value stored under ``key``. + + Raises: + KeyError: If the key is absent. + """ return self._dict[key] def __iter__(self) -> Iterator[str]: + """Iterate over the keys. + + Returns: + An iterator over the keys. The order is the one the pairs + were supplied to the constructor or parsed from the header in. + :meth:`add` and :meth:`update` put their new pair first. + """ return iter(self._dict) def __len__(self) -> int: + """Return how many pairs are stored. + + Returns: + The number of pairs, never more than 32. + """ return len(self._dict) def __repr__(self) -> str: + """Render the pairs for debugging. + + Returns: + A string listing each pair, which is not the ``tracestate`` header + format. Use :meth:`to_header` for that. + """ pairs = [f"{{key={key}, value={value}}}" for key, value in self._dict.items()] return str(pairs) @@ -419,15 +590,42 @@ def from_header(cls, header_list: list[str]) -> TraceState: @classmethod def get_default(cls) -> TraceState: + """Return an empty trace state. + + Returns: + A trace state holding no pairs, which is what a span context + carries when no vendor has added anything. + """ return cls() def keys(self) -> typing.KeysView[str]: + """Return a view of the keys. + + Returns: + A view of the keys. The order is the one the pairs + were supplied to the constructor or parsed from the header in. + :meth:`add` and :meth:`update` put their new pair first. + """ return self._dict.keys() def items(self) -> typing.ItemsView[str, str]: + """Return a view of the pairs. + + Returns: + A view of the key/value pairs. The order is the one the pairs + were supplied to the constructor or parsed from the header in. + :meth:`add` and :meth:`update` put their new pair first. + """ return self._dict.items() def values(self) -> typing.ValuesView[str]: + """Return a view of the values. + + Returns: + A view of the values. The order is the one the pairs + were supplied to the constructor or parsed from the header in. + :meth:`add` and :meth:`update` put their new pair first. + """ return self._dict.values() @@ -448,6 +646,11 @@ class SpanContext(tuple[int, int, bool, "TraceFlags", "TraceState", bool]): is_remote: True if propagated from a remote parent. trace_flags: Trace options to propagate. trace_state: Tracing-system-specific info to propagate. + + A span context is a :class:`tuple` subclass and is immutable: assigning to + an attribute is ignored and logged at debug level rather than raising. A + context whose trace id or span id is zero, or out of range, is marked + invalid, which is how ``INVALID_SPAN_CONTEXT`` is built. """ def __new__( @@ -458,6 +661,16 @@ def __new__( trace_flags: TraceFlags | None = DEFAULT_TRACE_OPTIONS, trace_state: TraceState | None = DEFAULT_TRACE_STATE, ) -> SpanContext: + """Build the span context and compute whether it is valid. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. Passing ``None`` for ``trace_flags`` + or ``trace_state`` substitutes the default rather than storing + ``None``. + + Returns: + A new, immutable span context. + """ if trace_flags is None: trace_flags = DEFAULT_TRACE_OPTIONS if trace_state is None: @@ -475,6 +688,12 @@ def __new__( def __getnewargs__( self, ) -> tuple[int, int, bool, TraceFlags, TraceState]: + """Return the arguments needed to rebuild this context when unpickling. + + Returns: + The five constructor arguments. The validity flag is left out + because :meth:`__new__` recomputes it. + """ return ( self.trace_id, self.span_id, @@ -485,38 +704,96 @@ def __getnewargs__( @property def trace_id(self) -> int: + """The id of the trace this span belongs to. + + Returns: + A 128-bit integer. It is zero in ``INVALID_SPAN_CONTEXT``, + but a context can be invalid while this id is non-zero, because + :attr:`is_valid` requires both ids to be in range. Test + :attr:`is_valid` rather than comparing this against zero. + """ return self[0] # pylint: disable=unsubscriptable-object @property def span_id(self) -> int: + """The id of this span. + + Returns: + A 64-bit integer. It is zero in ``INVALID_SPAN_CONTEXT``, but + a context can be invalid while this id is non-zero, because + :attr:`is_valid` requires both ids to be in range. Test + :attr:`is_valid` rather than comparing this against zero. + """ return self[1] # pylint: disable=unsubscriptable-object @property def is_remote(self) -> bool: + """Whether this context arrived from another process. + + Returns: + ``True`` when the context was extracted from a carrier by a + propagator, ``False`` when the span was started in this process. + """ return self[2] # pylint: disable=unsubscriptable-object @property def trace_flags(self) -> TraceFlags: + """The trace options to propagate. + + Returns: + The flags, including the sampled bit. + """ return self[3] # pylint: disable=unsubscriptable-object @property def trace_state(self) -> TraceState: + """The vendor-specific trace information to propagate. + + Returns: + The trace state, empty when no vendor has added anything. + """ return self[4] # pylint: disable=unsubscriptable-object @property def is_valid(self) -> bool: + """Whether this context identifies a real span. + + Returns: + ``True`` when both the trace id and the span id are non-zero and + in range. A propagator writes no header for an invalid context. + """ return self[5] # pylint: disable=unsubscriptable-object def __setattr__(self, *args: str) -> None: + """Ignore an attempt to set an attribute, since the type is immutable. + + The call is logged at debug level rather than raising. + + Args: + *args: The attribute name and value that were to be set. + """ _logger.debug("Immutable type, ignoring call to set attribute", stack_info=True) def __delattr__(self, *args: str) -> None: + """Ignore an attempt to delete an attribute, since the type is immutable. + + The call is logged at debug level rather than raising. + + Args: + *args: The name of the attribute that was to be deleted. + """ _logger.debug( "Immutable type, ignoring call to delete attribute", stack_info=True, ) def __repr__(self) -> str: + """Render the context with its ids in hexadecimal. + + Returns: + A string naming the trace id, span id, trace flags, trace state + and remote flag. + """ return f"{type(self).__name__}(trace_id=0x{format_trace_id(self.trace_id)}, span_id=0x{format_span_id(self.span_id)}, trace_flags=0x{self.trace_flags:02x}, trace_state={self.trace_state!r}, is_remote={self.is_remote})" @@ -524,25 +801,58 @@ class NonRecordingSpan(Span): """The Span that is used when no Span implementation is available. All operations are no-op except context propagation. + + Args: + context: The span context to carry. It is the only state this + span holds. """ def __init__(self, context: SpanContext) -> None: + """Wrap a span context so it can be made current and propagated. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._context = context def get_span_context(self) -> SpanContext: + """Return the wrapped span context. + + Returns: + The context this span was built with, unchanged. + """ return self._context def is_recording(self) -> bool: + """Report that nothing set on this span is recorded. + + Returns: + Always ``False``, so instrumentation skips building attribute + values that would be discarded. + """ return False def end(self, end_time: int | None = None) -> None: - pass + """Discard the call, since this span has no duration to record. + + Args: + end_time: Ignored. + """ def set_attributes(self, attributes: Mapping[str, types.AnyValue]) -> None: - pass + """Discard the attributes. + + Args: + attributes: Ignored. + """ def set_attribute(self, key: str, value: types.AnyValue) -> None: - pass + """Discard the attribute. + + Args: + key: Ignored. + value: Ignored. + """ def add_event( self, @@ -550,24 +860,44 @@ def add_event( attributes: types.Attributes = None, timestamp: int | None = None, ) -> None: - pass + """Discard the event. + + Args: + name: Ignored. + attributes: Ignored. + timestamp: Ignored. + """ def add_link( self, context: SpanContext, attributes: types.Attributes = None, ) -> None: - pass + """Discard the link. + + Args: + context: Ignored. + attributes: Ignored. + """ def update_name(self, name: str) -> None: - pass + """Discard the new name, since this span has none to update. + + Args: + name: Ignored. + """ def set_status( self, status: Status | StatusCode, description: str | None = None, ) -> None: - pass + """Discard the status. + + Args: + status: Ignored. + description: Ignored. + """ def record_exception( self, @@ -576,9 +906,21 @@ def record_exception( timestamp: int | None = None, escaped: bool = False, ) -> None: - pass + """Discard the exception. + + Args: + exception: Ignored. + attributes: Ignored. + timestamp: Ignored. + escaped: Ignored. + """ def __repr__(self) -> str: + """Render the span and the context it carries. + + Returns: + A string naming the class and the wrapped span context. + """ return f"NonRecordingSpan({self._context!r})" @@ -595,7 +937,8 @@ def __repr__(self) -> str: def format_trace_id(trace_id: int) -> str: - """Convenience trace ID formatting method + """Convenience trace ID formatting method. + Args: trace_id: Trace ID int @@ -606,7 +949,8 @@ def format_trace_id(trace_id: int) -> str: def format_span_id(span_id: int) -> str: - """Convenience span ID formatting method + """Convenience span ID formatting method. + Args: span_id: Span ID int diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index 88c07a69107..72558ce2191 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -1,6 +1,15 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""The outcome recorded on a finished span. + +A span carries a :class:`Status` saying whether the operation it covers +succeeded. The status is :attr:`StatusCode.UNSET` unless instrumentation sets +it, and a non-empty description may accompany it only when the code is +:attr:`StatusCode.ERROR`. A description that is ``None`` or the empty string +is stored whatever the code is. +""" + import enum import logging @@ -27,6 +36,12 @@ class Status: status_code: The canonical status code that describes the result status of the operation. description: An optional description of the status. + + A non-empty description is kept only when ``status_code`` is + :attr:`StatusCode.ERROR` and the description is a :class:`str`. Otherwise + it is discarded and a warning is logged, which is what the specification + requires. A description that is ``None`` or the empty string skips those + checks altogether and is stored as it was given, with no warning. """ def __init__( @@ -34,6 +49,11 @@ def __init__( status_code: StatusCode = StatusCode.UNSET, description: str | None = None, ): + """Build the status, discarding a description that is not permitted. + + Every parameter is documented on the class itself, since that is the + form the API reference renders. + """ self._status_code = status_code self._description = None @@ -49,20 +69,43 @@ def __init__( @property def status_code(self) -> StatusCode: - """Represents the canonical status code of a finished Span.""" + """Represents the canonical status code of a finished Span. + + Returns: + The code this status was built with. + """ return self._status_code @property def description(self) -> str | None: - """Status description""" + """Status description. + + Returns: + The description as it was stored. That is ``None`` when none was + supplied and when the one supplied was discarded for not being a + :class:`str` or for accompanying a non-error status code, and the + empty string when the empty string was supplied. + """ return self._description @property def is_ok(self) -> bool: - """Returns false if this represents an error, true otherwise.""" + """Returns false if this represents an error, true otherwise. + + Returns: + ``True`` when the status code is :attr:`StatusCode.OK` or + :attr:`StatusCode.UNSET`, ``False`` when it is + :attr:`StatusCode.ERROR`. + """ return self.is_unset or self._status_code is StatusCode.OK @property def is_unset(self) -> bool: - """Returns true if unset, false otherwise.""" + """Returns true if unset, false otherwise. + + Returns: + ``True`` when no status code has been set on this status, which is + the state of a span that finished without instrumentation deciding + its outcome. + """ return self._status_code is StatusCode.UNSET diff --git a/opentelemetry-api/src/opentelemetry/util/_decorator.py b/opentelemetry-api/src/opentelemetry/util/_decorator.py index 09b6e9acc5d..083d87e93ce 100644 --- a/opentelemetry-api/src/opentelemetry/util/_decorator.py +++ b/opentelemetry-api/src/opentelemetry/util/_decorator.py @@ -1,6 +1,16 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Context manager that also decorates coroutine functions correctly. + +:func:`_agnosticcontextmanager` replaces :func:`contextlib.contextmanager` for +the helpers that both wrap a block and decorate a function, such as +``Tracer.start_as_current_span``. Decorating a coroutine function with the +standard decorator closes the block when the coroutine object is created +rather than when it finishes, which reports a near-zero duration. The manager +here awaits the coroutine inside the block instead. +""" + import contextlib import functools import inspect @@ -35,9 +45,17 @@ class that will decorate async functions with an async context manager """ def __enter__(self) -> R: - """Reimplementing __enter__ to avoid the type error. + """Enter the context and return the value the generator yielded. + + This overrides the inherited method so that the declared return type is + ``R`` rather than :data:`~typing.Any`. - The original __enter__ method returns Any type, but we want to return R. + Returns: + The value yielded by the wrapped generator, for example the span + started by ``Tracer.start_as_current_span``. + + Raises: + RuntimeError: If the wrapped generator returned without yielding. """ del self.args, self.kwds, self.func # type: ignore try: @@ -46,10 +64,32 @@ def __enter__(self) -> R: raise RuntimeError("generator didn't yield") from None def __call__(self, func: V) -> V: # pyright: ignore [reportIncompatibleMethodOverride] + """Decorate ``func`` so each call runs inside a fresh context. + + A coroutine function is wrapped so that the context closes once the + coroutine finishes rather than once it is created. Any other callable + is handed to the inherited implementation. + + Args: + func: The callable to decorate. Both a coroutine function and an + ordinary function are accepted. + + Returns: + A wrapper around ``func`` with the same signature. + """ if inspect.iscoroutinefunction(func): @functools.wraps(func) # type: ignore async def async_wrapper(*args: Pargs, **kwargs: Pkwargs) -> R: # pyright: ignore [reportInvalidTypeVarUse] + """Await the coroutine with a fresh context manager entered. + + Args: + *args: Positional arguments forwarded to the coroutine. + **kwargs: Keyword arguments forwarded to the coroutine. + + Returns: + Whatever the coroutine returns. + """ with self._recreate_cm(): # type: ignore return await func(*args, **kwargs) # type: ignore @@ -60,8 +100,33 @@ async def async_wrapper(*args: Pargs, **kwargs: Pkwargs) -> R: # pyright: ignor def _agnosticcontextmanager( func: "Callable[P, Iterator[R]]", ) -> "Callable[P, _AgnosticContextManager[R]]": + """Turn a generator function into a context manager and a decorator. + + This behaves like :func:`contextlib.contextmanager`, except that the + returned object decorates a coroutine function correctly. See + :class:`_AgnosticContextManager`. + + Args: + func: A generator function yielding exactly once. The value it yields + becomes the result of the ``with`` statement. + + Returns: + A callable with the same signature as ``func`` that returns an + :class:`_AgnosticContextManager` usable as a context manager or as a + decorator. + """ + @functools.wraps(func) def helper(*args: Pargs, **kwargs: Pkwargs) -> _AgnosticContextManager[R]: # pyright: ignore [reportInvalidTypeVarUse] + """Build the context manager for one call of the wrapped function. + + Args: + *args: Positional arguments forwarded to the generator function. + **kwargs: Keyword arguments forwarded to the generator function. + + Returns: + A context manager bound to this particular set of arguments. + """ return _AgnosticContextManager(func, args, kwargs) # pyright: ignore [reportArgumentType] # Ignoring the type to keep the original signature of the function diff --git a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py index 656afbb809a..b2652864810 100644 --- a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py +++ b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py @@ -27,6 +27,22 @@ def _as_entry_points(eps: Any) -> EntryPoints: + """Normalize the result of ``importlib.metadata.entry_points()``. + + The standard library returns different types on different Python versions. + This flattens whichever form it receives into a single + :class:`importlib.metadata.EntryPoints` holding every entry point of every + group. + + Args: + eps: The value returned by ``importlib.metadata.entry_points()``. It is + an :class:`~importlib.metadata.EntryPoints` on Python 3.12 and + later, and a dict-like ``SelectableGroups`` on Python 3.10 and 3.11. + + Returns: + Every entry point from ``eps``, collected into one + :class:`~importlib.metadata.EntryPoints`. + """ # Python versions greater than 3.11 return EntryPoints. if isinstance(eps, EntryPoints): return eps @@ -42,10 +58,37 @@ def _as_entry_points(eps: Any) -> EntryPoints: @cache def _original_entry_points_cached() -> EntryPoints: + """Read every installed entry point and cache the result. + + Reading entry points touches the filesystem, so the result is cached for + the lifetime of the process. A distribution installed after the first call + is therefore not visible. + + :func:`functools.cache` holds no lock across the wrapped call, so threads + that arrive before the first call returns each perform the read, and the + last one to finish supplies the cached value. + + Returns: + Every entry point installed in the current environment. + """ return _as_entry_points(original_entry_points()) def entry_points(**params) -> EntryPoints: + """Select installed entry points, reading them from cache. + + This is a drop-in replacement for ``importlib.metadata.entry_points()`` + that behaves the same on every supported Python version and avoids + re-reading the filesystem on each call. + + Args: + **params: Selection criteria forwarded to + :meth:`importlib.metadata.EntryPoints.select`, typically ``group`` + and ``name``. Passing none selects every entry point. + + Returns: + The entry points matching ``params``, empty if none match. + """ return _original_entry_points_cached().select(**params) diff --git a/opentelemetry-api/src/opentelemetry/util/_once.py b/opentelemetry-api/src/opentelemetry/util/_once.py index 59f562385e6..64a3bd567e2 100644 --- a/opentelemetry-api/src/opentelemetry/util/_once.py +++ b/opentelemetry-api/src/opentelemetry/util/_once.py @@ -1,17 +1,31 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""One-shot execution primitive used to guard global provider initialisation. + +This module provides :class:`Once`, which runs a callable the first time it is +asked to and ignores every later request once that call has returned. The API +uses it to make sure a global provider is set exactly once even when several +threads race to set it. A call that raises leaves the instance unmarked, so the +next request runs the callable again. +""" + from collections.abc import Callable from threading import Lock class Once: - """Execute a function exactly once and block all callers until the function returns + """Execute a function once and block all callers until the function returns + + A call that raises leaves the instance unmarked, so the next call to + :meth:`do_once` executes the callable again. Counting only the calls that + return, the callable runs exactly once per instance. Same as golang's `sync.Once `_ """ def __init__(self) -> None: + """Initialize the lock and mark the guarded function as not yet executed.""" self._lock = Lock() self._done = False @@ -20,6 +34,14 @@ def do_once(self, func: Callable[[], None]) -> bool: Will block until ``func`` has been called by one thread. + Args: + func: The callable to execute. It takes no argument and its return + value is discarded. It is called at most once per :class:`Once` + instance no matter how many threads call this method, counting + only calls that return. A call that raises leaves the instance + unmarked and propagates the exception, so the next call to this + method runs ``func`` again. + Returns: Whether or not ``func`` was executed in this call """ diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index ef4d234ebeb..644511e974a 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -1,6 +1,13 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Entry-point lookup used to resolve the globally configured providers. + +The API never imports an SDK directly. Instead it reads the name of the wanted +implementation from an environment variable and loads the matching +``opentelemetry_*`` entry point, which is what :func:`_load_provider` does. +""" + from logging import getLogger from os import environ from typing import TYPE_CHECKING, TypeVar, cast @@ -15,6 +22,28 @@ def _load_provider(provider_environment_variable: str, provider: str) -> Provider: # type: ignore[type-var] + """Instantiate the provider implementation named by an environment variable. + + The value of ``provider_environment_variable`` is looked up as an entry + point name in the ``opentelemetry_{provider}`` group. When the variable is + unset, the name ``default_{provider}`` is used instead. The entry point is + loaded and called with no argument to build the provider. + + Args: + provider_environment_variable: Name of the environment variable holding + the entry point name to load, for example + ``"OTEL_PYTHON_TRACER_PROVIDER"``. + provider: Suffix identifying the entry point group and the default + entry point name, for example ``"tracer_provider"``. + + Returns: + A newly constructed provider of the type matching ``provider``. + + Raises: + Exception: Re-raises whatever the lookup or the construction raised, + after logging it. A missing entry point surfaces as + :exc:`StopIteration`. + """ # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 entry_points, diff --git a/opentelemetry-api/src/opentelemetry/util/re.py b/opentelemetry-api/src/opentelemetry/util/re.py index 595fcdfd999..3d57550373c 100644 --- a/opentelemetry-api/src/opentelemetry/util/re.py +++ b/opentelemetry-api/src/opentelemetry/util/re.py @@ -1,6 +1,14 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Parsing of HTTP headers supplied through environment variables. + +Several ``OTEL_*`` environment variables carry a list of HTTP headers in one +string, for example ``OTEL_EXPORTER_OTLP_HEADERS``. :func:`parse_env_headers` +turns such a string into a mapping, following the header format of the +OpenTelemetry protocol exporter specification. +""" + from collections.abc import Mapping from logging import getLogger from re import compile @@ -43,17 +51,44 @@ @deprecated("You should use parse_env_headers. Deprecated since version 1.15.0.") def parse_headers(s: str) -> Mapping[str, str]: + """Parse HTTP headers taken from an environment variable. + + Args: + s: The raw environment variable value holding the headers. + + Returns: + A mapping from lowercased header name to header value. + + .. deprecated:: 1.15.0 + Use :func:`parse_env_headers` instead. + """ return parse_env_headers(s) def parse_env_headers(s: str, liberal: bool = False) -> Mapping[str, str]: - """ - Parse ``s``, which is a ``str`` instance containing HTTP headers encoded - for use in ENV variables per the W3C Baggage HTTP header format at - https://www.w3.org/TR/baggage/#baggage-http-header-format, except that - additional semi-colon delimited metadata is not supported. - If ``liberal`` is True we try to parse ``s`` anyway to be more compatible - with other languages SDKs that accept non URL-encoded headers by default. + """Parse HTTP headers encoded for use in an environment variable. + + ``s`` holds comma separated ``name=value`` pairs in the W3C Baggage HTTP + header format at https://www.w3.org/TR/baggage/#baggage-http-header-format, + except that the additional semi-colon delimited metadata is not supported. + Names are lowercased. Both halves are URL-decoded only for a pair that + matches the strict pattern above; a pair accepted only because ``liberal`` + is set is taken verbatim, so a percent escape in it survives into the + result. + + A pair that does not match the expected format is skipped and a warning is + logged, so a single malformed entry does not discard the rest. + + Args: + s: The raw environment variable value holding the headers. + liberal: When ``True``, also accept values that are not URL-encoded, + which matches the behaviour of the SDKs of several other languages. + A space is permitted inside a value in this mode. Defaults to + ``False``, which accepts only the spec-conformant encoding. + + Returns: + A mapping from lowercased header name to header value, holding one + entry per pair that parsed successfully. """ headers: dict[str, str] = {} headers_list: list[str] = [header.strip(" \t") for header in s.split(",")] diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index f37d7c99cc1..9b2f3b3d6c0 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -1,6 +1,14 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Type aliases for the attribute and log body values the API accepts. + +The aliases here spell out which Python values are valid wherever the API takes +attributes or a log record body. :data:`AnyValue` is the recursive alias +matching the ``any`` type of the OpenTelemetry log data model, and +:data:`Attributes` is the mapping form accepted by spans, metrics and logs. +""" + from collections.abc import Mapping, Sequence from typing import TypeAlias @@ -8,8 +16,25 @@ # For more details, refer to the OTel specification: # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#type-any AnyValue: TypeAlias = str | bool | int | float | bytes | Sequence["AnyValue"] | Mapping[str, "AnyValue"] | None +"""Any value the OpenTelemetry log data model accepts. + +The alias is recursive: a value is a scalar, ``None``, a sequence of +:data:`AnyValue`, or a mapping from :class:`str` to :data:`AnyValue`. +""" + AttributeValue: TypeAlias = AnyValue +"""The value half of a single attribute. + +This is an alias of :data:`AnyValue`, kept as a separate name so that code +annotating attributes reads as such. +""" + Attributes = Mapping[str, AnyValue] | None +"""A full set of attributes, or ``None`` when no attribute is supplied. + +Keys are non-empty strings. ``None`` and an empty mapping are both accepted +wherever the API takes attributes, and both mean that no attribute is set. +""" # Deprecated type, do not use. AttributesAsKey = tuple[ @@ -26,3 +51,9 @@ ], ..., ] +"""A set of attributes flattened into a hashable tuple of name/value pairs. + +.. deprecated:: 1.45.0 + This alias is retained for backwards compatibility and is not used by the + API. Use :data:`Attributes` instead. +""" diff --git a/opentelemetry-api/src/opentelemetry/version/__init__.py b/opentelemetry-api/src/opentelemetry/version/__init__.py index 6e653a1c7ce..3d26c68ddf5 100644 --- a/opentelemetry-api/src/opentelemetry/version/__init__.py +++ b/opentelemetry-api/src/opentelemetry/version/__init__.py @@ -1,4 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +"""Version of the ``opentelemetry-api`` distribution.""" + __version__ = "1.46.0.dev" +"""The installed version of ``opentelemetry-api``, as a :pep:`440` string."""