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.
-
In a Typescript NextJS app,
req
has a type ofNextApiRequest
, which does not have alog
property. In a T3 App, Typesafety Isn’ Optional. -
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.
- 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,
})
);
- Inside context.ts, add an
isAxiomAPIRequest
type guard to make surereq.log
exists and confirm the request is anAxiomAPIRequest
, notNextApiRequest
. This will get us type safety both at compile time and runtime. Feel free to make this check more exhaustive (eg also chechlog.with
exists).
// 1 new import
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,
};
};
- Inside your TRPC queries and mutations, use and re-assign the logger as needed. Here,
req
is a TRPC request, not aNextApiResponse
orAxiomAPIRequest
, but we can access the logger onreq.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 }
)
}
})
- Inside your main router in
server/router/index.ts
, add middleware to copy the reference to the newest logger back on to theNexApiRequest
(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);