@flowrdesk/silo 1.0.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/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +320 -0
- package/index.js +20 -0
- package/package.json +36 -0
- package/src/functions.js +247 -0
- package/src/logs.js +511 -0
- package/src/settings.js +83 -0
- package/tests/demo.js +73 -0
- package/tests/log_instance_test.js +112 -0
- package/tests/memory_test.js +114 -0
package/src/logs.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 John Spriggs (Flowrdesk LLC)
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* File that handles everything related to the logging part of our app
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
// local dependencies
|
|
23
|
+
import os from 'node:os'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
import { createWriteStream } from 'node:fs'
|
|
26
|
+
|
|
27
|
+
// local settings
|
|
28
|
+
import { _config } from './settings.js'
|
|
29
|
+
|
|
30
|
+
// local functions
|
|
31
|
+
import { isString, isNumber, colors, verifyAndFormatLog, createDir, indexFile, checkTheFileSize, getTimeStamp, getDate} from './functions.js'
|
|
32
|
+
|
|
33
|
+
export default class Logs {
|
|
34
|
+
constructor({filename, level = null, maxSize = 250, txtColor, bgColor, benchmark = false, toFile = true, toTerminal = true, terminalRaw = false, maxQueueDepth = 50_000}){
|
|
35
|
+
if(!isString(filename)) throw new Error(`JS-Log_Manager supports 'Strings' for the filename only!`)
|
|
36
|
+
if(level !== null && !isString(level) && !isNumber(level)) throw new Error(`JS-Log_Manager supports 'Strings' or 'Numbers (zero or greater)' for the level only!`)
|
|
37
|
+
if(!isNumber(maxSize) || maxSize <= 0) throw new Error(`JS-Log_Manager supports 'Numbers (greater than zero)' for the maxSize only!`)
|
|
38
|
+
|
|
39
|
+
Object.assign(this, { filename, level, benchmark, toFile, toTerminal, terminalRaw })
|
|
40
|
+
|
|
41
|
+
this.txtColor = isString(txtColor) && colors.txt.hasOwnProperty(txtColor) ? colors.txt[txtColor] : null
|
|
42
|
+
this.bgColor = isString(bgColor) && colors.bg.hasOwnProperty(bgColor) ? colors.bg[bgColor] : null
|
|
43
|
+
|
|
44
|
+
// variables needed to handle the processing of the terminal logs
|
|
45
|
+
this.terminalQueue = []
|
|
46
|
+
this.terminalStream = null
|
|
47
|
+
this.isTerminalDraining = false
|
|
48
|
+
this.terminalProcessing = false
|
|
49
|
+
|
|
50
|
+
// variables needed to handle the processing of the raw terminal logs
|
|
51
|
+
this.rawTerminalQueue = []
|
|
52
|
+
this.rawTerminalStream = null
|
|
53
|
+
this.isRawTerminalDraining = false
|
|
54
|
+
this.rawTerminalProcessing = false
|
|
55
|
+
|
|
56
|
+
// variables needed to handle the processing of the file logs
|
|
57
|
+
this.doesDirecttoryExist = false
|
|
58
|
+
this.writeStream = null
|
|
59
|
+
this.streamFilename = null
|
|
60
|
+
this.isDraining = false
|
|
61
|
+
this.maxBufferSizeByKB = 1024 * _config.maxBufferSize
|
|
62
|
+
this.fileQueue = []
|
|
63
|
+
this.maxFileSizeByMB = 1024 * 1024 * maxSize
|
|
64
|
+
this.cachedFileSize = 0
|
|
65
|
+
this.isIndexing = false
|
|
66
|
+
this.checkedFileSize = false
|
|
67
|
+
this.endingWriteStream = false
|
|
68
|
+
this.fileProcessing = false
|
|
69
|
+
|
|
70
|
+
this.maxQueueDepth = maxQueueDepth
|
|
71
|
+
this.drainThreshold = Math.floor(maxQueueDepth * 0.5)
|
|
72
|
+
this.fileQueueWaiters = []
|
|
73
|
+
// ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
// variables needed to handle flush (benefit for benchmarking)
|
|
76
|
+
this.flushPromise = null
|
|
77
|
+
this.terminalFlushPromise = null
|
|
78
|
+
this.rawTerminalFlushPromise = null
|
|
79
|
+
this.resolveFlush = null
|
|
80
|
+
this.terminalResolveFlush = null
|
|
81
|
+
this.rawTerminalResolveFlush = null
|
|
82
|
+
|
|
83
|
+
// holding metadata
|
|
84
|
+
this.hostname = os.hostname()
|
|
85
|
+
this.pid = process.pid
|
|
86
|
+
|
|
87
|
+
// the start up function that handles all the processes needed when the app starts
|
|
88
|
+
this.initStart()
|
|
89
|
+
this.count = 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
waitForQueueSpace = () => {
|
|
93
|
+
if (this.fileQueue.length < this.maxQueueDepth) {
|
|
94
|
+
return Promise.resolve()
|
|
95
|
+
}
|
|
96
|
+
return new Promise(resolve => {
|
|
97
|
+
this.fileQueueWaiters.push(resolve)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_releaseWaiters = () => {
|
|
102
|
+
if (this.fileQueueWaiters.length === 0) return
|
|
103
|
+
const waiters = this.fileQueueWaiters.splice(0)
|
|
104
|
+
for (const resolve of waiters) resolve()
|
|
105
|
+
}
|
|
106
|
+
// ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
// creates a stream to handle processing the terminal's queue
|
|
109
|
+
startTerminalStream = () => {
|
|
110
|
+
const system = os.platform()
|
|
111
|
+
|
|
112
|
+
const streamPath = this.benchmark && system === 'win32' ? 'NUL'
|
|
113
|
+
: this.benchmark && (system === 'darwin' || system === 'linux') ? '/dev/null'
|
|
114
|
+
: process.stdout
|
|
115
|
+
|
|
116
|
+
const settings = streamPath === '/dev/null' || streamPath === 'NUL'
|
|
117
|
+
? {flags: 'w', highWaterMark: this.maxBufferSizeByKB}
|
|
118
|
+
: {fd: 1, highWaterMark: this.maxBufferSizeByKB}
|
|
119
|
+
|
|
120
|
+
this.terminalStream = createWriteStream(streamPath, settings)
|
|
121
|
+
this.processTerminalQue()
|
|
122
|
+
|
|
123
|
+
this.terminalStream.on('drain', () => {
|
|
124
|
+
if(this.isTerminalDraining) this.isTerminalDraining = false
|
|
125
|
+
setImmediate(() => {
|
|
126
|
+
this.processTerminalQue()
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// creates a stream to handle processing the raw terminal queue
|
|
132
|
+
startRawTerminalStream = () => {
|
|
133
|
+
const system = os.platform()
|
|
134
|
+
|
|
135
|
+
const streamPath = this.benchmark && system === 'win32' ? 'NUL'
|
|
136
|
+
: this.benchmark && (system === 'darwin' || system === 'linux') ? '/dev/null'
|
|
137
|
+
: process.stdout
|
|
138
|
+
|
|
139
|
+
const settings = streamPath === '/dev/null' || streamPath === 'NUL'
|
|
140
|
+
? {flags: 'w', highWaterMark: this.maxBufferSizeByKB}
|
|
141
|
+
: {fd: 1, highWaterMark: this.maxBufferSizeByKB}
|
|
142
|
+
|
|
143
|
+
this.rawTerminalStream = createWriteStream(streamPath, settings)
|
|
144
|
+
this.processRawTerminalQue()
|
|
145
|
+
|
|
146
|
+
this.rawTerminalStream.on('drain', () => {
|
|
147
|
+
if(this.isRawTerminalDraining) this.isRawTerminalDraining = false
|
|
148
|
+
setImmediate(() => this.processRawTerminalQue())
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// function to end the Terminal stream
|
|
153
|
+
endTerminalStream = (callback = () => {}) => {
|
|
154
|
+
if(!this.terminalStream) return
|
|
155
|
+
this.terminalStream.end(() => {
|
|
156
|
+
this.terminalStream = null
|
|
157
|
+
callback()
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// function to end the Raw Terminal stream
|
|
162
|
+
endRawTerminalStream = (callback = () => {}) => {
|
|
163
|
+
if(!this.rawTerminalStream) return
|
|
164
|
+
this.rawTerminalStream.end(() => {
|
|
165
|
+
this.rawTerminalStream = null
|
|
166
|
+
callback()
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// function that prints to the terminal with ANSI colors if provided in settings
|
|
171
|
+
terminal = (userLog, manualTime = null) => {
|
|
172
|
+
const timeStamp = manualTime ? manualTime : getTimeStamp()
|
|
173
|
+
const level = this.level ? (typeof(this.level) === 'string' ? `"level":"${this.level}",` : `"level":${this.level},`) : ''
|
|
174
|
+
const metadata = !this.benchmark
|
|
175
|
+
? `"logTime":"${timeStamp}","hostname":"${this.hostname}","pid":${this.pid},`
|
|
176
|
+
: ''
|
|
177
|
+
const verifiedLog = verifyAndFormatLog(userLog)
|
|
178
|
+
let colorCode = ''
|
|
179
|
+
if(this.txtColor) colorCode += '\x1b[' + this.txtColor + 'm'
|
|
180
|
+
if(this.bgColor) colorCode += '\x1b[' + this.bgColor + 'm'
|
|
181
|
+
const fullLog = colorCode + '{' + level + metadata + verifiedLog + '}' + colors.reset
|
|
182
|
+
|
|
183
|
+
this.terminalQueue.push(fullLog + '\n')
|
|
184
|
+
if(!this.terminalProecssing) {
|
|
185
|
+
this.terminalProecssing = true
|
|
186
|
+
setImmediate(() => {
|
|
187
|
+
this.processTerminalQue()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// function to process the terminal queue
|
|
193
|
+
processTerminalQue = () => {
|
|
194
|
+
if(this.terminalQueue.length === 0 || this.isTerminalDraining) return
|
|
195
|
+
if(!this.terminalStream){
|
|
196
|
+
this.startTerminalStream()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
while(!this.isTerminalDraining && this.terminalQueue.length > 0){
|
|
200
|
+
const terminalBackPressure = this.terminalStream.write(this.terminalQueue.shift())
|
|
201
|
+
if(!terminalBackPressure) {
|
|
202
|
+
this.isTerminalDraining = true
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// function that prints to the terminal without formatting (mainly used for benchmarking)
|
|
209
|
+
terminal_raw = (userLog, manualTime = null) => {
|
|
210
|
+
this.count++
|
|
211
|
+
const level = this.level ? (typeof(this.level) === 'string' ? `"level":"${this.level}",` : `"level":${this.level},`) : ''
|
|
212
|
+
const time = manualTime ? Date.now(_config.todayDate + manualTime) : Date.now()
|
|
213
|
+
const metadata = !this.benchmark
|
|
214
|
+
? `"logTime":"${time}","hostname":"${this.hostname}","pid":${this.pid},`
|
|
215
|
+
: ''
|
|
216
|
+
const verifiedLog = verifyAndFormatLog(userLog)
|
|
217
|
+
this.rawTerminalQueue.push('{' + level + metadata + verifiedLog + '}' + '\n')
|
|
218
|
+
if(!this.rawTerminalProcessing) {
|
|
219
|
+
this.rawTerminalProcessing = true
|
|
220
|
+
setImmediate(() => {
|
|
221
|
+
this.processRawTerminalQue()
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// function to process the raw terminal queue
|
|
227
|
+
processRawTerminalQue = () => {
|
|
228
|
+
if((this.rawTerminalQueue.length === 0 && !this.rawTerminalProcessing) || this.isRawTerminalDraining) return
|
|
229
|
+
if(!this.rawTerminalStream){
|
|
230
|
+
this.startRawTerminalStream()
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let str = ''
|
|
235
|
+
const target = this.maxBufferSizeByKB * .75
|
|
236
|
+
|
|
237
|
+
while(!this.isRawTerminalDraining && this.rawTerminalQueue.length > 0){
|
|
238
|
+
const grab = this.rawTerminalQueue.length > 5000 ? 5000 : this.rawTerminalQueue.length
|
|
239
|
+
const chucks = this.rawTerminalQueue.splice(0, grab)
|
|
240
|
+
str += chucks.join('')
|
|
241
|
+
|
|
242
|
+
if(str.length >= target || (this.rawTerminalQueue.length === 0 && str.length > 0)) {
|
|
243
|
+
const rawTerminalBackPressure = this.rawTerminalStream.write(str)
|
|
244
|
+
if(!rawTerminalBackPressure) {
|
|
245
|
+
this.isRawTerminalDraining = true
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
str = ''
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if(this.rawTerminalQueue.length > 0 && !this.isRawTerminalDraining) {
|
|
253
|
+
setImmediate(() => this.processRawTerminalQue())
|
|
254
|
+
} else if(this.rawTerminalQueue.length === 0) {
|
|
255
|
+
this.rawTerminalProcessing = false
|
|
256
|
+
if(this.rawTerminalResolveFlush) {
|
|
257
|
+
this.rawTerminalFlush()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// creates the directory for the file log folder
|
|
263
|
+
_createDir = async () => {
|
|
264
|
+
if(this.doesDirecttoryExist) return
|
|
265
|
+
await createDir()
|
|
266
|
+
this.doesDirecttoryExist = true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// creates a writable stream and handles draining
|
|
270
|
+
startWriteStream = () => {
|
|
271
|
+
if(this.writeStream || this.endingWriteStream) return
|
|
272
|
+
|
|
273
|
+
const fileDate = getDate()
|
|
274
|
+
this.streamFilename = this.filename + '_' + fileDate + '.log'
|
|
275
|
+
const filePath = path.join(_config.baseDir, this.streamFilename)
|
|
276
|
+
|
|
277
|
+
this.writeStream = createWriteStream(filePath, {
|
|
278
|
+
highWaterMark: this.maxBufferSizeByKB,
|
|
279
|
+
flags: 'a'
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
setImmediate(() => this.processFileQue())
|
|
283
|
+
|
|
284
|
+
this.writeStream.on('drain', () => {
|
|
285
|
+
this.isDraining = false
|
|
286
|
+
setImmediate(() => this.processFileQue())
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// function to end the write stream
|
|
291
|
+
endWriteStream = (callback = () => {}) => {
|
|
292
|
+
if(!this.writeStream || this.endingWriteStream) return
|
|
293
|
+
if(!this.endingWriteStream) this.endingWriteStream = true
|
|
294
|
+
this.writeStream.end(() => {
|
|
295
|
+
this.writeStream = null
|
|
296
|
+
if(this.endingWriteStream) this.endingWriteStream = false
|
|
297
|
+
callback()
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
file = async (userLog, manualTime = null) => {
|
|
302
|
+
const timeStamp = manualTime ? manualTime : getTimeStamp()
|
|
303
|
+
const level = this.level ? (typeof(this.level) === 'string' ? `"level":"${this.level}",` : `"level":${this.level},`) : ''
|
|
304
|
+
const metadata = !this.benchmark
|
|
305
|
+
? `"logTime":"${timeStamp}","hostname":"${this.hostname}","pid":${this.pid},`
|
|
306
|
+
: ''
|
|
307
|
+
const verifiedLog = verifyAndFormatLog(userLog)
|
|
308
|
+
|
|
309
|
+
this.fileQueue.push('{' + level + metadata + verifiedLog + '}' + '\n')
|
|
310
|
+
|
|
311
|
+
if(this.fileQueue.length >= this.maxQueueDepth){
|
|
312
|
+
await this.waitForQueueSpace()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if(!this.fileProcessing) {
|
|
316
|
+
this.fileProcessing = true
|
|
317
|
+
setImmediate(() => {
|
|
318
|
+
this.processFileQue()
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
processFileQue = () => {
|
|
324
|
+
if(this.fileQueue.length === 0 && !this.fileProecssing) {
|
|
325
|
+
if(this.resolveFlush) this.flush()
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
if(this.isDraining || this.isIndexing || !this.doesDirecttoryExist || this.endingWriteStream) return
|
|
329
|
+
if(!this.writeStream) {
|
|
330
|
+
this.startWriteStream()
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let str = ''
|
|
335
|
+
const targetSize = this.maxBufferSizeByKB * .80
|
|
336
|
+
|
|
337
|
+
while(!this.isDraining && !this.isIndexing && !this.endingWriteStream && this.fileQueue.length > 0) {
|
|
338
|
+
const grab = this.fileQueue.length > 5000 ? 5000 : this.fileQueue.length
|
|
339
|
+
const chunks = this.fileQueue.splice(0, grab)
|
|
340
|
+
str += chunks.join('')
|
|
341
|
+
|
|
342
|
+
if(str.length >= targetSize
|
|
343
|
+
|| (this.cachedFileSize + str.length) >= this.maxFileSizeByMB
|
|
344
|
+
|| (this.fileQueue.length === 0 && str.length > 0)) {
|
|
345
|
+
|
|
346
|
+
const noBackPressure = this.writeStream.write(str)
|
|
347
|
+
this.cachedFileSize += str.length
|
|
348
|
+
|
|
349
|
+
if(noBackPressure === false) {
|
|
350
|
+
this.isDraining = true
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if(this.cachedFileSize >= this.maxFileSizeByMB) {
|
|
354
|
+
this._indexFile(this.streamFilename)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
str = ''
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Release parked callers once queue drains to threshold ──
|
|
361
|
+
if(this.fileQueue.length <= this.drainThreshold) {
|
|
362
|
+
this._releaseWaiters()
|
|
363
|
+
}
|
|
364
|
+
// ──────────────────────────────────────────────────────────
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if(this.fileQueue.length > 0 && !this.isDraining && !this.isIndexing) {
|
|
368
|
+
setImmediate(() => this.processFileQue())
|
|
369
|
+
} else if(this.fileQueue.length === 0) {
|
|
370
|
+
this.fileProecssing = false
|
|
371
|
+
// Release any remaining waiters on full drain
|
|
372
|
+
this._releaseWaiters()
|
|
373
|
+
if(this.resolveFlush) {
|
|
374
|
+
this.flush()
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// The Unified Logging Function
|
|
380
|
+
logg = async (data) => {
|
|
381
|
+
const timeStamp = getTimeStamp()
|
|
382
|
+
|
|
383
|
+
if (this.toFile) await this.file(data, timeStamp)
|
|
384
|
+
|
|
385
|
+
if (this.toTerminal) {
|
|
386
|
+
if (this.terminalRaw) {
|
|
387
|
+
this.terminal_raw(data, timeStamp)
|
|
388
|
+
}else {
|
|
389
|
+
this.terminal(data, timeStamp)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// index the file and add the index to the filename
|
|
395
|
+
_indexFile = (filename) => {
|
|
396
|
+
if(this.isIndexing) return
|
|
397
|
+
this.isIndexing = true
|
|
398
|
+
this.streamFilename = null
|
|
399
|
+
this.endWriteStream(async () => {
|
|
400
|
+
if(this.writeStream === null) {
|
|
401
|
+
try {
|
|
402
|
+
await indexFile(filename)
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error(`[Error Handling Indexing]: ${error.message}`)
|
|
405
|
+
} finally {
|
|
406
|
+
this.isIndexing = false
|
|
407
|
+
this.cachedFileSize = 0
|
|
408
|
+
if(this.isDraining) this.isDraining = false
|
|
409
|
+
setImmediate(() => this.startWriteStream())
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// checks the size of the file if it exists and updates the cache
|
|
416
|
+
_checkTheFileSize = async () => {
|
|
417
|
+
if(this.checkedFileSize) return
|
|
418
|
+
const fileDate = getDate()
|
|
419
|
+
const filename = this.filename + '_' + fileDate + '.log'
|
|
420
|
+
const size = await checkTheFileSize(filename)
|
|
421
|
+
if(size > 0) this.cachedFileSize = size
|
|
422
|
+
this.checkedFileSize = true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// function that starts all the upfront processes
|
|
426
|
+
initStart = async () => {
|
|
427
|
+
await this._createDir()
|
|
428
|
+
await this._checkTheFileSize()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
flush = () => {
|
|
432
|
+
if(!this.flushPromise){
|
|
433
|
+
this.flushPromise = new Promise(resolve => {
|
|
434
|
+
this.resolveFlush = resolve
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
if(this.fileQueue.length === 0) {
|
|
438
|
+
if(this.writeStream){
|
|
439
|
+
this.endWriteStream(() => {
|
|
440
|
+
this.resolveFlush()
|
|
441
|
+
this.flushPromise = null
|
|
442
|
+
this.resolveFlush = null
|
|
443
|
+
})
|
|
444
|
+
} else {
|
|
445
|
+
this.resolveFlush()
|
|
446
|
+
this.flushPromise = null
|
|
447
|
+
this.resolveFlush = null
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
if(!this.fileProecssing) {
|
|
451
|
+
this.fileProecssing = true
|
|
452
|
+
setImmediate(() => this.processFileQue())
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return this.flushPromise
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
terminalFlush = () => {
|
|
459
|
+
if(!this.terminalFlushPromise) {
|
|
460
|
+
this.terminalFlushPromise = new Promise(resolve => {
|
|
461
|
+
this.terminalResolveFlush = resolve
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
if(this.terminalQueue.length === 0) {
|
|
465
|
+
if(this.terminalStream) {
|
|
466
|
+
this.endTerminalStream(() => {
|
|
467
|
+
this.terminalResolveFlush()
|
|
468
|
+
this.terminalFlushPromise = null
|
|
469
|
+
this.terminalResolveFlush = null
|
|
470
|
+
})
|
|
471
|
+
} else {
|
|
472
|
+
this.terminalResolveFlush()
|
|
473
|
+
this.terminalFlushPromise = null
|
|
474
|
+
this.terminalResolveFlush = null
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
if(!this.terminalProcessing) {
|
|
478
|
+
this.terminalProcessing = true
|
|
479
|
+
setImmediate(() => this.processTerminalQue())
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return this.terminalFlushPromise
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
rawTerminalFlush = () => {
|
|
486
|
+
if(!this.rawTerminalFlushPromise) {
|
|
487
|
+
this.rawTerminalFlushPromise = new Promise(resolve => {
|
|
488
|
+
this.rawTerminalResolveFlush = resolve
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
if(this.rawTerminalQueue.length === 0) {
|
|
492
|
+
if(this.rawTerminalStream) {
|
|
493
|
+
this.endRawTerminalStream(() => {
|
|
494
|
+
this.rawTerminalResolveFlush()
|
|
495
|
+
this.rawTerminalFlushPromise = null
|
|
496
|
+
this.rawTerminalResolveFlush = null
|
|
497
|
+
})
|
|
498
|
+
} else {
|
|
499
|
+
this.rawTerminalResolveFlush()
|
|
500
|
+
this.rawTerminalFlushPromise = null
|
|
501
|
+
this.rawTerminalResolveFlush = null
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
if(!this.rawTerminalProcessing) {
|
|
505
|
+
this.rawTerminalProcessing = true
|
|
506
|
+
setImmediate(() => this.processRawTerminalQue())
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return this.rawTerminalFlushPromise
|
|
510
|
+
}
|
|
511
|
+
}
|
package/src/settings.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 John Spriggs (Flowrdesk LLC)
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// local dependencies
|
|
18
|
+
import path from "node:path"
|
|
19
|
+
import os from "node:os"
|
|
20
|
+
|
|
21
|
+
// local functions
|
|
22
|
+
import { isString, isNumber, isFilenameSafe } from "./functions.js"
|
|
23
|
+
|
|
24
|
+
export const _config = {
|
|
25
|
+
baseDir: path.join(process.cwd(), '.logs'),
|
|
26
|
+
maxBufferSize: 256, //kb
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const appSetting = {
|
|
30
|
+
setDir: dir => {
|
|
31
|
+
if(!isString(dir)) throw new Error(`JS-Log-Manager support strings for the directory update`)
|
|
32
|
+
|
|
33
|
+
if(!isFilenameSafe(dir)) {
|
|
34
|
+
const prohibited = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '.'];
|
|
35
|
+
throw new Error(`Directory name is unsafe or contains prohibited characters. The following are NOT allowed: ${prohibited.join(' ')}`);
|
|
36
|
+
} else {
|
|
37
|
+
_config.baseDir = path.join(process.cwd(), dir)
|
|
38
|
+
return _config
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
setBufferSize: (num) => {
|
|
43
|
+
if(!isNumber(num) || num <= 0) throw new Error(`The Buffer Size(KB) must be a number that is greater than zero!`)
|
|
44
|
+
|
|
45
|
+
_config.maxBufferSize = num
|
|
46
|
+
return _config
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const bufferAutoTune = () => {
|
|
51
|
+
|
|
52
|
+
const budget = 64,
|
|
53
|
+
balanced = 128,
|
|
54
|
+
extreme = 256
|
|
55
|
+
|
|
56
|
+
const model = os.cpus()[0].model
|
|
57
|
+
|
|
58
|
+
// 1. Check for high-end "Extreme" chips
|
|
59
|
+
const isExtreme = /[iR][79]-/.test(model) || /Max|Ultra|Threadripper|EPYC/i.test(model);
|
|
60
|
+
|
|
61
|
+
// 2. Check for mid-range "Balanced" chips
|
|
62
|
+
const isBalanced = /[iR]5-/.test(model) || /Apple M[1-3](?! (Max|Ultra))/.test(model);
|
|
63
|
+
|
|
64
|
+
if(isExtreme) {
|
|
65
|
+
_config.maxBufferSize = extreme
|
|
66
|
+
console.log(`JS-Log-Manager has set your write buffer to 256kb (Extreme Mode)!`)
|
|
67
|
+
} else if(isBalanced) {
|
|
68
|
+
_config.maxBufferSize = balanced
|
|
69
|
+
console.log(`JS-Log-Manager has set your write buffer to 128kb (Balance Mode)!`)
|
|
70
|
+
} else {
|
|
71
|
+
_config.maxBufferSize = budget
|
|
72
|
+
console.log(`JS-Log-Manager has set your write buffer to 64kb (Budget Mode)!`)
|
|
73
|
+
}
|
|
74
|
+
return _config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const configuration = obj => {
|
|
78
|
+
for(const key in obj) {
|
|
79
|
+
if(key in appSetting) {
|
|
80
|
+
appSetting[key](obj[key])
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/tests/demo.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 John Spriggs (Flowrdesk LLC)
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import Logs from '../index.js';
|
|
18
|
+
|
|
19
|
+
const LOG_COUNT = parseInt(process.argv.slice(2)) || 1_000_000;
|
|
20
|
+
|
|
21
|
+
const clearMemory = () => {
|
|
22
|
+
if (global.gc) {
|
|
23
|
+
global.gc();
|
|
24
|
+
} else {
|
|
25
|
+
console.warn("Warming: GC not exposed. Run with --expose-gc for consistent results.");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const run = async () => {
|
|
30
|
+
const myLogger = new Logs({
|
|
31
|
+
filename: 'silo_demo_log',
|
|
32
|
+
level: 30,
|
|
33
|
+
benchmark: true
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const startUsage = process.cpuUsage();
|
|
37
|
+
const startMem = process.memoryUsage().heapUsed;
|
|
38
|
+
const startTime = process.hrtime.bigint();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
clearMemory()
|
|
42
|
+
console.log('🧹 GC Clearing Memory')
|
|
43
|
+
console.log(`🚀 SILO: Executing ${LOG_COUNT.toLocaleString()} logs...`);
|
|
44
|
+
for (let i = 0; i < LOG_COUNT; i++) {
|
|
45
|
+
await myLogger.file({ iteration: (i + 1), mode: 'minimalist' });
|
|
46
|
+
}
|
|
47
|
+
await myLogger.flush();
|
|
48
|
+
|
|
49
|
+
const endTime = process.hrtime.bigint();
|
|
50
|
+
const endMem = process.memoryUsage().heapUsed;
|
|
51
|
+
const timeInSecs = Number(endTime - startTime) / 1e9;
|
|
52
|
+
const endUsage = process.cpuUsage(startUsage);
|
|
53
|
+
|
|
54
|
+
console.log(`✅ SILO PASSED`);
|
|
55
|
+
console.table({
|
|
56
|
+
time: timeInSecs.toFixed(4),
|
|
57
|
+
lps: Math.round(LOG_COUNT / timeInSecs).toLocaleString(),
|
|
58
|
+
cpu: (((endUsage.user + endUsage.system) / (timeInSecs * 1000000))).toFixed(2) + '%',
|
|
59
|
+
mem: ((endMem - startMem) / 1024 / 1024).toFixed(2) + ' MB'
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(`❌ SILO FAILED`);
|
|
63
|
+
console.table({
|
|
64
|
+
Status: "FAILED",
|
|
65
|
+
Time_Elapsed: timeInSecs.toFixed(4) + "s",
|
|
66
|
+
Mem_at_Failure: ((endMem - startMem) / 1024 / 1024).toFixed(2) + " MB",
|
|
67
|
+
CPU_Usage: (((endUsage.user + endUsage.system) / (timeInSecs * 1000000)) * 100).toFixed(2) + "%"
|
|
68
|
+
});
|
|
69
|
+
console.error(err);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
run();
|