├── .gitattributes ├── .gitignore ├── LICENSE ├── README-ZH.md ├── README.md ├── setup.py ├── src └── logo_transparent.png └── ulogger └── __init__.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | test.py 116 | logging.log 117 | .vscode/ 118 | *.mpy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Youkii-Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | # micropython-ulogger 2 | 在 `micropython` 上做logging不是一件容易的事情, `micropython` 有很多尚未完成的接口, 因此能记录到的日志内容非常有限, 我根据 `micropython` 的情况设计了这个 `ulogger` 的库. 3 | 4 | [English](./README.md)|**简体中文** 5 | ![LOGO](./src/logo_transparent.png) 6 | 7 | ## 特色: 8 | 在 `micropython` 中, 由于单片机的计算能力有限, 一切事情都需要快速地被处理和响应, 因此本模块设计的目的就是减少无所谓的操作. 因此本模块会和 `CPython` 的标准库 `logging` 有很大的不同. 9 | 10 | 11 | 12 | ## 如何安装? 13 | ### 通过 `pypi` 安装 14 | ```python 15 | #repl on your board 16 | import upip 17 | upip.install("micropython-ulogger") 18 | ``` 19 | 20 | ### 手动安装(推荐) 21 | 22 | 请在本项目的 [release](https://github.com/Li-Lian1069/micropython-ulogger/releases) 中下载一个最新版的`.mpy`文件, 将其放到开发板的 `/lib` 目录中或您程序的目录中. 23 | 24 | **注意**: 推荐使用 `.mpy` 的文件, 这是针对 `micropython` 已经事先编译好的文件, 可以有效提高执行速度和内存开销. 25 | 26 | 27 | 28 | ## 如何使用? 29 | ### 快速入门 30 | 这是一个最简单的例子: 31 | ```python 32 | import ulogger 33 | loggor = ulogger.Loggor(__name__) 34 | loggor.info("hello world") 35 | ``` 36 | 在上面的例子中, 一切将会使用默认的参数(级别为INFO, 输出到终端中.) 37 | 38 | 39 | ### Handler的使用 40 | **现在. 我们来给他加点料** 41 | ```python 42 | import ulogger 43 | 44 | handler_to_term = ulogger.Handler( 45 | level=ulogger.INFO, 46 | colorful=True, 47 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 48 | clock=None, 49 | direction=ulogger.TO_TERM, 50 | ) 51 | handler_to_file = ulogger.Handler( 52 | level=ulogger.INFO, 53 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 54 | clock=None, 55 | direction=ulogger.TO_FILE, 56 | file_name="logging.log", 57 | max_file_size=1024 # max for 1k 58 | ) 59 | logger = ulogger.Logger( 60 | name = __name__, 61 | handlers = ( 62 | handler_to_term, 63 | handler_to_file 64 | ) 65 | ) 66 | 67 | logger.info("hello", "world") 68 | # 支持多参数填入 69 | ``` 70 | 在 `ulogger` 中, 有一点与 `CPython` 的 `logging` 模块不一样: 在 `logging` 模块中, `formatter` 和 `handler` 是分开的, 但是在本模块中, 我将两者合为一体, 这可以减少日志模块的工作量(你肯定不希望你的开发板花费太多时间在记录日志吧!), 当然这会减少可配置性, 但是我们总是要为了提高性能付出一点代价. 71 | 72 | 73 | 74 | #### Handler可以使用的参数: 75 | ```python 76 | # default args 77 | ulogger.Handler( 78 | level: int = INFO, 79 | colorful: bool = True, 80 | fmt: str = "&(time)% - &(level)% - &(name)% - &(msg)%", 81 | clock: BaseClock = None, 82 | direction: int = TO_TERM, 83 | file_name: str = "logging.log", 84 | max_file_size: int = 4096 85 | ) 86 | 87 | ``` 88 | - level: 指定这个Handler接受的最低等级, 可选: 89 | - ulogger.DEBUG 90 | - ulogger.INFO 91 | - ulogger.WARN 92 | - ulogger.ERROR 93 | - ulogger.CRITICAL 94 | 95 | - colorful: 指定是否启用控制台文本颜色(仅在 `direction=TO_TERM` 时可用) 96 | 97 | - fmt: 设置输出的文本格式, 支持内置变量, 目前支持: 98 | - `&(time)%`: 打印时间戳, 关于时间的格式详见 `clock` 参数. 99 | - `&(level)%`: 打印消息的级别 100 | - `&(name)%`: 打印Logger的名字, 在实例化Logger时提供. 101 | - `&(fnname)%`: 打印函数名, 由于`micropython`目前尚未支持 `traceback` 和更高级的错误管理, 因此需要手动提供信息. 如果在调用记录时未提供本项, 默认值为 `unknownfn`, 例子: 102 | ```python 103 | def hello(): 104 | logger.info("in hello", fn=hello.__name__) 105 | # or 106 | logger.info("in hello", fn="hello") 107 | # or fill in what you want 108 | logger.info("in hello", fn="world") 109 | ``` 110 | - `&(msg)%`: 打印消息, 即你在 `info()` 中填入的信息 111 | 112 | - clock: 由于单片机没有一直通电的 `rtc` 模块, 因此它的时间并不一定是与国际时间同步的, 因此我们开放了时钟接口, 当每次需要记录 `&(time)%` 的时候我们会从这个时钟中获取时间 113 | 114 | - direction: 设置文本输出的方向, 可选 `TO_TERM`(输出到终端) 或 `TO_FILE`(输出到文件) 115 | 116 | - file_name: 输出的文件名, 在 `TO_FILE` 时才会被启用, 可以是相对路径也可以是绝对路径. 117 | 118 | - max_file_size: 设置最大文件大小(单位:byte), 当文件大小超过上限的时候, 我们会自动清空文件. 119 | 120 | 121 | 122 | ### Clock的使用 123 | 也许你已经注意到在 `Handler` 中的 `clock` 参数, 我们提供了一个自定义时间输出格式的接口, 您只需要继承 `BaseClock` 这个类即可自定义您的时钟. 124 | 125 | 例子: 126 | 127 | ```python 128 | import ntptime, machine, time 129 | class RealClock(ulogger.BaseClock): 130 | def __init__ (self): 131 | self.rct = machine.RTC () 132 | ntptime.settime() # 获取并设置网络时间 133 | # * 注意: 此处获取的是国际标准时间 134 | # * 关于RTC模块的更多信息, 详见: 135 | # http://docs.micropython.org/en/latest/library/machine.RTC.html#machine.RTC 136 | 137 | def __call__(self) -> str: 138 | # 当需要获取时间时, 此函数会自动被调用, 并将其返回值作为时间戳文本. 139 | # self.rtc.datetime () -> (year, month, day, weekday, hours, minutes, seconds, subseconds) 140 | y,m,d,_,h,mi,s,_ = self.rtc.datetime () 141 | 142 | return '%d-%d-%d %d:%d:%d' % (y,m,d,h,mi,s) 143 | # 在 micropython 中, 使用 '%' 格式化文本是最快的处理方式, 144 | # 详见: https://blog.m-jay.cn/?p=329 145 | ``` 146 | 147 | 如果您想自定义您的时区, 可以按照以下方法: 148 | 149 | ```python 150 | def __init__ (self): 151 | self.rct = machine.RTC () 152 | # now = ntptime.time () 153 | # tp_time = time.localtime (now) 154 | # self.rtc.init (tp_time) 155 | self.rtc.init ( 156 | time.localtime ( 157 | ntptime.time () + 28800 # 增加八小时(北京时间) 158 | ) 159 | ) 160 | ``` 161 | 162 | 原理就是在获取的网络时间上增加或减去相应的时间, 例如北京时间(+8), 就增加 60\*60\*8=28800秒 163 | 164 | 165 | 166 | ### 一份完整的示例代码: 167 | 168 | ```python 169 | import ulogger 170 | 171 | # Example for esp8266 & esp32 172 | from machine import RTC 173 | import ntptime 174 | class Clock(ulogger.BaseClock): 175 | def __init__(self): 176 | self.rtc = RTC() 177 | ntptime.host = "ntp.ntsc.ac.cn" # 设置更快的ntf服务器, 可以减少延迟 178 | ntptime.settime() 179 | 180 | def __call__(self) -> str: 181 | y,m,d,_,h,mi,s,_ = self.rtc.datetime () 182 | return '%d-%d-%d %d:%d:%d' % (y,m,d,h,mi,s) 183 | 184 | clock = Clock() 185 | handler_to_term = ulogger.Handler( 186 | level=ulogger.INFO, 187 | colorful=True, 188 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 189 | clock=clock, 190 | direction=ulogger.TO_TERM, 191 | ) 192 | 193 | handler_to_file = ulogger.Handler( 194 | level=ulogger.INFO, 195 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 196 | clock=clock, 197 | direction=ulogger.TO_FILE, 198 | file_name="logging.log", 199 | max_file_size=1024 # max for 1k 200 | ) 201 | logger = ulogger.Logger( 202 | name = __name__, 203 | handlers = (handler_to_term, handler_to_file) 204 | ) 205 | 206 | logger.info("hello world") 207 | ``` 208 | 209 | ## 程序结构 210 | - 常量: 211 | - 适用于level: 212 | - DEBUG 213 | - INFO 214 | - WARN 215 | - ERROR 216 | - CRITICAL 217 | - 适用于direction: 218 | - TO_FILE 219 | - TO_TERM 220 | - VERSION 221 | 222 | - class Logger 223 | - 公开方法: 224 | - \_\_init\_\_(name, handlers: Iterable) 225 | - debug(*args, fn: str) 226 | - info(*args, fn: str) 227 | - warn(*args, fn: str) 228 | - error(*args, fn: str) 229 | - critical(*args, fn: str) 230 | - 公开属性 231 | - handlers: list 232 | 233 | - class Handler 234 | - 公开方法: 235 | - \_\_init\_\_(level, colorful, fmt, clock, direction, file_name, max_file_size) 236 | - 公开属性 237 | - level: 通过修改它来控制输出级别. 238 | 239 | - class BaseClock 240 | - 公开方法: 241 | - \_\_call\_\_() 242 | 243 | 244 | 245 | ## 设计指南 246 | ### 将其封装为模块 247 | 248 | 在每一个 `python` 文件里都单独配置一个 `Clock` 或者 `Handler` 是不太科学的, 而且会占据大量空间, 因此我们建议将其封装为一个模块来使用, 例子: 249 | 250 | ```python 251 | # loguitl.py 252 | import ulogger 253 | 254 | from machine import RTC 255 | import ntptime 256 | class Clock(ulogger.BaseClock): 257 | def __init__(self): 258 | self.rtc = RTC() 259 | ntptime.host = "ntp.ntsc.ac.cn" # 设置更快的ntf服务器, 可以减少延迟 260 | ntptime.settime() 261 | 262 | def __call__(self) -> str: 263 | y,m,d,_,h,mi,s,_ = self.rtc.datetime () 264 | return '%d-%d-%d %d:%d:%d' % (y,m,d,h,mi,s) 265 | clock = Clock() 266 | 267 | handlers = ( 268 | ulogger.Handler( 269 | level=ulogger.INFO, 270 | colorful=True, 271 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 272 | clock=clock, 273 | direction=ulogger.TO_TERM, 274 | ), 275 | ulogger.Handler( 276 | level=ulogger.ERROR, 277 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 278 | clock=clock, 279 | direction=ulogger.TO_FILE, 280 | file_name="logging.log", 281 | max_file_size=1024 # max for 1k 282 | ) 283 | ) 284 | 285 | def get_logger(name: str): 286 | return ulogger.Logger(name, handlers) 287 | all = (get_logger) 288 | ``` 289 | 290 | **注意:** 上面将两个 `Handler` 保存到一个 `tuple` 中, 这样当多次调用 `get_logger` 的时候就可以避免发生多次内存分配. 你告诉我在嵌入式设备上运行的代码怎么设计? 我会告诉你 **能省则省** 291 | 292 | ### 减少 IO 操作 293 | 294 | IO操作一直以来就是计算机中最慢的一环, 在单片机上更是如此, 因此, 您可以: 295 | 296 | - 减少打印不必要的信息 297 | 298 | - 简化 `Handler` 的 `fmt` 模板(输出更少的内容) 299 | 300 | - 将 `TO_FILE` 的级别设置为更高: `ERROR` 或 `CRITICAL` 301 | 302 | - 使用更高效的文本处理方式(最高可提速`400%`) 303 | 详见: [micropython中处理文本最快的方法 - M - Jay 的个人博客](https://blog.m-jay.cn/?p=329) 304 | 305 | ### 代码提示 306 | 你可以在你编写代码的计算机中安装本模块来启用编辑器的代码提示功能: 307 | ```shell 308 | pip install micropython-ulogger 309 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micropython-ulogger 2 | Logging on `micropython` is not an easy task. `micropython` has many unfinished interfaces, so the log content that can be recorded is very limited. I designed this `ulogger` library based on the situation of `micropython`. 3 | 4 | **Note:** This article uses `Google Translate` as a reference. There may be many ambiguities due to inaccurate translations. Welcome to submit a pull request. 5 | 6 | **English**|[简体中文](./README-ZH.md) 7 | ![LOGO](./src/logo_transparent.png) 8 | 9 | ## Features: 10 | In `micropython`, due to the limited computing power of the single-chip microcomputer, everything needs to be processed and responded quickly, so the purpose of this module is to reduce worthless operations. Therefore, this module will be more differences with the standard library `logging` of `CPython`. 11 | 12 | 13 | 14 | ## How to install? 15 | ### by `pypi` 16 | ```python 17 | #repl on your board 18 | import upip 19 | upip.install("micropython-ulogger") 20 | ``` 21 | 22 | ### by manually 23 | Please download the latest version of `.mpy` file in the [release](https://github.com/Li-Lian1069/micropython-ulogger/releases) of this project, and put it in the `/lib` directory of the development board or the directory of your program. 24 | 25 | 26 | 27 | ## How to use? 28 | ### quick-start 29 | This is the simplest example: 30 | ```python 31 | import ulogger 32 | loggor = ulogger.Logger(__name__) 33 | loggor.info("hello world") 34 | ``` 35 | 36 | 37 | ### About Handler 38 | **Now. Let's give it some condiment** 39 | ```python 40 | import ulogger 41 | 42 | handler_to_term = ulogger.Handler( 43 | level=ulogger.INFO, 44 | colorful=True, 45 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 46 | clock=None, 47 | direction=ulogger.TO_TERM, 48 | ) 49 | handler_to_file = ulogger.Handler( 50 | level=ulogger.INFO, 51 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 52 | clock=None, 53 | direction=ulogger.TO_FILE, 54 | file_name="logging.log", 55 | max_file_size=1024 # max for 1k 56 | ) 57 | logger = ulogger.Logger( 58 | name = __name__, 59 | handlers = ( 60 | handler_to_term, 61 | handler_to_file 62 | ) 63 | ) 64 | 65 | logger.info("hello", "world") 66 | # Support multi-parameter filling. 67 | ``` 68 | 69 | In `ulogger`, one thing is different with the `logging` module of `CPython`: In the `logging` module, `formatter` and `handler` are separate, but in this module, I combine them into One, this can reduce the workload of this module (you certainly don't want your development board to spend too much time to recording logs!), of course, this will reduce configurability, but we always have to pay a little price for improving performance. 70 | 71 | 72 | 73 | #### The params Handler can use: 74 | ```python 75 | # default args 76 | ulogger.Handler( 77 | level: int = INFO, 78 | colorful: bool = True, 79 | fmt: str = "&(time)% - &(level)% - &(name)% - &(msg)%", 80 | clock: BaseClock = None, 81 | direction: int = TO_TERM, 82 | file_name: str = "logging.log", 83 | max_file_size: int = 4096 84 | ) 85 | ``` 86 | - level: Specify the lowest level accepted by this Handler, optional: 87 | - ulogger.DEBUG 88 | - ulogger.INFO 89 | - ulogger.WARN 90 | - ulogger.ERROR 91 | - ulogger.CRITICAL 92 | 93 | - colorful: Specify whether to enable the color of the console text (only available when `direction=TO_TERM`) 94 | 95 | - fmt: Set the output text format, support built-in variables, currently supports: 96 | - `&(time)%`: Print the timestamp, see the `clock` parameter for the time format. 97 | - `&(level)%`: print the level of the message 98 | - `&(name)%`: Print the name of the Logger, which is provided when the Logger is instantiated. 99 | - `&(fnname)%`: Print function name, because `micropython` does not support `traceback` currently and more advanced error management, so you need to provide information manually. If this item is not provided when calling the record, the default value is `unknownfn`, example: 100 | ```python 101 | def hello(): 102 | logger.info("in hello", fn=hello.__name__) 103 | # or 104 | logger.info("in hello", fn="hello") 105 | # or fill in what you want 106 | logger.info("in hello", fn="world") 107 | ``` 108 | - `&(msg)%`: print the message, that is, the information you filled in `info()` 109 | 110 | - clock: Because the MCU does not have the `rtc` module that is always powered on, its time is not necessarily synchronized with the international time, so we open the clock interface, when we need to record `&(time)%` every time will get the time from this clock 111 | 112 | - direction: Set the direction of text output, optional `TO_TERM`(output to terminal) or `TO_FILE`(output to file) 113 | 114 | - file_name: The output file name, which will only be enabled when `TO_FILE`, and it can be a relative path or an absolute path. 115 | 116 | - max_file_size: Set the maximum file size (unit: byte), when the file size exceeds the upper limit, we will automatically empty the file. 117 | 118 | 119 | ### About Clock 120 | Perhaps you have noticed the `clock` parameter in `Handler`, we provide an interface for customizing the time output format, you only need to inherit the `BaseClock` class to customize your clock. 121 | 122 | For Example: 123 | ```python 124 | import ntptime, machine 125 | class RealClock(ulogger.BaseClock): 126 | def __init__ (self): 127 | self.rct = machine.RTC () 128 | ntptime.settime() # Get and set network time 129 | # * Note: Get here is the international standard time 130 | # * For more information about the RTC module, see: 131 | # http://docs.micropython.org/en/latest/library/machine.RTC.html#machine.RTC 132 | 133 | def __call__(self) -> str: 134 | # When we need to get the time, this function will be called automatically, and its return value will be used as the timestamp text. 135 | 136 | # self.rtc.datetime () -> (year, month, day, weekday, hours, minutes, seconds, subseconds) 137 | y,m,d,_,h,mi,s,_ = self.rtc.datetime () 138 | return '%d-%d-%d %d:%d:%d' % (y,m,d,h,mi,s) 139 | # In micropython, formatting text with'%' is the fastest way to process it. 140 | # See details at: https://blog.m-jay.cn/?p=329 141 | ``` 142 | If you want to customize your time-zone, you can follow this methods: 143 | ```python 144 | def __init__ (self): 145 | self.rct = machine.RTC () 146 | # now = ntptime.time () 147 | # tp_time = time.localtime (now) 148 | # self.rtc.init (tp_time) 149 | self.rtc.init ( 150 | time.localtime ( 151 | ntptime.time () + 28800 # Add eight hours (Beijing time) 152 | ) 153 | ) 154 | ``` 155 | The principle is to add or subtract the corresponding time from the obtained network time, for example, Beijing time (+8), add 60\*60\*8=28800 seconds 156 | 157 | 158 | 159 | 160 | ### A complete sample code: 161 | ```python 162 | import ulogger 163 | 164 | # Example for esp8266 & esp32 165 | from machine import RTC 166 | import ntptime 167 | class Clock(ulogger.BaseClock): 168 | def __init__(self): 169 | self.rtc = RTC() 170 | ntptime.host = "ntp.ntsc.ac.cn" # Setting up a faster ntf server can reduce latency 171 | ntptime.settime() 172 | 173 | def __call__(self) -> str: 174 | y,m,d,_,h,mi,s,_ = self.rtc.datetime () 175 | return '%d-%d-%d %d:%d:%d' % (y,m,d,h,mi,s) 176 | 177 | clock = Clock() 178 | handler_to_term = ulogger.Handler( 179 | level=ulogger.INFO, 180 | colorful=True, 181 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 182 | clock=clock, 183 | direction=ulogger.TO_TERM, 184 | ) 185 | 186 | handler_to_file = ulogger.Handler( 187 | level=ulogger.INFO, 188 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 189 | clock=clock, 190 | direction=ulogger.TO_FILE, 191 | file_name="logging.log", 192 | max_file_size=1024 # max for 1k 193 | ) 194 | logger = ulogger.Logger( 195 | name = __name__, 196 | handlers = (handler_to_term, handler_to_file) 197 | ) 198 | 199 | logger.info("hello world") 200 | ``` 201 | 202 | 203 | 204 | 205 | ## Structure 206 | - Constants: 207 | - use for level: 208 | - DEBUG 209 | - INFO 210 | - WARN 211 | - ERROR 212 | - CRITICAL 213 | - use for direction: 214 | - TO_FILE 215 | - TO_TERM 216 | - VERSION 217 | 218 | - class Logger 219 | - Public_Methods: 220 | - \_\_init\_\_(name, handlers: Iterable) 221 | - debug(*args, fn: str) 222 | - info(*args, fn: str) 223 | - warn(*args, fn: str) 224 | - error(*args, fn: str) 225 | - critical(*args, fn: str) 226 | - Public_Attributes 227 | - handlers: list 228 | 229 | - class Handler 230 | - Public_Methods: 231 | - \_\_init\_\_(level, colorful, fmt, clock, direction, file_name, max_file_size) 232 | - Public_Attributes 233 | - level: change it to modify level. 234 | 235 | - class BaseClock 236 | - Public_Methods: 237 | - \_\_call\_\_() 238 | 239 | 240 | ## Design Guidelines 241 | ### Encapsulate it as a module 242 | 243 | It is not scientific to configure a Clock or Handler separately in each python file, and it will take up a lot of space, so we recommend encapsulating it as a module for use, for example: 244 | 245 | ```python 246 | # loguitl.py 247 | import ulogger 248 | 249 | from machine import RTC 250 | import ntptime 251 | class Clock(ulogger.BaseClock): 252 | def __init__(self): 253 | self.rtc = RTC() 254 | ntptime.host = "ntp.ntsc.ac.cn" 255 | ntptime.settime() 256 | 257 | def __call__(self) -> str: 258 | y,m,d,_,h,mi,s,_ = self.rtc.datetime () 259 | return '%d-%d-%d %d:%d:%d' % (y,m,d,h,mi,s) 260 | clock = Clock() 261 | 262 | handlers = ( 263 | ulogger.Handler( 264 | level=ulogger.INFO, 265 | colorful=True, 266 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 267 | clock=clock, 268 | direction=ulogger.TO_TERM, 269 | ), 270 | ulogger.Handler( 271 | level=ulogger.ERROR, 272 | fmt="&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%", 273 | clock=clock, 274 | direction=ulogger.TO_FILE, 275 | file_name="logging.log", 276 | max_file_size=1024 # max for 1k 277 | ) 278 | ) 279 | 280 | def get_logger(name: str): 281 | return ulogger.Logger(name, handlers) 282 | all = (get_logger) 283 | ``` 284 | 285 | **Note:** The above saves two `Handler` into a `tuple`, so that multiple memory allocations can be avoided when `get_logger` is called multiple times. If you ask me embedded devices How to design the faster code? I will tell you **can save, just save** 286 | 287 | ### Reduce IO operations 288 | 289 | IO operation has always been the slowest part of the computer, especially on the microcontroller. Therefore, you can: 290 | 291 | - Reduce printing unnecessary information 292 | - Simplified `fmt` template of `Handler` (output less content) 293 | - Set the level of `TO_FILE` to a higher level: `ERROR` or `CRITICAL` 294 | - Use more efficient text processing methods (up to `400%` faster) 295 | See : [The fastest way to process text in micropython ](https://blog.m-jay.cn/?p=329) 296 | 297 | ### Code hint 298 | You can install this module on the computer where you write the code to enable the editor's code hint function: 299 | ```shell 300 | pip install micropython-ulogger 301 | ``` 302 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | from ulogger import __version__ 4 | setup( 5 | name='micropython-ulogger', 6 | version=__version__, 7 | description="Log module customized for micropython.", 8 | packages=find_packages(), 9 | 10 | url='https://github.com/Li-Lian1069/micropython-ulogger', 11 | author='M-Jay', 12 | author_email='m-jay-1376@qq.com', 13 | classifiers=[ 14 | 'Development Status :: 5 - Production/Stable', 15 | ], 16 | keywords='micropython', 17 | ) 18 | -------------------------------------------------------------------------------- /src/logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majoson-chen/micropython-ulogger/d28c097e285b9d3973b5cb3d4d8676566b668d5d/src/logo_transparent.png -------------------------------------------------------------------------------- /ulogger/__init__.py: -------------------------------------------------------------------------------- 1 | try: import time 2 | except: import utime as time 3 | 4 | try: from micropython import const 5 | except: const = lambda x:x # for debug 6 | 7 | from io import TextIOWrapper 8 | 9 | __version__ = "v1.2" 10 | 11 | DEBUG: int = const(10) 12 | INFO: int = const(20) 13 | WARN: int = const(30) 14 | ERROR: int = const(40) 15 | CRITICAL: int = const(50) 16 | 17 | TO_FILE = const(100) 18 | TO_TERM = const(200) 19 | 20 | # fmt map 的可选参数 21 | _level = const(0) 22 | _msg = const(1) 23 | _time = const(2) 24 | _name = const(3) 25 | _fnname = const(4) 26 | 27 | 28 | def level_name(level: int, color: bool = False) -> str: 29 | if not color: 30 | if level == INFO: 31 | return "INFO" 32 | elif level == DEBUG: 33 | return "DEBUG" 34 | elif level == WARN: 35 | return "WARN" 36 | elif level == ERROR: 37 | return "ERROR" 38 | elif level == CRITICAL: 39 | return "CRITICAL" 40 | else: 41 | if level == INFO: 42 | return "\033[97mINFO\033[0m" 43 | elif level == DEBUG: 44 | return "\033[37mDEBUG\033[0m" 45 | elif level == WARN: 46 | return "\033[93mWARN\033[0m" 47 | elif level == ERROR: 48 | return "\033[35mERROR\033[0m" 49 | elif level == CRITICAL: 50 | return "\033[91mCRITICAL\033[0m" 51 | 52 | 53 | class BaseClock (): 54 | """ 55 | This is a BaseClock for the logger. 56 | Please inherit this class by your custom. 57 | """ 58 | 59 | def __call__(self) -> str: 60 | """ 61 | Acquire the time of now, please inherit this method. 62 | We will use the return value of this function as the time format of the log, 63 | such as `2021 - 6 - 13` or `12:45:23` and so on. 64 | 65 | :return: the time string. 66 | """ 67 | return '%d' % time.time() 68 | 69 | 70 | class Handler(): 71 | """The Handler for logger. 72 | """ 73 | _template: str 74 | _map: bytes 75 | level: int 76 | _direction: int 77 | _clock: BaseClock 78 | _color: bool 79 | _file_name: str 80 | _max_size: int 81 | _file = TextIOWrapper 82 | 83 | def __init__(self, 84 | level: int = INFO, 85 | colorful: bool = True, 86 | fmt: str = "&(time)% - &(level)% - &(name)% - &(msg)%", 87 | clock: BaseClock = None, 88 | direction: int = TO_TERM, 89 | file_name: str = "logging.log", 90 | max_file_size: int = 4096 91 | ): 92 | """ 93 | Create a Handler that you can add to the logger later 94 | 95 | ## Options available for fmt. 96 | - &(level)% : the log level 97 | - &(msg)% : the log message 98 | - &(time)% : the time acquire from clock, see `BaseClock` 99 | - &(name)% : the logger's name 100 | - &(fnname)% : the function name which you will pass on. 101 | - more optional is developing. 102 | 103 | ## Options available for level. 104 | - DEBUG 105 | - INFO 106 | - WARN 107 | - ERROR 108 | - CRITICAL 109 | 110 | ## Options available for direction. 111 | - TO_FILE : output to a file 112 | - TO_TERM : output to terminal 113 | 114 | :param level: Set a minimum level you want to be log 115 | :type level: int(see the consts in this module) 116 | 117 | :param colorful: Whether to use color display information to terminal(Not applicable to files) 118 | :type colorful: bool 119 | 120 | :param fmt: the format string like: "&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%"(default) 121 | :type fmt: str 122 | 123 | :param clock: The Clock which will provide time str. see `BaseClock` 124 | :type clock: BaseClock(can be inherit ) 125 | 126 | :param direction: Set the direction where logger will output 127 | :type direction: int (`TO_FILE` or `TO_TERM`) 128 | 129 | :param file_name: available when you set `TO_FILE` to param `direction`. (default for `logging.log`) 130 | :type file_name: str 131 | :param max_file_size: available when you set `TO_FILE` to param `direction`. The unit is `byte`, (default for 4k) 132 | :type max_file_size: str 133 | """ 134 | #TODO: 文件按日期存储, 最大份数的设置. 135 | self._direction = direction 136 | self.level = level 137 | self._clock = clock if clock else BaseClock() 138 | self._color = colorful 139 | self._file_name = file_name if direction == TO_FILE else '' 140 | self._max_size = max_file_size if direction == TO_FILE else 0 141 | 142 | if direction == TO_FILE: 143 | self._file = open(file_name, 'a+') 144 | 145 | # 特么的re居然不能全局匹配, 烦了, 只能自己来. 146 | # m = re.match(r"&\((.*?)\)%", fmt) 147 | # i = 0 148 | # while True: 149 | # # 由于蛋疼的 ure 不能直接获取匹配结果的数量, 只能使用这种蠢蛋方法来循环. 150 | # try: 151 | # text = m.group(i) 152 | 153 | # except: 154 | # # 发生错误说明已经遍历完毕 155 | # break 156 | 157 | # # 使用指针代替文本来减少开销 158 | # if text == "level": 159 | # self._map.append(_level) 160 | # elif text == "msg": 161 | # self._map.append(_msg) 162 | # elif text == "time": 163 | # self._map.append(_time) 164 | # elif text == "name": 165 | # self._map.append(_name) 166 | # elif text == "fnname": 167 | # self._map.append(_fnname) 168 | 169 | # i += 1 170 | 171 | # 添加映射 172 | self._map = bytearray() 173 | idx = 0 174 | while True: 175 | idx = fmt.find("&(", idx) 176 | if idx >= 0: # 有找到 177 | a_idx = fmt.find(")%", idx+2) 178 | if a_idx < 0: 179 | # 没找到后缀, 报错 180 | raise Exception( 181 | "Unable to parse text format successfully.") 182 | text = fmt[idx+2:a_idx] 183 | idx = a_idx+2 # 交换位置 184 | if text == "level": 185 | self._map.append(_level) 186 | elif text == "msg": 187 | self._map.append(_msg) 188 | elif text == "time": 189 | self._map.append(_time) 190 | elif text == "name": 191 | self._map.append(_name) 192 | elif text == "fnname": 193 | self._map.append(_fnname) 194 | else: # 没找到, 代表后面没有了 195 | break 196 | 197 | # 将 template 变成可被格式化的文本 198 | # 确保最后一个是换行字符 199 | 200 | self._template = fmt.replace("&(level)%", "%s")\ 201 | .replace("&(msg)%", "%s")\ 202 | .replace("&(time)%", "%s")\ 203 | .replace("&(name)%", "%s")\ 204 | .replace("&(fnname)%", "%s")\ 205 | + "\n" if fmt[:-1] != '\n' else '' 206 | 207 | def _msg(self, *args, level: int, name: str, fnname: str): 208 | """ 209 | Log a msg 210 | """ 211 | 212 | if level < self.level: 213 | return 214 | # generate msg 215 | temp_map = [] 216 | text = '' 217 | for item in self._map: 218 | if item == _msg: 219 | for text_ in args: # 将元组内的文本添加到一起 220 | text = "%s%s" % (text, text_) # 防止用户输入其他类型(int, float) 221 | temp_map.append(text) 222 | elif item == _level: 223 | if self._direction == TO_TERM: # only terminal can use color. 224 | temp_map.append(level_name(level, self._color)) 225 | else: 226 | temp_map.append(level_name(level)) 227 | elif item == _time: 228 | temp_map.append(self._clock()) 229 | elif item == _name: 230 | temp_map.append(name) 231 | elif item == _fnname: 232 | temp_map.append(fnname if fnname else "unknownfn") 233 | 234 | if self._direction == TO_TERM: 235 | self._to_term(tuple(temp_map)) 236 | else: 237 | self._to_file(tuple(temp_map)) 238 | # TODO: 待验证: 转换为 tuple 和使用 fromat 谁更快 239 | 240 | def _to_term(self, map: tuple): 241 | print(self._template % map, end='') 242 | 243 | def _to_file(self, map: tuple): 244 | fp = self._file 245 | # 检查是否超出大小限制. 246 | prev_idx = fp.tell() # 保存原始指针位置 247 | # 将读写指针跳到文件最大限制的地方, 248 | # 如果能读出数据, 说明文件大于指定的大小 249 | fp.seek(self._max_size) 250 | if fp.read(1): # 能读到数据, 说明超出大小限制了 251 | fp = self._file = open(self._file_name, 'w') # 使用 w 选项清空文件内容 252 | else: 253 | # 没有超出限制 254 | fp.seek(prev_idx) # 指针回到原来的地方 255 | 256 | # 检查完毕, 开始写入数据 257 | fp.write(self._template % map) 258 | fp.flush() 259 | 260 | 261 | class Logger(): 262 | _handlers: list 263 | 264 | def __init__(self, 265 | name: str, 266 | handlers: list = None, 267 | ): 268 | 269 | self.name = name 270 | if not handlers: 271 | # 如果没有指定处理器, 自动创建一个默认的 272 | self._handlers = [Handler()] 273 | else: 274 | self._handlers = handlers 275 | 276 | @property 277 | def handlers(self): 278 | return self._handlers 279 | 280 | def _msg(self, *args, level: int, fn: str): 281 | 282 | for item in self._handlers: 283 | #try: 284 | item._msg(*args, level=level, fnname=fn, name=self.name) 285 | #except: 286 | # print("Failed while trying to record") 287 | 288 | def debug(self, *args, fn: str = None): 289 | self._msg(*args, level=DEBUG, fn=fn) 290 | 291 | def info(self, *args, fn: str = None): 292 | self._msg(*args, level=INFO, fn=fn) 293 | 294 | def warn(self, *args, fn: str = None): 295 | self._msg(*args, level=WARN, fn=fn) 296 | 297 | def error(self, *args, fn: str = None): 298 | self._msg(*args, level=ERROR, fn=fn) 299 | 300 | def critical(self, *args, fn: str = None): 301 | self._msg(*args, level=CRITICAL, fn=fn) 302 | 303 | 304 | __all__ = [ 305 | Logger, 306 | Handler, 307 | BaseClock, 308 | 309 | 310 | DEBUG, 311 | INFO, 312 | WARN, 313 | ERROR, 314 | CRITICAL, 315 | 316 | TO_FILE, 317 | TO_TERM, 318 | 319 | __version__ 320 | ] 321 | --------------------------------------------------------------------------------