-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsetUpApplicationDefaults.js
More file actions
191 lines (165 loc) · 6.84 KB
/
Copy pathsetUpApplicationDefaults.js
File metadata and controls
191 lines (165 loc) · 6.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import pino from 'pino';
import pinoHttp, { stdSerializers } from 'pino-http';
import { randomUUID } from 'crypto';
import createErrorSerializer from 'serialize-every-error';
import finalhandler from 'dexpress-finalhandler';
import helmet from 'helmet';
import cors from 'cors';
import createHttpError from 'http-errors';
import methods from 'methods';
import asyncHandler from 'express-async-handler';
import express from 'express';
import promBundle from 'express-prom-bundle';
import { promisify } from 'util';
import httpGracefulShutdown from 'http-graceful-shutdown';
//TODO: better way to handle asynchrony here?
export default async (app, config, existingLogger) => {
let server;
let metricsServer;
if (existingLogger) {
// we should probably check if this is a pino logger, not sure we support other types of loggers
}
const logger = existingLogger || pino();
app.logger = logger;
const loggerMiddleware = pinoHttp({
logger,
autoLogging: false,
serializers: {
...stdSerializers,
req: (req) => {
const serialized = { ...stdSerializers.req(req) };
if(serialized?.headers?.authorization) {
serialized.headers = {
...serialized.headers,
authorization: '####REDACTED####',
};
}
return serialized;
},
// TODO: some non-async version of serializer with everything enabled pls
err: await createErrorSerializer(),
user: config.userSerializer ? config.userSerializer : (user) => {
if(user?.id || user?._id) {
return { id: user.id ?? user._id }
}
return {};
}
},
genReqId: randomUUID,
wrapSerializers: false,
});
app.use(loggerMiddleware);
app.use((req, res, next) => {
// here we default to true, unless explicitly turned off
if (config.enableRequestLogger !== false) req.log.info({ req }, 'New request');
// An alternative to pino-http autoLogger on the following lines
// autologger creates new Errors for http 500, which is super bad behavior
// instead we would probably like to report errors as errors and everything
// as plain completion
const startTime = Date.now();
const onResponse = () => {
res.removeListener('close', onResponse);
res.removeListener('finish', onResponse);
const responseTime = Date.now() - startTime;
// here we default to true, unless explicitly turned off
if (config.enableRequestLogger !== false) req.log.info({ res, responseTime, user: req.user }, 'Response sent');
};
res.on('close', onResponse);
res.on('finish', onResponse);
const onResponseError = (err) => {
res.removeListener('error', onResponseError);
req.log.error({ err, res, user: req.user }, 'Error while sending response');
}
res.on('error', onResponseError);
next();
});
// Attach always useful middleware
app.use(helmet(config.helmet));
app.use(cors(config.cors));
const prometheusMetricsConfig = { enabled: true, ...config.prometheusMetrics };
// Add prometheus monitoring metrics
if (prometheusMetricsConfig.enabled) {
if (prometheusMetricsConfig.port) {
app.metricsApp = express();
}
app.use(promBundle({
metricsApp: app.metricsApp, // its fine to pass an undefined here, if we don't want an alternative app for metrics
autoregister: !Boolean(prometheusMetricsConfig.port), // if no port defined, we register on primary express app
includeMethod: true,
includePath: true,
promClient: {
collectDefaultMetrics: { },
...prometheusMetricsConfig.promClient, // see https://github.com/siimon/prom-client for config options
},
}));
}
// Polyfill async handling in endpoints until support hits express
[ ...methods, 'all' ].forEach((method) => {
const originalMethod = app[method];
app[method] = function(path) {
if (arguments.length === 1) return originalMethod.call(app, path);
// Wrap middleware or arrays of middlewares in aysnchandler
const middlewares = Array.prototype.slice.call(arguments, 1)
.map((middleware) => {
if(Array.isArray(middleware)) {
return middleware.map(asyncHandler);
}
return asyncHandler(middleware);
});
originalMethod.apply(app, [ path, ...middlewares ]);
};
});
const originalUse = app.use;
app.use = function() {
// Wrap middlewares in async handler if they are functions or arrays
const middlewares = Array.prototype.slice.call(arguments)
.map((middleware) => {
if(Array.isArray(middleware)) {
return middleware.map(asyncHandler);
}
if(typeof middleware === 'function') {
return asyncHandler(middleware);
}
return middleware;
});
return originalUse.apply(app, middlewares);
};
// Attach configurable finalhandler
const originalHandle = app.handle.bind(app);
app.handle = (req, res, callback) => {
originalHandle(req, res, callback || finalhandler(req, res, {
onservererror: (err) => req.log.error({ err }, 'An error occurred'),
errortransform: (err) => {
if(typeof err !== 'object') {
return err;
}
if(err.issues && err.name === 'ZodError') {
return createHttpError(400, 'Invalid request', { issues: err.issues });
}
return err;
},
}));
}
// patch listen to also start metrics app if it exists
const originalListen = app.listen.bind(app);
app.listen = (port, callback) => {
if (!callback) callback = () => {};
const server = originalListen(port, () => {
if (app.metricsApp) {
metricsServer = app.metricsApp.listen(prometheusMetricsConfig.port, callback);
return;
}
callback();
});
const closeOriginal = server.close;
server.close = async (callback) => {
const options = { forceExit: false, signals: '' };
if (metricsServer) await httpGracefulShutdown(metricsServer, options)();
server.close = closeOriginal;
await httpGracefulShutdown(server, options)();
if (callback) callback();
return;
};
return server;
}
};