@viplance/nestjs-logger 0.4.7 → 0.4.9

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.
@@ -11,6 +11,10 @@ const logTypes = Object.keys(selectedLogTypes).filter((key) => key !== `all`);
11
11
 
12
12
  let logs = [];
13
13
  let text = '';
14
+ let currentPage = 1;
15
+ let isLoading = false;
16
+ let hasMore = true;
17
+ const limit = 10;
14
18
 
15
19
  connectWebSocket();
16
20
 
@@ -36,7 +40,9 @@ document.addEventListener(`click`, (e) => {
36
40
  });
37
41
  }
38
42
 
39
- renderLogs();
43
+ currentPage = 1;
44
+ hasMore = true;
45
+ getLogs(1);
40
46
 
41
47
  return;
42
48
  }
@@ -53,7 +59,9 @@ document.addEventListener(`click`, (e) => {
53
59
  selectedLogTypes[`all`] = false;
54
60
  unsetSelectorActive(document.querySelector(`li.all`));
55
61
 
56
- getLogs();
62
+ currentPage = 1;
63
+ hasMore = true;
64
+ getLogs(1);
57
65
 
58
66
  return;
59
67
  }
@@ -141,24 +149,9 @@ function getLogHtmlElement(log) {
141
149
  function renderLogs(logList = logs) {
142
150
  let html = '';
143
151
 
144
- logList
145
- .filter((log) => {
146
- return selectedLogTypes['all'] || selectedLogTypes[log.type];
147
- })
148
- .filter((log) => {
149
- if (text === '') return true;
150
-
151
- return (
152
- log.message.toLowerCase().includes(text) ||
153
- log.trace?.toLowerCase().includes(text) ||
154
- JSON.stringify(log.context || {})
155
- .toLowerCase()
156
- .includes(text)
157
- );
158
- })
159
- .forEach((log) => {
160
- html += getLogHtmlElement(log);
161
- });
152
+ logList.forEach((log) => {
153
+ html += getLogHtmlElement(log);
154
+ });
162
155
 
163
156
  document.getElementById('logs').innerHTML = html;
164
157
  }
@@ -166,40 +159,83 @@ function renderLogs(logList = logs) {
166
159
  async function checkElementsVisibility(logList = logs) {
167
160
  if (logList.length === 0) {
168
161
  document.getElementById('no-logs').style.display = 'block';
169
- document.getElementById('search').style.display = 'none';
170
162
  document.querySelector('.table-header').style.display = 'none';
171
- document.querySelector('nav').style.display = 'none';
163
+ document.querySelector('nav').style.display = 'flex';
172
164
  } else {
173
165
  document.getElementById('no-logs').style.display = 'none';
174
- document.getElementById('search').style.display = 'inline-block';
175
166
  document.querySelector('.table-header').style.display = 'flex';
176
167
  document.querySelector('nav').style.display = 'flex';
177
168
  }
178
169
  }
179
170
 
180
- async function getLogs() {
181
- const { origin, pathname, search } = window.location;
182
- const searchParams = new URLSearchParams(search);
171
+ async function getLogs(page = 1) {
172
+ if (isLoading && page > 1) return;
173
+
174
+ if (page > 1 && !hasMore) return;
175
+
176
+ isLoading = true;
177
+ currentPage = page;
178
+ document.getElementById('loader').style.display = 'block';
179
+
180
+ const { origin, pathname, search: urlSearch } = window.location;
181
+ const searchParams = new URLSearchParams(urlSearch);
183
182
  const key = searchParams.get('key');
184
183
 
185
- if (!!socket) {
184
+ const types = selectedLogTypes.all
185
+ ? []
186
+ : Object.keys(selectedLogTypes).filter(
187
+ (key) => selectedLogTypes[key] && key !== 'all',
188
+ );
189
+
190
+ if (!!socket && socket.readyState === WebSocket.OPEN) {
186
191
  socket.send(
187
192
  JSON.stringify({
188
193
  action: 'getLogs',
189
194
  key,
195
+ page,
196
+ limit,
197
+ search: text,
198
+ types,
190
199
  }),
191
200
  );
192
201
  } else {
193
- const res = await fetch(`${origin}${pathname}api${search}`);
202
+ const apiParams = new URLSearchParams(urlSearch);
203
+ apiParams.set('page', page);
204
+ apiParams.set('limit', limit);
205
+
206
+ if (text) {
207
+ apiParams.set('search', text);
208
+ }
209
+
210
+ if (types.length > 0) {
211
+ apiParams.set('types', types.join(','));
212
+ }
213
+
214
+ const res = await fetch(`${origin}${pathname}api?${apiParams.toString()}`);
194
215
 
195
216
  if (res.ok) {
196
- logs = await res.json();
217
+ const newLogs = await res.json();
197
218
 
198
- checkElementsVisibility();
219
+ if (page === 1) {
220
+ logs = newLogs;
221
+ } else {
222
+ logs = logs.concat(newLogs);
223
+ }
224
+
225
+ hasMore = newLogs.length === limit;
226
+ isLoading = false;
227
+ document.getElementById('loader').style.display = 'none';
199
228
 
229
+ checkElementsVisibility();
200
230
  renderLogs();
201
231
  checkAndUpdatePopup();
232
+
233
+ if (hasMore) {
234
+ setTimeout(checkScrollAnchorVisibility, 100);
235
+ }
202
236
  } else {
237
+ isLoading = false;
238
+ document.getElementById('loader').style.display = 'none';
203
239
  alert('An error occurred while fetching logs.');
204
240
  }
205
241
  }
@@ -244,8 +280,104 @@ async function deleteLog(_id) {
244
280
  }
245
281
  }
246
282
 
283
+ let searchTimeout;
247
284
  function search(event) {
248
285
  text = event.target.value.toLowerCase();
249
286
 
287
+ clearTimeout(searchTimeout);
288
+ searchTimeout = setTimeout(() => {
289
+ currentPage = 1;
290
+ hasMore = true;
291
+ getLogs(1);
292
+ }, 300);
293
+ }
294
+
295
+ // Infinite scrolling
296
+ const observer = new IntersectionObserver(
297
+ (entries) => {
298
+ if (entries[0].isIntersecting && !isLoading && hasMore) {
299
+ getLogs(currentPage + 1);
300
+ }
301
+ },
302
+ { threshold: 0.1 },
303
+ );
304
+
305
+ document.addEventListener('DOMContentLoaded', () => {
306
+ const scrollAnchor = document.getElementById('scroll-anchor');
307
+ if (scrollAnchor) {
308
+ observer.observe(scrollAnchor);
309
+ }
310
+ });
311
+
312
+ function matchesFilter(log) {
313
+ // Check types
314
+ if (!selectedLogTypes['all'] && !selectedLogTypes[log.type]) {
315
+ return false;
316
+ }
317
+
318
+ // Check search text
319
+ if (text !== '') {
320
+ const matches =
321
+ log.message.toLowerCase().includes(text) ||
322
+ log.trace?.toLowerCase().includes(text) ||
323
+ JSON.stringify(log.context || {})
324
+ .toLowerCase()
325
+ .includes(text);
326
+
327
+ if (!matches) return false;
328
+ }
329
+
330
+ return true;
331
+ }
332
+
333
+ function handleWsInsert(log) {
334
+ if (matchesFilter(log)) {
335
+ logs.unshift(log);
336
+ checkElementsVisibility();
337
+ renderLogs();
338
+ }
339
+ }
340
+
341
+ function handleWsUpdate(updatedLog) {
342
+ const idx = logs.findIndex((l) => l._id === updatedLog._id);
343
+ if (idx > -1) {
344
+ logs.splice(idx, 1);
345
+ }
346
+
347
+ if (matchesFilter(updatedLog)) {
348
+ logs.unshift(updatedLog);
349
+ }
350
+
351
+ checkElementsVisibility();
250
352
  renderLogs();
353
+ checkAndUpdatePopup();
251
354
  }
355
+
356
+ function handleWsDelete(id) {
357
+ const idx = logs.findIndex((l) => l._id === id);
358
+ if (idx > -1) {
359
+ logs.splice(idx, 1);
360
+ checkElementsVisibility();
361
+ renderLogs();
362
+ checkAndUpdatePopup();
363
+ }
364
+ }
365
+
366
+ function checkScrollAnchorVisibility() {
367
+ if (isLoading || !hasMore) return;
368
+ const scrollAnchor = document.getElementById('scroll-anchor');
369
+ if (!scrollAnchor) return;
370
+
371
+ const rect = scrollAnchor.getBoundingClientRect();
372
+ const isVisible = rect.top < window.innerHeight;
373
+
374
+ if (isVisible) {
375
+ getLogs(currentPage + 1);
376
+ }
377
+ }
378
+
379
+ window.addEventListener('resize', () => {
380
+ if (hasMore) {
381
+ checkScrollAnchorVisibility();
382
+ }
383
+ });
@@ -21,8 +21,8 @@ function showLogDetails(log) {
21
21
  )}.&nbsp;&nbsp;&nbsp;First seen: ${getDate(log.createdAt)}`;
22
22
 
23
23
  popup.innerHTML = `
24
- <div id="drag-handle" style="margin: -2rem -2rem 1rem -2rem; height: 1.5rem; background-color: #f1f1f1; border-bottom: 1px solid #ddd; cursor: grab; display: flex; align-items: center; justify-content: center;" title="Drag to move">
25
- <div style="width: 50px; height: 5px; background-color: #ccc; border-radius: 5px;"></div>
24
+ <div id="drag-handle" title="Drag to move">
25
+ <div class="handle-indicator"></div>
26
26
  </div>
27
27
  <div class="content center">
28
28
  <div class="container">
@@ -1,8 +1,17 @@
1
1
  // WebSocket connection
2
2
  let socket;
3
+ let connected = false;
4
+ let connectionAttempts = 0;
3
5
  let frozen = false;
4
6
 
5
7
  async function connectWebSocket() {
8
+ connectionAttempts++;
9
+
10
+ if (connectionAttempts > 3) {
11
+ alert('Failed to connect to WebSocket. Check the `key` url parameter.');
12
+ return;
13
+ }
14
+
6
15
  const { hostname, origin, pathname, search } = window.location;
7
16
 
8
17
  const res = await fetch(`${origin}${pathname}settings${search}`);
@@ -39,11 +48,14 @@ async function connectWebSocket() {
39
48
  };
40
49
 
41
50
  socket.onopen = (event) => {
51
+ connected = true;
52
+ connectionAttempts = 0;
42
53
  getLogs();
43
54
  };
44
55
 
45
56
  socket.onclose = (event) => {
46
- console.log(event);
57
+ connected = false;
58
+ console.error(event);
47
59
  setTimeout(connectWebSocket, 5000);
48
60
  };
49
61
 
@@ -53,24 +65,42 @@ async function connectWebSocket() {
53
65
  if (data['action'] && !frozen) {
54
66
  switch (data['action']) {
55
67
  case 'list':
56
- logs = data['data'];
68
+ if (currentPage === 1) {
69
+ logs = data['data'];
70
+ } else {
71
+ logs = logs.concat(data['data']);
72
+ }
73
+ hasMore = data['data'].length === limit;
74
+ isLoading = false;
75
+ document.getElementById('loader').style.display = 'none';
57
76
  checkElementsVisibility(logs);
58
77
  renderLogs(logs);
59
78
  checkAndUpdatePopup();
79
+
80
+ if (hasMore) {
81
+ setTimeout(checkScrollAnchorVisibility, 100);
82
+ }
60
83
  break;
61
84
  case 'insert':
62
- getLogs();
85
+ if (currentPage === 1) {
86
+ handleWsInsert(data['data']);
87
+ }
63
88
  break;
64
89
  case 'update':
65
- getLogs();
90
+ handleWsUpdate(data['data']);
66
91
  break;
67
92
  case 'delete':
68
- getLogs();
93
+ handleWsDelete(data['data']._id);
69
94
  break;
70
95
  }
71
96
  }
72
97
  };
98
+
99
+ setTimeout(() => {
100
+ if (!connected) connectWebSocket(); // fix for Safari browser
101
+ }, 300);
73
102
  }
103
+
74
104
  function sendMessage(message) {
75
105
  socket.send(JSON.stringify(message));
76
106
  }
@@ -54,7 +54,7 @@ h3 {
54
54
  }
55
55
 
56
56
  #logs {
57
- margin-top: 4rem;
57
+ margin-top: 7rem;
58
58
  }
59
59
 
60
60
  #no-logs {
@@ -63,6 +63,18 @@ h3 {
63
63
  text-align: center;
64
64
  }
65
65
 
66
+ #loader {
67
+ display: none;
68
+ text-align: center;
69
+ padding: 1rem;
70
+ color: var(--teal);
71
+ font-weight: bold;
72
+ }
73
+
74
+ #scroll-anchor {
75
+ height: 20px;
76
+ }
77
+
66
78
  /* buttons */
67
79
  button {
68
80
  display: flex;
@@ -316,7 +328,27 @@ nav ul li:hover {
316
328
  overflow-y: scroll;
317
329
  z-index: 1;
318
330
  opacity: 0.97;
319
- box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
331
+ box-shadow:
332
+ 0 4px 8px 0 rgba(0, 0, 0, 0.2),
333
+ 0 6px 20px 0 rgba(0, 0, 0, 0.19);
334
+ }
335
+
336
+ #drag-handle {
337
+ margin: -2rem -2rem 1rem -2rem;
338
+ height: 1.5rem;
339
+ background-color: #f1f1f1;
340
+ border-bottom: 1px solid #ddd;
341
+ cursor: grab;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ }
346
+
347
+ #drag-handle .handle-indicator {
348
+ width: 50px;
349
+ height: 5px;
350
+ background-color: #ccc;
351
+ border-radius: 5px;
320
352
  }
321
353
 
322
354
  /* JSON viewer */
@@ -347,3 +379,146 @@ nav ul li:hover {
347
379
  box-shadow: none;
348
380
  }
349
381
  }
382
+
383
+ @media (max-width: 768px) {
384
+ /* Allow header to flow naturally */
385
+ header {
386
+ position: relative !important;
387
+ padding-bottom: 1rem;
388
+ height: auto;
389
+ }
390
+
391
+ /* Reset logo from previous media query */
392
+ .logo {
393
+ width: 100%;
394
+ margin: 0 !important;
395
+ justify-content: center;
396
+ }
397
+
398
+ /* Show title again for branding */
399
+ .logo h2 {
400
+ display: block !important;
401
+ margin-left: 0.5rem;
402
+ font-size: 1.2rem;
403
+ }
404
+
405
+ /* Wrap controls */
406
+ .controls {
407
+ flex-wrap: wrap;
408
+ padding: 0 1rem;
409
+ }
410
+
411
+ /* Nav */
412
+ nav {
413
+ width: 100%;
414
+ order: 2;
415
+ overflow-x: auto;
416
+ margin-bottom: 1rem;
417
+ -webkit-overflow-scrolling: touch;
418
+ margin-top: 1rem;
419
+ height: 2rem;
420
+ }
421
+
422
+ nav ul {
423
+ width: max-content;
424
+ padding: 0 0.5rem;
425
+ gap: 1.5rem;
426
+ justify-content: flex-start;
427
+ }
428
+
429
+ /* Search */
430
+ #search {
431
+ order: 3;
432
+ width: 100%;
433
+ margin-bottom: 1rem;
434
+ box-sizing: border-box;
435
+ }
436
+
437
+ /* Buttons */
438
+ #refresh,
439
+ #freeze {
440
+ order: 4;
441
+ width: 48%;
442
+ }
443
+
444
+ #refresh button,
445
+ #freeze button {
446
+ width: 100%;
447
+ justify-content: center;
448
+ padding: 10px;
449
+ margin-bottom: 1rem;
450
+ }
451
+
452
+ /* Logs Container */
453
+ #logs {
454
+ margin-top: 0;
455
+ }
456
+
457
+ /* Hide Table Header */
458
+ .table-header {
459
+ display: none;
460
+ }
461
+
462
+ /* Row Layout */
463
+ .row {
464
+ flex-wrap: wrap;
465
+ height: auto;
466
+ padding: 0.8rem 0;
467
+ border-bottom: 1px solid var(--light);
468
+ align-items: flex-start;
469
+ }
470
+
471
+ .row:hover {
472
+ border-left: 2px solid transparent;
473
+ background-color: transparent;
474
+ }
475
+
476
+ .row:active {
477
+ background-color: #f5f5f5;
478
+ }
479
+
480
+ /* Type */
481
+ .row > :first-child {
482
+ flex: 0 0 3.5rem;
483
+ font-size: 0.75rem;
484
+ text-transform: uppercase;
485
+ font-weight: bold;
486
+ padding-left: 0.5rem;
487
+ text-align: left;
488
+ }
489
+
490
+ /* Info */
491
+ .row > :nth-child(2) {
492
+ flex: 1 1 auto;
493
+ max-width: calc(100% - 6.5rem);
494
+ padding: 0 0.5rem;
495
+ }
496
+
497
+ .log-info {
498
+ white-space: normal;
499
+ display: -webkit-box;
500
+ line-clamp: 2;
501
+ -webkit-line-clamp: 2;
502
+ -webkit-box-orient: vertical;
503
+ max-height: 2.8rem;
504
+ margin-bottom: 0.2rem;
505
+ }
506
+
507
+ /* Hide Context */
508
+ .row > :nth-child(3) {
509
+ display: none;
510
+ }
511
+
512
+ /* Count */
513
+ .row > :last-child {
514
+ flex: 0 0 2rem;
515
+ font-size: 0.8rem;
516
+ color: #aaa;
517
+ text-align: right;
518
+ padding-right: 0.5rem;
519
+ }
520
+
521
+ #drag-handle {
522
+ display: none;
523
+ }
524
+ }
package/src/log.module.ts CHANGED
@@ -64,11 +64,17 @@ export class LogModule {
64
64
  }
65
65
  );
66
66
 
67
- // get all logs endpoint
67
+ // get logs endpoint
68
68
  httpAdapter.get(join(options.path, 'api'), async (req: any, res: any) => {
69
69
  logAccessGuard.canActivate(req);
70
70
 
71
- res.json(await logService.getAll());
71
+ const params = querystring.parse(req.url.split('?')[1]);
72
+ const page = params.page ? parseInt(params.page.toString()) : 1;
73
+ const limit = params.limit ? parseInt(params.limit.toString()) : 50;
74
+ const search = params.search ? params.search.toString() : '';
75
+ const types = params.types ? params.types.toString().split(',') : [];
76
+
77
+ res.json(await logService.getAll(page, limit, search, types));
72
78
  });
73
79
 
74
80
  // delete log endpoint
@@ -11,6 +11,8 @@ import {
11
11
  DataSourceOptions,
12
12
  EntityManager,
13
13
  EntitySchema,
14
+ Like,
15
+ In,
14
16
  } from 'typeorm';
15
17
  import { createLogEntity } from '../entities/log.entity';
16
18
  import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
@@ -94,7 +96,12 @@ export class LogService implements LoggerService, OnApplicationShutdown {
94
96
  case 'getLogs':
95
97
  this.wsService.sendMessage({
96
98
  action: 'list',
97
- data: await this.getAll(),
99
+ data: await this.getAll(
100
+ message.page,
101
+ message.limit,
102
+ message.search,
103
+ message.types
104
+ ),
98
105
  });
99
106
  break;
100
107
  case 'delete':
@@ -159,8 +166,16 @@ export class LogService implements LoggerService, OnApplicationShutdown {
159
166
  });
160
167
  }
161
168
 
162
- async getAll(): Promise<any[]> {
163
- return this.getConnection().find(LogService.Log, {
169
+ async getAll(
170
+ page: number = 1,
171
+ limit: number = 50,
172
+ search: string = '',
173
+ types: string[] = []
174
+ ): Promise<any[]> {
175
+ const skip = (page - 1) * limit;
176
+ const take = limit;
177
+
178
+ const findOptions: any = {
164
179
  select: [
165
180
  '_id',
166
181
  'type',
@@ -173,7 +188,49 @@ export class LogService implements LoggerService, OnApplicationShutdown {
173
188
  'breadcrumbs',
174
189
  ],
175
190
  order: { updatedAt: 'DESC' },
176
- });
191
+ take,
192
+ skip,
193
+ };
194
+
195
+ if (search) {
196
+ if (LogService.options?.database?.type === 'mongodb') {
197
+ const where: any = {
198
+ $or: [
199
+ { message: { $regex: search, $options: 'i' } },
200
+ { trace: { $regex: search, $options: 'i' } },
201
+ ],
202
+ };
203
+
204
+ if (types.length > 0) {
205
+ where.type = { $in: types };
206
+ }
207
+
208
+ findOptions.where = where;
209
+ } else {
210
+ findOptions.where = [
211
+ { message: Like(`%${search}%`) },
212
+ { trace: Like(`%${search}%`) },
213
+ ];
214
+
215
+ if (types.length > 0) {
216
+ findOptions.where.forEach((option: any) => {
217
+ option.type = In(types);
218
+ });
219
+ }
220
+ }
221
+ } else if (types.length > 0) {
222
+ if (LogService.options?.database?.type === 'mongodb') {
223
+ findOptions.where = {
224
+ type: { $in: types },
225
+ };
226
+ } else {
227
+ findOptions.where = {
228
+ type: In(types),
229
+ };
230
+ }
231
+ }
232
+
233
+ return this.getConnection().find(LogService.Log, findOptions);
177
234
  }
178
235
 
179
236
  async delete(_id: string) {
@@ -197,7 +254,10 @@ export class LogService implements LoggerService, OnApplicationShutdown {
197
254
  // find the same log in DB
198
255
  let log;
199
256
 
200
- if (LogService.options?.join) {
257
+ if (
258
+ LogService.options &&
259
+ (LogService.options.join || LogService.options.join === undefined)
260
+ ) {
201
261
  log = await connection.findOne(LogService.Log, {
202
262
  where: {
203
263
  type: data.type,
@@ -207,8 +267,8 @@ export class LogService implements LoggerService, OnApplicationShutdown {
207
267
  }
208
268
 
209
269
  const context =
210
- data.context instanceof ExecutionContextHost
211
- ? this.parseContext(data.context)
270
+ data.context && typeof (data.context as any).getArgs === 'function'
271
+ ? this.parseContext(data.context as any)
212
272
  : data.context;
213
273
 
214
274
  if (log) {
@@ -272,7 +332,7 @@ export class LogService implements LoggerService, OnApplicationShutdown {
272
332
  return LogService.connection?.manager || this.memoryDbService;
273
333
  }
274
334
 
275
- private parseContext(context: ExecutionContextHost): Partial<Context> {
335
+ private parseContext(context: any): Partial<Context> {
276
336
  const res: Partial<Context> = {};
277
337
  const args = context.getArgs();
278
338