You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Next.js + tRPC + Auth.js的T3应用中为Azure Application Insights配置用户ID

在Next.js + tRPC + Auth.js的T3应用中为Azure Application Insights配置用户ID

我之前在T3栈里也遇到过几乎一模一样的问题——想把Auth.js的用户ID关联到Azure Application Insights的请求追踪里,折腾了好一会儿才搞清楚Context传播和Span生命周期的坑。下面是我验证过的可行方案,分步骤给你讲清楚:

为什么你之前的方法没生效?

核心问题是OpenTelemetry的Context和tRPC/Next.js的请求上下文没有正确绑定:

  • 你在tRPC中间件或layout里设置的Context值,没有被传播到Span的生命周期中(Span创建时的parentContext没有包含你的KEY_USER_ID)
  • SpanProcessor的onEnd阶段,当前活跃的Context可能已经切换到了其他请求,导致拿不到正确的用户ID

解决方案:从两个维度入手

我们要确保请求早期获取用户ID,并把它注入到当前请求的所有Span中,分两种场景处理:tRPC接口请求和Next.js Server Component页面请求。

1. 先配置自定义的SpanProcessor(关键)

先写一个能从Span的父Context中读取用户ID并注入属性的SpanProcessor,这次我们用onStart方法(比onEnd可靠,因为它在Span创建时就执行,能拿到创建Span时的上下文):

// src/utils/otelSpanProcessor.ts
import { SpanProcessor, ReadableSpan } from "@opentelemetry/sdk-trace-base";
import { context, ContextKey } from "@opentelemetry/api";
import { SemanticAttributes } from "@opentelemetry/semantic-conventions";

// 创建全局的Context Key,用于存储用户ID
export const USER_ID_CONTEXT_KEY: ContextKey<string> = context.createContextKey(
  "AUTH_USER_ID"
);

export class UserIdEnrichingProcessor implements SpanProcessor {
  // 当Span创建时执行,这里能拿到创建Span的父上下文
  onStart(span: ReadableSpan, parentContext: context.Context) {
    const userId = parentContext.getValue(USER_ID_CONTEXT_KEY);
    if (userId) {
      // 用OpenTelemetry标准的用户ID属性键
      span.setAttribute(SemanticAttributes.ENDUSER_ID, userId);
    }
  }

  onEnd(_span: ReadableSpan) {
    // 这里可以留空,或者做其他收尾操作
  }

  // 必须实现的SpanProcessor接口方法
  forceFlush(): Promise<void> {
    return Promise.resolve();
  }

  shutdown(): Promise<void> {
    return Promise.resolve();
  }
}

然后在instrumentation.ts里把这个处理器加进去:

// instrumentation.ts
import { useAzureMonitor } from "@azure/monitor-opentelemetry";
import { UserIdEnrichingProcessor } from "~/utils/otelSpanProcessor";

export function register() {
  useAzureMonitor({
    azureMonitorExporterOptions: {
      connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
    },
    spanProcessors: [new UserIdEnrichingProcessor()],
  });
}

2. 在请求早期注入用户ID到OTel Context

接下来要在请求处理的最早期拿到用户ID,并存入OTel的Context中,这样后续创建的所有Span都能通过onStart方法获取到用户ID。

场景1:tRPC接口请求

T3栈的tRPC Context已经集成了Auth.js的Session,我们直接在全局中间件里注入用户ID:

// src/server/api/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import { getServerAuthSession } from "~/server/auth";
import { context } from "@opentelemetry/api";
import { USER_ID_CONTEXT_KEY } from "~/utils/otelSpanProcessor";

const createInnerTRPCContext = async (opts: {
  session: Awaited<ReturnType<typeof getServerAuthSession>>;
  req: Request;
  res: Response;
}) => {
  // 拿到用户ID后,更新OTel的当前上下文
  if (opts.session?.user?.id) {
    const currentCtx = context.active();
    const newCtx = currentCtx.setValue(USER_ID_CONTEXT_KEY, opts.session.user.id);
    // 把新的上下文设为活跃上下文,确保后续Span创建用这个上下文
    context.with(newCtx, () => {});
  }

  return {
    session: opts.session,
    req: opts.req,
    res: opts.res,
  };
};

export const createTRPCContext = async (opts: CreateNextContextOptions) => {
  const session = await getServerAuthSession({ req: opts.req, res: opts.res });
  return createInnerTRPCContext({ session, req: opts.req, res: opts.res });
};

const t = initTRPC.context<typeof createTRPCContext>().create();

// 全局中间件,确保已认证的请求都注入用户ID
export const middleware = t.middleware;
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
  if (!ctx.session || !ctx.session.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }

  // 双重保险,再次确认用户ID注入
  if (ctx.session.user.id) {
    const currentCtx = context.active();
    const newCtx = currentCtx.setValue(USER_ID_CONTEXT_KEY, ctx.session.user.id);
    context.with(newCtx, () => {});
  }

  return next({
    ctx: {
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

export const publicProcedure = t.procedure;
export const router = t.router;

场景2:Next.js Server Component页面请求

对于App Router的Server Component(比如layout.tsx或page.tsx),我们在服务器端获取Session后注入用户ID:

// app/layout.tsx
import { getServerAuthSession } from "~/server/auth";
import { context } from "@opentelemetry/api";
import { USER_ID_CONTEXT_KEY } from "~/utils/otelSpanProcessor";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getServerAuthSession();

  if (session?.user?.id) {
    const currentCtx = context.active();
    const newCtx = currentCtx.setValue(USER_ID_CONTEXT_KEY, session.user.id);
    // 确保后续页面组件渲染时用这个上下文
    context.with(newCtx, () => {});
  }

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

3. 更直接的备选方案:直接修改当前Span属性

如果上面的Context传播还是有问题,你可以跳过SpanProcessor,直接在拿到用户ID后修改当前活跃Span的属性:

// 在tRPC Context或Server Component里
import { trace, context } from "@opentelemetry/api";
import { SemanticAttributes } from "@opentelemetry/semantic-conventions";

if (session?.user?.id) {
  const activeSpan = trace.getSpan(context.active());
  if (activeSpan) {
    activeSpan.setAttribute(SemanticAttributes.ENDUSER_ID, session.user.id);
  }
}

这种方式更简单,适合快速验证,缺点是需要在每个需要的地方重复代码,而SpanProcessor是全局生效的。

验证是否生效

部署后,打开Azure Portal的Application Insights,进入Logs,执行以下查询:

traces
| where customDimensions contains "ENDUSER_ID"
| project timestamp, message, customDimensions.ENDUSER_ID

如果能看到用户ID,就说明配置成功了!

常见坑点

  • Runtime兼容:确保你的OTEL配置是针对Node.js Runtime的,Next.js Edge Runtime的OTEL配置逻辑不同,T3栈默认用Node.js Runtime。
  • Session获取时机:必须在服务器端获取Session,客户端无法拿到安全的用户ID,所以只能在Server Components、tRPC Procedures、API路由里操作。
  • Context传播:用context.with(newCtx, () => {})确保后续操作在新的上下文环境中执行,否则Span创建时可能还是用旧的上下文。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 10:38:10