# Copyright cocotb contributors
# Copyright (c) 2013, 2018 Potential Ventures Ltd
# Copyright (c) 2013 SolarFlare Communications Inc
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause
"""
Everything related to logging
"""
import logging
import os
import sys
from typing import TYPE_CHECKING, Union
from cocotb import _ANSI, simulator
from cocotb._deprecation import deprecated
from cocotb._utils import want_color_output
from cocotb.utils import get_sim_time, get_time_from_sim_steps
try:
_suppress = int(os.environ.get("COCOTB_REDUCED_LOG_FMT", "1"))
except ValueError:
_suppress = 1
# Column alignment
_LEVEL_CHARS = len("CRITICAL")
_RECORD_CHARS = 34
_FILENAME_CHARS = 20
_LINENO_CHARS = 4
_FUNCNAME_CHARS = 31
# Custom log level
logging.TRACE = 5 # type: ignore[attr-defined] # type checkers don't like adding module attributes after the fact
logging.addLevelName(5, "TRACE")
[docs]
def default_config() -> None:
"""Apply the default cocotb log formatting to the root logger.
This hooks up the logger to write to stdout, using either
:class:`SimColourLogFormatter` or :class:`SimLogFormatter` depending
on whether colored output is requested. It also adds a
:class:`SimTimeContextFilter` filter so that
:attr:`~logging.LogRecord.created_sim_time` is available to the formatter.
The logging level for cocotb logs is set based on the
:envvar:`COCOTB_LOG_LEVEL` environment variable, which defaults to ``INFO``.
The logging level for GPI logs is set based on the
:envvar:`GPI_LOG_LEVEL` environment variable, which defaults to ``INFO``.
If desired, this logging configuration can be overwritten by calling
``logging.basicConfig(..., force=True)`` (in Python 3.8 onwards), or by
manually resetting the root logger instance.
An example of this can be found in the section on :ref:`rotating-logger`.
.. versionadded:: 1.4
"""
# construct an appropriate handler
hdlr = logging.StreamHandler(sys.stdout)
hdlr.addFilter(SimTimeContextFilter())
if want_color_output():
hdlr.setFormatter(SimColourLogFormatter())
else:
hdlr.setFormatter(SimLogFormatter())
logging.setLoggerClass(SimBaseLog)
logging.basicConfig()
logging.getLogger().handlers = [hdlr] # overwrite default handlers
def set_level(logger_name: str, envvar: str, default_level: str) -> None:
log_level = os.environ.get(envvar, default_level)
log_level = log_level.upper()
logger = logging.getLogger(logger_name)
try:
logger.setLevel(log_level)
except ValueError:
valid_levels = ", ".join(
("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE", "NOTSET")
)
raise ValueError(
f"Invalid log level {log_level!r} passed through the "
f"{envvar} environment variable. Valid log "
f"levels: {valid_levels}"
)
set_level("gpi", "GPI_LOG_LEVEL", "INFO")
set_level("cocotb", "COCOTB_LOG_LEVEL", "INFO")
if TYPE_CHECKING:
LoggerClass = logging.Logger
else:
LoggerClass = logging.getLoggerClass()
class SimBaseLog(LoggerClass):
# For hooking setLevel to inform the GPI's local log level
def setLevel(self, level: Union[int, str]) -> None:
super().setLevel(level)
if self.name == "gpi":
simulator.set_gpi_log_level(self.getEffectiveLevel())
@deprecated('Use `logging.getLogger(f"{name}.0x{ident:x}")` instead')
def SimLog(name: str, ident: Union[int, None] = None) -> logging.Logger:
"""Like logging.getLogger, but append a numeric identifier to the name.
Args:
name: Logger name.
ident: Unique integer identifier.
Returns:
The Logger named ``{name}.0x{ident:x}``.
.. deprecated:: 2.0
Use ``logging.getLogger(f"{name}.0x{ident:x}")`` instead.
"""
if ident is not None:
name = f"{name}.0x{ident:x}"
return logging.getLogger(name)
[docs]
class SimTimeContextFilter(logging.Filter):
"""
A filter to inject simulator times into the log records.
This uses the approach described in the :ref:`Python logging cookbook <python:filters-contextual>`.
This adds the :attr:`~logging.LogRecord.created_sim_time` attribute.
.. versionadded:: 1.4
"""
# needed to make our docs render well
def __init__(self) -> None:
""""""
super().__init__()
def filter(self, record: logging.LogRecord) -> bool:
try:
record.created_sim_time = get_sim_time()
except RecursionError:
# get_sim_time may try to log - if that happens, we can't
# attach a simulator time to this message.
record.created_sim_time = None
return True
def _log_from_c(
logger: logging.Logger,
level: int,
filename: str,
lineno: int,
msg: str,
function_name: str,
) -> None:
"""
This is for use from the C world, and allows us to insert C stack
information.
"""
if logger.isEnabledFor(level):
record = logger.makeRecord(
logger.name, level, filename, lineno, msg, (), None, function_name
)
logger.handle(record)