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,
reqhas a type ofNextApiRequest, which does not have alogproperty. 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: NextApiRequestobject.logwill 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
withAxiomlike 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
isAxiomAPIRequesttype guard to make surereq.logexists 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.withexists).
// 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,
reqis a TRPC request, not aNextApiResponseorAxiomAPIRequest, but we can access the logger onreq.ctx.logwith 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);