diff --git a/logger_config.json b/logger_config.json
index e58ae4c..283a3b0 100644
--- a/logger_config.json
+++ b/logger_config.json
@@ -7,7 +7,7 @@
"enable_console": true,
"enable_file": true,
"console_color": true,
- "console_level": "INFO",
+ "console_level": "DEBUG",
"console_format": "{time} {levelname} | {prefix}{message}",
"console_prefix": "Auto",
"console_encoding": "utf-8",
diff --git a/src/logiliteal/levels/levels.py b/src/logiliteal/levels/levels.py
index 6271d4d..4bd9942 100644
--- a/src/logiliteal/levels/levels.py
+++ b/src/logiliteal/levels/levels.py
@@ -11,96 +11,116 @@ from ..utils.fmt import fmt_file, fmt_message, fmt_console
from ..utils.configs import get_config, set_config
from ..utils.time import get_asctime
import pathlib
+from pathlib import Path
def _get_full_path(file_path, file_name):
file_path.mkdir(parents=True, exist_ok=True)
return file_path / file_name
-file_path = pathlib.Path(get_config("file_path"))
-file_name = get_config("file_name")
-file_format = get_config("file_format")
-file_encoding = get_config("file_encoding")
-is_enable_console = get_config("enable_console")
-is_enable_file = get_config("enable_file")
-
class Logger:
def __init__(self):
- if pathlib.Path(file_path).exists():
- if not pathlib.Path(file_path).is_dir():
- self.warn("日志文件路径不是目录,已自动自动使用默认目录")
+ project_root = Path(__file__).parent.parent.parent.parent
+ file_path = project_root / get_config("file_path")
+ file_path.mkdir(parents=True, exist_ok=True)
+ if file_path.exists():
+ if not file_path.is_dir():
+ self.warn("日志文件路径不是目录,已自动使用默认目录")
set_config("file_path", "./logs")
pathlib.Path("./logs").mkdir(parents=True, exist_ok=True)
- if _get_full_path(file_path, file_name).exists():
+ current_file = _get_full_path(file_path, get_config("file_name"))
+ if current_file.exists():
from os import rename
- rename(_get_full_path(file_path, file_name), _get_full_path(file_path, f"{get_asctime().replace(':', '-')}.log"))
+ rename(current_file, _get_full_path(file_path, f"{get_asctime().replace(':', '-')}.log"))
self.debug("日志文件已存在,已自动重命名")
-
- def _log(self, msg, pf, lvn):
- if is_enable_file:
- with open(_get_full_path(file_path, file_name), "a", encoding=file_encoding) as f:
- f.write(fmt_file(lvn, fmt_message(msg, no_placeholder=True), pf))
- if is_enable_console:
+
+ def _log(self, msg, pf, lvn, no_file: bool = False, no_console: bool = False):
+ project_root = Path(__file__).parent.parent.parent.parent
+ file_path = project_root / get_config("file_path")
+ file_path.mkdir(parents=True, exist_ok=True)
+ file_name = get_config("file_name")
+ file_encoding = get_config("file_encoding")
+ is_enable_file = get_config("enable_file")
+ is_enable_console = get_config("enable_console")
+ if not no_file and is_enable_file:
+ try:
+ with open(_get_full_path(file_path, file_name), "a", encoding=file_encoding) as f:
+ f.write(fmt_file(lvn, fmt_message(msg, no_placeholder=True, no_process=True), pf))
+ except Exception as e:
+ self.error(f"日志写入失败: {str(e)}", no_file=True)
+ if not no_console and is_enable_console:
console_output = fmt_console(lvn, fmt_message(msg, no_placeholder=True), pf)
if console_output is not None:
print(console_output)
return fmt_console(lvn, fmt_message(msg, no_placeholder=True), pf)
- def debug(self, message: Any, prefix: str | None = None, level: int = 0) -> Optional[str]:
+ def debug(self, message: Any, prefix: str | None = None, level: int = 0, no_file: bool = False, no_console: bool = False) -> Optional[str]:
"""
调试日志
Debug log
:param message: 消息内容 Message content
:param prefix: 前缀 Prefix
:param level: 日志级别 Log level(0~9)
+ :param no_file: 不写入文件 Do not write to file
+ :param no_console: 不输出到控制台 Do not output to console
"""
- return self._log(message, prefix, level)
+ return self._log(message, prefix, level, no_file, no_console)
- def info(self, message: Any, prefix: str | None = None, level: int = 10) -> Optional[str]:
+ def info(self, message: Any, prefix: str | None = None, level: int = 10, no_file: bool = False, no_console: bool = False) -> Optional[str]:
"""
信息日志
Info log
:param message: 消息内容 Message content
:param prefix: 前缀 Prefix
:param level: 日志级别 Log level(10~19)
+ :param no_file: 不写入文件 Do not write to file
+ :param no_console: 不输出到控制台 Do not output to console
"""
- return self._log(message, prefix, level)
+ return self._log(message, prefix, level, no_file, no_console)
- def warn(self, message: Any, prefix: str | None = None, level: int = 20) -> Optional[str]:
+ def warn(self, message: Any, prefix: str | None = None, level: int = 20, no_file: bool = False, no_console: bool = False) -> Optional[str]:
"""
警告日志
Warn log
:param message: 消息内容 Message content
:param prefix: 前缀 Prefix
:param level: 日志级别 Log level(20~29)
+ :param no_file: 不写入文件 Do not write to file
+ :param no_console: 不输出到控制台 Do not output to console
"""
- return self._log(message, prefix, level)
+ return self._log(message, prefix, level, no_file, no_console)
- def error(self, message: Any, prefix: str | None = None, level: int = 30) -> Optional[str]:
+ def error(self, message: Any, prefix: str | None = None, level: int = 30, no_file: bool = False, no_console: bool = False) -> Optional[str]:
"""
错误日志
Error log
:param message: 消息内容 Message content
:param prefix: 前缀 Prefix
:param level: 日志级别 Log level(30~39)
+ :param no_file: 不写入文件 Do not write to file
+ :param no_console: 不输出到控制台 Do not output to console
"""
- return self._log(message, prefix, level)
+ return self._log(message, prefix, level, no_file, no_console)
- def critical(self, message: Any, prefix: str | None = None, level: int = 40) -> Optional[str]:
+ def critical(self, message: Any, prefix: str | None = None, level: int = 40, no_file: bool = False, no_console: bool = False) -> Optional[str]:
"""
严重错误日志
Critical error log
:param message: 消息内容 Message content
:param prefix: 前缀 Prefix
:param level: 日志级别 Log level(40~49)
+ :param no_file: 不写入文件 Do not write to file
+ :param no_console: 不输出到控制台 Do not output to console
"""
- return self._log(message, prefix, level)
+ return self._log(message, prefix, level, no_file, no_console)
- def log(self, message: Any, prefix: str | None = None, level: int = 50) -> Optional[str]:
+ def log(self, message: Any, prefix: str | None = None, level: int = 50, no_file: bool = False, no_console: bool = False) -> Optional[str]:
"""
自定义日志
Custom log
:param message: 消息内容 Message content
:param prefix: 前缀 Prefix
:param level: 日志级别 Log level(50~59...)
+ :param no_file: 不写入文件 Do not write to file
+ :param no_console: 不输出到控制台 Do not output to console
"""
- return self._log(message, prefix, level)
\ No newline at end of file
+ return self._log(message, prefix, level, no_file, no_console)
diff --git a/src/logiliteal/utils/configs.py b/src/logiliteal/utils/configs.py
index d236326..ed4e4b4 100644
--- a/src/logiliteal/utils/configs.py
+++ b/src/logiliteal/utils/configs.py
@@ -9,11 +9,43 @@ py-logiliteal's config settings, used to set py-logiliteal's global config
import json
from os import remove
import shutil
+import os
from pathlib import Path
from typing import Union, Optional, Tuple
from logging import error
-DEFAULT_CONFIG_PATH = "logger_config.json"
+def get_config_path():
+ # 检查环境变量
+ env_config = os.getenv('LOGILITEAL_CONFIG')
+ if env_config and os.path.exists(env_config):
+ return env_config
+
+ # 检查当前工作目录
+ cwd_config = os.path.join(os.getcwd(), 'logger_config.json')
+ if os.path.exists(cwd_config):
+ return cwd_config
+
+ # 检查XDG配置目录
+ xdg_config_dir = os.getenv('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
+ xdg_config_path = os.path.join(xdg_config_dir, 'logiliteal', 'logger_config.json')
+
+ # 创建目录(如果不存在)
+ if not os.path.exists(os.path.dirname(xdg_config_path)):
+ try:
+ os.makedirs(os.path.dirname(xdg_config_path), exist_ok=True)
+ except Exception as e:
+ error(f"创建配置目录失败: {e}")
+ # 回退到项目根目录的配置文件
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
+ fallback_config = os.path.join(project_root, 'logger_config.json')
+ if os.path.exists(fallback_config):
+ return fallback_config
+ else:
+ return xdg_config_path
+
+ return xdg_config_path
+
+DEFAULT_CONFIG_PATH = get_config_path()
DEFAULT_CONFIG = {
"file_level": "DEBUG",
"file_name": "latest.log",
@@ -23,7 +55,7 @@ DEFAULT_CONFIG = {
"enable_console": True,
"enable_file": True,
"console_color": True,
- "console_level": "INFO",
+ "console_level": "DEBUG",
"console_format": "{time} {levelname} | {prefix}{message}",
"console_prefix": "Auto",
"console_encoding": "utf-8",
@@ -94,6 +126,8 @@ def get_config(select: str = None) -> Union[dict, str, bool, int, None]:
g_config_cache = json.load(f)
g_config_mtime = current_mtime
else:
+ # 确保目录存在
+ config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f:
json.dump(DEFAULT_CONFIG, f, indent=4)
g_config_cache = DEFAULT_CONFIG
diff --git a/src/logiliteal/utils/fmt.py b/src/logiliteal/utils/fmt.py
index 83bb91c..3c895a8 100644
--- a/src/logiliteal/utils/fmt.py
+++ b/src/logiliteal/utils/fmt.py
@@ -10,7 +10,15 @@ from .configs import get_config
from typing import Any, Optional
from .time import get_asctime, get_time, get_weekday, get_date
from .styles import set_color, set_bg_color
-import re
+from .regex import (
+ process_links,
+ process_markdown_formats,
+ process_html_styles,
+ process_special_tags,
+ process_color_formatting
+)
+from .placeholder import process_placeholder, SafeDict
+
if get_config("time_color") is None:
time_color = "#28ffb6"
@@ -61,29 +69,28 @@ def fmt_placeholder(message: Any, use_date_color: bool = True) -> str:
:param message: 消息内容 Message content
:return: 格式化后的消息 Formatted message
"""
- class SafeDict(dict):
- def __missing__(self, key):
- return f'{{{key}}}'
-
if not isinstance(message, str):
message = str(message)
- if use_date_color:
- message = message.format_map(SafeDict(
- asctime = set_color(get_asctime(),time_color),
- time = set_color(get_time(),time_color),
- weekday = set_color(get_weekday(),time_color),
- date = set_color(get_date(),time_color)
- ))
- else:
- message = message.format_map(SafeDict(
- asctime = get_asctime(),
- time = get_time(),
- weekday = get_weekday(),
- date = get_date(),
- ))
- return message
-def fmt_message(message: Any, no_placeholder: bool = False, no_color: bool = False) -> str:
+ context = {
+ "asctime": set_color(get_asctime(), time_color) if use_date_color else get_asctime(),
+ "time": set_color(get_time(), time_color) if use_date_color else get_time(),
+ "weekday": set_color(get_weekday(), time_color) if use_date_color else get_weekday(),
+ "date": set_color(get_date(), time_color) if use_date_color else get_date(),
+ }
+
+ return process_placeholder(message, context)
+
+def fmt_message(
+ message: Any,
+ no_placeholder: bool = False,
+ no_style: bool = False,
+ no_process: bool = False,
+ no_tags: bool = False,
+ no_links: bool = False,
+ no_markdown: bool = False,
+ no_html: bool = False,
+ ) -> str:
"""
格式化消息内容
Format message content
@@ -91,26 +98,29 @@ def fmt_message(message: Any, no_placeholder: bool = False, no_color: bool = Fal
:return: 格式化后的消息 Formatted message
"""
- def process_color_tags(msg: str) -> str:
- # 优化处理顺序,确保链接和基础格式优先处理
- processed = _process_color_formatting(
- _process_special_tags(
- _process_html_styles(
- _process_markdown_formats(
- _process_links(msg)
- )
- )
- )
+ def process_color_tags(msg: str, no_process: bool = False) -> str:
+ processed = process_color_formatting(
+ process_special_tags(
+ process_html_styles(
+ process_markdown_formats(
+ process_links(msg, no_links),
+ no_markdown
+ ),
+ no_html
+ ),
+ no_tags
+ ),
+ no_process
)
return processed
- if no_color:
- processed_message = str(message)
- else:
- processed_message = process_color_tags(str(message))
- if no_placeholder:
- return processed_message
- else:
- return process_color_tags(fmt_placeholder(processed_message)) if not no_color else fmt_placeholder(processed_message)
+ processed_message = str(message)
+ if not no_placeholder:
+ processed_message = fmt_placeholder(processed_message)
+ if not no_style:
+ processed_message = process_color_tags(processed_message)
+ if no_process:
+ return message
+ return processed_message
def fmt_level_name(level_name: str) -> str:
if get_config("console_color") != True:
@@ -137,7 +147,7 @@ def fmt_console(level: int, message: Any, prefix: str | None = None) -> Optional
:return: 格式化后的消息 Formatted message
"""
console_level = get_config("console_level")
- if level != -1 and fmt_level(console_level) > level:
+ if level != -1 and level < fmt_level(console_level):
return None
fmt = get_config("console_format")
prefix = prefix or ""
@@ -157,111 +167,14 @@ def fmt_file(level: int, message: Any, prefix: str | None = None) -> Optional[st
"""
fl = get_config("file_level")
fmt = get_config("file_format")
- if fmt_level(fl) > level:
+ if level != -1 and level < fmt_level(fl):
return None
if prefix is None:
prefix = ""
fmt = fmt_placeholder(fmt, use_date_color=False)
return f"{fmt.format(
levelname = fmt_level_number(level),
- prefix = fmt_message(prefix, no_placeholder=True, no_color=True),
- message = fmt_message(message, no_placeholder=True, no_color=True)
+ prefix = fmt_message(prefix, no_placeholder=True, no_style=True),
+ message = fmt_message(message, no_placeholder=True, no_style=True)
)}\n"
-def _process_links(text: str) -> str:
- from collections import deque
-
- link_stack = deque()
- placeholder_count = 0
- placeholders = {}
-
- def replace_link(m):
- nonlocal placeholder_count
- placeholder_count += 1
- if len(m.groups()) == 2 and m.group(2) and not m.group(1).startswith('http'):
- # Markdown链接 [text](url)
- url = m.group(2).strip()
- text = m.group(1)
- else:
- url = m.group(1)
- text = m.group(2)
- placeholder = f"__LINK_PLACEHOLDER_{placeholder_count}__"
- placeholders[placeholder] = (url if url else "#", text)
- link_stack.append(placeholder)
- return placeholder
-
- text = re.sub(r'(.*?)', replace_link, text, flags=re.DOTALL)
- text = re.sub(r'(.*?)', replace_link, text, flags=re.DOTALL)
- text = re.sub(r'\[(.*?)\]\((.*?)\)', replace_link, text)
-
- for placeholder, (url, text_content) in placeholders.items():
- ansi_link = f'\033]8;;{url}\033\\{set_color("\033[4m" + text_content, "#5f93ff")}\033]8;;\033\\'
- text = text.replace(placeholder, ansi_link)
-
- return text
-
-def _process_markdown_formats(text: str) -> str:
- # Markdown粗体 (**text**)
- text = re.sub(r'\*\*(.*?)\*\*', '\033[1m\\g<1>\033[22m', text)
- # Markdown斜体 (*text*)
- text = re.sub(r'\*(.*?)\*', '\033[3m\\g<1>\033[23m', text)
- # Markdown下划线 (__text__)
- text = re.sub(r'__(.*?)__', '\033[4m\\g<1>\033[24m', text)
- # Markdown删除线 (~~text~~)
- text = re.sub(r'~~(.*?)~~', '\033[9m\\g<1>\033[29m', text)
- return text
-
-def _process_html_styles(text: str) -> str:
- text = re.sub(r'([^<]*?)(|$)',
- lambda m: '\033[3m' + m.group(1) + '\033[23m', text, flags=re.DOTALL)
- text = re.sub(r'([^<]*?)',
- lambda m: '\033[1m' + m.group(1) + '\033[22m', text)
- text = re.sub(r'([^<]*?)',
- lambda m: '\033[4m' + m.group(1) + '\033[24m', text)
- text = re.sub(r' (.*?) (.*?)(([^<]*?)(|$)',
- lambda m: '\033[9m' + m.group(1) + '\033[29m', text, flags=re.DOTALL)
- return text
-
-def _process_special_tags(text: str) -> str:
- text = re.sub(r'
', '\n', text)
- text = re.sub(r'
(.*?)
', r'\n\033[0m\\g<1>\033[0m\n', text, flags=re.DOTALL) + text = re.sub(r'(.*?)(
|$)', r'\n\033[0m\\g<1>\033[0m\n', text, flags=re.DOTALL) + text = re.sub(r'', '\033[0m\n', text) + return text + + +def process_color_formatting(text: str, no_process: bool = False) -> str: + """处理颜色标签""" + if no_process: + return text + color_pattern = r'<#([0-9a-fA-F]{6})>' + close_pattern = r'>' + + parts = re.split(f'({color_pattern}|{close_pattern})', text) + result = [] + color_stack = [] + + for part in parts: + if part and re.fullmatch(color_pattern, part): + color = re.match(color_pattern, part).group(1) + color_stack.append(color) + continue + elif part == '>': + if color_stack: + color_stack.pop() + continue + elif part: + if color_stack: + current_color = color_stack[-1] + r = int(current_color[0:2], 16) + g = int(current_color[2:4], 16) + b = int(current_color[4:6], 16) + ansi_code = f'\033[38;2;{r};{g};{b}m' + reset_code = '\033[0m' + result.append(f'{ansi_code}{part}{reset_code}') + else: + processed_text = part + processed_text = re.sub(r'<#([0-9a-fA-F]{6})>', lambda m: f'<{set_color(f"#{m.group(1)}")}>', processed_text) + result.append(processed_text) + + processed_text = ''.join(result) + processed_text = re.sub(f'{color_pattern}|{close_pattern}', '', processed_text) + processed_text = re.sub(r'[0-9a-fA-F]{6}', '', processed_text) + + return processed_text \ No newline at end of file diff --git a/tests/logger_config.json b/tests/logger_config.json deleted file mode 100644 index 5194963..0000000 --- a/tests/logger_config.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "file_level": "DEBUG", - "file_name": "latest.log", - "file_path": "./logs", - "file_format": "{asctime} {levelname} | {prefix}{message}", - "file_encoding": "utf-8", - "enable_console": true, - "enable_file": true, - "console_color": true, - "console_level": "INFO", - "console_format": "{time} {levelname} | {prefix}{message}", - "console_prefix": "Auto", - "console_encoding": "utf-8", - "asctime_format": "%Y-%m-%d %H:%M:%S", - "time_format": "%H:%M:%S", - "date_format": "%Y-%m-%d", - "weekday_format": "%A", - "level_name": { - "DEBUG": "DEBUG", - "INFO": "INFO", - "WARN": "WARN", - "ERRO": "ERRO", - "CRIT": "CRIT" - }, - "level_color": { - "DEBUG": "#c1d5ff", - "INFO": "#c1ffff", - "WARN": "#fff600", - "ERRO": "#ffa000", - "CRIT": "#ff8181" - } -} \ No newline at end of file diff --git a/tests/t-level.py b/tests/t-level.py index 536513a..1131774 100644 --- a/tests/t-level.py +++ b/tests/t-level.py @@ -8,8 +8,8 @@ from src.logiliteal import Logger log = Logger() -log.info("测试信息日志") log.debug("测试调试日志") +log.info("测试信息日志") log.warn("测试警告日志") log.error("测试错误日志") log.critical("测试严重错误日志") \ No newline at end of file