August 7, 2022

Logging with Axiom on NextJS API Routes

I had to do a bit of extra work to combine a create-t3-app-generated application with Axiom’s backend logging feature, but I got it work. Here’s my approach.

The Challenge

Part Axiom’s NextJS Integration involves wrapping your API route handlers with withAxiom. This will make Axiom’s log functionality available on req.log inside your API routes.

// serverless function
async function handler(req, res) {
  req.log.info("hello from function")
  res.status(200).text('hi')
}

export default withAxiom(handler)

This is super convenient if you are using JS and no extra libraries, but poses to challenges for an app based on the T3 Stack.

  1. In a Typescript NextJS app, req has a type of NextApiRequest, which does not have a log property. In a T3 App, Typesafety Isn’ Optional.

  2. A T3 app ues TRPC to manage paths under an API routes, and TRPC has its own abstracton over the req: NextApiRequest object. log will not automatically be easily available on that abstraction, and the types won’t acknowledge it exists

A Solution

Here’s my second attempt at a solution.

  1. Wrap the handler function in [trpc].ts with withAxiom like the docs say to.
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { appRouter } from "../../../server/router";
import { createContext } from "../../../server/router/context";
import { withAxiom } from 'next-axiom'

// export API handler
export default withAxiom(
  createNextApiHandler({
    router: appRouter,
    createContext,
  })
);
  1. Inside context.ts, add an isAxiomAPIRequest type guard to make sure req.log exists and confirm the request is an AxiomAPIRequest, not NextApiRequest. This will get us type safety both at compile time and runtime. Feel free to make this check more exhaustive (eg also chech log.with exists).
// a bunch of other imports, and then these 2
import {log as AxiomLogger} from 'next-axiom';
import { AxiomAPIRequest } from "next-axiom/dist/withAxiom";

const isAxiomAPIRequest = (
  req?: NextApiRequest | AxiomAPIRequest
): req is AxiomAPIRequest => {
  return Boolean((req as AxiomAPIRequest)?.log);
};

export const createContext = async (
  opts?: trpcNext.CreateNextContextOptions
) => {
  const req = opts?.req;
  const res = opts?.res;

  if (!isAxiomAPIRequest(req)) {
    throw new Error("req is not the AxiomAPIRequest I expected");
  }

  const session =
    req && res && (await getServerSession(req, res, nextAuthOptions));

  const log = session ? req.log.with({ userId: session.user.id }) : req.log;

  return {
    req,
    res,
    session,
    prisma,
    log,
  };
};
  1. Inside your TRPC queries and mutations, use and re-assign the logger as needed. Here, req is a TRPC request, not a NextApiResponse or AxiomAPIRequest, but we can access the logger on req.ctx.log with the expected type information.
.mutation("create-signed-url", {
  async resolve(req) {

    // add some data to all following log messages by creating a new logger using `with`
    req.ctx.log = req.ctx.log.with({ data })

    // or log a message
    req.ctx.log.info(
      'Here\'s some info', { mediaInfo }
    )
  }
})
  1. Inside your main router in server/router/index.ts, add middleware to copy the reference to the newest logger back on to the NexApiRequest (ctx.req) so that Axiom will flush the correct instance of the logger when the request is about to be finished.
export const appRouter = createRouter()
  .middleware(async ({ ctx, next }) => {
    const result = await next();
    (ctx.req as AxiomAPIRequest).log = ctx.log;
    return result
  })
  .merge("example.", exampleRouter)
  .merge("auth.", authRouter);