Skip to content

Instantly share code, notes, and snippets.

@justjake
Last active January 29, 2024 16:56
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save justjake/6fa39b07421a58ad58ff013b141b7172 to your computer and use it in GitHub Desktop.
Save justjake/6fa39b07421a58ad58ff013b141b7172 to your computer and use it in GitHub Desktop.
Customizing NextJS for error reporting and Datadog APM (dd-trace) integration. See https://jake.tl/notes/2021-04-04-nextjs-preload-hack
// @ts-check
"use strict"
/**
* Set up datadog tracing. This should be called first, so Datadog can hook
* all the other dependencies like `http`.
*/
function setUpDatadogTracing() {
const { tracer: Tracer } = require('dd-trace')
const tracer = Tracer.init({
// Your options here.
runtimeMetrics: true,
logInjection: true,
})
}
/**
* Polyfill DOMParser for react-intl
* Otherwise react-intl spews errors related to formatting
* messages with <xml>in them</xml>
*/
function setUpDOMParser() {
const xmldom = require("xmldom")
global["DOMParser"] = xmldom.DOMParser
}
/**
* Set up logging. Monkey patches a bunch of stuff.
*/
function setUpLogging() {
// pino is a simple JSON logger with Datadog integration.
// By default it logs to STDOUT.
const pino = require('pino')
const logger = pino({
// Your options here.
})
function getLoggingFunction(/** @type {string} */ levelName) {
const baseLogFn = (logger[levelName] || logger.info).bind(logger)
return function patchedLog(/** @type {any[]} */ ...parts) {
/** @type {object | undefined} */
let data = undefined
/** @type {object | undefined} */
let error = undefined
/** @type {object | undefined} */
const nativeError = parts.find(
it =>
(it && it instanceof Error) ||
(it && typeof it === "object" && "name" in it && "message" in it)
)
if (nativeError) {
error = cleanObjectForSerialization(nativeError)
// If you use Sentry, Rollbar, etc, you could capture the error here.
// ErrorThingy.report(nativeError)
}
// If next is trying to log funky stuff, put it into the data object.
if (parts.length > 1) {
data = data || {}
data.parts = parts.map(part => cleanObjectForSerialization(part))
}
const messages =
nativeError && parts.length === 1 ? [nativeError.toString()] : parts
baseLogFn({ data, error, type: levelName }, ...messages)
}
}
// Monkey-patch Next.js logger.
// See https://github.com/atkinchris/next-logger/blob/main/index.js
// See https://github.com/vercel/next.js/blob/canary/packages/next/build/output/log.ts
const nextBuiltInLogger = require("next/dist/build/output/log")
for (const [property, value] of Object.entries(nextBuiltInLogger)) {
if (typeof value !== "function") {
continue
}
nextBuiltInLogger[property] = getLoggingFunction(property)
}
/**
* Monkey-patch global console.log logger. Yes. Sigh.
* @type {Array<keyof typeof console>}
*/
const loggingProperties = ["log", "debug", "info", "warn", "error"]
for (const property of loggingProperties) {
console[property] = getLoggingFunction(property)
}
// Add general error logging.
process.on("unhandledRejection", (error, promise) => {
logger.error(
{
type: "unhandledRejection",
error: cleanObjectForSerialization(error),
data: { promise: cleanObjectForSerialization(promise) },
},
`${error}`
)
})
process.on("uncaughtException", error => {
logger.error(
{ type: "uncaughtException", error: cleanObjectForSerialization(error) },
`${error}`
)
})
}
function cleanObjectForSerialization(value) {
// Clean up or copy `value` so our logger or error reporting system
// can record it.
//
// Because our logger `pino` uses JSON.stringify, we need to do
// the following here:
//
// 1. Remove all cycles. JSON.stringify throws an error when you pass
// a value with cyclical references.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
// 2. Because JSON.stringify only serializes enumerable properties, we
// need to copy interesting, but non-enumerable properties like
// value.name and value.message for errors:
// JSON.stringify(new Error('nothing serialized')) returns '{}'
//
// Implementing this correctly is beyond the scope of my example.
return value
}
setUpDatadogTracing()
setUpDOMParser()
setUpLogging()
@arihantverma
Copy link

arihantverma commented Jul 15, 2021

Hi @justjake!

I have general logging question on top of your write up. How do we grab hold of incoming request, for unhandled promise rejections or unhandled errors events, that say happen somewhere in our getServerSideProps code, because of an incoming request? Wouldn't it be a good idea to be able to associate request meta data with those unhandled promise rejections or unhandled errors?

I've never implemented logging in any application before and am trying to wrap my head around what data logged where helps in tracing something having gone wrong. I couldn't find any production code or resources which would help me find some of these answers. If you could direct me to some that you might know that'd be of help.

@justjake
Copy link
Author

justjake commented Aug 6, 2021

  1. You can read the Nextjs source code to find out how Nextjs handles requests
  2. You can read the dd-trace source code to find out how Datadog hooks libraries to make them inspectable. For example: https://github.com/DataDog/dd-trace-js/blob/master/packages/datadog-plugin-http/src/server.js

@spikebrehm
Copy link

thanks jake

@y-a-v-a
Copy link

y-a-v-a commented Feb 20, 2023

Be careful though to not use pino-pretty this way, at least not as how leerob is suggesting here: vercel/next.js#32215 (comment) since next dev or next build will create too many instances of pino for its workers. This will lead to a huge memory consumption and is not workable.

Imagine:

const logger = pino({
  name: 'my-storefront-logger',
  level: 'debug',
  transport: {
    target: 'pino-pretty',
    options: {
      translateTime: 'yyyy-mm-dd HH:MM:ss.l o',
      sync: true,
    },
  },
});

and then start a next dev like this:

$ NODE_OPTIONS='-r ./server-preload.js' node ../node_modules/.bin/next dev

@DopamineDriven
Copy link

@y-a-v-a thanks for the heads up, much appreciated 🍻

@bradleyberwick
Copy link

bradleyberwick commented Jan 29, 2024

Hi @justjake,

This is just what I need so thank you! However, I'm currently getting the below error when running it, any ideas how to resolve this please?

nextBuiltInLogger[property] = getLoggingFunction(property)
^

TypeError: Cannot set property wait of # which has only a getter
at setUpLogging (C:...logger.js:58:33)
at Object. (C:...logger.js:109:1)
at Module._compile (node:internal/modules/cjs/loader:1241:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1295:10)
at Module.load (node:internal/modules/cjs/loader:1091:32)
at Module._load (node:internal/modules/cjs/loader:938:12)
at internalRequire (node:internal/modules/cjs/loader:166:19)
at Module._preloadModules (node:internal/modules/cjs/loader:1420:5)
at loadPreloadModules (node:internal/process/pre_execution:705:5)
at setupUserModules (node:internal/process/pre_execution:170:3)

@justjake
Copy link
Author

@bradleyberwick it’s been 3 years so things changed in NextJS. It seems like the wait property of next built in logger object isn’t writable anymore, so you’ll need to change the code to avoid trying to set that property, or use Object.defineProperty. You can find answers about the logger object by reading the NextJS source code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment