Skip to content

Commit 09cb85b

Browse files
committed
[Logging] put the actor name in the python logger
Injects [actor=<the fully qualified actor name>] into the log message of the python actor. The prefix is configurable by redefining `monarch.actor.per_actor_logging_prefix` callback. Setting to None disables the filter that alters the logs. Hopefully this works pretty broadly since it hooks in as a filter on logging handlers. Differential Revision: [D85994688](https://our.internmc.facebook.com/intern/diff/D85994688/) **NOTE FOR REVIEWERS**: This PR has internal Meta-specific changes or comments, please review them on [Phabricator](https://our.internmc.facebook.com/intern/diff/D85994688/)! ghstack-source-id: 321422564 Pull Request resolved: #1767
1 parent aec5fb8 commit 09cb85b

File tree

3 files changed

+94
-5
lines changed

3 files changed

+94
-5
lines changed

python/monarch/_src/actor/actor_mesh.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
import collections
1111
import contextvars
1212
import functools
13+
import importlib
1314
import inspect
1415
import itertools
1516
import logging
1617
import threading
1718
from abc import abstractproperty
1819

1920
from dataclasses import dataclass
21+
22+
from functools import cache
2023
from pprint import pformat
2124
from textwrap import indent
2225
from traceback import TracebackException
@@ -257,6 +260,51 @@ def _root_client_context() -> "Context": ...
257260
"monarch.actor_mesh._context"
258261
)
259262

263+
264+
@cache
265+
def _monarch_actor() -> Any:
266+
return importlib.import_module("monarch.actor")
267+
268+
269+
class _ActorFilter(logging.Filter):
270+
def __init__(self) -> None:
271+
super().__init__()
272+
273+
def filter(self, record: Any) -> bool:
274+
fn = _monarch_actor().per_actor_logging_prefix
275+
ctx = _context.get(None)
276+
if ctx is not None and fn is not None:
277+
record.msg = fn(ctx.actor_instance) + record.msg
278+
return True
279+
280+
281+
def per_actor_logging_prefix(instance: Instance | CreatorInstance) -> str:
282+
return f"[actor={instance}] "
283+
284+
285+
@cache
286+
def _init_context_log_handler() -> None:
287+
af: _ActorFilter = _ActorFilter()
288+
logger = logging.getLogger()
289+
for handler in logger.handlers:
290+
handler.addFilter(af)
291+
292+
_original_addHandler: Any = logging.Logger.addHandler
293+
294+
def _patched_addHandler(self: logging.Logger, hdlr: logging.Handler) -> None:
295+
_original_addHandler(self, hdlr)
296+
if af not in hdlr.filters:
297+
hdlr.addFilter(af)
298+
299+
# pyre-ignore[8]: Intentionally monkey-patching Logger.addHandler
300+
logging.Logger.addHandler = _patched_addHandler
301+
302+
303+
def _set_context(c: Context) -> None:
304+
_init_context_log_handler()
305+
_context.set(c)
306+
307+
260308
T = TypeVar("T")
261309

262310

@@ -305,7 +353,7 @@ def context() -> Context:
305353
c = _context.get(None)
306354
if c is None:
307355
c = Context._root_client_context()
308-
_context.set(c)
356+
_set_context(c)
309357

310358
from monarch._src.actor.host_mesh import create_local_host_mesh
311359
from monarch._src.actor.proc_mesh import _get_controller_controller
@@ -919,7 +967,7 @@ async def handle(
919967
# response_port can be None. If so, then sending to port will drop the response,
920968
# and raise any exceptions to the caller.
921969
try:
922-
_context.set(ctx)
970+
_set_context(ctx)
923971

924972
DebugContext.set(DebugContext())
925973

@@ -1053,7 +1101,7 @@ def _post_mortem_debug(self, exc_tb: Any) -> None:
10531101
def _handle_undeliverable_message(
10541102
self, cx: Context, message: UndeliverableMessageEnvelope
10551103
) -> bool:
1056-
_context.set(cx)
1104+
_set_context(cx)
10571105
handle_undeliverable = getattr(
10581106
self.instance, "_handle_undeliverable_message", None
10591107
)
@@ -1063,7 +1111,7 @@ def _handle_undeliverable_message(
10631111
return False
10641112

10651113
def __supervise__(self, cx: Context, *args: Any, **kwargs: Any) -> object:
1066-
_context.set(cx)
1114+
_set_context(cx)
10671115
instance = self.instance
10681116
if instance is None:
10691117
# This could happen because of the following reasons. Both

python/monarch/actor/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
current_size,
2828
enable_transport,
2929
Endpoint,
30+
per_actor_logging_prefix,
3031
Point,
3132
Port,
3233
PortReceiver,
@@ -107,5 +108,6 @@
107108
"Context",
108109
"ChannelTransport",
109110
"unhandled_fault_hook",
111+
"per_actor_logging_prefix",
110112
"MeshFailure",
111113
]

python/tests/test_python_actors.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import asyncio
1010
import ctypes
1111
import importlib.resources
12+
import io
1213
import logging
1314
import operator
1415
import os
@@ -20,6 +21,7 @@
2021
import time
2122
import unittest
2223
import unittest.mock
24+
from contextlib import contextmanager
2325
from tempfile import TemporaryDirectory
2426
from types import ModuleType
2527
from typing import Any, cast, Tuple
@@ -68,7 +70,6 @@
6870
from monarch.tools.config import defaults
6971
from typing_extensions import assert_type
7072

71-
7273
needs_cuda = pytest.mark.skipif(
7374
not torch.cuda.is_available(),
7475
reason="CUDA not available",
@@ -1733,9 +1734,31 @@ def test_setup_async() -> None:
17331734
time.sleep(10)
17341735

17351736

1737+
class CaptureLogs:
1738+
def __init__(self):
1739+
log_stream = io.StringIO()
1740+
handler = logging.StreamHandler(log_stream)
1741+
handler.setFormatter(logging.Formatter("%(message)s"))
1742+
1743+
logger = logging.getLogger("capture")
1744+
logger.setLevel(logging.INFO)
1745+
logger.addHandler(handler)
1746+
1747+
self.log_stream = log_stream
1748+
self.logger = logger
1749+
1750+
@property
1751+
def contents(self) -> str:
1752+
return self.log_stream.getvalue()
1753+
1754+
17361755
class Named(Actor):
17371756
@endpoint
17381757
def report(self) -> Any:
1758+
logs = CaptureLogs()
1759+
logs.logger.error("HUH")
1760+
assert "test_python_actors.Named the_name{'f': 0/2}>" in logs.contents
1761+
17391762
return context().actor_instance.creator, str(context().actor_instance)
17401763

17411764

@@ -1751,3 +1774,19 @@ def test_instance_name():
17511774
assert "test_python_actors.Named the_name{'f': 0/2}>" in result
17521775
assert cr.name == "root"
17531776
assert str(context().actor_instance) == "<root>"
1777+
1778+
logs = CaptureLogs()
1779+
logs.logger.error("HUH")
1780+
assert "actor=<root>" in logs.contents
1781+
default = monarch.actor.per_actor_logging_prefix
1782+
try:
1783+
monarch.actor.per_actor_logging_prefix = lambda inst: "<test>"
1784+
logs = CaptureLogs()
1785+
logs.logger.error("HUH")
1786+
assert "<test>" in logs.contents
1787+
monarch.actor.per_actor_logging_prefix = None
1788+
# make sure we can set _per_actor_logging_prefix to none.
1789+
logs = CaptureLogs()
1790+
logs.logger.error("HUH")
1791+
finally:
1792+
monarch.actor.per_actor_logging_prefix = default

0 commit comments

Comments
 (0)