wb_logging.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Logging configuration for the "wandb" logger.
  2. Most log statements in wandb are made in the context of a run and should be
  3. redirected to that run's log file (usually named 'debug.log'). This module
  4. provides a context manager to temporarily set the current run ID and registers
  5. a global handler for the 'wandb' logger that sends log statements to the right
  6. place.
  7. All functions in this module are threadsafe.
  8. NOTE: The pytest caplog fixture will fail to capture logs from the wandb logger
  9. because they are not propagated to the root logger.
  10. """
  11. from __future__ import annotations
  12. import contextlib
  13. import contextvars
  14. import logging
  15. import pathlib
  16. from collections.abc import Iterator
  17. class _NotRunSpecific:
  18. """Sentinel for `not_run_specific()`."""
  19. _NOT_RUN_SPECIFIC = _NotRunSpecific()
  20. _run_id: contextvars.ContextVar[str | _NotRunSpecific | None] = contextvars.ContextVar(
  21. "_run_id",
  22. default=None,
  23. )
  24. _logger = logging.getLogger("wandb")
  25. def configure_wandb_logger() -> None:
  26. """Configures the global 'wandb' logger.
  27. The wandb logger is not intended to be customized by users. Instead, it is
  28. used as a mechanism to redirect log messages into wandb run-specific log
  29. files.
  30. This function is idempotent: calling it multiple times has the same effect.
  31. """
  32. # Send all DEBUG and above messages to registered handlers.
  33. #
  34. # Per-run handlers can set different levels.
  35. _logger.setLevel(logging.DEBUG)
  36. # Do not propagate wandb logs to the root logger, which the user may have
  37. # configured to point elsewhere. All wandb log messages should go to a run's
  38. # log file.
  39. _logger.propagate = False
  40. # If no handlers are configured for the 'wandb' logger, don't activate the
  41. # "lastResort" handler which sends messages to stderr with a level of
  42. # WARNING by default.
  43. #
  44. # This occurs in wandb code that runs outside the context of a Run and
  45. # not as part of the CLI.
  46. #
  47. # Most such code uses the `termlog` / `termwarn` / `termerror` methods
  48. # to communicate with the user. When that code executes while a run is
  49. # active, its logger messages go to that run's log file.
  50. if not _logger.handlers:
  51. _logger.addHandler(logging.NullHandler())
  52. @contextlib.contextmanager
  53. def log_to_run(run_id: str | None) -> Iterator[None]:
  54. """Direct all wandb log messages to the given run.
  55. Args:
  56. id: The current run ID, or None if actions in the context manager are
  57. not associated to a specific run. In the latter case, log messages
  58. will go to all runs.
  59. Usage:
  60. with wb_logging.run_id(...):
  61. ... # Log messages here go to the specified run's logger.
  62. """
  63. token = _run_id.set(run_id)
  64. try:
  65. yield
  66. finally:
  67. _run_id.reset(token)
  68. @contextlib.contextmanager
  69. def log_to_all_runs() -> Iterator[None]:
  70. """Direct wandb log messages to all runs.
  71. Unlike `log_to_run(None)`, this indicates an intentional choice.
  72. This is often convenient to use as a decorator:
  73. @wb_logging.log_to_all_runs()
  74. def my_func():
  75. ... # Log messages here go to the specified run's logger.
  76. """
  77. token = _run_id.set(_NOT_RUN_SPECIFIC)
  78. try:
  79. yield
  80. finally:
  81. _run_id.reset(token)
  82. def add_file_handler(run_id: str, filepath: pathlib.Path) -> logging.Handler:
  83. """Direct log messages for a run to a file.
  84. Args:
  85. run_id: The run for which to create a log file.
  86. filepath: The file to write log messages to.
  87. Returns:
  88. The added handler which can then be configured further or removed
  89. from the 'wandb' logger directly.
  90. The default logging level is INFO.
  91. """
  92. handler = logging.FileHandler(filepath)
  93. handler.setLevel(logging.INFO)
  94. handler.addFilter(_RunIDFilter(run_id))
  95. handler.setFormatter(
  96. logging.Formatter(
  97. "%(asctime)s %(levelname)-7s %(threadName)-10s:%(process)d"
  98. " [%(filename)s:%(funcName)s():%(lineno)s]%(run_id_tag)s"
  99. " %(message)s"
  100. )
  101. )
  102. _logger.addHandler(handler)
  103. return handler
  104. class _RunIDFilter:
  105. """Filters out messages logged for a different run."""
  106. def __init__(self, run_id: str) -> None:
  107. """Create a _RunIDFilter.
  108. Args:
  109. run_id: Allows messages when the run ID is this or None.
  110. """
  111. self._run_id = run_id
  112. def filter(self, record: logging.LogRecord) -> bool:
  113. """Modify a log record and return whether it matches the run."""
  114. run_id = _run_id.get()
  115. if run_id is None:
  116. record.run_id_tag = " [no run ID]"
  117. return True
  118. elif isinstance(run_id, _NotRunSpecific):
  119. record.run_id_tag = " [all runs]"
  120. return True
  121. else:
  122. record.run_id_tag = ""
  123. return run_id == self._run_id