@naturalcycles/backend-lib 9.3.1 → 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 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,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 (!isGAE() && !isCloudRun() && !isTest) {
29
+ if (isDev) {
30
30
  app.use(simpleRequestLoggerMiddleware());
31
31
  }
32
32
  // app.use(safeJsonMiddleware()) // optional
@@ -1,12 +1,7 @@
1
- import type { ErrorObject } from '@naturalcycles/js-lib';
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 { _anyToError, _errorLikeToErrorObject, _filterUndefinedValues } from '@naturalcycles/js-lib';
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 sentryService, so it's available to `respondWithError` function
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
- let errorId;
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.headersSent = headersSent || undefined;
54
- _filterUndefinedValues(httpError.data, true);
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
- }
@@ -58,7 +58,11 @@ function logToCI(args) {
58
58
  export function logMiddleware() {
59
59
  if (isGAE || isCloudRun) {
60
60
  return function gcpStructuredLogHandler(req, _res, next) {
61
- const meta = {};
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
+ };
62
66
  // CloudRun does NOT have this env variable set,
63
67
  // so you have to set it manually on deployment, like this:
64
68
  // gcloud run deploy my-service \
@@ -1,3 +1,3 @@
1
1
  import type { BackendRequest } from './server.model.js';
2
- export declare function logRequest(req: BackendRequest, statusCode: number, ...tokens: any[]): void;
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 logRequest(req, statusCode, ...tokens) {
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,7 @@
1
+ import type { BackendRequestHandler } from '../index.js';
2
+ /**
3
+ * Experimental request logger for Cloud Run.
4
+ *
5
+ * @experimental
6
+ */
7
+ export declare function requestLoggerMiddleware(): BackendRequestHandler;
@@ -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.
@@ -1,13 +1,8 @@
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 { logRequest } from './request.log.util.js';
5
- const { APP_ENV } = process.env;
4
+ import { logRequestWithColors } from './request.log.util.js';
6
5
  export function simpleRequestLoggerMiddleware(cfg = {}) {
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
6
  const { logStart = false, logFinish = true } = cfg;
12
7
  return (req, res, next) => {
13
8
  const started = Date.now();
@@ -16,13 +11,7 @@ export function simpleRequestLoggerMiddleware(cfg = {}) {
16
11
  }
17
12
  if (logFinish) {
18
13
  onFinished(res, () => {
19
- logRequest(req, res.statusCode, dimGrey(_since(started)));
20
- // Avoid logging twice. It was previously logged by genericErrorHandler
21
- // if (res.__err) {
22
- // logRequest(req, res.statusCode, dimGrey(_since(started)), _inspect(res.__err))
23
- // } else {
24
- // logRequest(req, res.statusCode, dimGrey(_since(started)))
25
- // }
14
+ logRequestWithColors(req, res.statusCode, dimGrey(_since(started)));
26
15
  });
27
16
  }
28
17
  next();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.3.1",
4
+ "version": "9.4.0",
5
5
  "peerDependencies": {
6
6
  "@sentry/node": "^9"
7
7
  },
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,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 (!isGAE() && !isCloudRun() && !isTest) {
44
+ if (isDev) {
45
45
  app.use(simpleRequestLoggerMiddleware())
46
46
  }
47
47
 
@@ -1,16 +1,14 @@
1
- import type { AppError, BackendErrorResponseObject, ErrorObject } from '@naturalcycles/js-lib'
2
- import { _anyToError, _errorLikeToErrorObject, _filterUndefinedValues } from '@naturalcycles/js-lib'
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 sentryService, so it's available to `respondWithError` function
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
- let errorId: string | undefined
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.headersSent = headersSent || undefined
97
- _filterUndefinedValues(httpError.data, true)
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
- }
@@ -73,7 +73,11 @@ function logToCI(args: any[]): void {
73
73
  export function logMiddleware(): BackendRequestHandler {
74
74
  if (isGAE || isCloudRun) {
75
75
  return function gcpStructuredLogHandler(req, _res, next) {
76
- const meta: StringMap = {}
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
+ }
77
81
 
78
82
  // CloudRun does NOT have this env variable set,
79
83
  // so you have to set it manually on deployment, like this:
@@ -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 logRequest(req: BackendRequest, statusCode: number, ...tokens: any[]): void {
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
+ }
@@ -14,6 +14,10 @@ export interface BackendRequest extends Request {
14
14
  error: CommonLogFunction
15
15
 
16
16
  requestId?: string
17
+ /**
18
+ * Only used for request logging purposes.
19
+ */
20
+ userId?: string
17
21
 
18
22
  /**
19
23
  * Set by requestTimeoutMiddleware.
@@ -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 { logRequest } from './request.log.util.js'
7
-
8
- const { APP_ENV } = process.env
6
+ import { logRequestWithColors } from './request.log.util.js'
9
7
 
10
8
  export interface SimpleRequestLoggerMiddlewareCfg {
11
9
  /**
@@ -22,10 +20,6 @@ export interface SimpleRequestLoggerMiddlewareCfg {
22
20
  export function simpleRequestLoggerMiddleware(
23
21
  cfg: Partial<SimpleRequestLoggerMiddlewareCfg> = {},
24
22
  ): BackendRequestHandler {
25
- // Disable logger in AppEngine, as it doesn't make sense there
26
- // UPD: Only log in dev environment
27
- if (APP_ENV !== 'dev') return (_req, _res, next) => next()
28
-
29
23
  const { logStart = false, logFinish = true } = cfg
30
24
 
31
25
  return (req, res, next) => {
@@ -37,14 +31,7 @@ export function simpleRequestLoggerMiddleware(
37
31
 
38
32
  if (logFinish) {
39
33
  onFinished(res, () => {
40
- logRequest(req, res.statusCode, dimGrey(_since(started)))
41
-
42
- // Avoid logging twice. It was previously logged by genericErrorHandler
43
- // if (res.__err) {
44
- // logRequest(req, res.statusCode, dimGrey(_since(started)), _inspect(res.__err))
45
- // } else {
46
- // logRequest(req, res.statusCode, dimGrey(_since(started)))
47
- // }
34
+ logRequestWithColors(req, res.statusCode, dimGrey(_since(started)))
48
35
  })
49
36
  }
50
37