@naturalcycles/backend-lib 9.3.0 → 9.4.0
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 +1 -0
- package/dist/index.js +1 -0
- package/dist/server/asyncLocalStorageMiddleware.js +5 -4
- package/dist/server/createDefaultApp.js +2 -2
- package/dist/server/genericErrorMiddleware.d.ts +1 -6
- package/dist/server/genericErrorMiddleware.js +10 -28
- package/dist/server/logMiddleware.d.ts +2 -2
- package/dist/server/logMiddleware.js +32 -29
- package/dist/server/request.log.util.d.ts +1 -1
- package/dist/server/request.log.util.js +1 -1
- package/dist/server/requestLoggerMiddleware.d.ts +7 -0
- package/dist/server/requestLoggerMiddleware.js +19 -0
- package/dist/server/server.model.d.ts +4 -0
- package/dist/server/simpleRequestLoggerMiddleware.d.ts +1 -1
- package/dist/server/simpleRequestLoggerMiddleware.js +4 -20
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/server/asyncLocalStorageMiddleware.ts +5 -4
- package/src/server/createDefaultApp.ts +2 -2
- package/src/server/genericErrorMiddleware.ts +14 -39
- package/src/server/logMiddleware.ts +34 -29
- package/src/server/request.log.util.ts +5 -1
- package/src/server/requestLoggerMiddleware.ts +27 -0
- package/src/server/server.model.ts +4 -0
- package/src/server/simpleRequestLoggerMiddleware.ts +4 -22
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './server/notFoundMiddleware.js';
|
|
|
19
19
|
export * from './server/okMiddleware.js';
|
|
20
20
|
export * from './server/request.log.util.js';
|
|
21
21
|
export * from './server/request.util.js';
|
|
22
|
+
export * from './server/requestLoggerMiddleware.js';
|
|
22
23
|
export * from './server/requestTimeoutMiddleware.js';
|
|
23
24
|
export * from './server/safeJsonMiddleware.js';
|
|
24
25
|
export * from './server/server.model.js';
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export * from './server/notFoundMiddleware.js';
|
|
|
19
19
|
export * from './server/okMiddleware.js';
|
|
20
20
|
export * from './server/request.log.util.js';
|
|
21
21
|
export * from './server/request.util.js';
|
|
22
|
+
export * from './server/requestLoggerMiddleware.js';
|
|
22
23
|
export * from './server/requestTimeoutMiddleware.js';
|
|
23
24
|
export * from './server/safeJsonMiddleware.js';
|
|
24
25
|
export * from './server/server.model.js';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { _lazyValue } from '@naturalcycles/js-lib';
|
|
3
|
-
import { ciLogger, devLogger,
|
|
4
|
-
const { GAE_INSTANCE, CI } = process.env;
|
|
3
|
+
import { ciLogger, devLogger, gcpStructuredLogger } from './logMiddleware.js';
|
|
4
|
+
const { GAE_INSTANCE, K_SERVICE, CI } = process.env;
|
|
5
5
|
const isGAE = !!GAE_INSTANCE;
|
|
6
6
|
const isCI = !!CI;
|
|
7
|
-
const isCloudRun = !!
|
|
7
|
+
const isCloudRun = !!K_SERVICE;
|
|
8
8
|
// Singleton, for simplicity
|
|
9
9
|
// Create it lazily (on demand)
|
|
10
10
|
const storage = _lazyValue(() => new AsyncLocalStorage());
|
|
@@ -25,7 +25,8 @@ export function getRequest() {
|
|
|
25
25
|
* @experimental
|
|
26
26
|
*/
|
|
27
27
|
export function getRequestLogger() {
|
|
28
|
-
return (storage().getStore()?.req ||
|
|
28
|
+
return (storage().getStore()?.req ||
|
|
29
|
+
(isGAE || isCloudRun ? gcpStructuredLogger : isCI ? ciLogger : devLogger));
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* CommonLogger implementation that is Request-bound.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import cookieParser from 'cookie-parser';
|
|
2
2
|
import cors from 'cors';
|
|
3
3
|
import express from 'express';
|
|
4
|
-
import { isCloudRun, isGAE } from '../util.js';
|
|
5
4
|
import { asyncLocalStorageMiddleware } from './asyncLocalStorageMiddleware.js';
|
|
6
5
|
import { genericErrorMiddleware } from './genericErrorMiddleware.js';
|
|
7
6
|
import { logMiddleware } from './logMiddleware.js';
|
|
@@ -10,6 +9,7 @@ import { notFoundMiddleware } from './notFoundMiddleware.js';
|
|
|
10
9
|
import { requestTimeoutMiddleware } from './requestTimeoutMiddleware.js';
|
|
11
10
|
import { simpleRequestLoggerMiddleware } from './simpleRequestLoggerMiddleware.js';
|
|
12
11
|
const isTest = process.env['APP_ENV'] === 'test';
|
|
12
|
+
const isDev = process.env['APP_ENV'] === 'dev';
|
|
13
13
|
export async function createDefaultApp(cfg) {
|
|
14
14
|
const { sentryService } = cfg;
|
|
15
15
|
const app = express();
|
|
@@ -26,7 +26,7 @@ export async function createDefaultApp(cfg) {
|
|
|
26
26
|
app.use(requestTimeoutMiddleware());
|
|
27
27
|
// app.use(serverStatsMiddleware()) // disabled by default
|
|
28
28
|
// app.use(bodyParserTimeout()) // removed by default
|
|
29
|
-
if (
|
|
29
|
+
if (isDev) {
|
|
30
30
|
app.use(simpleRequestLoggerMiddleware());
|
|
31
31
|
}
|
|
32
32
|
// app.use(safeJsonMiddleware()) // optional
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ErrorObject } from '@naturalcycles/js-lib';
|
|
2
2
|
import type { BackendErrorRequestHandler, BackendRequest, BackendResponse } from './server.model.js';
|
|
3
3
|
export interface GenericErrorMiddlewareCfg {
|
|
4
4
|
errorReportingService?: ErrorReportingService;
|
|
5
|
-
/**
|
|
6
|
-
* Defaults to false.
|
|
7
|
-
* So, by default, it will report ALL errors, not only 5xx.
|
|
8
|
-
*/
|
|
9
|
-
reportOnly5xx?: boolean;
|
|
10
5
|
/**
|
|
11
6
|
* Generic hook that can be used to **mutate** errors before they are returned to client.
|
|
12
7
|
* This function does not affect data sent to sentry.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _objectAssign, } from '@naturalcycles/js-lib';
|
|
2
|
+
import { _anyToError, _errorLikeToErrorObject } from '@naturalcycles/js-lib';
|
|
2
3
|
const { APP_ENV } = process.env;
|
|
3
4
|
const includeErrorStack = APP_ENV === 'dev';
|
|
4
|
-
// Hacky way to store the
|
|
5
|
+
// Hacky way to store the errorService, so it's available to `respondWithError` function
|
|
5
6
|
let errorService;
|
|
6
|
-
let reportOnly5xx = false;
|
|
7
7
|
let formatError;
|
|
8
8
|
/**
|
|
9
9
|
* Generic error handler.
|
|
@@ -12,7 +12,6 @@ let formatError;
|
|
|
12
12
|
*/
|
|
13
13
|
export function genericErrorMiddleware(cfg = {}) {
|
|
14
14
|
errorService ||= cfg.errorReportingService;
|
|
15
|
-
reportOnly5xx = cfg.reportOnly5xx || false;
|
|
16
15
|
formatError = cfg.formatError;
|
|
17
16
|
return (err, req, res, _next) => {
|
|
18
17
|
// if (res.headersSent) {
|
|
@@ -31,6 +30,8 @@ export function genericErrorMiddleware(cfg = {}) {
|
|
|
31
30
|
}
|
|
32
31
|
export function respondWithError(req, res, err) {
|
|
33
32
|
const { headersSent } = res;
|
|
33
|
+
// todo: add endpoint to the log
|
|
34
|
+
// todo: add userId from the "Context" (or, just req.userId?) to the log
|
|
34
35
|
if (headersSent) {
|
|
35
36
|
req.error(`error after headersSent:`, err);
|
|
36
37
|
}
|
|
@@ -38,38 +39,19 @@ export function respondWithError(req, res, err) {
|
|
|
38
39
|
req.error(err);
|
|
39
40
|
}
|
|
40
41
|
const originalError = _anyToError(err);
|
|
41
|
-
|
|
42
|
-
const shouldReport = errorService && shouldReportToSentry(originalError);
|
|
43
|
-
if (shouldReport) {
|
|
44
|
-
errorId = errorService?.captureException(originalError);
|
|
45
|
-
}
|
|
42
|
+
const errorId = errorService?.captureException(originalError);
|
|
46
43
|
if (headersSent)
|
|
47
44
|
return;
|
|
48
45
|
const httpError = _errorLikeToErrorObject(originalError);
|
|
49
46
|
if (!includeErrorStack)
|
|
50
47
|
delete httpError.stack;
|
|
51
|
-
httpError.data.errorId = errorId;
|
|
52
48
|
httpError.data.backendResponseStatusCode ||= 500; // default to 500
|
|
53
|
-
httpError.data
|
|
54
|
-
|
|
49
|
+
_objectAssign(httpError.data, {
|
|
50
|
+
errorId,
|
|
51
|
+
headersSent: headersSent || undefined,
|
|
52
|
+
});
|
|
55
53
|
formatError?.(httpError); // Mutates
|
|
56
54
|
res.status(httpError.data.backendResponseStatusCode).json({
|
|
57
55
|
error: httpError,
|
|
58
56
|
});
|
|
59
57
|
}
|
|
60
|
-
function shouldReportToSentry(err) {
|
|
61
|
-
const e = err;
|
|
62
|
-
// By default - report
|
|
63
|
-
if (!e?.data)
|
|
64
|
-
return true;
|
|
65
|
-
// If `report` is set - do as it says
|
|
66
|
-
if (e.data.report === true)
|
|
67
|
-
return true;
|
|
68
|
-
if (e.data.report === false)
|
|
69
|
-
return false;
|
|
70
|
-
// Report if http 5xx, otherwise not
|
|
71
|
-
// If no httpCode - report
|
|
72
|
-
// if httpCode >= 500 - report
|
|
73
|
-
// Otherwise - report, unless !reportOnly5xx is set
|
|
74
|
-
return (!reportOnly5xx || !e.data.backendResponseStatusCode || e.data.backendResponseStatusCode >= 500);
|
|
75
|
-
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { CommonLogger } from '@naturalcycles/js-lib';
|
|
2
2
|
import type { BackendRequestHandler } from './server.model.js';
|
|
3
3
|
/**
|
|
4
|
-
* Logger that logs in
|
|
4
|
+
* Logger that logs in "GCP structured log" format.
|
|
5
5
|
* To be used in outside-of-request situations (otherwise req.log should be used).
|
|
6
6
|
*/
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const gcpStructuredLogger: CommonLogger;
|
|
8
8
|
/**
|
|
9
9
|
* Fancy development logger, to be used in outside-of-request situations
|
|
10
10
|
* (otherwise req.log should be used).
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
2
|
import { _inspect, dimGrey } from '@naturalcycles/nodejs-lib';
|
|
3
|
-
const { GOOGLE_CLOUD_PROJECT, GAE_INSTANCE, APP_ENV } = process.env;
|
|
3
|
+
const { GOOGLE_CLOUD_PROJECT, GAE_INSTANCE, K_SERVICE, APP_ENV } = process.env;
|
|
4
4
|
const isGAE = !!GAE_INSTANCE;
|
|
5
|
-
|
|
5
|
+
const isCloudRun = !!K_SERVICE;
|
|
6
6
|
// const isTest = APP_ENV === 'test'
|
|
7
7
|
const isDev = APP_ENV === 'dev';
|
|
8
8
|
// Simple "request counter" (poor man's "correlation id") counter, to use on dev machine (not in the cloud)
|
|
9
9
|
let reqCounter = 0;
|
|
10
10
|
/**
|
|
11
|
-
* Logger that logs in
|
|
11
|
+
* Logger that logs in "GCP structured log" format.
|
|
12
12
|
* To be used in outside-of-request situations (otherwise req.log should be used).
|
|
13
13
|
*/
|
|
14
|
-
export const
|
|
15
|
-
log: (...args) =>
|
|
16
|
-
warn: (...args) =>
|
|
17
|
-
error: (...args) =>
|
|
14
|
+
export const gcpStructuredLogger = {
|
|
15
|
+
log: (...args) => writeGCPStructuredLog({}, args),
|
|
16
|
+
warn: (...args) => writeGCPStructuredLog({ severity: 'WARNING' }, args),
|
|
17
|
+
error: (...args) => writeGCPStructuredLog({ severity: 'ERROR' }, args),
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* Fancy development logger, to be used in outside-of-request situations
|
|
@@ -34,7 +34,8 @@ export const ciLogger = {
|
|
|
34
34
|
error: (...args) => logToCI(args),
|
|
35
35
|
};
|
|
36
36
|
// Documented here: https://cloud.google.com/logging/docs/structured-logging
|
|
37
|
-
|
|
37
|
+
// Cloud Run logging: https://cloud.google.com/run/docs/logging
|
|
38
|
+
function writeGCPStructuredLog(meta, args) {
|
|
38
39
|
console.log(JSON.stringify({
|
|
39
40
|
message: args.map(a => (typeof a === 'string' ? a : inspect(a))).join(' '),
|
|
40
41
|
...meta,
|
|
@@ -55,31 +56,33 @@ function logToCI(args) {
|
|
|
55
56
|
console.log(args.map(a => _inspect(a, { includeErrorStack: true, colors: false })).join(' '));
|
|
56
57
|
}
|
|
57
58
|
export function logMiddleware() {
|
|
58
|
-
if (isGAE) {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
if (isGAE || isCloudRun) {
|
|
60
|
+
return function gcpStructuredLogHandler(req, _res, next) {
|
|
61
|
+
const meta = {
|
|
62
|
+
// Experimental!
|
|
63
|
+
// Testing to include userId in metadata (not message payload) to see if it's searchable
|
|
64
|
+
userId: req.userId,
|
|
65
|
+
};
|
|
66
|
+
// CloudRun does NOT have this env variable set,
|
|
67
|
+
// so you have to set it manually on deployment, like this:
|
|
68
|
+
// gcloud run deploy my-service \
|
|
69
|
+
// --update-env-vars=GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
|
|
70
|
+
if (GOOGLE_CLOUD_PROJECT) {
|
|
61
71
|
const traceHeader = req.header('x-cloud-trace-context');
|
|
62
72
|
if (traceHeader) {
|
|
63
73
|
const [trace] = traceHeader.split('/');
|
|
64
|
-
|
|
65
|
-
'logging.googleapis.com/trace': `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`,
|
|
66
|
-
'appengine.googleapis.com/request_id': req.header('x-appengine-request-log-id'),
|
|
67
|
-
};
|
|
68
|
-
Object.assign(req, {
|
|
69
|
-
log: (...args) => logToAppEngine({ ...meta, severity: 'INFO' }, args),
|
|
70
|
-
warn: (...args) => logToAppEngine({ ...meta, severity: 'WARNING' }, args),
|
|
71
|
-
error: (...args) => logToAppEngine({ ...meta, severity: 'ERROR' }, args),
|
|
72
|
-
});
|
|
74
|
+
meta['logging.googleapis.com/trace'] = `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`;
|
|
73
75
|
req.requestId = trace;
|
|
74
76
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
}
|
|
78
|
+
if (isGAE) {
|
|
79
|
+
meta['appengine.googleapis.com/request_id'] = req.header('x-appengine-request-log-id');
|
|
80
|
+
}
|
|
81
|
+
Object.assign(req, {
|
|
82
|
+
log: (...args) => writeGCPStructuredLog({ ...meta, severity: 'INFO' }, args),
|
|
83
|
+
warn: (...args) => writeGCPStructuredLog({ ...meta, severity: 'WARNING' }, args),
|
|
84
|
+
error: (...args) => writeGCPStructuredLog({ ...meta, severity: 'ERROR' }, args),
|
|
85
|
+
});
|
|
83
86
|
next();
|
|
84
87
|
};
|
|
85
88
|
}
|
|
@@ -93,7 +96,7 @@ export function logMiddleware() {
|
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
98
|
// Otherwise, return "simple" logger
|
|
96
|
-
// This includes: unit tests,
|
|
99
|
+
// This includes: unit tests, CI environments
|
|
97
100
|
return function simpleLogHandler(req, _res, next) {
|
|
98
101
|
req.log = req.warn = req.error = (...args) => logToCI(args);
|
|
99
102
|
next();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BackendRequest } from './server.model.js';
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function logRequestWithColors(req: BackendRequest, statusCode: number, ...tokens: any[]): void;
|
|
3
3
|
export declare function coloredHttpCode(statusCode: number): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { boldGrey, green, red, yellow } from '@naturalcycles/nodejs-lib';
|
|
2
|
-
export function
|
|
2
|
+
export function logRequestWithColors(req, statusCode, ...tokens) {
|
|
3
3
|
req[logLevel(statusCode)]([coloredHttpCode(statusCode), req.method, boldGrey(req.url), ...tokens].join(' '));
|
|
4
4
|
}
|
|
5
5
|
export function coloredHttpCode(statusCode) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { _since } from '@naturalcycles/js-lib';
|
|
2
|
+
import { onFinished } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Experimental request logger for Cloud Run.
|
|
5
|
+
*
|
|
6
|
+
* @experimental
|
|
7
|
+
*/
|
|
8
|
+
export function requestLoggerMiddleware() {
|
|
9
|
+
return (req, res, next) => {
|
|
10
|
+
const started = Date.now();
|
|
11
|
+
req.log([req.method, req.originalUrl, req.userId].filter(Boolean).join(' '));
|
|
12
|
+
onFinished(res, () => {
|
|
13
|
+
req.log([res.statusCode || '0', _since(started), req.method, req.originalUrl, req.userId]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(' '));
|
|
16
|
+
});
|
|
17
|
+
next();
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -12,6 +12,10 @@ export interface BackendRequest extends Request {
|
|
|
12
12
|
warn: CommonLogFunction;
|
|
13
13
|
error: CommonLogFunction;
|
|
14
14
|
requestId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Only used for request logging purposes.
|
|
17
|
+
*/
|
|
18
|
+
userId?: string;
|
|
15
19
|
/**
|
|
16
20
|
* Set by requestTimeoutMiddleware.
|
|
17
21
|
* Can be used to cancel/override the timeout.
|
|
@@ -9,4 +9,4 @@ export interface SimpleRequestLoggerMiddlewareCfg {
|
|
|
9
9
|
*/
|
|
10
10
|
logFinish: boolean;
|
|
11
11
|
}
|
|
12
|
-
export declare function simpleRequestLoggerMiddleware(
|
|
12
|
+
export declare function simpleRequestLoggerMiddleware(cfg?: Partial<SimpleRequestLoggerMiddlewareCfg>): BackendRequestHandler;
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { _since } from '@naturalcycles/js-lib';
|
|
2
2
|
import { boldGrey, dimGrey } from '@naturalcycles/nodejs-lib';
|
|
3
3
|
import { onFinished } from '../index.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// Disable logger in AppEngine, as it doesn't make sense there
|
|
8
|
-
// UPD: Only log in dev environment
|
|
9
|
-
if (APP_ENV !== 'dev')
|
|
10
|
-
return (_req, _res, next) => next();
|
|
11
|
-
const cfg = {
|
|
12
|
-
logStart: false,
|
|
13
|
-
logFinish: true,
|
|
14
|
-
..._cfg,
|
|
15
|
-
};
|
|
16
|
-
const { logStart, logFinish } = cfg;
|
|
4
|
+
import { logRequestWithColors } from './request.log.util.js';
|
|
5
|
+
export function simpleRequestLoggerMiddleware(cfg = {}) {
|
|
6
|
+
const { logStart = false, logFinish = true } = cfg;
|
|
17
7
|
return (req, res, next) => {
|
|
18
8
|
const started = Date.now();
|
|
19
9
|
if (logStart) {
|
|
@@ -21,13 +11,7 @@ export function simpleRequestLoggerMiddleware(_cfg = {}) {
|
|
|
21
11
|
}
|
|
22
12
|
if (logFinish) {
|
|
23
13
|
onFinished(res, () => {
|
|
24
|
-
|
|
25
|
-
// Avoid logging twice. It was previously logged by genericErrorHandler
|
|
26
|
-
// if (res.__err) {
|
|
27
|
-
// logRequest(req, res.statusCode, dimGrey(_since(started)), _inspect(res.__err))
|
|
28
|
-
// } else {
|
|
29
|
-
// logRequest(req, res.statusCode, dimGrey(_since(started)))
|
|
30
|
-
// }
|
|
14
|
+
logRequestWithColors(req, res.statusCode, dimGrey(_since(started)));
|
|
31
15
|
});
|
|
32
16
|
}
|
|
33
17
|
next();
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './server/notFoundMiddleware.js'
|
|
|
19
19
|
export * from './server/okMiddleware.js'
|
|
20
20
|
export * from './server/request.log.util.js'
|
|
21
21
|
export * from './server/request.util.js'
|
|
22
|
+
export * from './server/requestLoggerMiddleware.js'
|
|
22
23
|
export * from './server/requestTimeoutMiddleware.js'
|
|
23
24
|
export * from './server/safeJsonMiddleware.js'
|
|
24
25
|
export * from './server/server.model.js'
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
2
|
import type { CommonLogger } from '@naturalcycles/js-lib'
|
|
3
3
|
import { _lazyValue } from '@naturalcycles/js-lib'
|
|
4
|
-
import { ciLogger, devLogger,
|
|
4
|
+
import { ciLogger, devLogger, gcpStructuredLogger } from './logMiddleware.js'
|
|
5
5
|
import type { BackendRequest, BackendRequestHandler } from './server.model.js'
|
|
6
6
|
|
|
7
|
-
const { GAE_INSTANCE, CI } = process.env
|
|
7
|
+
const { GAE_INSTANCE, K_SERVICE, CI } = process.env
|
|
8
8
|
const isGAE = !!GAE_INSTANCE
|
|
9
9
|
const isCI = !!CI
|
|
10
|
-
const isCloudRun = !!
|
|
10
|
+
const isCloudRun = !!K_SERVICE
|
|
11
11
|
|
|
12
12
|
export interface RequestLocalStorage {
|
|
13
13
|
req: BackendRequest
|
|
@@ -38,7 +38,8 @@ export function getRequest(): BackendRequest | undefined {
|
|
|
38
38
|
*/
|
|
39
39
|
export function getRequestLogger(): CommonLogger {
|
|
40
40
|
return (
|
|
41
|
-
storage().getStore()?.req ||
|
|
41
|
+
storage().getStore()?.req ||
|
|
42
|
+
(isGAE || isCloudRun ? gcpStructuredLogger : isCI ? ciLogger : devLogger)
|
|
42
43
|
)
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import cookieParser from 'cookie-parser'
|
|
2
2
|
import cors from 'cors'
|
|
3
3
|
import express from 'express'
|
|
4
|
-
import { isCloudRun, isGAE } from '../util.js'
|
|
5
4
|
import { asyncLocalStorageMiddleware } from './asyncLocalStorageMiddleware.js'
|
|
6
5
|
import type {
|
|
7
6
|
BackendRequestHandlerCfg,
|
|
@@ -17,6 +16,7 @@ import type { BackendApplication } from './server.model.js'
|
|
|
17
16
|
import { simpleRequestLoggerMiddleware } from './simpleRequestLoggerMiddleware.js'
|
|
18
17
|
|
|
19
18
|
const isTest = process.env['APP_ENV'] === 'test'
|
|
19
|
+
const isDev = process.env['APP_ENV'] === 'dev'
|
|
20
20
|
|
|
21
21
|
export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendApplication> {
|
|
22
22
|
const { sentryService } = cfg
|
|
@@ -41,7 +41,7 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
|
|
|
41
41
|
// app.use(serverStatsMiddleware()) // disabled by default
|
|
42
42
|
// app.use(bodyParserTimeout()) // removed by default
|
|
43
43
|
|
|
44
|
-
if (
|
|
44
|
+
if (isDev) {
|
|
45
45
|
app.use(simpleRequestLoggerMiddleware())
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
_objectAssign,
|
|
3
|
+
type BackendErrorResponseObject,
|
|
4
|
+
type ErrorObject,
|
|
5
|
+
} from '@naturalcycles/js-lib'
|
|
6
|
+
import { _anyToError, _errorLikeToErrorObject } from '@naturalcycles/js-lib'
|
|
3
7
|
import type { BackendErrorRequestHandler, BackendRequest, BackendResponse } from './server.model.js'
|
|
4
8
|
|
|
5
9
|
export interface GenericErrorMiddlewareCfg {
|
|
6
10
|
errorReportingService?: ErrorReportingService
|
|
7
11
|
|
|
8
|
-
/**
|
|
9
|
-
* Defaults to false.
|
|
10
|
-
* So, by default, it will report ALL errors, not only 5xx.
|
|
11
|
-
*/
|
|
12
|
-
reportOnly5xx?: boolean
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
13
|
* Generic hook that can be used to **mutate** errors before they are returned to client.
|
|
16
14
|
* This function does not affect data sent to sentry.
|
|
@@ -34,9 +32,8 @@ export interface ErrorReportingService {
|
|
|
34
32
|
const { APP_ENV } = process.env
|
|
35
33
|
const includeErrorStack = APP_ENV === 'dev'
|
|
36
34
|
|
|
37
|
-
// Hacky way to store the
|
|
35
|
+
// Hacky way to store the errorService, so it's available to `respondWithError` function
|
|
38
36
|
let errorService: ErrorReportingService | undefined
|
|
39
|
-
let reportOnly5xx = false
|
|
40
37
|
let formatError: GenericErrorMiddlewareCfg['formatError']
|
|
41
38
|
|
|
42
39
|
/**
|
|
@@ -48,7 +45,6 @@ export function genericErrorMiddleware(
|
|
|
48
45
|
cfg: GenericErrorMiddlewareCfg = {},
|
|
49
46
|
): BackendErrorRequestHandler {
|
|
50
47
|
errorService ||= cfg.errorReportingService
|
|
51
|
-
reportOnly5xx = cfg.reportOnly5xx || false
|
|
52
48
|
formatError = cfg.formatError
|
|
53
49
|
|
|
54
50
|
return (err, req, res, _next) => {
|
|
@@ -71,6 +67,8 @@ export function genericErrorMiddleware(
|
|
|
71
67
|
export function respondWithError(req: BackendRequest, res: BackendResponse, err: any): void {
|
|
72
68
|
const { headersSent } = res
|
|
73
69
|
|
|
70
|
+
// todo: add endpoint to the log
|
|
71
|
+
// todo: add userId from the "Context" (or, just req.userId?) to the log
|
|
74
72
|
if (headersSent) {
|
|
75
73
|
req.error(`error after headersSent:`, err)
|
|
76
74
|
} else {
|
|
@@ -79,22 +77,18 @@ export function respondWithError(req: BackendRequest, res: BackendResponse, err:
|
|
|
79
77
|
|
|
80
78
|
const originalError = _anyToError(err)
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const shouldReport = errorService && shouldReportToSentry(originalError)
|
|
85
|
-
if (shouldReport) {
|
|
86
|
-
errorId = errorService?.captureException(originalError)
|
|
87
|
-
}
|
|
80
|
+
const errorId = errorService?.captureException(originalError)
|
|
88
81
|
|
|
89
82
|
if (headersSent) return
|
|
90
83
|
|
|
91
84
|
const httpError = _errorLikeToErrorObject(originalError)
|
|
92
85
|
if (!includeErrorStack) delete httpError.stack
|
|
93
86
|
|
|
94
|
-
httpError.data.errorId = errorId
|
|
95
87
|
httpError.data.backendResponseStatusCode ||= 500 // default to 500
|
|
96
|
-
httpError.data
|
|
97
|
-
|
|
88
|
+
_objectAssign(httpError.data, {
|
|
89
|
+
errorId,
|
|
90
|
+
headersSent: headersSent || undefined,
|
|
91
|
+
})
|
|
98
92
|
|
|
99
93
|
formatError?.(httpError) // Mutates
|
|
100
94
|
|
|
@@ -102,22 +96,3 @@ export function respondWithError(req: BackendRequest, res: BackendResponse, err:
|
|
|
102
96
|
error: httpError,
|
|
103
97
|
} satisfies BackendErrorResponseObject)
|
|
104
98
|
}
|
|
105
|
-
|
|
106
|
-
function shouldReportToSentry(err: Error): boolean {
|
|
107
|
-
const e = err as AppError
|
|
108
|
-
|
|
109
|
-
// By default - report
|
|
110
|
-
if (!e?.data) return true
|
|
111
|
-
|
|
112
|
-
// If `report` is set - do as it says
|
|
113
|
-
if (e.data.report === true) return true
|
|
114
|
-
if (e.data.report === false) return false
|
|
115
|
-
|
|
116
|
-
// Report if http 5xx, otherwise not
|
|
117
|
-
// If no httpCode - report
|
|
118
|
-
// if httpCode >= 500 - report
|
|
119
|
-
// Otherwise - report, unless !reportOnly5xx is set
|
|
120
|
-
return (
|
|
121
|
-
!reportOnly5xx || !e.data.backendResponseStatusCode || e.data.backendResponseStatusCode >= 500
|
|
122
|
-
)
|
|
123
|
-
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { inspect } from 'node:util'
|
|
2
|
-
import type { AnyObject, CommonLogger } from '@naturalcycles/js-lib'
|
|
2
|
+
import type { AnyObject, CommonLogger, StringMap } from '@naturalcycles/js-lib'
|
|
3
3
|
import { _inspect, dimGrey } from '@naturalcycles/nodejs-lib'
|
|
4
4
|
import type { BackendRequestHandler } from './server.model.js'
|
|
5
5
|
|
|
6
|
-
const { GOOGLE_CLOUD_PROJECT, GAE_INSTANCE, APP_ENV } = process.env
|
|
6
|
+
const { GOOGLE_CLOUD_PROJECT, GAE_INSTANCE, K_SERVICE, APP_ENV } = process.env
|
|
7
7
|
const isGAE = !!GAE_INSTANCE
|
|
8
|
-
|
|
8
|
+
const isCloudRun = !!K_SERVICE
|
|
9
9
|
// const isTest = APP_ENV === 'test'
|
|
10
10
|
const isDev = APP_ENV === 'dev'
|
|
11
11
|
|
|
@@ -13,13 +13,13 @@ const isDev = APP_ENV === 'dev'
|
|
|
13
13
|
let reqCounter = 0
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Logger that logs in
|
|
16
|
+
* Logger that logs in "GCP structured log" format.
|
|
17
17
|
* To be used in outside-of-request situations (otherwise req.log should be used).
|
|
18
18
|
*/
|
|
19
|
-
export const
|
|
20
|
-
log: (...args) =>
|
|
21
|
-
warn: (...args) =>
|
|
22
|
-
error: (...args) =>
|
|
19
|
+
export const gcpStructuredLogger: CommonLogger = {
|
|
20
|
+
log: (...args) => writeGCPStructuredLog({}, args),
|
|
21
|
+
warn: (...args) => writeGCPStructuredLog({ severity: 'WARNING' }, args),
|
|
22
|
+
error: (...args) => writeGCPStructuredLog({ severity: 'ERROR' }, args),
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -42,7 +42,8 @@ export const ciLogger: CommonLogger = {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Documented here: https://cloud.google.com/logging/docs/structured-logging
|
|
45
|
-
|
|
45
|
+
// Cloud Run logging: https://cloud.google.com/run/docs/logging
|
|
46
|
+
function writeGCPStructuredLog(meta: AnyObject, args: any[]): void {
|
|
46
47
|
console.log(
|
|
47
48
|
JSON.stringify({
|
|
48
49
|
message: args.map(a => (typeof a === 'string' ? a : inspect(a))).join(' '),
|
|
@@ -70,32 +71,36 @@ function logToCI(args: any[]): void {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export function logMiddleware(): BackendRequestHandler {
|
|
73
|
-
if (isGAE) {
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
if (isGAE || isCloudRun) {
|
|
75
|
+
return function gcpStructuredLogHandler(req, _res, next) {
|
|
76
|
+
const meta: StringMap = {
|
|
77
|
+
// Experimental!
|
|
78
|
+
// Testing to include userId in metadata (not message payload) to see if it's searchable
|
|
79
|
+
userId: req.userId,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// CloudRun does NOT have this env variable set,
|
|
83
|
+
// so you have to set it manually on deployment, like this:
|
|
84
|
+
// gcloud run deploy my-service \
|
|
85
|
+
// --update-env-vars=GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
|
|
86
|
+
if (GOOGLE_CLOUD_PROJECT) {
|
|
76
87
|
const traceHeader = req.header('x-cloud-trace-context')
|
|
77
88
|
if (traceHeader) {
|
|
78
89
|
const [trace] = traceHeader.split('/')
|
|
79
|
-
|
|
80
|
-
'logging.googleapis.com/trace': `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`,
|
|
81
|
-
'appengine.googleapis.com/request_id': req.header('x-appengine-request-log-id'),
|
|
82
|
-
}
|
|
83
|
-
Object.assign(req, {
|
|
84
|
-
log: (...args: any[]) => logToAppEngine({ ...meta, severity: 'INFO' }, args),
|
|
85
|
-
warn: (...args: any[]) => logToAppEngine({ ...meta, severity: 'WARNING' }, args),
|
|
86
|
-
error: (...args: any[]) => logToAppEngine({ ...meta, severity: 'ERROR' }, args),
|
|
87
|
-
})
|
|
90
|
+
meta['logging.googleapis.com/trace'] = `projects/${GOOGLE_CLOUD_PROJECT}/traces/${trace}`
|
|
88
91
|
req.requestId = trace
|
|
89
|
-
} else {
|
|
90
|
-
Object.assign(req, gaeLogger)
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
-
next()
|
|
94
93
|
}
|
|
95
|
-
|
|
94
|
+
if (isGAE) {
|
|
95
|
+
meta['appengine.googleapis.com/request_id'] = req.header('x-appengine-request-log-id')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Object.assign(req, {
|
|
99
|
+
log: (...args: any[]) => writeGCPStructuredLog({ ...meta, severity: 'INFO' }, args),
|
|
100
|
+
warn: (...args: any[]) => writeGCPStructuredLog({ ...meta, severity: 'WARNING' }, args),
|
|
101
|
+
error: (...args: any[]) => writeGCPStructuredLog({ ...meta, severity: 'ERROR' }, args),
|
|
102
|
+
})
|
|
96
103
|
|
|
97
|
-
return function appEngineLogHandler(req, _res, next) {
|
|
98
|
-
Object.assign(req, gaeLogger)
|
|
99
104
|
next()
|
|
100
105
|
}
|
|
101
106
|
}
|
|
@@ -111,7 +116,7 @@ export function logMiddleware(): BackendRequestHandler {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
// Otherwise, return "simple" logger
|
|
114
|
-
// This includes: unit tests,
|
|
119
|
+
// This includes: unit tests, CI environments
|
|
115
120
|
return function simpleLogHandler(req, _res, next) {
|
|
116
121
|
req.log = req.warn = req.error = (...args: any[]) => logToCI(args)
|
|
117
122
|
next()
|
|
@@ -2,7 +2,11 @@ import type { CommonLogLevel } from '@naturalcycles/js-lib'
|
|
|
2
2
|
import { boldGrey, green, red, yellow } from '@naturalcycles/nodejs-lib'
|
|
3
3
|
import type { BackendRequest } from './server.model.js'
|
|
4
4
|
|
|
5
|
-
export function
|
|
5
|
+
export function logRequestWithColors(
|
|
6
|
+
req: BackendRequest,
|
|
7
|
+
statusCode: number,
|
|
8
|
+
...tokens: any[]
|
|
9
|
+
): void {
|
|
6
10
|
req[logLevel(statusCode)](
|
|
7
11
|
[coloredHttpCode(statusCode), req.method, boldGrey(req.url), ...tokens].join(' '),
|
|
8
12
|
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { UnixTimestampMillis } from '@naturalcycles/js-lib'
|
|
2
|
+
import { _since } from '@naturalcycles/js-lib'
|
|
3
|
+
import type { BackendRequestHandler } from '../index.js'
|
|
4
|
+
import { onFinished } from '../index.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Experimental request logger for Cloud Run.
|
|
8
|
+
*
|
|
9
|
+
* @experimental
|
|
10
|
+
*/
|
|
11
|
+
export function requestLoggerMiddleware(): BackendRequestHandler {
|
|
12
|
+
return (req, res, next) => {
|
|
13
|
+
const started = Date.now() as UnixTimestampMillis
|
|
14
|
+
|
|
15
|
+
req.log([req.method, req.originalUrl, req.userId].filter(Boolean).join(' '))
|
|
16
|
+
|
|
17
|
+
onFinished(res, () => {
|
|
18
|
+
req.log(
|
|
19
|
+
[res.statusCode || '0', _since(started), req.method, req.originalUrl, req.userId]
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join(' '),
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
next()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -3,9 +3,7 @@ import { _since } from '@naturalcycles/js-lib'
|
|
|
3
3
|
import { boldGrey, dimGrey } from '@naturalcycles/nodejs-lib'
|
|
4
4
|
import type { BackendRequestHandler } from '../index.js'
|
|
5
5
|
import { onFinished } from '../index.js'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const { APP_ENV } = process.env
|
|
6
|
+
import { logRequestWithColors } from './request.log.util.js'
|
|
9
7
|
|
|
10
8
|
export interface SimpleRequestLoggerMiddlewareCfg {
|
|
11
9
|
/**
|
|
@@ -20,18 +18,9 @@ export interface SimpleRequestLoggerMiddlewareCfg {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export function simpleRequestLoggerMiddleware(
|
|
23
|
-
|
|
21
|
+
cfg: Partial<SimpleRequestLoggerMiddlewareCfg> = {},
|
|
24
22
|
): BackendRequestHandler {
|
|
25
|
-
|
|
26
|
-
// UPD: Only log in dev environment
|
|
27
|
-
if (APP_ENV !== 'dev') return (_req, _res, next) => next()
|
|
28
|
-
|
|
29
|
-
const cfg: SimpleRequestLoggerMiddlewareCfg = {
|
|
30
|
-
logStart: false,
|
|
31
|
-
logFinish: true,
|
|
32
|
-
..._cfg,
|
|
33
|
-
}
|
|
34
|
-
const { logStart, logFinish } = cfg
|
|
23
|
+
const { logStart = false, logFinish = true } = cfg
|
|
35
24
|
|
|
36
25
|
return (req, res, next) => {
|
|
37
26
|
const started = Date.now() as UnixTimestampMillis
|
|
@@ -42,14 +31,7 @@ export function simpleRequestLoggerMiddleware(
|
|
|
42
31
|
|
|
43
32
|
if (logFinish) {
|
|
44
33
|
onFinished(res, () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Avoid logging twice. It was previously logged by genericErrorHandler
|
|
48
|
-
// if (res.__err) {
|
|
49
|
-
// logRequest(req, res.statusCode, dimGrey(_since(started)), _inspect(res.__err))
|
|
50
|
-
// } else {
|
|
51
|
-
// logRequest(req, res.statusCode, dimGrey(_since(started)))
|
|
52
|
-
// }
|
|
34
|
+
logRequestWithColors(req, res.statusCode, dimGrey(_since(started)))
|
|
53
35
|
})
|
|
54
36
|
}
|
|
55
37
|
|