ayon-core/client/ayon_core/lib/log.py
2024-11-01 12:45:13 +01:00

249 lines
6.9 KiB
Python

import os
import sys
import getpass
import logging
import platform
import socket
import time
import threading
import copy
from . import Terminal
class LogStreamHandler(logging.StreamHandler):
"""StreamHandler class.
This was originally designed to handle UTF errors in python 2.x hosts,
however currently solely remains for backwards compatibility.
"""
def __init__(self, stream=None):
super(LogStreamHandler, self).__init__(stream)
self.enabled = True
def enable(self):
"""Enable StreamHandler
Make StreamHandler output again
"""
self.enabled = True
def disable(self):
"""Disable StreamHandler
Used to silence output
"""
self.enabled = False
def emit(self, record):
if not self.enabled or self.stream is None:
return
try:
msg = self.format(record)
msg = Terminal.log(msg)
stream = self.stream
stream.write(f"{msg}\n")
self.flush()
except (KeyboardInterrupt, SystemExit):
raise
except OSError:
self.handleError(record)
except Exception:
print(repr(record))
self.handleError(record)
class LogFormatter(logging.Formatter):
DFT = '%(levelname)s >>> { %(name)s }: [ %(message)s ]'
default_formatter = logging.Formatter(DFT)
def __init__(self, formats):
super(LogFormatter, self).__init__()
self.formatters = {}
for loglevel in formats:
self.formatters[loglevel] = logging.Formatter(formats[loglevel])
def format(self, record):
formatter = self.formatters.get(record.levelno, self.default_formatter)
_exc_info = record.exc_info
record.exc_info = None
out = formatter.format(record)
record.exc_info = _exc_info
if record.exc_info is not None:
line_len = len(str(record.exc_info[1]))
if line_len > 30:
line_len = 30
out = "{}\n{}\n{}\n{}\n{}".format(
out,
line_len * "=",
str(record.exc_info[1]),
line_len * "=",
self.formatException(record.exc_info)
)
return out
class Logger:
DFT = '%(levelname)s >>> { %(name)s }: [ %(message)s ] '
DBG = " - { %(name)s }: [ %(message)s ] "
INF = ">>> [ %(message)s ] "
WRN = "*** WRN: >>> { %(name)s }: [ %(message)s ] "
ERR = "!!! ERR: %(asctime)s >>> { %(name)s }: [ %(message)s ] "
CRI = "!!! CRI: %(asctime)s >>> { %(name)s }: [ %(message)s ] "
FORMAT_FILE = {
logging.INFO: INF,
logging.DEBUG: DBG,
logging.WARNING: WRN,
logging.ERROR: ERR,
logging.CRITICAL: CRI,
}
# Is static class initialized
initialized = False
_init_lock = threading.Lock()
# Logging level - AYON_LOG_LEVEL
log_level = None
# Data same for all record documents
process_data = None
# Cached process name or ability to set different process name
_process_name = None
@classmethod
def get_logger(cls, name=None):
if not cls.initialized:
cls.initialize()
logger = logging.getLogger(name or "__main__")
logger.setLevel(cls.log_level)
add_console_handler = True
for handler in logger.handlers:
if isinstance(handler, LogStreamHandler):
add_console_handler = False
if add_console_handler:
logger.addHandler(cls._get_console_handler())
# Do not propagate logs to root logger
logger.propagate = False
return logger
@classmethod
def _get_console_handler(cls):
formatter = LogFormatter(cls.FORMAT_FILE)
console_handler = LogStreamHandler()
console_handler.set_name("LogStreamHandler")
console_handler.setFormatter(formatter)
return console_handler
@classmethod
def initialize(cls):
# TODO update already created loggers on re-initialization
if not cls._init_lock.locked():
with cls._init_lock:
cls._initialize()
else:
# If lock is locked wait until is finished
while cls._init_lock.locked():
time.sleep(0.1)
@classmethod
def _initialize(cls):
# Change initialization state to prevent runtime changes
# if is executed during runtime
cls.initialized = False
# Define what is logging level
log_level = os.getenv("AYON_LOG_LEVEL")
if not log_level:
# Check AYON_DEBUG for debug level
op_debug = os.getenv("AYON_DEBUG")
if op_debug and int(op_debug) > 0:
log_level = 10
else:
log_level = 20
cls.log_level = int(log_level)
# Mark as initialized
cls.initialized = True
@classmethod
def get_process_data(cls):
"""Data about current process which should be same for all records.
Process data are used for each record sent to mongo database.
"""
if cls.process_data is not None:
return copy.deepcopy(cls.process_data)
if not cls.initialized:
cls.initialize()
host_name = socket.gethostname()
try:
host_ip = socket.gethostbyname(host_name)
except socket.gaierror:
host_ip = "127.0.0.1"
process_name = cls.get_process_name()
cls.process_data = {
"hostname": host_name,
"hostip": host_ip,
"username": getpass.getuser(),
"system_name": platform.system(),
"process_name": process_name
}
return copy.deepcopy(cls.process_data)
@classmethod
def set_process_name(cls, process_name):
"""Set process name for mongo logs."""
# Just change the attribute
cls._process_name = process_name
# Update process data if are already set
if cls.process_data is not None:
cls.process_data["process_name"] = process_name
@classmethod
def get_process_name(cls):
"""Process name that is like "label" of a process.
AYON logging can be used from OpenPyppe itself of from hosts.
Even in AYON process it's good to know if logs are from tray or
from other cli commands. This should help to identify that information.
"""
if cls._process_name is not None:
return cls._process_name
# Get process name
process_name = os.environ.get("AYON_APP_NAME")
if not process_name:
try:
import psutil
process = psutil.Process(os.getpid())
process_name = process.name()
except ImportError:
pass
if not process_name:
process_name = os.path.basename(sys.executable)
cls._process_name = process_name
return cls._process_name