Rate this Page

Source code for monarch._src.actor.telemetry

# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# pyre-strict


import functools
import inspect
import logging
import warnings
from typing import Callable, Dict, Optional, overload, Sequence, TypeVar

import opentelemetry.metrics as metrics  # @manual=fbsource//third-party/pypi/opentelemetry-api:opentelemetry-api
import opentelemetry.trace as trace  # @manual=fbsource//third-party/pypi/opentelemetry-api:opentelemetry-api
from monarch._rust_bindings.monarch_hyperactor.proc import ActorAddr
from monarch._rust_bindings.monarch_hyperactor.telemetry import (  # @manual=//monarch/monarch_extension:monarch_extension
    forward_to_tracing,
    PyCounter,
    PyHistogram,
    PySpan,
    PyUpDownCounter,
)
from opentelemetry.context import Context
from opentelemetry.metrics import CallbackT
from opentelemetry.util.types import Attributes
from typing_extensions import ParamSpec


def _current_actor_id() -> ActorAddr | None:
    from monarch._src.actor.actor_mesh import _context

    ctx = _context.get(None)
    return None if ctx is None else ctx.actor_instance.actor_id


def span(name: str) -> PySpan:
    return PySpan(name, _current_actor_id())


_P = ParamSpec("_P")
_R = TypeVar("_R")


@overload
def traced(fn: Callable[_P, _R]) -> Callable[_P, _R]: ...


@overload
def traced(*, name: str) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ...


[docs]def traced( fn: Callable[_P, _R] | None = None, *, name: str | None = None ) -> Callable[_P, _R] | Callable[[Callable[_P, _R]], Callable[_P, _R]]: """Decorator that wraps a function in a telemetry span. Works with both sync and async functions. The span is automatically associated with the current actor context, if any. When no name is provided, the function's ``__name__`` is used as the span name. Usage:: @traced async def do_work(): ... @traced(name="custom_name") def compute(): ... """ def decorator(fn: Callable[_P, _R]) -> Callable[_P, _R]: span_name: str = name if name is not None else fn.__name__ if inspect.iscoroutinefunction(fn): @functools.wraps(fn) # pyrefly: ignore [bad-return] async def async_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: with span(span_name): return await fn(*args, **kwargs) # type: ignore[misc] return async_wrapper # type: ignore[return-value] else: @functools.wraps(fn) # pyrefly: ignore [bad-return] def sync_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: with span(span_name): return fn(*args, **kwargs) return sync_wrapper if fn is not None: return decorator(fn) return decorator
def log_with_tracing( level: int, msg: object, *args: object, stack_info: bool = False, stacklevel: int = 1, extra: Dict[str, object] | None = None, logger: logging.Logger | None = None, ) -> None: """Emit a log record through the normal `logging` handler chain and also forward it directly to Rust tracing. Use this in library code that must guarantee the event reaches the tracing backend even when no `TracingForwarder` handler is attached to the calling process's loggers (e.g., a non-actor driver process). `stacklevel=1` attributes the record's pathname/lineno/funcName to the immediate caller of `log_with_tracing`. """ logger = logger or logging.getLogger(__name__) fn, lno, func, sinfo = logger.findCaller( stack_info=stack_info, stacklevel=stacklevel + 1, ) record = logger.makeRecord( logger.name, level, fn, lno, msg, args, None, func, extra=extra, sinfo=sinfo, ) logger.handle(record) forward_to_tracing(record) class TracingForwarder(logging.Handler): def emit(self, record: logging.LogRecord) -> None: # Try to add actor_id from the current context to the logging record try: actor_id = _current_actor_id() if actor_id is not None: # Add actor_id as an attribute to the logging record record.actor_id = str(actor_id) # type: ignore[attr-defined] except Exception: # If we can't get the context or actor_id for any reason, just continue # without adding the actor_id field pass forward_to_tracing(record) class Counter(metrics.Counter): inner: PyCounter def __init__(self, name: str) -> None: # pyrefly: ignore [missing-attribute] super().__init__(name) self.inner = PyCounter(name) def add( self, amount: int | float, attributes: Optional[Attributes] = None, context: Optional[Context] = None, ) -> None: rust_attributes = None if attributes: rust_attributes = {str(k): str(v) for k, v in attributes.items()} return self.inner.add(int(amount), attributes=rust_attributes) class UpDownCounter(metrics.UpDownCounter): inner: PyUpDownCounter def __init__(self, name: str) -> None: # pyrefly: ignore [missing-attribute] super().__init__(name) self.inner = PyUpDownCounter(name) def add( self, amount: int | float, attributes: Optional[Attributes] = None, context: Optional[Context] = None, ) -> None: rust_attributes = None if attributes: rust_attributes = {str(k): str(v) for k, v in attributes.items()} self.inner.add(int(amount), attributes=rust_attributes) class Histogram(metrics.Histogram): inner: PyHistogram def __init__(self, name: str) -> None: # pyrefly: ignore [missing-attribute] super().__init__(name) self.inner = PyHistogram(name) def record( self, amount: int | float, attributes: Optional[Attributes] = None, context: Optional[Context] = None, ) -> None: rust_attributes = None if attributes: rust_attributes = {str(k): str(v) for k, v in attributes.items()} self.inner.record(amount, attributes=rust_attributes) class Meter(metrics.Meter): def create_counter( self, name: str, unit: str = "", description: str = "", ) -> metrics.Counter: return Counter(name) def create_up_down_counter( self, name: str, unit: str = "", description: str = "", ) -> metrics.UpDownCounter: return UpDownCounter(name) def create_observable_counter( self, name: str, callbacks: Optional[Sequence[CallbackT]] = None, unit: str = "", description: str = "", ) -> metrics.ObservableCounter: raise NotImplementedError() def create_histogram( self, name: str, unit: str = "", description: str = "", *, explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None, ) -> metrics.Histogram: return Histogram(name) def create_gauge( # type: ignore # pylint: disable=no-self-use self, name: str, unit: str = "", description: str = "", ) -> metrics._Gauge: # pyright: ignore[reportReturnType] warnings.warn( "create_gauge() is not implemented and will be a no-op", stacklevel=2 ) raise NotImplementedError() def create_observable_gauge( self, name: str, callbacks: Optional[Sequence[CallbackT]] = None, unit: str = "", description: str = "", ) -> metrics.ObservableGauge: raise NotImplementedError() def create_observable_up_down_counter( self, name: str, callbacks: Optional[Sequence[CallbackT]] = None, unit: str = "", description: str = "", ) -> metrics.ObservableUpDownCounter: raise NotImplementedError() class MeterProvider(metrics.MeterProvider): def get_meter( self, name: str, version: Optional[str] = None, schema_url: Optional[str] = None, attributes: Optional[Attributes] = None, ) -> metrics.Meter: return Meter(name, version, schema_url) _TRACER: trace.Tracer = trace.NoOpTracer() def get_monarch_tracer() -> trace.Tracer: """ Return a no-op OTEL tracer for compatibility with older call sites. Prefer `span()` for new code. """ return _TRACER _INSTALLED = False METER: metrics.Meter = Meter("monarch") def install() -> None: global _INSTALLED if _INSTALLED: return metrics.set_meter_provider(MeterProvider()) global METER METER = metrics.get_meter("monarch") _INSTALLED = True