Skip to content

Commit

Permalink
feat: add-logging-for-llm-app.md
Browse files Browse the repository at this point in the history
  • Loading branch information
levy committed Dec 16, 2023
1 parent ded60f9 commit 3330c46
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 3 deletions.
7 changes: 4 additions & 3 deletions src/.vuepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ export default sidebar({
]
},
{
text: "DevOps,
text: "DevOps",
prefix: "/devops",
children: [
'docker-build-and-push-script',
'reduce-python-image-size',
'what-is-the-difference-between-sh-and-bash',
'about-arm-things-you-need-to-know',
'common-solutions-of-object-storage-for-static-assets',
Expand Down Expand Up @@ -93,8 +95,7 @@ export default sidebar({
text: "Python",
prefix: "/python",
children: [
'docker-build-and-push-script',
'reduce-python-image-size',
'add-logging-for-llm-app.md',
"mr.py",
"export-mysql-table-into-excel",
]
Expand Down
138 changes: 138 additions & 0 deletions src/python/add-logging-for-llm-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
date: 2023-12-04
tag:
- Python
---
# 给LLM应用添加日志

## logging替代print
目前公司的LLM应用开发使用的是 Python 技术栈,观察源码,发现没有多少日志,纵使有,也是用的 print。

print 的作用,就相当于 Java 的 System.out.print,相当于 Node.js 的 console.log,一般只适合在本地调试,不适合作为日志输出的。

<!-- more -->

打包成 Docker 镜像时,Python 应用很可能看不到 [print 输出](https://stackoverflow.com/questions/29663459/why-doesnt-python-app-print-anything-when-run-in-a-detached-docker-container)。使用 pytest 执行测试用例时,[默认也是看不到 print](https://stackoverflow.com/questions/24617397/how-do-i-print-to-console-in-pytest) 输出的。再加上 print 太简陋了,输出没有时间,没有级别分类,建议还是弃用,改用专门的日志模块。

使用 Python 内置的日志模块 logging,直接 import 即可使用:
```python
import logging
logging.info("log")
```

如果要在 pytest 中显示日志,还要在项目根目录添加 pytest.ini 文件,补充如下内容:
```shell
[pytest]
log_cli = 1
log_cli_level = INFO
```
logging 模块完整的用法,可以[点击查看文章](https://betterstack.com/community/guides/logging/how-to-start-logging-with-python/)
## 何时打印日志
为避免排查线上问题时,被迫吐槽:“怎么一点日志都没有!”建议平时养成打日志的习惯,方便应用的迭代与维护。

下面给出一些通用的打印日志的实践建议,与语言无关,可按需采纳。

1. 外部调用
2. 异常捕获
3. 提前返回
4. 复杂或特殊的if-else

### 外部调用
调用另一个应用的API,与中间件(如 redis, rocketmq)交互,都属于外部调用,最好调用前后都打印日志(当然,如果返回的数据量太大,酌情可以考虑省略打印部分返回信息)

示例:
```java
log.info("准备发送 mq");
MQRequest mqRequest = buildMQRequest(dto);
mqApi.sendMessage(mqRequest);
```

```java
public void sendMessage(MqRequest mqRequest){
String destination = mqRequest.getTopic() + ":" + mqRequest.getTag();
rocketMQTemplate.asyncSend(destination, JSONObject.toJSONString(mqRequest.getData()), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
logger.info("发送成功, {}, 内容: {}",sendResult.toString(), JSONObject.toJSONString(mqRequest.getData()));
}
@Override
public void onException(Throwable throwable) {
logger.error("发送异常:",throwable);
retry(mqRequest)
}
});
}
```
### 异常捕获
异常捕获后,一定要打印日志。实在想偷懒,直接打印堆栈信息都能授受。最忌讳的是,捕获了异常,然后什么都不做,直接把异常信息给“吃了”,这绝对是排查问题的恶手。

示例:
```java
private void saveDataToHive() {
log.info("累积{}条数据,开始存储!", cachedDataList.size());

try {
//省略执行写文件操作代码...

log.info("存储成功!");
} catch (IOException e) {
logException(e, "保存数据时,把 CSV 文件内容写到 HDFS 上出错");
} finally {
//省略关闭流代码...
}
}

```
### 提前返回
如果函数有提前 return 的逻辑,最好返回前也打印日志,不然排查问题时,发现请求进来了,却什么日志也没有,容易让人一头雾水。

示例:
```java
public Payload<List<MenuTreeVO>> getMenuByAppId(@Valid Param param) {
if (!checkoutSubscribe(param.getAppId())) {
log.info("checkoutSubscribe: false, 因此返回空数组");
return new Payload<>(Collections.emptyList());
}
log.info("checkoutSubscribe: true");

return new Payload<>(menuResourceService.getAccountMenuList(param));
}

```
### 复杂或特殊的if-else
如何定义复杂,又如何定义特殊,这就见仁见智,需要个人在实践中去总结理解了。

一个常见的场景是,某段逻辑因为业务变化要加 if-else 进行特殊处理,你得在这个 if-else 前加上注释,解释其原因。那么这段逻辑就可以添加日志,日志内容就是你的注释内容,也即把注释改写成日志即可。

示例:
```java
if (SPECIAL_APP_CODE.equals(req.getAppCode())) {
log.info("特殊应用,跳过从 plugin.json 读取配置的代码");
}
else {
log.info("从 plugin.json 中读取配置...");
confs = CommonUtils.readFromProperties(localPath, "");
}

```
```java
public void checkAndSetAppPlugins(DatasourceDO datasourceDO, SubApplicationDTO subApplicationDTO) {
if (Boolean.TRUE.equals(datasourceDO.getIsUsePlugin())) {
log.info("检查 {} 插件中...", subApplicationDTO.getAppCode());

String datasourceType = datasourceDO.getType();
Long datasourceTypeId = getTypeId(datasourceType.toLowerCase());

if (Objects.isNull(datasourceTypeId)) {
throw new ApplicationException(String.format("%s缺少%s的插件,请联系运营人员上传相应插件",
subApplicationDTO.getAppCode(), datasourceType));
}
log.info("完成检查, {} 存在 {} 插件...", subApplicationDTO.getAppCode(), datasourceType);

setAppPlugins(driverPluginDOList);
}
else {
log.info("不需要检查插件");
}
}
```

0 comments on commit 3330c46

Please sign in to comment.