Skip to content

Commit 602a86e

Browse files
committed
python-stdlib/logging: add formatter, handlers
add formatting support and embedded device oriented handlers (circular log file, socket and syslog)
1 parent cdd260f commit 602a86e

File tree

4 files changed

+301
-23
lines changed

4 files changed

+301
-23
lines changed

python-stdlib/logging/example_logging.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#### Example 1 - basic stream log ####
12
import logging
23

34
logging.basicConfig(level=logging.INFO)
@@ -22,3 +23,103 @@ def emit(self, record):
2223

2324
logging.getLogger().addHandler(MyHandler())
2425
logging.info("Test message7")
26+
27+
28+
#### Example 2.1 - simple logger using basicConfig with explicit filename ####
29+
import logging
30+
logging.basicConfig(level=logging.INFO, filename="log.txt")
31+
logging.info("Test message logged into a file")
32+
33+
34+
#### Example 2.2 - simple logger using basicConfig with explicit stream ####
35+
import logging
36+
import sys
37+
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
38+
logging.info("Test message logged into sys.stderr")
39+
40+
41+
#### Example 2.3 - simple logger using basicConfig with both stream and filename ####
42+
import logging
43+
logging.basicConfig(level=logging.INFO, stream=sys.stderr, filename="log.txt")
44+
logging.info("Test message logged into a sys.stderr and a file")
45+
46+
47+
#### Example 2.4 - simple logger using basicConfig with custom format string, %-style ####
48+
import logging
49+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s", style="%")
50+
logging.info("Test message logged into sys.stderr, (%d) %s", 42, "foo")
51+
52+
53+
#### Example 2.4 - simple logger using basicConfig with custom format string, {-style ####
54+
import logging
55+
logging.basicConfig(level=logging.INFO, format="{asctime} {message}", style="{")
56+
logging.info("Test message logged into sys.stderr")
57+
58+
59+
#### Example 3 - Circular Log File with custom parameters ####
60+
import logging
61+
log = logging.getLogger("test")
62+
f = logging.Formatter(fmt="%(asctime)s %(uptime)s %(levelname)s %(message)s")
63+
h = logging.CircularFileHandler('log.txt', maxsize=512_000) # 512 kB log file
64+
h.setFormatter(f)
65+
log.addHandler(h)
66+
67+
log.info("Test message 1 logged into a file")
68+
log.info("Test message 2 logged into a file")
69+
70+
71+
#### Example 4 - log to a custom log receiver at 198.51.100.1 UDP port 3000 ####
72+
import logging
73+
log = logging.getLogger("test")
74+
log.addHandler(logging.SocketHandler('198.51.100.1', 3000))
75+
log.info("Test message 1 logged into a UDP socket")
76+
log.info("Test message 2 logged into a UDP socket")
77+
78+
79+
#### Example 5 - log to a custom log receiver at 198.51.100.1 TCP port 3000 ####
80+
import logging
81+
import socket
82+
log = logging.getLogger("test")
83+
log.addHandler(logging.SocketHandler('198.51.100.1', 3000, socktype=socket.SOCK_STREAM))
84+
log.info("Test message 1 logged into a TCP socket")
85+
log.info("Test message 2 logged into a TCP socket")
86+
87+
88+
#### Example 6 - log to a syslog server at 198.51.100.1 using UDP transport ####
89+
import logging
90+
log = logging.getLogger("test")
91+
log.addHandler(logging.SysLogHandler('198.51.100.1'))
92+
log.info("Test message 1 logged to a syslog server over UDP")
93+
log.info("Test message 2 logged to a syslog server over UDP")
94+
95+
96+
#### Example 7 - log to a syslog server at 198.51.100.1 using TCP transport ####
97+
import logging
98+
import socket
99+
import network
100+
101+
log_defaults = {
102+
"ip": network.WLAN(network.STA_IF).ifconfig()[0],
103+
"hostname": network.WLAN(network.STA_IF).config("dhcp_hostname"),
104+
}
105+
106+
log = logging.getLogger("test")
107+
f = logging.Formatter(fmt=logging.SYSLOG_FORMAT, defaults=log_defaults)
108+
h = logging.SysLogHandler('198.51.100.1', socktype=socket.SOCK_STREAM)
109+
h.setFormatter(f)
110+
log.addHandler(h)
111+
112+
log.info("Test message 1 logged to a syslog server over TCP")
113+
log.info("Test message 2 logged to a syslog server over TCP")
114+
115+
116+
#### Example 8 - Circular Log File with custom parameters and {-style formatting string ####
117+
import logging
118+
log = logging.getLogger("test")
119+
f = logging.Formatter(fmt="{asctime} {uptime} {message}", style="{")
120+
h = logging.CircularFileHandler('log.txt', maxsize=512_000)
121+
h.setFormatter(f)
122+
log.addHandler(h)
123+
124+
log.info("Test message 1 logged into a file")
125+
log.info("Test message 2 logged into a file")

python-stdlib/logging/logging.py

Lines changed: 198 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import os
2+
import socket
13
import sys
4+
import time
25

36
CRITICAL = 50
47
ERROR = 40
@@ -7,6 +10,14 @@
710
DEBUG = 10
811
NOTSET = 0
912

13+
SYSLOG_FORMAT = "<%(pri)s>1 %(asctime)s %(ip)s - - - - %(message)s"
14+
SYSLOG_DATE_FORMAT = "{0}-{1:02}-{2:02}T{3:02}:{4:02}:{5:02}Z"
15+
16+
DEFAULT_FORMAT = "%(levelname)s:%(name)s:%(message)s"
17+
DEFAULT_DATE_FORMAT = "%d-%02d-%02d %02d:%02d:%02d"
18+
19+
SYSLOG_LOCAL_0 = 16
20+
1021
_level_dict = {
1122
CRITICAL: "CRIT",
1223
ERROR: "ERROR",
@@ -15,7 +26,13 @@
1526
DEBUG: "DEBUG",
1627
}
1728

18-
_stream = sys.stderr
29+
_syslog_severity = {
30+
CRITICAL: 2,
31+
ERROR: 3,
32+
WARNING: 4,
33+
INFO: 6,
34+
DEBUG: 7,
35+
}
1936

2037

2138
class LogRecord:
@@ -30,8 +47,11 @@ class Handler:
3047
def __init__(self):
3148
pass
3249

33-
def setFormatter(self, fmtr):
34-
pass
50+
def setFormatter(self, fmt):
51+
self.formatter = fmt
52+
53+
def format(self, record):
54+
return self.formatter.format(record)
3555

3656

3757
class Logger:
@@ -57,19 +77,20 @@ def isEnabledFor(self, level):
5777

5878
def log(self, level, msg, *args):
5979
if self.isEnabledFor(level):
60-
levelname = self._level_str(level)
61-
if args:
62-
msg = msg % args
80+
d = self.record.__dict__
81+
d["levelname"] = self._level_str(level)
82+
d["levelno"] = level
83+
d["msg"] = msg
84+
d["args"] = args
85+
d["name"] = self.name
86+
d["created"] = time.time()
87+
d["uptime"] = time.ticks_ms() // 1000
88+
d["pri"] = (SYSLOG_LOCAL_0 << 3) + _syslog_severity[level]
6389
if self.handlers:
64-
d = self.record.__dict__
65-
d["levelname"] = levelname
66-
d["levelno"] = level
67-
d["message"] = msg
68-
d["name"] = self.name
6990
for h in self.handlers:
7091
h.emit(self.record)
7192
else:
72-
print(levelname, ":", self.name, ":", msg, sep="", file=_stream)
93+
print(_formatter.format(self.record), file=_stream)
7394

7495
def debug(self, msg, *args):
7596
self.log(DEBUG, msg, *args)
@@ -97,9 +118,152 @@ def addHandler(self, hndlr):
97118
self.handlers.append(hndlr)
98119

99120

121+
class Formatter():
122+
def __init__(self, fmt=None, datefmt=None, style="%", defaults=None):
123+
self.fmt = fmt or DEFAULT_FORMAT
124+
self.datefmt = datefmt or DEFAULT_DATE_FORMAT
125+
126+
if style not in ("%", "{"):
127+
raise ValueError("Style must be one of: %, {")
128+
129+
self.style = style
130+
self.defaults = defaults if defaults else {}
131+
132+
def usesTime(self):
133+
if self.style == "%":
134+
return "%(asctime)" in self.fmt
135+
elif self.style == "{":
136+
return "{asctime" in self.fmt
137+
138+
def format(self, record):
139+
# merge formatter defaults dict
140+
if self.defaults:
141+
record.__dict__.update(self.defaults)
142+
143+
# The message attribute of the record is computed using msg % args.
144+
if record.args:
145+
record.__dict__["message"] = record.msg % record.args
146+
else:
147+
record.__dict__["message"] = record.msg
148+
149+
# If the formatting string contains "(asctime)", formatTime() is called to
150+
# format the event time.
151+
if self.usesTime():
152+
record.__dict__["asctime"] = self.formatTime(record, self.datefmt)
153+
154+
# The record’s attribute dictionary is used as the operand to a string
155+
# formatting operation.
156+
if self.style == "%":
157+
return self.fmt % record.__dict__
158+
else:
159+
return self.fmt.format(**record.__dict__)
160+
161+
def formatTime(self, record, datefmt=None):
162+
if not datefmt:
163+
datefmt = self.datefmt
164+
created = time.gmtime(record.created)[:6]
165+
_result = datefmt.format(*created)
166+
return _result if _result != datefmt else datefmt % created
167+
168+
def formatException(self, exc_info):
169+
raise NotImplementedError()
170+
171+
def formatStack(self, stack_info):
172+
raise NotImplementedError()
173+
174+
175+
class StreamHandler(Handler):
176+
def __init__(self, stream=None):
177+
self.stream = stream or sys.stderr
178+
self.formatter = Formatter()
179+
180+
def emit(self, record):
181+
try:
182+
self.stream.write(self.format(record))
183+
except OSError:
184+
pass
185+
186+
def flush(self):
187+
pass
188+
189+
190+
class SocketHandler(Handler):
191+
def __init__(self, host, port, socktype=socket.SOCK_DGRAM):
192+
if socktype not in (socket.SOCK_STREAM, socket.SOCK_DGRAM):
193+
raise ValueError("Invalid socktype")
194+
195+
self.s = socket.socket(socket.AF_INET, socktype)
196+
self.addr = socket.getaddrinfo(host, port)[0][-1]
197+
self.terminator = ""
198+
self.socktype = socktype
199+
self.formatter = Formatter()
200+
self.connected = False
201+
202+
if socktype == socket.SOCK_STREAM:
203+
self.terminator = "\n" # adds PUSH flag to TCP packets
204+
try:
205+
self.s.connect(self.addr)
206+
self.connected = True
207+
except OSError as e:
208+
if e.errno == 118:
209+
print("No network connection. ", end="")
210+
elif e.errno == 113:
211+
print("Connection timeout. ", end="")
212+
else:
213+
print("Network error. ", end="")
214+
print("Cannot connect to server {0} on TCP port {1}.".format(self.addr[0], self.addr[1]))
215+
216+
def emit(self, record):
217+
try:
218+
if self.socktype == socket.SOCK_DGRAM or self.connected:
219+
self.s.sendto(self.format(record) + self.terminator, self.addr)
220+
except OSError:
221+
print("Network error, cannot send log to the server.")
222+
223+
224+
class SysLogHandler(SocketHandler):
225+
"""Mostly RFC 5424 compliant logging handler. Does not implement TLS-based transport."""
226+
def __init__(self, host, port=514, socktype=socket.SOCK_DGRAM):
227+
super().__init__(host, port, socktype)
228+
self.formatter = Formatter(fmt=SYSLOG_FORMAT, datefmt=SYSLOG_DATE_FORMAT, defaults={"ip": "-"})
229+
230+
231+
class CircularFileHandler(Handler):
232+
def __init__(self, filename, maxsize=128_000):
233+
if maxsize < 256:
234+
raise ValueError("maxsize must be at least 256 B")
235+
self.filename = filename
236+
self.maxsize = maxsize
237+
self.formatter = Formatter()
238+
239+
try:
240+
# checks if file exists and prevents overwriting it
241+
self.pos = os.stat(self.filename)[6]
242+
self.file = open(filename, "r+")
243+
except OSError:
244+
self.pos = 0
245+
self.file = open(filename, "w+")
246+
247+
self.file.seek(self.pos)
248+
249+
def emit(self, record):
250+
message = self.format(record)
251+
if len(message) + self.pos > self.maxsize:
252+
remaining = self.maxsize - self.pos
253+
self.file.write(message[:remaining])
254+
message = message[remaining:]
255+
self.pos = 0
256+
self.file.seek(self.pos)
257+
self.file.write(message)
258+
self.file.write("\n")
259+
self.file.flush()
260+
self.pos += len(message) + 1
261+
262+
263+
_stream = sys.stderr
100264
_level = INFO
101265
_loggers = {}
102-
266+
_formatter = Formatter()
103267

104268
def getLogger(name="root"):
105269
if name in _loggers:
@@ -108,21 +272,34 @@ def getLogger(name="root"):
108272
_loggers[name] = l
109273
return l
110274

275+
def critical(msg, *args):
276+
getLogger().critical(msg, *args)
277+
278+
def error(msg, *args):
279+
getLogger().error(msg, *args)
280+
281+
def warning(msg, *args):
282+
getLogger().warning(msg, *args)
111283

112284
def info(msg, *args):
113285
getLogger().info(msg, *args)
114286

115-
116287
def debug(msg, *args):
117288
getLogger().debug(msg, *args)
118289

119-
120-
def basicConfig(level=INFO, filename=None, stream=None, format=None):
121-
global _level, _stream
290+
def basicConfig(level=INFO, filename=None, stream=None, format=None, datefmt=None, style=None):
291+
global _level, _stream, _formatter
122292
_level = level
293+
getLogger().handlers = []
123294
if stream:
124295
_stream = stream
125-
if filename is not None:
126-
print("logging.basicConfig: filename arg is not supported")
127-
if format is not None:
128-
print("logging.basicConfig: format arg is not supported")
296+
if filename:
297+
if stream:
298+
getLogger().addHandler(StreamHandler(stream))
299+
getLogger().addHandler(CircularFileHandler(filename))
300+
if format:
301+
if not style:
302+
raise ValueError("style must be specified when the `format` option is used")
303+
_formatter = Formatter(fmt=format, datefmt=datefmt, style=style)
304+
for handler in getLogger().handlers:
305+
handler.setFormatter(_formatter)

python-stdlib/logging/metadata.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
srctype = micropython-lib
22
type = module
3-
version = 0.3
3+
version = 0.4

0 commit comments

Comments
 (0)