Browse Source

Add utils.setup_logging to help set up logging outside of Client.run

pull/8351/head
Rapptz 3 years ago
parent
commit
2bf2bfc9b4
  1. 95
      discord/client.py
  2. 117
      discord/utils.py
  3. 2
      docs/api.rst
  4. 11
      docs/logging.rst
  5. 3
      examples/advanced_startup.py

95
discord/client.py

@ -27,8 +27,6 @@ from __future__ import annotations
import asyncio import asyncio
import datetime import datetime
import logging import logging
import sys
import os
from typing import ( from typing import (
Any, Any,
AsyncIterator, AsyncIterator,
@ -113,61 +111,6 @@ class _LoopSentinel:
_loop: Any = _LoopSentinel() _loop: Any = _LoopSentinel()
def stream_supports_colour(stream: Any) -> bool:
is_a_tty = hasattr(stream, 'isatty') and stream.isatty()
if sys.platform != 'win32':
return is_a_tty
# ANSICON checks for things like ConEmu
# WT_SESSION checks if this is Windows Terminal
# VSCode built-in terminal supports colour too
return is_a_tty and ('ANSICON' in os.environ or 'WT_SESSION' in os.environ or os.environ.get('TERM_PROGRAM') == 'vscode')
class _ColourFormatter(logging.Formatter):
# ANSI codes are a bit weird to decipher if you're unfamiliar with them, so here's a refresher
# It starts off with a format like \x1b[XXXm where XXX is a semicolon separated list of commands
# The important ones here relate to colour.
# 30-37 are black, red, green, yellow, blue, magenta, cyan and white in that order
# 40-47 are the same except for the background
# 90-97 are the same but "bright" foreground
# 100-107 are the same as the bright ones but for the background.
# 1 means bold, 2 means dim, 0 means reset, and 4 means underline.
LEVEL_COLOURS = [
(logging.DEBUG, '\x1b[40;1m'),
(logging.INFO, '\x1b[34;1m'),
(logging.WARNING, '\x1b[33;1m'),
(logging.ERROR, '\x1b[31m'),
(logging.CRITICAL, '\x1b[41m'),
]
FORMATS = {
level: logging.Formatter(
f'\x1b[30;1m%(asctime)s\x1b[0m {colour}%(levelname)-8s\x1b[0m \x1b[35m%(name)s\x1b[0m %(message)s',
'%Y-%m-%d %H:%M:%S',
)
for level, colour in LEVEL_COLOURS
}
def format(self, record):
formatter = self.FORMATS.get(record.levelno)
if formatter is None:
formatter = self.FORMATS[logging.DEBUG]
# Override the traceback to always print in red
if record.exc_info:
text = formatter.formatException(record.exc_info)
record.exc_text = f'\x1b[31m{text}\x1b[0m'
output = formatter.format(record)
# Remove the cache layer
record.exc_text = None
return output
class Client: class Client:
r"""Represents a client connection that connects to Discord. r"""Represents a client connection that connects to Discord.
This class is used to interact with the Discord WebSocket and API. This class is used to interact with the Discord WebSocket and API.
@ -858,10 +801,6 @@ class Client:
The default log level for the library's logger. This is only applied if the The default log level for the library's logger. This is only applied if the
``log_handler`` parameter is not ``None``. Defaults to ``logging.INFO``. ``log_handler`` parameter is not ``None``. Defaults to ``logging.INFO``.
Note that the *root* logger will always be set to ``logging.INFO`` and this
only controls the library's log level. To control the root logger's level,
you can use ``logging.getLogger().setLevel(level)``.
.. versionadded:: 2.0 .. versionadded:: 2.0
root_logger: :class:`bool` root_logger: :class:`bool`
Whether to set up the root logger rather than the library logger. Whether to set up the root logger rather than the library logger.
@ -877,32 +816,13 @@ class Client:
async with self: async with self:
await self.start(token, reconnect=reconnect) await self.start(token, reconnect=reconnect)
if log_level is MISSING:
log_level = logging.INFO
if log_handler is MISSING:
log_handler = logging.StreamHandler()
if log_formatter is MISSING:
if isinstance(log_handler, logging.StreamHandler) and stream_supports_colour(log_handler.stream):
log_formatter = _ColourFormatter()
else:
dt_fmt = '%Y-%m-%d %H:%M:%S'
log_formatter = logging.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{')
logger = None
if log_handler is not None: if log_handler is not None:
library, _, _ = __name__.partition('.') utils.setup_logging(
logger = logging.getLogger(library) handler=log_handler,
formatter=log_formatter,
log_handler.setFormatter(log_formatter) level=log_level,
logger.setLevel(log_level) root=root_logger,
logger.addHandler(log_handler) )
if root_logger:
logger = logging.getLogger()
logger.setLevel(log_level)
logger.addHandler(log_handler)
try: try:
asyncio.run(runner()) asyncio.run(runner())
@ -911,9 +831,6 @@ class Client:
# `asyncio.run` handles the loop cleanup # `asyncio.run` handles the loop cleanup
# and `self.start` closes all sockets and the HTTPClient instance. # and `self.start` closes all sockets and the HTTPClient instance.
return return
finally:
if log_handler is not None and logger is not None:
logger.removeHandler(log_handler)
# properties # properties

117
discord/utils.py

@ -63,9 +63,11 @@ from operator import attrgetter
from urllib.parse import urlencode from urllib.parse import urlencode
import json import json
import re import re
import os
import sys import sys
import types import types
import warnings import warnings
import logging
import yarl import yarl
@ -91,6 +93,7 @@ __all__ = (
'as_chunks', 'as_chunks',
'format_dt', 'format_dt',
'MISSING', 'MISSING',
'setup_logging',
) )
DISCORD_EPOCH = 1420070400000 DISCORD_EPOCH = 1420070400000
@ -1180,3 +1183,117 @@ def format_dt(dt: datetime.datetime, /, style: Optional[TimestampStyle] = None)
if style is None: if style is None:
return f'<t:{int(dt.timestamp())}>' return f'<t:{int(dt.timestamp())}>'
return f'<t:{int(dt.timestamp())}:{style}>' return f'<t:{int(dt.timestamp())}:{style}>'
def stream_supports_colour(stream: Any) -> bool:
is_a_tty = hasattr(stream, 'isatty') and stream.isatty()
if sys.platform != 'win32':
return is_a_tty
# ANSICON checks for things like ConEmu
# WT_SESSION checks if this is Windows Terminal
# VSCode built-in terminal supports colour too
return is_a_tty and ('ANSICON' in os.environ or 'WT_SESSION' in os.environ or os.environ.get('TERM_PROGRAM') == 'vscode')
class _ColourFormatter(logging.Formatter):
# ANSI codes are a bit weird to decipher if you're unfamiliar with them, so here's a refresher
# It starts off with a format like \x1b[XXXm where XXX is a semicolon separated list of commands
# The important ones here relate to colour.
# 30-37 are black, red, green, yellow, blue, magenta, cyan and white in that order
# 40-47 are the same except for the background
# 90-97 are the same but "bright" foreground
# 100-107 are the same as the bright ones but for the background.
# 1 means bold, 2 means dim, 0 means reset, and 4 means underline.
LEVEL_COLOURS = [
(logging.DEBUG, '\x1b[40;1m'),
(logging.INFO, '\x1b[34;1m'),
(logging.WARNING, '\x1b[33;1m'),
(logging.ERROR, '\x1b[31m'),
(logging.CRITICAL, '\x1b[41m'),
]
FORMATS = {
level: logging.Formatter(
f'\x1b[30;1m%(asctime)s\x1b[0m {colour}%(levelname)-8s\x1b[0m \x1b[35m%(name)s\x1b[0m %(message)s',
'%Y-%m-%d %H:%M:%S',
)
for level, colour in LEVEL_COLOURS
}
def format(self, record):
formatter = self.FORMATS.get(record.levelno)
if formatter is None:
formatter = self.FORMATS[logging.DEBUG]
# Override the traceback to always print in red
if record.exc_info:
text = formatter.formatException(record.exc_info)
record.exc_text = f'\x1b[31m{text}\x1b[0m'
output = formatter.format(record)
# Remove the cache layer
record.exc_text = None
return output
def setup_logging(
*,
handler: logging.Handler = MISSING,
formatter: logging.Formatter = MISSING,
level: int = MISSING,
root: bool = True,
) -> None:
"""A helper function to setup logging.
This is superficially similar to :func:`logging.basicConfig` but
uses different defaults and a colour formatter if the stream can
display colour.
This is used by the :class:`~discord.Client` to set up logging
if ``log_handler`` is not ``None``.
.. versionadded:: 2.0
Parameters
-----------
handler: :class:`logging.Handler`
The log handler to use for the library's logger.
The default log handler if not provided is :class:`logging.StreamHandler`.
formatter: :class:`logging.Formatter`
The formatter to use with the given log handler. If not provided then it
defaults to a colour based logging formatter (if available). If colour
is not available then a simple logging formatter is provided.
level: :class:`int`
The default log level for the library's logger. Defaults to ``logging.INFO``.
root: :class:`bool`
Whether to set up the root logger rather than the library logger.
Unlike the default for :class:`~discord.Client`, this defaults to ``True``.
"""
if level is MISSING:
level = logging.INFO
if handler is MISSING:
handler = logging.StreamHandler()
if formatter is MISSING:
if isinstance(handler, logging.StreamHandler) and stream_supports_colour(handler.stream):
formatter = _ColourFormatter()
else:
dt_fmt = '%Y-%m-%d %H:%M:%S'
formatter = logging.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{')
if root:
logger = logging.getLogger()
else:
library, _, _ = __name__.partition('.')
logger = logging.getLogger(library)
handler.setFormatter(formatter)
logger.setLevel(level)
logger.addHandler(handler)

2
docs/api.rst

@ -1370,6 +1370,8 @@ Utility Functions
.. autofunction:: discord.utils.get .. autofunction:: discord.utils.get
.. autofunction:: discord.utils.setup_logging
.. autofunction:: discord.utils.snowflake_time .. autofunction:: discord.utils.snowflake_time
.. autofunction:: discord.utils.time_snowflake .. autofunction:: discord.utils.time_snowflake

11
docs/logging.rst

@ -43,6 +43,17 @@ Likewise, configuring the log level to ``logging.DEBUG`` is also possible:
This is recommended, especially at verbose levels such as ``DEBUG``, as there are a lot of events logged and it would clog the stderr of your program. This is recommended, especially at verbose levels such as ``DEBUG``, as there are a lot of events logged and it would clog the stderr of your program.
If you want to setup logging using the library provided configuration without using :meth:`Client.run`, you can use :func:`discord.utils.setup_logging`:
.. code-block:: python3
import discord
discord.utils.setup_logging()
# or, for example
discord.utils.setup_logging(level=logging.INFO, root=False)
More advanced setups are possible with the :mod:`logging` module. The example below configures a rotating file handler that outputs DEBUG output for everything the library outputs, except for HTTP requests: More advanced setups are possible with the :mod:`logging` module. The example below configures a rotating file handler that outputs DEBUG output for everything the library outputs, except for HTTP requests:
.. code-block:: python3 .. code-block:: python3

3
examples/advanced_startup.py

@ -75,6 +75,9 @@ async def main():
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
# Alternatively, you could use:
# discord.utils.setup_logging(handler=handler, root=False)
# One of the reasons to take over more of the process though # One of the reasons to take over more of the process though
# is to ensure use with other libraries or tools which also require their own cleanup. # is to ensure use with other libraries or tools which also require their own cleanup.

Loading…
Cancel
Save