Wiznet makers

ruilixin6

Published August 24, 2026 ©

187 UCC

0 VAR

0 Contests

0 Followers

0 Following

Original Link

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

COMPONENTS
PROJECT DESCRIPTION

【Preliminary Note】The original hardware example in this article was written based on the RP2040. The actual hardware used in this hands-on demonstration features the W55RP20 as the main controller chip. The circuit logic and UF2 flashing operation principles are universally applicable, with only the main controller model differing. The original chip model mentioned in the circuit descriptions below is provided for reference purposes only.
 

The following code implements a custom logging module with these capabilities:

1.PNG

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 ===========================================
  1. Implementation principle of logger class

The logger class provides these member methods:

2.png

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

3.png

@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.

4.png

@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.

5.png

  1. Acquire mutex lock to eliminate multi‑thread race‑conditions. Evaluate log‑rotation criteria.
  2. Select output destination:
    1. terminal mode: directly print message
    2. file mode: write text into open log‑file handle
  3. 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.

6.png

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

7.png

_open_log_file() workflow:

  1. Acquire local‑time timestamp and build unique filename YYYYMMDD_HHMMSS.
  2. Branch for terminal output or file‑output path.
  3. Open target file in append mode. Count existing newline characters to restore log‑entry counter.
  4. 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.

8.png

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.

  1. 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

9.png 10.png 11.png

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}"
  1. 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:

13.png

After flashing firmware and opening serial‑terminal:

os.listdir() reveals multiple timestamp‑suffixed log‑files.

15.png

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.

mmexport1782423889111.gif

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).

21.png

@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")

22.png

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.

Documents
Comments Write