@naturalcycles/backend-lib 2.72.0 → 2.73.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -3
- package/dist/sentry/sentry.shared.service.d.ts +9 -0
- package/dist/sentry/sentry.shared.service.js +28 -1
- package/dist/server/handlers/asyncLocalStorage.mw.d.ts +1 -1
- package/dist/server/handlers/asyncLocalStorage.mw.js +2 -2
- package/dist/server/handlers/createGaeLogMiddleware.d.ts +6 -4
- package/dist/server/handlers/createGaeLogMiddleware.js +10 -9
- package/dist/server/handlers/genericErrorHandler.mw.js +15 -12
- package/dist/server/handlers/serverStatsMiddleware.js +2 -14
- package/dist/server/handlers/simpleRequestLogger.mw.js +3 -2
- package/package.json +4 -2
- package/src/index.ts +3 -2
- package/src/sentry/sentry.shared.service.ts +36 -2
- package/src/server/handlers/asyncLocalStorage.mw.ts +4 -4
- package/src/server/handlers/createGaeLogMiddleware.ts +9 -9
- package/src/server/handlers/genericErrorHandler.mw.ts +23 -15
- package/src/server/handlers/serverStatsMiddleware.ts +6 -17
- package/src/server/handlers/simpleRequestLogger.mw.ts +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,9 @@ import { coloredHttpCode, logRequest } from './server/request.log.util';
|
|
|
27
27
|
import { BackendServer, startServer } from './server/startServer';
|
|
28
28
|
import { StartServerCfg, StartServerData } from './server/startServer.model';
|
|
29
29
|
import { createAsyncLocalStorage, getRequest, getRequestLogger, requestLogger } from './server/handlers/asyncLocalStorage.mw';
|
|
30
|
-
import {
|
|
30
|
+
import type { RequestWithLog } from './server/handlers/createGaeLogMiddleware';
|
|
31
|
+
export * from './server/handlers/createGaeLogMiddleware';
|
|
31
32
|
import { safeJsonMiddleware } from './server/handlers/safeJsonMiddleware';
|
|
33
|
+
export * from './server/request.util';
|
|
32
34
|
export type { MethodOverrideCfg, SentrySharedServiceCfg, RequestHandlerWithPath, RequestHandlerCfg, DefaultAppCfg, StartServerCfg, StartServerData, EnvSharedServiceCfg, BaseEnv, AdminMiddleware, AdminServiceCfg, AdminInfo, RequireAdminCfg, SecureHeaderMiddlewareCfg, BodyParserTimeoutCfg, RequestTimeoutCfg, SimpleRequestLoggerCfg, ReqValidationOptions, RequestWithLog, };
|
|
33
|
-
export { BackendServer, SentrySharedService, EnvSharedService, reqValidation, notFoundHandler, genericErrorHandler, methodOverride, createDefaultApp, startServer, catchWrapper, getDefaultRouter, isGAE, statusHandler, statusHandlerData, okHandler, getDeployInfo, onFinished, respondWithError, logRequest, FirebaseSharedService, createAdminMiddleware, BaseAdminService, loginHtml, createSecureHeaderMiddleware, bodyParserTimeout, clearBodyParserTimeout, requestTimeout, simpleRequestLogger, coloredHttpCode, validateBody, validateParams, validateQuery, createAsyncLocalStorage,
|
|
35
|
+
export { BackendServer, SentrySharedService, EnvSharedService, reqValidation, notFoundHandler, genericErrorHandler, methodOverride, createDefaultApp, startServer, catchWrapper, getDefaultRouter, isGAE, statusHandler, statusHandlerData, okHandler, getDeployInfo, onFinished, respondWithError, logRequest, FirebaseSharedService, createAdminMiddleware, BaseAdminService, loginHtml, createSecureHeaderMiddleware, bodyParserTimeout, clearBodyParserTimeout, requestTimeout, simpleRequestLogger, coloredHttpCode, validateBody, validateParams, validateQuery, createAsyncLocalStorage, getRequest, getRequestLogger, requestLogger, serverStatsHTMLHandler, serverStatsMiddleware, safeJsonMiddleware, };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.safeJsonMiddleware = exports.serverStatsMiddleware = exports.serverStatsHTMLHandler = exports.requestLogger = exports.getRequestLogger = exports.getRequest = exports.
|
|
3
|
+
exports.safeJsonMiddleware = exports.serverStatsMiddleware = exports.serverStatsHTMLHandler = exports.requestLogger = exports.getRequestLogger = exports.getRequest = exports.createAsyncLocalStorage = exports.validateQuery = exports.validateParams = exports.validateBody = exports.coloredHttpCode = exports.simpleRequestLogger = exports.requestTimeout = exports.clearBodyParserTimeout = exports.bodyParserTimeout = exports.createSecureHeaderMiddleware = exports.loginHtml = exports.BaseAdminService = exports.createAdminMiddleware = exports.FirebaseSharedService = exports.logRequest = exports.respondWithError = exports.onFinished = exports.getDeployInfo = exports.okHandler = exports.statusHandlerData = exports.statusHandler = exports.isGAE = exports.getDefaultRouter = exports.catchWrapper = exports.startServer = exports.createDefaultApp = exports.methodOverride = exports.genericErrorHandler = exports.notFoundHandler = exports.reqValidation = exports.EnvSharedService = exports.SentrySharedService = exports.BackendServer = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
4
5
|
const onFinished = require("on-finished");
|
|
5
6
|
exports.onFinished = onFinished;
|
|
6
7
|
const admin_mw_1 = require("./admin/admin.mw");
|
|
@@ -65,7 +66,7 @@ Object.defineProperty(exports, "createAsyncLocalStorage", { enumerable: true, ge
|
|
|
65
66
|
Object.defineProperty(exports, "getRequest", { enumerable: true, get: function () { return asyncLocalStorage_mw_1.getRequest; } });
|
|
66
67
|
Object.defineProperty(exports, "getRequestLogger", { enumerable: true, get: function () { return asyncLocalStorage_mw_1.getRequestLogger; } });
|
|
67
68
|
Object.defineProperty(exports, "requestLogger", { enumerable: true, get: function () { return asyncLocalStorage_mw_1.requestLogger; } });
|
|
68
|
-
|
|
69
|
-
Object.defineProperty(exports, "createGAELogMiddleware", { enumerable: true, get: function () { return createGaeLogMiddleware_1.createGAELogMiddleware; } });
|
|
69
|
+
(0, tslib_1.__exportStar)(require("./server/handlers/createGaeLogMiddleware"), exports);
|
|
70
70
|
const safeJsonMiddleware_1 = require("./server/handlers/safeJsonMiddleware");
|
|
71
71
|
Object.defineProperty(exports, "safeJsonMiddleware", { enumerable: true, get: function () { return safeJsonMiddleware_1.safeJsonMiddleware; } });
|
|
72
|
+
(0, tslib_1.__exportStar)(require("./server/request.util"), exports);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CommonLogger } from '@naturalcycles/js-lib';
|
|
1
2
|
import { Severity } from '@sentry/node';
|
|
2
3
|
import type { Breadcrumb, NodeOptions } from '@sentry/node';
|
|
3
4
|
import type * as SentryLib from '@sentry/node';
|
|
@@ -35,4 +36,12 @@ export declare class SentrySharedService {
|
|
|
35
36
|
*/
|
|
36
37
|
captureMessage(msg: string, level?: Severity): string;
|
|
37
38
|
addBreadcrumb(breadcrumb: Breadcrumb): void;
|
|
39
|
+
/**
|
|
40
|
+
* Currently it will only use `logger.error` ("error" level) and ignore `log` and `warn`.
|
|
41
|
+
*
|
|
42
|
+
* For each `logger.error` - it'll do a captureException.
|
|
43
|
+
*
|
|
44
|
+
* @experimental
|
|
45
|
+
*/
|
|
46
|
+
getCommonLogger(): CommonLogger;
|
|
38
47
|
}
|
|
@@ -84,7 +84,7 @@ class SentrySharedService {
|
|
|
84
84
|
}),
|
|
85
85
|
// data: (err as AppError).data, // included in message
|
|
86
86
|
});
|
|
87
|
-
return this.sentry().captureException((0, js_lib_1._anyToError)(err, {
|
|
87
|
+
return this.sentry().captureException((0, js_lib_1._anyToError)(err, Error, {
|
|
88
88
|
stringifyFn: nodejs_lib_1.inspectAnyStringifyFn,
|
|
89
89
|
}));
|
|
90
90
|
}
|
|
@@ -98,6 +98,33 @@ class SentrySharedService {
|
|
|
98
98
|
addBreadcrumb(breadcrumb) {
|
|
99
99
|
this.sentry().addBreadcrumb(breadcrumb);
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Currently it will only use `logger.error` ("error" level) and ignore `log` and `warn`.
|
|
103
|
+
*
|
|
104
|
+
* For each `logger.error` - it'll do a captureException.
|
|
105
|
+
*
|
|
106
|
+
* @experimental
|
|
107
|
+
*/
|
|
108
|
+
getCommonLogger() {
|
|
109
|
+
return {
|
|
110
|
+
log: () => { },
|
|
111
|
+
warn: () => { },
|
|
112
|
+
error: (...args) => {
|
|
113
|
+
const message = args
|
|
114
|
+
.map(arg => (0, nodejs_lib_1.inspectAny)(arg, {
|
|
115
|
+
includeErrorData: true,
|
|
116
|
+
colors: false,
|
|
117
|
+
}))
|
|
118
|
+
.join(' ');
|
|
119
|
+
this.sentry().addBreadcrumb({
|
|
120
|
+
message,
|
|
121
|
+
});
|
|
122
|
+
this.sentry().captureException((0, js_lib_1._anyToError)(args.length === 1 ? args[0] : args, Error, {
|
|
123
|
+
stringifyFn: nodejs_lib_1.inspectAnyStringifyFn,
|
|
124
|
+
}));
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
101
128
|
}
|
|
102
129
|
(0, tslib_1.__decorate)([
|
|
103
130
|
(0, js_lib_1._Memo)()
|
|
@@ -4,7 +4,7 @@ export interface RequestLocalStorage {
|
|
|
4
4
|
req: Request;
|
|
5
5
|
}
|
|
6
6
|
export declare function createAsyncLocalStorage(): RequestHandler;
|
|
7
|
-
export declare function getRequest(): Request;
|
|
7
|
+
export declare function getRequest(): Request | undefined;
|
|
8
8
|
/**
|
|
9
9
|
* It requires both `createAsyncLocalStorage` and `createGAELogMiddleware` to be in use to work.
|
|
10
10
|
*
|
|
@@ -19,7 +19,7 @@ function createAsyncLocalStorage() {
|
|
|
19
19
|
}
|
|
20
20
|
exports.createAsyncLocalStorage = createAsyncLocalStorage;
|
|
21
21
|
function getRequest() {
|
|
22
|
-
return storage().getStore()
|
|
22
|
+
return storage().getStore()?.req;
|
|
23
23
|
}
|
|
24
24
|
exports.getRequest = getRequest;
|
|
25
25
|
/**
|
|
@@ -28,7 +28,7 @@ exports.getRequest = getRequest;
|
|
|
28
28
|
* @experimental
|
|
29
29
|
*/
|
|
30
30
|
function getRequestLogger() {
|
|
31
|
-
return storage().getStore()?.req || (isGAE ? createGaeLogMiddleware_1.
|
|
31
|
+
return storage().getStore()?.req || (isGAE ? createGaeLogMiddleware_1.gaeLogger : createGaeLogMiddleware_1.devLogger);
|
|
32
32
|
}
|
|
33
33
|
exports.getRequestLogger = getRequestLogger;
|
|
34
34
|
/**
|
|
@@ -18,11 +18,13 @@ declare module 'http' {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Logger that logs in AppEngine format.
|
|
22
|
+
* To be used in outside-of-request situations (otherwise req.log should be used).
|
|
22
23
|
*/
|
|
23
|
-
export declare const
|
|
24
|
+
export declare const gaeLogger: CommonLogger;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* Fancy development logger, to be used in outside-of-request situations
|
|
27
|
+
* (otherwise req.log should be used).
|
|
26
28
|
*/
|
|
27
|
-
export declare const
|
|
29
|
+
export declare const devLogger: CommonLogger;
|
|
28
30
|
export declare function createGAELogMiddleware(): RequestHandler;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createGAELogMiddleware = exports.
|
|
3
|
+
exports.createGAELogMiddleware = exports.devLogger = exports.gaeLogger = void 0;
|
|
4
4
|
const util_1 = require("util");
|
|
5
5
|
const colors_1 = require("@naturalcycles/nodejs-lib/dist/colors");
|
|
6
6
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
@@ -9,17 +9,19 @@ const isGAE = !!GAE_INSTANCE;
|
|
|
9
9
|
// Simple "request counter" (poor man's "correlation id") counter, to use on dev machine (not in the cloud)
|
|
10
10
|
let reqCounter = 0;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Logger that logs in AppEngine format.
|
|
13
|
+
* To be used in outside-of-request situations (otherwise req.log should be used).
|
|
13
14
|
*/
|
|
14
|
-
exports.
|
|
15
|
+
exports.gaeLogger = {
|
|
15
16
|
log: (...args) => logToAppEngine({}, args),
|
|
16
17
|
warn: (...args) => logToAppEngine({ severity: 'WARNING' }, args),
|
|
17
18
|
error: (...args) => logToAppEngine({ severity: 'ERROR' }, args),
|
|
18
19
|
};
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
+
* Fancy development logger, to be used in outside-of-request situations
|
|
22
|
+
* (otherwise req.log should be used).
|
|
21
23
|
*/
|
|
22
|
-
exports.
|
|
24
|
+
exports.devLogger = {
|
|
23
25
|
log: (...args) => logToDev(null, args),
|
|
24
26
|
warn: (...args) => logToDev(null, args),
|
|
25
27
|
error: (...args) => logToDev(null, args),
|
|
@@ -50,14 +52,13 @@ function createGAELogMiddleware() {
|
|
|
50
52
|
}
|
|
51
53
|
// Otherwise, we're in AppEngine
|
|
52
54
|
return function gaeLogMiddleware(req, res, next) {
|
|
53
|
-
const meta = {};
|
|
54
55
|
const traceHeader = req.header('x-cloud-trace-context');
|
|
55
56
|
if (traceHeader) {
|
|
56
57
|
const [trace] = traceHeader.split('/');
|
|
57
|
-
|
|
58
|
+
const meta = {
|
|
58
59
|
'logging.googleapis.com/trace': `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`,
|
|
59
60
|
'appengine.googleapis.com/request_id': req.header('x-appengine-request-log-id'),
|
|
60
|
-
}
|
|
61
|
+
};
|
|
61
62
|
Object.assign(req, {
|
|
62
63
|
log: (...args) => logToAppEngine({ ...meta, severity: 'INFO' }, args),
|
|
63
64
|
warn: (...args) => logToAppEngine({ ...meta, severity: 'WARNING' }, args),
|
|
@@ -66,7 +67,7 @@ function createGAELogMiddleware() {
|
|
|
66
67
|
req.requestId = trace;
|
|
67
68
|
}
|
|
68
69
|
else {
|
|
69
|
-
Object.assign(req, exports.
|
|
70
|
+
Object.assign(req, exports.gaeLogger);
|
|
70
71
|
}
|
|
71
72
|
next();
|
|
72
73
|
};
|
|
@@ -37,26 +37,29 @@ function respondWithError(req, res, err) {
|
|
|
37
37
|
var _a, _b;
|
|
38
38
|
const { headersSent } = res;
|
|
39
39
|
req.error(`genericErrorHandler${headersSent ? ' after headersSent' : ''}:\n`, err);
|
|
40
|
-
const
|
|
40
|
+
const originalError = (0, js_lib_1._anyToError)(err, Error, {
|
|
41
41
|
stringifyFn: nodejs_lib_1.inspectAnyStringifyFn,
|
|
42
|
-
includeErrorStack,
|
|
43
|
-
includeErrorData: true,
|
|
44
42
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
(0, js_lib_1._filterUndefinedValues)(error.data, true);
|
|
49
|
-
if (sentryService && shouldReportToSentry(error)) {
|
|
50
|
-
error.data.errorId = sentryService.captureException(error, false);
|
|
43
|
+
let errorId;
|
|
44
|
+
if (sentryService && shouldReportToSentry(originalError)) {
|
|
45
|
+
errorId = sentryService.captureException(originalError, false);
|
|
51
46
|
}
|
|
52
47
|
if (res.headersSent)
|
|
53
48
|
return;
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
const httpError = (0, js_lib_1._errorToErrorObject)(originalError, includeErrorStack);
|
|
50
|
+
httpError.data.errorId = errorId;
|
|
51
|
+
(_a = httpError.data).httpStatusCode || (_a.httpStatusCode = 500); // default to 500
|
|
52
|
+
httpError.data.headersSent = headersSent || undefined;
|
|
53
|
+
(_b = httpError.data).report || (_b.report = undefined); // set to undefined if false
|
|
54
|
+
(0, js_lib_1._filterUndefinedValues)(httpError.data, true);
|
|
55
|
+
res.status(httpError.data.httpStatusCode).json({
|
|
56
|
+
error: httpError,
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
exports.respondWithError = respondWithError;
|
|
59
60
|
function shouldReportToSentry(err) {
|
|
60
61
|
// Only report 5xx
|
|
61
|
-
return err
|
|
62
|
+
return (err?.data?.report ||
|
|
63
|
+
!err?.data ||
|
|
64
|
+
err.data.httpStatusCode >= 500);
|
|
62
65
|
}
|
|
@@ -23,8 +23,7 @@ const serverStatsHTMLHandler = (req, res) => {
|
|
|
23
23
|
// calc things
|
|
24
24
|
(0, js_lib_1._stringMapValues)(serverStatsMap).forEach(s => {
|
|
25
25
|
s.total = s['2xx'] + s['4xx'] + s['5xx'];
|
|
26
|
-
s.pc =
|
|
27
|
-
percentiles.forEach(pc => (s.pc[pc] = Math.round((0, js_lib_1._percentile)(s.stack.items, pc))));
|
|
26
|
+
s.pc = (0, js_lib_1._mapValues)(s.stack.percentiles(percentiles), (_k, v) => Math.round(v), true);
|
|
28
27
|
});
|
|
29
28
|
const allLatencies = (0, js_lib_1._stringMapValues)(serverStatsMap).flatMap(s => s.stack.items);
|
|
30
29
|
const all2xx = (0, js_lib_1._sum)((0, js_lib_1._stringMapValues)(serverStatsMap).flatMap(s => s['2xx']));
|
|
@@ -80,7 +79,7 @@ function serverStatsMiddleware() {
|
|
|
80
79
|
const latency = now - started;
|
|
81
80
|
const endpoint = (0, request_util_1.getRequestEndpoint)(req);
|
|
82
81
|
serverStatsMap[endpoint] || (serverStatsMap[endpoint] = {
|
|
83
|
-
stack: new
|
|
82
|
+
stack: new js_lib_1.NumberStack(SIZE),
|
|
84
83
|
'2xx': 0,
|
|
85
84
|
'4xx': 0,
|
|
86
85
|
'5xx': 0,
|
|
@@ -112,14 +111,3 @@ function getStatusFamily(statusCode) {
|
|
|
112
111
|
return '4xx';
|
|
113
112
|
return '5xx';
|
|
114
113
|
}
|
|
115
|
-
class SizeLimitedStack {
|
|
116
|
-
constructor(size) {
|
|
117
|
-
this.size = size;
|
|
118
|
-
this.index = 0;
|
|
119
|
-
this.items = [];
|
|
120
|
-
}
|
|
121
|
-
push(item) {
|
|
122
|
-
this.items[this.index] = item;
|
|
123
|
-
this.index = this.index === this.size ? 0 : this.index + 1;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
@@ -5,9 +5,11 @@ const js_lib_1 = require("@naturalcycles/js-lib");
|
|
|
5
5
|
const colors_1 = require("@naturalcycles/nodejs-lib/dist/colors");
|
|
6
6
|
const index_1 = require("../../index");
|
|
7
7
|
const request_log_util_1 = require("../request.log.util");
|
|
8
|
+
const { APP_ENV } = process.env;
|
|
8
9
|
function simpleRequestLogger(_cfg = {}) {
|
|
9
10
|
// Disable logger in AppEngine, as it doesn't make sense there
|
|
10
|
-
|
|
11
|
+
// UPD: Only log in dev environment
|
|
12
|
+
if (APP_ENV !== 'dev')
|
|
11
13
|
return (req, res, next) => next();
|
|
12
14
|
const cfg = {
|
|
13
15
|
logStart: false,
|
|
@@ -18,7 +20,6 @@ function simpleRequestLogger(_cfg = {}) {
|
|
|
18
20
|
return (req, res, next) => {
|
|
19
21
|
const started = Date.now();
|
|
20
22
|
if (logStart) {
|
|
21
|
-
;
|
|
22
23
|
req.log(['>>', req.method, (0, colors_1.boldGrey)(req.url)].join(' '));
|
|
23
24
|
}
|
|
24
25
|
if (logFinish) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/backend-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.73.3",
|
|
4
4
|
"scripts": {
|
|
5
|
-
"prepare": "husky install",
|
|
5
|
+
"prepare": "husky install && patch-package",
|
|
6
6
|
"serve": "APP_ENV=dev nodemon",
|
|
7
7
|
"docs-serve": "vuepress dev docs",
|
|
8
8
|
"docs-build": "vuepress build docs",
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"@types/ejs": "^3.0.0",
|
|
46
46
|
"@types/js-yaml": "^4.0.0",
|
|
47
47
|
"@types/node": "^16.4.1",
|
|
48
|
+
"esbuild": "^0.13.13",
|
|
49
|
+
"esbuild-register": "^3.1.2",
|
|
48
50
|
"fastify": "^3.20.1",
|
|
49
51
|
"jest": "^27.0.1",
|
|
50
52
|
"nodemon": "^2.0.14",
|
package/src/index.ts
CHANGED
|
@@ -51,8 +51,10 @@ import {
|
|
|
51
51
|
getRequestLogger,
|
|
52
52
|
requestLogger,
|
|
53
53
|
} from './server/handlers/asyncLocalStorage.mw'
|
|
54
|
-
import {
|
|
54
|
+
import type { RequestWithLog } from './server/handlers/createGaeLogMiddleware'
|
|
55
|
+
export * from './server/handlers/createGaeLogMiddleware'
|
|
55
56
|
import { safeJsonMiddleware } from './server/handlers/safeJsonMiddleware'
|
|
57
|
+
export * from './server/request.util'
|
|
56
58
|
|
|
57
59
|
export type {
|
|
58
60
|
MethodOverrideCfg,
|
|
@@ -110,7 +112,6 @@ export {
|
|
|
110
112
|
validateParams,
|
|
111
113
|
validateQuery,
|
|
112
114
|
createAsyncLocalStorage,
|
|
113
|
-
createGAELogMiddleware,
|
|
114
115
|
getRequest,
|
|
115
116
|
getRequestLogger,
|
|
116
117
|
requestLogger,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _anyToError, _Memo, CommonLogLevel } from '@naturalcycles/js-lib'
|
|
1
|
+
import { _anyToError, _Memo, CommonLogger, CommonLogLevel } from '@naturalcycles/js-lib'
|
|
2
2
|
import { inspectAny, inspectAnyStringifyFn } from '@naturalcycles/nodejs-lib'
|
|
3
3
|
import { Severity } from '@sentry/node'
|
|
4
4
|
import type { Breadcrumb, NodeOptions } from '@sentry/node'
|
|
@@ -98,7 +98,7 @@ export class SentrySharedService {
|
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
return this.sentry().captureException(
|
|
101
|
-
_anyToError(err, {
|
|
101
|
+
_anyToError(err, Error, {
|
|
102
102
|
stringifyFn: inspectAnyStringifyFn,
|
|
103
103
|
}),
|
|
104
104
|
)
|
|
@@ -115,4 +115,38 @@ export class SentrySharedService {
|
|
|
115
115
|
addBreadcrumb(breadcrumb: Breadcrumb): void {
|
|
116
116
|
this.sentry().addBreadcrumb(breadcrumb)
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Currently it will only use `logger.error` ("error" level) and ignore `log` and `warn`.
|
|
121
|
+
*
|
|
122
|
+
* For each `logger.error` - it'll do a captureException.
|
|
123
|
+
*
|
|
124
|
+
* @experimental
|
|
125
|
+
*/
|
|
126
|
+
getCommonLogger(): CommonLogger {
|
|
127
|
+
return {
|
|
128
|
+
log: () => {}, // noop
|
|
129
|
+
warn: () => {}, // noop
|
|
130
|
+
error: (...args) => {
|
|
131
|
+
const message = args
|
|
132
|
+
.map(arg =>
|
|
133
|
+
inspectAny(arg, {
|
|
134
|
+
includeErrorData: true,
|
|
135
|
+
colors: false,
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
.join(' ')
|
|
139
|
+
|
|
140
|
+
this.sentry().addBreadcrumb({
|
|
141
|
+
message,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
this.sentry().captureException(
|
|
145
|
+
_anyToError(args.length === 1 ? args[0] : args, Error, {
|
|
146
|
+
stringifyFn: inspectAnyStringifyFn,
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|
|
118
152
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks'
|
|
2
2
|
import { _lazyValue, CommonLogger } from '@naturalcycles/js-lib'
|
|
3
3
|
import { Request, RequestHandler } from 'express'
|
|
4
|
-
import {
|
|
4
|
+
import { gaeLogger, devLogger } from './createGaeLogMiddleware'
|
|
5
5
|
|
|
6
6
|
const { GAE_INSTANCE } = process.env
|
|
7
7
|
const isGAE = !!GAE_INSTANCE
|
|
@@ -24,8 +24,8 @@ export function createAsyncLocalStorage(): RequestHandler {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function getRequest(): Request {
|
|
28
|
-
return storage().getStore()
|
|
27
|
+
export function getRequest(): Request | undefined {
|
|
28
|
+
return storage().getStore()?.req
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -34,7 +34,7 @@ export function getRequest(): Request {
|
|
|
34
34
|
* @experimental
|
|
35
35
|
*/
|
|
36
36
|
export function getRequestLogger(): CommonLogger {
|
|
37
|
-
return storage().getStore()?.req || (isGAE ?
|
|
37
|
+
return storage().getStore()?.req || (isGAE ? gaeLogger : devLogger)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -32,18 +32,20 @@ const isGAE = !!GAE_INSTANCE
|
|
|
32
32
|
let reqCounter = 0
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Logger that logs in AppEngine format.
|
|
36
|
+
* To be used in outside-of-request situations (otherwise req.log should be used).
|
|
36
37
|
*/
|
|
37
|
-
export const
|
|
38
|
+
export const gaeLogger: CommonLogger = {
|
|
38
39
|
log: (...args) => logToAppEngine({}, args),
|
|
39
40
|
warn: (...args) => logToAppEngine({ severity: 'WARNING' }, args),
|
|
40
41
|
error: (...args) => logToAppEngine({ severity: 'ERROR' }, args),
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
+
* Fancy development logger, to be used in outside-of-request situations
|
|
46
|
+
* (otherwise req.log should be used).
|
|
45
47
|
*/
|
|
46
|
-
export const
|
|
48
|
+
export const devLogger: CommonLogger = {
|
|
47
49
|
log: (...args) => logToDev(null, args),
|
|
48
50
|
warn: (...args) => logToDev(null, args),
|
|
49
51
|
error: (...args) => logToDev(null, args),
|
|
@@ -83,15 +85,13 @@ export function createGAELogMiddleware(): RequestHandler {
|
|
|
83
85
|
// Otherwise, we're in AppEngine
|
|
84
86
|
|
|
85
87
|
return function gaeLogMiddleware(req, res, next) {
|
|
86
|
-
const meta: AnyObject = {}
|
|
87
|
-
|
|
88
88
|
const traceHeader = req.header('x-cloud-trace-context')
|
|
89
89
|
if (traceHeader) {
|
|
90
90
|
const [trace] = traceHeader.split('/')
|
|
91
|
-
|
|
91
|
+
const meta = {
|
|
92
92
|
'logging.googleapis.com/trace': `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`,
|
|
93
93
|
'appengine.googleapis.com/request_id': req.header('x-appengine-request-log-id'),
|
|
94
|
-
}
|
|
94
|
+
}
|
|
95
95
|
Object.assign(req, {
|
|
96
96
|
log: (...args: any[]) => logToAppEngine({ ...meta, severity: 'INFO' }, args),
|
|
97
97
|
warn: (...args: any[]) => logToAppEngine({ ...meta, severity: 'WARNING' }, args),
|
|
@@ -99,7 +99,7 @@ export function createGAELogMiddleware(): RequestHandler {
|
|
|
99
99
|
})
|
|
100
100
|
req.requestId = trace
|
|
101
101
|
} else {
|
|
102
|
-
Object.assign(req,
|
|
102
|
+
Object.assign(req, gaeLogger)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
next()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
_anyToError,
|
|
3
|
+
_errorToErrorObject,
|
|
3
4
|
_filterUndefinedValues,
|
|
4
5
|
HttpError,
|
|
5
6
|
HttpErrorData,
|
|
@@ -53,29 +54,36 @@ export function respondWithError(req: RequestWithLog, res: Response, err: any):
|
|
|
53
54
|
|
|
54
55
|
req.error(`genericErrorHandler${headersSent ? ' after headersSent' : ''}:\n`, err)
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
+
const originalError = _anyToError(err, Error, {
|
|
57
58
|
stringifyFn: inspectAnyStringifyFn,
|
|
58
|
-
|
|
59
|
-
includeErrorData: true,
|
|
60
|
-
}) as HttpError
|
|
59
|
+
})
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
error.data.headersSent = headersSent || undefined
|
|
64
|
-
error.data.report ||= undefined // set to undefined if false
|
|
65
|
-
_filterUndefinedValues(error.data, true)
|
|
61
|
+
let errorId: string | undefined
|
|
66
62
|
|
|
67
|
-
if (sentryService && shouldReportToSentry(
|
|
68
|
-
|
|
63
|
+
if (sentryService && shouldReportToSentry(originalError)) {
|
|
64
|
+
errorId = sentryService.captureException(originalError, false)
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
if (res.headersSent) return
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const httpError = _errorToErrorObject<HttpErrorData>(originalError, includeErrorStack)
|
|
70
|
+
|
|
71
|
+
httpError.data.errorId = errorId
|
|
72
|
+
httpError.data.httpStatusCode ||= 500 // default to 500
|
|
73
|
+
httpError.data.headersSent = headersSent || undefined
|
|
74
|
+
httpError.data.report ||= undefined // set to undefined if false
|
|
75
|
+
_filterUndefinedValues(httpError.data, true)
|
|
76
|
+
|
|
77
|
+
res.status(httpError.data.httpStatusCode).json({
|
|
78
|
+
error: httpError,
|
|
75
79
|
} as HttpErrorResponse)
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
function shouldReportToSentry(err:
|
|
82
|
+
function shouldReportToSentry(err: Error): boolean {
|
|
79
83
|
// Only report 5xx
|
|
80
|
-
return
|
|
84
|
+
return (
|
|
85
|
+
(err as HttpError)?.data?.report ||
|
|
86
|
+
!(err as HttpError)?.data ||
|
|
87
|
+
(err as HttpError).data.httpStatusCode >= 500
|
|
88
|
+
)
|
|
81
89
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
_get,
|
|
3
|
+
_mapValues,
|
|
3
4
|
_mb,
|
|
4
5
|
_ms,
|
|
5
6
|
_percentile,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
_stringMapEntries,
|
|
8
9
|
_stringMapValues,
|
|
9
10
|
_sum,
|
|
11
|
+
NumberStack,
|
|
10
12
|
StringMap,
|
|
11
13
|
} from '@naturalcycles/js-lib'
|
|
12
14
|
import { RequestHandler } from 'express'
|
|
@@ -17,14 +19,14 @@ const { GAE_INSTANCE } = process.env
|
|
|
17
19
|
|
|
18
20
|
// Map from "endpoint" to latency
|
|
19
21
|
interface Stat {
|
|
20
|
-
stack:
|
|
22
|
+
stack: NumberStack
|
|
21
23
|
'2xx': number
|
|
22
24
|
'4xx': number
|
|
23
25
|
'5xx': number
|
|
24
26
|
|
|
25
27
|
// calculated on the fly
|
|
26
28
|
total?: number
|
|
27
|
-
pc?:
|
|
29
|
+
pc?: Record<number, number> // e.g 50 => 123
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const serverStatsMap: StringMap<Stat> = {}
|
|
@@ -49,8 +51,7 @@ export const serverStatsHTMLHandler: RequestHandler = (req, res) => {
|
|
|
49
51
|
// calc things
|
|
50
52
|
_stringMapValues(serverStatsMap).forEach(s => {
|
|
51
53
|
s.total = s['2xx'] + s['4xx'] + s['5xx']
|
|
52
|
-
s.pc =
|
|
53
|
-
percentiles.forEach(pc => (s.pc![pc] = Math.round(_percentile(s.stack.items, pc))))
|
|
54
|
+
s.pc = _mapValues(s.stack.percentiles(percentiles), (_k, v) => Math.round(v), true)
|
|
54
55
|
})
|
|
55
56
|
const allLatencies = _stringMapValues(serverStatsMap).flatMap(s => s.stack.items)
|
|
56
57
|
const all2xx = _sum(_stringMapValues(serverStatsMap).flatMap(s => s['2xx']))
|
|
@@ -121,7 +122,7 @@ export function serverStatsMiddleware(): RequestHandler {
|
|
|
121
122
|
const endpoint = getRequestEndpoint(req)
|
|
122
123
|
|
|
123
124
|
serverStatsMap[endpoint] ||= {
|
|
124
|
-
stack: new
|
|
125
|
+
stack: new NumberStack(SIZE),
|
|
125
126
|
'2xx': 0,
|
|
126
127
|
'4xx': 0,
|
|
127
128
|
'5xx': 0,
|
|
@@ -155,15 +156,3 @@ function getStatusFamily(statusCode: number): '2xx' | '4xx' | '5xx' {
|
|
|
155
156
|
if (statusCode < 500) return '4xx'
|
|
156
157
|
return '5xx'
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
-
class SizeLimitedStack<T> {
|
|
160
|
-
constructor(public size: number) {}
|
|
161
|
-
|
|
162
|
-
index = 0
|
|
163
|
-
items: T[] = []
|
|
164
|
-
|
|
165
|
-
push(item: T): void {
|
|
166
|
-
this.items[this.index] = item
|
|
167
|
-
this.index = this.index === this.size ? 0 : this.index + 1
|
|
168
|
-
}
|
|
169
|
-
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { _since } from '@naturalcycles/js-lib'
|
|
2
2
|
import { boldGrey, dimGrey } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
3
3
|
import { RequestHandler, Response } from 'express'
|
|
4
|
-
import {
|
|
4
|
+
import { onFinished } from '../../index'
|
|
5
5
|
import { logRequest } from '../request.log.util'
|
|
6
6
|
|
|
7
|
+
const { APP_ENV } = process.env
|
|
8
|
+
|
|
7
9
|
export interface SimpleRequestLoggerCfg {
|
|
8
10
|
/**
|
|
9
11
|
* @default false
|
|
@@ -18,7 +20,8 @@ export interface SimpleRequestLoggerCfg {
|
|
|
18
20
|
|
|
19
21
|
export function simpleRequestLogger(_cfg: Partial<SimpleRequestLoggerCfg> = {}): RequestHandler {
|
|
20
22
|
// Disable logger in AppEngine, as it doesn't make sense there
|
|
21
|
-
|
|
23
|
+
// UPD: Only log in dev environment
|
|
24
|
+
if (APP_ENV !== 'dev') return (req, res, next) => next()
|
|
22
25
|
|
|
23
26
|
const cfg: SimpleRequestLoggerCfg = {
|
|
24
27
|
logStart: false,
|
|
@@ -31,7 +34,7 @@ export function simpleRequestLogger(_cfg: Partial<SimpleRequestLoggerCfg> = {}):
|
|
|
31
34
|
const started = Date.now()
|
|
32
35
|
|
|
33
36
|
if (logStart) {
|
|
34
|
-
|
|
37
|
+
req.log(['>>', req.method, boldGrey(req.url)].join(' '))
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
if (logFinish) {
|