Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set up standard logging module #2064

Closed
wants to merge 73 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
d3436d4
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
3ed7e94
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
17d826d
new sway_idle module: to display the idle state of sway wm (#2058)
Aug 27, 2021
e085d93
sway_idle module: chmod -x and remove useless encoding shebang
ultrabug Aug 27, 2021
56cf8b0
speedtest module: fix error when clicking too fast, closes #2060
ultrabug Aug 27, 2021
355c006
clock module: compatible with tzlocal 3.0, thx to @flyingapfopenguin
ultrabug Aug 27, 2021
e813cb9
version 3.39
ultrabug Aug 30, 2021
3a64b42
make qa run tox
ultrabug Aug 30, 2021
07fe1b8
README: update readthedocs links to new documentation thx to @oceyral
ultrabug Aug 31, 2021
96dbe5b
i3 contrib page: update readthedocs links to new documentation
ultrabug Aug 31, 2021
9a9ee14
i3 contrib page: update to sync with i3.github.io
ultrabug Aug 31, 2021
60cde69
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
b4f49e2
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
ac08112
Merge branch 'logging' of github.com:rlerm/py3status into logging
rlerm Sep 4, 2021
25184cb
Remove unused import.
rlerm Sep 5, 2021
c8421de
Fix oversight: Make "info" be the default logging threshold.
rlerm Sep 5, 2021
6a7229b
Fix syslog setup.
rlerm Sep 5, 2021
88361de
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
ac9140c
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
0924f4a
new sway_idle module: to display the idle state of sway wm (#2058)
Aug 27, 2021
eeb38b0
sway_idle module: chmod -x and remove useless encoding shebang
ultrabug Aug 27, 2021
1bf1315
i3 contrib page: update readthedocs links to new documentation
ultrabug Aug 31, 2021
084d92a
i3 contrib page: update to sync with i3.github.io
ultrabug Aug 31, 2021
fc6ab26
Remove unused import.
rlerm Sep 5, 2021
06f128d
Fix oversight: Make "info" be the default logging threshold.
rlerm Sep 5, 2021
480c87e
Fix syslog setup.
rlerm Sep 5, 2021
3963f25
Allow configuring logging using a JSON file.
rlerm Sep 19, 2021
b0c8324
Add documentation explaining how to configure logging while testing m…
rlerm Sep 19, 2021
7736dfa
Merge branch 'logging' of github.com:rlerm/py3status into logging
rlerm Sep 19, 2021
255c337
Fix bad merge conflicts.
rlerm Sep 20, 2021
e5a8077
Fix lint error.
rlerm Sep 20, 2021
a86242e
Fix another merge artifact.
rlerm Sep 20, 2021
ff34d7b
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
c7b5ac0
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
7106485
new sway_idle module: to display the idle state of sway wm (#2058)
Aug 27, 2021
fa13e22
i3 contrib page: update readthedocs links to new documentation
ultrabug Aug 31, 2021
0bbfa70
Fix lint error.
rlerm Sep 20, 2021
3587450
Merge branch 'logging' of github.com:rlerm/py3status into logging
rlerm Sep 20, 2021
d8ab680
Fix syslog handler, and eagerly import module.
rlerm Oct 5, 2021
1df8341
Run black formatter.
rlerm Oct 8, 2021
e83a67a
Makes 'disable_existing_loggers=False' the default.
rlerm Dec 17, 2021
381925d
Merge branch 'master' into logging
rlerm Dec 30, 2021
d04ab0d
Run formatter.
rlerm Jan 3, 2022
5023536
Port the xrandr module to use the builtin logging library.
rlerm Jan 5, 2022
27caf06
Merge branch 'master' into logging
ultrabug Jan 31, 2022
4b2ee49
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
37bc19e
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
ae4ccbc
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
0257927
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
d6da4ca
new sway_idle module: to display the idle state of sway wm (#2058)
Aug 27, 2021
531b45b
sway_idle module: chmod -x and remove useless encoding shebang
ultrabug Aug 27, 2021
62c6acc
i3 contrib page: update readthedocs links to new documentation
ultrabug Aug 31, 2021
c2260d1
i3 contrib page: update to sync with i3.github.io
ultrabug Aug 31, 2021
868805a
Remove unused import.
rlerm Sep 5, 2021
abaaef5
Fix oversight: Make "info" be the default logging threshold.
rlerm Sep 5, 2021
fd41bf3
Fix syslog setup.
rlerm Sep 5, 2021
e56fb64
Allow configuring logging using a JSON file.
rlerm Sep 19, 2021
9bbbbe3
Add documentation explaining how to configure logging while testing m…
rlerm Sep 19, 2021
5ebd7bf
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
7d8f5b9
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
664a006
new sway_idle module: to display the idle state of sway wm (#2058)
Aug 27, 2021
58f00c3
i3 contrib page: update readthedocs links to new documentation
ultrabug Aug 31, 2021
1b2ba50
Fix lint error.
rlerm Sep 20, 2021
37b19a5
Use the logging module directly instead of home-grown code.
rlerm Aug 23, 2021
6277b64
Use standard logging infrastructure on "debug" statements.
rlerm Aug 24, 2021
ffd2f9e
Remove unused import.
rlerm Sep 5, 2021
70fa768
Fix syslog handler, and eagerly import module.
rlerm Oct 5, 2021
0ef5c3e
Run black formatter.
rlerm Oct 8, 2021
6354215
Makes 'disable_existing_loggers=False' the default.
rlerm Dec 17, 2021
bd3a338
Run formatter.
rlerm Jan 3, 2022
18817ac
Port the xrandr module to use the builtin logging library.
rlerm Jan 5, 2022
3e3ff6d
Merge branch 'logging' of github.com:rlerm/py3status into logging
rlerm Jan 31, 2022
0049671
Merge branch 'master' into logging
ultrabug Oct 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/dev-guide/writing-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,54 @@ Loadavg 1.41 1.61 1.82
Loadavg 1.41 1.61 1.82
^C
```

## Logging

Modules are encouraged to use Python's standard
[`logging`](https://docs.python.org/3/library/logging.config.html?highlight=logging#logging-config-dictschema)
module for debugging -- see the `xrandr` module as an example. By default, logs
will be written to
[`syslog`](https://docs.python.org/3/library/logging.config.html?highlight=logging#logging-config-dictschema)
with a minimum level of `INFO`.

Several existing modules will write to logs, with varying levels of details.
Therefore, when debugging a specific module, it may be useful to show only the
one you're interested in. This can be done by using the `--log-config` flag to
pass a JSON file that configures logging. This file must be in the format
specified in
[`logging`'s configuration schema](https://docs.python.org/3/library/logging.config.html?highlight=logging#logging-config-dictschema).
For example, to show only logs from the `xrandr` module in a `DEBUG` level, while
keeping all others at `WARNING`, you can use:

```json
{
"version": 1,
"handlers": {
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "/tmp/py3status_log.log",
"maxBytes": 2048,
"formatter": "default"
}
},
"formatters": {
"default": {
"validate": true,
"format":"%(asctime)s %(levelname)s %(module)s %(message)s",
"datefmt":"%Y-%m-%d %H:%M:%S"
}
},
"loggers": {
"root": {"handlers": ["file"], "level": "WARNING"},
"xrandr": {"level": "DEBUG"}
}
}
```

In this example, logs will be written to the `/tmp/py3status_log.log` file,
but you can configure the logging library to log to other destinations like
stderr or syslog.

## Publishing custom modules on PyPI

You can share your custom modules and make them available for py3status
Expand Down
42 changes: 27 additions & 15 deletions py3status/argparsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,6 @@ def _format_action_invocation(self, action):
metavar="FILE",
type=Path,
)
parser.add_argument(
"-d",
"--debug",
action="store_true",
help="enable debug logging in syslog or log file if --log-file option is passed",
)
parser.add_argument(
"-g",
"--gevent",
Expand All @@ -104,15 +98,6 @@ def _format_action_invocation(self, action):
metavar="PATH",
type=Path,
)
parser.add_argument(
"-l",
"--log-file",
action="store",
dest="log_file",
help="enable logging to FILE (this option is not set by default)",
metavar="FILE",
type=Path,
)
parser.add_argument(
"-s",
"--standalone",
Expand Down Expand Up @@ -164,6 +149,33 @@ def _format_action_invocation(self, action):
help="specify window manager i3 or sway",
)

logging_args = parser.add_argument_group()
logging_args.add_argument(
"-d",
"--debug",
action="store_true",
help="enable debug logging in syslog or log file if --log-file option is passed",
)
logging_args.add_argument(
"-l",
"--log-file",
action="store",
dest="log_file",
help="enable logging to FILE (this option is not set by default)",
metavar="FILE",
type=Path,
)
logging_args.add_argument(
"--log-config",
action="store",
dest="log_config",
help="path to a file that fully configures the 'logging' module. This "
"must contain a JSON dictionary in the format expected by "
"logging.config.dictConfig.",
metavar="FILE",
type=Path,
)

# deprecations
parser.add_argument("-n", "--interval", help=argparse.SUPPRESS)

Expand Down
124 changes: 76 additions & 48 deletions py3status/core.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import logging
import logging.handlers
import logging.config
import pkg_resources
import sys
import time

from collections import deque
from json import dumps
from json import load, dumps, JSONDecodeError
from pathlib import Path
from pprint import pformat
from signal import signal, Signals, SIGTERM, SIGUSR1, SIGTSTP, SIGCONT
from subprocess import Popen
from threading import Event, Thread
from syslog import syslog, LOG_ERR, LOG_INFO, LOG_WARNING
from traceback import extract_tb, format_tb, format_stack

from py3status.command import CommandServer
Expand All @@ -22,7 +23,11 @@
from py3status.profiling import profile
from py3status.udev_monitor import UdevMonitor

LOG_LEVELS = {"error": LOG_ERR, "warning": LOG_WARNING, "info": LOG_INFO}
LOGGING_LEVELS = {
"error": logging.ERROR,
"warning": logging.WARNING,
"info": logging.INFO,
}

DBUS_LEVELS = {"error": "critical", "warning": "normal", "info": "low"}

Expand All @@ -40,6 +45,8 @@
ENTRY_POINT_NAME = "py3status"
ENTRY_POINT_KEY = "entry_point"

_logger = logging.getLogger("core")


class Runner(Thread):
"""
Expand Down Expand Up @@ -535,16 +542,65 @@ def load_modules(self, modules_list, user_modules):
# only handle modules with available methods
if my_m.methods:
self.modules[module] = my_m
elif self.config["debug"]:
self.log(f'ignoring module "{module}" (no methods found)')
_logger.debug('ignoring module "%s" (no methods found)', module)
except Exception:
err = sys.exc_info()[1]
msg = f'Loading module "{module}" failed ({err}).'
self.report_exception(msg, level="warning")

def _setup_logging(self):
"""Set up the global logger."""
log_config = self.config.get("log_config")
if log_config:
if self.config.get("debug"):
self.report_exception("--debug is invalid when --log-config is passed")
if self.config.get("log_file"):
self.report_exception(
"--log-file is invalid when --log-config is passed"
)

with log_config.open() as f:
try:
config_dict = load(f, strict=False)
config_dict.setdefault("disable_existing_loggers", False)
logging.config.dictConfig(config_dict)
except JSONDecodeError as e:
self.report_exception(str(e))
# Nothing else to do. All logging config is provided by the config
# dictionary.
return

root = logging.getLogger(name=None)
if self.config.get("debug"):
root.setLevel(logging.DEBUG)
else:
root.setLevel(logging.INFO)

log_file = self.config.get("log_file")
if log_file:
handler = logging.FileHandler(log_file, encoding="utf8")
else:
# https://stackoverflow.com/a/3969772/340862
handler = logging.handlers.SysLogHandler(address="/dev/log")
handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s %(levelname)s %(module)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
style="%",
)
)
root.addHandler(handler)

def setup(self):
"""
Setup py3status and spawn i3status/events/modules threads.
"""
self._setup_logging()

def _log_gitversion(self):
# A git repo is detected looking for the .git directory


git_path = Path(__file__).resolve().parent.parent / ".git"
if not git_path.exists():
return
Expand Down Expand Up @@ -601,8 +657,7 @@ def setup(self):

self.log("window manager: {}".format(self.config["wm_name"]))

if self.config["debug"]:
self.log(f"py3status started with config {self.config}")
_logger.debug("py3status started with config %s", self.config)

if self.config["gevent"]:
self.is_gevent = self.gevent_monkey_patch_report()
Expand Down Expand Up @@ -647,12 +702,9 @@ def setup(self):
i3s_mode = "mocked"
break
time.sleep(0.1)
if self.config["debug"]:
self.log(
"i3status thread {} with config {}".format(
i3s_mode, self.config["py3_config"]
)
)
_logger.debug(
"i3status thread %s with config %s", i3s_mode, self.config["py3_config"]
)

# add i3status thread monitoring task
if i3s_mode == "started":
Expand All @@ -663,15 +715,13 @@ def setup(self):
self.events_thread = Events(self)
self.events_thread.daemon = True
self.events_thread.start()
if self.config["debug"]:
self.log("events thread started")
_logger.debug("events thread started")

# initialise the command server
self.commands_thread = CommandServer(self)
self.commands_thread.daemon = True
self.commands_thread.start()
if self.config["debug"]:
self.log("commands thread started")
_logger.debug("commands thread started")

# initialize the udev monitor (lazy)
self.udev_monitor = UdevMonitor(self)
Expand Down Expand Up @@ -716,8 +766,7 @@ def setup(self):
# get a dict of all user provided modules
self.log("modules include paths: {}".format(self.config["include_paths"]))
user_modules = self.get_user_configured_modules()
if self.config["debug"]:
self.log(f"user_modules={user_modules}")
_logger.debug("user_modules=%s", user_modules)

if self.py3_modules:
# load and spawn i3status.conf configured modules threads
Expand Down Expand Up @@ -815,8 +864,7 @@ def stop(self):

try:
self.lock.set()
if self.config["debug"]:
self.log("lock set, exiting")
_logger.debug("lock set, exiting")
# run kill() method on all py3status modules
for module in self.modules.values():
module.kill()
Expand Down Expand Up @@ -847,12 +895,10 @@ def refresh_modules(self, module_string=None, exact=True):
or (not exact and name.startswith(module_string))
):
if module["type"] == "py3status":
if self.config["debug"]:
self.log(f"refresh py3status module {name}")
_logger.debug("refresh py3status module %s", name)
module["module"].force_update()
else:
if self.config["debug"]:
self.log(f"refresh i3status module {name}")
_logger.debug("refresh i3status module %s", name)
update_i3status = True
if update_i3status:
self.i3status_thread.refresh_i3status()
Expand Down Expand Up @@ -926,29 +972,11 @@ def notify_update(self, update, urgent=False):
def log(self, msg, level="info"):
"""
log this information to syslog or user provided logfile.

This is soft-deprecated; prefer using the 'logging' module directly in
new code.
"""
if not self.config.get("log_file"):
# If level was given as a str then convert to actual level
level = LOG_LEVELS.get(level, level)
syslog(level, f"{msg}")
else:
# Binary mode so fs encoding setting is not an issue
with self.config["log_file"].open("ab") as f:
log_time = time.strftime("%Y-%m-%d %H:%M:%S")
# nice formatting of data structures using pretty print
if isinstance(msg, (dict, list, set, tuple)):
msg = pformat(msg)
# if multiline then start the data output on a fresh line
# to aid readability.
if "\n" in msg:
msg = "\n" + msg
out = f"{log_time} {level.upper()} {msg}\n"
try:
# Encode unicode strings to bytes
f.write(out.encode("utf-8"))
except (AttributeError, UnicodeDecodeError):
# Write any byte strings straight to log
f.write(out)
_logger.log(LOGGING_LEVELS.get(level, logging.DEBUG), msg)

def create_output_modules(self):
"""
Expand Down
Loading