log_utils.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. # log_utils.py -- Logging utilities for Dulwich
  2. # Copyright (C) 2010 Google, Inc.
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Logging utilities for Dulwich.
  22. Any module that uses logging needs to do compile-time initialization to set up
  23. the logging environment. Since Dulwich is also used as a library, clients may
  24. not want to see any logging output. In that case, we need to use a special
  25. handler to suppress spurious warnings like "No handlers could be found for
  26. logger dulwich.foo".
  27. For details on the _NullHandler approach, see:
  28. http://docs.python.org/library/logging.html#configuring-logging-for-a-library
  29. For many modules, the only function from the logging module they need is
  30. getLogger; this module exports that function for convenience. If a calling
  31. module needs something else, it can import the standard logging module
  32. directly.
  33. """
  34. import logging
  35. import os
  36. import sys
  37. from typing import Optional, Union
  38. getLogger = logging.getLogger
  39. class _NullHandler(logging.Handler):
  40. """No-op logging handler to avoid unexpected logging warnings."""
  41. def emit(self, record: logging.LogRecord) -> None:
  42. pass
  43. _NULL_HANDLER = _NullHandler()
  44. _DULWICH_LOGGER = getLogger("dulwich")
  45. _DULWICH_LOGGER.addHandler(_NULL_HANDLER)
  46. def _should_trace() -> bool:
  47. """Check if GIT_TRACE is enabled.
  48. Returns True if tracing should be enabled, False otherwise.
  49. """
  50. trace_value = os.environ.get("GIT_TRACE", "")
  51. if not trace_value or trace_value.lower() in ("0", "false"):
  52. return False
  53. return True
  54. def _get_trace_target() -> Optional[Union[str, int]]:
  55. """Get the trace target from GIT_TRACE environment variable.
  56. Returns:
  57. - None if tracing is disabled
  58. - 2 for stderr output (values "1", "2", "true")
  59. - int (3-9) for file descriptor
  60. - str for file path (absolute paths or directories)
  61. """
  62. trace_value = os.environ.get("GIT_TRACE", "")
  63. if not trace_value or trace_value.lower() in ("0", "false"):
  64. return None
  65. if trace_value.lower() in ("1", "2", "true"):
  66. return 2 # stderr
  67. # Check if it's a file descriptor (integer 3-9)
  68. try:
  69. fd = int(trace_value)
  70. if 3 <= fd <= 9:
  71. return fd
  72. except ValueError:
  73. pass
  74. # If it's an absolute path, return it as a string
  75. if os.path.isabs(trace_value):
  76. return trace_value
  77. # For any other value, treat it as disabled
  78. return None
  79. def _configure_logging_from_trace() -> bool:
  80. """Configure logging based on GIT_TRACE environment variable.
  81. Returns True if trace configuration was successful, False otherwise.
  82. """
  83. trace_target = _get_trace_target()
  84. if trace_target is None:
  85. return False
  86. trace_format = "%(asctime)s %(name)s %(levelname)s: %(message)s"
  87. if trace_target == 2:
  88. # stderr
  89. logging.basicConfig(level=logging.DEBUG, stream=sys.stderr, format=trace_format)
  90. return True
  91. if isinstance(trace_target, int):
  92. # File descriptor
  93. try:
  94. stream = os.fdopen(trace_target, "w", buffering=1)
  95. logging.basicConfig(level=logging.DEBUG, stream=stream, format=trace_format)
  96. return True
  97. except OSError as e:
  98. sys.stderr.write(
  99. f"Warning: Failed to open GIT_TRACE fd {trace_target}: {e}\n"
  100. )
  101. return False
  102. # File path
  103. try:
  104. if os.path.isdir(trace_target):
  105. # For directories, create a file per process
  106. filename = os.path.join(trace_target, f"trace.{os.getpid()}")
  107. else:
  108. filename = trace_target
  109. logging.basicConfig(
  110. level=logging.DEBUG, filename=filename, filemode="a", format=trace_format
  111. )
  112. return True
  113. except OSError as e:
  114. sys.stderr.write(
  115. f"Warning: Failed to open GIT_TRACE file {trace_target}: {e}\n"
  116. )
  117. return False
  118. def default_logging_config() -> None:
  119. """Set up the default Dulwich loggers.
  120. Respects the GIT_TRACE environment variable for trace output:
  121. - If GIT_TRACE is set to "1", "2", or "true", trace to stderr
  122. - If GIT_TRACE is set to an integer 3-9, trace to that file descriptor
  123. - If GIT_TRACE is set to an absolute path, trace to that file
  124. - If the path is a directory, trace to files in that directory (per process)
  125. - Otherwise, use default stderr output
  126. """
  127. remove_null_handler()
  128. # Try to configure from GIT_TRACE, fall back to default if it fails
  129. if not _configure_logging_from_trace():
  130. logging.basicConfig(
  131. level=logging.INFO,
  132. stream=sys.stderr,
  133. format="%(asctime)s %(levelname)s: %(message)s",
  134. )
  135. def remove_null_handler() -> None:
  136. """Remove the null handler from the Dulwich loggers.
  137. If a caller wants to set up logging using something other than
  138. default_logging_config, calling this function first is a minor optimization
  139. to avoid the overhead of using the _NullHandler.
  140. """
  141. _DULWICH_LOGGER.removeHandler(_NULL_HANDLER)