给LLM应用添加日志
给LLM应用添加日志
logging替代print
目前公司的LLM应用开发使用的是 Python 技术栈,观察源码,发现没有多少日志,纵使有,也是用的 print。
print 的作用,就相当于 Java 的 System.out.print,相当于 Node.js 的 console.log,一般只适合在本地调试,不适合作为日志输出的。
打包成 Docker 镜像时,Python 应用很可能看不到 print 输出。使用 pytest 执行测试用例时,默认也是看不到 print 输出的。再加上 print 太简陋了,输出没有时间,没有级别分类,建议还是弃用,改用专门的日志模块。
使用 Python 内置的日志模块 logging,直接 import 即可使用:
import logging
# 在入口函数添加以下一行
logging.basicConfig(level=logging.INFO)
# 使用示例
logging.info("log")
如果要在 pytest 中显示日志,还要在项目根目录添加 pytest.ini 文件,补充如下内容:
[pytest]
log_cli = 1
log_cli_level = INFO
logging 模块完整的用法,可以点击查看文章。
何时打印日志
为避免排查线上问题时,被迫吐槽:“怎么一点日志都没有!”建议平时养成打日志的习惯,方便应用的迭代与维护。
下面给出一些通用的打印日志的实践建议,与语言无关,可按需采纳。
- 外部调用
- 异常捕获
- 提前返回
- 复杂或特殊的if-else
外部调用
调用另一个应用的API,与中间件(如 redis, rocketmq)交互,都属于外部调用,最好调用前后都打印日志(当然,如果返回的数据量太大,酌情可以考虑省略打印部分返回信息)
示例:
log.info("准备发送 mq");
MQRequest mqRequest = buildMQRequest(dto);
mqApi.sendMessage(mqRequest);
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)
}
});
}
异常捕获
异常捕获后,一定要打印日志。实在想偷懒,直接打印堆栈信息都能授受。最忌讳的是,捕获了异常,然后什么都不做,直接把异常信息给“吃了”,这绝对是排查问题的恶手。
示例:
private void saveDataToHive() {
log.info("累积{}条数据,开始存储!", cachedDataList.size());
try {
//省略执行写文件操作代码...
log.info("存储成功!");
} catch (IOException e) {
logException(e, "保存数据时,把 CSV 文件内容写到 HDFS 上出错");
} finally {
//省略关闭流代码...
}
}
提前返回
如果函数有提前 return 的逻辑,最好返回前也打印日志,不然排查问题时,发现请求进来了,却什么日志也没有,容易让人一头雾水。
示例:
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 前加上注释,解释其原因。那么这段逻辑就可以添加日志,日志内容就是你的注释内容,也即把注释改写成日志即可。
示例:
if (SPECIAL_APP_CODE.equals(req.getAppCode())) {
log.info("特殊应用,跳过从 plugin.json 读取配置的代码");
}
else {
log.info("从 plugin.json 中读取配置...");
confs = CommonUtils.readFromProperties(localPath, "");
}
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("不需要检查插件");
}
}