mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-06-07 12:18:59 +00:00
Add utils.setup_logging to help set up logging outside of Client.run
This commit is contained in:
parent
1c7747fe9d
commit
2bf2bfc9b4
@ -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
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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user