diff --git a/bfxapi/_utils/logger.py b/bfxapi/_utils/logger.py index 6ebac5a..df9c807 100644 --- a/bfxapi/_utils/logger.py +++ b/bfxapi/_utils/logger.py @@ -1,51 +1,67 @@ -import logging, sys +from typing import \ + TYPE_CHECKING, Literal, Optional -BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) +#pylint: disable-next=wildcard-import,unused-wildcard-import +from logging import * -COLOR_SEQ, ITALIC_COLOR_SEQ = "\033[1;%dm", "\033[3;%dm" +from copy import copy -COLORS = { - "DEBUG": CYAN, - "INFO": BLUE, - "WARNING": YELLOW, - "ERROR": RED -} +import sys -RESET_SEQ = "\033[0m" +if TYPE_CHECKING: + _Level = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] -class _ColorFormatter(logging.Formatter): - def __init__(self, msg, use_color = True): - logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S") +_BLACK, _RED, _GREEN, _YELLOW, \ +_BLUE, _MAGENTA, _CYAN, _WHITE = \ + [ f"\033[0;{90 + i}m" for i in range(8) ] - self.use_color = use_color +_BOLD_BLACK, _BOLD_RED, _BOLD_GREEN, _BOLD_YELLOW, \ +_BOLD_BLUE, _BOLD_MAGENTA, _BOLD_CYAN, _BOLD_WHITE = \ + [ f"\033[1;{90 + i}m" for i in range(8) ] - def format(self, record): - levelname = record.levelname - if self.use_color and levelname in COLORS: - record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ - record.levelname = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ - return logging.Formatter.format(self, record) +_NC = "\033[0m" -class ColorLogger(logging.Logger): - FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s" +class _ColorFormatter(Formatter): + __LEVELS = { + "INFO": _BLUE, + "WARNING": _YELLOW, + "ERROR": _RED, + "CRITICAL": _BOLD_RED, + "DEBUG": _BOLD_WHITE + } - def __init__(self, name, level): - logging.Logger.__init__(self, name, level) + def format(self, record: LogRecord) -> str: + _record = copy(record) + _record.name = _MAGENTA + record.name + _NC + _record.levelname = _ColorFormatter.__format_level(record.levelname) - colored_formatter = _ColorFormatter(self.FORMAT, use_color=True) - handler = logging.StreamHandler(stream=sys.stderr) - handler.setFormatter(fmt=colored_formatter) + return super().format(_record) - self.addHandler(hdlr=handler) + #pylint: disable-next=invalid-name + def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str: + return _GREEN + super().formatTime(record, datefmt) + _NC -class FileLogger(logging.Logger): - FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s" + @staticmethod + def __format_level(level: str) -> str: + return _ColorFormatter.__LEVELS[level] + level + _NC - def __init__(self, name, level, filename): - logging.Logger.__init__(self, name, level) +_FORMAT = "%(asctime)s %(name)s %(levelname)s %(message)s" - formatter = logging.Formatter(self.FORMAT) - handler = logging.FileHandler(filename=filename) +_DATE_FORMAT = "%d-%m-%Y %H:%M:%S" + +class ColorLogger(Logger): + __FORMATTER = Formatter(_FORMAT,_DATE_FORMAT) + + def __init__(self, name: str, level: "_Level" = "NOTSET") -> None: + super().__init__(name, level) + + formatter = _ColorFormatter(_FORMAT, _DATE_FORMAT) + + handler = StreamHandler(stream=sys.stderr) handler.setFormatter(fmt=formatter) - + self.addHandler(hdlr=handler) + + def register(self, filename: str) -> None: + handler = FileHandler(filename=filename) + handler.setFormatter(fmt=ColorLogger.__FORMATTER) self.addHandler(hdlr=handler) diff --git a/bfxapi/client.py b/bfxapi/client.py index 5e5292d..4a7bbdf 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,8 +1,10 @@ from typing import List, Literal, Optional -from .rest import BfxRestInterface -from .websocket import BfxWebSocketClient -from .urls import REST_HOST, WSS_HOST +from bfxapi._utils.logger import ColorLogger + +from bfxapi.rest import BfxRestInterface +from bfxapi.websocket import BfxWebSocketClient +from bfxapi.urls import REST_HOST, WSS_HOST class Client: def __init__( @@ -15,10 +17,14 @@ class Client: filters: Optional[List[str]] = None, wss_timeout: Optional[float] = 60 * 15, log_filename: Optional[str] = None, - log_level: Literal["ERROR", "WARNING", "INFO", "DEBUG"] = "INFO" + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" ) -> None: + logger = ColorLogger("bfxapi", level=log_level) + + if log_filename: + logger.register(filename=log_filename) + self.rest = BfxRestInterface(rest_host, api_key, api_secret) self.wss = BfxWebSocketClient(wss_host, api_key, api_secret, - filters=filters, wss_timeout=wss_timeout, log_filename=log_filename, - log_level=log_level) + filters=filters, wss_timeout=wss_timeout, logger=logger) diff --git a/bfxapi/websocket/_client/bfx_websocket_client.py b/bfxapi/websocket/_client/bfx_websocket_client.py index 8ef5490..8ff481f 100644 --- a/bfxapi/websocket/_client/bfx_websocket_client.py +++ b/bfxapi/websocket/_client/bfx_websocket_client.py @@ -1,8 +1,9 @@ from typing import \ TYPE_CHECKING, TypeVar, TypedDict,\ - Callable, Optional, Literal,\ - Tuple, List, Dict, \ - Any + Callable, Optional, Tuple, \ + List, Dict, Any + +from logging import Logger from datetime import datetime @@ -19,9 +20,6 @@ from websockets.legacy.client import connect as _websockets__connect from bfxapi._utils.json_encoder import JSONEncoder -from bfxapi._utils.logger import \ - ColorLogger, FileLogger - from bfxapi.websocket._handlers import \ PublicChannelsHandler, AuthEventsHandler @@ -38,8 +36,6 @@ from .bfx_websocket_bucket import BfxWebSocketBucket from .bfx_websocket_inputs import BfxWebSocketInputs if TYPE_CHECKING: - from logging import Logger - from asyncio import Task _T = TypeVar("_T", bound=Callable[..., None]) @@ -50,6 +46,8 @@ if TYPE_CHECKING: _Reconnection = TypedDict("_Reconnection", { "attempts": int, "reason": str, "timestamp": datetime }) +_DEFAULT_LOGGER = Logger("bfxapi.websocket._client", level=0) + class BfxWebSocketClient(Connection, Connection.Authenticable): VERSION = BfxWebSocketBucket.VERSION @@ -69,22 +67,14 @@ class BfxWebSocketClient(Connection, Connection.Authenticable): def __init__(self, host: str, - api_key: Optional[str] = None, - api_secret: Optional[str] = None, *, - filters: Optional[List[str]] = None, - wss_timeout: Optional[float] = 60 * 15, - log_filename: Optional[str] = None, - log_level: Literal["ERROR", "WARNING", "INFO", "DEBUG"] = "INFO") -> None: + credentials: Optional["_Credentials"] = None, + timeout: Optional[float] = 60 * 15, + logger: Logger = _DEFAULT_LOGGER) -> None: super().__init__(host) - self.__credentials: Optional["_Credentials"] = None - - if api_key and api_secret: - self.__credentials = \ - { "api_key": api_key, "api_secret": api_secret, "filters": filters } - - self.__wss_timeout = wss_timeout + self.__credentials, self.__timeout, self.__logger = \ + credentials, timeout, logger self.__event_emitter = BfxEventEmitter(targets = \ PublicChannelsHandler.ONCE_PER_SUBSCRIPTION + \ @@ -100,16 +90,15 @@ class BfxWebSocketClient(Connection, Connection.Authenticable): self.__reconnection: Optional[_Reconnection] = None - self.__logger: "Logger" + @self.__event_emitter.on("error") + def error(exception: Exception) -> None: + header = f"{type(exception).__name__}: {str(exception)}" - if log_filename is None: - self.__logger = ColorLogger("BfxWebSocketClient", level=log_level) - else: self.__logger = FileLogger("BfxWebSocketClient", level=log_level, filename=log_filename) + stack_trace = traceback.format_exception( \ + type(exception), exception, exception.__traceback__) - self.__event_emitter.add_listener("error", - lambda exception: self.__logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" + - str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) - ) + self.__logger.critical( \ + header + "\n" + str().join(stack_trace)[:-1]) @property def inputs(self) -> BfxWebSocketInputs: @@ -166,7 +155,7 @@ class BfxWebSocketClient(Connection, Connection.Authenticable): _sleep: Optional["Task"] = None - def _on_wss_timeout(): + def _on_timeout(): if not self.open: if _sleep: _sleep.cancel() @@ -180,7 +169,7 @@ class BfxWebSocketClient(Connection, Connection.Authenticable): await _sleep except asyncio.CancelledError: raise ReconnectionTimeoutError("Connection has been offline for too long " \ - f"without being able to reconnect (wss_timeout: {self.__wss_timeout}s).") \ + f"without being able to reconnect (timeout: {self.__timeout}s).") \ from None try: @@ -212,9 +201,9 @@ class BfxWebSocketClient(Connection, Connection.Authenticable): self.__logger.info("WSS server is about to restart, clients need " \ "to reconnect (server sent 20051). Reconnection attempt in progress...") - if self.__wss_timeout is not None: + if self.__timeout is not None: asyncio.get_event_loop().call_later( - self.__wss_timeout, _on_wss_timeout) + self.__timeout, _on_timeout) self.__reconnection = \ { "attempts": 1, "reason": error.reason, "timestamp": datetime.now() }