next-axiom
next-axiom copied to clipboard
Usage with TRPC - how does this look?
Hey, I'm building an application with the T3 stack and wanted to check if there is a better way to use Axiom with TRPC's router in a Next API route. Let me know if you see room for improvement or want more context on this.
- 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 check thewithfunction exists on log).Extra context: This is where the NextAPIRequest/AxiomAPIRequest is transformed into part of TRPC's own
reqobject.
// 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("this is not the request type 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 theAxiomAPIRequest(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);
Hey @adamsullovey, that's really cool, thanks! We'll def try it out and get back to you 💯
@adamsullovey thanks for this! If I wrap [trpc].ts with withAxiom, do I still need to wrap my NextConfig with withAxiom?
Also, where is AxiomLogger used in context.ts? Seems like you are importing it like this import {log as AxiomLogger} from 'next-axiom'; but don't actually use it.
@adamsullovey thanks for this! If I wrap
[trpc].tswithwithAxiom, do I still need to wrap myNextConfigwithwithAxiom?
// withAxiom can be called either with NextConfig, which will add proxy rewrites
// to improve deliverability of Web-Vitals and logs, or with NextApiRequest or
// NextMiddleware which will automatically log exceptions and flush logs.
So if you want to improve deliverability of that info, I guess wrap NextConfig with withAxiom too. withAxiom does different things based on what it is passed to it.
Also, where is
AxiomLoggerused incontext.ts? Seems like you are importing it like thisimport {log as AxiomLogger} from 'next-axiom';but don't actually use it.
Thank-you for spotting it, I will update the code sample.
I went down a bit of a rabbit hole with this so thought i'd share my learnings.
My app was also bootstrapped with create-t3-app and uses trpc for it's api. I recently had an error in my testing where a 500 was being thrown in a trpc endpoint, and I noticed error messages don't flow through to my axiom log stream since trpc technically catches and handles the error for it's own react query retry logic.
I saw this post and followed the steps to integrate next-axiom, and found that it was playing havoc with my SSR and render times. It seemed that wrapping the trpc api handler in withAxiom caused some sort of slowness in - dropping my lighthouse performance score about 30 percent.
It then occurred to me that axiom is already piping all stdout output into the axiom log stream and capturing it, so why don't I just console.error() it 🤦 . So that's what I did and i'm really pleased with it. It also triggered me to go pepper my important endpoints with contextual info logging which has also been incredibly useful for debugging. Can add user context for searchability too if you have server side auth.
Note put your console.error statements in the trpc catchall onError handler so they don't trigger for every retry and you dont need to wrap endpoints in their own try catch.
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { createContext } from '../../../server/trpc/context';
import { appRouter } from '../../../server/trpc/router/_app';
export default createNextApiHandler({
router: appRouter,
createContext,
onError: ({ path, error }) => {
console.error(`❌ tRPC failed on ${path}: ${error}`);
},
});
Nice find! I did not do before-and-after performance testing with Lighthouse.
Hi @reubenjh, does this mean I don't have to wrap my createNextApiHandler() with withAxiom() like this?
export default withAxiom(
createNextApiHandler({
router: appRouter,
...
If so, how do we send error logs to axiom?
Hi @reubenjh, does this mean I don't have to wrap my createNextApiHandler() with withAxiom() like this?
export default withAxiom( createNextApiHandler({ router: appRouter, ... If so, how do we send error logs to axiom?
Hi @jacobhjkim , If you've set up axiom via the vercel integration then all you need is the NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT env var (that the integration adds for you automatically when you add it). I don't even have an axiom npm module, attached is a screenshot of a global search for axiom in my codebase. It's all happening through the vercel integration. Since it's piping all your serverless function logs to your log stream, all you need to do is console.log / console.error etc. You can also set up alerting in axiom too.
The only reason I can see you would need the withAxiom wrapper or next-axiom in general is for FE logging

Hi. Currently building a project using TRPC and Next 13.4. Will this serve as a documentation on how to integrate Axiom, or will there be an official one published along with the current integrations/sdks?
Hey, I am also using axiom with a Next.js 13, page router, tRPC project.
Lately, I noticed some longer logs are truncated because of the Vercel 4KB log limit. Therefore, I want to use this package to make sure all logs are logged in their full length.
@bahlo will there be an official guide on how to use this package with tRPC? tRPC is used by many devs (e.g. t3-stack) and the community and axiom customers would highly benefit from this.
Thanks in advance 🙏
Therefore, I want to use this package to make sure all logs are logged in their full length.
I did find this blog post from @adamsullovey on it which you may find helpful.
I've held off on integrating Axiom hoping for an update here, but obviously still waiting. Will probably play around with it myself and see if I can get it working. An official tRPC guide or example would be greatly appreciated, though.
Hi 👋
We've been playing with a few ideas, both using next-axiom, and the possibility of a new @axiomhq/trpc package.
Would like to share an update: I personally believe that the best way to implement this in a typesafe way is using tRPC's standalone middleware, which is still unstable (in the semver sense of the word). Once that changes we might consider adding this stuff at the library level as something you can import in your app. But for now the implementation is just something we would add to our docs and you would copy/paste into your app.
What I like about this implementation:
- You don't need to change the type of the Request anywhere other than axiom-specific code
- You can stick an object of meta stuff specific to the current procedure (for example a session, or anything else you might want to log) before arriving inside the procedure handler
- The logger is on
ctx.loginstead of onreq
Here are versions of this implementation for Pages/App Router, both started from Create T3 App output. The only difference between the two is the type of the Request. It would also transfer to other tRPC environments, although you'd need to get a logger from somewhere yourself as you won't have the one that's injected into the API handler (most likely option would be @axiomhq/js).
Pages Router: https://github.com/c-ehrlich/next-axiom-trpc-example/commit/e3e3f1eae4c511c339538706d6d8395636a60345 App Router: https://github.com/c-ehrlich/next-axiom-trpc-example/commit/99789b236cd201e9f4b260308a4383039d369123
How do you like this compared to the other implementations that have been posted here? Are there any downsides you see compared to existing solutions? Especially @adamsullovey, but also anyone else who has been using Axiom with tRPC / T3 Stack. Any feedback is appreciated!
I have a monorepo (based on t3-turbo) with a @acme/logger package that uses @axiomhq/js internally (by tRPC in @acme/api and by some other packages) to send logs. I used @axiomhq/js to have a bit more customization to the metadata sent to Axiom and some other simple features that I wanted to add to it.
Let's say that now I would like also to send Vitals to Axiom through <AxiomWebVitals/> that is offered by next-axiom. What would you recommend? @c-ehrlich Use next-axiom everywhere? or import only next-axiom only for the app?
I have a monorepo (based on t3-turbo) with a
@acme/loggerpackage that uses@axiomhq/jsinternally (by tRPC in@acme/apiand by some other packages) to send logs. I used@axiomhq/jsto have a bit more customization to the metadata sent to Axiom and some other simple features that I wanted to add to it.Let's say that now I would like also to send Vitals to Axiom through that is offered by next-axiom. What would you recommend? @c-ehrlich Use next-axiom everywhere? or import only next-axiom only for the app?
It's a tradeoff between configurability and ease of use.
Without knowing more about your app, I'd probably suggest use next-axiom for webvitals, and share the same server-side axiom-js based logger code in all your server side stuff. I still prefer the pattern of creating a new logger for each tRPC procedure, but even that isn't strictly necessary depending on the details of your app.
Here's an example of how the standalone middleware pattern might work with an existing logger.
import { Axiom } from "@axiomhq/js";
import { experimental_standaloneMiddleware } from "@trpc/server";
export type NextAxiomTRPCMiddlewareCtx = {
/**
* Anything you want to stick on all logs that are sent throughout the duration of the current procedure
* This is currently not optional, but can pass an empty object.
*/
axiomTRPCMeta: Record<string, unknown>;
};
// 🚨 use your existing logger instead of this
function createTRPCLogger(meta: Record<string, unknown>) {
const axiom = new Axiom({
token: process.env.AXIOM_TOKEN,
orgId: process.env.AXIOM_ORG_ID,
});
return {
log: (
severity: "debug" | "info" | "warn" | "error",
message: string,
event: Record<string, unknown>
) => {
axiom.ingest("my-dataset", [
{
severity,
message,
meta,
event,
},
]);
},
flush: async () => {
await axiom.flush();
},
};
}
export const nextAxiomTRPCMiddleware = experimental_standaloneMiddleware<{
ctx: NextAxiomTRPCMiddlewareCtx;
}>().create((opts) => {
const logger = createTRPCLogger(opts.ctx.axiomTRPCMeta);
const res = opts.next({
ctx: { log: logger.log },
});
// 🚨 could also use AxiomWithoutBatching instead of flushing here
logger.flush();
return res;
});
Thanks! I also currently create a new logger for each request in the tRPC context. Do you think it would be better to use a standaloneMiddleware or having the logger (your createTRPCLogger for example) in the tRPC Context?
The main purpose of the standalone middleware is to inject the logger into the procedure's context. In a simple app, skipping the middleware pattern and just doing it in createContext is probably fine, but this pattern is a bit more modular, lets you optionally do stuff after the procedure, etc.
I went down a bit of a rabbit hole with this so thought i'd share my learnings.
My app was also bootstrapped with create-t3-app and uses trpc for it's api. I recently had an error in my testing where a 500 was being thrown in a trpc endpoint, and I noticed error messages don't flow through to my axiom log stream since trpc technically catches and handles the error for it's own react query retry logic.
I saw this post and followed the steps to integrate next-axiom, and found that it was playing havoc with my SSR and render times. It seemed that wrapping the trpc api handler in withAxiom caused some sort of slowness in - dropping my lighthouse performance score about 30 percent.
It then occurred to me that axiom is already piping all stdout output into the axiom log stream and capturing it, so why don't I just console.error() it 🤦 . So that's what I did and i'm really pleased with it. It also triggered me to go pepper my important endpoints with contextual info logging which has also been incredibly useful for debugging. Can add user context for searchability too if you have server side auth.
Note put your console.error statements in the trpc catchall onError handler so they don't trigger for every retry and you dont need to wrap endpoints in their own try catch.
import { createNextApiHandler } from '@trpc/server/adapters/next'; import { createContext } from '../../../server/trpc/context'; import { appRouter } from '../../../server/trpc/router/_app'; export default createNextApiHandler({ router: appRouter, createContext, onError: ({ path, error }) => { console.error(`❌ tRPC failed on ${path}: ${error}`); }, });
Hey wondering if 1.3 years later you are still doing this successfully? I assume this is captured by axiom line-by-line as stdout instead of nice structured logs right?
I went down a bit of a rabbit hole with this so thought i'd share my learnings. My app was also bootstrapped with create-t3-app and uses trpc for it's api. I recently had an error in my testing where a 500 was being thrown in a trpc endpoint, and I noticed error messages don't flow through to my axiom log stream since trpc technically catches and handles the error for it's own react query retry logic. I saw this post and followed the steps to integrate next-axiom, and found that it was playing havoc with my SSR and render times. It seemed that wrapping the trpc api handler in withAxiom caused some sort of slowness in - dropping my lighthouse performance score about 30 percent. It then occurred to me that axiom is already piping all stdout output into the axiom log stream and capturing it, so why don't I just console.error() it 🤦 . So that's what I did and i'm really pleased with it. It also triggered me to go pepper my important endpoints with contextual info logging which has also been incredibly useful for debugging. Can add user context for searchability too if you have server side auth. Note put your console.error statements in the trpc catchall onError handler so they don't trigger for every retry and you dont need to wrap endpoints in their own try catch.
import { createNextApiHandler } from '@trpc/server/adapters/next'; import { createContext } from '../../../server/trpc/context'; import { appRouter } from '../../../server/trpc/router/_app'; export default createNextApiHandler({ router: appRouter, createContext, onError: ({ path, error }) => { console.error(`❌ tRPC failed on ${path}: ${error}`); }, });Hey wondering if 1.3 years later you are still doing this successfully? I assume this is captured by axiom line-by-line as stdout instead of nice structured logs right?
Hey yeah it's still working exactly the same and been perfect. Definitely readable enough, screenshots attached with raw view and structured view in axiom
@c-ehrlich Hi, your example is for the pages dir is with the createNextApiHandler but I am currently using:
pages dir + fetchRequestHandler.
Can I just use the withAxiomRouteHandler from the app dir example or what should I do? And what should I do on the backend?
@c-ehrlich Hi, your example is for the pages dir is with the
createNextApiHandlerbut I am currently using:pages dir +
fetchRequestHandler.Can I just use the
withAxiomRouteHandlerfrom the app dir example or what should I do? And what should I do on the backend?
should be the same as this example: https://github.com/c-ehrlich/next-axiom-trpc-example/commit/99789b236cd201e9f4b260308a4383039d369123
except that you have to do a default export check and for the http method instead of exporting an object, see: https://nextjs.org/docs/pages/building-your-application/routing/api-routes#http-methods
Hi, I found that this example uses deprecated functions. I think this has to be migrated to .unstable_concat()
Ok, I just used a normal middleware (because it's not deprecated). Is there some problems with it?
const axiomMiddleware = t.middleware(async ({ ctx, next }) => {
const req = ctx.req;
if (!isAxiomRequest(req)) {
throw new Error(
"`nextAxiomTRPCMiddleware` could not find logger. Did you forget to wrap your route handler in `withAxiom`? See: TODO: link to docs",
);
}
const log = req.log.with({ axiomTRPCMeta: ctx.axiomTRPCMeta });
return next({
ctx: { log },
});
});
We've added an example for the current versions of App Router + tRPC using concat.
Standard middleware works as well, but is a bit less composable. Either is fine :)
thanks @adamsullovey and @FleetAdmiralJakob for the work on this as well.
https://github.com/axiomhq/next-axiom/tree/main/examples/trpc-app-router
closing for now, but please ping me if something still isn't working.