日志记录

记录事件可以大大改善调试体验。Frappe 的开发和生产环境都自带日志记录功能。

根据访问方式的不同,日志主要分为两大类。桌面日志(Desk Logs)存储在站点数据库中,可以作为文档进行访问和查询;服务器日志(Server Logs)则存储在由日志轮转系统管理的文件中。

桌面日志

可以通过桌面界面(Desk UI)访问的日志(通常是从 Awesomebar 中搜索查找)。这些日志一般用于追踪操作事件;但你也可以利用它们的 API 来追踪 Frappe 应用中的任何内容。

Core 模块中的部分日志包括:

  • 访问日志
  • 活动日志
  • 错误日志
  • 计划任务日志

你可以通过嵌入的链接了解更多信息。了解每种日志的最佳方式是直接在你的站点上查看它们。

服务器日志

与桌面日志相比,服务器日志通常包含更底层的、事务性的数据。从版本 13 开始,日志也可以在站点级别获取。这些站点日志由 Frappe 应用程序创建,而许多 bench 级别的日志文件则由支持 Frappe 环境的进程生成。在你的 bench 文件夹中,你可以在以下位置找到日志:

  • ./logs
  • ./sites/{site}/logs

在撰写本文时,只有 frappe.web.log 和 scheduler.log 会在站点和 bench 级别记录日志。在 bench 级别,一些最有用的文件可能是:

  • bench.log
  • scheduler.log
  • worker.log

在这些日志中,你可以追踪已执行的 bench 命令、调度器或后台任务运行的状态。

> 要在你的站点上启用 Frappe Web 日志记录,请使用 > enable_frappe_logger 更新站点配置:true

错误快照

除此之外,Frappe 还会为你的站点维护 HTTP 响应码 500 及以上的错误快照。这样做是为了在发生服务器错误时提供更多的故障上下文。例如,某个桌面操作因 HTTP 状态码 500 而失败,这会在 ./sites/{site}/frappe.log 文件中添加一条相应的日志,如下所示:

2021-05-06 14:08:12,177 ERROR frappe New Exception collected with snapshot id: 2021-05-06 14:08:11.940809-127.0.0.1-bde
Site: frappeframework.com
Form Dict: {'docs': '{"name":"assets.frappeframework.com","owner":"Administrator","creation":"2020-04-04 14:58:18.771247","modified":"2021-02-08 09:40:56.820300","modified_by":"gavin@frappe.io","idx":0,"docstatus":0,"status":"Active","hostname":"assets","domain":"frappeframework.com","cluster":"Default","provider":"Generic","is_server_setup":1,"ip":"192.168.29.100","private_ip":"10.120.120.120","agent_password":"*****","is_primary":1,"doctype":"Server","__last_sync_on":"2021-05-06T08:38:08.931Z"}', 'method': 'ping_agent', 'cmd': 'run_doc_method'}

日志的第一行提到了该错误的快照 ID,在本例中是 2021-05-06 14:08:11.940809-127.0.0.1-bde。在 ./sites/frappeframework.com/error-snapshots 下应该有一个以该快照 ID 标识的对应文件,文件名为 2021-05-06 14:08:11.940809-127.0.0.1-bde.json。文件内容可能是:

    {
    "etype": "UnboundLocalError",
    "evalue": "UnboundLocalError("local variable 'headers' referenced before assignment")",
    "exception": {
      "args": [...],
      "with_traceback": [...]
    },
    "frames": [
      {
      "call": "(request=<request 'https:="" frappeframework.com="" api="" method="" run_doc_method'="" [post]="">)",
      "dump": {
        "frappe. api": "<module 'frappe.api'="" from="" '="" home="" frappe="" frappe-bench="" apps="" api.py'="">",
        "frappe.api. handle": "<function handle="">",
        "global frappe": "<module 'frappe'="" from="" '="" home="" frappe="" frappe-bench="" apps="" __init__.py'="">",
        "response": "None"
      },
      "file": "/home/frappe/frappe-bench/apps/frappe/frappe/app.py",
      "func": "application",
      "lines": {
        "66": "",
        "67": "elif request.path.startswith("/api/"):",
        "68": "response = frappe.api.handle()",
        "69": "",
        "70": "elif request.path.startswith('/backups'):"
      },
      "lnum": 68
      },
    ...
    ],
    "locals": {
      "data": "None",
      "files": "None",
      "method": "GET",
      "path": "ping",
      "self": "<orchestrator.agent.agent object="">",
      "url": "https://assets.frappeframework.com:443/agent/ping"
    },
    "pyver": "Python 3.8.2: /home/frappe/frappe-bench/env/bin/python (prefix: /home/frappe/frappe-bench/env)",
    "timestamp": "2021-05-06 14:08:11.940971",
    "traceback": "Traceback (most recent call last):
 File "/home/frappe/frappe-bench/env/lib/python3.8/site-packages/cryptography/fernet.py", line 119, in _verify_signature
 h.verify(data[-32:])
 File "/home/frappe/frappe-bench/env/lib/python3.8/site-packages/cryptography/hazmat/primitives/hmac.py", line 74, in verify
 ctx.verify(signature)
 File "/home/frappe/frappe-bench/env/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/hmac.py", line 75, in verify
 raise InvalidSignature("Signature did not match digest.")
cryptography.exceptions.InvalidSignature: Signature did not match digest.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
 File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 68, in application
 response = frappe.api.handle()
 File "/home/frappe/frappe-bench/apps/frappe/frappe/api.py", line 54, in handle
 return frappe.handler.handle()
 File "/home/frappe/frappe-bench/apps/orchestrator/orchestrator/agent.py", line 474, in request
 headers=headers,
UnboundLocalError: local variable 'headers' referenced before assignment
"
    }

这些快照会定期与站点数据库同步,并可以通过桌面端访问,记录会添加到 DocType Error Snapshot 下。它们从创建之日起保留一个月,之后每条记录会被删除。

> 错误快照默认启用追踪。要为你站点禁用此功能,请使用 > disable_error_snapshot 更新站点配置:true

进程

服务器日志可能由 bench CLI、你的进程管理器或 Frappe 应用程序直接生成。你的进程管理器负责启动和运行 Frappe 环境的所有组件,并将输出流和错误流定向到日志文件。

除了“始终运行”的默认日志记录外,你还可以使用 Monitor 来记录更多关于站点请求的信息。

维护

如果你不注意的话,进程管理器生成的日志文件可能会随着时间的推移变得非常大。要了解你需要追踪哪些文件,请查看你 bench 上的 supervisor.conf 或 Procfile,具体取决于你运行的是生产实例还是开发实例。

旧日志的自动清理

Frappe 中的大多数日志 DocType 都支持可配置的保留期限,并带有合理的默认值。

“Log Settings” DocType 让你可以为默认的日志 DocType 配置保留期限。

你也可以通过向 DocType 控制器添加一个静态方法来将自定义日志 DocType 注册到日志设置中,如下所示。实现了此方法的自定义日志 DocType 将自动显示在 Log Settings 中。

class CustomLoggingDoctype(Document):
 @staticmethod
    def clear_old_logs(days=180):
        from frappe.query_builder import Interval
        from frappe.query_builder.functions import Now

        table = frappe.qb.DocType("Custom Logging Doctype")
        frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))

更多

在生产环境中,你可能希望为 Frappe 应用程序或构成 Frappe 环境的进程记录更多信息。以下是一些你可以查阅的资源,以了解更多。

Frappe 应用程序

  • Frappe 日志 API

服务器监控

  • MySQL
  • PostgreSQL
  • NGINX

翻译

Frappe 内置了翻译系统,可以从代码中捕获可翻译的字符串,
并将其提取到不同语言的 CSV 文件中。

工作原理

翻译分为三个步骤。

1. 提取可翻译的字符串

许多字符串,如字段 label 和 description,可以很容易地从
DocType 的 JSON 文件中解析出来,它们会被自动标记为可翻译,无需任何
显式提示。

其他字符串则从源代码文件中提取,例如 .json、.js、
.py。只有当作者在 _() 或 __() 方法中
显式标记时,这些字符串才会被提取。

例如:

message = _("You don't have permissions to access this file")

2. 翻译提取出的字符串

一旦我们有了想要翻译的字符串列表,它们会按语言被放入 CSV
文件中。翻译主要由 Frappe 和 ERPNext 的各类用户在翻译门户上贡献。

提交的翻译经过验证后,每周由机器人转换为 Pull Request
并合并到核心代码中。

3. 在应用中翻译字符串

翻译本质上就是由原始字符串及其在某种语言中的译文组成的键值对。
它可能看起来像这样:

{
 "1 hour ago": "एक घंटे पहले",
 "1 minute ago": "1 मिनट पहले",
 "1 month": "1 महीना",
 "1 week": "1 सप्ताह",
 "1 year": "1 साल",
 "3 months": "3 महीने",
 "6 months": "6 महीने",
 "About": "के बारे में",
 "About Us Settings": "हमारे बारे में सेटिंग्स",
 "About Us Team Member": "हमारे बारे में टीम के सदस्य",
 "Academic Term": "शैक्षणिक शब्द",
 "Academic Year": "शैक्षणिक वर्ष",
 "Academics User": "अकादमिक उपयोगकर्ता",
 ...
}

当代码中将字符串写为 __('1 hour ago') 时,__ 方法
会查找字典,看该字符串是否存在翻译。如果存在,
就会返回翻译后的字符串并显示出来。

这是一个 JavaScript 文件中翻译如何工作的示例,但同样的
机制在 Python 中也适用。

编写有效可翻译字符串的技巧

为了让翻译解析器能够正确地从代码中提取字符串,
需要遵循一些规则。

1. 字面量字符串

要翻译的字符串必须始终是字面量字符串,不能是变量或
表达式。

Python 示例

# This will work
message = _('Document submitted successfully')
frappe.msgprint(message)

# This will also work
frappe.msgprint(_('Document submitted successfully'))

# This won't work
message = 'Document submitted successfully'
frappe.msgprint(_(message))

JavaScript 示例

// This will work
message = __('Document submitted successfully')
frappe.msgprint(message)

// This won't work
message = 'Document submitted successfully'
frappe.msgprint(__(message))

2. 变量

如果字符串中包含变量,必须使用位置格式化符
{0},其他类型的格式化符都不起作用。

Python 示例

# This is fine
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)

# These are not
_('Welcome %s, get started with <a href="https://erpnext.yuannext.com">ERPNext</a> in just a few clicks.' % full_name)

_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})

# This one uses the positional formatter,
# but won't work because the string is formatted before it is passed to _()
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))

JavaScript 示例

// This is fine
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name])

// This is not
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`)

3. 代码块

不要将字符串拆分成多个独立的字符串块再进行拼接。
不要写多行字符串。始终将字符串写在一行中,
即使字符串非常长。

Python 示例

# This is fine
_('You have {0} subscribers in your mailing list.').format(len(subscribers))

# Don't split strings
_('You have ') + len(subscribers) + _(' subscribers in your mailing list.')

# Don't write multiline strings
_('You have {0} subscribers \
 in your mailing list').format(len(subscribers))

JavaScript 示例

// This is fine
__('You have {0} subscribers in your mailing list.', [subscribers.length])

// Don't split strings
__('You have ') + subscribers.length + __(' subscribers in your mailing list.')

// Don't write multiline strings
__('You have {0} subscribers' +
 'in your mailing list', [subscribers.length])

4. 复数

不要试图用逻辑来处理单词的复数形式。每种语言都有不同的复数
形式。

# Don't do this
msg = _("You have {0} pending invoice").format(invoice_count)
if invoice_count > 1:
 msg += _("s")

# Write separate strings
# Every language has different plural forms
if invoice_count > 1:
 msg = _("You have {0} pending invoices").format(invoice_count)
else:
 msg = _("You have {0} pending invoice").format(invoice_count)

5. 不要有尾部空格

不要在句子的开头或结尾添加空格。通过翻译引擎处理时,其他语言的尾部空格会被去除。

如果需要在文本周围添加空格,可以在翻译语法之外进行。

# Don't do this
msg = _(" You have {0} pending invoice ")

# Do this
msg = ' ' + _("You have {0} pending invoices") + ' '

6. 为字符串添加上下文

一个可翻译的字符串在不同上下文中可能有不同的含义。
例如,字符串 “Change” 可以表示 “使不同” 或 “硬币”。
为了解决这个问题,可以使用 context 变量来为
字符串设置上下文,这样在翻译工具中就可以根据不同
语言进行不同的翻译。

JavaScript 中


__("Change", null, "Coins")

// Here "Coins" is the context for text "Change"

Python 中


_("Change", context="Switch")

# Here "Switch" is the context for text "Change"

添加新语言

要添加新语言,请按照以下步骤操作:

第 1 步:导出到文件

$ bench --site sitename get-untranslated [lang] [path-to-file]

第 2 步:翻译

创建另一个包含更新翻译的文件(顺序与源文件
相同)。为此,你可以使用 Google 翻译或
Bing 翻译。

第 3 步:导入你的翻译

$ bench update-translations [lang] [source-path] [translated-path]

会在每个应用的 translations 文件夹中创建一个名为 [lang].csv
的新文件。

第 4 步:更新 languages.json

在 frappe/geo/languages.json 中添加你的语言

第 5 步:提交每个应用并推送

将包含 .csv 文件的更改提交到每个应用,并推送到它们的
代码仓库。

性能分析与监控

记录器 – SQL 性能分析器

Frappe 记录器是 Frappe 框架内置的一个性能分析工具,用于捕获所有请求和后台任务,以及执行的 SQL 查询和相应的堆栈跟踪。

使用示例:你发现某个 DocType 保存时耗时过长,怀疑 SQL 查询可能是瓶颈。在这种情况下,启动记录器然后提交你的文档,就能完整捕获保存文档时触发的所有查询。

  1. 以管理员身份登录。从 Awesomebar 打开记录器,点击“开始”。
  2. 系统会要求你配置记录选项。选择适当的选项并开始记录。
  3. 执行你想要分析的操​​作(最好在另一个标签页中进行)。
  4. 捕获到足够的信息后,再次进入记录器并停止记录。

现在你将看到所有已处理和记录的请求或任务的列表。你可以按各种列进行排序,以找出有问题的请求。

点击某一行可打开该请求以查看额外信息。捕获中包含以下信息:

  1. path – 请求的路径或任务名称,例如 /app
  2. cmd – 方法的点分路径(后台任务为空)。
  3. time – 请求创建的时间。
  4. duration – 完成请求的耗时(参见下方的实现说明)。
  5. number of queries – 为完成该请求执行的 SQL 查询数量。
  6. Time in queries – SQL 查询所花费的时间。
  7. Request headers – 随请求收到的 HTTP 头(后台任务为空)。
  8. Form Dict – 随请求收到的表单数据(后台任务为空)。
  9. SQL queries – 所有已运行的 SQL 查询的表格。

要了解某个特定查询的更多信息,点击该行可展开额外信息。这包括查询的耗时、堆栈跟踪以及该查询的 SQL EXPLAIN 输出。

实现说明:记录器在捕获详细信息时会增加相当大的开销,因此总耗时并不代表真实环境下的性能。不过查询时间非常接近真实环境下的性能。

导入和导出 Frappe 记录器捕获数据

你可以导出记录器捕获数据,并将其导入到另一个站点以进行进一步分析。

  1. 进入记录器页面。记录完成后,点击菜单(三个点)> 导出。
  2. 这将下载一个包含捕获数据的 JSON 文件。要在另一个站点上查看,你可以点击导入并选择该 JSON 文件来查看捕获的数据。

使用记录器进行 Python 性能分析

除了捕获 SQL 查询外,记录器还可以运行内置的 Python 性能分析器“cProfile”,为你提供运行 Python 函数的性能分析输出。你可以在启动记录器时启用此选项。

如果你在记录器中启用 cProfiler,它会在记录期间增加更高的开销。请确保在调试完成后立即将其禁用。你也可以过滤请求/任务,以避免影响生产流量。

使用 bench 对函数进行性能分析

Bench 的 execute 命令会运行指向方法的点分路径,并且它也支持性能分析。

▶ bench --site [sitename] --profile execute erpnext.projects.doctype.task.task.set_tasks_as_overdue

现在你应该能够通过 execute 运行大多数可以通过控制台运行的命令,包括 db 方法。

▶ bench --site [sitename] execute frappe.db.get_database_size
6784

Frappe 监控器

监控器会记录请求和任务的元数据。要启用此功能,请在你的站点配置中设置 "monitor": 1。

收集到的数据会缓存在 redis 缓存中,并通过一个计划任务 frappe.monitor.flush 定期移动到 logs 目录下的 monitor.json.log 文件中。

{
    "duration": 807142,
    "request": {
        "ip": "127.0.0.1",
        "method": "GET",
        "path": "/api/method/frappe.realtime.get_user_info",
        "response_length": 9687,
        "status_code": 500
    },
    "site": "frappe.local",
    "timestamp": "2020-03-05 09:37:17.397884",
    "transaction_type": "request",
    "uuid": "83be6a4c-27a1-497a-9ce6-c815bca4e420"
}
{
    "duration": 1364,
    "job": {
        "method": "frappe.ping",
        "scheduled": false,
        "wait": 90204
    },
    "site": "frappe.local",
    "timestamp": "2020-03-05 09:37:40.124682",
    "transaction_type": "job",
    "uuid": "8225ab76-8bee-462c-b9fc-a556406b1ee7"
}

后台任务监控

Frappe 使用 RQ(Redis 队列)在后台异步执行长时间任务。你可以使用以下内置虚拟 doctype 来监控 RQ:

1. RQ Worker – 监控后台工作进程

RQ worker doctype 显示你站点上所有正在消费后台任务队列的后台工作进程。它还包含关于该工作进程的基本统计信息,如名称、时间、成功和失败的任务数量以及当前状态。

2. RQ Job – 监控和控制后台任务。

RQ Job 是一个虚拟 doctype,提供关于所有后台任务的信息。你可以按队列和状态过滤任务。

RQ 任务的表单视图显示该任务的所有信息。

调试卡住的进程

如果你认为某个工作进程卡住了,可以向它发送 SIGUSR1 信号,它会将其所有线程的当前堆栈打印到 stderr。

示例:kill -SIGUSR1 <pid></pid>

在典型的部署中,你可以在日志文件中找到写入 stderr 的输出:

  • Web 工作进程 – bench/logs/web.error.log
  • 后台工作进程 – bench/logs/worker.error.log
  • 调度器 – bench/logs/schedule.error.log

监控服务的整体健康状况

“系统健康报告”可用于快速检查系统健康状况。这包括但不限于:

  • 后台任务
  • 调度器
  • 数据库
  • 缓存
  • 电子邮件
  • 错误日志
  • 存储
  • 备份
  • 用户

在 Awesomebar 中搜索“系统健康报告”即可查看此报告。

为Frappe应用文档做贡献

Frappe 应用的文档维护在 Frappe Wiki 中。这样每位社区成员都可以为新应用贡献文档,或改进现有文档。就像 Wiki 社区一样,这里有编辑者和审核者。作为个人,你可以通过编辑新功能和改进的文档来建立自己的声誉,直到晋升为审核者角色。

贡献文档的步骤

登录和访问页面

在编辑文档之前,请确保先注册。你可以从这里登录/注册。

登录后,你将能看到编辑现有文档页面的选项。

编辑页面

在页面底部,点击省略号菜单(…)并选择 edit。

Markdown 编辑器

点击编辑按钮后,你应该会看到这样的界面

现在你可以编辑 markdown 代码,并在 `预览标签页` 中查看预览

所见即所得编辑器

如果你不想编辑 markdown,而是想通过所见即所得编辑器来编辑,请点击切换按钮并选择富文本

在 Markdown 中上传媒体文件

在 Markdown 模式下上传图片的步骤。

  1. 点击“添加附件”按钮上传图片
  2. 使用复制链接按钮将链接复制下来!
  3. 然后将链接粘贴到编辑器中以显示图片

在所见即所得编辑器中

在所见即所得模式下上传图片的步骤

  1. 你可以粘贴已复制的图片
  2. 列表项:拖放图片
  3. 使用编辑器顶部的图片按钮

提交文档

编辑完文件后,点击提交按钮,等待你的更改被提交。提交后,你将看到此界面

在此界面上,你可以跟踪所有贡献,并等待审核者审核你的更改,然后批准、拒绝或请求修改。

你可以点击打开的贡献来继续你的工作,或使用评论系统与审核者沟通。

创建新的 Wiki 页面

如果你想创建新页面而不是编辑页面,可以在左侧边栏底部找到新建页面链接。之后的流程与编辑类似。

编辑侧边栏

你还可以在编辑/新建页面时拖放侧边栏来重新排列它们(仅在必要时使用)

期待看到大量的贡献!

调试

服务器

在开发过程中运行 bench start 命令时,Procfile 中每个进程的日志都会输出到终端窗口中。

▶ bench start
14:55:17 system | redis_cache.1 started (pid=4085)
14:55:17 system | redis_socketio.1 started (pid=4086)
14:55:17 system | redis_queue.1 started (pid=4088)
14:55:17 system | web.1 started (pid=4089)
14:55:17 system | socketio.1 started (pid=4090)
14:55:17 system | watch.1 started (pid=4094)
14:55:17 system | worker_short.1 started (pid=4096)
14:55:17 system | schedule.1 started (pid=4095)
14:55:17 redis_queue.1 | 4088:C 22 May 14:55:17.257 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
14:55:17 redis_queue.1 | 4088:C 22 May 14:55:17.264 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=4088, just started
14:55:17 redis_queue.1 | 4088:C 22 May 14:55:17.264 # Configuration loaded
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.265 * Increased maximum number of open files to 10032 (it was originally set to 4864).
14:55:17 redis_cache.1 | 4085:C 22 May 14:55:17.262 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
14:55:17 redis_cache.1 | 4085:C 22 May 14:55:17.268 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=4085, just started
14:55:17 redis_cache.1 | 4085:C 22 May 14:55:17.268 # Configuration loaded
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.269 * Increased maximum number of open files to 10032 (it was originally set to 4864).
14:55:17 redis_socketio.1 | 4086:C 22 May 14:55:17.262 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
14:55:17 redis_socketio.1 | 4086:C 22 May 14:55:17.270 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=4086, just started
14:55:17 redis_socketio.1 | 4086:C 22 May 14:55:17.270 # Configuration loaded
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.272 * Increased maximum number of open files to 10032 (it was originally set to 4864).
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.285 * Running mode=standalone, port=11002.
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.285 # Server initialized
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.286 * Ready to accept connections
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.287 * Running mode=standalone, port=13002.
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.292 # Server initialized
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.292 * Ready to accept connections
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.294 * Running mode=standalone, port=12002.
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.294 # Server initialized
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.295 * Ready to accept connections
14:55:17 system | worker_long.1 started (pid=4098)
14:55:17 system | worker_default.1 started (pid=4100)
14:55:18 socketio.1 | listening on *: 9002
14:55:20 socketio.1 | { Error: connect ECONNREFUSED 0.0.0.0:8002
14:55:20 socketio.1 | at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1191:14)
14:55:20 socketio.1 | errno: 'ECONNREFUSED',
14:55:20 socketio.1 | code: 'ECONNREFUSED',
14:55:20 socketio.1 | syscall: 'connect',
14:55:20 socketio.1 | address: '0.0.0.0',
14:55:20 socketio.1 | port: 8002,
14:55:20 socketio.1 | response: undefined }
14:55:24 web.1 | * Running on http://0.0.0.0:8002/ (Press CTRL+C to quit)
14:55:24 web.1 | * Restarting with fsevents reloader
14:55:24 watch.1 | yarn run v1.10.1
14:55:24 watch.1 | $ node rollup/watch.js
14:55:25 web.1 | * Debugger is active!
14:55:25 web.1 | * Debugger PIN: 321-355-865
14:55:26 watch.1 |
14:55:26 watch.1 | Rollup Watcher Started
14:55:26 watch.1 |
14:55:26 watch.1 | Watching...
14:55:26 watch.1 | Rebuilding frappe-web.css
14:55:27 watch.1 | Rebuilding frappe-web-b4.css
14:55:27 watch.1 | Rebuilding chat.js
14:55:28 web.1 |
14:55:28 web.1 | test print

当你在 Python 代码中编写任何 print 语句时,如果是请求/响应相关的,它会显示在 web: 进程日志中;如果代码在后台任务中运行,则会显示在某个 worker_ 进程中。

> 如果你是 VSCode 用户,可以直接在编辑器中通过设置断点来调试代码。按照这些[步骤](在 VS Code 中调试 / 调试适配器协议)进行设置。

日志

如果你运行的是生产环境,你就更需要日志来跟踪 Frappe 环境的尽可能多的信息。

开箱即用,日志存储在 bench 中的 ./logs 文件夹下。从 Frappe 13 版本开始,日志也可以在站点级别访问,位于 ./sites/{site}/logs 下。

这些站点日志由 Frappe 应用程序创建,而许多 bench 级别的日志文件则由支持你 Frappe 环境的进程生成。查看 bench 中的 Procfile 或 supervisor.conf 以获取更多信息。

> 在此处了解更多关于 Frappe 日志的信息,在此处了解 Frappe 日志 API。

控制台

要使用 Python API,bench 提供了一个 iPython shell。运行以下命令后,它会导入 frappe、初始化它并连接到数据库。

▶ bench --site [sitename] console

In [1]: frappe.get_doc('Task', 'TASK00004')
Out[1]: <erpnext.projects.doctype.task.task.task at="" 0x10825d710="">

客户端

客户端调试就像在你的 JS 文件中添加一个 debugger 语句一样简单。你必须在浏览器中打开开发者工具,它才会在该语句处暂停。

frappe.db.get_value('Task', 'TASK00004', 'status')
    .then(values => {
        debugger
        console.log(values);
    })

控制台

要使用客户端 API,你可以打开浏览器的控制台,使用全局可用的 frappe 对象来探索和运行方法以及访问属性。

浏览器控制台

> 在此处了解更多关于客户端 API 的信息

在 VS Code 中调试 / 调试适配器协议

正常运行的检查清单。

  • 更新 Procfile
  • 获取 Visual Studio Code(废话!)
  • 为 VS Code 安装 Python 扩展
  • 更新 launch.json
  • 开始调试

注意事项:

  1. 禁用自动重载功能(不过你可以通过手动重载(⌘⇧F5)达到相同效果)
  2. 禁用 Werkzeug 多线程

1. 更新 Procfile

注意:这会修改 bench start 的行为

从 Procfile(位于 bench 目录中)注释掉一行(在其前面加上 #),该行看起来像这样。

web: bench serve --port 8000

我们将从 VS Code 运行此进程,而不是用 bench start 来运行它。

2. 更新 launch.json 在 VS Code 中为你的 launch.json 添加一个配置,应该看起来像这样(这大致上就是被移除的 Procfile 行所做的事情)。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Bench",
            "type": "python",
            "request": "launch",
            "justMyCode": false,
            "program": "${workspaceFolder}/frappe/frappe/utils/bench_helper.py",
            "args": [
             "frappe", "serve", "--port", "8000", "--noreload", "--nothreading"
            ],
            "python": "${workspaceFolder}/../env/bin/python",
            "cwd": "${workspaceFolder}/../sites",
            "env": {
             "DEV_SERVER": "1"
            }
        }
    ]
}

给定配置中提到的路径假设你将 apps 目录作为你的工作区目录(你用 code 打开的目录)。workspaceFolder 是一个 VS Code 变量,指向(如果从名字看不出来的话)工作区目录。

你并非必须使用 apps 作为你的工作区目录,但请记得相应地修改 workspaceFolder、pythonPath 和 cwd。

3. 执行 bench start

这应该像往常一样保持运行。

4. 开始调试

VS Code -> 调试面板(⌘⇧D)-> 开始调试,或使用键盘快捷键(⌘⇧F5)。

说明

1. program 和 args

"program": "${workspaceFolder}/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "serve", "--port", "8000", "--noreload", "--nothreading"],

做的事情与 bench serve --port 8000 --noreload --nothreading 完全相同,也就是等同于

cd sites
../env/bin/python ../apps/frappe/frappe/utils/bench_helper.py frappe serve  --port 8000 --noreload --nothreading

--noreload 禁用了 Werkzeug 的自动重载功能,--nothreading 禁用了多线程。

2. cwd

"cwd": "${workspaceFolder}/../sites",

上述命令必须从 sites 目录执行。

3. env

"env": {
    "DEV_SERVER": "1"
}

bench start 创建一个环境变量 DEV_SERVER 并将其设置为 1。没有这个,Socket.io 无法正常工作(说来话长)。

仅运行 bench serve 不会设置此变量,所以你需要显式设置它。

注意:

目前,frappe 使用 use_reloader=True 和 threaded=True 运行,VS Code 调试器由于某些原因与这些功能配合不佳,Django 和 Flask 也有这个问题。

办公桌

在 Frappe 列表视图中快速选择行

  • 点击复选框并在行上拖动,即可选择多行。
  • 要取消选择,再次点击并在已选中的行上拖动即可。

快速查看数据库字段名称

  • 查看表单时,按住 alt 并将鼠标悬停在字段上,即可查看该字段在数据库中的存储名称。

界面测试

你可以使用 Cypress 编写 UI 测试。它是一个基于 NodeJS 的
全栈测试框架,不依赖 Selenium。

要编写集成测试,请在 cypress/integration
目录中创建一个 .js 文件。

示例

以下是一个用于检查待办事项插入的集成测试示例

context('ToDo', () => {
 before(() => {
 cy.login('Administrator', 'admin');
 cy.visit('/desk');
 });

 it('creates a new todo', () => {
 cy.visit('/app/todo/new-todo-1');
 cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
 cy.get('.page-title').should('contain', 'Not Saved');
 cy.get('.primary-action').click();
 cy.visit('/desk#List/ToDo');
 cy.location('hash').should('eq', '/app/todo');
 cy.get('.list-row').should('contain', 'this is a test todo');
 });
});

在本地运行 Cypress

Cypress 使用你系统上安装的任意基于 Chromium 的浏览器来运行测试。
每个应用都有自己的 Cypress 测试套件。要运行某个应用的测试,请在 frappe-bench 目录中运行
以下命令。

bench --site [sitename] run-ui-tests [app]

这将打开 Cypress Electron 界面,你可以在其中手动运行任意测试,或
运行所有测试。

你也可以在无头模式下运行测试。

# run in headless mode
bench --site [sitename] run-ui-tests [app] --headless

要启用 Cypress 并行测试,你可以传入 --parallel 标志。
有关 Cypress 并行测试工作原理的更多信息可以在此处找到。

# run tests parallelly
bench --site [sitename] run-ui-tests [app] --parallel

代码覆盖率

代码覆盖率有助于识别测试期间执行了源代码的哪些行。为了测量代码覆盖率,需要对源代码进行插桩,并且需要将此插桩后的源代码与我们的测试运行器集成,以收集覆盖率并生成报告。

对于 Frappe 中的 Cypress 测试,.js 文件使用 Istanbul 进行插桩,并使用 Cypress 代码覆盖率插件合并每个测试的覆盖率并保存合并结果。

代码插桩:

为了计算源代码的哪些行被执行了,会通过插桩向代码中插入额外的计数器。例如:

插桩前:

function foo(a, b) {
 if (a < b)
 return b - a;
 else
 return a - b;
}

插桩后:

cov_1m1jljnmzu();

function foo(a, b) {
 cov_1m1jljnmzu().f[0]++;
 cov_1m1jljnmzu().s[0]++;

 if (a < b) {
 cov_1m1jljnmzu().b[0][0]++;
 cov_1m1jljnmzu().s[1]++;
 return b - a;
 } else {
 cov_1m1jljnmzu().b[0][1]++;
 cov_1m1jljnmzu().s[2]++;
 return a - b;
 }
}

当使用这个修改过(插桩)的源代码进行测试时,这些计数器会随着代码的执行而递增,并生成一个覆盖率对象。然后 Cypress 代码覆盖率插件会处理收集到的覆盖率并生成覆盖率报告。

在本地生成代码覆盖率报告:

  1. 使用 istanbul/nyc 对源代码进行插桩:
npx nyc instrument -x 'frappe/public/dist/**' -x 'frappe/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place frappe

这会将 frappe 文件夹中的现有源代码替换为插桩后的源代码。-x 标志用于排除指定的路径。你也可以使用 -n 标志来指定要包含的路径。有关 nyc instrument 命令的更多详细信息,请参见此处

  1. 运行 Cypress 测试:
bench --site test_site run-ui-tests frappe --with-coverage
  1. 生成报告:
npx nyc report --reporter=text

有关其他报告格式,请参见此处

Testing-Library 查询

你也可以在 Cypress 测试中使用 Testing Library 查询。Testing Library 提供了以下测试工具:

  • 让编写模拟用户与应用交互方式的 UI 测试变得更加容易
  • 让在不需要了解所有实现细节的情况下查找 DOM 中的元素变得更加容易
  • 帮助保持测试的可维护性(这样小的重构就不会破坏测试)

有关用法的更多详细信息,请参见 Testing Library 文档

Testing Library 提供了多种查询来查找页面上的元素。以下是一些示例:

  • ByRole

在此处查看 HTML 元素及其默认角色的表格

查询 元素
findByRole('button', {name: 'Save'}) “ 且可访问名称为 ‘Save’
findByRole('checkbox') “
findByRole('textbox') , (也匹配其他默认 role=’textbox’ 的元素)
findByRole('searchbox') <input type="search">
findByRole('listbox') <select></select>, “
  • ByLabelText
查询 元素
findByLabelText('Optimize') 与标签 ‘Optimize’ 关联的元素
  • ByPlaceholderText
查询 元素
findByPlaceholderText('Name') placeholder=’Name’ 的元素
  • ByText
查询 元素
findByText('example.json') textContent=’example.json’ 的元素
  • ByDisplayValue
查询 元素
findByDisplayValue('Option 1') 且选中的 <option></option> 为 ‘Option 1’ (也匹配值属性相符的 <input> 或 <textarea></textarea>)
  • ByTitle
查询 元素
findByTitle('Open Link') title=’Open Link’ 的元素
  • ByAltText
  • ByTestId

测试

Frappe 提供了一些基本工具来编写自动化测试。以下是一些基本规则:

  1. 测试可以放在仓库中的任何位置,但必须以 test_ 开头,并且应该是一个 .py 文件。
  2. 测试运行器会自动为依赖的 DocType 构建测试记录,这些 DocType 由 Link 类型字段(外键)标识。
  3. 对于非 DocType 测试,你可以编写简单的单元测试,并在文件名前加上 test_。

编写测试

当你创建一个新的 DocType(在开发者模式下)时,样板文件中也包含 test_{doctype}.py 文件。测试文件应该负责创建依赖项并清理它们。

以下是一个参考自 test_event.py 的示例测试文件。

import frappe
import unittest

def create_events():
 if frappe.flags.test_events_created:
 return

 frappe.set_user("Administrator")
 doc = frappe.get_doc({
 "doctype": "Event",
 "subject":"_Test Event 1",
 "starts_on": "2014-01-01",
 "event_type": "Public"
 }).insert()

 doc = frappe.get_doc({
 "doctype": "Event",
 "subject":"_Test Event 3",
 "starts_on": "2014-01-01",
 "event_type": "Public"
 "event_individuals": [{
 "person": "test1@example.com"
 }]
 }).insert()

 frappe.flags.test_events_created = True

class TestEvent(unittest.TestCase):
 def setUp(self):
 create_events()

 def tearDown(self):
 frappe.set_user("Administrator")

 def test_allowed_public(self):
 frappe.set_user("test1@example.com")
 doc = frappe.get_doc("Event", frappe.db.get_value("Event",
 {"subject":"_Test Event 1"}))
 self.assertTrue(frappe.has_permission("Event", doc=doc))

 def test_not_allowed_private(self):
 frappe.set_user("test1@example.com")
 doc = frappe.get_doc("Event", frappe.db.get_value("Event",
 {"subject":"_Test Event 2"}))
 self.assertFalse(frappe.has_permission("Event", doc=doc))

为命令编写测试

要为你的 Bench 命令编写测试,你可以将测试分组到一个类中,该类继承自 frappe.tests.test_commands 中的 BaseTestCommands 和 unittest.TestCase,以便它在 bench run-tests 命令期间运行。

作为参考,以下是为 bench execute 命令编写的一些测试。

class TestCommands(BaseTestCommands, unittest.TestCase):
 def test_execute(self):
 # test 1: execute a command expecting a numeric output
 self.execute("bench --site {site} execute frappe.db.get_database_size")
 self.assertEqual(self.returncode, 0)
 self.assertIsInstance(float(self.stdout), float)

 # test 2: execute a command expecting an errored output as local won't exist
 self.execute("bench --site {site} execute frappe.local.site")
 self.assertEqual(self.returncode, 1)
 self.assertIsNotNone(self.stderr)

 # test 3: execute a command with kwargs
 # Note:
 # terminal command has been escaped to avoid .format string replacement
 # The returned value has quotes which have been trimmed for the test
 self.execute("""bench --site {site} execute frappe.bold --kwargs '{{"text": "DocType"}}'""")
 self.assertEqual(self.returncode, 0)
 self.assertEqual(self.stdout[1:-1], frappe.bold(text='DocType'))

运行测试

运行测试可能需要应用在其 dev-requirements.txt 文件中指定的额外依赖项。在运行测试之前,请确保所有应用都已使用 bench setup requirements --dev 安装了开发依赖项。

运行以下命令来运行所有测试。它会先构建所有测试依赖项一次,然后运行你的测试。你应该从 frappe_bench 文件夹中运行测试。

# run all tests
bench --site [sitename] run-tests

# run tests for only frappe app
bench --site [sitename] run-tests --app frappe

# run tests for the Task doctype
bench --site [sitename] run-tests --doctype "Task"

# run tests for All doctypes in specified Module Def
bench --site [sitename] run-tests --module-def "Contacts"

# run a test using module path
bench --site [sitename] run-tests --module frappe.tests.test_api

# run a specific test from a test file
bench --site [sitename] run-tests --module frappe.tests.test_api --test test_insert_many

# run tests without creating test records
bench --site [sitename] run-tests --skip-test-records --doctype "Task"

# profile tests and show a report after tests execute
bench --site [sitename] run-tests --profile --doctype "Task"
.
----------------------------------------------------------------------
Ran 1 test in 0.010s

OK

9133 function calls (8912 primitive calls) in 0.011 seconds

Ordered by: cumulative time

ncalls tottime percall cumtime percall filename:lineno(function)
 2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert)
 1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate)
 13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql)
 255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get)
 12 0.000 0.000 0.002 0.000

# verbose log level for tests
bench --site [sitename] --verbose run-tests

检查测试是否正在运行

你可以通过评估 frappe.in_test 全局变量来检查你的代码是否作为测试的一部分被执行。在较旧的框架版本中,你可以改为评估 frappe.flags.in_test(这将在即将发布的版本中被弃用)。

你可以使用该全局变量根据测试是否正在运行来在代码中添加分支。例如:

if not frappe.in_test:
    notify_error_to_user()

并行运行测试

随着项目中测试数量的增长,如果在一台机器上串行运行,测试完成需要很长时间。
在多台测试机器上并行运行测试可以在持续集成(CI)中节省时间。

并行测试

命令:

bench --site [sitename] --app [app-name] run-parallel-tests --build-id <build-number> --total-build <total-number-of-builds>

用法:

如果你想在 2 个 CI 实例上运行测试,你的命令如下:

# in first CI instance
bench --site [sitename] run-parallel-tests --build-id 1 --total-builds 2

# in second CI instance
bench --site [sitename] run-parallel-tests --build-id 2 --total-builds 2

注意:该命令会将所有测试文件分成 2 部分,并在这些 CI 实例中执行它们。
测试列表的前半部分将在第一个实例中执行,测试列表的后半部分将在第二个实例中执行。

使用编排器进行并行测试

可能会出现每个测试完成所需时间不同的情况,这可能导致 CI 构建之间的时间不均衡。为了缓解这个问题,你可以使用测试编排器,它根据 CI 实例的可用性来运行下一个测试。使用测试编排器进行并行测试的命令如下。

命令:

bench --site [sitename] --app [app-name] run-parallel-tests --use-orchestrator

用法:

如果你想在 2 个 CI 实例上运行测试,你的命令如下

# in first CI instance
bench --site [sitename] run-parallel-tests --use-orchestrator

# in second CI instance
bench --site [sitename] run-parallel-tests --use-orchestrator

注意:此命令需要环境变量 CI_BUILD_ID 和 ORCHESTRATOR_URL。CI_BUILD_ID 是你在每次 CI 构建运行时获得的唯一 ID。
ORCHESTRATOR_URL 是你托管编排器后获得的公开可访问 URL。

对比

为了清楚地了解上述并行测试命令变体的工作方式,请查看以下示例。

假设有 4 个测试文件如下

test_module_one.py 4 mins (execution time)
test_module_two.py 2 mins
test_module_three.py 1 min
test_module_four.py 1 min

不使用并行测试命令所需的时间。

test_module_one.py 4 mins
test_module_two.py 2 mins
test_module_three.py 1 min
test_module_four.py 1 min
==============================
Total Wait Time 8 mins

使用第一个命令将测试文件自动拆分到 2 个测试实例所需的时间。

# First instance # Second instance
test_module_one.py 4 mins test_module_three.py 1 min
test_module_two.py 2 mins test_module_four.py 1 min
---------------------------- ----------------------------
 6 mins 2 mins

==============================
Total Wait Time 6 mins

使用第二个命令(使用编排器根据 2 个测试实例的可用性运行测试)所需的时间可能如下。

# First instance # Second instance
test_module_one.py 4 mins test_module_two.py 2 mins
---------------------------- test_module_three.py 1 mins
 4 mins test_module_four.py 1 min
 ----------------------------
 4 mins
==============================
Total Wait Time 4 mins

注意:第一个实例上只执行了一个测试文件,因为它忙碌了 4 分钟。到那时,第二个实例能够执行其他测试文件,这有助于平衡构建之间的时间。

速率限制

Frappe 框架开箱即用地支持对 HTTP 请求进行速率限制。

Frappe 框架基于请求所消耗的时间实现了固定窗口速率限制。该限制针对在配置窗口内所有 HTTP 请求所耗时间之和进行约束。每经过 window 秒后周期会重置,例如,将 window 设置为 3600 秒,就会根据站点时区在每个小时开始时将使用计数器重置为 0。

注意:超过限制的请求不会被处理,并会返回 HTTP 429(请求过多)响应。

你可以通过在 site_config.json 中添加类似以下配置来为你的站点启用速率限制:

{
 "rate_limit": {
 "limit": 600,
 "window": 3600
 }
}
键 描述
limit 在速率限制窗口内允许使用的最大时间量(以秒为单位)。
window 速率限制窗口的大小(以秒为单位)。

每个 HTTP 请求返回的 HTTP 头都会显示当前的速率限制状态,例如:

curl -i https://frappe.io/docs
HTTP/1.1 200 OK
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 518060453
X-RateLimit-Reset: 3513
X-RateLimit-Used: 100560

如果在配置的限制耗尽后仍有请求,则会返回 HTTP 429 响应以及速率限制状态:

curl -i https://frappe.io/docs
HTTP/1.1 429 TOO MANY REQUESTS
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1242
Retry-After: 1242
头部 描述
Retry-After 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Limit 在一个速率限制窗口内允许使用的时间(以微秒为单位)。
X-RateLimit-Remaining 当前速率限制窗口中剩余(可使用)的时间(以微秒为单位)。
X-RateLimit-Reset 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Used 处理当前请求所使用的时间(以微秒为单位)。