@naturalcycles/backend-lib 9.25.0 → 9.27.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
@@ -3,6 +3,7 @@ export * from './sentry/sentry.shared.service.js';
3
3
  export * from './server/asyncLocalStorageMiddleware.js';
4
4
  export * from './server/basicAuthMiddleware.js';
5
5
  export * from './server/bodyParserTimeoutMiddleware.js';
6
+ export * from './server/eventLoop.util.js';
6
7
  export * from './server/genericErrorMiddleware.js';
7
8
  export * from './server/logMiddleware.js';
8
9
  export * from './server/methodOverrideMiddleware.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from './sentry/sentry.shared.service.js';
3
3
  export * from './server/asyncLocalStorageMiddleware.js';
4
4
  export * from './server/basicAuthMiddleware.js';
5
5
  export * from './server/bodyParserTimeoutMiddleware.js';
6
+ export * from './server/eventLoop.util.js';
6
7
  export * from './server/genericErrorMiddleware.js';
7
8
  export * from './server/logMiddleware.js';
8
9
  export * from './server/methodOverrideMiddleware.js';
@@ -0,0 +1,46 @@
1
+ import type { NumberOfMilliseconds, NumberOfPercent } from '@naturalcycles/js-lib/types';
2
+ /**
3
+ * @experimental
4
+ */
5
+ export declare class EventLoopMonitor {
6
+ constructor(cfg?: EventLoopMonitorCfg);
7
+ private interval;
8
+ private eld;
9
+ private lastElu;
10
+ /**
11
+ * Undefined until the first interval has completed.
12
+ */
13
+ lastStats?: EventLoopStats;
14
+ stop(): void;
15
+ }
16
+ export interface EventLoopMonitorCfg {
17
+ /**
18
+ * Defaults to 20.
19
+ */
20
+ resolution?: NumberOfMilliseconds;
21
+ /**
22
+ * Defaults to 60_000 ms
23
+ */
24
+ measureInterval?: NumberOfMilliseconds;
25
+ /**
26
+ * Callback to be invoked with EventLoopStats.
27
+ * Called every `measureInterval` milliseconds.
28
+ */
29
+ onStats?: (stats: EventLoopStats) => void;
30
+ }
31
+ export interface EventLoopStats {
32
+ p50: NumberOfMilliseconds;
33
+ p90: NumberOfMilliseconds;
34
+ p99: NumberOfMilliseconds;
35
+ max: NumberOfMilliseconds;
36
+ mean: NumberOfMilliseconds;
37
+ /**
38
+ * EventLoopUtilization in percent.
39
+ *
40
+ * Calculated as:
41
+ * idle: <nanoseconds event loop was idle>,
42
+ * active: <nanoseconds event loop was busy>,
43
+ * utilization: active / (idle + active)
44
+ */
45
+ elu: NumberOfPercent;
46
+ }
@@ -0,0 +1,46 @@
1
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
2
+ /**
3
+ * @experimental
4
+ */
5
+ export class EventLoopMonitor {
6
+ constructor(cfg = {}) {
7
+ const { resolution = 20, measureInterval = 60_000 } = cfg;
8
+ this.eld = monitorEventLoopDelay({ resolution });
9
+ this.eld.enable();
10
+ this.lastElu = performance.eventLoopUtilization();
11
+ this.interval = setInterval(() => {
12
+ // Delay stats are reported in **nanoseconds**
13
+ const { eld } = this;
14
+ const p50 = Math.round(eld.percentile(50) / 1e6);
15
+ const p90 = Math.round(eld.percentile(90) / 1e6);
16
+ const p99 = Math.round(eld.percentile(99) / 1e6);
17
+ const max = Math.round(eld.max / 1e6);
18
+ const mean = Math.round(eld.mean / 1e6);
19
+ const currentElu = performance.eventLoopUtilization();
20
+ const deltaElu = performance.eventLoopUtilization(this.lastElu, currentElu);
21
+ this.lastElu = currentElu;
22
+ const elu = Math.round(deltaElu.utilization * 100);
23
+ this.lastStats = {
24
+ p50,
25
+ p90,
26
+ p99,
27
+ max,
28
+ mean,
29
+ elu,
30
+ };
31
+ cfg.onStats?.(this.lastStats);
32
+ eld.reset();
33
+ }, measureInterval);
34
+ }
35
+ interval;
36
+ eld;
37
+ lastElu;
38
+ /**
39
+ * Undefined until the first interval has completed.
40
+ */
41
+ lastStats;
42
+ stop() {
43
+ this.interval.close();
44
+ this.eld.disable();
45
+ }
46
+ }
@@ -6,5 +6,8 @@ import type { BackendRequest } from './server.model.js';
6
6
  *
7
7
  * Gets the correct full path when used from sub-router-resources.
8
8
  * Strips away the queryString.
9
+ *
10
+ * If stripPrefix (e.g `/api/v2`) is provided, and the path starts with it (like path.startsWith(stripPrefix)),
11
+ * it will be stripped from the beginning of the path.
9
12
  */
10
- export declare function getRequestEndpoint(req: BackendRequest): string;
13
+ export declare function getRequestEndpoint(req: BackendRequest, stripPrefix?: string): string;
@@ -5,11 +5,17 @@
5
5
  *
6
6
  * Gets the correct full path when used from sub-router-resources.
7
7
  * Strips away the queryString.
8
+ *
9
+ * If stripPrefix (e.g `/api/v2`) is provided, and the path starts with it (like path.startsWith(stripPrefix)),
10
+ * it will be stripped from the beginning of the path.
8
11
  */
9
- export function getRequestEndpoint(req) {
12
+ export function getRequestEndpoint(req, stripPrefix) {
10
13
  let path = (req.baseUrl + (req.route?.path || req.path)).toLowerCase();
11
14
  if (path.length > 1 && path.endsWith('/')) {
12
15
  path = path.slice(0, path.length - 1);
13
16
  }
17
+ if (stripPrefix && path.startsWith(stripPrefix)) {
18
+ path = path.slice(stripPrefix.length);
19
+ }
14
20
  return [req.method, path].join(' ');
15
21
  }
@@ -10,9 +10,11 @@ export function serverStatusMiddleware(projectDir, extra) {
10
10
  };
11
11
  }
12
12
  export function getServerStatusData(projectDir = process.cwd(), extra) {
13
- const { ts } = getDeployInfo(projectDir);
14
- const t = localTime(ts);
15
- const deployBuildTime = DEPLOY_BUILD_TIME || t.toPretty();
13
+ let deployBuildTime = DEPLOY_BUILD_TIME;
14
+ if (!deployBuildTime) {
15
+ const { ts } = getDeployInfo(projectDir);
16
+ deployBuildTime = localTime(ts).toPretty();
17
+ }
16
18
  return _filterNullishValues({
17
19
  nodeProcessStarted: getStartedStr(),
18
20
  deployBuildTime,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.25.0",
4
+ "version": "9.27.0",
5
5
  "peerDependencies": {
6
6
  "@sentry/node": "^10"
7
7
  },
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from './sentry/sentry.shared.service.js'
3
3
  export * from './server/asyncLocalStorageMiddleware.js'
4
4
  export * from './server/basicAuthMiddleware.js'
5
5
  export * from './server/bodyParserTimeoutMiddleware.js'
6
+ export * from './server/eventLoop.util.js'
6
7
  export * from './server/genericErrorMiddleware.js'
7
8
  export * from './server/logMiddleware.js'
8
9
  export * from './server/methodOverrideMiddleware.js'
@@ -0,0 +1,95 @@
1
+ import type { EventLoopUtilization, IntervalHistogram } from 'node:perf_hooks'
2
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks'
3
+ import type { NumberOfMilliseconds, NumberOfPercent } from '@naturalcycles/js-lib/types'
4
+
5
+ /**
6
+ * @experimental
7
+ */
8
+ export class EventLoopMonitor {
9
+ constructor(cfg: EventLoopMonitorCfg = {}) {
10
+ const { resolution = 20, measureInterval = 60_000 } = cfg
11
+
12
+ this.eld = monitorEventLoopDelay({ resolution })
13
+ this.eld.enable()
14
+
15
+ this.lastElu = performance.eventLoopUtilization()
16
+
17
+ this.interval = setInterval(() => {
18
+ // Delay stats are reported in **nanoseconds**
19
+ const { eld } = this
20
+ const p50 = Math.round(eld.percentile(50) / 1e6)
21
+ const p90 = Math.round(eld.percentile(90) / 1e6)
22
+ const p99 = Math.round(eld.percentile(99) / 1e6)
23
+ const max = Math.round(eld.max / 1e6)
24
+ const mean = Math.round(eld.mean / 1e6)
25
+
26
+ const currentElu = performance.eventLoopUtilization()
27
+ const deltaElu = performance.eventLoopUtilization(this.lastElu, currentElu)
28
+ this.lastElu = currentElu
29
+
30
+ const elu = Math.round(deltaElu.utilization * 100)
31
+
32
+ this.lastStats = {
33
+ p50,
34
+ p90,
35
+ p99,
36
+ max,
37
+ mean,
38
+ elu,
39
+ }
40
+
41
+ cfg.onStats?.(this.lastStats)
42
+
43
+ eld.reset()
44
+ }, measureInterval)
45
+ }
46
+
47
+ private interval: NodeJS.Timeout
48
+ private eld: IntervalHistogram
49
+ private lastElu: EventLoopUtilization
50
+ /**
51
+ * Undefined until the first interval has completed.
52
+ */
53
+ lastStats?: EventLoopStats
54
+
55
+ stop(): void {
56
+ this.interval.close()
57
+ this.eld.disable()
58
+ }
59
+
60
+ // cfg: Required<EventLoopMonitorCfg>
61
+ }
62
+
63
+ export interface EventLoopMonitorCfg {
64
+ /**
65
+ * Defaults to 20.
66
+ */
67
+ resolution?: NumberOfMilliseconds
68
+
69
+ /**
70
+ * Defaults to 60_000 ms
71
+ */
72
+ measureInterval?: NumberOfMilliseconds
73
+ /**
74
+ * Callback to be invoked with EventLoopStats.
75
+ * Called every `measureInterval` milliseconds.
76
+ */
77
+ onStats?: (stats: EventLoopStats) => void
78
+ }
79
+
80
+ export interface EventLoopStats {
81
+ p50: NumberOfMilliseconds
82
+ p90: NumberOfMilliseconds
83
+ p99: NumberOfMilliseconds
84
+ max: NumberOfMilliseconds
85
+ mean: NumberOfMilliseconds
86
+ /**
87
+ * EventLoopUtilization in percent.
88
+ *
89
+ * Calculated as:
90
+ * idle: <nanoseconds event loop was idle>,
91
+ * active: <nanoseconds event loop was busy>,
92
+ * utilization: active / (idle + active)
93
+ */
94
+ elu: NumberOfPercent
95
+ }
@@ -7,12 +7,19 @@ import type { BackendRequest } from './server.model.js'
7
7
  *
8
8
  * Gets the correct full path when used from sub-router-resources.
9
9
  * Strips away the queryString.
10
+ *
11
+ * If stripPrefix (e.g `/api/v2`) is provided, and the path starts with it (like path.startsWith(stripPrefix)),
12
+ * it will be stripped from the beginning of the path.
10
13
  */
11
- export function getRequestEndpoint(req: BackendRequest): string {
14
+ export function getRequestEndpoint(req: BackendRequest, stripPrefix?: string): string {
12
15
  let path = (req.baseUrl + (req.route?.path || req.path)).toLowerCase()
13
16
  if (path.length > 1 && path.endsWith('/')) {
14
17
  path = path.slice(0, path.length - 1)
15
18
  }
16
19
 
20
+ if (stripPrefix && path.startsWith(stripPrefix)) {
21
+ path = path.slice(stripPrefix.length)
22
+ }
23
+
17
24
  return [req.method, path].join(' ')
18
25
  }
@@ -28,9 +28,11 @@ export function getServerStatusData(
28
28
  projectDir: string = process.cwd(),
29
29
  extra?: any,
30
30
  ): Record<string, any> {
31
- const { ts } = getDeployInfo(projectDir)
32
- const t = localTime(ts)
33
- const deployBuildTime = DEPLOY_BUILD_TIME || t.toPretty()
31
+ let deployBuildTime = DEPLOY_BUILD_TIME
32
+ if (!deployBuildTime) {
33
+ const { ts } = getDeployInfo(projectDir)
34
+ deployBuildTime = localTime(ts).toPretty()
35
+ }
34
36
 
35
37
  return _filterNullishValues({
36
38
  nodeProcessStarted: getStartedStr(),