@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 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, gaeLogger } from './logMiddleware.js';
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 = !!CI;
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 || (isGAE ? gaeLogger : isCI || isCloudRun ? ciLogger : devLogger));
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 (!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
- }
@@ -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 AppEngine format.
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 gaeLogger: CommonLogger;
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
- // const isCloudRun = !!K_SERVICE
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 AppEngine format.
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 gaeLogger = {
15
- log: (...args) => logToAppEngine({}, args),
16
- warn: (...args) => logToAppEngine({ severity: 'WARNING' }, args),
17
- error: (...args) => logToAppEngine({ severity: '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
- function logToAppEngine(meta, args) {
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
- if (GOOGLE_CLOUD_PROJECT) {
60
- return function appEngineLogHandler(req, _res, next) {
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
- const meta = {
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
- else {
76
- Object.assign(req, gaeLogger);
77
- }
78
- next();
79
- };
80
- }
81
- return function appEngineLogHandler(req, _res, next) {
82
- Object.assign(req, gaeLogger);
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, CloudRun, CI environments
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 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.
@@ -9,4 +9,4 @@ export interface SimpleRequestLoggerMiddlewareCfg {
9
9
  */
10
10
  logFinish: boolean;
11
11
  }
12
- export declare function simpleRequestLoggerMiddleware(_cfg?: Partial<SimpleRequestLoggerMiddlewareCfg>): BackendRequestHandler;
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 { logRequest } from './request.log.util.js';
5
- const { APP_ENV } = process.env;
6
- 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
- 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
- logRequest(req, res.statusCode, dimGrey(_since(started)));
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.3.0",
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,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, gaeLogger } from './logMiddleware.js'
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 = !!CI
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 || (isGAE ? gaeLogger : isCI || isCloudRun ? ciLogger : devLogger)
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 (!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
- }
@@ -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
- // const isCloudRun = !!K_SERVICE
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 AppEngine format.
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 gaeLogger: CommonLogger = {
20
- log: (...args) => logToAppEngine({}, args),
21
- warn: (...args) => logToAppEngine({ severity: 'WARNING' }, args),
22
- error: (...args) => logToAppEngine({ severity: '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
- function logToAppEngine(meta: AnyObject, args: any[]): void {
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
- if (GOOGLE_CLOUD_PROJECT) {
75
- return function appEngineLogHandler(req, _res, next) {
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
- const meta = {
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, CloudRun, CI environments
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 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
  /**
@@ -20,18 +18,9 @@ export interface SimpleRequestLoggerMiddlewareCfg {
20
18
  }
21
19
 
22
20
  export function simpleRequestLoggerMiddleware(
23
- _cfg: Partial<SimpleRequestLoggerMiddlewareCfg> = {},
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
- 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
- logRequest(req, res.statusCode, dimGrey(_since(started)))
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