Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions cluster.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
22 changes: 22 additions & 0 deletions instrument.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
15 changes: 15 additions & 0 deletions logging.js
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions modules/api/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
32 changes: 28 additions & 4 deletions server.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
}
Expand All @@ -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
});
}
}
Loading