把 Odoo 日志升级成 IDE 级体验:彩色高亮、可点击源码、统一格式(VS Code)

在日常 Odoo 开发中,我们经常会遇到一个痛点:

日志全是白色、密密麻麻,异常堆栈不明显

看文件路径需要手动复制粘贴

werkzeug / Odoo / 3rd-party 日志格式还不统一

如果你在 VS Code 的终端里运行 Odoo,大概是这样的 👇

复制代码
2026-01-06 03:38:55,432 51844 INFO ? odoo.addons...: You need Wkhtmltopdf...
Traceback (most recent call last):
...

说实话:

👉 问题能排,但效率不高。


🎯 本文目标:让 Odoo 日志变成这样

(VS Code 终端内 ↓)

  • 🌈 日志带颜色(INFO / WARNING / ERROR 非常明显)

  • 📎 右侧显示 file.py:line

  • 🖱 Ctrl+点击 → 直接跳到源码对应行

  • 🧩 Odoo / werkzeug / 模块日志 格式统一

  • ❌ 没有 "同一条日志打印两次" 的问题

效果类似:

复制代码
[2026-01-06 03:33:37]  INFO  HTTP service running ...                               server.py:239
[2026-01-06 03:33:44]  INFO  Generating asset bundle ...                             assetsbundle.py:302
[2026-01-06 03:33:52] ERROR  Something bad happened                                  my_module.py:88

🔧 思路:用 Rich 接管 Odoo 日志系统

Python 的 logging 系统是分层级的 logger 树

默认情况:

复制代码
root
 ├── odoo
 │    ├── odoo.modules.loading
 │    ├── odoo.service.server
 │    └── ...
 └── werkzeug

Odoo 会注册自己的 handler(格式化日志)

导致:

  • 输出"老样式"

  • 颜色为零

  • 有时同一条日志被打印两次

我们做的事情只有两步:

✔ 使用 Rich 渲染日志(支持颜色+可点击路径)

✔ 让 Odoo & werkzeug 的日志统统走这一个 handler


📁 一、创建启动脚本 run_odoo_rich.py

在你的项目根目录里:

复制代码
project/
 ├── bin/
 │   ├── odoo.conf
 │   └── run_odoo_rich.py   ← 新建这个
 ├── src/
 │   └── odoo/

粘贴完整脚本(已经调试稳定):

📌 这个版本包含:

  • 可点击路径修复(Windows-friendly file:///C:/...

  • 仅挂接必要 logger(避免重复输出)

  • 彩色 traceback

  • 统一 Odoo/werkzeug 日志格式

👉 完整代码见下:

python 复制代码
# bin/run_odoo_rich.py
import os
import sys
import logging
from datetime import datetime
from pathlib import Path
from typing import Iterable, Optional, Union

from rich._log_render import FormatTimeCallable, LogRender
from rich.console import Console, RenderableType
from rich.table import Table
from rich.text import Text, TextType
from rich.logging import RichHandler
from rich.traceback import install

# ========= 让 Python 能找到 src/odoo =========
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ODOO_PATH = os.path.join(BASE_DIR, "src", "odoo")
if ODOO_PATH not in sys.path:
    sys.path.insert(0, ODOO_PATH)
# ===========================================


class OdooLogRender(LogRender):
    """
    自定义 LogRender:
    把 Rich 默认的 file://C:\\... 改成合法的 file:///C:/... 超链接,
    这样 VSCode 里右侧的 path 点击才不会报错。
    """

    def __call__(
        self,
        console: "Console",
        renderables: Iterable["RenderableType"],
        log_time: Optional[datetime] = None,
        time_format: Optional[Union[str, FormatTimeCallable]] = None,
        level: TextType = "",
        path: Optional[str] = None,
        line_no: Optional[int] = None,
        link_path: Optional[str] = None,
    ) -> "Table":
        from rich.containers import Renderables  # 避免循环导入

        output = Table.grid(padding=(0, 5), pad_edge=True)
        output.expand = True

        if self.show_time:
            output.add_column(style="log.time")
        if self.show_level:
            output.add_column(style="log.level")
        output.add_column(style="log.message", ratio=1)
        if self.show_path and path:
            output.add_column(style="log.path")

        row = []

        # 时间列
        if self.show_time:
            if log_time is not None:
                if isinstance(time_format, str) or time_format is None:
                    _fmt = time_format or "[%x %X]"

                    def _time_fmt(dt: datetime) -> str:
                        return dt.strftime(_fmt)

                    time_format_callable: FormatTimeCallable = _time_fmt
                else:
                    time_format_callable = time_format

                log_time_display = Text(time_format_callable(log_time))
            else:
                log_time_display = Text("")

            if log_time_display == self._last_time and self.omit_repeated_times:
                row.append(Text(" " * len(log_time_display)))
            else:
                row.append(log_time_display)
            self._last_time = log_time_display

        # 等级
        if self.show_level:
            row.append(level)

        # 消息内容
        row.append(Renderables(renderables))

        # 右侧文件列(可点击)
        if self.show_path and path:
            path_text = Text()

            url: Optional[str] = None
            if link_path:
                try:
                    url = Path(link_path).resolve().as_uri()
                except Exception:
                    url = None

            style_path = f"link {url}" if url else ""
            style_line = f"link {url}#{line_no}" if (url and line_no) else style_path

            path_text.append(path, style=style_path)
            if line_no:
                path_text.append(":")
                path_text.append(str(line_no), style=style_line)

            row.append(path_text)

        output.add_row(*row)
        return output


class OdooRichHandler(RichHandler):
    def __init__(
        self,
        level=logging.NOTSET,
        console=None,
        *,
        show_time=True,
        omit_repeated_times=True,
        show_level=True,
        show_path=True,
        enable_link_path=True,
        markup=True,
        rich_tracebacks=True,
        log_time_format="[ %Y-%m-%d %H:%M:%S ]",
        **kwargs,
    ):
        super().__init__(
            level=level,
            console=console,
            show_time=show_time,
            omit_repeated_times=omit_repeated_times,
            show_level=show_level,
            show_path=show_path,
            enable_link_path=enable_link_path,
            markup=markup,
            rich_tracebacks=rich_tracebacks,
            log_time_format=log_time_format,
            **kwargs,
        )

        self._log_render = OdooLogRender(
            show_time=show_time,
            show_level=show_level,
            show_path=show_path,
            time_format=log_time_format,
            omit_repeated_times=omit_repeated_times,
        )


install(show_locals=False)

LOG_LEVEL = logging.INFO

handler = OdooRichHandler(level=LOG_LEVEL)

# Odoo 日志
odoo_logger = logging.getLogger("odoo")
odoo_logger.handlers.clear()
odoo_logger.setLevel(LOG_LEVEL)
odoo_logger.addHandler(handler)
odoo_logger.propagate = False

# werkzeug 日志
werkzeug_logger = logging.getLogger("werkzeug")
werkzeug_logger.handlers.clear()
werkzeug_logger.setLevel(LOG_LEVEL)
werkzeug_logger.addHandler(handler)
werkzeug_logger.propagate = False


if __name__ == "__main__":
    from odoo.cli import main
    main()

▶️ 二、VS Code 启动 Odoo:走这个脚本

.vscode/launch.json 中:

jsonc 复制代码
{
  "program": "${workspaceFolder}/bin/run_odoo_rich.py",
  "args": [
    "-c",
    "${workspaceFolder}/bin/odoo.conf"
  ],
  "console": "integratedTerminal",
  "justMyCode": false
}

F5,启动!


🧪 验证 5 个关键点

1️⃣ 彩色 INFO / WARNING / ERROR

有。

2️⃣ Traceback 彩色

异常时特别清晰。

3️⃣ 右侧 file.py:line

鼠标悬停 → Ctrl + 点击

✔ 直接跳进源码。

4️⃣ werkzeug 日志格式统一

请求日志和 Odoo 日志一样清爽。

5️⃣ 没有重复日志

(同一条不会一行老样式一行新样式)


🧨 常见坑(以及我们已经帮你避开的)

问题 原因 我们的解决
file://C:\... 点击报错 Windows file URL 不合法 转为 file:///C:/...
同一日志打印两次 logger 上挂了两个 handler 清理 + 精准接管
werkzeug 日志不统一 走 root handler 独立接管 werkzeug
路径不显示 Rich 默认裁剪 自定义 LogRender
Traceback 没颜色 默认 logging 启用 rich.traceback

🎁 进阶

后续做:

  • SQL 语句单独高亮

  • 某些 noisy 模块降级日志级别

  • 根据数据库名做颜色分组

  • 给这套日志输出写一个 toggle 开关

相关推荐
浪潮IT馆11 小时前
在 VSCode 中调试 JavaScript 的 Jest 测试用例
javascript·ide·vscode
椰汁菠萝12 小时前
VSCode中properties文件读写
ide·vscode·properties
weixin_5500831513 小时前
QTdesigner配置在pycharm里使用anaconda环境配置安装成功
ide·python·pycharm
dvlinker14 小时前
C/C++编程开发工具及实用软件推荐
ide·vscode·visual studio·qt creator·c/c++·source insight·编程工具
Kazefuku15 小时前
VS Code 和Visual Studio:简单易懂的区别
ide·windows·visual studio
Boxsc_midnight15 小时前
【一款支持Ollama本地部署的Visual Studio 2022 编程助手插件的编译和生成之路】解决打包安装问题
ide·visual studio·vs插件
-凌凌漆-15 小时前
vscode运行npm报错,npm : 无法加载文件 xxxxx/npm.ps1,因为在此系统上禁止运行脚本。
ide·vscode·npm
lingzhilab15 小时前
零知IDE——基于ESP32的ADS1115 多通道数据采集系统:从差分测量到Web实时监控
ide
Lxinccode15 小时前
python(70) : 网页IDE
开发语言·ide·python·网页ide
shishi5211 天前
trae重装后,无法预览调试弹窗报错的解决方案
ide·计算机视觉·语言模型