diff --git a/app.js b/app.js index 1f5a365f4..7f29dacb0 100644 --- a/app.js +++ b/app.js @@ -59,6 +59,13 @@ app.use(errorHandler); function logErrors(err, req, res, next) { + // Optional Sentry (instrument.js sets the global only when the + // linked module is present AND config has SENTRY_DSN — plain + // open-source installs never see it). + if (global.__Sentry) { + global.__Sentry.captureException(err); + } + if (CONFIG.RICH_LOG_ENABLED) { console.error(err.stack); } else { diff --git a/cluster.js b/cluster.js index cec7f50b5..ded9a3c3b 100644 --- a/cluster.js +++ b/cluster.js @@ -1,3 +1,6 @@ +// Optional Sentry — covers the cluster MASTER; workers re-import via +// server.js (module cache dedups). No-op on open-source installs. +import './instrument.js'; import { GracefulCluster } from 'graceful-cluster'; import * as sysUtils from './utils.js'; diff --git a/instrument.js b/instrument.js new file mode 100644 index 000000000..7286f29d7 --- /dev/null +++ b/instrument.js @@ -0,0 +1,22 @@ +// Optional instrumentation bootstrap. Import FIRST from an entry +// module (server.js / cluster.js). +// +// Deployment-injectable: when CONFIG.INSTRUMENT_MODULE names a module +// (resolvable in this install — e.g. linked in by the deployment) and +// that module exports initSentry(CONFIG), it is initialized and the +// SDK instance is exposed as `global.__Sentry` for the few call sites +// that can't import it (app.js logErrors). Plain installs configure +// nothing here and this file is a silent no-op. +import CONFIG from './config.loader.js'; + +if (CONFIG.INSTRUMENT_MODULE) { + // .then(), not top-level await — TLA would make every importer's + // graph async (`require()` consumers of the package would break). + import(CONFIG.INSTRUMENT_MODULE).then((m) => { + if (m.initSentry && m.initSentry(CONFIG)) { + global.__Sentry = m.Sentry; + } + }).catch((e) => { + console.log('INSTRUMENT_MODULE "' + CONFIG.INSTRUMENT_MODULE + '" failed to load: ' + e.message); + }); +} diff --git a/logging.js b/logging.js index 0a3af6dca..2a5cfa68f 100644 --- a/logging.js +++ b/logging.js @@ -1,6 +1,16 @@ import moment from 'moment'; import CONFIG from './config.loader.js'; +// Host-injectable logging sink. When iframely is used as a library +// inside another application, the host may call setLogger(l) with a +// logger object ({info, warn, error}) to route every iframely log +// line into its own logging pipeline. Standalone installs never call +// setLogger and keep the classic prefixed console output below. +let sink = null; +export function setLogger(l) { + sink = l; +} + export default function log() { var args = Array.prototype.slice.apply(arguments); @@ -14,6 +24,11 @@ export default function log() { } } + if (sink) { + sink.info.apply(sink, args); + return; + } + if (CONFIG.LOG_DATE_FORMAT) { args.splice(0, 0, "--", moment().utc().format(CONFIG.LOG_DATE_FORMAT) + process.pid); } else { diff --git a/modules/api/views.js b/modules/api/views.js index 89d6dfa23..6c8da1ebb 100644 --- a/modules/api/views.js +++ b/modules/api/views.js @@ -115,6 +115,11 @@ function processInitialErrors(uri, next) { export default function(app) { app.get('/health_check', function(req, res, next) { + // 503 while draining (graceful shutdown, see server.js) + // so the LB ejects this worker before connections close. + if (global.__draining) { + return res.sendStatus(503); + } res.sendStatus(200); }); diff --git a/server.js b/server.js index 6e969f166..2a9157385 100644 --- a/server.js +++ b/server.js @@ -1,3 +1,16 @@ +// Entrypoint for the standalone run modes: +// - plain `node server.js` (open-source installs) — just listens +// - pm2 / cluster.js — legacy GracefulServer shutdown +// Supervised worker deployments run an EXTERNAL entrypoint instead, +// which imports this module and wires drain/readiness/watchdog around +// the listener. This module's side of that contract is passive: it +// exposes the listener as `global.__listener`, /health_check returns +// 503 while `global.__draining` is set, and the legacy shutdown +// handler stays off when INSTANCE is set. + +// Optional Sentry (no-op without the linked module + config field). +import './instrument.js'; +import { GracefulServer } from 'graceful-cluster'; import * as sysUtils from './utils.js'; import app from './app.js'; import * as https from 'https'; @@ -7,6 +20,9 @@ var server = app.listen(process.env.PORT || CONFIG.port, process.env.HOST || CON console.log('API endpoints: /oembed and /iframely; Debugger UI: /debug\n'); }); +// Contract with external worker supervisors (see header). +global.__listener = server; + if (CONFIG.ssl) { https.createServer(CONFIG.ssl, app).listen(CONFIG.ssl.port); } @@ -16,12 +32,20 @@ console.log(' - support@iframely.com - if you need help'); console.log(' - twitter.com/iframely - news & updates'); console.log(' - github.com/itteco/iframely - star & contribute'); -import { GracefulServer } from 'graceful-cluster'; - -if (!CONFIG.DEBUG) { +// INSTANCE is set only under supervised worker deployments, where an +// external entrypoint owns shutdown/readiness around the exposed +// listener — the legacy handler must not also react to signals there. +if (process.env.INSTANCE === undefined && !CONFIG.DEBUG) { + // Legacy graceful shutdown for the pm2 / standalone paths. + // Deliberately a STATIC import (top of file): the constructor + // registers the SIGTERM/SIGINT handlers, and an `await import` + // here would (a) widen the unguarded-signal window after listen() + // and (b) make this module async (`require()` consumers would get + // ERR_REQUIRE_ASYNC_MODULE). graceful-cluster is a real + // package.json dependency — nothing to gain from lazy loading. new GracefulServer({ server: server, log: sysUtils.log, shutdownTimeout: CONFIG.SHUTDOWN_TIMEOUT }); -} \ No newline at end of file +}