11import importlib .metadata
2+ import logging
3+ import logging .config
4+ import logging .handlers
25import sys
36import time
47from collections import deque
8+ from json import JSONDecodeError , load
59from pathlib import Path
6- from pprint import pformat
710from signal import SIGCONT , SIGTERM , SIGTSTP , SIGUSR1 , Signals , signal
811from subprocess import Popen
9- from syslog import LOG_ERR , LOG_INFO , LOG_WARNING , syslog
1012from threading import Event , Thread
1113from traceback import extract_tb , format_stack , format_tb
1214
2123from py3status .profiling import profile
2224from py3status .udev_monitor import UdevMonitor
2325
24- LOG_LEVELS = {"error" : LOG_ERR , "warning" : LOG_WARNING , "info" : LOG_INFO }
26+ LOGGING_LEVELS = {
27+ "error" : logging .ERROR ,
28+ "warning" : logging .WARNING ,
29+ "info" : logging .INFO ,
30+ }
2531
2632DBUS_LEVELS = {"error" : "critical" , "warning" : "normal" , "info" : "low" }
2733
3945ENTRY_POINT_NAME = "py3status"
4046ENTRY_POINT_KEY = "entry_point"
4147
48+ _logger = logging .getLogger ("core" )
49+
4250
4351class Runner (Thread ):
4452 """
@@ -521,8 +529,7 @@ def load_modules(self, modules_list, user_modules):
521529 # only handle modules with available methods
522530 if my_m .methods :
523531 self .modules [module ] = my_m
524- elif self .config ["debug" ]:
525- self .log (f'ignoring module "{ module } " (no methods found)' )
532+ _logger .debug ('ignoring module "%s" (no methods found)' , module )
526533 except Exception :
527534 err = sys .exc_info ()[1 ]
528535 msg = f'Loading module "{ module } " failed ({ err } ).'
@@ -571,10 +578,52 @@ def _log_gitversion(self):
571578 self .log (f"git commit: { commit .hexsha [:7 ]} { commit .summary } " )
572579 self .log (f"git clean: { not repo .is_dirty ()!s} " )
573580
581+ def _setup_logging (self ):
582+ """Set up the global logger."""
583+ log_config = self .config .get ("log_config" )
584+ if log_config :
585+ if self .config .get ("debug" ):
586+ self .report_exception ("--debug is invalid when --log-config is passed" )
587+ if self .config .get ("log_file" ):
588+ self .report_exception ("--log-file is invalid when --log-config is passed" )
589+
590+ with log_config .open () as f :
591+ try :
592+ config_dict = load (f , strict = False )
593+ config_dict .setdefault ("disable_existing_loggers" , False )
594+ logging .config .dictConfig (config_dict )
595+ except JSONDecodeError as e :
596+ self .report_exception (str (e ))
597+ # Nothing else to do. All logging config is provided by the config
598+ # dictionary.
599+ return
600+
601+ root = logging .getLogger (name = None )
602+ if self .config .get ("debug" ):
603+ root .setLevel (logging .DEBUG )
604+ else :
605+ root .setLevel (logging .INFO )
606+
607+ log_file = self .config .get ("log_file" )
608+ if log_file :
609+ handler = logging .FileHandler (log_file , encoding = "utf8" )
610+ else :
611+ # https://stackoverflow.com/a/3969772/340862
612+ handler = logging .handlers .SysLogHandler (address = "/dev/log" )
613+ handler .setFormatter (
614+ logging .Formatter (
615+ fmt = "%(asctime)s %(levelname)s %(module)s %(message)s" ,
616+ datefmt = "%Y-%m-%d %H:%M:%S" ,
617+ style = "%" ,
618+ )
619+ )
620+ root .addHandler (handler )
621+
574622 def setup (self ):
575623 """
576624 Setup py3status and spawn i3status/events/modules threads.
577625 """
626+ self ._setup_logging ()
578627
579628 # log py3status and python versions
580629 self .log ("=" * 8 )
@@ -586,8 +635,7 @@ def setup(self):
586635
587636 self .log ("window manager: {}" .format (self .config ["wm_name" ]))
588637
589- if self .config ["debug" ]:
590- self .log (f"py3status started with config { self .config } " )
638+ _logger .debug ("py3status started with config %s" , self .config )
591639
592640 # read i3status.conf
593641 config_path = self .config ["i3status_config_path" ]
@@ -637,10 +685,7 @@ def setup(self):
637685 i3s_mode = "mocked"
638686 break
639687 time .sleep (0.1 )
640- if self .config ["debug" ]:
641- self .log (
642- "i3status thread {} with config {}" .format (i3s_mode , self .config ["py3_config" ])
643- )
688+ _logger .debug ("i3status thread %s with config %s" , i3s_mode , self .config ["py3_config" ])
644689
645690 # add i3status thread monitoring task
646691 if i3s_mode == "started" :
@@ -651,15 +696,13 @@ def setup(self):
651696 self .events_thread = Events (self )
652697 self .events_thread .daemon = True
653698 self .events_thread .start ()
654- if self .config ["debug" ]:
655- self .log ("events thread started" )
699+ _logger .debug ("events thread started" )
656700
657701 # initialise the command server
658702 self .commands_thread = CommandServer (self )
659703 self .commands_thread .daemon = True
660704 self .commands_thread .start ()
661- if self .config ["debug" ]:
662- self .log ("commands thread started" )
705+ _logger .debug ("commands thread started" )
663706
664707 # initialize the udev monitor (lazy)
665708 self .udev_monitor = UdevMonitor (self )
@@ -702,8 +745,7 @@ def setup(self):
702745 # get a dict of all user provided modules
703746 self .log ("modules include paths: {}" .format (self .config ["include_paths" ]))
704747 user_modules = self .get_user_configured_modules ()
705- if self .config ["debug" ]:
706- self .log (f"user_modules={ user_modules } " )
748+ _logger .debug ("user_modules=%s" , user_modules )
707749
708750 if self .py3_modules :
709751 # load and spawn i3status.conf configured modules threads
@@ -813,8 +855,7 @@ def stop(self):
813855
814856 try :
815857 self .lock .set ()
816- if self .config ["debug" ]:
817- self .log ("lock set, exiting" )
858+ _logger .debug ("lock set, exiting" )
818859 # run kill() method on all py3status modules
819860 for module in self .modules .values ():
820861 module .kill ()
@@ -845,12 +886,10 @@ def refresh_modules(self, module_string=None, exact=True):
845886 or (not exact and name .startswith (module_string ))
846887 ):
847888 if module ["type" ] == "py3status" :
848- if self .config ["debug" ]:
849- self .log (f"refresh py3status module { name } " )
889+ _logger .debug ("refresh py3status module %s" , name )
850890 module ["module" ].force_update ()
851891 else :
852- if self .config ["debug" ]:
853- self .log (f"refresh i3status module { name } " )
892+ _logger .debug ("refresh i3status module %s" , name )
854893 update_i3status = True
855894 if update_i3status :
856895 self .i3status_thread .refresh_i3status ()
@@ -924,29 +963,11 @@ def notify_update(self, update, urgent=False):
924963 def log (self , msg , level = "info" ):
925964 """
926965 log this information to syslog or user provided logfile.
966+
967+ This is soft-deprecated; prefer using the 'logging' module directly in
968+ new code.
927969 """
928- if not self .config .get ("log_file" ):
929- # If level was given as a str then convert to actual level
930- level = LOG_LEVELS .get (level , level )
931- syslog (level , f"{ msg } " )
932- else :
933- # Binary mode so fs encoding setting is not an issue
934- with self .config ["log_file" ].open ("ab" ) as f :
935- log_time = time .strftime ("%Y-%m-%d %H:%M:%S" )
936- # nice formatting of data structures using pretty print
937- if isinstance (msg , (dict , list , set , tuple )):
938- msg = pformat (msg )
939- # if multiline then start the data output on a fresh line
940- # to aid readability.
941- if "\n " in msg :
942- msg = "\n " + msg
943- out = f"{ log_time } { level .upper ()} { msg } \n "
944- try :
945- # Encode unicode strings to bytes
946- f .write (out .encode ("utf-8" ))
947- except (AttributeError , UnicodeDecodeError ):
948- # Write any byte strings straight to log
949- f .write (out )
970+ _logger .log (LOGGING_LEVELS .get (level , logging .DEBUG ), msg )
950971
951972 def create_output_modules (self ):
952973 """
0 commit comments