23 KiB
Logger Env 显式传递实现计划
给 agent 执行者: 必选子技能:使用
superpowers:subagent-driven-development(推荐)或superpowers:executing-plans按任务逐项实现本计划。步骤统一使用 checkbox(- [ ])语法跟踪。
目标: 移除 server/src 中 logger 对空对象 Env 和模块级全局 logger 的依赖,强制 logger 初始化从入口真实 env 显式传递。
方案概览: 先扩展单元扫描测试,让默认 Env、initLogger({}, {})、export const logger 和从本项目 logtailUtils 导入全局 logger 都变成可见失败。再收紧 initLogger / logtailUtils 签名,删除 import-time logger,并按已知调用点把 env 或 ctx.logger 逐层传递。最后运行类型检查、单元测试和静态搜索,确认没有回归。
技术栈: TypeScript、Bun test、Hono、pino、Cloudflare Workers bindings、tsgo
文件结构
- 修改:
server/tests/unit/logging/init-logger-env.test.ts
职责:测试initLogger使用显式Env,并扫描server/src禁止默认 env 和全局 logger 模式。 - 修改:
server/src/utils/logging/initLogger.ts
职责:将env参数从Partial<Env>收紧为Env。 - 修改:
server/src/external/logtail/logtailUtils.ts
职责:删除模块级 logger,只保留createLogger(env)、createDualLogger(env)和Logger类型。 - 修改:
server/src/external/aws/eventbridge/eventBridgeUtils.ts
职责:用函数参数env: Env创建局部 logger,不导入全局logger。 - 修改:
server/src/internal/balances/utils/lock/cancelLockExpiry.ts
职责:把 Worker bindingsenv: Env显式传给deleteSchedule,业务环境重命名为appEnv。 - 修改:
server/src/internal/balances/finalizeLock/runFinalizeLock.ts
职责:调用cancelLockExpiry时传入workerEnv: ctx.bindings或等价显式 WorkerEnv。如果当前RequestContext没有 bindings,按本任务步骤新增窄字段。 - 修改:
server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts
职责:调用cancelLockExpiry时传入显式 WorkerEnv和业务AppEnv,保持 V2 finalize lock 路径与 V1 一致。 - 修改:
server/src/honoUtils/HonoEnv.ts
职责:如果 finalize lock 调用链暂时没有 WorkerEnv,为RequestContext增加明确字段bindings: Env,只用于 Worker bindings,不替代业务ctx.env: AppEnv。 - 修改:
server/src/honoMiddlewares/baseMiddleware.ts
职责:如果新增RequestContext.bindings,在 Hono 边界把c.env写入上下文。 - 修改:
server/src/queue/processMessage.ts
职责:processMessage接收env: Env,局部创建 logger,并用env.NODE_ENV替代runtimeEnv.NODE_ENV。 - 修改:
server/src/queue/initWorkers.ts
职责:调用processMessage时传入入口 env。 - 修改:
server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts
职责:用ctx.logger记录错误,删除全局logger导入。 - 检查并按需修改:
server/src/external/aws/eventbridge/initEventBridge.ts、server/src/queue/initSqs.ts
职责:确保 EventBridge/SQS client 初始化没有新增空 env fallback。
任务 1:扩展扫描测试,先暴露 logger env 违规
文件:
-
修改:
server/tests/unit/logging/init-logger-env.test.ts -
步骤 1:替换测试文件内容
将 server/tests/unit/logging/init-logger-env.test.ts 改为:
import { afterEach, describe, expect, test } from "bun:test";
import { readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { initLogger } from "@/utils/logging/initLogger.js";
const makeEnv = (overrides: Partial<Env>): Env => overrides as Env;
const sourceRoot = join(process.cwd(), "src");
const forbiddenSourcePatterns = [
{
name: "Env parameter default",
pattern: /\benv\s*:\s*Env\s*=/,
},
{
name: "optional Env parameter",
pattern: /\benv\?\s*:\s*Env\b/,
},
{
name: "empty Env parameter fallback",
pattern: /\benv\s*=\s*\{\}\s+as\s+Env\b/,
},
{
name: "empty Env nullish fallback",
pattern: /\benv\s*\?\?\s*\(?\s*\{\}\s+as\s+Env\b/,
},
{
name: "initLogger empty Env",
pattern: /\binitLogger\(\s*\{\s*\}\s*,\s*\{\s*\}\s*\)/,
},
{
name: "createLoggerStructure empty Env",
pattern: /\bcreateLoggerStructure\([^;]*,\s*\{\s*\}\s*\)/,
},
{
name: "global logtail logger export",
pattern: /\bexport\s+const\s+logger\b/,
},
{
name: "global logtail logger import",
pattern:
/import\s+\{[^}]*\blogger\b[^}]*\}\s+from\s+["'](?:@\/external\/logtail\/logtailUtils(?:\.js)?|[^"']*external\/logtail\/logtailUtils(?:\.js)?)["']/,
},
{
name: "logger Env typed as Partial",
pattern: /\benv\s*:\s*Partial<Env>\b/,
files: new Set([
"src/utils/logging/initLogger.ts",
"src/external/logtail/logtailUtils.ts",
]),
},
];
const collectTypeScriptFiles = (dir: string): string[] =>
readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name);
if (entry.isDirectory()) return collectTypeScriptFiles(path);
if (!entry.isFile()) return [];
return /\.(ts|tsx)$/.test(entry.name) ? [path] : [];
});
const findForbiddenSourcePatterns = () =>
collectTypeScriptFiles(sourceRoot).flatMap((file) => {
const relativePath = relative(process.cwd(), file);
const contents = readFileSync(file, "utf8");
return forbiddenSourcePatterns
.filter((rule) => !rule.files || rule.files.has(relativePath))
.filter((rule) => rule.pattern.test(contents))
.map((rule) => `${relativePath}: ${rule.name}`);
});
describe("initLogger env configuration", () => {
const originalNodeEnv = process.env.NODE_ENV;
afterEach(() => {
if (originalNodeEnv === undefined) {
delete (process.env as Record<string, string | undefined>).NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}
});
test("uses the passed Env instead of process.env to resolve dev mode", () => {
process.env.NODE_ENV = "production";
const logger = initLogger(
{},
makeEnv({
NODE_ENV: "development",
AXIOM_TOKEN: "",
}),
);
expect(logger.level).toBe("debug");
});
test("does not allow default Env or global logtail logger usage", () => {
expect(findForbiddenSourcePatterns()).toEqual([]);
});
});
- 步骤 2:运行测试,确认它先失败
运行:
cd server && bun test --isolate tests/unit/logging/init-logger-env.test.ts
预期:FAIL。失败列表至少包含:
src/external/logtail/logtailUtils.ts: initLogger empty Env
src/external/logtail/logtailUtils.ts: createLoggerStructure empty Env
src/external/logtail/logtailUtils.ts: global logtail logger export
src/external/logtail/logtailUtils.ts: logger Env typed as Partial
如果同时列出 src/queue/processMessage.ts、src/external/aws/eventbridge/eventBridgeUtils.ts 或 src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts 的全局 logger import,这是符合预期的。
- 步骤 3:提交失败测试
git add server/tests/unit/logging/init-logger-env.test.ts
git commit -m "test: forbid implicit logger env"
任务 2:收紧 logger factory,删除模块级 logger
文件:
-
修改:
server/src/utils/logging/initLogger.ts -
修改:
server/src/external/logtail/logtailUtils.ts -
测试:
server/tests/unit/logging/init-logger-env.test.ts -
步骤 1:收紧
initLogger签名
在 server/src/utils/logging/initLogger.ts 中,把函数签名从:
export const initLogger = (
options: InitLoggerOptions,
env: Partial<Env>,
) => {
改为:
export const initLogger = (options: InitLoggerOptions, env: Env) => {
函数体不需要改变,继续读取 env.NODE_ENV 和 env.AXIOM_TOKEN。
- 步骤 2:删除
logtailUtils.ts的 import-time logger
在 server/src/external/logtail/logtailUtils.ts 中删除这两段:
const pinoLogger = initLogger({}, {});
export const logger = createLoggerStructure(pinoLogger, {});
- 步骤 3:收紧
createLoggerStructure的 env 类型
在 server/src/external/logtail/logtailUtils.ts 中,把:
const createLoggerStructure = (
basePinoLogger: pino.Logger,
env: Partial<Env>,
): Logger => ({
改为:
const createLoggerStructure = (
basePinoLogger: pino.Logger,
env: Env,
): Logger => ({
- 步骤 4:确认 factory 出口仍然显式接收 Env
确认 server/src/external/logtail/logtailUtils.ts 中保留以下代码:
export const createLogger = (env: Env) =>
createLoggerStructure(initLogger({}, env), env);
/**
* Lazy dual-output logger (stdout JSON + axiom). Used only by long-running
* trigger.dev tasks so their lines surface in both the trigger run UI and
* our axiom store. Default `logger` / `createLogger` are unaffected.
*/
export const createDualLogger = (env: Env) =>
createLoggerStructure(initLogger({ mode: "dual" }, env), env);
如果注释中的 Default logger 文案不再准确,把注释中的这一句改为:
* `createLogger` is unaffected.
- 步骤 5:运行测试,确认剩余失败只来自调用点
运行:
cd server && bun test --isolate tests/unit/logging/init-logger-env.test.ts
预期:仍 FAIL,但不再包含:
src/external/logtail/logtailUtils.ts: initLogger empty Env
src/external/logtail/logtailUtils.ts: createLoggerStructure empty Env
src/external/logtail/logtailUtils.ts: global logtail logger export
src/external/logtail/logtailUtils.ts: logger Env typed as Partial
如果仍失败,应只剩其他文件从 logtailUtils 导入全局 logger。
- 步骤 6:提交 logger factory 改动
git add server/src/utils/logging/initLogger.ts server/src/external/logtail/logtailUtils.ts
git commit -m "refactor: require env for logger factories"
任务 3:迁移 EventBridge logger,并补齐取消 schedule 的 Worker Env
文件:
-
修改:
server/src/external/aws/eventbridge/eventBridgeUtils.ts -
修改:
server/src/internal/balances/utils/lock/cancelLockExpiry.ts -
修改:
server/src/internal/balances/finalizeLock/runFinalizeLock.ts -
修改:
server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts -
按需修改:
server/src/honoUtils/HonoEnv.ts -
按需修改:
server/src/honoMiddlewares/baseMiddleware.ts -
测试:
server/tests/unit/logging/init-logger-env.test.ts -
步骤 1:把 EventBridge 全局 logger 改成 factory
在 server/src/external/aws/eventbridge/eventBridgeUtils.ts 中,把导入:
import { logger } from "@/external/logtail/logtailUtils.js";
改为:
import { createLogger } from "@/external/logtail/logtailUtils.js";
在 createSchedule 函数体开始处加入:
const logger = createLogger(env);
完整目标片段:
export const createSchedule = async ({
env,
scheduleName,
scheduleAt,
sqsMessageBody,
messageGroupId,
}: {
env: Env;
scheduleName: string;
scheduleAt: Date;
sqsMessageBody: string;
messageGroupId: string;
}) => {
const logger = createLogger(env);
if (isLocalQueue(env)) {
logger.debug(
"[EventBridge] createSchedule skipped (local SQS queue — no EventBridge in dev)",
);
return;
}
在 deleteSchedule 函数体开始处加入:
const logger = createLogger(env);
完整目标片段:
export const deleteSchedule = async ({
env,
scheduleName,
}: {
env: Env;
scheduleName: string;
}) => {
const logger = createLogger(env);
if (isLocalQueue(env)) {
logger.debug(
"[EventBridge] deleteSchedule skipped (local SQS queue — no EventBridge in dev)",
);
return;
}
- 步骤 2:让
cancelLockExpiry同时接收 Worker Env 和业务 AppEnv
把 server/src/internal/balances/utils/lock/cancelLockExpiry.ts 改为:
import { deleteSchedule } from "@/external/aws/eventbridge/eventBridgeUtils.js";
import { buildLockScheduleName } from "./buildLockScheduleName.js";
/** Cancels the EventBridge expiry schedule for a lock receipt. Safe to call even if no schedule exists. */
export const cancelLockExpiry = async ({
workerEnv,
orgId,
appEnv,
hashedKey,
}: {
workerEnv: Env;
orgId: string;
appEnv: string;
hashedKey: string;
}) => {
const scheduleName = buildLockScheduleName({
orgId,
env: appEnv,
hashedKey,
});
await deleteSchedule({ env: workerEnv, scheduleName });
};
- 步骤 3:如果
RequestContext还没有 Worker bindings,新增窄字段
检查 server/src/honoUtils/HonoEnv.ts。如果 RequestContext 中没有 Worker bindings 字段,在 logger: Logger; 后加入:
bindings: Env;
目标片段:
db: DrizzleCli;
dbGeneral: DrizzleCli;
logger: Logger;
bindings: Env;
/** V2 Redis instance for this request. Populated by every ctx-building
然后在 server/src/honoMiddlewares/baseMiddleware.ts 的 c.set("ctx", { ... }) 对象里,紧跟 logger, 加入:
bindings: c.env,
目标片段:
db,
dbGeneral: db,
logger,
bindings: c.env,
redisV2,
如果已有等价字段,例如 workerEnv: Env,不要新增第二个字段;后续步骤统一使用已有字段名。
- 步骤 4:更新 finalize lock 调用点
在 server/src/internal/balances/finalizeLock/runFinalizeLock.ts 中,把:
await cancelLockExpiry({
orgId: ctx.org.id,
env: ctx.env,
hashedKey: Bun.hash(params.lock_id).toString(),
});
改为:
await cancelLockExpiry({
workerEnv: ctx.bindings,
orgId: ctx.org.id,
appEnv: ctx.env,
hashedKey: Bun.hash(params.lock_id).toString(),
});
在 server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts 中做同样替换。
- 步骤 5:运行局部静态检查
运行:
rg -n "import \\{[^}]*logger[^}]*\\} from [\"']@/external/logtail/logtailUtils|deleteSchedule\\(\\{ scheduleName|cancelLockExpiry\\(\\{" server/src/external/aws/eventbridge server/src/internal/balances
预期:不再有 logger 从本项目 logtailUtils 导入;不再有 deleteSchedule({ scheduleName })。cancelLockExpiry({ 的调用点必须都包含 workerEnv 和 appEnv。
- 步骤 6:运行 logger 扫描测试
运行:
cd server && bun test --isolate tests/unit/logging/init-logger-env.test.ts
预期:如果还有失败,应来自其他全局 logger 调用点,不来自 eventBridgeUtils.ts。
- 步骤 7:提交 EventBridge 改动
git add server/src/external/aws/eventbridge/eventBridgeUtils.ts server/src/internal/balances/utils/lock/cancelLockExpiry.ts server/src/internal/balances/finalizeLock/runFinalizeLock.ts server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts server/src/honoUtils/HonoEnv.ts server/src/honoMiddlewares/baseMiddleware.ts
git commit -m "refactor: pass env into eventbridge logging"
任务 4:迁移 SQS message 处理 logger 和 runtimeEnv 读取
文件:
-
修改:
server/src/queue/processMessage.ts -
修改:
server/src/queue/initWorkers.ts -
测试:
server/tests/unit/logging/init-logger-env.test.ts -
测试:
server/tests/unit/runtime-env-imports.test.ts -
步骤 1:替换
processMessage.ts的导入
在 server/src/queue/processMessage.ts 中删除:
import { runtimeEnv } from "@/utils/envUtils.js";
把:
import { logger } from "@/external/logtail/logtailUtils.js";
改为:
import { createLogger } from "@/external/logtail/logtailUtils.js";
- 步骤 2:让
processMessage显式接收 Worker Env
把函数签名从:
export const processMessage = async ({
message,
db,
}: {
message: Message;
db: DrizzleCli;
}) => {
改为:
export const processMessage = async ({
env,
message,
db,
}: {
env: Env;
message: Message;
db: DrizzleCli;
}) => {
- 步骤 3:用入口 env 创建 worker logger
把:
const workerLogger = addWorkflowToLogs({
logger: logger,
workflowContext: {
id: workflowId,
name: job.name,
payload: job.data,
},
});
改为:
const workerLogger = addWorkflowToLogs({
logger: createLogger(env),
workflowContext: {
id: workflowId,
name: job.name,
payload: job.data,
},
});
- 步骤 4:用显式 env 替代 runtimeEnv
把:
if (runtimeEnv.NODE_ENV === "development") {
改为:
if (env.NODE_ENV === "development") {
- 步骤 5:更新
initWorkers.ts调用点
在 server/src/queue/initWorkers.ts 中,把两个调用点:
await processMessage({ message, db });
和:
fn: () => processMessage({ message, db }),
改为:
await processMessage({ env, message, db });
和:
fn: () => processMessage({ env, message, db }),
这里的 env 必须来自 initWorkers(env: Env) 或等价入口参数;不要从模块级变量读取。
- 步骤 6:运行局部静态检查
运行:
rg -n "runtimeEnv|import \\{[^}]*logger[^}]*\\} from [\"']@/external/logtail/logtailUtils|processMessage\\(\\{ message, db \\}\\)" server/src/queue
预期:无输出。
- 步骤 7:运行相关扫描测试
运行:
cd server && bun test --isolate tests/unit/runtime-env-imports.test.ts tests/unit/logging/init-logger-env.test.ts
预期:如果仍失败,不应再包含 src/queue/processMessage.ts。
- 步骤 8:提交队列改动
git add server/src/queue/processMessage.ts server/src/queue/initWorkers.ts
git commit -m "refactor: pass env into sqs message logging"
任务 5:迁移 cache guard 的全局 logger
文件:
-
修改:
server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts -
测试:
server/tests/unit/logging/init-logger-env.test.ts -
步骤 1:删除全局 logger 导入
在 server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts 中删除:
import { logger } from "@/external/logtail/logtailUtils.js";
- 步骤 2:使用
ctx.logger记录 set 失败
把:
logger.error(`Failed to set test fullCustomer cache guard: ${error}`);
改为:
ctx.logger.error(`Failed to set test fullCustomer cache guard: ${error}`);
- 步骤 3:使用
ctx.logger记录 remove 失败
把:
logger.error(`Failed to remove test fullCustomer cache guard: ${error}`);
改为:
ctx.logger.error(`Failed to remove test fullCustomer cache guard: ${error}`);
- 步骤 4:运行 logger 扫描测试
运行:
cd server && bun test --isolate tests/unit/logging/init-logger-env.test.ts
预期:如果任务 1-5 全部完成,测试 PASS,或者失败列表指向新的全局 logger import 文件。
- 步骤 5:如果扫描发现新的全局 logger import,逐个迁移
运行:
rg -n "import \\{[^}]*logger[^}]*\\} from [\"'](?:@/external/logtail/logtailUtils(?:\\.js)?|[^\"']*external/logtail/logtailUtils(?:\\.js)?)[\"']" server/src
对每个命中文件使用同一规则迁移:
// 如果函数已有 ctx
ctx.logger.error("message");
// 如果函数已有 env: Env
const logger = createLogger(env);
logger.error("message");
迁移后再次运行同一个 rg 命令,预期无输出。不要修改 better-auth 的 logger 导入。
- 步骤 6:提交 cache guard 改动
git add server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts
git commit -m "refactor: use context logger for cache guard"
如果步骤 5 迁移了额外文件,把这些文件加入同一个提交,并在提交信息中使用:
git commit -m "refactor: remove global logtail logger imports"
任务 6:全量静态搜索和类型检查修复
文件:
-
按类型检查输出修改相关
server/src/**文件 -
测试:
server/tests/unit/logging/init-logger-env.test.ts -
测试:
server/tests/unit/runtime-env-imports.test.ts -
步骤 1:运行最终静态搜索
运行:
rg -n "initLogger\\(\\s*\\{\\s*\\}\\s*,\\s*\\{\\s*\\}\\s*\\)|createLoggerStructure\\([^;]*,\\s*\\{\\s*\\}\\s*\\)|export const logger\\b|import \\{[^}]*logger[^}]*\\} from [\"'](?:@/external/logtail/logtailUtils(?:\\.js)?|[^\"']*external/logtail/logtailUtils(?:\\.js)?)[\"']|env\\?\\s*:\\s*Env\\b|env\\s*:\\s*Env\\s*=|env\\s*=\\s*\\{\\}\\s+as\\s+Env" server/src
预期:无输出。
- 步骤 2:运行 runtime env 和 logger 单元扫描测试
运行:
cd server && bun test --isolate tests/unit/runtime-env-imports.test.ts tests/unit/logging/init-logger-env.test.ts
预期:PASS。
- 步骤 3:运行类型检查
运行:
cd server && bun ts
预期:PASS。
如果失败,按错误文件修复。常见修复:
// 缺 env 参数
await processMessage({ env, message, db });
// 业务 AppEnv 和 Worker Env 混用
await cancelLockExpiry({
workerEnv: ctx.bindings,
orgId: ctx.org.id,
appEnv: ctx.env,
hashedKey,
});
// logger singleton 删除后改用上下文 logger
ctx.logger.warn("message");
每次修复后重新运行:
cd server && bun ts
直到类型检查 PASS。
- 步骤 4:提交类型修复
如果步骤 3 修改了文件:
git add server/src server/tests/unit/logging/init-logger-env.test.ts
git commit -m "fix: thread env through logger call sites"
如果步骤 3 没有源码改动,不创建提交。
任务 7:运行单元测试并提交最终验证状态
文件:
-
检查:
server/src/** -
检查:
server/tests/unit/** -
步骤 1:运行完整单元测试
运行:
cd server && bun test:unit
预期:PASS。
如果失败且是本次 logger/env 改动引起,修复后重跑:
cd server && bun test:unit
如果失败来自外部服务、缺少本地环境或已存在不相关失败,记录失败命令和首个错误摘要,不把结果报告为通过。
- 步骤 2:检查工作区只包含本次相关改动
运行:
git status --short
预期:至少本轮提交都已完成;如果仍有未提交改动,只能包含本计划相关文件。不要回滚用户已有改动。
- 步骤 3:如果还有本次相关未提交改动,提交它们
运行:
git add server/src server/tests/unit/logging/init-logger-env.test.ts
git commit -m "test: verify explicit logger env"
如果没有未提交的本次相关改动,不创建提交。
- 步骤 4:汇总验证结果
最终回复中列出:
类型检查:cd server && bun ts -> PASS
单元测试:cd server && bun test:unit -> PASS
静态搜索:无隐式 logger env 或全局 logtail logger
提交:列出本轮提交 hash 和 message
如果任何命令未通过,使用实际结果替换 PASS,并说明第一个阻塞错误。