Skip to content
This repository was archived by the owner on Aug 28, 2019. It is now read-only.

Commit 49e6835

Browse files
committed
Add default logging configuration when using Client.run
While it is possible to do this type of your set up yourself, it's better for beginners to have logging automatically set up for them. This has come up often in the help channel over the years. This also provides an escape hatch to disable it.
1 parent 768242b commit 49e6835

File tree

2 files changed

+192
-28
lines changed

2 files changed

+192
-28
lines changed

discord/client.py

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import datetime
2929
import logging
3030
import sys
31+
import os
3132
import traceback
3233
from typing import (
3334
Any,
@@ -113,6 +114,61 @@ def __getattr__(self, attr: str) -> None:
113114
_loop: Any = _LoopSentinel()
114115

115116

117+
def stream_supports_colour(stream: Any) -> bool:
118+
is_a_tty = hasattr(stream, 'isatty') and stream.isatty()
119+
if sys.platform != 'win32':
120+
return is_a_tty
121+
122+
# ANSICON checks for things like ConEmu
123+
# WT_SESSION checks if this is Windows Terminal
124+
# VSCode built-in terminal supports colour too
125+
return is_a_tty and ('ANSICON' in os.environ or 'WT_SESSION' in os.environ or os.environ.get('TERM_PROGRAM') == 'vscode')
126+
127+
128+
class _ColourFormatter(logging.Formatter):
129+
130+
# ANSI codes are a bit weird to decipher if you're unfamiliar with them, so here's a refresher
131+
# It starts off with a format like \x1b[XXXm where XXX is a semicolon separated list of commands
132+
# The important ones here relate to colour.
133+
# 30-37 are black, red, green, yellow, blue, magenta, cyan and white in that order
134+
# 40-47 are the same except for the background
135+
# 90-97 are the same but "bright" foreground
136+
# 100-107 are the same as the bright ones but for the background.
137+
# 1 means bold, 2 means dim, 0 means reset, and 4 means underline.
138+
139+
LEVEL_COLOURS = [
140+
(logging.DEBUG, '\x1b[40;1m'),
141+
(logging.INFO, '\x1b[34;1m'),
142+
(logging.WARNING, '\x1b[33;1m'),
143+
(logging.ERROR, '\x1b[31m'),
144+
(logging.CRITICAL, '\x1b[41m'),
145+
]
146+
147+
FORMATS = {
148+
level: logging.Formatter(
149+
f'\x1b[30;1m%(asctime)s\x1b[0m {colour}%(levelname)-8s\x1b[0m \x1b[35m%(name)s\x1b[0m %(message)s',
150+
'%Y-%m-%d %H:%M:%S',
151+
)
152+
for level, colour in LEVEL_COLOURS
153+
}
154+
155+
def format(self, record):
156+
formatter = self.FORMATS.get(record.levelno)
157+
if formatter is None:
158+
formatter = self.FORMATS[logging.DEBUG]
159+
160+
# Override the traceback to always print in red
161+
if record.exc_info:
162+
text = formatter.formatException(record.exc_info)
163+
record.exc_text = f'\x1b[31m{text}\x1b[0m'
164+
165+
output = formatter.format(record)
166+
167+
# Remove the cache layer
168+
record.exc_text = None
169+
return output
170+
171+
116172
class Client:
117173
r"""Represents a client connection that connects to Discord.
118174
This class is used to interact with the Discord WebSocket and API.
@@ -706,6 +762,17 @@ async def start(self, token: str, *, reconnect: bool = True) -> None:
706762
707763
A shorthand coroutine for :meth:`login` + :meth:`connect`.
708764
765+
Parameters
766+
-----------
767+
token: :class:`str`
768+
The authentication token. Do not prefix this token with
769+
anything as the library will do it for you.
770+
reconnect: :class:`bool`
771+
If we should attempt reconnecting, either due to internet
772+
failure or a specific failure on Discord's part. Certain
773+
disconnects that lead to bad state will not be handled (such as
774+
invalid sharding payloads or bad tokens).
775+
709776
Raises
710777
-------
711778
TypeError
@@ -714,31 +781,93 @@ async def start(self, token: str, *, reconnect: bool = True) -> None:
714781
await self.login(token)
715782
await self.connect(reconnect=reconnect)
716783

717-
def run(self, *args: Any, **kwargs: Any) -> None:
784+
def run(
785+
self,
786+
token: str,
787+
*,
788+
reconnect: bool = True,
789+
log_handler: Optional[logging.Handler] = MISSING,
790+
log_formatter: logging.Formatter = MISSING,
791+
log_level: int = MISSING,
792+
) -> None:
718793
"""A blocking call that abstracts away the event loop
719794
initialisation from you.
720795
721796
If you want more control over the event loop then this
722797
function should not be used. Use :meth:`start` coroutine
723798
or :meth:`connect` + :meth:`login`.
724799
725-
Roughly Equivalent to: ::
726-
727-
try:
728-
asyncio.run(self.start(*args, **kwargs))
729-
except KeyboardInterrupt:
730-
return
800+
This function also sets up the logging library to make it easier
801+
for beginners to know what is going on with the library. For more
802+
advanced users, this can be disabled by passing ``None`` to
803+
the ``log_handler`` parameter.
731804
732805
.. warning::
733806
734807
This function must be the last function to call due to the fact that it
735808
is blocking. That means that registration of events or anything being
736809
called after this function call will not execute until it returns.
810+
811+
Parameters
812+
-----------
813+
token: :class:`str`
814+
The authentication token. Do not prefix this token with
815+
anything as the library will do it for you.
816+
reconnect: :class:`bool`
817+
If we should attempt reconnecting, either due to internet
818+
failure or a specific failure on Discord's part. Certain
819+
disconnects that lead to bad state will not be handled (such as
820+
invalid sharding payloads or bad tokens).
821+
log_handler: Optional[:class:`logging.Handler`]
822+
The log handler to use for the library's logger. If this is ``None``
823+
then the library will not set up anything logging related. Logging
824+
will still work if ``None`` is passed, though it is your responsibility
825+
to set it up.
826+
827+
The default log handler if not provided is :class:`logging.StreamHandler`.
828+
829+
.. versionadded:: 2.0
830+
log_formatter: :class:`logging.Formatter`
831+
The formatter to use with the given log handler. If not provided then it
832+
defaults to a colour based logging formatter (if available).
833+
834+
.. versionadded:: 2.0
835+
log_level: :class:`int`
836+
The default log level for the library's logger. This is only applied if the
837+
``log_handler`` parameter is not ``None``. Defaults to ``logging.INFO``.
838+
839+
Note that the *root* logger will always be set to ``logging.INFO`` and this
840+
only controls the library's log level. To control the root logger's level,
841+
you can use ``logging.getLogger().setLevel(level)``.
842+
843+
.. versionadded:: 2.0
737844
"""
738845

739846
async def runner():
740847
async with self:
741-
await self.start(*args, **kwargs)
848+
await self.start(token, reconnect=reconnect)
849+
850+
if log_level is MISSING:
851+
log_level = logging.INFO
852+
853+
if log_handler is MISSING:
854+
log_handler = logging.StreamHandler()
855+
856+
if log_formatter is MISSING:
857+
if isinstance(log_handler, logging.StreamHandler) and stream_supports_colour(log_handler.stream):
858+
log_formatter = _ColourFormatter()
859+
else:
860+
dt_fmt = '%Y-%m-%d %H:%M:%S'
861+
log_formatter = logging.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{')
862+
863+
logger = None
864+
if log_handler is not None:
865+
library, _, _ = __name__.partition('.')
866+
logger = logging.getLogger(library)
867+
868+
log_handler.setFormatter(log_formatter)
869+
logger.setLevel(log_level)
870+
logger.addHandler(log_handler)
742871

743872
try:
744873
asyncio.run(runner())
@@ -747,6 +876,9 @@ async def runner():
747876
# `asyncio.run` handles the loop cleanup
748877
# and `self.start` closes all sockets and the HTTPClient instance.
749878
return
879+
finally:
880+
if log_handler is not None and logger is not None:
881+
logger.removeHandler(log_handler)
750882

751883
# properties
752884

docs/logging.rst

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,78 @@
11
:orphan:
22

3+
.. currentmodule:: discord
34
.. versionadded:: 0.6.0
45
.. _logging_setup:
56

67
Setting Up Logging
78
===================
89

910
*discord.py* logs errors and debug information via the :mod:`logging` python
10-
module. It is strongly recommended that the logging module is
11-
configured, as no errors or warnings will be output if it is not set up.
12-
Configuration of the ``logging`` module can be as simple as::
11+
module. In order to streamline this process, the library provides default configuration for the ``discord`` logger when using :meth:`Client.run`. It is strongly recommended that the logging module is configured, as no errors or warnings will be output if it is not set up.
12+
13+
The default logging configuration provided by the library will print to :data:`sys.stderr` using coloured output. You can configure it to send to a file instead by using one of the built-in :mod:`logging.handlers`, such as :class:`logging.FileHandler`.
14+
15+
This can be done by passing it through :meth:`Client.run`:
16+
17+
.. code-block:: python3
18+
19+
import logging
20+
21+
handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
22+
23+
# Assume client refers to a discord.Client subclass...
24+
client.run(token, log_handler=handler)
25+
26+
You can also disable the library's logging configuration completely by passing ``None``:
27+
28+
.. code-block:: python3
29+
30+
client.run(token, log_handler=None)
31+
32+
33+
Likewise, configuring the log level to ``logging.DEBUG`` is also possible:
34+
35+
.. code-block:: python3
1336
1437
import logging
1538
16-
logging.basicConfig(level=logging.INFO)
39+
handler = handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
1740
18-
Placed at the start of the application. This will output the logs from
19-
discord as well as other libraries that use the ``logging`` module
20-
directly to the console.
41+
# Assume client refers to a discord.Client subclass...
42+
client.run(token, log_handler=handler, log_level=logging.DEBUG)
2143
22-
The optional ``level`` argument specifies what level of events to log
23-
out and can be any of ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, and
24-
``DEBUG`` and if not specified defaults to ``WARNING``.
44+
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.
2545

26-
More advanced setups are possible with the :mod:`logging` module. For
27-
example to write the logs to a file called ``discord.log`` instead of
28-
outputting them to the console the following snippet can be used::
46+
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:
47+
48+
.. code-block:: python3
2949
3050
import discord
3151
import logging
3252
3353
logger = logging.getLogger('discord')
3454
logger.setLevel(logging.DEBUG)
35-
handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
36-
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
55+
logging.getLogger('discord.http').setLevel(logging.INFO)
56+
57+
handler = logging.RotatingFileHandler(
58+
filename='discord.log',
59+
encoding='utf-8',
60+
maxBytes=32 * 1024 * 1024, # 32 MiB
61+
backupCount=5, # Rotate through 5 files
62+
)
63+
formatter = logging.Formatter()
64+
dt_fmt = '%Y-%m-%d %H:%M:%S'
65+
formatter = logging.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{')
66+
handler.setFormatter(formatter)
3767
logger.addHandler(handler)
3868
39-
This is recommended, especially at verbose levels such as ``INFO``
40-
and ``DEBUG``, as there are a lot of events logged and it would clog the
41-
stdout of your program.
69+
# Assume client refers to a discord.Client subclass...
70+
# Suppress the default configuration since we have our own
71+
client.run(token, log_handler=None)
72+
4273
74+
For more information, check the documentation and tutorial of the :mod:`logging` module.
4375

76+
.. versionchanged:: 2.0
4477

45-
For more information, check the documentation and tutorial of the
46-
:mod:`logging` module.
78+
The library now provides a default logging configuration.

0 commit comments

Comments
 (0)