2024 注意:即使之前能正常工作,现在日志记录器的绑定不再有效 - 我们还不太清楚原因
🐤 smol logger
一个用于提示/LLM应用工程的最小可行日志记录器。
使用你的IDE作为日志UI - 一个快速、简单、可扩展、零依赖的Node.js日志工具。
- 在应用开发中:一个快速的日志工具,使用文件系统作为日志UI。
- 在提示工程中:包装和转换对OpenAI等的异步调用,以清晰地捕获提示与响应。
- CLI可将日志导出为
.tsv
文件以导入电子表格。电子表格就是你进行提示工程所需的全部工具!
- CLI可将日志导出为
- 在生产环境中:易于扩展以将日志发送到日志存储如Logflare
简短视频演示:https://www.loom.com/share/ae818b7d058343c1ad6caf8deee2e430
特性
- 默认情况下,同时记录到终端和本地json文件,便于导航/版本控制
- 自动记录经过的时间、文件路径、行号、日志调用顺序
- 通过删除
.logs
文件夹清除日志(可自定义) - CLI可将json日志编译为
.tsv
文件以导入电子表格(如Google Sheets、Excel、Quadratic)
- 包装和转换异步调用以捕获提示与响应对
- 可扩展
- 将日志持久化到远程存储,如LogFlare
- 自定义从命名/缩进到终端日志颜色的一切
- 零依赖,核心代码不到100行。比Winston和Bunyan更快(参见基准测试)
- 使用Typescript,非常流行
- MIT开源:https://github.com/smol-ai/logger -(待完成)由Codium AI进行测试
非目标:
- 无日志级别。太复杂 - 只需添加描述性的日志名称,要么发送所有日志,要么一条不发
- 暂不考虑在浏览器中运行。如果想讨论,请开一个issue!不确定将文件存储替换为其他方式会有多复杂。
使用方法
安装:
npm install @smol-ai/logger
使用:
import { SmolLogger } from '@smol-ai/logger';
const logger = new SmolLogger({ logToConsole: true, logToStore: true }) // 易于关闭
const log = logger.log // 可选的便捷别名,减少冗长
log('日志名称(必填)', payload) // 基本用法
默认情况下,所有日志都会发送到控制台和文件系统,因此你可以在IDE中轻松导航:
日志看起来是这样的!
log
函数是一个单一参数的"恒等函数" - 返回payload,因此你可以就地修改。
mySingleArityFunction({foo, bar, baz}) // 哦不,我需要记录bar和baz
mySingleArityFunction({foo, ...log({bar, baz})) // 搞定!
mySingleArityFunction(log({foo, bar, baz})) // 为什么不全部记录呢
myBadMultiArityFunction(foo, bar, baz) // 哦不,我需要记录bar
myBadMultiArityFunction(foo, log(bar), baz) // 搞定!
myBadMultiArityFunction(...log({foo, bar, baz}).values()) // 为什么不全部记录呢
我们默认使用单一参数以鼓励JS生态系统中的这种做法。
清除日志
要清除你的日志 - 删除.logs
文件夹!就这么简单!下次运行日志时它会重新生成。
生产环境:远程存储
对于生产环境的日志记录,可以覆盖Smol Logger的存储目标。我们喜欢Logflare!
// 可选:保留默认的本地文件存储以便重用
const localStore = logger.store
// 用你自己的远程文件存储覆盖默认的本地文件存储
// { logName: string, loggedLine: string | null, payload: any, secondsSinceStart: number, secondsSinceLastLog: number }
logger.store = ({ logName, loggedLine, payload, secondsSinceStart, secondsSinceLastLog }) => {
fetch("https://api.logflare.app/logs/json?source=YOUR_SOURCE_ID", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
"X-API-KEY": "YOUR_API_KEY_HERE"
},
body: JSON.stringify({ message: logName, metadata: {loggedLine, payload, secondsSinceStart, secondsSinceLastLog }})
})
// 可选:同时记录到本地文件存储
localStore({ logName, loggedLine, payload, secondsSinceStart, secondsSinceLastLog })
}
如果你预期会有大量日志,应该对它们进行批处理:
// 这是目前未经测试的示例代码,如果你运行并尝试过,请发送PR
const logMessages = []
function throttle(func, delay = 1000) {
let timeout = null;
return function(...args) {
const { logName, loggedLine, payload, secondsSinceStart, secondsSinceLastLog } = args;
logMessages.push(({ message: logName, metadata: {loggedLine, payload, secondsSinceStart, secondsSinceLastLog }}));
if (!timeout) {
timeout = setTimeout(() => {
func.call(this, ...args);
timeout = null;
}, delay);
}
};
}
const sendToLogFlare = ({ logName, loggedLine, payload, secondsSinceStart, secondsSinceLastLog }) => { fetch("https://api.logflare.app/logs/json?source=YOUR_SOURCE_ID", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8", "X-API-KEY": "YOUR_API_KEY" }, body: JSON.stringify({"batch": logMessages}) }) .then(() => logMessages = []) }
log.store = throttle(sendToLogFlare, 1000)
<details>
<summary>
<strong>异步/阻塞日志记录</strong>
</summary>
在 smol logger 中,日志记录默认是同步的。
请注意,在上面的例子中,我们在同步调用内部触发了一个异步fetch。如果你的应用程序崩溃,由于它是异步运行的,有一个很小的可能性日志可能无法完成发送。如果你需要为异步调用进行阻塞,你可以使用 `asyncLog` 方法和一个异步存储:
```js
// 可选:存储本地文件存储以便重用
const oldStore = logger.store
// 用你自己的远程文件存储覆盖默认的本地文件存储
// { logName: string, loggedLine: string | null, payload: any, secondsSinceStart: number, secondsSinceLastLog: number }
logger.store = async ({ logName, loggedLine, payload, secondsSinceStart, secondsSinceLastLog }) => {
const res = await fetch("https://api.logflare.app/logs/json?source=YOUR_SOURCE_ID", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
"X-API-KEY": "YOUR_API_KEY_HERE"
},
body: JSON.stringify({ message: logName, metadata: {loggedLine, payload, secondsSinceStart, secondsSinceLastLog }})
}).then(res => res.json())
// 只是演示你可以在这个存储中使用await
}
// 现在你可以在异步上下文中阻塞执行
await logger.asyncLog('我的消息在这里', { foo: 'bar' })
注意:这个功能是新的且未经测试,请尝试并提供修复/反馈
LLM应用:拦截输入与输出
这会同时记录你想要监控的异步函数的输入和输出。主要用于提示工程,在这种情况下你非常关心输入与输出对在同一日志文件中的可见性。
import OpenAI from 'openai'; // 这是用于openai v4包的!v3说明在下面
import {SmolLogger} from '@smol-ai/logger';
const openai = new OpenAI({
apiKey: 'my api key', // 默认为 process.env["OPENAI_API_KEY"]
});
const logger = new SmolLogger({logToConsole: true, logToStore: true}); // 两个参数都是可选的,只是为你列出默认值以便轻松关闭
const wrapped = logger.wrap(
openai.chat.completions.create.bind(openai) // 绑定很重要,因为OpenAI内部是如何检索其配置的
async function main() {
const completion = await wrapped({
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "你是一个有帮助的助手",
},
{
role: "user",
content: "选择一个著名的流行歌手,并给我他们的3首歌",
},
],
});
console.log(completion.choices);
}
main();
Openai SDK V3说明
很快就会过时,所以我们把它隐藏在这里
import { Configuration, OpenAIApi } from 'openai';
const openai = new OpenAIApi(new Configuration({ apiKey: process.env.OPENAI_API_KEY }));
const wrapped = logger.wrap(openai.createChatCompletion.bind(openai)) // 绑定很重要,因为OpenAI内部是如何检索其配置的
const response = await wrapped({
model: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
messages: [/* 等等 */ ],
});
有时输出可能非常冗长(就像OpenAI chatCompletion的情况)。所以我们还允许你暴露一个简单的"日志转换器",这是一个任意函数,可以将拦截的输出修改为你喜欢的格式:
// 编辑上面的代码以添加logTransformer
const wrapped = logger.wrap(
openai.createChatCompletion.bind(openai), // 绑定很重要,因为OpenAI内部是如何检索其配置的
{
wrapLogName: 'chatGPT APIcall', // 可选 - 自定义显示在日志上的名称。默认为 "wrap(fn.name)"
logTransformer: (args, result) => ({ // 可以是异步的
// ...result, // 可选 - 如果你想要完整的原始结果本身
prompt: args[0].messages,
response: result.choices[0].message, // 这是v4 api;v3是 result.data.choices[0].message
})
}
)
const response = await wrapped({
model: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
messages: [/* 等等 */ ],
});
// 现在记录的响应只包含我们感兴趣的特定字段,见下面的截图
log2tsv
CLI
单个JSON日志文件对于调试单次运行很有用。这是smol-logger的良好默认设置。
对于提示工程,你还需要轻松比较不同运行之间的提示与输出,并对它们进行评分/编写评估。对于这一点,没有比电子表格更灵活或更强大的界面了。因此,我们帮助你将日志导出为电子表格。
你可以运行 log2tsv
CLI,它会在你的 ./logs
文件夹中输出一个 logs.tsv
文件(我们会接受PR来自定义这个)。你可以在Google Sheets/Excel/Quadratic等中导入这个 .tsv
文件,以进行进一步的提示工程。
./node_modules/.bin/log2tsv # 在安装了 @smol-ai/logger 的目录中
你也可以将其放入npm脚本中,它就会运行:
// package.json
{
"scripts": {
"tsv": "log2tsv" // 然后运行 npm run tsv
}
}
请注意,标题是从第一条日志中提取的 - 很可能不会完全匹配所有日志的主体内容,特别是当你有不规则形状的日志时。我们相信你能够自行在电子表格中重新命名它们,如果它们足够重要的话。
如果你需要,我们会接受一个PR来使其可以通过编程方式运行(而不仅仅是CLI)。
自定义其他内容
自定义的一般规则是覆盖类中暴露的任何变量和方法:
logger.logDirectory = '.smol-logs' // 更改默认文件存储目录
logger.logToConsole = false // 关闭终端日志记录
logger.logToStore = false // 关闭存储日志记录
// 更多使用想法
logger.logToStore = Math.random() < 0.05 // 只记录5%的流量
logger.logToStore = user.isVIPCustomer() // 基于功能标志记录日志
logger.logName = (name: string) => `我的自定义日志名称:${name}` // 更改日志命名!
logger.LOGCOLOR = (logName: string) => "\x1b[35m" + logName + "\x1b[0m"; // 将日志颜色设置为洋红色而不是黄色
其他可以尝试的日志颜色:
\x1b[31m: 红色
\x1b[32m: 绿色
\x1b[33m: 黄色
\x1b[34m: 蓝色
\x1b[35m: 洋红色
\x1b[36m: 青色
\x1b[37m: 白色
发挥创意!例如,你可以在单个归约函数中堆叠logName函数...
// 对于高级应用,你可以在堆栈中添加日志名称
const logStack = [logger.logName] // 将原始logName函数存储在堆栈底部
logger.logName = (name) => logStack.reduce((prev, fn) => fn(prev), name)
let temp = 0
do {
logStack.unshift(name => ' ' + name)
// 这里记录的所有内容都缩进一级
log('logname1 here ' + temp, temp)
let temp2 = 0
do {
logStack.unshift(name => ' ' + name)
// 这里记录的所有内容都缩进两级
log('logname2 here ' + temp2, temp2)
logStack.shift()
} while (temp2++ < 5)
logStack.shift()
} while (temp++ < 5)
将来我们可能会为嵌套日志记录提供更正式的API: (这在代码中但未经测试)
const sublog = logger.newSubLog('prefix') // Logger的新实例,带有缩进日志设置 let temp = 0 do { sublog('logname1 here ' + temp, temp) // 这里记录的所有内容都缩进一级 const sublog2 = sublog.sublog() let temp2 = 0 do { sublog2('logname2 here ' + temp2, temp2) // 这里记录的所有内容都缩进两级 } while (temp2++ < 5) } while (temp++ < 5)
如果感兴趣,请开启一个issue,这里有很多设计空间
基准测试
我们并不是真的追求最高速度,因为我们更关心开发中的开发者体验,但请参见 /benchmark
:
$ cd benchmark && npm install
$ node bench.js > bench.txt
$ grep "^bench" bench.txt
benchWinston*100000: 1.690s
benchBunyan*100000: 1.820s
benchPino*100000: 892.089ms
benchSmol*100000: 1.290s
benchWinston*100000: 1.620s
benchBunyan*100000: 1.712s
benchPino*100000: 911.538ms
benchSmol*100000: 1.284s
这希望能证明我们比Winston/Bunyan更快,并且与Pino相比具有竞争力,同时提供更好的功能集。
贡献者注意事项
这个仓库是使用 https://github.com/alexjoverm/typescript-library-starter 初始化的(必须使用 https://github.com/alexjoverm/typescript-library-starter/issues/333 进行修改)
发布
按照控制台说明安装semantic release并运行它(对"Do you want a .travis.yml
file with semantic-release setup?"问题回答NO)。
注意:确保你已经在package.json
文件中设置了repository.url
npm install -g semantic-release-cli
semantic-release-cli setup
# 重要!!对"Do you want a `.travis.yml` file with semantic-release setup?"问题回答NO。它已经为你准备好了 :P
从现在开始,你需要使用npm run commit
,这是创建常规提交的便捷方式。
自动发布得益于semantic release,它会自动在github和npm上发布你的代码,并自动生成更新日志。
swyx注 - 无法使发布工作正常,一直失败。
目前:
npm run build
npm version patch
npm publish --access=public