next-axiom icon indicating copy to clipboard operation
next-axiom copied to clipboard

Usage with TRPC - how does this look?

Open adamsullovey opened this issue 3 years ago • 17 comments

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.

  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 anisAxiomAPIRequest 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 check the with function exists on log).

    Extra context: This is where the NextAPIRequest/AxiomAPIRequest is transformed into part of TRPC's own req object.

// 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,
  };
};
  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 AxiomAPIRequest (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);

adamsullovey avatar Sep 11 '22 01:09 adamsullovey

Hey @adamsullovey, that's really cool, thanks! We'll def try it out and get back to you 💯

bahlo avatar Sep 12 '22 15:09 bahlo

@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.

jacobhjkim avatar Nov 11 '22 14:11 jacobhjkim

@adamsullovey thanks for this! If I wrap [trpc].ts with withAxiom, do I still need to wrap my NextConfig with withAxiom?

Code comments say

// 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 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.

Thank-you for spotting it, I will update the code sample.

adamsullovey avatar Nov 12 '22 17:11 adamsullovey

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}`);
  },
});

reubenjh avatar Jan 11 '23 02:01 reubenjh

Nice find! I did not do before-and-after performance testing with Lighthouse.

adamsullovey avatar Jan 16 '23 03:01 adamsullovey

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?

jacobhjkim avatar Jan 16 '23 04:01 jacobhjkim

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

Screenshot 2023-01-18 at 12 48 33 PM

Screenshot 2023-01-18 at 12 50 25 PM

reubenjh avatar Jan 17 '23 23:01 reubenjh

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?

jaaneh avatar May 10 '23 09:05 jaaneh

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 🙏

jhb-dev avatar Jun 26 '23 07:06 jhb-dev

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.

jaaneh avatar Jun 26 '23 14:06 jaaneh

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.log instead of on req

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!

c-ehrlich avatar Nov 16 '23 15:11 c-ehrlich

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?

413n avatar Nov 17 '23 13:11 413n

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 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;
});

c-ehrlich avatar Nov 17 '23 14:11 c-ehrlich

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?

413n avatar Nov 17 '23 15:11 413n

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.

c-ehrlich avatar Nov 17 '23 15:11 c-ehrlich

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?

davidy3k avatar Mar 27 '24 01:03 davidy3k

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

Screenshot 2024-04-29 at 11 54 16 AM Screenshot 2024-04-29 at 11 54 27 AM

reubenjh avatar Apr 28 '24 23:04 reubenjh

@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?

FleetAdmiralJakob avatar May 25 '24 17:05 FleetAdmiralJakob

@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?

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

c-ehrlich avatar May 26 '24 22:05 c-ehrlich

Hi, I found that this example uses deprecated functions. I think this has to be migrated to .unstable_concat()

FleetAdmiralJakob avatar Jun 10 '24 17:06 FleetAdmiralJakob

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 },
  });
});

FleetAdmiralJakob avatar Jun 13 '24 11:06 FleetAdmiralJakob

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

c-ehrlich avatar Jul 02 '24 12:07 c-ehrlich

closing for now, but please ping me if something still isn't working.

c-ehrlich avatar Jul 02 '24 12:07 c-ehrlich