@sschepis/magazine 0.1.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 +21 -0
- package/README.md +167 -0
- package/dist/magazine.cjs.js +5 -0
- package/dist/magazine.cjs.js.map +1 -0
- package/dist/magazine.es.js +4149 -0
- package/dist/magazine.es.js.map +1 -0
- package/dist/magazine.umd.js +5 -0
- package/dist/magazine.umd.js.map +1 -0
- package/package.json +74 -0
- package/src/components/ConfigManager.js +202 -0
- package/src/components/ConfigSchema.js +506 -0
- package/src/components/DataCompressor.js +378 -0
- package/src/components/DataTransformer.js +1172 -0
- package/src/components/DebugUtils.js +657 -0
- package/src/components/EventFilter.js +474 -0
- package/src/components/EventMetadata.js +273 -0
- package/src/components/EventStore.js +443 -0
- package/src/components/EventSyncer.js +353 -0
- package/src/components/Logger.js +299 -0
- package/src/components/NetworkManager.js +461 -0
- package/src/components/Paginator.js +343 -0
- package/src/index.js +571 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import Logger from './Logger.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EventSyncer - Handles blockchain event synchronization logic
|
|
5
|
+
*/
|
|
6
|
+
export default class EventSyncer {
|
|
7
|
+
constructor(networkManager, eventStore, configManager, retryOperation) {
|
|
8
|
+
this.networkManager = networkManager;
|
|
9
|
+
this.eventStore = eventStore;
|
|
10
|
+
this.configManager = configManager;
|
|
11
|
+
this.retryOperation = retryOperation;
|
|
12
|
+
|
|
13
|
+
this.logger = new Logger({
|
|
14
|
+
context: 'EventSyncer',
|
|
15
|
+
level: configManager.get('logLevel') || 'info'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Configuration for batch processing
|
|
19
|
+
this.batchSize = 1000; // Process events in batches of 1000 blocks
|
|
20
|
+
this.lastSyncTime = 0;
|
|
21
|
+
|
|
22
|
+
this.logger.info('EventSyncer initialized', {
|
|
23
|
+
batchSize: this.batchSize
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Sync events from the blockchain to the magazine
|
|
28
|
+
async syncEvents() {
|
|
29
|
+
if (this._syncing) {
|
|
30
|
+
this.logger.warn('Sync already in progress, skipping');
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
this._setSyncing(true);
|
|
34
|
+
try {
|
|
35
|
+
return await this.logger.time('syncEvents', async () => {
|
|
36
|
+
return this.retryOperation(async () => {
|
|
37
|
+
// Get the starting block number and latest block number
|
|
38
|
+
const startBlockNumber = await this.eventStore.getStartBlockNumber();
|
|
39
|
+
const latestBlockNumber = await this.networkManager.getLatestBlockNumber();
|
|
40
|
+
|
|
41
|
+
if (startBlockNumber > latestBlockNumber) {
|
|
42
|
+
this.logger.info('Already up to date', {
|
|
43
|
+
startBlockNumber,
|
|
44
|
+
latestBlockNumber
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.logger.info('Starting event synchronization', {
|
|
50
|
+
startBlockNumber,
|
|
51
|
+
latestBlockNumber,
|
|
52
|
+
totalBlocks: latestBlockNumber - startBlockNumber + 1
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Process events in batches to avoid overwhelming the provider
|
|
56
|
+
let currentStart = startBlockNumber;
|
|
57
|
+
let totalEventsProcessed = 0;
|
|
58
|
+
|
|
59
|
+
while (currentStart <= latestBlockNumber) {
|
|
60
|
+
const currentEnd = Math.min(currentStart + this.batchSize - 1, latestBlockNumber);
|
|
61
|
+
|
|
62
|
+
const batchEvents = await this._processBatch(currentStart, currentEnd);
|
|
63
|
+
totalEventsProcessed += batchEvents;
|
|
64
|
+
currentStart = currentEnd + 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update the start block number for next sync
|
|
68
|
+
await this.eventStore.setStartBlockNumber(latestBlockNumber + 1);
|
|
69
|
+
|
|
70
|
+
this.lastSyncTime = Date.now();
|
|
71
|
+
|
|
72
|
+
this.logger.info('Event synchronization completed', {
|
|
73
|
+
totalEventsProcessed,
|
|
74
|
+
newStartBlockNumber: latestBlockNumber + 1
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return totalEventsProcessed;
|
|
78
|
+
}, 'Event synchronization');
|
|
79
|
+
});
|
|
80
|
+
} finally {
|
|
81
|
+
this._setSyncing(false);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Process a batch of blocks for events
|
|
86
|
+
async _processBatch(fromBlock, toBlock) {
|
|
87
|
+
return this.logger.time('processBatch', async () => {
|
|
88
|
+
this.logger.debug('Processing batch', { fromBlock, toBlock });
|
|
89
|
+
|
|
90
|
+
const logs = await this.networkManager.getLogs(fromBlock, toBlock);
|
|
91
|
+
let eventsStored = 0;
|
|
92
|
+
|
|
93
|
+
// Process each log
|
|
94
|
+
for (const log of logs) {
|
|
95
|
+
const parsedEvent = this.networkManager.parseLog(log);
|
|
96
|
+
|
|
97
|
+
if (parsedEvent) {
|
|
98
|
+
try {
|
|
99
|
+
// Store event with unique key to prevent duplicates
|
|
100
|
+
const eventKey = this.networkManager.createEventKey(log);
|
|
101
|
+
await this.eventStore.storeEvent(
|
|
102
|
+
parsedEvent.eventName,
|
|
103
|
+
eventKey,
|
|
104
|
+
parsedEvent.eventData
|
|
105
|
+
);
|
|
106
|
+
eventsStored++;
|
|
107
|
+
|
|
108
|
+
this.logger.trace('Event stored', {
|
|
109
|
+
eventName: parsedEvent.eventName,
|
|
110
|
+
eventKey,
|
|
111
|
+
blockNumber: log.blockNumber
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.logger.warn('Failed to store event', {
|
|
115
|
+
eventName: parsedEvent.eventName,
|
|
116
|
+
blockNumber: log.blockNumber,
|
|
117
|
+
transactionHash: log.transactionHash,
|
|
118
|
+
error: error.message
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.logger.debug('Batch processing completed', {
|
|
125
|
+
fromBlock,
|
|
126
|
+
toBlock,
|
|
127
|
+
logsFound: logs.length,
|
|
128
|
+
eventsStored
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return eventsStored;
|
|
132
|
+
}, { fromBlock, toBlock });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Sync events from a specific block range
|
|
136
|
+
async syncEventsFromRange(fromBlock, toBlock) {
|
|
137
|
+
this.configManager.validateBlockNumber(fromBlock, 'fromBlock');
|
|
138
|
+
this.configManager.validateBlockNumber(toBlock, 'toBlock');
|
|
139
|
+
|
|
140
|
+
if (fromBlock > toBlock) {
|
|
141
|
+
throw new Error('fromBlock cannot be greater than toBlock');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this._syncing) {
|
|
145
|
+
this.logger.warn('Sync already in progress, skipping range sync');
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
this._setSyncing(true);
|
|
149
|
+
try {
|
|
150
|
+
return await this.logger.time('syncEventsFromRange', async () => {
|
|
151
|
+
return this.retryOperation(async () => {
|
|
152
|
+
this.logger.info('Syncing events from specific range', {
|
|
153
|
+
fromBlock,
|
|
154
|
+
toBlock,
|
|
155
|
+
totalBlocks: toBlock - fromBlock + 1
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Process the range in batches
|
|
159
|
+
let currentStart = fromBlock;
|
|
160
|
+
let totalEventsProcessed = 0;
|
|
161
|
+
|
|
162
|
+
while (currentStart <= toBlock) {
|
|
163
|
+
const currentEnd = Math.min(currentStart + this.batchSize - 1, toBlock);
|
|
164
|
+
const batchEvents = await this._processBatch(currentStart, currentEnd);
|
|
165
|
+
totalEventsProcessed += batchEvents;
|
|
166
|
+
currentStart = currentEnd + 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.lastSyncTime = Date.now();
|
|
170
|
+
|
|
171
|
+
this.logger.info('Range synchronization completed', {
|
|
172
|
+
fromBlock,
|
|
173
|
+
toBlock,
|
|
174
|
+
totalEventsProcessed
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return totalEventsProcessed;
|
|
178
|
+
}, `Event synchronization for range ${fromBlock}-${toBlock}`);
|
|
179
|
+
}, { fromBlock, toBlock });
|
|
180
|
+
} finally {
|
|
181
|
+
this._setSyncing(false);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Sync events from a specific transaction
|
|
186
|
+
async syncEventsFromTransaction(txHash) {
|
|
187
|
+
if (!txHash || typeof txHash !== 'string') {
|
|
188
|
+
throw new Error('Transaction hash must be a non-empty string');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.retryOperation(async () => {
|
|
192
|
+
const receipt = await this.networkManager.getTransactionReceipt(txHash);
|
|
193
|
+
if (!receipt) {
|
|
194
|
+
throw new Error(`Transaction ${txHash} not found`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const contractAddress = this.configManager.get('address').toLowerCase();
|
|
198
|
+
const relevantLogs = receipt.logs.filter(
|
|
199
|
+
log => log.address.toLowerCase() === contractAddress
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
for (const log of relevantLogs) {
|
|
203
|
+
const parsedEvent = this.networkManager.parseLog(log);
|
|
204
|
+
|
|
205
|
+
if (parsedEvent) {
|
|
206
|
+
const eventKey = this.networkManager.createEventKey(log);
|
|
207
|
+
await this.eventStore.storeEvent(
|
|
208
|
+
parsedEvent.eventName,
|
|
209
|
+
eventKey,
|
|
210
|
+
parsedEvent.eventData
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.lastSyncTime = Date.now();
|
|
216
|
+
return relevantLogs.length;
|
|
217
|
+
}, `Syncing events from transaction ${txHash}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Get sync status information
|
|
221
|
+
async getSyncStatus() {
|
|
222
|
+
return this.retryOperation(async () => {
|
|
223
|
+
const startBlockNumber = await this.eventStore.getStartBlockNumber();
|
|
224
|
+
const latestBlockNumber = await this.networkManager.getLatestBlockNumber();
|
|
225
|
+
const networkInfo = await this.networkManager.getNetworkInfo();
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
nextSyncBlock: startBlockNumber,
|
|
229
|
+
latestNetworkBlock: latestBlockNumber,
|
|
230
|
+
blocksBehind: Math.max(0, latestBlockNumber - startBlockNumber + 1),
|
|
231
|
+
networkInfo,
|
|
232
|
+
isUpToDate: startBlockNumber > latestBlockNumber
|
|
233
|
+
};
|
|
234
|
+
}, 'Getting sync status');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate network and contract before syncing
|
|
238
|
+
async validateBeforeSync() {
|
|
239
|
+
await this.networkManager.validateNetwork();
|
|
240
|
+
await this.networkManager.validateContract();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Set batch size for processing (useful for performance tuning)
|
|
245
|
+
setBatchSize(size) {
|
|
246
|
+
if (!Number.isInteger(size) || size < 1) {
|
|
247
|
+
throw new Error('Batch size must be a positive integer');
|
|
248
|
+
}
|
|
249
|
+
this.batchSize = size;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Get current batch size
|
|
253
|
+
getBatchSize() {
|
|
254
|
+
return this.batchSize;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Reset sync position to a specific block
|
|
258
|
+
async resetSyncPosition(blockNumber) {
|
|
259
|
+
this.configManager.validateBlockNumber(blockNumber, 'blockNumber');
|
|
260
|
+
await this.eventStore.setStartBlockNumber(blockNumber);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Get events count by name
|
|
264
|
+
async getEventCount(eventName) {
|
|
265
|
+
const events = await this.eventStore.getEvents(eventName);
|
|
266
|
+
return events.length;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Start real-time event streaming via WebSocket or EventEmitter provider.
|
|
271
|
+
*/
|
|
272
|
+
startRealtimeSync() {
|
|
273
|
+
const provider = this.networkManager.getProvider();
|
|
274
|
+
|
|
275
|
+
if (!provider || typeof provider.on !== 'function') {
|
|
276
|
+
throw new Error(
|
|
277
|
+
'Real-time sync requires a provider that supports event subscriptions ' +
|
|
278
|
+
'(e.g., WebSocketProvider). The current provider does not have an .on() method.'
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (this._realtimeListener) {
|
|
283
|
+
this.logger.warn('Real-time sync is already running, call stopRealtimeSync() first');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const contractAddress = this.configManager.get('address');
|
|
288
|
+
const filter = { address: contractAddress };
|
|
289
|
+
|
|
290
|
+
this.logger.info('Starting real-time event sync', {
|
|
291
|
+
contractAddress,
|
|
292
|
+
providerType: provider.constructor.name
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const listener = async (log) => {
|
|
296
|
+
try {
|
|
297
|
+
const parsedEvent = this.networkManager.parseLog(log);
|
|
298
|
+
if (parsedEvent) {
|
|
299
|
+
const eventKey = this.networkManager.createEventKey(log);
|
|
300
|
+
await this.eventStore.storeEvent(
|
|
301
|
+
parsedEvent.eventName,
|
|
302
|
+
eventKey,
|
|
303
|
+
parsedEvent.eventData
|
|
304
|
+
);
|
|
305
|
+
this.logger.debug('Real-time event stored', {
|
|
306
|
+
eventName: parsedEvent.eventName,
|
|
307
|
+
eventKey,
|
|
308
|
+
blockNumber: log.blockNumber
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.logger.warn('Failed to process real-time event', {
|
|
313
|
+
blockNumber: log.blockNumber,
|
|
314
|
+
error: error.message
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
provider.on(filter, listener);
|
|
320
|
+
this._realtimeListener = listener;
|
|
321
|
+
this._realtimeFilter = filter;
|
|
322
|
+
this.logger.info('Real-time event sync started');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Stop real-time event streaming and clean up the listener.
|
|
327
|
+
*/
|
|
328
|
+
stopRealtimeSync() {
|
|
329
|
+
if (!this._realtimeListener) {
|
|
330
|
+
this.logger.debug('No real-time sync listener to stop');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const provider = this.networkManager.getProvider();
|
|
335
|
+
if (provider && typeof provider.off === 'function') {
|
|
336
|
+
provider.off(this._realtimeFilter, this._realtimeListener);
|
|
337
|
+
this.logger.info('Real-time event sync stopped');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this._realtimeListener = null;
|
|
341
|
+
this._realtimeFilter = null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check if syncer is busy (for preventing concurrent syncs)
|
|
345
|
+
isSyncing() {
|
|
346
|
+
return this._syncing || false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Set syncing state
|
|
350
|
+
_setSyncing(state) {
|
|
351
|
+
this._syncing = state;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger - Structured logging system with configurable levels
|
|
3
|
+
*/
|
|
4
|
+
export default class Logger {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.level = config.level || 'info';
|
|
7
|
+
this.context = config.context || 'Magazine';
|
|
8
|
+
this.enableConsole = config.enableConsole !== false;
|
|
9
|
+
this.enableFile = config.enableFile || false;
|
|
10
|
+
this.filePath = config.filePath || './magazine.log';
|
|
11
|
+
|
|
12
|
+
// File logging state (lazily initialized)
|
|
13
|
+
this._fsModule = null;
|
|
14
|
+
this._fsAvailable = null; // null = not yet checked, true/false after init
|
|
15
|
+
|
|
16
|
+
// Log levels with numeric values for comparison
|
|
17
|
+
this.levels = {
|
|
18
|
+
error: 0,
|
|
19
|
+
warn: 1,
|
|
20
|
+
info: 2,
|
|
21
|
+
debug: 3,
|
|
22
|
+
trace: 4
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
this.currentLevelValue = this.levels[this.level.toLowerCase()] || this.levels.info;
|
|
26
|
+
|
|
27
|
+
// Initialize file logging if enabled
|
|
28
|
+
if (this.enableFile) {
|
|
29
|
+
this._initFileLogging();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create a formatted log entry
|
|
34
|
+
_formatLog(level, message, data = {}) {
|
|
35
|
+
const timestamp = new Date().toISOString();
|
|
36
|
+
const logEntry = {
|
|
37
|
+
timestamp,
|
|
38
|
+
level: level.toUpperCase(),
|
|
39
|
+
context: this.context,
|
|
40
|
+
message,
|
|
41
|
+
...data
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
formatted: `[${timestamp}] ${level.toUpperCase()} [${this.context}] ${message}${
|
|
46
|
+
Object.keys(data).length > 0 ? ' ' + JSON.stringify(data, this._getCircularReplacer()) : ''
|
|
47
|
+
}`,
|
|
48
|
+
structured: logEntry
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get a replacer function to handle circular references in JSON.stringify
|
|
53
|
+
_getCircularReplacer() {
|
|
54
|
+
const seen = new WeakSet();
|
|
55
|
+
return (key, value) => {
|
|
56
|
+
if (typeof value === "object" && value !== null) {
|
|
57
|
+
if (seen.has(value)) {
|
|
58
|
+
return "[Circular]";
|
|
59
|
+
}
|
|
60
|
+
seen.add(value);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if current level allows logging at specified level
|
|
67
|
+
_shouldLog(level) {
|
|
68
|
+
const levelValue = this.levels[level.toLowerCase()];
|
|
69
|
+
return levelValue !== undefined && levelValue <= this.currentLevelValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Write log to console
|
|
73
|
+
_writeToConsole(level, formatted) {
|
|
74
|
+
if (!this.enableConsole) return;
|
|
75
|
+
|
|
76
|
+
switch (level.toLowerCase()) {
|
|
77
|
+
case 'error':
|
|
78
|
+
console.error(formatted);
|
|
79
|
+
break;
|
|
80
|
+
case 'warn':
|
|
81
|
+
console.warn(formatted);
|
|
82
|
+
break;
|
|
83
|
+
case 'debug':
|
|
84
|
+
case 'trace':
|
|
85
|
+
console.debug(formatted);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
console.log(formatted);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Initialize file system module for file logging
|
|
93
|
+
_initFileLogging() {
|
|
94
|
+
if (this._fsAvailable !== null) return; // already initialized
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Use dynamic import to support both Node.js and browser environments.
|
|
98
|
+
// import() returns a promise, so we kick it off here and store the result
|
|
99
|
+
// once resolved. File writes before init completes are silently skipped.
|
|
100
|
+
const fsPromise = import('fs');
|
|
101
|
+
const pathPromise = import('path');
|
|
102
|
+
|
|
103
|
+
Promise.all([fsPromise, pathPromise]).then(([fsModule, pathModule]) => {
|
|
104
|
+
this._fsModule = fsModule.default || fsModule;
|
|
105
|
+
this._pathModule = pathModule.default || pathModule;
|
|
106
|
+
this._fsAvailable = true;
|
|
107
|
+
}).catch(() => {
|
|
108
|
+
this._fsAvailable = false;
|
|
109
|
+
});
|
|
110
|
+
} catch (_err) {
|
|
111
|
+
// Static import() not available (older environments) - disable file logging
|
|
112
|
+
this._fsAvailable = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Write log entry as a JSON line to the configured file
|
|
117
|
+
_writeToFile(structured) {
|
|
118
|
+
if (!this.enableFile) return;
|
|
119
|
+
if (this._fsAvailable === false) return;
|
|
120
|
+
if (!this._fsModule) return; // fs not yet loaded (async init still pending)
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const line = JSON.stringify(structured, this._getCircularReplacer()) + '\n';
|
|
124
|
+
this._fsModule.appendFileSync(this.filePath, line, 'utf8');
|
|
125
|
+
} catch (_err) {
|
|
126
|
+
// File logging failures must not crash the application
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Rotate log file if it exceeds maxSizeBytes. Keeps 1 rotated backup.
|
|
131
|
+
rotateLogs(maxSizeBytes = 10 * 1024 * 1024) {
|
|
132
|
+
if (!this._fsModule || this._fsAvailable === false) return;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
if (!this._fsModule.existsSync(this.filePath)) return;
|
|
136
|
+
|
|
137
|
+
const stats = this._fsModule.statSync(this.filePath);
|
|
138
|
+
if (stats.size < maxSizeBytes) return;
|
|
139
|
+
|
|
140
|
+
const backupPath = this.filePath + '.1';
|
|
141
|
+
|
|
142
|
+
// Remove old backup if it exists, then rename current to backup
|
|
143
|
+
if (this._fsModule.existsSync(backupPath)) {
|
|
144
|
+
this._fsModule.unlinkSync(backupPath);
|
|
145
|
+
}
|
|
146
|
+
this._fsModule.renameSync(this.filePath, backupPath);
|
|
147
|
+
|
|
148
|
+
// Start a fresh log file
|
|
149
|
+
this._fsModule.writeFileSync(this.filePath, '', 'utf8');
|
|
150
|
+
} catch (_err) {
|
|
151
|
+
// Rotation failures must not crash the application
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Core logging method
|
|
156
|
+
_log(level, message, data = {}) {
|
|
157
|
+
if (!this._shouldLog(level)) return;
|
|
158
|
+
|
|
159
|
+
const { formatted, structured } = this._formatLog(level, message, data);
|
|
160
|
+
|
|
161
|
+
this._writeToConsole(level, formatted);
|
|
162
|
+
this._writeToFile(structured);
|
|
163
|
+
|
|
164
|
+
return structured;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Logging level methods
|
|
168
|
+
error(message, data = {}) {
|
|
169
|
+
return this._log('error', message, data);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
warn(message, data = {}) {
|
|
173
|
+
return this._log('warn', message, data);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
info(message, data = {}) {
|
|
177
|
+
return this._log('info', message, data);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
debug(message, data = {}) {
|
|
181
|
+
return this._log('debug', message, data);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
trace(message, data = {}) {
|
|
185
|
+
return this._log('trace', message, data);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Performance logging - measure function execution time
|
|
189
|
+
async time(operation, fn, context = {}) {
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
const startMessage = `Starting operation: ${operation}`;
|
|
192
|
+
|
|
193
|
+
this.debug(startMessage, context);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const result = await fn();
|
|
197
|
+
const duration = Date.now() - startTime;
|
|
198
|
+
|
|
199
|
+
this.info(`Completed operation: ${operation}`, {
|
|
200
|
+
...context,
|
|
201
|
+
duration: `${duration}ms`,
|
|
202
|
+
success: true
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const duration = Date.now() - startTime;
|
|
208
|
+
|
|
209
|
+
this.error(`Failed operation: ${operation}`, {
|
|
210
|
+
...context,
|
|
211
|
+
duration: `${duration}ms`,
|
|
212
|
+
success: false,
|
|
213
|
+
error: error?.message || error?.toString() || 'Unknown error',
|
|
214
|
+
stack: error?.stack
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create a child logger with additional context
|
|
222
|
+
child(additionalContext = {}) {
|
|
223
|
+
return new Logger({
|
|
224
|
+
level: this.level,
|
|
225
|
+
context: this.context,
|
|
226
|
+
enableConsole: this.enableConsole,
|
|
227
|
+
enableFile: this.enableFile,
|
|
228
|
+
filePath: this.filePath,
|
|
229
|
+
...additionalContext
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Change log level dynamically
|
|
234
|
+
setLevel(level) {
|
|
235
|
+
if (this.levels[level.toLowerCase()] !== undefined) {
|
|
236
|
+
this.level = level.toLowerCase();
|
|
237
|
+
this.currentLevelValue = this.levels[this.level];
|
|
238
|
+
this.info(`Log level changed to: ${level.toUpperCase()}`);
|
|
239
|
+
} else {
|
|
240
|
+
this.warn(`Invalid log level: ${level}. Valid levels: ${Object.keys(this.levels).join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Get current configuration
|
|
245
|
+
getConfig() {
|
|
246
|
+
return {
|
|
247
|
+
level: this.level,
|
|
248
|
+
context: this.context,
|
|
249
|
+
enableConsole: this.enableConsole,
|
|
250
|
+
enableFile: this.enableFile,
|
|
251
|
+
filePath: this.filePath
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Log system information
|
|
256
|
+
logSystemInfo(info = {}) {
|
|
257
|
+
this.info('System Information', {
|
|
258
|
+
nodeVersion: process.version,
|
|
259
|
+
platform: process.platform,
|
|
260
|
+
arch: process.arch,
|
|
261
|
+
memoryUsage: process.memoryUsage(),
|
|
262
|
+
uptime: process.uptime(),
|
|
263
|
+
...info
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Log network request/response
|
|
268
|
+
logNetworkActivity(type, details = {}) {
|
|
269
|
+
this.debug(`Network ${type}`, {
|
|
270
|
+
type,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
...details
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Log blockchain interaction
|
|
277
|
+
logBlockchainActivity(operation, details = {}) {
|
|
278
|
+
this.info(`Blockchain: ${operation}`, {
|
|
279
|
+
operation,
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
...details
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Log Gun.js database activity
|
|
286
|
+
logDatabaseActivity(operation, details = {}) {
|
|
287
|
+
this.debug(`Database: ${operation}`, {
|
|
288
|
+
operation,
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
...details
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Export a default logger instance
|
|
296
|
+
export const defaultLogger = new Logger({
|
|
297
|
+
level: 'info',
|
|
298
|
+
context: 'Magazine'
|
|
299
|
+
});
|