@nxtedition/lib 28.0.1 → 28.0.3

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/app.d.ts CHANGED
@@ -46,6 +46,8 @@ export interface App<
46
46
  | Subscription
47
47
  | ((logger: Logger) => void)
48
48
  | ((logger: Logger) => Promise<void>)
49
+ | (() => void)
50
+ | (() => Promise<void>)
49
51
  | Disposable
50
52
  | AsyncDisposable
51
53
  >
package/app.js CHANGED
@@ -41,6 +41,7 @@ import { isTimeBetween } from './time.js'
41
41
  import makeUnderPressure from './under-pressure.js'
42
42
  import { nice } from '@nxtedition/sched'
43
43
  import { setAffinity } from './numa.js'
44
+ import { getContainerMemoryLimit, getContainerMemoryUsage } from './memory.js'
44
45
 
45
46
  /**
46
47
  * @param {object} appConfig
@@ -670,6 +671,8 @@ export function makeApp(appConfig, onTerminateOrMeta, metaOrNull) {
670
671
  heapUsed: mem.heapUsed,
671
672
  external: mem.external,
672
673
  arrayBuffers: mem.arrayBuffers,
674
+ containerLimit: getContainerMemoryLimit(),
675
+ containerUsage: getContainerMemoryUsage(),
673
676
  totalHeapTotal: 0,
674
677
  totalHeapUsed: 0,
675
678
  totalExternal: 0,
@@ -776,16 +779,45 @@ export function makeApp(appConfig, onTerminateOrMeta, metaOrNull) {
776
779
  rx.repeatWhen((complete$) => complete$.pipe(rx.delay(10e3))),
777
780
  ),
778
781
  stats$.pipe(
779
- rx.map(({ undici: stats }) => {
780
- return stats
781
- ? [
782
- {
783
- id: 'app:undici_upstream_sockets',
784
- level: stats.sockets > 8192 ? 50 : stats.sockets > 4096 ? 40 : 30,
785
- msg: `Undici: ${stats.sockets} upstream connected`,
786
- },
787
- ]
788
- : []
782
+ rx.map(({ memory, heap, utilization, undici }) => {
783
+ const messages = []
784
+
785
+ if (memory?.containerLimit) {
786
+ const usagePercent = (memory.containerUsage / memory.containerLimit) * 100
787
+ messages.push({
788
+ id: 'app:container_memory_usage',
789
+ level: usagePercent > 90 ? 50 : usagePercent > 70 ? 40 : 30,
790
+ msg: `Memory Usage: ${usagePercent.toFixed(2)}%`,
791
+ })
792
+ }
793
+
794
+ if (heap) {
795
+ const usagePercent = (heap.used_heap_size / heap.heap_size_limit) * 100
796
+ messages.push({
797
+ id: 'app:heap_memory_usage',
798
+ level: usagePercent > 90 ? 50 : usagePercent > 70 ? 40 : 30,
799
+ msg: `Heap Usage: ${usagePercent.toFixed(2)}%`,
800
+ })
801
+ }
802
+
803
+ if (utilization) {
804
+ const elp = utilization.utilization * 100
805
+ messages.push({
806
+ id: 'app:event_loop_utilization',
807
+ level: elp > 95 ? 50 : elp > 80 ? 40 : 30,
808
+ msg: `Event Loop Utilization: ${elp.toFixed(2)}%`,
809
+ })
810
+ }
811
+
812
+ if (undici) {
813
+ messages.push({
814
+ id: 'app:undici_upstream_sockets',
815
+ level: undici.sockets > 8192 ? 50 : undici.sockets > 4096 ? 40 : 30,
816
+ msg: `Undici: ${undici.sockets} upstream connected`,
817
+ })
818
+ }
819
+
820
+ return messages
789
821
  }),
790
822
  ),
791
823
  toobusy?.appLag$.pipe(
package/logger.js CHANGED
@@ -1,16 +1,15 @@
1
- import assert from 'node:assert'
2
1
  import { isMainThread } from 'node:worker_threads'
3
2
  import serializers from './serializers.js'
4
3
  import pino from 'pino'
5
4
 
6
5
  const isProduction = process.env.NODE_ENV === 'production'
7
6
 
8
- export function createLogger(
9
- { level = isProduction ? 'debug' : 'trace', flushInterval = 1e3, stream = null, ...options } = {},
10
- onTerminate,
11
- ) {
12
- assert(!onTerminate)
13
-
7
+ export function createLogger({
8
+ level = isProduction ? 'debug' : 'trace',
9
+ flushInterval = 1e3,
10
+ stream,
11
+ ...options
12
+ } = {}) {
14
13
  if (!stream) {
15
14
  if (
16
15
  process.stdout.write !== process.stdout.constructor.prototype.write ||
@@ -23,15 +22,24 @@ export function createLogger(
23
22
  if (stream) {
24
23
  // Do nothing...
25
24
  } else if (!isProduction) {
26
- stream = pino.destination({ fd: process.stdout.fd ?? 1, sync: true })
25
+ stream = pino.destination({ fd: process.stdout.fd ?? 1, sync: true, fsync: false })
27
26
  } else if (!isMainThread) {
28
27
  // TODO (perf): Async mode doesn't work super well in workers.
29
- stream = pino.destination({ fd: 1, sync: true })
28
+ stream = pino.destination({ fd: 1, sync: true, fsync: false })
30
29
  } else {
31
- stream = pino.destination({ sync: false, minLength: 4 * 1024, maxWrite: 32 * 1024 })
30
+ stream = pino.destination({
31
+ sync: false,
32
+ fsync: false,
33
+ minLength: 4 * 1024,
34
+ maxWrite: 32 * 1024,
35
+ })
32
36
 
33
37
  let flushing = 0
34
- setInterval(() => {
38
+ const onFlush = () => {
39
+ flushing--
40
+ }
41
+
42
+ const flushTimeout = setInterval(() => {
35
43
  if (flushing >= 10) {
36
44
  try {
37
45
  logger.warn('logger is flushing too slow')
@@ -41,10 +49,9 @@ export function createLogger(
41
49
  }
42
50
  } else {
43
51
  flushing++
44
- stream.flush(() => {
45
- flushing--
46
- })
52
+ stream.flush(onFlush)
47
53
  }
54
+ flushTimeout.refresh()
48
55
  }, flushInterval).unref()
49
56
  }
50
57
 
package/memory.js ADDED
@@ -0,0 +1,43 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ function readFile(path) {
4
+ try {
5
+ return readFileSync(path, 'utf8').trim()
6
+ } catch {
7
+ return null
8
+ }
9
+ }
10
+
11
+ export function getContainerMemoryLimit() {
12
+ // cgroups v2
13
+ const v2Limit = readFile('/sys/fs/cgroup/memory.max')
14
+ if (v2Limit) {
15
+ return v2Limit === 'max' ? null : Number(v2Limit)
16
+ }
17
+
18
+ // cgroups v1
19
+ const v1Limit = readFile('/sys/fs/cgroup/memory/memory.limit_in_bytes')
20
+ if (v1Limit) {
21
+ const limit = Number(v1Limit)
22
+ // Very large number usually means "no limit"
23
+ return limit > 1e15 ? null : limit
24
+ }
25
+
26
+ return undefined
27
+ }
28
+
29
+ export function getContainerMemoryUsage() {
30
+ // cgroups v2
31
+ const v2Usage = readFile('/sys/fs/cgroup/memory.current')
32
+ if (v2Usage) {
33
+ return Number(v2Usage)
34
+ }
35
+
36
+ // cgroups v1
37
+ const v1Usage = readFile('/sys/fs/cgroup/memory/memory.usage_in_bytes')
38
+ if (v1Usage) {
39
+ return Number(v1Usage)
40
+ }
41
+
42
+ return undefined
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "28.0.1",
3
+ "version": "28.0.3",
4
4
  "license": "UNLICENSED",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  "shared.js",
25
25
  "logger.js",
26
26
  "logger.d.ts",
27
+ "memory.js",
27
28
  "mime.js",
28
29
  "numa.js",
29
30
  "proxy.js",
@@ -36,6 +37,7 @@
36
37
  "errors.js",
37
38
  "errors.d.ts",
38
39
  "worker.js",
40
+ "slice.js",
39
41
  "stream.js",
40
42
  "transcript.js",
41
43
  "docker-secrets.js",
@@ -90,5 +92,5 @@
90
92
  "pino": ">=7.0.0",
91
93
  "rxjs": "^7.0.0"
92
94
  },
93
- "gitHead": "2d70510dc7bad9dc43cf36884a293948bc23a6a6"
95
+ "gitHead": "15f4fd00f86a558ae56672b1efa3e909b23feb3f"
94
96
  }
package/serializers.js CHANGED
@@ -118,6 +118,7 @@ export default {
118
118
  req: (req) =>
119
119
  req && {
120
120
  id: req.id || getHeader(req, 'request-id'),
121
+ httpVersion: req.httpVersion,
121
122
  method: req.method,
122
123
  target: getTarget(req),
123
124
  url: getUrl(req),
@@ -138,6 +139,7 @@ export default {
138
139
  ureq: (ureq) =>
139
140
  ureq && {
140
141
  id: ureq.id || getHeader(ureq, 'request-id'),
142
+ httpVersion: ureq.httpVersion,
141
143
  method: ureq.method,
142
144
  target: getTarget(ureq),
143
145
  url: getUrl(ureq),
@@ -154,6 +156,7 @@ export default {
154
156
  res: (res) =>
155
157
  res && {
156
158
  id: res.id || getHeader(res, 'request-id') || getHeader(res.req, 'request-id'),
159
+ httpVersion: res.httpVersion,
157
160
  headers: getHeaders(res),
158
161
  statusCode: res.statusCode || res.status,
159
162
  timing: res.timing,
@@ -171,6 +174,7 @@ export default {
171
174
  ures: (ures) =>
172
175
  ures && {
173
176
  id: ures.id || getHeader(ures, 'request-id') || getHeader(ures.req, 'request-id'),
177
+ httpVersion: ures.httpVersion,
174
178
  headers: getHeaders(ures),
175
179
  statusCode: ures.statusCode ?? ures.status,
176
180
  timing: ures.timing,
package/slice.js ADDED
@@ -0,0 +1,278 @@
1
+ import util from 'node:util'
2
+
3
+ const EMPTY_BUF = Buffer.alloc(0)
4
+ const POOL = []
5
+
6
+ export class Slice {
7
+ buffer
8
+ byteOffset = 0
9
+ byteLength = 0
10
+ maxByteLength = 0
11
+
12
+ static EMPTY_BUF = EMPTY_BUF
13
+
14
+ /**
15
+ *
16
+ * @param {Buffer} [buffer]
17
+ * @param {number} [byteOffset]
18
+ * @param {number} [byteLength]
19
+ * @param {number} [maxByteLength]
20
+ */
21
+ constructor(
22
+ buffer = Slice.EMPTY_BUF,
23
+ byteOffset = 0,
24
+ byteLength = buffer.byteLength,
25
+ maxByteLength = byteLength,
26
+ ) {
27
+ if (byteOffset < 0 || !Number.isInteger(byteOffset)) {
28
+ throw new RangeError(`Invalid byteOffset: ${byteOffset}`)
29
+ }
30
+
31
+ if (byteLength < 0 || !Number.isInteger(byteLength)) {
32
+ throw new RangeError(`Invalid byteLength: ${byteLength}`)
33
+ }
34
+
35
+ if (
36
+ maxByteLength < byteLength ||
37
+ maxByteLength > buffer.byteLength ||
38
+ !Number.isInteger(maxByteLength)
39
+ ) {
40
+ throw new RangeError(`Invalid maxByteLength: ${maxByteLength}`)
41
+ }
42
+
43
+ this.buffer = buffer
44
+ this.byteOffset = byteOffset
45
+ this.byteLength = byteLength
46
+ this.maxByteLength = maxByteLength
47
+ }
48
+
49
+ static create(buffer, byteOffset, byteLength, maxByteLength) {
50
+ if (buffer === undefined) {
51
+ buffer = Slice.EMPTY_BUF
52
+ }
53
+ if (byteOffset === undefined) {
54
+ byteOffset = 0
55
+ }
56
+ if (byteLength === undefined) {
57
+ byteLength = buffer.byteLength
58
+ }
59
+ if (maxByteLength === undefined) {
60
+ maxByteLength = byteLength
61
+ }
62
+
63
+ const slice = POOL.pop()
64
+
65
+ if (slice) {
66
+ slice.buffer = buffer
67
+ slice.byteOffset = byteOffset
68
+ slice.byteLength = byteLength
69
+ slice.maxByteLength = maxByteLength
70
+ return slice
71
+ }
72
+
73
+ return new Slice(buffer, byteOffset, byteLength, maxByteLength)
74
+ }
75
+
76
+ static free(slice) {
77
+ if (slice == null) {
78
+ return
79
+ }
80
+
81
+ slice.reset()
82
+
83
+ if (POOL.length < 16 * 1024) {
84
+ POOL.push(slice)
85
+ }
86
+ }
87
+
88
+ reset() {
89
+ this.buffer = Slice.EMPTY_BUF
90
+ this.byteOffset = 0
91
+ this.byteLength = 0
92
+ this.maxByteLength = 0
93
+ }
94
+
95
+ get length() {
96
+ return this.byteLength
97
+ }
98
+
99
+ /**
100
+ * @param {Buffer|Slice} target
101
+ * @param {number} [targetStart = 0]
102
+ * @param {number} [sourceStart = 0]
103
+ * @param {number} [sourceEnd = this.byteOffset]
104
+ * @returns {number} Number of bytes written.
105
+ */
106
+ copy(target, targetStart, sourceStart, sourceEnd) {
107
+ if (target instanceof Slice) {
108
+ if (targetStart === undefined) {
109
+ targetStart = target.byteOffset
110
+ } else {
111
+ targetStart += target.byteOffset
112
+ }
113
+
114
+ target = target.buffer
115
+ }
116
+
117
+ if (sourceStart === undefined) {
118
+ sourceStart = this.byteOffset
119
+ } else {
120
+ sourceStart += this.byteOffset
121
+ }
122
+
123
+ if (sourceEnd === undefined) {
124
+ sourceEnd = this.byteLength + this.byteOffset
125
+ } else {
126
+ sourceEnd += this.byteOffset
127
+ }
128
+
129
+ return this.buffer.copy(target, targetStart, sourceStart, sourceEnd)
130
+ }
131
+
132
+ /**
133
+ * @param {Buffer|Slice} target
134
+ * @param {number} [targetStart = 0]
135
+ * @param {number} [targetEnd = target.byteOffset]
136
+ * @param {number} [sourceStart = 0]
137
+ * @param {number} [sourceEnd = this.byteOffset]
138
+ * @returns
139
+ */
140
+ compare(target, targetStart, targetEnd, sourceStart, sourceEnd) {
141
+ if (target instanceof Slice) {
142
+ if (targetStart === undefined) {
143
+ targetStart = target.byteOffset
144
+ } else {
145
+ targetStart += target.byteOffset
146
+ }
147
+
148
+ if (targetEnd === undefined) {
149
+ targetEnd = target.byteLength + target.byteOffset
150
+ } else {
151
+ targetEnd += target.byteOffset
152
+ }
153
+
154
+ target = target.buffer
155
+ }
156
+
157
+ if (sourceStart === undefined) {
158
+ sourceStart = this.byteOffset
159
+ } else {
160
+ sourceStart += this.byteOffset
161
+ }
162
+
163
+ if (sourceEnd === undefined) {
164
+ sourceEnd = this.byteLength + this.byteOffset
165
+ } else {
166
+ sourceEnd += this.byteOffset
167
+ }
168
+
169
+ return this.buffer.compare(target, targetStart, targetEnd, sourceStart, sourceEnd)
170
+ }
171
+
172
+ /**
173
+ *
174
+ * @param {string} string
175
+ * @param {number} [offset=0]
176
+ * @param {number} [length=this.byteLength - offset]
177
+ * @param {NodeJS.BufferEncoding} [encoding='utf8']
178
+ * @returns {number} Number of bytes written.
179
+ */
180
+ write(string, offset, length, encoding) {
181
+ if (offset === undefined) {
182
+ offset = this.byteOffset
183
+ } else {
184
+ offset += this.byteOffset
185
+ }
186
+
187
+ if (length === undefined) {
188
+ length = this.byteLength + this.byteOffset - offset
189
+ }
190
+
191
+ return this.buffer.write(string, offset, length, encoding)
192
+ }
193
+
194
+ set(array, offset) {
195
+ if (array == null) {
196
+ return
197
+ }
198
+
199
+ if (offset === undefined) {
200
+ offset = this.byteOffset
201
+ } else {
202
+ offset += this.byteOffset
203
+ }
204
+
205
+ array?.copy(this.buffer, offset)
206
+ }
207
+
208
+ at(index) {
209
+ return index >= 0
210
+ ? this.buffer[this.byteOffset + index]
211
+ : this.buffer[this.byteOffset + this.byteLength + index]
212
+ }
213
+
214
+ test(expr) {
215
+ return expr.test(this.buffer, this.byteOffset, this.byteLength)
216
+ }
217
+
218
+ readBigUInt64BE(offset = 0) {
219
+ if (offset === undefined) {
220
+ offset = this.byteOffset
221
+ } else {
222
+ offset += this.byteOffset
223
+ }
224
+
225
+ return this.buffer.readBigUInt64BE(offset)
226
+ }
227
+
228
+ /**
229
+ *
230
+ * @param {BufferEncoding} [encoding='utf8']
231
+ * @param {number} [start=0]
232
+ * @param {number} [end=this.byteLength]
233
+ * @returns {string}
234
+ */
235
+ toString(encoding, start, end) {
236
+ if (start === undefined) {
237
+ start = this.byteOffset
238
+ } else {
239
+ start += this.byteOffset
240
+ }
241
+
242
+ if (end === undefined) {
243
+ end = this.byteLength + this.byteOffset
244
+ } else {
245
+ end += this.byteOffset
246
+ }
247
+
248
+ return this.buffer.toString(encoding, start, end)
249
+ }
250
+
251
+ toBuffer(start, end) {
252
+ if (start === undefined) {
253
+ start = this.byteOffset
254
+ } else {
255
+ start += this.byteOffset
256
+ }
257
+
258
+ if (end === undefined) {
259
+ end = this.byteLength + this.byteOffset
260
+ } else {
261
+ end += this.byteOffset
262
+ }
263
+
264
+ return this.buffer.subarray(start, end)
265
+ }
266
+
267
+ [Symbol.toStringTag]() {
268
+ return this.toString()
269
+ }
270
+
271
+ [util.inspect.custom](depth, options, inspect) {
272
+ const bytes = []
273
+ for (let i = 0; i < this.byteLength; i++) {
274
+ bytes.push(this.buffer[this.byteOffset + i].toString(16).padStart(2, '0'))
275
+ }
276
+ return `Slice: "${this.toString()}" <${bytes.join(' ')}>`
277
+ }
278
+ }