This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
@@ -0,0 +1,47 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""
The OpenTelemetry logging API describes the classes used to generate logs and events.
The :class:`.LoggerProvider` provides users access to the :class:`.Logger`.
This module provides abstract (i.e. unimplemented) classes required for
logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications
to use the API package alone without a supporting implementation.
To get a logger, you need to provide the package name from which you are
calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger`
with the calling module name and the version of your package.
The following code shows how to obtain a logger using the global :class:`.LoggerProvider`::
from opentelemetry._logs import get_logger
logger = get_logger("example-logger")
.. versionadded:: 1.15.0
"""
from opentelemetry._logs._internal import (
Logger,
LoggerProvider,
LogRecord,
NoOpLogger,
NoOpLoggerProvider,
get_logger,
get_logger_provider,
set_logger_provider,
)
from opentelemetry._logs.severity import SeverityNumber
__all__ = [
"LogRecord",
"Logger",
"LoggerProvider",
"NoOpLogger",
"NoOpLoggerProvider",
"SeverityNumber",
"get_logger",
"get_logger_provider",
"set_logger_provider",
]
@@ -0,0 +1,484 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""
The OpenTelemetry logging API describes the classes used to generate logs and events.
The :class:`.LoggerProvider` provides users access to the :class:`.Logger`.
This module provides abstract (i.e. unimplemented) classes required for
logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications
to use the API package alone without a supporting implementation.
To get a logger, you need to provide the package name from which you are
calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger`
with the calling module name and the version of your package.
The following code shows how to obtain a logger using the global :class:`.LoggerProvider`::
from opentelemetry._logs import get_logger
logger = get_logger("example-logger")
.. versionadded:: 1.15.0
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from logging import getLogger
from os import environ
from time import time_ns
from typing import cast, overload
from typing_extensions import deprecated
from opentelemetry._logs.severity import SeverityNumber
from opentelemetry.context import get_current
from opentelemetry.context.context import Context
from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER
from opentelemetry.trace import get_current_span
from opentelemetry.trace.span import TraceFlags
from opentelemetry.util._once import Once
from opentelemetry.util._providers import _load_provider
from opentelemetry.util.types import AnyValue, Attributes
_logger = getLogger(__name__)
class LogRecord(ABC):
"""A LogRecord instance represents an event being logged.
LogRecord instances are created and emitted via `Logger`
every time something is logged. They contain all the information
pertinent to the event being logged.
"""
@overload
def __init__(
self,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
severity_text: str | None = None,
severity_number: SeverityNumber | None = None,
body: AnyValue = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None: ...
@overload
@deprecated(
"LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead."
)
def __init__(
self,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
trace_id: int | None = None,
span_id: int | None = None,
trace_flags: TraceFlags | None = None,
severity_text: str | None = None,
severity_number: SeverityNumber | None = None,
body: AnyValue = None,
attributes: Attributes = None,
) -> None: ...
def __init__(
self,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
trace_id: int | None = None,
span_id: int | None = None,
trace_flags: TraceFlags | None = None,
severity_text: str | None = None,
severity_number: SeverityNumber | None = None,
body: AnyValue = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None:
if not context:
context = get_current()
span_context = get_current_span(context).get_span_context()
self.timestamp = timestamp
if observed_timestamp is None:
observed_timestamp = time_ns()
self.observed_timestamp = observed_timestamp
self.context = context
self.trace_id = trace_id or span_context.trace_id
self.span_id = span_id or span_context.span_id
self.trace_flags = trace_flags or span_context.trace_flags
self.severity_text = severity_text
self.severity_number = severity_number
self.body = body
self.attributes = attributes
self.event_name = event_name
self.exception = exception
class Logger(ABC):
"""Handles emitting events and logs via `LogRecord`."""
def __init__(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> None:
super().__init__()
self._name = name
self._version = version
self._schema_url = schema_url
self._attributes = attributes
@overload
def emit(
self,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
severity_text: str | None = None,
body: AnyValue | None = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None: ...
@overload
def emit(
self,
record: LogRecord,
) -> None: ...
@abstractmethod
def emit(
self,
record: LogRecord | None = None,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
severity_text: str | None = None,
body: AnyValue | None = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None:
"""Emits a :class:`LogRecord` representing a log to the processing pipeline."""
@abstractmethod
def enabled(
self,
*,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
"""Returns whether the logger is enabled for the given arguments.
Instrumentation should call this before performing expensive work to
construct a log record, and skip that work if ``False`` is returned.
The returned value may change over time and should be checked each time
before emitting a log record.
"""
class NoOpLogger(Logger):
"""The default Logger used when no Logger implementation is available.
All operations are no-op.
"""
@overload
def emit(
self,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
severity_text: str | None = None,
body: AnyValue | None = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None: ...
@overload
def emit( # pylint:disable=arguments-differ
self,
record: LogRecord,
) -> None: ...
def emit(
self,
record: LogRecord | None = None,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
severity_text: str | None = None,
body: AnyValue | None = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None:
pass
def enabled(
self,
*,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
return False
class ProxyLogger(Logger):
def __init__( # pylint: disable=super-init-not-called
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
):
self._name = name
self._version = version
self._schema_url = schema_url
self._attributes = attributes
self._real_logger: Logger | None = None
self._noop_logger = NoOpLogger(name)
@property
def _logger(self) -> Logger:
if self._real_logger:
return self._real_logger
if _LOGGER_PROVIDER:
self._real_logger = _LOGGER_PROVIDER.get_logger(
self._name,
self._version,
self._schema_url,
self._attributes,
)
return self._real_logger
return self._noop_logger
@overload
def emit(
self,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
severity_text: str | None = None,
body: AnyValue | None = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None: ...
@overload
def emit( # pylint:disable=arguments-differ
self,
record: LogRecord,
) -> None: ...
def emit(
self,
record: LogRecord | None = None,
*,
timestamp: int | None = None,
observed_timestamp: int | None = None,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
severity_text: str | None = None,
body: AnyValue | None = None,
attributes: Attributes = None,
event_name: str | None = None,
exception: BaseException | None = None,
) -> None:
if record:
self._logger.emit(record)
else:
self._logger.emit(
timestamp=timestamp,
observed_timestamp=observed_timestamp,
context=context,
severity_number=severity_number,
severity_text=severity_text,
body=body,
attributes=attributes,
event_name=event_name,
exception=exception,
)
def enabled(
self,
*,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
return self._logger.enabled(
context=context,
severity_number=severity_number,
event_name=event_name,
)
class LoggerProvider(ABC):
"""
LoggerProvider is the entry point of the API. It provides access to Logger instances.
"""
@abstractmethod
def get_logger(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> Logger:
"""Returns a `Logger` for use by the given instrumentation library.
For any two calls with identical parameters, it is undefined whether the same
or different `Logger` instances are returned.
This function may return different `Logger` types (e.g. a no-op logger
vs. a functional logger).
Args:
name: The name of the instrumenting module, package or class.
This should *not* be the name of the module, package or class that is
instrumented but the name of the code doing the instrumentation.
E.g., instead of ``"requests"``, use
``"opentelemetry.instrumentation.requests"``.
For log sources which define a logger name (e.g. logging.Logger.name)
the Logger Name should be recorded as the instrumentation scope name.
version: Optional. The version string of the
instrumenting library. Usually this should be the same as
``importlib.metadata.version(instrumenting_library_name)``.
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
attributes: Optional. Specifies the instrumentation scope attributes to
associate with emitted telemetry.
"""
class NoOpLoggerProvider(LoggerProvider):
"""The default LoggerProvider used when no LoggerProvider implementation is available."""
def get_logger(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> Logger:
"""Returns a NoOpLogger."""
return NoOpLogger(name, version=version, schema_url=schema_url, attributes=attributes)
class ProxyLoggerProvider(LoggerProvider):
def get_logger(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> Logger:
if _LOGGER_PROVIDER:
return _LOGGER_PROVIDER.get_logger(
name,
version=version,
schema_url=schema_url,
attributes=attributes,
)
return ProxyLogger(
name,
version=version,
schema_url=schema_url,
attributes=attributes,
)
_LOGGER_PROVIDER_SET_ONCE = Once()
_LOGGER_PROVIDER: LoggerProvider | None = None
_PROXY_LOGGER_PROVIDER = ProxyLoggerProvider()
def get_logger_provider() -> LoggerProvider:
"""Gets the current global :class:`~.LoggerProvider` object."""
global _LOGGER_PROVIDER # pylint: disable=global-variable-not-assigned
if _LOGGER_PROVIDER is None:
if _OTEL_PYTHON_LOGGER_PROVIDER not in environ:
return _PROXY_LOGGER_PROVIDER
logger_provider: LoggerProvider = _load_provider( # type: ignore
_OTEL_PYTHON_LOGGER_PROVIDER, "logger_provider"
)
_set_logger_provider(logger_provider, log=False)
# _LOGGER_PROVIDER will have been set by one thread
return cast("LoggerProvider", _LOGGER_PROVIDER)
def _set_logger_provider(logger_provider: LoggerProvider, log: bool) -> None:
def set_lp() -> None:
global _LOGGER_PROVIDER # pylint: disable=global-statement
_LOGGER_PROVIDER = logger_provider
did_set = _LOGGER_PROVIDER_SET_ONCE.do_once(set_lp)
if log and not did_set:
_logger.warning("Overriding of current LoggerProvider is not allowed")
def set_logger_provider(logger_provider: LoggerProvider) -> None:
"""Sets the current global :class:`~.LoggerProvider` object.
This can only be done once, a warning will be logged if any further attempt
is made.
"""
_set_logger_provider(logger_provider, log=True)
def get_logger(
instrumenting_module_name: str,
instrumenting_library_version: str = "",
logger_provider: LoggerProvider | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> Logger:
"""Returns a `Logger` for use within a python process.
This function is a convenience wrapper for
opentelemetry.sdk._logs.LoggerProvider.get_logger.
If logger_provider param is omitted the current configured one is used.
"""
if logger_provider is None:
logger_provider = get_logger_provider()
return logger_provider.get_logger(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
attributes,
)
@@ -0,0 +1,44 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
import enum
class SeverityNumber(enum.Enum):
"""Numerical value of severity.
Smaller numerical values correspond to less severe events
(such as debug events), larger numerical values correspond
to more severe events (such as errors and critical events).
See the `Log Data Model`_ spec for more info and how to map the
severity from source format to OTLP Model.
.. _Log Data Model: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber
"""
UNSPECIFIED = 0
TRACE = 1
TRACE2 = 2
TRACE3 = 3
TRACE4 = 4
DEBUG = 5
DEBUG2 = 6
DEBUG3 = 7
DEBUG4 = 8
INFO = 9
INFO2 = 10
INFO3 = 11
INFO4 = 12
WARN = 13
WARN2 = 14
WARN3 = 15
WARN4 = 16
ERROR = 17
ERROR2 = 18
ERROR3 = 19
ERROR4 = 20
FATAL = 21
FATAL2 = 22
FATAL3 = 23
FATAL4 = 24
@@ -0,0 +1,243 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
# pyright: reportUnnecessaryIsInstance=false
import copy
import logging
import threading
from collections.abc import Mapping, MutableMapping, Sequence
from types import NoneType
from typing import TYPE_CHECKING, Any, overload
from typing_extensions import assert_never, deprecated
from opentelemetry.util import types
_logger = logging.getLogger(__name__)
# Calling str(x) will use an object's `__str__` method if it exists, otherwise it will use it's `__repr__` method.
# 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:
return type(key).__str__ is not object.__str__ or type(key).__repr__ is not object.__repr__
@overload
def _clean_attribute_value(
value: Mapping[str, types.AnyValue],
max_string_value_length: int | None,
) -> Mapping[str, types.AnyValue]: ...
@overload
def _clean_attribute_value(
value: types.AnyValue,
max_string_value_length: int | None,
) -> types.AnyValue: ...
def _clean_attribute_value(
value: types.AnyValue,
max_string_value_length: int | None,
) -> types.AnyValue:
"""Recursively checks if an attribute value is valid and cleans it if required.
String values are truncated to max_string_value_length if provided.
Anything that isn't of `types.AnyValue`, we attempt to cast to `str`.
If this fails, the value is replaced with None. Sequence's are converted to tuples and mappings
are copied into new dicts.
Returns:
The recursively cleaned AnyValue.
"""
if isinstance(value, (NoneType, bool, int, float, bytes)):
return value
if isinstance(value, str):
if max_string_value_length is not None and len(value) > max_string_value_length:
_logger.warning(
"String attribute value exceeds max length of %d, truncating.",
max_string_value_length,
)
value = value[:max_string_value_length]
return value
if isinstance(value, Sequence):
return tuple(_clean_attribute_value(v, max_string_value_length) for v in value)
if isinstance(value, Mapping):
cleaned_mapping: dict[str, types.AnyValue] = {}
for key, val in value.items():
if not key:
_logger.warning(
"invalid attribute key `%s`. must be non-empty string. Dropping key from attributes.",
key,
)
continue
# Spec says to convert unknown types to strings if possible (here and below too).
if not isinstance(key, str):
_logger.warning(
"Invalid type `%s` for attribute key `%s`, must be a str. Key's `__str__/__repr__` method will be called if it exists, otherwise the key/value pair will be dropped.",
type(key),
key,
)
if _is_non_custom_str(key):
key = str(key)
else:
continue
cleaned_mapping[key] = _clean_attribute_value(val, max_string_value_length)
return cleaned_mapping
if TYPE_CHECKING:
assert_never(value)
_logger.warning(
"Invalid type `%s` for attribute value. Expected one of bool, str, None, bytes, int, float or a "
"Mapping or Sequence of those types. Value's __str__ method will be called if it exists, otherwise the value will be replaced with None.",
type(value),
)
if _is_non_custom_str(value):
return str(value)
return None
class BoundedAttributes(MutableMapping[str, types.AnyValue]):
"""A dict with a fixed max capacity which cleans and potentially drops values to ensure they are valid attribute values.
Args:
maxlen: The maximum number of attributes to store, use None for no limit.
attributes: The initial attributes to store.
immutable: Defaults to true. Whether to allow adding/removing of attributes after the initialisation of the instance.
max_value_len: The maximum length of string values, use None for no limit.
extended_attributes: Deprecated. Kept for backwards compatibility. Extended attributes are now always used for attributes everywhere.
When the dict is full and a new element is added, the oldest element is dropped. Attributes are made to be immutable when set in this container.
So passing a mutable list as an attribute value, and then mutating it after will not change it's value in this container.
"""
@overload
def __init__(
self,
maxlen: int | None = None,
attributes: types.Attributes = None,
immutable: bool = True,
max_value_len: int | None = None,
) -> None: ...
@overload
@deprecated(
"Creating BoundedAttributes with `extended_attributes` set is deprecated. "
"The `extended_attributes` param is no longer used and will be removed "
"in a future release. Extended attributes are now always used for attributes everywhere."
)
def __init__(
self,
maxlen: int | None = None,
attributes: types.Attributes = None,
immutable: bool = True,
max_value_len: int | None = None,
extended_attributes: bool = False,
) -> None: ...
def __init__(
self,
maxlen: int | None = None,
attributes: types.Attributes = None,
immutable: bool = True,
max_value_len: int | None = None,
extended_attributes: bool = False,
) -> None:
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:
raise ValueError("max_value_len must be valid int greater or equal to 0")
self._dict: dict[str, types.AnyValue] = {}
self.maxlen = maxlen
self.dropped = 0
self.max_value_len = max_value_len
self._lock = threading.Lock()
# setting False before adding items
self._immutable = False
if attributes:
self._set_items(attributes)
self._immutable = immutable
def __repr__(self) -> str:
return f"{dict(self._dict)}"
def __getitem__(self, key: str) -> types.AnyValue:
return self._dict[key]
def _raise_if_immutable(self) -> None:
if self._immutable:
raise TypeError("Cannot mutate immutable BoundedAttributes")
def __setitem__(self, key: str, value: types.AnyValue) -> None:
self._raise_if_immutable()
if self.maxlen is not None and self.maxlen == 0:
with self._lock:
self.dropped += 1
return
if not key or not isinstance(key, str):
_logger.warning(
"invalid key `%s`. must be non-empty string. Dropping key from attributes.",
key,
)
with self._lock:
self.dropped += 1
return
cleaned = _clean_attribute_value(value, self.max_value_len)
with self._lock:
self._setitem_locked(key, cleaned)
def _set_items(self, attributes: Mapping[str, types.AnyValue]) -> None:
self._raise_if_immutable()
if self.maxlen is not None and self.maxlen == 0:
with self._lock:
self.dropped += len(attributes)
return
cleaned_attributes: Mapping[str, types.AnyValue] = _clean_attribute_value(attributes, self.max_value_len)
with self._lock:
self.dropped += len(attributes) - len(cleaned_attributes)
for key, value in cleaned_attributes.items():
self._setitem_locked(key, value)
def _setitem_locked(self, key: str, value: types.AnyValue) -> None:
if key in self._dict:
del self._dict[key]
if self.maxlen is not None and len(self._dict) >= self.maxlen:
_logger.warning(
"Attributes dict is full. Dropping the oldest key-value pair from attributes to make space for the new key-value pair.",
)
# Dictionaries are insertion ordered in Python, this is the recommended way to get the oldest value.
del self._dict[next(iter(self._dict.keys()))]
self.dropped += 1
self._dict[key] = value
def __delitem__(self, key: str) -> None:
self._raise_if_immutable()
del self._dict[key]
def __iter__(self):
if self._immutable:
return iter(self._dict)
with self._lock:
return iter(list(self._dict))
def __len__(self) -> int:
return len(self._dict)
def __deepcopy__(self, memo: dict[int, Any]) -> "BoundedAttributes":
copy_ = BoundedAttributes(
maxlen=self.maxlen,
immutable=self._immutable,
max_value_len=self.max_value_len,
)
memo[id(self)] = copy_
with self._lock:
# Assign _dict directly to avoid re-cleaning already clean values
# and to bypass the immutability guard in __setitem__
copy_._dict = copy.deepcopy(self._dict, memo)
copy_.dropped = self.dropped
return copy_
def copy(self) -> MutableMapping[str, types.AnyValue]:
return self._dict.copy()
@@ -0,0 +1,122 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Mapping
from logging import getLogger
from re import compile
from types import MappingProxyType
from opentelemetry.context import create_key, get_value, set_value
from opentelemetry.context.context import Context
from opentelemetry.util.re import (
_BAGGAGE_PROPERTY_FORMAT,
_KEY_FORMAT,
_VALUE_FORMAT,
)
_BAGGAGE_KEY = create_key("baggage")
_logger = getLogger(__name__)
_KEY_PATTERN = compile(_KEY_FORMAT)
_VALUE_PATTERN = compile(_VALUE_FORMAT)
_PROPERT_PATTERN = compile(_BAGGAGE_PROPERTY_FORMAT)
def get_all(
context: Context | None = None,
) -> Mapping[str, object]:
"""Returns the name/value pairs in the Baggage
Args:
context: The Context to use. If not set, uses current Context
Returns:
The name/value pairs in the Baggage
"""
return MappingProxyType(_get_baggage_value(context=context))
def get_baggage(name: str, context: Context | None = None) -> object | None:
"""Provides access to the value for a name/value pair in the
Baggage
Args:
name: The name of the value to retrieve
context: The Context to use. If not set, uses current Context
Returns:
The value associated with the given name, or null if the given name is
not present.
"""
return _get_baggage_value(context=context).get(name)
def set_baggage(name: str, value: object, context: Context | None = None) -> Context:
"""Sets a value in the Baggage
Args:
name: The name of the value to set
value: The value to set
context: The Context to use. If not set, uses current Context
Returns:
A Context with the value updated
"""
baggage = _get_baggage_value(context=context).copy()
baggage[name] = value
return set_value(_BAGGAGE_KEY, baggage, context=context)
def remove_baggage(name: str, context: Context | None = None) -> Context:
"""Removes a value from the Baggage
Args:
name: The name of the value to remove
context: The Context to use. If not set, uses current Context
Returns:
A Context with the name/value removed
"""
baggage = _get_baggage_value(context=context).copy()
baggage.pop(name, None)
return set_value(_BAGGAGE_KEY, baggage, context=context)
def clear(context: Context | None = None) -> Context:
"""Removes all values from the Baggage
Args:
context: The Context to use. If not set, uses current Context
Returns:
A Context with all baggage entries removed
"""
return set_value(_BAGGAGE_KEY, {}, context=context)
def _get_baggage_value(context: Context | None = None) -> dict[str, object]:
baggage = get_value(_BAGGAGE_KEY, context=context)
if isinstance(baggage, dict):
return baggage
return {}
def _is_valid_key(name: str) -> bool:
return _KEY_PATTERN.fullmatch(str(name)) is not None
def _is_valid_value(value: object) -> bool:
parts = str(value).split(";")
# W3C Baggage allows optional whitespace between the value and ";"
is_valid_value = _VALUE_PATTERN.fullmatch(parts[0].rstrip(" \t")) is not None
if len(parts) > 1: # one or more properties metadata
for property in parts[1:]:
if _PROPERT_PATTERN.fullmatch(property) is None:
is_valid_value = False
break
return is_valid_value
def _is_valid_pair(key: str, value: str) -> bool:
return _is_valid_key(key) and _is_valid_value(value)
@@ -0,0 +1,179 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
#
from collections.abc import Iterable, Iterator, Mapping
from logging import getLogger
from re import split
from urllib.parse import quote_plus, unquote_plus
from opentelemetry.baggage import _is_valid_pair, get_all, set_baggage
from opentelemetry.context import get_current
from opentelemetry.context.context import Context
from opentelemetry.propagators import textmap
from opentelemetry.util.re import _DELIMITER_PATTERN
_logger = getLogger(__name__)
def _filter_valid_entries(
entries: Iterable[str],
max_pair_length: int,
) -> Iterator[str]:
for entry in entries:
if not entry:
continue
if not entry.isascii():
_logger.warning(
"Baggage entry with key `%s` contains non-ASCII characters",
entry.split("=", 1)[0],
)
continue
if len(entry) > max_pair_length:
_logger.warning(
"Baggage entry with key `%s` exceeded the maximum number of bytes per list-member with length %d",
entry.split("=", 1)[0],
len(entry),
)
continue
yield entry
def _apply_baggage_limits(
entries: Iterable[str],
max_pairs: int,
max_pair_length: int,
max_header_length: int,
) -> Iterator[str]:
"""Apply W3C Baggage size limits to a sequence of baggage entries.
Yields entries that fit within the W3C specification limits.
Logs warnings when entries are dropped.
"""
length = 0
for index, entry in enumerate(_filter_valid_entries(entries, max_pair_length)):
if index >= max_pairs:
_logger.warning("Baggage exceeded the maximum number of list-members")
return
length += (1 if index > 0 else 0) + len(entry)
if length > max_header_length:
_logger.warning("Baggage exceeded the maximum number of bytes per baggage-string")
return
yield entry
class W3CBaggagePropagator(textmap.TextMapPropagator):
"""Extracts and injects Baggage which is used to annotate telemetry."""
_MAX_HEADER_LENGTH = 8192
_MAX_PAIR_LENGTH = 4096
_MAX_PAIRS = 180
_BAGGAGE_HEADER_NAME = "baggage"
def extract(
self,
carrier: textmap.CarrierT,
context: Context | None = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Extract Baggage from the carrier.
See
`opentelemetry.propagators.textmap.TextMapPropagator.extract`
"""
if context is None:
context = get_current()
header = _extract_first_element(getter.get(carrier, self._BAGGAGE_HEADER_NAME))
if not header:
return context
if len(header.encode()) > self._MAX_HEADER_LENGTH:
_logger.warning(
"Baggage header `%s` exceeded the maximum number of bytes per baggage-string",
header,
)
return context
baggage_entries = split(_DELIMITER_PATTERN, header)
for entry in _apply_baggage_limits(
baggage_entries,
max_pairs=self._MAX_PAIRS,
max_pair_length=self._MAX_PAIR_LENGTH,
max_header_length=self._MAX_HEADER_LENGTH,
):
try:
name, value = entry.split("=", 1)
except Exception: # pylint: disable=broad-exception-caught
_logger.warning("Baggage list-member `%s` doesn't match the format", entry)
continue
# W3C Baggage allows optional whitespace around "="
name = name.strip(" \t")
value = value.strip(" \t")
if not _is_valid_pair(name, value):
_logger.warning("Invalid baggage entry: `%s`", entry)
continue
name = unquote_plus(name).strip()
value = unquote_plus(value).strip()
context = set_baggage(
name,
value,
context=context,
)
return context
def inject(
self,
carrier: textmap.CarrierT,
context: Context | None = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Injects Baggage into the carrier.
See
`opentelemetry.propagators.textmap.TextMapPropagator.inject`
"""
baggage_entries = get_all(context=context)
if not baggage_entries:
return
baggage_string = ",".join(
_apply_baggage_limits(
_encode_baggage_pairs(baggage_entries),
max_pairs=self._MAX_PAIRS,
max_pair_length=self._MAX_PAIR_LENGTH,
max_header_length=self._MAX_HEADER_LENGTH,
)
)
if baggage_string:
setter.set(carrier, self._BAGGAGE_HEADER_NAME, baggage_string)
@property
def fields(self) -> set[str]:
"""Returns a set with the fields set in `inject`."""
return {self._BAGGAGE_HEADER_NAME}
def _encode_baggage_pairs(
baggage_entries: Mapping[str, object],
) -> Iterator[str]:
"""Yield URL-encoded 'key=value' pairs from baggage entries."""
for key, value in baggage_entries.items():
yield quote_plus(str(key)) + "=" + quote_plus(str(value))
def _extract_first_element(
items: Iterable[textmap.CarrierT] | None,
) -> textmap.CarrierT | None:
if items is None:
return None
return next(iter(items), None)
@@ -0,0 +1,147 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import logging
import os
from contextvars import Token
from uuid import uuid4
# pylint: disable=wrong-import-position
from opentelemetry.context.context import Context, _RuntimeContext
from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext
from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT
logger = logging.getLogger(__name__)
def _load_runtime_context() -> _RuntimeContext:
"""Initialize the RuntimeContext
Returns:
An instance of RuntimeContext.
"""
configured_context = os.environ.get(OTEL_PYTHON_CONTEXT)
if not configured_context:
return ContextVarsRuntimeContext()
# pylint: disable=import-outside-toplevel,no-name-in-module
from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415
entry_points,
)
try:
return next(iter(entry_points(group="opentelemetry_context", name=configured_context))).load()()
except Exception: # pylint: disable=broad-exception-caught
logger.exception(
"Failed to load context: %s, falling back to contextvars_context",
configured_context,
)
return ContextVarsRuntimeContext()
_RUNTIME_CONTEXT = _load_runtime_context()
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.
"""
return keyname + "-" + str(uuid4())
def get_value(key: str, context: Context | None = None) -> object:
"""To access the local state of a concern, the RuntimeContext API
provides a function which takes a context and a key as input,
and returns a value.
Args:
key: The key of the value to retrieve.
context: The context from which to retrieve the value, if None, the current context is used.
Returns:
The value associated with the key.
"""
return context.get(key) if context is not None else get_current().get(key)
def set_value(key: str, value: object, context: Context | None = None) -> Context:
"""To record the local state of a cross-cutting concern, the
RuntimeContext API provides a function which takes a context, a
key, and a value as input, and returns an updated context
which contains the new value.
Args:
key: The key of the entry to set.
value: The value of the entry to set.
context: The context to copy, if None, the current context is used.
Returns:
A new `Context` containing the value set.
"""
if context is None:
context = get_current()
new_values = context.copy()
new_values[key] = value
return Context(new_values)
def get_current() -> Context:
"""To access the context associated with program execution,
the Context API provides a function which takes no arguments
and returns a Context.
Returns:
The current `Context` object.
"""
return _RUNTIME_CONTEXT.get_current()
def attach(context: Context) -> Token[Context]:
"""Associates a Context with the caller's current execution unit. Returns
a token that can be used to restore the previous Context.
Args:
context: The Context to set as current.
Returns:
A token that can be used with `detach` to reset the context.
"""
return _RUNTIME_CONTEXT.attach(context)
def detach(token: Token[Context]) -> None:
"""Resets the Context associated with the caller's current execution unit
to the value it had before attaching a specified Context.
Args:
token: The Token that was returned by a previous call to attach a Context.
"""
try:
_RUNTIME_CONTEXT.detach(token)
except Exception: # pylint: disable=broad-exception-caught
logger.exception("Failed to detach context")
# FIXME This is a temporary location for the suppress instrumentation key.
# Once the decision around how to suppress instrumentation is made in the
# spec, this key should be moved accordingly.
_ON_EMIT_RECURSION_COUNT_KEY = create_key("on_emit_recursion_count")
_SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation")
_SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key("suppress_http_instrumentation")
__all__ = [
"Context",
"attach",
"create_key",
"detach",
"get_current",
"get_value",
"set_value",
]
@@ -0,0 +1,65 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from abc import ABC, abstractmethod
from contextvars import Token
class Context(dict[str, object]):
def __setitem__(self, key: str, value: object) -> None:
raise ValueError
def __delitem__(self, key: str) -> None:
raise ValueError
def setdefault(self, key: str, default: object = None) -> object:
raise ValueError
def pop(self, key: str, *args: object) -> object:
raise ValueError
def popitem(self) -> tuple[str, object]:
raise ValueError
def clear(self) -> None:
raise ValueError
def update(self, *args: object, **kwargs: object) -> None:
raise ValueError
def __ior__(self, other: object) -> Context:
raise ValueError
class _RuntimeContext(ABC):
"""The RuntimeContext interface provides a wrapper for the different
mechanisms that are used to propagate context in Python.
Implementations can be made available via entry_points and
selected through environment variables.
"""
@abstractmethod
def attach(self, context: Context) -> Token[Context]:
"""Sets the current `Context` object. Returns a
token that can be used to reset to the previous `Context`.
Args:
context: The Context to set.
"""
@abstractmethod
def get_current(self) -> Context:
"""Returns the current `Context` object."""
@abstractmethod
def detach(self, token: Token[Context]) -> None:
"""Resets Context to a previous value
Args:
token: A reference to a previous Context.
"""
__all__ = ["Context"]
@@ -0,0 +1,43 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from contextvars import ContextVar, Token
from opentelemetry.context.context import Context, _RuntimeContext
class ContextVarsRuntimeContext(_RuntimeContext):
"""An implementation of the RuntimeContext interface which wraps ContextVar under
the hood. This is the preferred implementation for usage with Python 3.5+
"""
_CONTEXT_KEY = "current_context"
def __init__(self) -> None:
self._current_context = ContextVar(self._CONTEXT_KEY, default=Context())
def attach(self, context: Context) -> Token[Context]:
"""Sets the current `Context` object. Returns a
token that can be used to reset to the previous `Context`.
Args:
context: The Context to set.
"""
return self._current_context.set(context)
def get_current(self) -> Context:
"""Returns the current `Context` object."""
return self._current_context.get()
def detach(self, token: Token[Context]) -> None:
"""Resets Context to a previous value
Args:
token: A reference to a previous Context.
"""
self._current_context.reset(token)
__all__ = ["ContextVarsRuntimeContext"]
@@ -0,0 +1,72 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER"
"""
.. envvar:: OTEL_LOGS_EXPORTER
"""
OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER"
"""
.. envvar:: OTEL_METRICS_EXPORTER
Specifies which exporter is used for metrics. See `General SDK Configuration
<https://opentelemetry.io/docs/concepts/sdk-configuration/general-sdk-configuration/#otel_metrics_exporter>`_.
**Default value:** ``"otlp"``
**Example:**
``export OTEL_METRICS_EXPORTER="prometheus"``
Accepted values for ``OTEL_METRICS_EXPORTER`` are:
- ``"otlp"``
- ``"prometheus"``
- ``"none"``: No automatically configured exporter for metrics.
.. note::
Exporter packages may add entry points for group ``opentelemetry_metrics_exporter`` which
can then be used with this environment variable by name. The entry point should point to
either a `opentelemetry.sdk.metrics.export.MetricExporter` (push exporter) or
`opentelemetry.sdk.metrics.export.MetricReader` (pull exporter) subclass; it must be
constructable without any required arguments. This mechanism is considered experimental and
may change in subsequent releases.
"""
OTEL_PROPAGATORS = "OTEL_PROPAGATORS"
"""
.. envvar:: OTEL_PROPAGATORS
"""
OTEL_PYTHON_CONTEXT = "OTEL_PYTHON_CONTEXT"
"""
.. envvar:: OTEL_PYTHON_CONTEXT
"""
OTEL_PYTHON_ID_GENERATOR = "OTEL_PYTHON_ID_GENERATOR"
"""
.. envvar:: OTEL_PYTHON_ID_GENERATOR
"""
OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER"
"""
.. envvar:: OTEL_TRACES_EXPORTER
"""
OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER"
"""
.. envvar:: OTEL_PYTHON_TRACER_PROVIDER
"""
OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER"
"""
.. envvar:: OTEL_PYTHON_METER_PROVIDER
"""
_OTEL_PYTHON_LOGGER_PROVIDER = "OTEL_PYTHON_LOGGER_PROVIDER"
"""
.. envvar:: OTEL_PYTHON_LOGGER_PROVIDER
"""
@@ -0,0 +1,121 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""
The OpenTelemetry metrics API describes the classes used to generate
metrics.
The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in
turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are
used to record measurements.
This module provides abstract (i.e. unimplemented) classes required for
metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications
to use the API package alone without a supporting implementation.
To get a meter, you need to provide the package name from which you are
calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter`
with the calling instrumentation name and the version of your package.
The following code shows how to obtain a meter using the global :class:`.MeterProvider`::
from opentelemetry.metrics import get_meter
meter = get_meter("example-meter")
counter = meter.create_counter("example-counter")
.. versionadded:: 1.10.0
.. versionchanged:: 1.12.0rc
"""
from opentelemetry.metrics._internal import (
Meter,
MeterProvider,
NoOpMeter,
NoOpMeterProvider,
get_meter,
get_meter_provider,
set_meter_provider,
)
from opentelemetry.metrics._internal.instrument import (
Asynchronous,
CallbackOptions,
CallbackT,
Counter,
Histogram,
Instrument,
NoOpCounter,
NoOpHistogram,
NoOpObservableCounter,
NoOpObservableGauge,
NoOpObservableUpDownCounter,
NoOpUpDownCounter,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
Synchronous,
UpDownCounter,
)
from opentelemetry.metrics._internal.instrument import Gauge as _Gauge
from opentelemetry.metrics._internal.instrument import NoOpGauge as _NoOpGauge
from opentelemetry.metrics._internal.observation import Observation
for obj in [
Counter,
Synchronous,
Asynchronous,
CallbackOptions,
_Gauge,
_NoOpGauge,
get_meter_provider,
get_meter,
Histogram,
Meter,
MeterProvider,
Instrument,
NoOpCounter,
NoOpHistogram,
NoOpMeter,
NoOpMeterProvider,
NoOpObservableCounter,
NoOpObservableGauge,
NoOpObservableUpDownCounter,
NoOpUpDownCounter,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
Observation,
set_meter_provider,
UpDownCounter,
]:
obj.__module__ = __name__
__all__ = [
"Asynchronous",
"CallbackOptions",
"CallbackT",
"Counter",
"Histogram",
"Instrument",
"Meter",
"MeterProvider",
"NoOpCounter",
"NoOpHistogram",
"NoOpMeter",
"NoOpMeterProvider",
"NoOpObservableCounter",
"NoOpObservableGauge",
"NoOpObservableUpDownCounter",
"NoOpUpDownCounter",
"ObservableCounter",
"ObservableGauge",
"ObservableUpDownCounter",
"Observation",
"Synchronous",
"UpDownCounter",
"_Gauge",
"_NoOpGauge",
"get_meter",
"get_meter_provider",
"set_meter_provider",
]
@@ -0,0 +1,892 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
# pylint: disable=too-many-ancestors
"""
The OpenTelemetry metrics API describes the classes used to generate
metrics.
The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in
turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are
used to record measurements.
This module provides abstract (i.e. unimplemented) classes required for
metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications
to use the API package alone without a supporting implementation.
To get a meter, you need to provide the package name from which you are
calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter`
with the calling instrumentation name and the version of your package.
The following code shows how to obtain a meter using the global :class:`.MeterProvider`::
from opentelemetry.metrics import get_meter
meter = get_meter("example-meter")
counter = meter.create_counter("example-counter")
.. versionadded:: 1.10.0
"""
import warnings
from abc import ABC, abstractmethod
from collections.abc import Sequence
from dataclasses import dataclass
from logging import getLogger
from os import environ
from threading import Lock
from typing import cast
from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER
from opentelemetry.metrics._internal.instrument import (
CallbackT,
Counter,
Gauge,
Histogram,
NoOpCounter,
NoOpGauge,
NoOpHistogram,
NoOpObservableCounter,
NoOpObservableGauge,
NoOpObservableUpDownCounter,
NoOpUpDownCounter,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
UpDownCounter,
_MetricsHistogramAdvisory,
_ProxyCounter,
_ProxyGauge,
_ProxyHistogram,
_ProxyObservableCounter,
_ProxyObservableGauge,
_ProxyObservableUpDownCounter,
_ProxyUpDownCounter,
)
from opentelemetry.util._once import Once
from opentelemetry.util._providers import _load_provider
from opentelemetry.util.types import (
Attributes,
)
_logger = getLogger(__name__)
# pylint: disable=invalid-name
_ProxyInstrumentT = (
_ProxyCounter
| _ProxyHistogram
| _ProxyGauge
| _ProxyObservableCounter
| _ProxyObservableGauge
| _ProxyObservableUpDownCounter
| _ProxyUpDownCounter
)
class MeterProvider(ABC):
"""
MeterProvider is the entry point of the API. It provides access to `Meter` instances.
"""
@abstractmethod
def get_meter(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> "Meter":
"""Returns a `Meter` for use by the given instrumentation library.
For any two calls it is undefined whether the same or different
`Meter` instances are returned, even for different library names.
This function may return different `Meter` types (e.g. a no-op meter
vs. a functional meter).
Args:
name: The name of the instrumenting module.
``__name__`` should be avoided as this can result in
different meter names if the meters are in different files.
It is better to use a fixed string that can be imported where
needed and used consistently as the name of the meter.
This should *not* be the name of the module that is
instrumented but the name of the module doing the instrumentation.
E.g., instead of ``"requests"``, use
``"opentelemetry.instrumentation.requests"``.
version: Optional. The version string of the
instrumenting library. Usually this should be the same as
``importlib.metadata.version(instrumenting_library_name)``.
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
attributes: Optional. Attributes that are associated with the emitted telemetry.
"""
class NoOpMeterProvider(MeterProvider):
"""The default MeterProvider used when no MeterProvider implementation is available."""
def get_meter(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> "Meter":
"""Returns a NoOpMeter."""
return NoOpMeter(name, version=version, schema_url=schema_url)
class _ProxyMeterProvider(MeterProvider):
def __init__(self) -> None:
self._lock = Lock()
self._meters: list[_ProxyMeter] = []
self._real_meter_provider: MeterProvider | None = None
def get_meter(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> "Meter":
with self._lock:
if self._real_meter_provider is not None:
return self._real_meter_provider.get_meter(name, version, schema_url)
meter = _ProxyMeter(name, version=version, schema_url=schema_url)
self._meters.append(meter)
return meter
def on_set_meter_provider(self, meter_provider: MeterProvider) -> None:
with self._lock:
self._real_meter_provider = meter_provider
for meter in self._meters:
meter.on_set_meter_provider(meter_provider)
@dataclass
class _InstrumentRegistrationStatus:
instrument_id: str
already_registered: bool
conflict: bool
current_advisory: _MetricsHistogramAdvisory | None
class Meter(ABC):
"""Handles instrument creation.
This class provides methods for creating instruments which are then
used to produce measurements.
"""
def __init__(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
) -> None:
super().__init__()
self._name = name
self._version = version
self._schema_url = schema_url
self._instrument_ids: dict[str, _MetricsHistogramAdvisory | None] = {}
self._instrument_ids_lock = Lock()
@property
def name(self) -> str:
"""
The name of the instrumenting module.
"""
return self._name
@property
def version(self) -> str | None:
"""
The version string of the instrumenting library.
"""
return self._version
@property
def schema_url(self) -> str | None:
"""
Specifies the Schema URL of the emitted telemetry
"""
return self._schema_url
def _register_instrument(
self,
name: str,
type_: type,
unit: str,
description: str,
advisory: _MetricsHistogramAdvisory | None = None,
) -> _InstrumentRegistrationStatus:
"""
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.
"""
instrument_id = ",".join([name.strip().lower(), type_.__name__, unit, description])
already_registered = False
conflict = False
current_advisory = None
with self._instrument_ids_lock:
# we are not using get because None is a valid value
already_registered = instrument_id in self._instrument_ids
if already_registered:
current_advisory = self._instrument_ids[instrument_id]
conflict = current_advisory != advisory
else:
self._instrument_ids[instrument_id] = advisory
return _InstrumentRegistrationStatus(
instrument_id=instrument_id,
already_registered=already_registered,
conflict=conflict,
current_advisory=current_advisory,
)
@staticmethod
def _log_instrument_registration_conflict(
name: str,
instrumentation_type: str,
unit: str,
description: str,
status: _InstrumentRegistrationStatus,
) -> None:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already with a "
"different advisory value %s and will be used instead.",
name,
instrumentation_type,
unit,
description,
status.current_advisory,
)
@abstractmethod
def create_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> Counter:
"""Creates a `Counter` instrument
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.
"""
@abstractmethod
def create_up_down_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> UpDownCounter:
"""Creates an `UpDownCounter` instrument
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.
"""
@abstractmethod
def create_observable_counter(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
"""Creates an `ObservableCounter` instrument
An observable counter observes a monotonically increasing count by calling provided
callbacks which accept a :class:`~opentelemetry.metrics.CallbackOptions` and return
multiple :class:`~opentelemetry.metrics.Observation`.
For example, an observable counter could be used to report system CPU
time periodically. Here is a basic implementation::
def cpu_time_callback(
options: CallbackOptions,
) -> Iterable[Observation]:
observations = []
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
for line in procstat:
if not line.startswith("cpu"):
break
cpu, *states = line.split()
observations.append(
Observation(
int(states[0]) // 100,
{"cpu": cpu, "state": "user"},
)
)
observations.append(
Observation(
int(states[1]) // 100,
{"cpu": cpu, "state": "nice"},
)
)
observations.append(
Observation(
int(states[2]) // 100,
{"cpu": cpu, "state": "system"},
)
)
# ... other states
return observations
meter.create_observable_counter(
"system.cpu.time",
callbacks=[cpu_time_callback],
unit="s",
description="CPU time",
)
To reduce memory usage, you can use generator callbacks instead of
building the full list::
def cpu_time_callback(
options: CallbackOptions,
) -> Iterable[Observation]:
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
for line in procstat:
if not line.startswith("cpu"):
break
cpu, *states = line.split()
yield Observation(
int(states[0]) // 100,
{"cpu": cpu, "state": "user"},
)
yield Observation(
int(states[1]) // 100,
{"cpu": cpu, "state": "nice"},
)
# ... other states
Alternatively, you can pass a sequence of generators directly instead of a sequence of
callbacks, which each should return iterables of :class:`~opentelemetry.metrics.Observation`::
def cpu_time_callback(
states_to_include: set[str],
) -> Iterable[Iterable[Observation]]:
# accept options sent in from OpenTelemetry
options = yield
while True:
observations = []
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
for line in procstat:
if not line.startswith("cpu"):
break
cpu, *states = line.split()
if "user" in states_to_include:
observations.append(
Observation(
int(states[0]) // 100,
{"cpu": cpu, "state": "user"},
)
)
if "nice" in states_to_include:
observations.append(
Observation(
int(states[1]) // 100,
{"cpu": cpu, "state": "nice"},
)
)
# ... other states
# yield the observations and receive the options for next iteration
options = yield observations
meter.create_observable_counter(
"system.cpu.time",
callbacks=[cpu_time_callback({"user", "system"})],
unit="s",
description="CPU time",
)
The :class:`~opentelemetry.metrics.CallbackOptions` contain a timeout which the
callback should respect. For example if the callback does asynchronous work, like
making HTTP requests, it should respect the timeout::
def scrape_http_callback(
options: CallbackOptions,
) -> Iterable[Observation]:
r = requests.get(
"http://scrapethis.com",
timeout=options.timeout_millis / 10**3,
)
for value in r.json():
yield Observation(value)
Args:
name: The name of the instrument to be created
callbacks: A sequence of callbacks that return an iterable of
:class:`~opentelemetry.metrics.Observation`. Alternatively, can be a sequence of generators that each
yields iterables 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.
"""
@abstractmethod
def create_histogram(
self,
name: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
) -> Histogram:
"""Creates a :class:`~opentelemetry.metrics.Histogram` instrument
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.
"""
def create_gauge( # type: ignore # pylint: disable=no-self-use
self,
name: str,
unit: str = "",
description: str = "",
) -> Gauge: # pyright: ignore[reportReturnType]
"""Creates a ``Gauge`` instrument
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.
"""
warnings.warn("create_gauge() is not implemented and will be a no-op")
@abstractmethod
def create_observable_gauge(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableGauge:
"""Creates an `ObservableGauge` instrument
Args:
name: The name of the instrument to be created
callbacks: A sequence of callbacks that return an iterable of
:class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables
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.
"""
@abstractmethod
def create_observable_up_down_counter(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableUpDownCounter:
"""Creates an `ObservableUpDownCounter` instrument
Args:
name: The name of the instrument to be created
callbacks: A sequence of callbacks that return an iterable of
:class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables
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.
"""
class _ProxyMeter(Meter):
def __init__(
self,
name: str,
version: str | None = None,
schema_url: str | None = None,
) -> None:
super().__init__(name, version=version, schema_url=schema_url)
self._lock = Lock()
self._instruments: list[_ProxyInstrumentT] = []
self._real_meter: Meter | None = None
def on_set_meter_provider(self, meter_provider: MeterProvider) -> None:
"""Called when a real meter provider is set on the creating _ProxyMeterProvider
Creates a real backing meter for this instance and notifies all created
instruments so they can create real backing instruments.
"""
real_meter = meter_provider.get_meter(self._name, self._version, self._schema_url)
with self._lock:
self._real_meter = real_meter
# notify all proxy instruments of the new meter so they can create
# real instruments to back themselves
for instrument in self._instruments:
instrument.on_meter_set(real_meter)
def create_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> Counter:
with self._lock:
if self._real_meter:
return self._real_meter.create_counter(name, unit, description)
proxy = _ProxyCounter(name, unit, description)
self._instruments.append(proxy)
return proxy
def create_up_down_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> UpDownCounter:
with self._lock:
if self._real_meter:
return self._real_meter.create_up_down_counter(name, unit, description)
proxy = _ProxyUpDownCounter(name, unit, description)
self._instruments.append(proxy)
return proxy
def create_observable_counter(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
with self._lock:
if self._real_meter:
return self._real_meter.create_observable_counter(name, callbacks, unit, description)
proxy = _ProxyObservableCounter(name, callbacks, unit=unit, description=description)
self._instruments.append(proxy)
return proxy
def create_histogram(
self,
name: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
) -> Histogram:
with self._lock:
if self._real_meter:
return self._real_meter.create_histogram(
name,
unit,
description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)
proxy = _ProxyHistogram(name, unit, description, explicit_bucket_boundaries_advisory)
self._instruments.append(proxy)
return proxy
def create_gauge(
self,
name: str,
unit: str = "",
description: str = "",
) -> Gauge:
with self._lock:
if self._real_meter:
return self._real_meter.create_gauge(name, unit, description)
proxy = _ProxyGauge(name, unit, description)
self._instruments.append(proxy)
return proxy
def create_observable_gauge(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableGauge:
with self._lock:
if self._real_meter:
return self._real_meter.create_observable_gauge(name, callbacks, unit, description)
proxy = _ProxyObservableGauge(name, callbacks, unit=unit, description=description)
self._instruments.append(proxy)
return proxy
def create_observable_up_down_counter(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableUpDownCounter:
with self._lock:
if self._real_meter:
return self._real_meter.create_observable_up_down_counter(
name,
callbacks,
unit,
description,
)
proxy = _ProxyObservableUpDownCounter(name, callbacks, unit=unit, description=description)
self._instruments.append(proxy)
return proxy
class NoOpMeter(Meter):
"""The default Meter used when no Meter implementation is available.
All operations are no-op.
"""
def create_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> Counter:
"""Returns a no-op Counter."""
status = self._register_instrument(name, NoOpCounter, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Counter.__name__,
unit,
description,
status,
)
return NoOpCounter(name, unit=unit, description=description)
def create_gauge(
self,
name: str,
unit: str = "",
description: str = "",
) -> Gauge:
"""Returns a no-op Gauge."""
status = self._register_instrument(name, NoOpGauge, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Gauge.__name__,
unit,
description,
status,
)
return NoOpGauge(name, unit=unit, description=description)
def create_up_down_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> UpDownCounter:
"""Returns a no-op UpDownCounter."""
status = self._register_instrument(name, NoOpUpDownCounter, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
UpDownCounter.__name__,
unit,
description,
status,
)
return NoOpUpDownCounter(name, unit=unit, description=description)
def create_observable_counter(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
"""Returns a no-op ObservableCounter."""
status = self._register_instrument(name, NoOpObservableCounter, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableCounter.__name__,
unit,
description,
status,
)
return NoOpObservableCounter(
name,
callbacks,
unit=unit,
description=description,
)
def create_histogram(
self,
name: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
) -> Histogram:
"""Returns a no-op Histogram."""
status = self._register_instrument(
name,
NoOpHistogram,
unit,
description,
_MetricsHistogramAdvisory(explicit_bucket_boundaries=explicit_bucket_boundaries_advisory),
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Histogram.__name__,
unit,
description,
status,
)
return NoOpHistogram(
name,
unit=unit,
description=description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)
def create_observable_gauge(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableGauge:
"""Returns a no-op ObservableGauge."""
status = self._register_instrument(name, NoOpObservableGauge, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableGauge.__name__,
unit,
description,
status,
)
return NoOpObservableGauge(
name,
callbacks,
unit=unit,
description=description,
)
def create_observable_up_down_counter(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> ObservableUpDownCounter:
"""Returns a no-op ObservableUpDownCounter."""
status = self._register_instrument(name, NoOpObservableUpDownCounter, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableUpDownCounter.__name__,
unit,
description,
status,
)
return NoOpObservableUpDownCounter(
name,
callbacks,
unit=unit,
description=description,
)
_METER_PROVIDER_SET_ONCE = Once()
_METER_PROVIDER: MeterProvider | None = None
_PROXY_METER_PROVIDER = _ProxyMeterProvider()
def get_meter(
name: str,
version: str = "",
meter_provider: MeterProvider | None = None,
schema_url: str | None = None,
attributes: Attributes = None,
) -> "Meter":
"""Returns a `Meter` for use by the given instrumentation library.
This function is a convenience wrapper for
`opentelemetry.metrics.MeterProvider.get_meter`.
If meter_provider is omitted the current configured one is used.
"""
if meter_provider is None:
meter_provider = get_meter_provider()
return meter_provider.get_meter(name, version, schema_url, attributes)
def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None:
def set_mp() -> None:
global _METER_PROVIDER # pylint: disable=global-statement
_METER_PROVIDER = meter_provider
# gives all proxies real instruments off the newly set meter provider
_PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider)
did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp)
if log and not did_set:
_logger.warning("Overriding of current MeterProvider is not allowed")
def set_meter_provider(meter_provider: MeterProvider) -> None:
"""Sets the current global :class:`~.MeterProvider` object.
This can only be done once, a warning will be logged if any further attempt
is made.
"""
_set_meter_provider(meter_provider, log=True)
def get_meter_provider() -> MeterProvider:
"""Gets the current global :class:`~.MeterProvider` object."""
if _METER_PROVIDER is None:
if OTEL_PYTHON_METER_PROVIDER not in environ:
return _PROXY_METER_PROVIDER
meter_provider: MeterProvider = _load_provider( # type: ignore
OTEL_PYTHON_METER_PROVIDER, "meter_provider"
)
_set_meter_provider(meter_provider, log=False)
# _METER_PROVIDER will have been set by one thread
return cast("MeterProvider", _METER_PROVIDER)
@@ -0,0 +1,540 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
# pylint: disable=too-many-ancestors
from abc import ABC, abstractmethod
from collections.abc import Callable, Generator, Iterable, Sequence
from dataclasses import dataclass
from logging import getLogger
from re import compile as re_compile
from typing import (
Generic,
TypeVar,
)
# pylint: disable=unused-import; needed for typing and sphinx
from opentelemetry import metrics
from opentelemetry.context import Context
from opentelemetry.metrics._internal.observation import Observation
from opentelemetry.util.types import (
Attributes,
)
_logger = getLogger(__name__)
_name_regex = re_compile(r"[a-zA-Z][-_./a-zA-Z0-9]{0,254}")
_unit_regex = re_compile(r"[\x00-\x7F]{0,63}")
@dataclass(frozen=True)
class _MetricsHistogramAdvisory:
explicit_bucket_boundaries: Sequence[float] | None = None
@dataclass(frozen=True)
class CallbackOptions:
"""Options for the callback
Args:
timeout_millis: Timeout for the callback's execution. If the callback does asynchronous
work (e.g. HTTP requests), it should respect this timeout.
"""
timeout_millis: float = 10_000
InstrumentT = TypeVar("InstrumentT", bound="Instrument")
# pylint: disable=invalid-name
CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None]
class Instrument(ABC):
"""Abstract class that serves as base for all instruments."""
@abstractmethod
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
pass
@staticmethod
def _check_name_unit_description(name: str, unit: str, description: str) -> dict[str, str | None]:
"""
Checks the following instrument name, unit and description for
compliance with the spec.
Returns a dict with keys "name", "unit" and "description", the
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.
"""
result: dict[str, str | None] = {}
if _name_regex.fullmatch(name) is not None:
result["name"] = name
else:
result["name"] = None
if unit is None:
unit = ""
if _unit_regex.fullmatch(unit) is not None:
result["unit"] = unit
else:
result["unit"] = None
if description is None:
result["description"] = ""
else:
result["description"] = description
return result
class _ProxyInstrument(ABC, Generic[InstrumentT]):
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
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"""
# 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
# created.
self._real_instrument = self._create_real_instrument(meter)
@abstractmethod
def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT:
"""Create an instance of the real instrument. Implement this."""
class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]):
def __init__(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit, description)
self._callbacks = callbacks
class Synchronous(Instrument):
"""Base class for all synchronous instruments"""
class Asynchronous(Instrument):
"""Base class for all asynchronous instruments"""
@abstractmethod
def __init__(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
class Counter(Synchronous):
"""A Counter is a synchronous `Instrument` which supports non-negative increments."""
@abstractmethod
def add(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
"""Records an increment to the counter.
Args:
amount: The amount to increment the counter by. Must be non-negative.
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.
"""
class NoOpCounter(Counter):
"""No-op implementation of `Counter`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def add(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
return super().add(amount, attributes=attributes, context=context)
class _ProxyCounter(_ProxyInstrument[Counter], Counter):
def add(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
if self._real_instrument:
self._real_instrument.add(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> Counter:
return meter.create_counter(
self._name,
self._unit,
self._description,
)
class UpDownCounter(Synchronous):
"""An UpDownCounter is a synchronous `Instrument` which supports increments and decrements."""
@abstractmethod
def add(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
"""Records an increment or decrement to the counter.
Unlike `Counter`, the ``amount`` may be negative, allowing the
instrument to track values that go up and down (e.g. number of
active requests, queue depth).
Args:
amount: The amount to add to the counter. May be positive or negative.
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.
"""
class NoOpUpDownCounter(UpDownCounter):
"""No-op implementation of `UpDownCounter`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def add(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
return super().add(amount, attributes=attributes, context=context)
class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter):
def add(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
if self._real_instrument:
self._real_instrument.add(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter:
return meter.create_up_down_counter(
self._name,
self._unit,
self._description,
)
class ObservableCounter(Asynchronous):
"""An ObservableCounter is an asynchronous `Instrument` which reports monotonically
increasing value(s) when the instrument is being observed.
"""
class NoOpObservableCounter(ObservableCounter):
"""No-op implementation of `ObservableCounter`."""
def __init__(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(
name,
callbacks,
unit=unit,
description=description,
)
class _ProxyObservableCounter(_ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter):
def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableCounter:
return meter.create_observable_counter(
self._name,
self._callbacks,
self._unit,
self._description,
)
class ObservableUpDownCounter(Asynchronous):
"""An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g.
the process heap size - it makes sense to report the heap size from multiple processes and sum them
up, so we get the total heap usage) when the instrument is being observed.
"""
class NoOpObservableUpDownCounter(ObservableUpDownCounter):
"""No-op implementation of `ObservableUpDownCounter`."""
def __init__(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(
name,
callbacks,
unit=unit,
description=description,
)
class _ProxyObservableUpDownCounter(
_ProxyAsynchronousInstrument[ObservableUpDownCounter],
ObservableUpDownCounter,
):
def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableUpDownCounter:
return meter.create_observable_up_down_counter(
self._name,
self._callbacks,
self._unit,
self._description,
)
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.
"""
@abstractmethod
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
) -> None:
pass
@abstractmethod
def record(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
"""Records a measurement.
Used to report measurements that are likely to be statistically
meaningful, such as request durations, payload sizes, or any value
for which a distribution (e.g. percentiles) is useful.
Args:
amount: The measurement to record. Should be non-negative in most
cases; negative values are only meaningful when the histogram
is used to track signed deltas.
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.
"""
class NoOpHistogram(Histogram):
"""No-op implementation of `Histogram`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
) -> None:
super().__init__(
name,
unit=unit,
description=description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)
def record(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
return super().record(amount, attributes=attributes, context=context)
class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram):
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
) -> None:
super().__init__(name, unit=unit, description=description)
self._explicit_bucket_boundaries_advisory = explicit_bucket_boundaries_advisory
def record(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
if self._real_instrument:
self._real_instrument.record(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram:
return meter.create_histogram(
self._name,
self._unit,
self._description,
explicit_bucket_boundaries_advisory=self._explicit_bucket_boundaries_advisory,
)
class ObservableGauge(Asynchronous):
"""Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g.
the room temperature - it makes no sense to report the temperature value from multiple rooms
and sum them up) when the instrument is being observed.
"""
class NoOpObservableGauge(ObservableGauge):
"""No-op implementation of `ObservableGauge`."""
def __init__(
self,
name: str,
callbacks: Sequence[CallbackT] | None = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(
name,
callbacks,
unit=unit,
description=description,
)
class _ProxyObservableGauge(
_ProxyAsynchronousInstrument[ObservableGauge],
ObservableGauge,
):
def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableGauge:
return meter.create_observable_gauge(
self._name,
self._callbacks,
self._unit,
self._description,
)
class Gauge(Synchronous):
"""A Gauge is a synchronous `Instrument` which can be used to record non-additive values as they occur."""
@abstractmethod
def set(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
"""Records the current value of the gauge.
The gauge reports the last recorded value when observed. It is
intended for non-additive measurements where only the current
value matters (e.g. CPU utilisation percentage, room temperature).
Args:
amount: The current 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.
"""
class NoOpGauge(Gauge):
"""No-op implementation of ``Gauge``."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def set(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
return super().set(amount, attributes=attributes, context=context)
class _ProxyGauge(
_ProxyInstrument[Gauge],
Gauge,
):
def set(
self,
amount: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
if self._real_instrument:
self._real_instrument.set(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> Gauge:
return meter.create_gauge(
self._name,
self._unit,
self._description,
)
@@ -0,0 +1,51 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from opentelemetry.context import Context
from opentelemetry.util.types import Attributes
class Observation:
"""A measurement observed in an asynchronous instrument
Return/yield instances of this class from asynchronous instrument callbacks.
Args:
value: The float or int measured value
attributes: The measurement's attributes
context: The measurement's context
"""
def __init__(
self,
value: float,
attributes: Attributes = None,
context: Context | None = None,
) -> None:
self._value = value
self._attributes = attributes
self._context = context
@property
def value(self) -> float | int:
return self._value
@property
def attributes(self) -> Attributes:
return self._attributes
@property
def context(self) -> Context | None:
return self._context
def __eq__(self, other: object) -> bool:
return (
isinstance(other, Observation)
and self.value == other.value
and self.attributes == other.attributes
and self.context == other.context
)
def __repr__(self) -> str:
return f"Observation(value={self.value}, attributes={self.attributes}, context={self.context})"
@@ -0,0 +1,171 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""
API for propagation of context.
The propagators for the
``opentelemetry.propagators.composite.CompositePropagator`` can be defined
via configuration in the ``OTEL_PROPAGATORS`` environment variable. This
variable should be set to a comma-separated string of names of values for the
``opentelemetry_propagator`` entry point. For example, setting
``OTEL_PROPAGATORS`` to ``tracecontext,baggage`` (which is the default value)
would instantiate
``opentelemetry.propagators.composite.CompositePropagator`` with 2
propagators, one of type
``opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator``
and other of type ``opentelemetry.baggage.propagation.W3CBaggagePropagator``.
Notice that these propagator classes are defined as
``opentelemetry_propagator`` entry points in the ``pyproject.toml`` file of
``opentelemetry``.
Example::
import flask
import requests
from opentelemetry import propagate
PROPAGATOR = propagate.get_global_textmap()
def get_header_from_flask_request(request, key):
return request.headers.get_all(key)
def set_header_into_requests_request(request: requests.Request, key: str, value: str):
request.headers[key] = value
def example_route():
context = PROPAGATOR.extract(get_header_from_flask_request, flask.request)
request_to_downstream = requests.Request("GET", "http://httpbin.org/get")
PROPAGATOR.inject(
set_header_into_requests_request,
request_to_downstream,
context=context,
)
session = requests.Session()
session.send(request_to_downstream.prepare())
.. _Propagation API Specification:
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md
"""
from logging import getLogger
from os import environ
from opentelemetry.context.context import Context
from opentelemetry.environment_variables import OTEL_PROPAGATORS
from opentelemetry.propagators import composite, textmap
logger = getLogger(__name__)
def extract(
carrier: textmap.CarrierT,
context: Context | None = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Uses the configured propagator to extract a Context from the carrier.
Args:
getter: an object which contains a get function that can retrieve zero
or more values from the carrier and a keys function that can get all the keys
from carrier.
carrier: and object which contains values that are
used to construct a Context. This object
must be paired with an appropriate getter
which understands how to extract a value from it.
context: an optional Context to use. Defaults to root
context if not set.
"""
return get_global_textmap().extract(carrier, context, getter=getter)
def inject(
carrier: textmap.CarrierT,
context: Context | None = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Uses the configured propagator to inject a Context into the carrier.
Args:
carrier: the medium used by Propagators to read
values from and write values to.
Should be paired with setter, which
should know how to set header values on the carrier.
context: An optional Context to use. Defaults to current
context if not set.
setter: An optional `Setter` object that can set values
on the carrier.
"""
get_global_textmap().inject(carrier, context=context, setter=setter)
def _load_propagators() -> textmap.TextMapPropagator:
configured = environ.get(OTEL_PROPAGATORS)
if not configured:
# pylint: disable=import-outside-toplevel,no-name-in-module
from opentelemetry.baggage.propagation import ( # noqa: PLC0415
W3CBaggagePropagator,
)
# pylint: disable=import-outside-toplevel,no-name-in-module
from opentelemetry.trace.propagation.tracecontext import ( # noqa: PLC0415
TraceContextTextMapPropagator,
)
return composite.CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()])
# pylint: disable=import-outside-toplevel,no-name-in-module
from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415
entry_points,
)
_propagators: list[textmap.TextMapPropagator] = []
for _propagator in configured.split(","):
_propagator = _propagator.strip()
if _propagator.lower() == "none":
logger.debug("OTEL_PROPAGATORS environment variable contains none, removing all propagators")
return composite.CompositePropagator([])
try:
_propagators.append(
next( # type: ignore
iter( # type: ignore
entry_points( # type: ignore[misc]
group="opentelemetry_propagator",
name=_propagator,
)
)
).load()()
)
except StopIteration:
raise ValueError(f"Propagator {_propagator} not found. It is either misspelled or not installed.")
except Exception: # pylint: disable=broad-exception-caught
logger.exception("Failed to load propagator: %s", _propagator)
raise
return composite.CompositePropagator(_propagators)
_HTTP_TEXT_FORMAT: textmap.TextMapPropagator = _load_propagators()
# Deprecated: propagators, environ_propagators and propagator names were never intendended to be part of the public API.
propagators: list[textmap.TextMapPropagator] = []
environ_propagators = environ.get(OTEL_PROPAGATORS, "tracecontext,baggage")
for propagator in environ_propagators.split(","): # type: ignore[assignment]
propagator = propagator.strip()
def get_global_textmap() -> textmap.TextMapPropagator:
return _HTTP_TEXT_FORMAT
def set_global_textmap(
http_text_format: textmap.TextMapPropagator,
) -> None:
global _HTTP_TEXT_FORMAT # pylint:disable=global-statement
_HTTP_TEXT_FORMAT = http_text_format
@@ -0,0 +1,94 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""Environment variable carriers for text map propagators.
Use :class:`EnvironmentGetter` with the environment mapping to extract from,
usually ``os.environ`` during application or child-process initialization.
Use :class:`EnvironmentSetter` with a mutable environment copy when preparing
the environment for a child process.
"""
import re
from collections.abc import Mapping, MutableMapping
from opentelemetry.propagators.textmap import Getter, Setter
def _normalize_key(key: str) -> str:
if not key:
return "_"
result = re.sub(r"[^A-Za-z0-9_]", "_", key).upper()
if result and result[0].isdigit():
result = "_" + result
return result
def _is_normalized_key(key: str) -> bool:
if not key:
return False
if "0" <= key[0] <= "9":
return False
return all("A" <= char <= "Z" or "0" <= char <= "9" or char == "_" for char in key)
class EnvironmentGetter(Getter[Mapping[str, str]]):
"""Getter implementation for extracting context and baggage from environment variables.
EnvironmentGetter reads from the mapping provided as the carrier, normalizes
requested keys, and provides simple data access without validation.
Example usage:
getter = EnvironmentGetter()
traceparent = getter.get(os.environ, "traceparent")
"""
def get(self, carrier: Mapping[str, str], key: str) -> list[str] | None:
"""Get a value from the environment carrier for the given key.
Args:
carrier: Mapping to read environment variables from
key: The key to look up (will be normalized)
Returns:
A list with a single string value if the key exists, None otherwise.
"""
val = carrier.get(_normalize_key(key))
if val is None:
return None
return [val]
def keys(self, carrier: Mapping[str, str]) -> list[str]:
"""Get all keys from the environment carrier.
Args:
carrier: Mapping to read environment variable keys from
Returns:
List of all already-normalized environment variable keys.
"""
return [key for key in carrier.keys() if _is_normalized_key(key)]
class EnvironmentSetter(Setter[MutableMapping[str, str]]):
"""Setter implementation for building environment variable dictionaries.
EnvironmentSetter builds a dictionary of environment variables that
can be passed to utilities like subprocess.run()
Example usage:
setter = EnvironmentSetter()
env_vars = {}
setter.set(env_vars, "traceparent", "00-trace-id-span-id-01")
subprocess.run(myCommand, env=env_vars)
"""
def set(self, carrier: MutableMapping[str, str], key: str, value: str) -> None:
"""Set a value in the carrier dictionary for the given key.
Args:
carrier: Dictionary to store environment variables
key: The key to set (normalized)
value: The value to set
"""
carrier[_normalize_key(key)] = value
@@ -0,0 +1,78 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
import collections.abc
import logging
from typing_extensions import deprecated
from opentelemetry.context.context import Context
from opentelemetry.propagators import textmap
logger = logging.getLogger(__name__)
class CompositePropagator(textmap.TextMapPropagator):
"""CompositePropagator provides a mechanism for combining multiple
propagators into a single one.
Args:
propagators: the list of propagators to use
"""
def __init__(self, propagators: collections.abc.Sequence[textmap.TextMapPropagator]) -> None:
self._propagators = propagators
def extract(
self,
carrier: textmap.CarrierT,
context: Context | None = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Run each of the configured propagators with the given context and carrier.
Propagators are run in the order they are configured, if multiple
propagators write the same context key, the propagator later in the list
will override previous propagators.
See `opentelemetry.propagators.textmap.TextMapPropagator.extract`
"""
for propagator in self._propagators:
context = propagator.extract(carrier, context, getter=getter)
return context # type: ignore
def inject(
self,
carrier: textmap.CarrierT,
context: Context | None = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Run each of the configured propagators with the given context and carrier.
Propagators are run in the order they are configured, if multiple
propagators write the same carrier key, the propagator later in the list
will override previous propagators.
See `opentelemetry.propagators.textmap.TextMapPropagator.inject`
"""
for propagator in self._propagators:
propagator.inject(carrier, context, setter=setter)
@property
def fields(self) -> set[str]:
"""Returns a set with the fields set in `inject`.
See
`opentelemetry.propagators.textmap.TextMapPropagator.fields`
"""
composite_fields = set()
for propagator in self._propagators:
for field in propagator.fields:
composite_fields.add(field)
return composite_fields
@deprecated("You should use CompositePropagator. Deprecated since version 1.2.0.")
class CompositeHTTPPropagator(CompositePropagator):
"""CompositeHTTPPropagator provides a mechanism for combining multiple
propagators into a single one.
"""
@@ -0,0 +1,181 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
import abc
import typing
from collections.abc import Iterable, Mapping, MutableMapping
from opentelemetry.context.context import Context
CarrierT = typing.TypeVar("CarrierT")
# pylint: disable=invalid-name
CarrierValT = list[str] | str
class Getter(abc.ABC, typing.Generic[CarrierT]):
"""This class implements a Getter that enables extracting propagated
fields from a carrier.
"""
@abc.abstractmethod
def get(self, carrier: CarrierT, key: str) -> list[str] | None:
"""Function that can retrieve zero
or more values from the carrier. In the case that
the value does not exist, returns None.
Args:
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.
"""
@abc.abstractmethod
def keys(self, carrier: CarrierT) -> list[str]:
"""Function that can retrieve all the keys in a carrier object.
Args:
carrier: An object which contains values that are
used to construct a Context.
Returns:
list of keys from the carrier.
"""
class Setter(abc.ABC, typing.Generic[CarrierT]):
"""This class implements a Setter that enables injecting propagated
fields into a carrier.
"""
@abc.abstractmethod
def set(self, carrier: CarrierT, key: str, value: str) -> None:
"""Function that can set a value into a carrier""
Args:
carrier: An object which contains values that are used to
construct a Context.
key: key of a field in carrier.
value: value for a field in carrier.
"""
class DefaultGetter(Getter[Mapping[str, CarrierValT]]):
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.
"""
val = carrier.get(key, None)
if val is None:
return None
if isinstance(val, Iterable) and not isinstance(val, str):
return list(val)
return [val]
def keys(self, carrier: Mapping[str, CarrierValT]) -> list[str]:
"""Keys implementation that returns all keys from a dictionary."""
return list(carrier.keys())
default_getter: Getter[CarrierT] = DefaultGetter() # type: ignore
class DefaultSetter(Setter[MutableMapping[str, CarrierValT]]):
def set(
self,
carrier: MutableMapping[str, CarrierValT],
key: str,
value: CarrierValT,
) -> None:
"""Setter implementation to set a value into a dictionary.
Args:
carrier: dictionary in which to set value
key: the key used to set the value
value: the value to set
"""
carrier[key] = value
default_setter: Setter[CarrierT] = DefaultSetter() # type: ignore
class TextMapPropagator(abc.ABC):
"""This class provides an interface that enables extracting and injecting
context into headers of HTTP requests. HTTP frameworks and clients
can integrate with TextMapPropagator by providing the object containing the
headers, and a getter and setter function for the extraction and
injection of values, respectively.
"""
@abc.abstractmethod
def extract(
self,
carrier: CarrierT,
context: Context | None = None,
getter: Getter[CarrierT] = default_getter,
) -> Context:
"""Create a Context from values in the carrier.
The extract function should retrieve values from the carrier
object using getter, and use values to populate a
Context value and return it.
Args:
getter: a function that can retrieve zero
or more values from the carrier. In the case that
the value does not exist, return an empty list.
carrier: and object which contains values that are
used to construct a Context. This object
must be paired with an appropriate getter
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
def inject(
self,
carrier: CarrierT,
context: Context | None = None,
setter: Setter[CarrierT] = default_setter,
) -> None:
"""Inject values from a Context into a carrier.
inject enables the propagation of values into HTTP clients or
other objects which perform an HTTP request. Implementations
should use the `Setter` 's set method to set values on the
carrier.
Args:
carrier: An object that a place to define HTTP headers.
Should be paired with setter, which should
know how to set header values on the carrier.
context: an optional Context to use. Defaults to current
context if not set.
setter: An optional `Setter` object that can set values
on the carrier.
"""
@property
@abc.abstractmethod
def fields(self) -> set[str]:
"""
Gets the fields set in the carrier by the `inject` method.
If the carrier is reused, its fields that correspond with the ones
present in this attribute should be deleted before calling `inject`.
Returns:
A set with the fields set in `inject`.
"""
@@ -0,0 +1,661 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""
The OpenTelemetry tracing API describes the classes used to generate
distributed traces.
The :class:`.Tracer` class controls access to the execution context, and
manages span creation. Each operation in a trace is represented by a
:class:`.Span`, which records the start, end time, and metadata associated with
the operation.
This module provides abstract (i.e. unimplemented) classes required for
tracing, and a concrete no-op :class:`.NonRecordingSpan` that allows applications
to use the API package alone without a supporting implementation.
To get a tracer, you need to provide the package name from which you are
calling the tracer APIs to OpenTelemetry by calling `TracerProvider.get_tracer`
with the calling module name and the version of your package.
The tracer supports creating spans that are "attached" or "detached" from the
context. New spans are "attached" to the context in that they are
created as children of the currently active span, and the newly-created span
can optionally become the new active span::
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
# Create a new root span, set it as the current span in context
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():
# Close child span, set parent as current
# Close parent span, set default span as current
When creating a span that's "detached" from the context the active span doesn't
change, and the caller is responsible for managing the span's lifetime::
# Explicit parent span assignment is done via the Context
from opentelemetry.trace import set_span_in_context
context = set_span_in_context(parent)
child = tracer.start_span("child", context=context)
try:
do_work(span=child)
finally:
child.end()
Applications should generally use a single global TracerProvider, and use
either implicit or explicit context propagation consistently throughout.
.. versionadded:: 0.1.0
.. versionchanged:: 0.3.0
`TracerProvider` was introduced and the global ``tracer`` getter was
replaced by ``tracer_provider``.
.. versionchanged:: 0.5.0
``tracer_provider`` was replaced by `get_tracer_provider`,
``set_preferred_tracer_provider_implementation`` was replaced by
`set_tracer_provider`.
"""
import os
from abc import ABC, abstractmethod
from collections.abc import Iterator, Sequence
from enum import Enum
from logging import getLogger
from typing import cast
from typing_extensions import deprecated
from opentelemetry import context as context_api
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.context.context import Context
from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER
from opentelemetry.trace.propagation import (
_SPAN_KEY,
get_current_span,
set_span_in_context,
)
from opentelemetry.trace.span import (
DEFAULT_TRACE_OPTIONS,
DEFAULT_TRACE_STATE,
INVALID_SPAN,
INVALID_SPAN_CONTEXT,
INVALID_SPAN_ID,
INVALID_TRACE_ID,
NonRecordingSpan,
Span,
SpanContext,
TraceFlags,
TraceState,
format_span_id,
format_trace_id,
)
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util import types
from opentelemetry.util._decorator import _agnosticcontextmanager
from opentelemetry.util._once import Once
from opentelemetry.util._providers import _load_provider
logger = getLogger(__name__)
class _LinkBase(ABC):
def __init__(self, context: "SpanContext") -> None:
self._context = context
@property
def context(self) -> "SpanContext":
return self._context
@property
@abstractmethod
def attributes(self) -> types.Attributes:
pass
class Link(_LinkBase):
"""A link to a `Span`. The attributes of a Link are immutable.
Args:
context: `SpanContext` of the `Span` to link to.
attributes: Link's attributes.
"""
def __init__(
self,
context: "SpanContext",
attributes: types.Attributes = None,
) -> None:
super().__init__(context)
self._attributes = attributes
@property
def attributes(self) -> types.Attributes:
return self._attributes
@property
def dropped_attributes(self) -> int:
if isinstance(self._attributes, BoundedAttributes):
return self._attributes.dropped
return 0
_Links = Sequence[Link] | None
class SpanKind(Enum):
"""Specifies additional details on how this span relates to its parent span.
Note that this enumeration is experimental and likely to change. See
https://github.com/open-telemetry/opentelemetry-specification/pull/226.
"""
#: Default value. Indicates that the span is used internally in the
# application.
INTERNAL = 0
#: Indicates that the span describes an operation that handles a remote
# request.
SERVER = 1
#: Indicates that the span describes a request to some remote service.
CLIENT = 2
#: Indicates that the span describes a producer sending a message to a
#: broker. Unlike client and server, there is usually no direct critical
#: path latency relationship between producer and consumer spans.
PRODUCER = 3
#: Indicates that the span describes a consumer receiving a message from a
#: broker. Unlike client and server, there is usually no direct critical
#: path latency relationship between producer and consumer spans.
CONSUMER = 4
class TracerProvider(ABC):
@abstractmethod
def get_tracer(
self,
instrumenting_module_name: str,
instrumenting_library_version: str | None = None,
schema_url: str | None = None,
attributes: types.Attributes = None,
) -> "Tracer":
"""Returns a `Tracer` for use by the given instrumentation library.
For any two calls it is undefined whether the same or different
`Tracer` instances are returned, even for different library names.
This function may return different `Tracer` types (e.g. a no-op tracer
vs. a functional tracer).
Args:
instrumenting_module_name: The uniquely identifiable name for instrumentation
scope, such as instrumentation library, package, module or class name.
``__name__`` should be avoided as this can result in
different tracer names if the tracers are in different files.
It is better to use a fixed string that can be imported where
needed and used consistently as the name of the tracer.
This should *not* be the name of the module that is
instrumented but the name of the module doing the instrumentation.
E.g., instead of ``"requests"``, use
``"opentelemetry.instrumentation.requests"``.
instrumenting_library_version: Optional. The version string of the
instrumenting library. Usually this should be the same as
``importlib.metadata.version(instrumenting_library_name)``.
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
attributes: Optional. Specifies the attributes of the emitted telemetry.
"""
class NoOpTracerProvider(TracerProvider):
"""The default TracerProvider, used when no implementation is available.
All operations are no-op.
"""
def get_tracer(
self,
instrumenting_module_name: str,
instrumenting_library_version: str | None = None,
schema_url: str | None = None,
attributes: types.Attributes = None,
) -> "Tracer":
# pylint:disable=no-self-use,unused-argument
return NoOpTracer()
@deprecated("You should use NoOpTracerProvider. Deprecated since version 1.9.0.")
class _DefaultTracerProvider(NoOpTracerProvider):
"""The default TracerProvider, used when no implementation is available.
All operations are no-op.
"""
class ProxyTracerProvider(TracerProvider):
def get_tracer(
self,
instrumenting_module_name: str,
instrumenting_library_version: str | None = None,
schema_url: str | None = None,
attributes: types.Attributes = None,
) -> "Tracer":
if _TRACER_PROVIDER:
return _TRACER_PROVIDER.get_tracer(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
attributes,
)
return ProxyTracer(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
attributes,
)
class Tracer(ABC):
"""Handles span creation and in-process context propagation.
This class provides methods for manipulating the context, creating spans,
and controlling spans' lifecycles.
"""
@abstractmethod
def start_span(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
"""Starts a span.
Create a new span. Start the span without setting it as the current
span in the context. To start the span and use the context in a single
method, see :meth:`start_as_current_span`.
By default the current span in the context will be used as parent, but an
explicit context can also be specified, by passing in a `Context` containing
a current `Span`. If there is no current span in the global `Context` or in
the specified context, the created span will be a root span.
The span can be used as a context manager. On exiting the context manager,
the span's end() method will be called.
Example::
# trace.get_current_span() will be used as the implicit parent.
# If none is found, the created span will be a root instance.
with tracer.start_span("one") as child:
child.add_event("child's event")
Args:
name: The name of the span to be created.
context: An optional Context containing the span's parent. Defaults to the
global context.
kind: The span's kind (relationship to parent). Note that is
meaningful even if there is no parent.
attributes: The span's attributes.
links: Links span to other spans
start_time: Sets the start time of a span
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines whether the span status will
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.
Returns:
The newly-created span.
"""
@_agnosticcontextmanager
@abstractmethod
def start_as_current_span(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
"""Context manager for creating a new span and set it
as the current span in this tracer's context.
Exiting the context manager will call the span's end method,
as well as return the current span to its previous value by
returning to the previous context.
Example::
with tracer.start_as_current_span("one") as parent:
parent.add_event("parent's event")
with tracer.start_as_current_span("two") as child:
child.add_event("child's event")
trace.get_current_span() # returns child
trace.get_current_span() # returns parent
trace.get_current_span() # returns previously active span
This is a convenience method for creating spans attached to the
tracer's context. Applications that need more control over the span
lifetime should use :meth:`start_span` instead. For example::
with tracer.start_as_current_span(name) as span:
do_work()
is equivalent to::
span = tracer.start_span(name)
with opentelemetry.trace.use_span(span, end_on_exit=True):
do_work()
This can also be used as a decorator::
@tracer.start_as_current_span("name")
def function(): ...
function()
Args:
name: The name of the span to be created.
context: An optional Context containing the span's parent. Defaults to the
global context.
kind: The span's kind (relationship to parent). Note that is
meaningful even if there is no parent.
attributes: The span's attributes.
links: Links span to other spans
start_time: Sets the start time of a span
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines whether the span status will
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.
end_on_exit: Whether to end the span automatically when leaving the
context manager.
Yields:
The newly-created span.
"""
class ProxyTracer(Tracer):
# pylint: disable=W0222,signature-differs
def __init__(
self,
instrumenting_module_name: str,
instrumenting_library_version: str | None = None,
schema_url: str | None = None,
attributes: types.Attributes = None,
):
self._instrumenting_module_name = instrumenting_module_name
self._instrumenting_library_version = instrumenting_library_version
self._schema_url = schema_url
self._attributes = attributes
self._real_tracer: Tracer | None = None
self._noop_tracer = NoOpTracer()
@property
def _tracer(self) -> Tracer:
if self._real_tracer:
return self._real_tracer
if _TRACER_PROVIDER:
self._real_tracer = _TRACER_PROVIDER.get_tracer(
self._instrumenting_module_name,
self._instrumenting_library_version,
self._schema_url,
self._attributes,
)
return self._real_tracer
return self._noop_tracer
def start_span(self, *args, **kwargs) -> Span: # type: ignore
return self._tracer.start_span(*args, **kwargs) # type: ignore
@_agnosticcontextmanager # type: ignore
def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]:
with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore
yield span
class NoOpTracer(Tracer):
"""The default Tracer, used when no Tracer implementation is available.
All operations are no-op.
"""
def start_span(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
current_span = get_current_span(context)
if isinstance(current_span, NonRecordingSpan):
return current_span
parent_span_context = current_span.get_span_context()
if parent_span_context is not None and not isinstance(parent_span_context, SpanContext):
logger.warning(
"Invalid span context for %s: %s",
current_span,
parent_span_context,
)
return INVALID_SPAN
return NonRecordingSpan(context=parent_span_context)
@_agnosticcontextmanager
def start_as_current_span(
self,
name: str,
context: Context | None = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: int | None = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
span = self.start_span(
name=name,
context=context,
kind=kind,
attributes=attributes,
links=links,
start_time=start_time,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
)
with use_span(
span,
end_on_exit=end_on_exit,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
) as span:
yield span
@deprecated("You should use NoOpTracer. Deprecated since version 1.9.0.")
class _DefaultTracer(NoOpTracer):
"""The default Tracer, used when no Tracer implementation is available.
All operations are no-op.
"""
_TRACER_PROVIDER_SET_ONCE = Once()
_TRACER_PROVIDER: TracerProvider | None = None
_PROXY_TRACER_PROVIDER = ProxyTracerProvider()
def get_tracer(
instrumenting_module_name: str,
instrumenting_library_version: str | None = None,
tracer_provider: TracerProvider | None = None,
schema_url: str | None = None,
attributes: types.Attributes = None,
) -> "Tracer":
"""Returns a `Tracer` for use by the given instrumentation library.
This function is a convenience wrapper for
opentelemetry.trace.TracerProvider.get_tracer.
If tracer_provider is omitted the current configured one is used.
"""
if tracer_provider is None:
tracer_provider = get_tracer_provider()
return tracer_provider.get_tracer(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
attributes,
)
def _set_tracer_provider(tracer_provider: TracerProvider, log: bool) -> None:
def set_tp() -> None:
global _TRACER_PROVIDER # pylint: disable=global-statement
_TRACER_PROVIDER = tracer_provider
did_set = _TRACER_PROVIDER_SET_ONCE.do_once(set_tp)
if log and not did_set:
logger.warning("Overriding of current TracerProvider is not allowed")
def set_tracer_provider(tracer_provider: TracerProvider) -> None:
"""Sets the current global :class:`~.TracerProvider` object.
This can only be done once, a warning will be logged if any further attempt
is made.
"""
_set_tracer_provider(tracer_provider, log=True)
def get_tracer_provider() -> TracerProvider:
"""Gets the current global :class:`~.TracerProvider` object."""
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
if OTEL_PYTHON_TRACER_PROVIDER not in os.environ:
return _PROXY_TRACER_PROVIDER
tracer_provider: TracerProvider = _load_provider(OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider")
_set_tracer_provider(tracer_provider, log=False)
# _TRACER_PROVIDER will have been set by one thread
return cast("TracerProvider", _TRACER_PROVIDER)
@_agnosticcontextmanager
def use_span(
span: Span,
end_on_exit: bool = False,
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> Iterator[Span]:
"""Takes a non-active span and activates it in the current context.
Args:
span: The span that should be activated in the current context.
end_on_exit: Whether to end the span automatically when leaving the
context manager scope.
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines whether the span status will
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.
"""
try:
token = context_api.attach(context_api.set_value(_SPAN_KEY, span))
try:
yield span
finally:
context_api.detach(token)
# Record only exceptions that inherit Exception class but not BaseException, because
# classes that directly inherit BaseException are not technically errors, e.g. GeneratorExit.
# See https://github.com/open-telemetry/opentelemetry-python/issues/4484
except Exception as exc: # pylint: disable=broad-exception-caught
if isinstance(span, Span) and span.is_recording():
# Record the exception as an event
if record_exception:
span.record_exception(exc)
# Set status in case exception was raised
if set_status_on_exception:
span.set_status(
Status(
status_code=StatusCode.ERROR,
description=f"{type(exc).__name__}: {exc}",
)
)
# This causes parent spans to set their status to ERROR and to record
# an exception as an event if a child span raises an exception even if
# such child span was started with both record_exception and
# set_status_on_exception attributes set to False.
raise
finally:
if end_on_exit:
span.end()
__all__ = [
"DEFAULT_TRACE_OPTIONS",
"DEFAULT_TRACE_STATE",
"INVALID_SPAN",
"INVALID_SPAN_CONTEXT",
"INVALID_SPAN_ID",
"INVALID_TRACE_ID",
"Link",
"NonRecordingSpan",
"Span",
"SpanContext",
"SpanKind",
"Status",
"StatusCode",
"TraceFlags",
"TraceState",
"Tracer",
"TracerProvider",
"format_span_id",
"format_trace_id",
"get_current_span",
"get_tracer",
"get_tracer_provider",
"set_span_in_context",
"set_tracer_provider",
"use_span",
]
@@ -0,0 +1,37 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from opentelemetry.context import create_key, get_value, set_value
from opentelemetry.context.context import Context
from opentelemetry.trace.span import INVALID_SPAN, Span
SPAN_KEY = "current-span"
_SPAN_KEY = create_key("current-span")
def set_span_in_context(span: Span, context: Context | None = None) -> Context:
"""Set the span in the given context.
Args:
span: The Span to set.
context: a Context object. if one is not passed, the
default current context is used instead.
"""
ctx = set_value(_SPAN_KEY, span, context=context)
return ctx
def get_current_span(context: Context | None = None) -> Span:
"""Retrieve the current span.
Args:
context: A Context object. If one is not passed, the
default current context is used instead.
Returns:
The Span set in the context if it exists. INVALID_SPAN otherwise.
"""
span = get_value(_SPAN_KEY, context=context)
if span is None or not isinstance(span, Span):
return INVALID_SPAN
return span
@@ -0,0 +1,115 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
#
import logging
import re
from opentelemetry import trace
from opentelemetry.context.context import Context
from opentelemetry.propagators import textmap
from opentelemetry.trace import format_span_id, format_trace_id
from opentelemetry.trace.span import (
_TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH,
TraceState,
)
_logger = logging.getLogger(__name__)
class TraceContextTextMapPropagator(textmap.TextMapPropagator):
"""Extracts and injects using w3c TraceContext's headers."""
_TRACEPARENT_HEADER_NAME = "traceparent"
_TRACESTATE_HEADER_NAME = "tracestate"
_TRACEPARENT_HEADER_FORMAT = "^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})" + "(-.*)?[ \t]*$"
_TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT)
def extract(
self,
carrier: textmap.CarrierT,
context: Context | None = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Extracts SpanContext from the carrier.
See `opentelemetry.propagators.textmap.TextMapPropagator.extract`
"""
if context is None:
context = Context()
header = getter.get(carrier, self._TRACEPARENT_HEADER_NAME)
if not header:
return context
match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0])
if not match:
return context
version: str = match.group(1)
trace_id: str = match.group(2)
span_id: str = match.group(3)
trace_flags: str = match.group(4)
if trace_id == "0" * 32 or span_id == "0" * 16:
return context
if version == "00":
if match.group(5): # type: ignore
return context
if version == "ff":
return context
tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME)
if tracestate_headers is None:
tracestate = None
elif (
# multiple header fields are combined with "," (RFC 9110, section 5.3)
sum(len(header) for header in tracestate_headers) + len(tracestate_headers) - 1
> _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH
):
_logger.warning(
"tracestate header exceeded the maximum length of %d characters",
_TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH,
)
tracestate = None
else:
tracestate = TraceState.from_header(tracestate_headers)
span_context = trace.SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
is_remote=True,
trace_flags=trace.TraceFlags(int(trace_flags, 16)),
trace_state=tracestate,
)
return trace.set_span_in_context(trace.NonRecordingSpan(span_context), context)
def inject(
self,
carrier: textmap.CarrierT,
context: Context | None = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Injects SpanContext into the carrier.
See `opentelemetry.propagators.textmap.TextMapPropagator.inject`
"""
span = trace.get_current_span(context)
span_context = span.get_span_context()
if span_context == trace.INVALID_SPAN_CONTEXT:
return
traceparent_string = f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}"
setter.set(carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string)
if span_context.trace_state:
tracestate_string = span_context.trace_state.to_header()
setter.set(carrier, self._TRACESTATE_HEADER_NAME, tracestate_string)
@property
def fields(self) -> set[str]:
"""Returns a set with the fields set in `inject`.
See
`opentelemetry.propagators.textmap.TextMapPropagator.fields`
"""
return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME}
@@ -0,0 +1,616 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import abc
import logging
import re
import types as python_types
import typing
import warnings
from collections.abc import Iterator, Mapping, Sequence
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util import types
# The key MUST begin with a lowercase letter or a digit,
# and can only contain lowercase letters (a-z), digits (0-9),
# underscores (_), dashes (-), asterisks (*), and forward slashes (/).
# For multi-tenant vendor scenarios, an at sign (@) can be used to
# prefix the vendor name. Vendors SHOULD set the tenant ID
# at the beginning of the key.
# key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )
# key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )
# lcalpha = %x61-7A ; a-z
_KEY_FORMAT = (
r"[a-z][_0-9a-z\-\*\/]{0,255}|"
r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}"
)
_KEY_PATTERN = re.compile(_KEY_FORMAT)
# The value is an opaque string containing up to 256 printable
# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E)
# except comma (,) and (=).
# value = 0*255(chr) nblk-chr
# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E
# chr = %x20 / nblk-chr
_VALUE_FORMAT = r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]"
_VALUE_PATTERN = re.compile(_VALUE_FORMAT)
_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32
_TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH = 8192
_member_pattern = re.compile(f"({_KEY_FORMAT})=({_VALUE_FORMAT})")
_logger = logging.getLogger(__name__)
def _is_valid_pair(key: str, value: str) -> bool:
return (
isinstance(key, str)
and _KEY_PATTERN.fullmatch(key) is not None
and isinstance(value, str)
and _VALUE_PATTERN.fullmatch(value) is not None
)
class Span(abc.ABC):
"""A span represents a single operation within a trace."""
@abc.abstractmethod
def end(self, end_time: int | None = None) -> None:
"""Sets the current time as the span's end time.
The span's end time is the wall time at which the operation finished.
Only the first call to `end` should modify the span, and
implementations are free to ignore or raise on further calls.
"""
@abc.abstractmethod
def get_span_context(self) -> SpanContext:
"""Gets the span's SpanContext.
Get an immutable, serializable identifier for this span that can be
used to create new child spans.
Returns:
A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state.
"""
@abc.abstractmethod
def set_attributes(self, attributes: Mapping[str, types.AnyValue]) -> None:
"""Sets Attributes.
Sets Attributes with the key and value passed as arguments dict.
Note: The behavior of `None` value attributes is undefined, and hence
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.
"""
@abc.abstractmethod
def set_attribute(self, key: str, value: types.AnyValue) -> None:
"""Sets an Attribute.
Sets a single Attribute with the key and value passed as arguments.
Note: The behavior of `None` value attributes is undefined, and hence
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.
"""
@abc.abstractmethod
def add_event(
self,
name: str,
attributes: types.Attributes = None,
timestamp: int | None = None,
) -> None:
"""Adds an `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.
"""
def add_link( # pylint: disable=no-self-use
self,
context: SpanContext,
attributes: types.Attributes = None,
) -> None:
"""Adds a `Link`.
Adds a single `Link` with the `SpanContext` of the span to link to and,
optionally, attributes passed as arguments. Implementations may ignore
calls with an invalid span context if both attributes and TraceState
are empty.
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.
"""
warnings.warn(
"Span.add_link() not implemented and will be a no-op. "
"Use opentelemetry-sdk >= 1.23 to add links after span creation"
)
@abc.abstractmethod
def update_name(self, name: str) -> None:
"""Updates the `Span` name.
This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`.
Upon this update, any sampling behavior based on Span name will depend
on the implementation.
"""
@abc.abstractmethod
def is_recording(self) -> bool:
"""Returns whether this span will be recorded.
Returns true if this Span is active and recording information like
events with the add_event operation and attributes using set_attribute.
"""
@abc.abstractmethod
def set_status(
self,
status: Status | StatusCode,
description: str | None = None,
) -> None:
"""Sets the Status of the Span. If used, this will override the default
Span status.
"""
@abc.abstractmethod
def record_exception(
self,
exception: BaseException,
attributes: types.Attributes = None,
timestamp: int | None = None,
escaped: bool = False,
) -> None:
"""Records an exception as a span event."""
def __enter__(self) -> Span:
"""Invoked when `Span` is used as a context manager.
Returns the `Span` itself.
"""
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: python_types.TracebackType | None,
) -> None:
"""Ends context manager and calls `end` on the `Span`."""
self.end()
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.
See the `W3C Trace Context - Traceparent`_ spec for details.
.. _W3C Trace Context - Traceparent:
https://www.w3.org/TR/trace-context-2/#trace-flags
"""
DEFAULT = 0x00
SAMPLED = 0x01
RANDOM_TRACE_ID = 0x02
@classmethod
def get_default(cls) -> TraceFlags:
return cls(cls.DEFAULT)
@property
def sampled(self) -> bool:
return bool(self & TraceFlags.SAMPLED)
@property
def random_trace_id(self) -> bool:
return bool(self & TraceFlags.RANDOM_TRACE_ID)
DEFAULT_TRACE_OPTIONS = TraceFlags.get_default()
class TraceState(Mapping[str, str]):
"""A list of key-value pairs representing vendor-specific trace info.
Keys and values are strings of up to 256 printable US-ASCII characters.
Implementations should conform to the `W3C Trace Context - Tracestate`_
spec, which describes additional restrictions on valid field values.
.. _W3C Trace Context - Tracestate:
https://www.w3.org/TR/trace-context/#tracestate-field
"""
def __init__(
self,
entries: Sequence[tuple[str, str]] | None = None,
) -> None:
self._dict = {} # type: dict[str, str]
if entries is None:
return
if len(entries) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
_logger.warning(
"There can't be more than %s key/value pairs.",
_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS,
)
return
for key, value in entries:
if _is_valid_pair(key, value):
if key in self._dict:
_logger.warning("Duplicate key: %s found.", key)
continue
self._dict[key] = value
else:
_logger.warning("Invalid key/value pair (%s, %s) found.", key, value)
def __contains__(self, item: object) -> bool:
return item in self._dict
def __getitem__(self, key: str) -> str:
return self._dict[key]
def __iter__(self) -> Iterator[str]:
return iter(self._dict)
def __len__(self) -> int:
return len(self._dict)
def __repr__(self) -> str:
pairs = [f"{{key={key}, value={value}}}" for key, value in self._dict.items()]
return str(pairs)
def add(self, key: str, value: str) -> TraceState:
"""Adds a key-value pair to tracestate. The provided pair should
adhere to w3c tracestate identifiers format.
Args:
key: A valid tracestate key to add
value: A valid tracestate value to add
Returns:
A new TraceState with the modifications applied.
If the provided key-value pair is invalid or results in tracestate
that violates tracecontext specification, they are discarded and
same tracestate will be returned.
"""
if not _is_valid_pair(key, value):
_logger.warning("Invalid key/value pair (%s, %s) found.", key, value)
return self
# There can be a maximum of 32 pairs
if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
_logger.warning("There can't be more 32 key/value pairs.")
return self
# Duplicate entries are not allowed
if key in self._dict:
_logger.warning("The provided key %s already exists.", key)
return self
new_state = [(key, value)] + list(self._dict.items())
return TraceState(new_state)
def update(self, key: str, value: str) -> TraceState:
"""Updates a key-value pair in tracestate. The provided pair should
adhere to w3c tracestate identifiers format.
Note:
This method performs an "upsert": if ``key`` is not already present
it is added (when the tracestate is below the 32-entry limit),
otherwise its value is updated. This upsert behaviour is intentional
but goes beyond what the OpenTelemetry specification defines for
``update`` (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#tracestate),
and is kept for backwards compatibility with callers that rely on it.
Args:
key: A valid tracestate key to update
value: A valid tracestate value to update for key
Returns:
A new TraceState with the modifications applied.
If the provided pair is invalid, or adding a new key would exceed
the maximum of 32 key/value pairs, they are discarded and the same
tracestate is returned unchanged. Updating an existing key is always
allowed, even at the maximum.
"""
if not _is_valid_pair(key, value):
_logger.warning("Invalid key/value pair (%s, %s) found.", key, value)
return self
# Adding a new key at the maximum would push the tracestate over the
# limit and cause the constructor to drop every entry. Return unchanged
# instead of silently discarding existing state.
if key not in self._dict and len(self._dict) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
_logger.warning("There can't be more 32 key/value pairs.")
return self
prev_state = self._dict.copy()
prev_state.pop(key, None)
new_state = [(key, value), *prev_state.items()]
return TraceState(new_state)
def delete(self, key: str) -> TraceState:
"""Deletes a key-value from tracestate.
Args:
key: A valid tracestate key to remove key-value pair from tracestate
Returns:
A new TraceState with the modifications applied.
If the provided key-value pair is invalid or results in tracestate
that violates tracecontext specification, they are discarded and
same tracestate will be returned.
"""
if key not in self._dict:
_logger.warning("The provided key %s doesn't exist.", key)
return self
prev_state = self._dict.copy()
prev_state.pop(key)
new_state = list(prev_state.items())
return TraceState(new_state)
def to_header(self) -> str:
"""Creates a w3c tracestate header from a TraceState.
Returns:
A string that adheres to the w3c tracestate
header format.
"""
return ",".join(key + "=" + value for key, value in self._dict.items())
@classmethod
def from_header(cls, header_list: list[str]) -> TraceState:
"""Parses one or more w3c tracestate header into a TraceState.
Args:
header_list: one or more w3c tracestate headers.
Returns:
A valid TraceState that contains values extracted from
the tracestate header.
If the format of one headers is illegal, all values will
be discarded and an empty tracestate will be returned.
If the number of keys is beyond the maximum, all values
will be discarded and an empty tracestate will be returned.
"""
pairs = {} # type: dict[str, str]
for header in header_list:
for member in header.split(","):
# remove optional whitespace (RFC 9110, section 5.6.3)
member = member.strip(" \t")
# empty members are valid, but no need to process further.
if not member:
continue
match = _member_pattern.fullmatch(member)
if not match:
_logger.warning(
"Member doesn't match the w3c identifiers format %s",
member,
)
return cls()
key, value = match.groups()
# duplicate keys are not legal in header
if key in pairs:
return cls()
pairs[key] = value
return cls(list(pairs.items()))
@classmethod
def get_default(cls) -> TraceState:
return cls()
def keys(self) -> typing.KeysView[str]:
return self._dict.keys()
def items(self) -> typing.ItemsView[str, str]:
return self._dict.items()
def values(self) -> typing.ValuesView[str]:
return self._dict.values()
DEFAULT_TRACE_STATE = TraceState.get_default()
_TRACE_ID_MAX_VALUE = 2**128 - 1
_SPAN_ID_MAX_VALUE = 2**64 - 1
class SpanContext(tuple[int, int, bool, "TraceFlags", "TraceState", bool]):
"""The state of a Span to propagate between processes.
This class includes the immutable attributes of a :class:`.Span` that must
be propagated to a span's children and across process boundaries.
Args:
trace_id: The ID of the trace that this span belongs to.
span_id: This span's ID.
is_remote: True if propagated from a remote parent.
trace_flags: Trace options to propagate.
trace_state: Tracing-system-specific info to propagate.
"""
def __new__(
cls,
trace_id: int,
span_id: int,
is_remote: bool,
trace_flags: TraceFlags | None = DEFAULT_TRACE_OPTIONS,
trace_state: TraceState | None = DEFAULT_TRACE_STATE,
) -> SpanContext:
if trace_flags is None:
trace_flags = DEFAULT_TRACE_OPTIONS
if trace_state is None:
trace_state = DEFAULT_TRACE_STATE
is_valid = (
INVALID_TRACE_ID < trace_id <= _TRACE_ID_MAX_VALUE and INVALID_SPAN_ID < span_id <= _SPAN_ID_MAX_VALUE
)
return tuple.__new__(
cls,
(trace_id, span_id, is_remote, trace_flags, trace_state, is_valid),
)
def __getnewargs__(
self,
) -> tuple[int, int, bool, TraceFlags, TraceState]:
return (
self.trace_id,
self.span_id,
self.is_remote,
self.trace_flags,
self.trace_state,
)
@property
def trace_id(self) -> int:
return self[0] # pylint: disable=unsubscriptable-object
@property
def span_id(self) -> int:
return self[1] # pylint: disable=unsubscriptable-object
@property
def is_remote(self) -> bool:
return self[2] # pylint: disable=unsubscriptable-object
@property
def trace_flags(self) -> TraceFlags:
return self[3] # pylint: disable=unsubscriptable-object
@property
def trace_state(self) -> TraceState:
return self[4] # pylint: disable=unsubscriptable-object
@property
def is_valid(self) -> bool:
return self[5] # pylint: disable=unsubscriptable-object
def __setattr__(self, *args: str) -> None:
_logger.debug("Immutable type, ignoring call to set attribute", stack_info=True)
def __delattr__(self, *args: str) -> None:
_logger.debug(
"Immutable type, ignoring call to delete attribute",
stack_info=True,
)
def __repr__(self) -> str:
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})"
class NonRecordingSpan(Span):
"""The Span that is used when no Span implementation is available.
All operations are no-op except context propagation.
"""
def __init__(self, context: SpanContext) -> None:
self._context = context
def get_span_context(self) -> SpanContext:
return self._context
def is_recording(self) -> bool:
return False
def end(self, end_time: int | None = None) -> None:
pass
def set_attributes(self, attributes: Mapping[str, types.AnyValue]) -> None:
pass
def set_attribute(self, key: str, value: types.AnyValue) -> None:
pass
def add_event(
self,
name: str,
attributes: types.Attributes = None,
timestamp: int | None = None,
) -> None:
pass
def add_link(
self,
context: SpanContext,
attributes: types.Attributes = None,
) -> None:
pass
def update_name(self, name: str) -> None:
pass
def set_status(
self,
status: Status | StatusCode,
description: str | None = None,
) -> None:
pass
def record_exception(
self,
exception: BaseException,
attributes: types.Attributes = None,
timestamp: int | None = None,
escaped: bool = False,
) -> None:
pass
def __repr__(self) -> str:
return f"NonRecordingSpan({self._context!r})"
INVALID_SPAN_ID = 0x0000000000000000
INVALID_TRACE_ID = 0x00000000000000000000000000000000
INVALID_SPAN_CONTEXT = SpanContext(
trace_id=INVALID_TRACE_ID,
span_id=INVALID_SPAN_ID,
is_remote=False,
trace_flags=DEFAULT_TRACE_OPTIONS,
trace_state=DEFAULT_TRACE_STATE,
)
INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT)
def format_trace_id(trace_id: int) -> str:
"""Convenience trace ID formatting method
Args:
trace_id: Trace ID int
Returns:
The trace ID (16 bytes) cast to a 32-character hexadecimal string
"""
return format(trace_id, "032x")
def format_span_id(span_id: int) -> str:
"""Convenience span ID formatting method
Args:
span_id: Span ID int
Returns:
The span ID (8 bytes) cast to a 16-character hexadecimal string
"""
return format(span_id, "016x")
@@ -0,0 +1,68 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
import enum
import logging
logger = logging.getLogger(__name__)
class StatusCode(enum.Enum):
"""Represents the canonical set of status codes of a finished Span."""
UNSET = 0
"""The default status."""
OK = 1
"""The operation has been validated by an Application developer or Operator to have completed successfully."""
ERROR = 2
"""The operation contains an error."""
class Status:
"""Represents the status of a finished Span.
Args:
status_code: The canonical status code that describes the result
status of the operation.
description: An optional description of the status.
"""
def __init__(
self,
status_code: StatusCode = StatusCode.UNSET,
description: str | None = None,
):
self._status_code = status_code
self._description = None
if description:
if not isinstance(description, str):
logger.warning("Invalid status description type, expected str")
return
if status_code is not StatusCode.ERROR:
logger.warning("description should only be set when status_code is set to StatusCode.ERROR")
return
self._description = description
@property
def status_code(self) -> StatusCode:
"""Represents the canonical status code of a finished Span."""
return self._status_code
@property
def description(self) -> str | None:
"""Status description"""
return self._description
@property
def is_ok(self) -> bool:
"""Returns false if this represents an error, true otherwise."""
return self.is_unset or self._status_code is StatusCode.OK
@property
def is_unset(self) -> bool:
"""Returns true if unset, false otherwise."""
return self._status_code is StatusCode.UNSET
@@ -0,0 +1,75 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
import contextlib
import functools
import inspect
from collections.abc import Callable, Iterator
from typing import TYPE_CHECKING, Generic, TypeVar
V = TypeVar("V")
R = TypeVar("R") # Return type
Pargs = TypeVar("Pargs") # Generic type for arguments
Pkwargs = TypeVar("Pkwargs") # Generic type for arguments
# We don't actually depend on typing_extensions but we can use it in CI with this conditional
# import. ParamSpec can be imported directly from typing after python 3.9 is dropped
# https://peps.python.org/pep-0612/.
if TYPE_CHECKING:
from typing_extensions import ParamSpec
P = ParamSpec("P") # Generic type for all arguments
class _AgnosticContextManager(
contextlib._GeneratorContextManager[R],
Generic[R],
): # pylint: disable=protected-access
"""Context manager that can decorate both async and sync functions.
This is an overridden version of the contextlib._GeneratorContextManager
class that will decorate async functions with an async context manager
to end the span AFTER the entire async function coroutine finishes.
Else it will report near zero spans durations for async functions.
We are overriding the contextlib._GeneratorContextManager class as
reimplementing it is a lot of code to maintain and this class (even if it's
marked as protected) doesn't seems like to be evolving a lot.
For more information, see:
https://github.com/open-telemetry/opentelemetry-python/pull/3633
"""
def __enter__(self) -> R:
"""Reimplementing __enter__ to avoid the type error.
The original __enter__ method returns Any type, but we want to return R.
"""
del self.args, self.kwds, self.func # type: ignore
try:
return next(self.gen) # type: ignore
except StopIteration:
raise RuntimeError("generator didn't yield") from None
def __call__(self, func: V) -> V: # pyright: ignore [reportIncompatibleMethodOverride]
if inspect.iscoroutinefunction(func):
@functools.wraps(func) # type: ignore
async def async_wrapper(*args: Pargs, **kwargs: Pkwargs) -> R: # pyright: ignore [reportInvalidTypeVarUse]
with self._recreate_cm(): # type: ignore
return await func(*args, **kwargs) # type: ignore
return async_wrapper # type: ignore
return super().__call__(func) # type: ignore
def _agnosticcontextmanager(
func: "Callable[P, Iterator[R]]",
) -> "Callable[P, _AgnosticContextManager[R]]":
@functools.wraps(func)
def helper(*args: Pargs, **kwargs: Pkwargs) -> _AgnosticContextManager[R]: # pyright: ignore [reportInvalidTypeVarUse]
return _AgnosticContextManager(func, args, kwargs) # pyright: ignore [reportArgumentType]
# Ignoring the type to keep the original signature of the function
return helper # type: ignore[return-value]
@@ -0,0 +1,61 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""
Caching and compatibility wrapper for standard library ``importlib.metadata``.
This module caches ``entry_points()`` to avoid reloading entry points from disk on every call.
It also normalizes minor differences across python versions 3.10+. References to issues:
- https://github.com/open-telemetry/opentelemetry-python/pull/4735
- https://github.com/open-telemetry/opentelemetry-python/pull/5203
"""
import itertools
from functools import cache
from importlib.metadata import (
Distribution,
EntryPoint,
EntryPoints,
PackageNotFoundError,
distributions,
requires,
version,
)
from importlib.metadata import entry_points as original_entry_points
from typing import Any
def _as_entry_points(eps: Any) -> EntryPoints:
# Python versions greater than 3.11 return EntryPoints.
if isinstance(eps, EntryPoints):
return eps
# In Python 3.10 and 3.11 entry_points() returns
# a dict-like SelectableGroups object.
# Use dict.values() instead of eps.values() to avoid the DeprecationWarning
# that SelectableGroups raises when calling .values().
if isinstance(eps, dict):
return EntryPoints(itertools.chain.from_iterable(dict.values(eps)))
# This case should be unreachable, but is included as a fallback.
return EntryPoints(ep for group in eps.groups for ep in eps.select(group=group))
@cache
def _original_entry_points_cached() -> EntryPoints:
return _as_entry_points(original_entry_points())
def entry_points(**params) -> EntryPoints:
return _original_entry_points_cached().select(**params)
__all__ = [
"Distribution",
"EntryPoint",
"EntryPoints",
"PackageNotFoundError",
"distributions",
"entry_points",
"requires",
"version",
]
@@ -0,0 +1,36 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Callable
from threading import Lock
class Once:
"""Execute a function exactly once and block all callers until the function returns
Same as golang's `sync.Once <https://pkg.go.dev/sync#Once>`_
"""
def __init__(self) -> None:
self._lock = Lock()
self._done = False
def do_once(self, func: Callable[[], None]) -> bool:
"""Execute ``func`` if it hasn't been executed or return.
Will block until ``func`` has been called by one thread.
Returns:
Whether or not ``func`` was executed in this call
"""
# fast path, try to avoid locking
if self._done:
return False
with self._lock:
if not self._done:
func()
self._done = True
return True
return False
@@ -0,0 +1,42 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from logging import getLogger
from os import environ
from typing import TYPE_CHECKING, TypeVar, cast
if TYPE_CHECKING:
from opentelemetry.metrics import MeterProvider
from opentelemetry.trace import TracerProvider
Provider = TypeVar("Provider", "TracerProvider", "MeterProvider")
logger = getLogger(__name__)
def _load_provider(provider_environment_variable: str, provider: str) -> Provider: # type: ignore[type-var]
# pylint: disable=import-outside-toplevel,no-name-in-module
from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415
entry_points,
)
try:
provider_name = cast(
str,
environ.get(provider_environment_variable, f"default_{provider}"),
)
return cast(
Provider,
next( # type: ignore
iter( # type: ignore
entry_points( # type: ignore
group=f"opentelemetry_{provider}",
name=provider_name,
)
)
).load()(),
)
except Exception: # pylint: disable=broad-exception-caught
logger.exception("Failed to load configured provider %s", provider)
raise
@@ -0,0 +1,93 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Mapping
from logging import getLogger
from re import compile, split
from urllib.parse import unquote
from typing_extensions import deprecated
_logger = getLogger(__name__)
# The following regexes reference this spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables
# Optional whitespace
_OWS = r"[ \t]*"
# A key contains printable US-ASCII characters except: SP and "(),/:;<=>?@[\]{}
_KEY_FORMAT = r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+"
# A value contains a URL-encoded UTF-8 string. The encoded form can contain any
# printable US-ASCII characters (0x20-0x7f) other than SP, DEL, and ",;/
_VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*"
# Like above with SP included
_LIBERAL_VALUE_FORMAT = r"[\x20\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*"
# A key-value is key=value, with optional whitespace surrounding key and value
_KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}"
_HEADER_PATTERN = compile(_KEY_VALUE_FORMAT)
_LIBERAL_HEADER_PATTERN = compile(rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_LIBERAL_VALUE_FORMAT}{_OWS}")
_DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*")
_BAGGAGE_PROPERTY_FORMAT = rf"{_KEY_VALUE_FORMAT}|{_OWS}{_KEY_FORMAT}{_OWS}"
_INVALID_HEADER_ERROR_MESSAGE_STRICT_TEMPLATE = (
"Header format invalid! Header values in environment variables must be "
"URL encoded per the OpenTelemetry Protocol Exporter specification: %s"
)
_INVALID_HEADER_ERROR_MESSAGE_LIBERAL_TEMPLATE = (
"Header format invalid! Header values in environment variables must be "
"URL encoded per the OpenTelemetry Protocol Exporter specification or "
"a comma separated list of name=value occurrences: %s"
)
# pylint: disable=invalid-name
@deprecated("You should use parse_env_headers. Deprecated since version 1.15.0.")
def parse_headers(s: str) -> Mapping[str, str]:
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.
"""
headers: dict[str, str] = {}
headers_list: list[str] = split(_DELIMITER_PATTERN, s)
for header in headers_list:
if not header: # empty string
continue
header_match = _HEADER_PATTERN.fullmatch(header.strip())
if not header_match and not liberal:
_logger.warning(_INVALID_HEADER_ERROR_MESSAGE_STRICT_TEMPLATE, header)
continue
if header_match:
match_string: str = header_match.string
# value may contain any number of `=`
name, value = match_string.split("=", 1)
name = unquote(name).strip().lower()
value = unquote(value).strip()
headers[name] = value
else:
# this is not url-encoded and does not match the spec but we decided to be
# liberal in what we accept to match other languages SDKs behaviour
liberal_header_match = _LIBERAL_HEADER_PATTERN.fullmatch(header.strip())
if not liberal_header_match:
_logger.warning(_INVALID_HEADER_ERROR_MESSAGE_LIBERAL_TEMPLATE, header)
continue
liberal_match_string: str = liberal_header_match.string
# value may contain any number of `=`
name, value = liberal_match_string.split("=", 1)
name = name.strip().lower()
value = value.strip()
headers[name] = value
return headers
@@ -0,0 +1,28 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Mapping, Sequence
from typing import TypeAlias
# This is the implementation of the "Any" type as specified by the specifications of OpenTelemetry data model for logs.
# 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
AttributeValue: TypeAlias = AnyValue
Attributes = Mapping[str, AnyValue] | None
# Deprecated type, do not use.
AttributesAsKey = tuple[
tuple[
str,
str
| bool
| int
| float
| tuple[str | None, ...]
| tuple[bool | None, ...]
| tuple[int | None, ...]
| tuple[float | None, ...],
],
...,
]
@@ -0,0 +1,4 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
__version__ = "1.45.1"