Must‑Have MicroPython Project: Step‑by‑Step Build uLogLite Logger with Level / Rotation / Filter
Hands‑on tutorial to implement uLogLite lightweight logger for MicroPython, covering log level, log rotation, filter, formatter, thread‑safety and exception
The following code implements a custom logging module with these capabilities:

Define log levels: define levels such as DEBUG, INFO, WARNING, ERROR and CRITICAL. Implement log filters: filter log messages based on log level. Implement log formatter: format output log messages. Build logger: combine logger instance, filters and formatter. Log file rotation: automatically create new log file when file reaches maximum entry count or rotation time interval, reset log entry counter. Exception handling: record exception stack trace, optional terminal printout for exceptions. Thread safety: use mutex lock to prevent race‑conditions during multi‑thread log writing.
Sample code:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/16 2:40 PM
# @Author : Li Qingshui
# @File : logger.py
# @Description : Logging module supporting level classification, log filters and formatters
# ======================================== Import related modules =========================================
# Import time module for timestamp acquisition
import time
# Import os module for file operations
import os
# Import thread and lock support
import _thread
# Import system‑related module
import sys
# Import micropython module for code optimisation
import micropython
# ======================================== Global variables ============================================
# ======================================== Function definitions ============================================
# ======================================== Custom classes ============================================
# Define log level constants
class LogLevel:
DEBUG = 1 # Debug level
INFO = 2 # Information level
WARNING = 3 # Warning level
ERROR = 4 # Error level
CRITICAL = 5 # Critical error level
# Logger implementation class
class uLogLite:
"""
Light‑weight logger class providing multi‑level logging, filtering, formatting, file rotation and thread‑safe output.
This class encapsulates full logging workflow including level filtering, message formatting, file output and rotation management.
Supports filter‑chains for flexible log filtering and custom formatters for output styling.
Attributes:
name (str): Logger name prefix used for log filenames
level (int): Current active log level (LogLevel enum value)
filters (List[Callable[[int, str], bool]]): List of filter callback functions
formatter (Optional[object]): Formatter object, must implement format(level,message)
output (str): Output target. "terminal" for console, other string means single‑level folder path
max_logs (int): Maximum log entries per single log file
rotate_interval (int): Log‑file rotation interval in hours
log_count (int): Count of entries written into current log file
last_rotation_time (float): UNIX timestamp of last rotation event
lock (Lock): Thread mutex lock object
log_file (Optional[TextIO]): File handle for active log file
Methods:
__init__(self, name: str, level: int = LogLevel.DEBUG, output: str = "terminal",
max_logs: int = 1000, rotate_interval: int = 24) -> None:
Initialise logger instance
add_filter(self, log_filter: Callable[[int, str], bool]) -> None:
Append log filter function
set_formatter(self, formatter: object) -> None:
Assign log formatter instance
log(self, level: int, message: str) -> None:
Base logging entry‑point method
debug(self, message: str) -> None:
Write DEBUG level log
info(self, message: str) -> None:
Write INFO level log
warning(self, message: str) -> None:
Write WARNING level log
error(self, message: str) -> None:
Write ERROR level log
critical(self, message: str) -> None:
Write CRITICAL level log
exception(self, exc: Exception, terminal_display: bool = True) -> None:
Record exception stack trace
close(self) -> None:
Safely close open log file
Private Methods:
_open_log_file(self) -> None:
Open / create log file and initialise entry counter
__is_single_level_path(self, path: str) -> bool:
Validate single‑level directory path (MicroPython filesystem constraint)
_apply_filters(self, level: int, message: str) -> bool:
Execute all registered filters
_format_message(self, level: int, message: str) -> str:
Generate formatted log string
_write_log(self, formatted_message: str) -> None:
Thread‑safe log write routine
_should_rotate(self) -> bool:
Evaluate whether rotation condition is satisfied
_rotate_log_file(self) -> None:
Perform log‑file rotation workflow
"""
def __init__(self, name: str, level: int = LogLevel.DEBUG, output: str = "terminal",
max_logs: int = 1000, rotate_interval: int = 24) -> None:
"""
Logger initialisation.
Args:
name (str): Logger identifier name
level (int): Minimum logging level from LogLevel enumeration
output (str): "terminal" or single‑level output directory path
max_logs (int): Maximum entries per log file
rotate_interval (int): Rotation interval measured in hours
Raises:
OSError: File creation failure
ValueError: Empty name string
TypeError: Invalid argument type for name / level / output
"""
if not name or name == "":
raise ValueError("name cannot be empty")
if not isinstance(name, str):
raise TypeError("name must be a string")
if not isinstance(level, int) or level not in [getattr(LogLevel, attr) for attr in dir(LogLevel) if
not attr.startswith('__')]:
raise TypeError("level must be an integer of LogLevel enum")
if not isinstance(output, str):
raise TypeError("output must be a string")
if output != "terminal":
if not self._is_single_level_path(output):
raise ValueError(
"Output path must be a single‑level directory\n"
"Examples: 'logs', '/data', 'mylog'"
)
self.name = name
self.level = level
self.filters = []
self.formatter = None
self.output = output
self.max_logs = max_logs
self.rotate_interval = rotate_interval
self.log_count = 0
self.last_rotation_time = time.time()
self.lock = _thread.allocate_lock()
self._open_log_file()
@staticmethod
def _is_single_level_path(path: str) -> bool:
"""
Validate single‑level directory path for MicroPython filesystem.
Valid formats:
"logs" Relative path under current directory
"/logs" Absolute path under root
"" or "." Current working directory
"/" Root directory
Args:
path (str): Input path string
Returns:
bool: True for valid single‑level path, False for multi‑level or illegal characters
"""
if not path or path == "." or path == "/":
return True
normalized = path.strip("/")
return "/" not in normalized and "\\" not in normalized
@micropython.native
def _open_log_file(self) -> None:
"""
Create or open log file. Count existing lines if file already exists, reset counter for new file.
Notes:
Filename format: {name}_{YYYYMMDD_HHMMSS}.txt
Automatically fall back to terminal output upon file‑open failure.
"""
t = time.localtime()
timestamp = "{:04}{:02}{:02}_{:02}{:02}{:02}".format(t[0], t[1], t[2], t[3], t[4], t[5])
if self.output == "terminal":
self.log_file = None
self.log_count = 0
else:
log_filename = f"{self.output}/{self.name}_{timestamp}.txt"
try:
os.mkdir(self.output)
except OSError:
pass
try:
try:
os.stat(log_filename)
file_exists = True
except OSError:
file_exists = False
self.log_file = open(log_filename, 'a')
if file_exists:
count = 0
with open(log_filename, 'r') as f:
while True:
chunk = f.read(128)
if not chunk:
break
count += chunk.count('\n')
self.log_count = count
else:
self.log_count = 0
except OSError as e:
print(f"cannot open file {log_filename}: {e}")
self.log_file = None
def add_filter(self, log_filter: callable[[int, str], bool]) -> None:
"""
Register new log filter callback.
Args:
log_filter (callable): Receives (level, message), returns boolean result
"""
self.filters.append(log_filter)
def set_formatter(self, formatter: object) -> None:
"""
Assign custom log formatter instance.
Args:
formatter (object): Must implement callable format(level,message) method
Raises:
TypeError: Formatter lacks required format() method
"""
if not hasattr(formatter, "format") or not callable(getattr(formatter, "format")):
raise TypeError("formatter must implement a callable format(level, message) method")
self.formatter = formatter
def _apply_filters(self, level: int, message: str) -> bool:
"""
Execute all registered filters. Return False if any filter rejects message.
Args:
level [int]: Log level identifier
message [str]: Raw log text
Returns:
bool: True only when all filters pass the message
"""
for log_filter in self.filters:
if not log_filter(level, message):
return False
return True
@micropython.native
def _format_message(self, level: int, message: str) -> str:
"""
Produce final formatted log string. Use custom formatter if assigned else default template.
"""
if self.formatter:
return self.formatter.format(level, message)
return f"{self.name} - {level}: {message}"
@micropython.native
def _write_log(self, formatted_message: str) -> None:
"""
Thread‑safe log output routine. Handles rotation, file or console output.
Workflow:
1. Acquire thread mutex lock
2. Check rotation trigger conditions
3. Perform actual output
4. Increment entry counter
5. Release lock
Notes:
Automatic log rotation. Fallback to terminal upon file write failure.
"""
with self.lock:
if self.output != "terminal" and (self.log_count >= self.max_logs or self._should_rotate()):
self._rotate_log_file()
if self.output == "terminal":
print(formatted_message)
else:
if self.log_file:
self.log_file.write(formatted_message + "\n")
self.log_file.flush()
self.log_count += 1
else:
print("can not open file, output to terminal:", formatted_message)
def _rotate_log_file(self) -> None:
"""
Execute log‑file rotation: close current file, create new timestamp‑named log file, reset counters.
"""
if self.log_file:
self.log_file.close()
self._open_log_file()
self.log_count = 0
self.last_rotation_time = time.time()
@micropython.native
def _should_rotate(self) -> bool:
"""
Evaluate whether time‑based rotation condition is satisfied.
Returns True when rotation interval elapsed or hour boundary crossed.
"""
current_hour = time.localtime()[3]
last_hour = time.localtime(self.last_rotation_time)[3]
time_diff = time.time() - self.last_rotation_time
return (time_diff >= self.rotate_interval * 3600) or \
(current_hour != last_hour and time_diff >= 3600)
@micropython.native
def log(self, level: int, message: str) -> None:
"""
Core logging entry‑point. Validates arguments, applies filters and triggers output.
Args:
level (int): LogLevel enumeration integer
message (str): Log content string
Raises:
TypeError: Invalid level value
ValueError: Empty or non‑string message
"""
if not isinstance(level, int) or level not in [getattr(LogLevel, attr) for attr in dir(LogLevel) if
not attr.startswith('__')]:
raise TypeError("level must be an integer of LogLevel enum")
if not isinstance(message, str) or message == "":
raise ValueError("message must be a string")
if level >= self.level and self._apply_filters(level, message):
formatted_message = self._format_message(level, message)
self._write_log(formatted_message)
def debug(self, message: str) -> None:
"""Emit DEBUG log entry."""
self.log(LogLevel.DEBUG, message)
def info(self, message: str) -> None:
"""Emit INFO log entry."""
self.log(LogLevel.INFO, message)
def warning(self, message: str) -> None:
"""Emit WARNING log entry."""
self.log(LogLevel.WARNING, message)
def error(self, message: str) -> None:
"""Emit ERROR log entry."""
self.log(LogLevel.ERROR, message)
def critical(self, message: str) -> None:
"""Emit CRITICAL log entry."""
self.log(LogLevel.CRITICAL, message)
def exception(self, exc: Exception, terminal_display: bool = True) -> None:
"""
Record exception stack trace. Write to log file if file output enabled.
Args:
exc (Exception): Exception object instance
terminal_display (bool): Set True to also print traceback to console
"""
if isinstance(exc, Exception):
if self.output != "terminal" and self.log_file:
sys.print_exception(exc, self.log_file)
if terminal_display:
sys.print_exception(exc)
def close(self) -> None:
"""Safely close open log file handle."""
if self.output != "terminal" and self.log_file:
self.log_file.close()
# ======================================== Initialization ==========================================
# ======================================== Main program ===========================================
- Implementation principle of logger class
The logger class provides these member methods:

Convenience methods debug(), info(), warning(), error(), critical() internally invoke the base log() method.

@micropython.native
def log(self, level: int, message: str) -> None:
"""
Base logging entry‑point.
Args:
level (int): LogLevel enumeration integer
message (str): Log content string
Returns:
None
Raises:
TypeError: Invalid level value
ValueError: Empty or non‑string message
"""
if not isinstance(level, int) or level not in [getattr(LogLevel, attr) for attr in dir(LogLevel) if
not attr.startswith('__')]:
raise TypeError("level must be an integer of LogLevel enum")
if not isinstance(message, str) or message == "":
raise ValueError("message must be a string")
if level >= self.level and self._apply_filters(level, message):
formatted_message = self._format_message(level, message)
self._write_log(formatted_message)
log() validates inputs, runs filter checks then invokes _write_log() for actual output.

@micropython.native
def _write_log(self, formatted_message: str) -> None:
"""
Thread‑safe log output routine. Handles rotation, file or console output.
Workflow:
1. Acquire thread mutex lock
2. Check rotation trigger conditions
3. Perform actual output
4. Increment entry counter
5. Release lock
Notes:
Automatic log rotation. Fallback to terminal upon file write failure.
"""
with self.lock:
if self.output != "terminal" and (self.log_count >= self.max_logs or self._should_rotate()):
self._rotate_log_file()
if self.output == "terminal":
print(formatted_message)
else:
if self.log_file:
self.log_file.write(formatted_message + "\n")
self.log_file.flush()
self.log_count += 1
else:
print("can not open file, output to terminal:", formatted_message)
_write_log() writes formatted text either to console or log‑file.

- Acquire mutex lock to eliminate multi‑thread race‑conditions. Evaluate log‑rotation criteria.
- Select output destination:
- terminal mode: directly print message
- file mode: write text into open log‑file handle
- When writing to file, invoke flush() to push buffered data to storage, increment log‑entry counter.
If rotation threshold (max entries or time interval) is reached, _rotate_log_file() closes current log, calls _open_log_file() to create new timestamp‑named log file and resets counters.

def _rotate_log_file(self) -> None:
"""
Perform log‑file rotation: close current file, create new timestamp‑named log file, reset counters.
"""
if self.log_file:
self.log_file.close()
self._open_log_file()
self.log_count = 0
self.last_rotation_time = time.time()
@micropython.native
def _should_rotate(self) -> bool:
"""
Evaluate whether time‑based rotation condition is satisfied.
Returns True when rotation interval elapsed or hour boundary crossed.
"""
current_hour = time.localtime()[3]
last_hour = time.localtime(self.last_rotation_time)[3]
time_diff = time.time() - self.last_rotation_time
return (time_diff >= self.rotate_interval * 3600) or \
(current_hour != last_hour and time_diff >= 3600)
_open_log_file() generates timestamp‑based filename. If file already exists it counts existing newline characters to restore log‑entry counter value.
@micropython.native
def _open_log_file(self) -> None:
"""
Create or open log file. Count existing lines if file already exists, reset counter for new file.
Notes:
Filename format: {name}_{YYYYMMDD_HHMMSS}.txt
Automatically fall back to terminal output upon file‑open failure.
"""
t = time.localtime()
timestamp = "{:04}{:02}{:02}_{:02}{:02}{:02}".format(t[0], t[1], t[2], t[3], t[4], t[5])
if self.output == "terminal":
self.log_file = None
self.log_count = 0
else:
log_filename = f"{self.output}/{self.name}_{timestamp}.txt"
try:
os.mkdir(self.output)
except OSError:
pass
try:
try:
os.stat(log_filename)
file_exists = True
except OSError:
file_exists = False
self.log_file = open(log_filename, 'a')
if file_exists:
count = 0
with open(log_filename, 'r') as f:
while True:
chunk = f.read(128)
if not chunk:
break
count += chunk.count('\n')
self.log_count = count
else:
self.log_count = 0
except OSError as e:
print(f"cannot open file {log_filename}: {e}")
self.log_file = None

_open_log_file() workflow:
- Acquire local‑time timestamp and build unique filename
YYYYMMDD_HHMMSS. - Branch for terminal output or file‑output path.
- Open target file in append mode. Count existing newline characters to restore log‑entry counter.
- Catch OSError exceptions and degrade gracefully to terminal output when file operations fail.
exception() method captures exception traceback:
def exception(self, exc: Exception, terminal_display: bool = True) -> None:
"""
Record exception stack trace. Write to log file if file output enabled.
Args:
exc (Exception): Exception object instance
terminal_display (bool): Set True to also print traceback to console
"""
if isinstance(exc, Exception):
if self.output != "terminal" and self.log_file:
sys.print_exception(exc, self.log_file)
if terminal_display:
sys.print_exception(exc)
sys.print_exception() writes full traceback either to file handle or console.
LogLevel is implemented as standalone class rather than class variables inside logger.

Reasons: Separation of concerns: LogLevel class purely manages enumeration constants, logger class focuses on logging business logic. Better extensibility: Adding new log‑levels only requires modifying LogLevel without touching logger source code.
- Implementation notes for log filters and formatters
Complete logging system requires cooperation between logger, filters and formatters.
Logger: manage event records, support level filtering, time‑ / count‑based rotation, output to terminal or file, thread‑safe writes. Log filter: predicate deciding whether given message shall be logged. User may implement filters based on level, keywords, timestamps, message length. Log formatter: defines output text layout, inject timestamps, ANSI colour codes etc.
Interface requirements:
Log filter: callable accepting (level:int, message:str), returns boolean. Return True = keep log, False = discard log. Sample filter implementations:
# Log‑level filter
def level_filter(level: int, message: str) -> bool:
"""
Simple filter: accept messages of INFO level and above
"""
return level >= LogLevel.INFO
# Message‑length filter factory
def length_filter(max_length: int) -> callable:
def filter_fn(level, message):
return len(message) <= max_length
return filter_fn
# Time‑window filter factory (24‑hour format)
def time_range_filter(start_time: str, end_time: str) -> callable:
def filter_fn(level, message):
current_time = time.localtime()
current_hour_minute = f"{current_time[3]:02}:{current_time[4]:02}"
return start_time <= current_hour_minute <= end_time
return filter_fn
# Keyword‑match filter factory
def keyword_filter(keywords: list) -> callable:
def filter_fn(level, message):
return any(keyword in message for keyword in keywords)
return filter_fn
Closure mechanism explanation: When invoking logger.add_filter(length_filter(50)), length_filter executes and returns inner function filter_fn. This inner function is appended into self.filters list. Only later inside _apply_filters() does the code invoke filter_fn(level, message). Outer‑function variables are preserved by Python closure.
def _apply_filters(self, level, message):
for log_filter in self.filters:
if not log_filter(level, message):
return False
return True

Log formatter requirement: class implementing format(level, message) returning formatted string. Example:
class TimeMessageFormatter:
"""
Simple formatter producing timestamp‑prefixed log lines
Output format: "YYYY‑MM‑DD HH:MM:SS - message"
"""
def format(self, level: int, message: str) -> str:
local_time = time.localtime()
current_time = "{:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
local_time[0],
local_time[1],
local_time[2],
local_time[3],
local_time[4],
local_time[5]
)
return f"{current_time} - {message}"
- Practical experiment with logger class
Demo source code resides inside resource‑package path elegance‑devkit v1\Demo\71 FileSys_Logger.
main.py sample:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/16 2:40 PM
# @Author : Li Qingshui
# @File : main.py
# @Description : Filesystem demo using custom logger module
# ======================================== Import related modules =========================================
from logger import uLogLite, LogLevel
import os
import time
import machine
# ======================================== Global variables ============================================
logfile_path = "/logs"
os.mkdir(logfile_path)
# ======================================== Function definitions ============================================
def level_filter(level: int, message: str) -> bool:
return level >= LogLevel.INFO
def length_filter(max_length: int) -> callable:
def filter_fn(level, message):
return len(message) <= max_length
return filter_fn
def time_range_filter(start_time: str, end_time: str) -> callable:
def filter_fn(level, message):
current_time = time.localtime()
current_hour_minute = f"{current_time[3]:02}:{current_time[4]:02}"
return start_time <= current_hour_minute <= end_time
return filter_fn
def keyword_filter(keywords: list) -> callable:
def filter_fn(level, message):
return any(keyword in message for keyword in keywords)
return filter_fn
def print_txt_files_in_directory(directory: str) -> None:
try:
files = os.listdir(directory)
for file in files:
if file.endswith('.txt'):
file_path = directory + '/' + file
print(f"Reading file: {file_path}")
try:
with open(file_path, 'r') as f:
for line in f:
print(line, end='')
except OSError as e:
print(f"Error reading file {file_path}: {e}")
print()
except OSError as e:
print(f"Error listing directory {directory}: {e}")
# ======================================== Custom classes ============================================
class ColoredFormatter:
"""
ANSI‑colour terminal formatter adding timestamp and level‑based text colours
"""
COLORS = {
LogLevel.DEBUG: '\033[94m',
LogLevel.INFO: '\033[92m',
LogLevel.WARNING: '\033[93m',
LogLevel.ERROR: '\033[91m',
LogLevel.CRITICAL: '\033[41m',
"ENDC": '\033[0m'
}
def format(self, level: int, message: str) -> str:
local_time = time.localtime()
timestamp = "{:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
local_time[0],
local_time[1],
local_time[2],
local_time[3],
local_time[4],
local_time[5]
)
color = self.COLORS.get(level, self.COLORS["ENDC"])
return f"{color} [log]: {timestamp} - {level}: {message}{self.COLORS['ENDC']}"
# ======================================== Initialization ==========================================
time.sleep(3)
print("FreakStudio : Using logger module to save status of system")
rtc = machine.RTC()
rtc.datetime((2024, 9, 17, 0, 14, 40, 0, 0))
logger = uLogLite("MyLogger", LogLevel.INFO, logfile_path, max_logs=20)
logger.add_filter(level_filter)
logger.add_filter(length_filter(50))
logger.add_filter(keyword_filter(["FreakStudio"]))
logger.set_formatter(ColoredFormatter())
# ======================================== Main program ===========================================
print("Test logger module output log")
logger.debug("FreakStudio : This is a debug message")
time.sleep_ms(30)
logger.info("FreakStudio : This is an info message")
logger.info("FreakStudio : This is a very very long long long long long info message,"
"and the length is more than 50")
time.sleep_ms(30)
logger.warning("FreakStudio : This is a warning message")
logger.warning("This is a warning message")
time.sleep_ms(30)
logger.error("FreakStudio : This is an error message")
time.sleep_ms(30)
logger.critical("FreakStudio : This is a critical message")
time.sleep_ms(30)
print("Test logger rotate log file")
for i in range(20):
logger.debug(f"FreakStudio : This is a debug message {i}")
time.sleep_ms(100)
logger.info(f"FreakStudio : This is an info message {i}")
time.sleep_ms(100)
logger.warning(f"FreakStudio : This is a warning message {i}")
time.sleep_ms(100)
logger.error(f"FreakStudio : This is an error message {i}")
time.sleep_ms(100)
logger.critical(f"FreakStudio : This is a critical message {i}")
time.sleep_ms(100)
print("Test logger record exception stack")
try:
1 / 0
except Exception as e:
logger.exception(e, terminal_display=True)
logger.close()
print("FreakStudio : logging file write done")
Workflow of demo code: Instantiate logger object with max‑entries‑per‑file set to 20, output path /logs. Attach multiple filters and coloured formatter. Set RTC datetime. Emit test log entries to verify filter and formatter behaviour. Loop generating many log entries to trigger automatic rotation. Force division‑by‑zero exception to test exception‑trace recording. Close log‑file handle.
ColouredFormatter uses ANSI escape sequences for terminal text colouring.
class ColoredFormatter:
"""
ANSI‑colour terminal formatter adding timestamp and level‑based text colours
"""
COLORS = {
LogLevel.DEBUG: '\033[94m',
LogLevel.INFO: '\033[92m',
LogLevel.WARNING: '\033[93m',
LogLevel.ERROR: '\033[91m',
LogLevel.CRITICAL: '\033[41m',
"ENDC": '\033[0m'
}
def format(self, level: int, message: str) -> str:
local_time = time.localtime()
timestamp = "{:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
local_time[0],
local_time[1],
local_time[2],
local_time[3],
local_time[4],
local_time[5]
)
color = self.COLORS.get(level, self.COLORS["ENDC"])
return f"{color} [log]: {timestamp} - {level}: {message}{self.COLORS['ENDC']}"
print_txt_files_in_directory() scans target folder and prints content of all .txt files:
def print_txt_files_in_directory(directory: str) -> None:
try:
files = os.listdir(directory)
for file in files:
if file.endswith('.txt'):
file_path = directory + '/' + file
print(f"Reading file: {file_path}")
try:
with open(file_path, 'r') as f:
for line in f:
print(line, end='')
except OSError as e:
print(f"Error reading file {file_path}: {e}")
print()
except OSError as e:
print(f"Error listing directory {directory}: {e}")
System workflow diagram:

After flashing firmware and opening serial‑terminal:
os.listdir() reveals multiple timestamp‑suffixed log‑files.

Calling print_txt_files_in_directory() prints stored log content.
Messages rejected by filters are absent from output:
logger.debug("FreakStudio : This is a debug message")
logger.info("FreakStudio : This is a very very long long long long long info message,and the length is more than 50")
logger.warning("This is a warning message")
Log entries are split across multiple rotated log‑files.
Exception tracebacks are persisted into log‑files.

Note: appending to growing log‑files increases seek‑and‑write overhead on flash‑based filesystems. Measuring write latency inside _write_log() demonstrates this effect.
os.stat() returns filesystem metadata tuple (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime).

@micropython.native
def _write_log(self, formatted_message: str) -> None:
"""
线程安全的日志写入操作,根据日志输出方式选择写入方式,可以选择写入文件或终端。
执行流程:
1. 获取线程锁
2. 检查轮转条件
3. 执行实际写入
4. 更新计数器
5. 释放锁
Args:
formatted_message [str]: 格式化后的完整日志条目。
Returns:
None
Notes:
- 自动处理日志轮转
- 写入失败时降级到终端输出
"""
# 锁定线程,确保线程安全
with self.lock:
# 记录日志写入开始时间
start_time = time.ticks_us()
... ...
# 记录日志写入结束时间
end_time = time.ticks_us()
# 计算写入日志耗时
# 计算耗时
write_time = time.ticks_diff(end_time, start_time)
# 打印耗时,单位是ms
print(f"write log cost {write_time / 1000} ms")
Custom filter‑implementation guidelines: Support composite conditions: log‑level, keyword strings, time‑window, message length etc. Unified calling interface: accept level and message parameters, return boolean result. Composable: multiple filters are AND‑logic combined; message must pass every filter rule.
Custom formatter options beyond ANSI‑coloured terminal output: JSON structured logging for machine parsing. Plain text output with timestamp + level + message. User‑defined template including device‑id, session identifiers.
Comparison against built‑in MicroPython logging module: uLogLite implements core features: log‑levels, formatting, file / console handlers, exception capture, thread‑safety and log rotation. The official MicroPython logging module supplies richer built‑in handlers and filter classes for complex production‑grade scenarios.
