add
This commit is contained in:
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
@@ -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,
|
||||
)
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
BIN
Binary file not shown.
@@ -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()
|
||||
BIN
Binary file not shown.
@@ -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)
|
||||
BIN
Binary file not shown.
+179
@@ -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)
|
||||
BIN
Binary file not shown.
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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"]
|
||||
+43
@@ -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"]
|
||||
+72
@@ -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
|
||||
"""
|
||||
BIN
Binary file not shown.
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
+892
@@ -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)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+540
@@ -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,
|
||||
)
|
||||
+51
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+115
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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"
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user