pinojs / pino

🌲 super fast, all natural json logger
http://getpino.io
MIT License
14.21k stars 875 forks source link

Pino & NextJS using Next API routes for browser logs #1655

Closed LinnJS closed 1 year ago

LinnJS commented 1 year ago

Hello Pino team just switched from Winston to Pino due to seeing that it was compatible with NextJS since I was having issues with Winston logging on the frontend.📋

After setting up Pino I thought the browser object in the config would make it "just work" but then saw it required browsify or a custom server as found in your example. Both of these require extra overhead and for another process to be running which made me sad. 😢

To be totally honest I am not 100% sure what the browser config really does since when trying to use Pino in the browser I get errors saying fs can not be found. Which was the exact same error I got when trying to use Winston on the frontend. I guess it adds extra formatting and manipulates the logs to make the FE console.logs look like the BE logs?❓

I saw this issue opened up about a year ago here which inspired me to open this issue to possibly pose a solution. ✅

I got the idea from plausible.io where they suggest to get around ad blockers by running your analytics through a serverless function so the blockers don't see the domain going to google or another analytics provider. 💪

So I used that same approach and it seems to be working wonderfully.

I made a logger API func in api/logger that looks like this:

import { NextApiRequest, NextApiResponse } from 'next';
import { logger as pino } from '@/utils/logger';

const logger = async (req: NextApiRequest, res: NextApiResponse) => {
  const { method, headers, body } = req;

  if (body.level === 'error') {
    pino.error({ method, headers, body }, `browser ${body.message} ${body.router.pathname}`);
  } else if (body.level === 'debug') {
    pino.debug({ method, headers, body }, `browser ${body.message} ${body.router.pathname}`);
  } else if (body.level === 'warn') {
    pino.warn({ method, headers, body }, `browser ${body.message} ${body.router.pathname}`);
  } else {
    pino.info({ method, headers, body }, `browser ${body.message} ${body.router.pathname}`);
  }

  res.status(200).end();
};

export default logger;

Then made a useLogger hook that make the API request easily duplicatable in hooks/useLogger:

import { useState, useCallback, useMemo } from 'react';
import { useRouter } from 'next/router';

interface LoggerParams {
  [key: string]: any;
}

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LoggerError {
  message: string;
}

export const useLogger = (): [
  logger: (level: LogLevel, message: string, params?: LoggerParams) => Promise<void>,
  error: LoggerError | null
] => {
  const [error, setError] = useState<LoggerError | null>(null);
  const router = useRouter();

  const logger = useCallback(
    async (level: LogLevel, message: string, params: LoggerParams = {}) => {
      setError(null);

      try {
        const response = await fetch('/api/logger', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ level, message, params, router }),
        });

        if (!response.ok) {
          throw new Error(`useLogger error: ${response.status}`);
        }
      } catch (error: any) {
        setError({ message: error.message });
      }
    },
    [router]
  );

  const memoizedLogger = useMemo(() => logger, [logger]);

  return [memoizedLogger, error];
};

This way I dont need any extra processes and get a pretty nice result. Also made the useLogger attach the route and add a browser prefix so I can easily discern between what is a server log and a browser log.

Would love to know what you all think and if you like this approach would love to add an example to your docs for NextJS users.

If there is any reason this is a bad idea please let me know but to me it seems to be pretty similar to what the custom server would have been doing right?

mcollina commented 1 year ago

After setting up Pino I thought the browser object in the config would make it "just work" but then saw it required browsify or a custom server as found in your example. Both of these require extra overhead and for another process to be running which made me sad. 😢

I don't really understand this. Could you clarify what's the problem?

To be totally honest I am not 100% sure what the browser config really does since when trying to use Pino in the browser I get errors saying fs can not be found. Which was the exact same error I got when trying to use Winston on the frontend. I guess it adds extra formatting and manipulates the logs to make the FE console.logs look like the BE logs?❓

I think you have some issue with your bundling, as it's not setting things up correctly.

LinnJS commented 1 year ago

@mcollina when I tried to import logger from the pino.createLogger instance and I import it into the "client-side" part of NextJS so any React component that is ran on the client side I get an error saying fs module cannot be found. This is because fs is a NodeJS server-side utility and does not exist in the browser. Looks like pino is using it and its a dependency and without using a custom server or browsify to strip node requirements out of the package which I dont really want to do. Feels like to much overhead.

So I am passing the params of pino through a NextJS function and running the pino logger on the backend at all times and seems like the result is similar to your alls NextJS example.

....

As I was typing out the above I went to go reproduce and at this point I removed all references to Winston and the error went away so I guess this entire issue is moot at this point... I am still going to keep my workflow of getting the FE logs to the BE to ease getting them into cloudwatch via stout but pino seems to be working on the FE side now as well. 🚀

Still unsure why the custom server is needed in the Pino example but is working as I expected now so I am happy. 🙂

Thanks for the 🦆 @mcollina

github-actions[bot] commented 1 year ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.