diff --git a/utils/build/docker/nodejs/nextjs.Dockerfile b/utils/build/docker/nodejs/nextjs.Dockerfile index 824d58639b8..b242eca4e3a 100644 --- a/utils/build/docker/nodejs/nextjs.Dockerfile +++ b/utils/build/docker/nodejs/nextjs.Dockerfile @@ -21,9 +21,14 @@ ENV DD_TRACE_HEADER_TAGS=user-agent # docker startup ENV DD_DATA_STREAMS_ENABLED=true +# Let src/instrumentation.js handle Next.js shutdown signals manually, as documented in: +# https://nextjs.org/docs/pages/guides/self-hosting#manual-graceful-shutdowns +# Support was added in https://github.com/vercel/next.js/pull/59117; this +# weblog is pinned to Next.js >=14.0.4 to support NEXT_MANUAL_SIG_HANDLE. +ENV NEXT_MANUAL_SIG_HANDLE=true ENV PORT=7777 ENV HOSTNAME=0.0.0.0 COPY utils/build/docker/nodejs/app.sh app.sh -RUN printf './node_modules/.bin/next start' >> app.sh ENV NODE_OPTIONS="--import dd-trace/initialize.mjs" -CMD ./app.sh +RUN printf 'exec ./node_modules/.bin/next start' >> app.sh +CMD ["./app.sh"] diff --git a/utils/build/docker/nodejs/nextjs/next.config.js b/utils/build/docker/nodejs/nextjs/next.config.js index 86e5f4cc8c6..c892b7d32b6 100644 --- a/utils/build/docker/nodejs/nextjs/next.config.js +++ b/utils/build/docker/nodejs/nextjs/next.config.js @@ -3,7 +3,11 @@ const nextConfig = { // Disabled because standalone mode does not support using modules directly // from node_modules which is necessary when using `npm link dd-trace`. // output: 'standalone' - outputFileTracing: false + outputFileTracing: false, + experimental: { + // Run out src/instrumentation.js hooks + instrumentationHook: true + } } module.exports = nextConfig diff --git a/utils/build/docker/nodejs/nextjs/src/instrumentation.js b/utils/build/docker/nodejs/nextjs/src/instrumentation.js new file mode 100644 index 00000000000..dfdfa6d557a --- /dev/null +++ b/utils/build/docker/nodejs/nextjs/src/instrumentation.js @@ -0,0 +1,33 @@ +'use strict' + +// Time to wait after invoking dd-trace's beforeExit handlers before forcing +// the process to exit. The handlers are fire-and-forget (no Promise, no +// awaitable signal) and initiate asynchronous HTTP exports for telemetry, +// traces, remote config, AppSec metrics, dogstatsd, etc., so we need a grace +// window for those in-flight requests to drain. Same as docker stop's default +// 10s SIGKILL timeout. +const SHUTDOWN_GRACE_MS = 10000 + +function shutdown () { + const ddTrace = globalThis[Symbol.for('dd-trace')] + + if (ddTrace?.beforeExitHandlers) { + for (const handler of ddTrace.beforeExitHandlers) { + try { + handler() + } catch (err) { + // Best-effort: keep running other handlers even if one throws. + console.error('dd-trace beforeExit handler threw', err) + } + } + } + + setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS).unref() +} + +export function register () { + if (!process.env.NEXT_MANUAL_SIG_HANDLE || process.env.NEXT_RUNTIME === 'edge') return + + process.once('SIGINT', shutdown) + process.once('SIGTERM', shutdown) +}