Files
cfw-autumn/docs/superpowers/plans/2026-06-17-explicit-env-logger.md

23 KiB
Raw Blame History

Logger Env 显式传递实现计划

给 agent 执行者: 必选子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 按任务逐项实现本计划。步骤统一使用 checkbox- [ ])语法跟踪。

目标: 移除 server/src 中 logger 对空对象 Env 和模块级全局 logger 的依赖,强制 logger 初始化从入口真实 env 显式传递。

方案概览: 先扩展单元扫描测试,让默认 EnvinitLogger({}, {})export const logger 和从本项目 logtailUtils 导入全局 logger 都变成可见失败。再收紧 initLogger / logtailUtils 签名,删除 import-time logger并按已知调用点把 envctx.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 bindings env: Env 显式传给 deleteSchedule,业务环境重命名为 appEnv
  • 修改:server/src/internal/balances/finalizeLock/runFinalizeLock.ts
    职责:调用 cancelLockExpiry 时传入 workerEnv: ctx.bindings 或等价显式 Worker Env。如果当前 RequestContext 没有 bindings按本任务步骤新增窄字段。
  • 修改:server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts
    职责:调用 cancelLockExpiry 时传入显式 Worker Env 和业务 AppEnv,保持 V2 finalize lock 路径与 V1 一致。
  • 修改:server/src/honoUtils/HonoEnv.ts
    职责:如果 finalize lock 调用链暂时没有 Worker Env,为 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.tsserver/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.tssrc/external/aws/eventbridge/eventBridgeUtils.tssrc/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_ENVenv.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;
	}
  • 步骤 2cancelLockExpiry 同时接收 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.tsc.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({ 的调用点必须都包含 workerEnvappEnv

  • 步骤 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";
  • 步骤 2processMessage 显式接收 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-authlogger 导入。

  • 步骤 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,并说明第一个阻塞错误。