@naturalcycles/backend-lib 9.73.0 → 9.74.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/admin/base.admin.service.js +5 -2
- package/dist/express/createDefaultApp.js +0 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/sentry/sentry.shared.service.js +14 -15
- package/dist/server/asyncLocalStorageMiddleware.js +2 -6
- package/dist/server/genericErrorMiddleware.js +2 -2
- package/dist/server/logMiddleware.js +25 -66
- package/dist/server/notFoundMiddleware.js +4 -1
- package/dist/server/server.model.d.ts +3 -11
- package/package.json +1 -1
- package/src/admin/base.admin.service.ts +5 -5
- package/src/express/createDefaultApp.ts +0 -2
- package/src/index.ts +0 -1
- package/src/sentry/sentry.shared.service.ts +15 -17
- package/src/server/asyncLocalStorageMiddleware.ts +4 -6
- package/src/server/genericErrorMiddleware.ts +2 -2
- package/src/server/logMiddleware.ts +35 -69
- package/src/server/notFoundMiddleware.ts +4 -1
- package/src/server/server.model.ts +3 -13
- package/dist/server/methodOverrideMiddleware.d.ts +0 -8
- package/dist/server/methodOverrideMiddleware.js +0 -13
- package/src/server/methodOverrideMiddleware.ts +0 -26
|
@@ -168,7 +168,7 @@ export class BaseAdminService {
|
|
|
168
168
|
err?.code?.includes('id-token-expired')) {
|
|
169
169
|
return; // skip logging, expected error
|
|
170
170
|
}
|
|
171
|
-
req.error(`getEmailByToken error:`, err);
|
|
171
|
+
req.error({ msg: `getEmailByToken error:`, err });
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
async getFirebaseAuth() {
|
|
@@ -191,7 +191,10 @@ export class BaseAdminService {
|
|
|
191
191
|
*/
|
|
192
192
|
// oxlint-disable-next-line max-params
|
|
193
193
|
async onPermissionCheck(req, email, reqPermissions, required, granted, meta = {}) {
|
|
194
|
-
req.log(
|
|
194
|
+
req.log({
|
|
195
|
+
...meta,
|
|
196
|
+
msg: `${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(reqPermissions.join(', '))}]: ${granted ? green('GRANTED') : red('DENIED')}`,
|
|
197
|
+
});
|
|
195
198
|
}
|
|
196
199
|
/**
|
|
197
200
|
* Install it on POST /admin/login url
|
|
@@ -2,7 +2,6 @@ import { asyncLocalStorageMiddleware } from '../server/asyncLocalStorageMiddlewa
|
|
|
2
2
|
import { compressionMiddleware } from '../server/compressionMiddleware.js';
|
|
3
3
|
import { genericErrorMiddleware } from '../server/genericErrorMiddleware.js';
|
|
4
4
|
import { logMiddleware } from '../server/logMiddleware.js';
|
|
5
|
-
import { methodOverrideMiddleware } from '../server/methodOverrideMiddleware.js';
|
|
6
5
|
import { notFoundMiddleware } from '../server/notFoundMiddleware.js';
|
|
7
6
|
import { requestTimeoutMiddleware } from '../server/requestTimeoutMiddleware.js';
|
|
8
7
|
import { simpleRequestLoggerMiddleware } from '../server/simpleRequestLoggerMiddleware.js';
|
|
@@ -24,7 +23,6 @@ export async function createDefaultApp(cfg) {
|
|
|
24
23
|
if (!isTest) {
|
|
25
24
|
app.use(asyncLocalStorageMiddleware());
|
|
26
25
|
}
|
|
27
|
-
app.use(methodOverrideMiddleware());
|
|
28
26
|
app.use(requestTimeoutMiddleware());
|
|
29
27
|
// app.use(serverStatsMiddleware()) // disabled by default
|
|
30
28
|
// app.use(bodyParserTimeout()) // removed by default
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ export * from './server/compressionMiddleware.js';
|
|
|
7
7
|
export * from './server/eventLoop.util.js';
|
|
8
8
|
export * from './server/genericErrorMiddleware.js';
|
|
9
9
|
export * from './server/logMiddleware.js';
|
|
10
|
-
export * from './server/methodOverrideMiddleware.js';
|
|
11
10
|
export * from './server/notFoundMiddleware.js';
|
|
12
11
|
export * from './server/okMiddleware.js';
|
|
13
12
|
export * from './server/request.log.util.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@ export * from './server/compressionMiddleware.js';
|
|
|
7
7
|
export * from './server/eventLoop.util.js';
|
|
8
8
|
export * from './server/genericErrorMiddleware.js';
|
|
9
9
|
export * from './server/logMiddleware.js';
|
|
10
|
-
export * from './server/methodOverrideMiddleware.js';
|
|
11
10
|
export * from './server/notFoundMiddleware.js';
|
|
12
11
|
export * from './server/okMiddleware.js';
|
|
13
12
|
export * from './server/request.log.util.js';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { _anyToError, _isErrorObject } from '@naturalcycles/js-lib/error';
|
|
2
|
-
import {
|
|
2
|
+
import { commonLoggerCreate } from '@naturalcycles/js-lib/log';
|
|
3
|
+
import { _omit } from '@naturalcycles/js-lib/object/object.util.js';
|
|
4
|
+
import { _inspect, logEntryToString } from '@naturalcycles/nodejs-lib';
|
|
3
5
|
import { getRequestLogger } from '../server/asyncLocalStorageMiddleware.js';
|
|
4
6
|
const sentrySeverityMap = {
|
|
5
7
|
debug: 'log',
|
|
@@ -60,7 +62,7 @@ export class SentrySharedService {
|
|
|
60
62
|
const data = _isErrorObject(err) ? err.data : undefined;
|
|
61
63
|
// Using request-aware logger here
|
|
62
64
|
// Log both the error and attached ErrorData (if any)
|
|
63
|
-
getRequestLogger().error(
|
|
65
|
+
getRequestLogger().error({ err: err_, ...data });
|
|
64
66
|
if (data?.report === false) {
|
|
65
67
|
// Skip reporting the error
|
|
66
68
|
return;
|
|
@@ -81,7 +83,7 @@ export class SentrySharedService {
|
|
|
81
83
|
* Returns "eventId"
|
|
82
84
|
*/
|
|
83
85
|
captureMessage(msg, level) {
|
|
84
|
-
getRequestLogger()[sentrySeverityMap[level] || 'log'](
|
|
86
|
+
getRequestLogger()[sentrySeverityMap[level] || 'log'](`captureMessage: ${msg}`);
|
|
85
87
|
return this.sentry.captureMessage(msg, level);
|
|
86
88
|
}
|
|
87
89
|
addBreadcrumb(breadcrumb) {
|
|
@@ -95,17 +97,14 @@ export class SentrySharedService {
|
|
|
95
97
|
* @experimental
|
|
96
98
|
*/
|
|
97
99
|
getCommonLogger() {
|
|
98
|
-
return {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.sentry.captureException(_anyToError(args.length === 1 ? args[0] : args));
|
|
108
|
-
},
|
|
109
|
-
};
|
|
100
|
+
return commonLoggerCreate(entry => {
|
|
101
|
+
if (entry.level !== 'error')
|
|
102
|
+
return; // noop
|
|
103
|
+
const { msg, err } = entry;
|
|
104
|
+
this.sentry.addBreadcrumb({
|
|
105
|
+
message: logEntryToString(entry, INSPECT_OPT),
|
|
106
|
+
});
|
|
107
|
+
this.sentry.captureException(_anyToError(err ?? msg ?? _omit(entry, ['level'])));
|
|
108
|
+
});
|
|
110
109
|
}
|
|
111
110
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { _lazyValue } from '@naturalcycles/js-lib';
|
|
3
|
+
import { commonLoggerCreate } from '@naturalcycles/js-lib/log';
|
|
3
4
|
import { ciLogger, devLogger, gcpStructuredLogger } from './logMiddleware.js';
|
|
4
5
|
const { GAE_INSTANCE, K_SERVICE, CI } = process.env;
|
|
5
6
|
const isGAE = !!GAE_INSTANCE;
|
|
@@ -43,9 +44,4 @@ export function getRequestLogger() {
|
|
|
43
44
|
*
|
|
44
45
|
* @experimental
|
|
45
46
|
*/
|
|
46
|
-
export const requestLogger = {
|
|
47
|
-
debug: (...args) => getRequestLogger().debug(...args),
|
|
48
|
-
log: (...args) => getRequestLogger().log(...args),
|
|
49
|
-
warn: (...args) => getRequestLogger().warn(...args),
|
|
50
|
-
error: (...args) => getRequestLogger().error(...args),
|
|
51
|
-
};
|
|
47
|
+
export const requestLogger = commonLoggerCreate(({ level, ...data }) => getRequestLogger()[level](data));
|
|
@@ -40,10 +40,10 @@ export function respondWithError(req, res, err) {
|
|
|
40
40
|
else {
|
|
41
41
|
// because errorService was not provided - we are going to log the error here
|
|
42
42
|
if (headersSent) {
|
|
43
|
-
req.error(`error after headersSent:`, err);
|
|
43
|
+
req.error({ msg: `error after headersSent:`, err });
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
-
req.error(err);
|
|
46
|
+
req.error({ err });
|
|
47
47
|
}
|
|
48
48
|
// todo: add endpoint to the log
|
|
49
49
|
// todo: add userId from the "Context" (or, just req.userId?) to the log
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
|
-
import { _isPlainObject } from '@naturalcycles/js-lib';
|
|
3
2
|
import { _anyToErrorObject } from '@naturalcycles/js-lib/error';
|
|
3
|
+
import { commonLoggerCreate } from '@naturalcycles/js-lib/log';
|
|
4
4
|
import { _safeJsonStringify } from '@naturalcycles/js-lib/string/safeJsonStringify.js';
|
|
5
5
|
import { _objectAssign } from '@naturalcycles/js-lib/types';
|
|
6
|
-
import {
|
|
6
|
+
import { logEntryToString } from '@naturalcycles/nodejs-lib';
|
|
7
7
|
import { dimGrey } from '@naturalcycles/nodejs-lib/colors';
|
|
8
8
|
const { GOOGLE_CLOUD_PROJECT, GAE_INSTANCE, K_SERVICE, APP_ENV } = process.env;
|
|
9
9
|
const isGAE = !!GAE_INSTANCE;
|
|
@@ -16,81 +16,49 @@ let reqCounter = 0;
|
|
|
16
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 gcpStructuredLogger = {
|
|
20
|
-
debug: (...args) => writeGCPStructuredLog({ severity: 'DEBUG' }, args),
|
|
21
|
-
log: (...args) => writeGCPStructuredLog({}, args),
|
|
22
|
-
warn: (...args) => writeGCPStructuredLog({ severity: 'WARNING' }, args),
|
|
23
|
-
error: (...args) => writeGCPStructuredLog({ severity: 'ERROR' }, args),
|
|
24
|
-
};
|
|
19
|
+
export const gcpStructuredLogger = commonLoggerCreate(entry => writeGCPStructuredLog({}, entry));
|
|
25
20
|
/**
|
|
26
21
|
* Fancy development logger, to be used in outside-of-request situations
|
|
27
22
|
* (otherwise req.log should be used).
|
|
28
23
|
*/
|
|
29
|
-
export const devLogger =
|
|
30
|
-
debug: (...args) => logToDev(null, args),
|
|
31
|
-
log: (...args) => logToDev(null, args),
|
|
32
|
-
warn: (...args) => logToDev(null, args),
|
|
33
|
-
error: (...args) => logToDev(null, args),
|
|
34
|
-
};
|
|
24
|
+
export const devLogger = commonLoggerCreate(entry => logToDev(null, entry));
|
|
35
25
|
/**
|
|
36
26
|
* Same as devLogger, but without colors (e.g to not confuse Sentry).
|
|
37
27
|
*/
|
|
38
|
-
export const ciLogger =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
export const ciLogger = commonLoggerCreate(logToCI);
|
|
29
|
+
const gcpSeverityByLevel = {
|
|
30
|
+
debug: 'DEBUG',
|
|
31
|
+
log: 'INFO',
|
|
32
|
+
warn: 'WARNING',
|
|
33
|
+
error: 'ERROR',
|
|
43
34
|
};
|
|
44
35
|
// Documented here: https://cloud.google.com/logging/docs/structured-logging
|
|
45
36
|
// Cloud Run logging: https://cloud.google.com/run/docs/logging
|
|
46
|
-
function writeGCPStructuredLog(meta,
|
|
47
|
-
|
|
48
|
-
console.log(JSON.stringify({
|
|
49
|
-
message: args.map(a => (typeof a === 'string' ? a : inspect(a))).join(' '),
|
|
50
|
-
...meta,
|
|
51
|
-
}));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const { msg, err, ...entry } = args[0];
|
|
37
|
+
function writeGCPStructuredLog(meta, entry) {
|
|
38
|
+
const { level, msg, err, ...fields } = entry;
|
|
55
39
|
if (err instanceof Error) {
|
|
56
|
-
|
|
57
|
-
|
|
40
|
+
fields['err'] = _anyToErrorObject(err);
|
|
41
|
+
fields['message'] = msg ? `${msg}\n${inspect(err)}` : inspect(err);
|
|
58
42
|
}
|
|
59
43
|
else {
|
|
60
44
|
if (err !== undefined)
|
|
61
|
-
|
|
45
|
+
fields['err'] = err;
|
|
62
46
|
if (msg !== undefined)
|
|
63
|
-
|
|
47
|
+
fields['message'] = msg;
|
|
64
48
|
}
|
|
65
|
-
console.log(_safeJsonStringify({ ...
|
|
49
|
+
console.log(_safeJsonStringify({ ...fields, ...meta, severity: gcpSeverityByLevel[level] }));
|
|
66
50
|
}
|
|
67
|
-
function logToDev(requestId,
|
|
51
|
+
function logToDev(requestId, entry) {
|
|
68
52
|
// Run on local machine
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
requestId ? [dimGrey(`[${requestId}]`)] : [],
|
|
72
|
-
...args.map(a => _inspect(a, { includeErrorStack: true, colors: true })),
|
|
73
|
-
].join(' '));
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const { msg, err, ...rest } = args[0];
|
|
77
|
-
const parts = [];
|
|
78
|
-
if (requestId)
|
|
79
|
-
parts.push(dimGrey(`[${requestId}]`));
|
|
80
|
-
if (msg !== undefined)
|
|
81
|
-
parts.push(String(msg));
|
|
82
|
-
if (err !== undefined)
|
|
83
|
-
parts.push(_inspect(err, { includeErrorStack: true, colors: true }));
|
|
84
|
-
if (Object.keys(rest).length)
|
|
85
|
-
parts.push(dimGrey(_inspect(rest, { colors: false })));
|
|
86
|
-
console.log(parts.join(' '));
|
|
53
|
+
const line = logEntryToString(entry, { colors: true });
|
|
54
|
+
console.log(requestId ? `${dimGrey(`[${requestId}]`)} ${line}` : line);
|
|
87
55
|
}
|
|
88
56
|
/**
|
|
89
57
|
* Same as logToDev, but without request and without colors.
|
|
90
58
|
* This is to not confuse e.g Sentry when it picks up messages with colors
|
|
91
59
|
*/
|
|
92
|
-
function logToCI(
|
|
93
|
-
console.log(
|
|
60
|
+
function logToCI(entry) {
|
|
61
|
+
console.log(logEntryToString(entry, { colors: false }));
|
|
94
62
|
}
|
|
95
63
|
export function logMiddleware() {
|
|
96
64
|
if (isGAE || isCloudRun) {
|
|
@@ -117,12 +85,7 @@ export function logMiddleware() {
|
|
|
117
85
|
if (isGAE) {
|
|
118
86
|
meta['appengine.googleapis.com/request_id'] = req.header('x-appengine-request-log-id');
|
|
119
87
|
}
|
|
120
|
-
_objectAssign(req,
|
|
121
|
-
debug: (...args) => writeGCPStructuredLog({ ...meta, severity: 'DEBUG' }, args),
|
|
122
|
-
log: (...args) => writeGCPStructuredLog({ ...meta, severity: 'INFO' }, args),
|
|
123
|
-
warn: (...args) => writeGCPStructuredLog({ ...meta, severity: 'WARNING' }, args),
|
|
124
|
-
error: (...args) => writeGCPStructuredLog({ ...meta, severity: 'ERROR' }, args),
|
|
125
|
-
});
|
|
88
|
+
_objectAssign(req, commonLoggerCreate(entry => writeGCPStructuredLog(meta, entry)));
|
|
126
89
|
next();
|
|
127
90
|
};
|
|
128
91
|
}
|
|
@@ -131,18 +94,14 @@ export function logMiddleware() {
|
|
|
131
94
|
return function devLogHandler(req, _res, next) {
|
|
132
95
|
// Local machine
|
|
133
96
|
req.requestId = String(++reqCounter);
|
|
134
|
-
req.
|
|
135
|
-
req.log =
|
|
136
|
-
req.warn =
|
|
137
|
-
req.error =
|
|
138
|
-
(...args) => logToDev(req.requestId, args);
|
|
97
|
+
_objectAssign(req, commonLoggerCreate(entry => logToDev(req.requestId, entry)));
|
|
139
98
|
next();
|
|
140
99
|
};
|
|
141
100
|
}
|
|
142
101
|
// Otherwise, return "simple" logger
|
|
143
102
|
// This includes: unit tests, CI environments
|
|
144
103
|
return function simpleLogHandler(req, _res, next) {
|
|
145
|
-
req
|
|
104
|
+
_objectAssign(req, ciLogger);
|
|
146
105
|
next();
|
|
147
106
|
};
|
|
148
107
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { getRequestEndpoint } from './request.util.js';
|
|
2
2
|
export function notFoundMiddleware() {
|
|
3
3
|
return (req, res) => {
|
|
4
|
-
res
|
|
4
|
+
res
|
|
5
|
+
.status(404)
|
|
6
|
+
.type('text/plain')
|
|
7
|
+
.send(`404 Not Found: ${getRequestEndpoint(req)}`);
|
|
5
8
|
};
|
|
6
9
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CommonLogger } from '@naturalcycles/js-lib/log';
|
|
2
2
|
import type { Promisable } from '@naturalcycles/js-lib/types';
|
|
3
3
|
import type { Application, IRouter, NextFunction, Request, Response } from 'express';
|
|
4
4
|
/**
|
|
@@ -8,11 +8,7 @@ import type { Application, IRouter, NextFunction, Request, Response } from 'expr
|
|
|
8
8
|
* `BackendRequest` seems to not conflict with anything right now.
|
|
9
9
|
* Previous name `ExpressRequest` was clashing with Sentry.
|
|
10
10
|
*/
|
|
11
|
-
export interface BackendRequest<BODY = unknown> extends Request {
|
|
12
|
-
debug: CommonLogFunction;
|
|
13
|
-
log: CommonLogFunction;
|
|
14
|
-
warn: CommonLogFunction;
|
|
15
|
-
error: CommonLogFunction;
|
|
11
|
+
export interface BackendRequest<BODY = unknown> extends Request, CommonLogger {
|
|
16
12
|
requestId?: string;
|
|
17
13
|
/**
|
|
18
14
|
* Only used for request logging purposes.
|
|
@@ -50,11 +46,7 @@ export type BackendErrorRequestHandler = (err: any, req: BackendRequest, res: Ba
|
|
|
50
46
|
export type BackendRouter = IRouter;
|
|
51
47
|
export type BackendApplication = Application;
|
|
52
48
|
declare module 'http' {
|
|
53
|
-
interface IncomingMessage {
|
|
54
|
-
debug: CommonLogFunction;
|
|
55
|
-
log: CommonLogFunction;
|
|
56
|
-
warn: CommonLogFunction;
|
|
57
|
-
error: CommonLogFunction;
|
|
49
|
+
interface IncomingMessage extends CommonLogger {
|
|
58
50
|
requestId?: string;
|
|
59
51
|
requestTimeout?: NodeJS.Timeout;
|
|
60
52
|
bodyParserTimeout?: NodeJS.Timeout;
|
package/package.json
CHANGED
|
@@ -220,7 +220,7 @@ export class BaseAdminService {
|
|
|
220
220
|
return // skip logging, expected error
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
req.error(`getEmailByToken error:`, err)
|
|
223
|
+
req.error({ msg: `getEmailByToken error:`, err })
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -257,12 +257,12 @@ export class BaseAdminService {
|
|
|
257
257
|
granted: boolean,
|
|
258
258
|
meta: AnyObject = {},
|
|
259
259
|
): Promise<void> {
|
|
260
|
-
req.log(
|
|
261
|
-
|
|
260
|
+
req.log({
|
|
261
|
+
...meta,
|
|
262
|
+
msg: `${dimGrey(email)} ${required ? 'required' : 'optional'} permissions check [${dimGrey(
|
|
262
263
|
reqPermissions.join(', '),
|
|
263
264
|
)}]: ${granted ? green('GRANTED') : red('DENIED')}`,
|
|
264
|
-
|
|
265
|
-
)
|
|
265
|
+
})
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/**
|
|
@@ -6,7 +6,6 @@ import { compressionMiddleware } from '../server/compressionMiddleware.js'
|
|
|
6
6
|
import { genericErrorMiddleware } from '../server/genericErrorMiddleware.js'
|
|
7
7
|
import type { GenericErrorMiddlewareCfg } from '../server/genericErrorMiddleware.js'
|
|
8
8
|
import { logMiddleware } from '../server/logMiddleware.js'
|
|
9
|
-
import { methodOverrideMiddleware } from '../server/methodOverrideMiddleware.js'
|
|
10
9
|
import { notFoundMiddleware } from '../server/notFoundMiddleware.js'
|
|
11
10
|
import { requestTimeoutMiddleware } from '../server/requestTimeoutMiddleware.js'
|
|
12
11
|
import type {
|
|
@@ -42,7 +41,6 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
|
|
|
42
41
|
app.use(asyncLocalStorageMiddleware())
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
app.use(methodOverrideMiddleware())
|
|
46
44
|
app.use(requestTimeoutMiddleware())
|
|
47
45
|
// app.use(serverStatsMiddleware()) // disabled by default
|
|
48
46
|
// app.use(bodyParserTimeout()) // removed by default
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ export * from './server/compressionMiddleware.js'
|
|
|
7
7
|
export * from './server/eventLoop.util.js'
|
|
8
8
|
export * from './server/genericErrorMiddleware.js'
|
|
9
9
|
export * from './server/logMiddleware.js'
|
|
10
|
-
export * from './server/methodOverrideMiddleware.js'
|
|
11
10
|
export * from './server/notFoundMiddleware.js'
|
|
12
11
|
export * from './server/okMiddleware.js'
|
|
13
12
|
export * from './server/request.log.util.js'
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { _anyToError, _isErrorObject } from '@naturalcycles/js-lib/error'
|
|
2
|
+
import { commonLoggerCreate } from '@naturalcycles/js-lib/log'
|
|
2
3
|
import type { CommonLogger, CommonLogLevel } from '@naturalcycles/js-lib/log'
|
|
4
|
+
import { _omit } from '@naturalcycles/js-lib/object/object.util.js'
|
|
3
5
|
import type { Primitive, StringMap } from '@naturalcycles/js-lib/types'
|
|
4
6
|
import type { InspectAnyOptions } from '@naturalcycles/nodejs-lib'
|
|
5
|
-
import { _inspect } from '@naturalcycles/nodejs-lib'
|
|
7
|
+
import { _inspect, logEntryToString } from '@naturalcycles/nodejs-lib'
|
|
6
8
|
import type { Breadcrumb, SeverityLevel } from '@sentry/node-core/light'
|
|
7
9
|
import type * as SentryLib from '@sentry/node-core/light'
|
|
8
10
|
import { getRequestLogger } from '../server/asyncLocalStorageMiddleware.js'
|
|
@@ -78,7 +80,7 @@ export class SentrySharedService {
|
|
|
78
80
|
|
|
79
81
|
// Using request-aware logger here
|
|
80
82
|
// Log both the error and attached ErrorData (if any)
|
|
81
|
-
getRequestLogger().error(
|
|
83
|
+
getRequestLogger().error({ err: err_, ...data })
|
|
82
84
|
|
|
83
85
|
if (data?.report === false) {
|
|
84
86
|
// Skip reporting the error
|
|
@@ -106,7 +108,7 @@ export class SentrySharedService {
|
|
|
106
108
|
* Returns "eventId"
|
|
107
109
|
*/
|
|
108
110
|
captureMessage(msg: string, level?: SeverityLevel): string {
|
|
109
|
-
getRequestLogger()[sentrySeverityMap[level!] || 'log'](
|
|
111
|
+
getRequestLogger()[sentrySeverityMap[level!] || 'log'](`captureMessage: ${msg}`)
|
|
110
112
|
return this.sentry.captureMessage(msg, level)
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -122,19 +124,15 @@ export class SentrySharedService {
|
|
|
122
124
|
* @experimental
|
|
123
125
|
*/
|
|
124
126
|
getCommonLogger(): CommonLogger {
|
|
125
|
-
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.sentry.captureException(_anyToError(args.length === 1 ? args[0] : args))
|
|
137
|
-
},
|
|
138
|
-
}
|
|
127
|
+
return commonLoggerCreate(entry => {
|
|
128
|
+
if (entry.level !== 'error') return // noop
|
|
129
|
+
const { msg, err } = entry
|
|
130
|
+
|
|
131
|
+
this.sentry.addBreadcrumb({
|
|
132
|
+
message: logEntryToString(entry, INSPECT_OPT),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
this.sentry.captureException(_anyToError(err ?? msg ?? _omit(entry, ['level'])))
|
|
136
|
+
})
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
2
|
import { _lazyValue } from '@naturalcycles/js-lib'
|
|
3
|
+
import { commonLoggerCreate } from '@naturalcycles/js-lib/log'
|
|
3
4
|
import type { CommonLogger } from '@naturalcycles/js-lib/log'
|
|
4
5
|
import { ciLogger, devLogger, gcpStructuredLogger } from './logMiddleware.js'
|
|
5
6
|
import type { BackendRequest, BackendRequestHandler } from './server.model.js'
|
|
@@ -53,9 +54,6 @@ export function getRequestLogger(): CommonLogger {
|
|
|
53
54
|
*
|
|
54
55
|
* @experimental
|
|
55
56
|
*/
|
|
56
|
-
export const requestLogger: CommonLogger = {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
warn: (...args) => getRequestLogger().warn(...args),
|
|
60
|
-
error: (...args) => getRequestLogger().error(...args),
|
|
61
|
-
}
|
|
57
|
+
export const requestLogger: CommonLogger = commonLoggerCreate(({ level, ...data }) =>
|
|
58
|
+
getRequestLogger()[level](data),
|
|
59
|
+
)
|
|
@@ -75,9 +75,9 @@ export function respondWithError(req: BackendRequest, res: BackendResponse, err:
|
|
|
75
75
|
// because errorService was not provided - we are going to log the error here
|
|
76
76
|
|
|
77
77
|
if (headersSent) {
|
|
78
|
-
req.error(`error after headersSent:`, err)
|
|
78
|
+
req.error({ msg: `error after headersSent:`, err })
|
|
79
79
|
} else {
|
|
80
|
-
req.error(err)
|
|
80
|
+
req.error({ err })
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// todo: add endpoint to the log
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { inspect } from 'node:util'
|
|
2
|
-
import { _isPlainObject } from '@naturalcycles/js-lib'
|
|
3
2
|
import { _anyToErrorObject } from '@naturalcycles/js-lib/error'
|
|
4
|
-
import
|
|
3
|
+
import { commonLoggerCreate } from '@naturalcycles/js-lib/log'
|
|
4
|
+
import type { CommonLogger, CommonLogLevel, LogEntry } from '@naturalcycles/js-lib/log'
|
|
5
5
|
import { _safeJsonStringify } from '@naturalcycles/js-lib/string/safeJsonStringify.js'
|
|
6
6
|
import { _objectAssign } from '@naturalcycles/js-lib/types'
|
|
7
7
|
import type { AnyObject } from '@naturalcycles/js-lib/types'
|
|
8
|
-
import {
|
|
8
|
+
import { logEntryToString } from '@naturalcycles/nodejs-lib'
|
|
9
9
|
import { dimGrey } from '@naturalcycles/nodejs-lib/colors'
|
|
10
10
|
import type { BackendRequestHandler } from './server.model.js'
|
|
11
11
|
|
|
@@ -22,85 +22,54 @@ let reqCounter = 0
|
|
|
22
22
|
* Logger that logs in "GCP structured log" format.
|
|
23
23
|
* To be used in outside-of-request situations (otherwise req.log should be used).
|
|
24
24
|
*/
|
|
25
|
-
export const gcpStructuredLogger: CommonLogger =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
warn: (...args) => writeGCPStructuredLog({ severity: 'WARNING' }, args),
|
|
29
|
-
error: (...args) => writeGCPStructuredLog({ severity: 'ERROR' }, args),
|
|
30
|
-
}
|
|
25
|
+
export const gcpStructuredLogger: CommonLogger = commonLoggerCreate(entry =>
|
|
26
|
+
writeGCPStructuredLog({}, entry),
|
|
27
|
+
)
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
30
|
* Fancy development logger, to be used in outside-of-request situations
|
|
34
31
|
* (otherwise req.log should be used).
|
|
35
32
|
*/
|
|
36
|
-
export const devLogger: CommonLogger =
|
|
37
|
-
debug: (...args) => logToDev(null, args),
|
|
38
|
-
log: (...args) => logToDev(null, args),
|
|
39
|
-
warn: (...args) => logToDev(null, args),
|
|
40
|
-
error: (...args) => logToDev(null, args),
|
|
41
|
-
}
|
|
33
|
+
export const devLogger: CommonLogger = commonLoggerCreate(entry => logToDev(null, entry))
|
|
42
34
|
|
|
43
35
|
/**
|
|
44
36
|
* Same as devLogger, but without colors (e.g to not confuse Sentry).
|
|
45
37
|
*/
|
|
46
|
-
export const ciLogger: CommonLogger =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
export const ciLogger: CommonLogger = commonLoggerCreate(logToCI)
|
|
39
|
+
|
|
40
|
+
const gcpSeverityByLevel: Record<CommonLogLevel, string> = {
|
|
41
|
+
debug: 'DEBUG',
|
|
42
|
+
log: 'INFO',
|
|
43
|
+
warn: 'WARNING',
|
|
44
|
+
error: 'ERROR',
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
// Documented here: https://cloud.google.com/logging/docs/structured-logging
|
|
54
48
|
// Cloud Run logging: https://cloud.google.com/run/docs/logging
|
|
55
|
-
function writeGCPStructuredLog(meta: AnyObject,
|
|
56
|
-
|
|
57
|
-
console.log(
|
|
58
|
-
JSON.stringify({
|
|
59
|
-
message: args.map(a => (typeof a === 'string' ? a : inspect(a))).join(' '),
|
|
60
|
-
...meta,
|
|
61
|
-
}),
|
|
62
|
-
)
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const { msg, err, ...entry } = args[0]
|
|
49
|
+
function writeGCPStructuredLog(meta: AnyObject, entry: LogEntry): void {
|
|
50
|
+
const { level, msg, err, ...fields } = entry
|
|
67
51
|
if (err instanceof Error) {
|
|
68
|
-
|
|
69
|
-
|
|
52
|
+
fields['err'] = _anyToErrorObject(err)
|
|
53
|
+
fields['message'] = msg ? `${msg}\n${inspect(err)}` : inspect(err)
|
|
70
54
|
} else {
|
|
71
|
-
if (err !== undefined)
|
|
72
|
-
if (msg !== undefined)
|
|
55
|
+
if (err !== undefined) fields['err'] = err
|
|
56
|
+
if (msg !== undefined) fields['message'] = msg
|
|
73
57
|
}
|
|
74
|
-
console.log(_safeJsonStringify({ ...
|
|
58
|
+
console.log(_safeJsonStringify({ ...fields, ...meta, severity: gcpSeverityByLevel[level] }))
|
|
75
59
|
}
|
|
76
60
|
|
|
77
|
-
function logToDev(requestId: string | null,
|
|
61
|
+
function logToDev(requestId: string | null, entry: LogEntry): void {
|
|
78
62
|
// Run on local machine
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
[
|
|
82
|
-
requestId ? [dimGrey(`[${requestId}]`)] : [],
|
|
83
|
-
...args.map(a => _inspect(a, { includeErrorStack: true, colors: true })),
|
|
84
|
-
].join(' '),
|
|
85
|
-
)
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const { msg, err, ...rest } = args[0]
|
|
90
|
-
const parts: string[] = []
|
|
91
|
-
if (requestId) parts.push(dimGrey(`[${requestId}]`))
|
|
92
|
-
if (msg !== undefined) parts.push(String(msg))
|
|
93
|
-
if (err !== undefined) parts.push(_inspect(err, { includeErrorStack: true, colors: true }))
|
|
94
|
-
if (Object.keys(rest).length) parts.push(dimGrey(_inspect(rest, { colors: false })))
|
|
95
|
-
console.log(parts.join(' '))
|
|
63
|
+
const line = logEntryToString(entry, { colors: true })
|
|
64
|
+
console.log(requestId ? `${dimGrey(`[${requestId}]`)} ${line}` : line)
|
|
96
65
|
}
|
|
97
66
|
|
|
98
67
|
/**
|
|
99
68
|
* Same as logToDev, but without request and without colors.
|
|
100
69
|
* This is to not confuse e.g Sentry when it picks up messages with colors
|
|
101
70
|
*/
|
|
102
|
-
function logToCI(
|
|
103
|
-
console.log(
|
|
71
|
+
function logToCI(entry: LogEntry): void {
|
|
72
|
+
console.log(logEntryToString(entry, { colors: false }))
|
|
104
73
|
}
|
|
105
74
|
|
|
106
75
|
export function logMiddleware(): BackendRequestHandler {
|
|
@@ -130,12 +99,10 @@ export function logMiddleware(): BackendRequestHandler {
|
|
|
130
99
|
meta['appengine.googleapis.com/request_id'] = req.header('x-appengine-request-log-id')
|
|
131
100
|
}
|
|
132
101
|
|
|
133
|
-
_objectAssign(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
error: (...args: any[]) => writeGCPStructuredLog({ ...meta, severity: 'ERROR' }, args),
|
|
138
|
-
} satisfies CommonLogger)
|
|
102
|
+
_objectAssign(
|
|
103
|
+
req,
|
|
104
|
+
commonLoggerCreate(entry => writeGCPStructuredLog(meta, entry)),
|
|
105
|
+
)
|
|
139
106
|
|
|
140
107
|
next()
|
|
141
108
|
}
|
|
@@ -146,11 +113,10 @@ export function logMiddleware(): BackendRequestHandler {
|
|
|
146
113
|
return function devLogHandler(req, _res, next) {
|
|
147
114
|
// Local machine
|
|
148
115
|
req.requestId = String(++reqCounter)
|
|
149
|
-
|
|
150
|
-
req
|
|
151
|
-
req.
|
|
152
|
-
|
|
153
|
-
(...args: any[]) => logToDev(req.requestId!, args)
|
|
116
|
+
_objectAssign(
|
|
117
|
+
req,
|
|
118
|
+
commonLoggerCreate(entry => logToDev(req.requestId!, entry)),
|
|
119
|
+
)
|
|
154
120
|
next()
|
|
155
121
|
}
|
|
156
122
|
}
|
|
@@ -158,7 +124,7 @@ export function logMiddleware(): BackendRequestHandler {
|
|
|
158
124
|
// Otherwise, return "simple" logger
|
|
159
125
|
// This includes: unit tests, CI environments
|
|
160
126
|
return function simpleLogHandler(req, _res, next) {
|
|
161
|
-
req
|
|
127
|
+
_objectAssign(req, ciLogger)
|
|
162
128
|
next()
|
|
163
129
|
}
|
|
164
130
|
}
|
|
@@ -3,6 +3,9 @@ import type { BackendRequestHandler } from './server.model.js'
|
|
|
3
3
|
|
|
4
4
|
export function notFoundMiddleware(): BackendRequestHandler {
|
|
5
5
|
return (req, res) => {
|
|
6
|
-
res
|
|
6
|
+
res
|
|
7
|
+
.status(404)
|
|
8
|
+
.type('text/plain')
|
|
9
|
+
.send(`404 Not Found: ${getRequestEndpoint(req)}`)
|
|
7
10
|
}
|
|
8
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CommonLogger } from '@naturalcycles/js-lib/log'
|
|
2
2
|
import type { Promisable } from '@naturalcycles/js-lib/types'
|
|
3
3
|
import type { Application, IRouter, NextFunction, Request, Response } from 'express'
|
|
4
4
|
|
|
@@ -9,12 +9,7 @@ import type { Application, IRouter, NextFunction, Request, Response } from 'expr
|
|
|
9
9
|
* `BackendRequest` seems to not conflict with anything right now.
|
|
10
10
|
* Previous name `ExpressRequest` was clashing with Sentry.
|
|
11
11
|
*/
|
|
12
|
-
export interface BackendRequest<BODY = unknown> extends Request {
|
|
13
|
-
debug: CommonLogFunction
|
|
14
|
-
log: CommonLogFunction
|
|
15
|
-
warn: CommonLogFunction
|
|
16
|
-
error: CommonLogFunction
|
|
17
|
-
|
|
12
|
+
export interface BackendRequest<BODY = unknown> extends Request, CommonLogger {
|
|
18
13
|
requestId?: string
|
|
19
14
|
/**
|
|
20
15
|
* Only used for request logging purposes.
|
|
@@ -71,12 +66,7 @@ export type BackendRouter = IRouter
|
|
|
71
66
|
export type BackendApplication = Application
|
|
72
67
|
|
|
73
68
|
declare module 'http' {
|
|
74
|
-
interface IncomingMessage {
|
|
75
|
-
debug: CommonLogFunction
|
|
76
|
-
log: CommonLogFunction
|
|
77
|
-
warn: CommonLogFunction
|
|
78
|
-
error: CommonLogFunction
|
|
79
|
-
|
|
69
|
+
interface IncomingMessage extends CommonLogger {
|
|
80
70
|
requestId?: string
|
|
81
71
|
|
|
82
72
|
requestTimeout?: NodeJS.Timeout
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { BackendRequestHandler } from './server.model.js';
|
|
2
|
-
export interface MethodOverrideMiddlewareCfg {
|
|
3
|
-
/**
|
|
4
|
-
* @default _method
|
|
5
|
-
*/
|
|
6
|
-
methodKey?: string;
|
|
7
|
-
}
|
|
8
|
-
export declare function methodOverrideMiddleware(cfg?: MethodOverrideMiddlewareCfg): BackendRequestHandler;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export function methodOverrideMiddleware(cfg = {}) {
|
|
2
|
-
const { methodKey } = {
|
|
3
|
-
methodKey: '_method',
|
|
4
|
-
...cfg,
|
|
5
|
-
};
|
|
6
|
-
return (req, _res, next) => {
|
|
7
|
-
if (req.query[methodKey]) {
|
|
8
|
-
req.method = req.query[methodKey];
|
|
9
|
-
// delete req.query[methodKey]
|
|
10
|
-
}
|
|
11
|
-
next();
|
|
12
|
-
};
|
|
13
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { BackendRequestHandler } from './server.model.js'
|
|
2
|
-
|
|
3
|
-
export interface MethodOverrideMiddlewareCfg {
|
|
4
|
-
/**
|
|
5
|
-
* @default _method
|
|
6
|
-
*/
|
|
7
|
-
methodKey?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function methodOverrideMiddleware(
|
|
11
|
-
cfg: MethodOverrideMiddlewareCfg = {},
|
|
12
|
-
): BackendRequestHandler {
|
|
13
|
-
const { methodKey } = {
|
|
14
|
-
methodKey: '_method',
|
|
15
|
-
...cfg,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (req, _res, next) => {
|
|
19
|
-
if (req.query[methodKey]) {
|
|
20
|
-
req.method = req.query[methodKey] as string
|
|
21
|
-
// delete req.query[methodKey]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
next()
|
|
25
|
-
}
|
|
26
|
-
}
|