# Copyright 2024 The etils Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Logging utils.""" import functools import logging as py_logging import sys from etils import epy from etils.epy import _internal with _internal.check_missing_deps(): # pylint: disable=g-import-not-at-top from absl import app from absl import flags from absl import logging as absl_logging # pylint: enable=g-import-not-at-top FLAGS = flags.FLAGS class TqdmStream: """File-object-like abstraction which wrap`tqdm.write`. By default using `logging.info` inside a `tqdm` scope creates visual artifacts. This simple wrapper uses `tqdm.write` instead. Usage: ```python logger = logging.getLogger() logger.addHandler(logging.StreamHandler(TqdmStream())) for _ in tqdm.tqdm(range(10)): logger.info('No visual artifacts') ``` """ def write(self, x: str) -> None: import tqdm # pylint: disable=g-import-not-at-top # pytype: disable=import-error tqdm.tqdm.write(x, end='') def flush(self) -> None: pass def close(self) -> None: pass def _better_logging() -> None: """Modify Python logging (internal).""" # If `absl.run` was not called (e.g. open source `pytest` tests) if not FLAGS.is_parsed(): return # User explicitly set --logtostderr, use default behavior if FLAGS.logtostderr or FLAGS.alsologtostderr: return # Display logs by default absl_logging.use_python_logging(quiet=True) file_link = '{filename}:{lineno}' # Using cleaner, less verbose logger formatter = py_logging.Formatter( # Only display single letter level (`INFO`, `DEBUG`,... -> `I`, `D`,...) f'{{levelname:1.1}} {{asctime}} [{file_link}]: {{message}}', # Do not display date by default (take a lot of space and is almost # never important locally. # Also milliseconds feel overkill datefmt='%H:%M:%S', style='{', ) python_handler = absl_logging.get_absl_handler().python_handler python_handler.setFormatter(formatter) if 'tqdm' in sys.modules: # Replace `sys.stderr` by the TQDM file # This avoid visual artifacts when `logging.info` is used inside # a `tqdm.tqdm` context. python_handler.setStream(TqdmStream()) def _terminal_link(uri: str, text: str) -> str: """Returns a clickable link on the terminal.""" parameters = '' # OSC 8 ; params ; URI ST OSC 8 ;; ST return f'\033]8;{parameters};{uri}\033\\{text}\033]8;;\033\\' def _new_factory(old_factory, *args, **kwargs) -> py_logging.LogRecord: """Update the logs.""" # TODO(epot): Add color ? record = old_factory(*args, **kwargs) return record def better_logging(): """Improve Python logging when running locally. * Display Python logs by default (even when user forgot `--logtostderr`), without being polluted by hundreds of C++ logs. * Cleaner minimal log format (e.g. `I 15:04:05 [main.py:24]:`) * Avoid visual artifacts between TQDM & `logging` * Clickable hyperlinks redirecting to code search (require terminal support) Usage: ```python if __name__ == '__main__': eapp.better_logging() app.run(main) ``` Note this has only effect when user run locally and without `--logtostderr`. """ app.call_after_init(_better_logging)