2
2
3
3
import argparse
4
4
import asyncio
5
+ from contextlib import suppress
5
6
import logging
7
+ from logging .handlers import RotatingFileHandler
6
8
import os
7
9
from pathlib import Path
10
+ import sys
11
+ import threading
12
+ from typing import Final
8
13
9
14
from aiorun import run
10
15
import coloredlogs
20
25
DEFAULT_LISTEN_ADDRESS = None
21
26
DEFAULT_STORAGE_PATH = os .path .join (Path .home (), ".matter_server" )
22
27
28
+ FORMAT_DATE : Final = "%Y-%m-%d"
29
+ FORMAT_TIME : Final = "%H:%M:%S"
30
+ FORMAT_DATETIME : Final = f"{ FORMAT_DATE } { FORMAT_TIME } "
31
+ MAX_LOG_FILESIZE = 1000000 * 10 # 10 MB
32
+
23
33
# Get parsed passed in arguments.
24
34
parser = argparse .ArgumentParser (
25
35
description = "Matter Controller Server using WebSockets."
86
96
87
97
88
98
def _setup_logging () -> None :
99
+ log_fmt = "%(asctime)s (%(threadName)s) %(levelname)s [%(name)s] %(message)s"
89
100
custom_level_style = {
90
101
** coloredlogs .DEFAULT_LEVEL_STYLES ,
91
102
"chip_automation" : {"color" : "green" , "faint" : True },
@@ -94,38 +105,68 @@ def _setup_logging() -> None:
94
105
"chip_error" : {"color" : "red" },
95
106
}
96
107
# Let coloredlogs handle all levels, we filter levels in the logging module
97
- coloredlogs .install (level = logging .NOTSET , level_styles = custom_level_style )
108
+ coloredlogs .install (
109
+ level = logging .NOTSET , level_styles = custom_level_style , fmt = log_fmt
110
+ )
111
+
112
+ # Capture warnings.warn(...) and friends messages in logs.
113
+ # The standard destination for them is stderr, which may end up unnoticed.
114
+ # This way they're where other messages are, and can be filtered as usual.
115
+ logging .captureWarnings (True )
98
116
99
- handlers = None
117
+ logging .basicConfig (level = args .log_level .upper ())
118
+ logger = logging .getLogger ()
119
+
120
+ # setup file handler
100
121
if args .log_file :
101
- handlers = [logging .FileHandler (args .log_file )]
102
- logging .basicConfig (handlers = handlers , level = args .log_level .upper ())
122
+ log_filename = os .path .join (args .log_file )
123
+ file_handler = RotatingFileHandler (
124
+ log_filename , maxBytes = MAX_LOG_FILESIZE , backupCount = 1
125
+ )
126
+ # rotate log at each start
127
+ with suppress (OSError ):
128
+ file_handler .doRollover ()
129
+ file_handler .setFormatter (logging .Formatter (log_fmt , datefmt = FORMAT_DATETIME ))
130
+ logger .addHandler (file_handler )
103
131
104
132
stack .init_logging (args .log_level_sdk .upper ())
105
- logging . getLogger () .setLevel (args .log_level .upper ())
133
+ logger .setLevel (args .log_level .upper ())
106
134
107
- if not logging . getLogger () .isEnabledFor (logging .DEBUG ):
135
+ if not logger .isEnabledFor (logging .DEBUG ):
108
136
logging .getLogger ("PersistentStorage" ).setLevel (logging .WARNING )
109
137
# Temporary disable the logger of chip.clusters.Attribute because it now logs
110
138
# an error on every custom attribute that couldn't be parsed which confuses people.
111
139
# We can restore the default log level again when we've patched the device controller
112
140
# to handle the raw attribute data to deal with custom clusters.
113
141
logging .getLogger ("chip.clusters.Attribute" ).setLevel (logging .CRITICAL )
114
- if not logging .getLogger ().isEnabledFor (logging .DEBUG ):
115
142
# (temporary) raise the log level of zeroconf as its a logs an annoying
116
143
# warning at startup while trying to bind to a loopback IPv6 interface
117
144
logging .getLogger ("zeroconf" ).setLevel (logging .ERROR )
118
145
146
+ # register global uncaught exception loggers
147
+ sys .excepthook = lambda * args : logger .exception (
148
+ "Uncaught exception" ,
149
+ exc_info = args ,
150
+ )
151
+ threading .excepthook = lambda args : logger .exception (
152
+ "Uncaught thread exception" ,
153
+ exc_info = ( # type: ignore[arg-type]
154
+ args .exc_type ,
155
+ args .exc_value ,
156
+ args .exc_traceback ,
157
+ ),
158
+ )
159
+
119
160
120
161
def main () -> None :
121
162
"""Run main execution."""
122
163
123
- _setup_logging ()
124
-
125
164
# make sure storage path exists
126
165
if not os .path .isdir (args .storage_path ):
127
166
os .mkdir (args .storage_path )
128
167
168
+ _setup_logging ()
169
+
129
170
# Init server
130
171
server = MatterServer (
131
172
args .storage_path ,
0 commit comments