@viplance/nestjs-logger 0.4.7 → 0.4.8

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,79 @@ 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();
202
232
  } else {
233
+ isLoading = false;
234
+ document.getElementById('loader').style.display = 'none';
203
235
  alert('An error occurred while fetching logs.');
204
236
  }
205
237
  }
@@ -244,8 +276,85 @@ async function deleteLog(_id) {
244
276
  }
245
277
  }
246
278
 
279
+ let searchTimeout;
247
280
  function search(event) {
248
281
  text = event.target.value.toLowerCase();
249
282
 
283
+ clearTimeout(searchTimeout);
284
+ searchTimeout = setTimeout(() => {
285
+ currentPage = 1;
286
+ hasMore = true;
287
+ getLogs(1);
288
+ }, 300);
289
+ }
290
+
291
+ // Infinite scrolling
292
+ const observer = new IntersectionObserver(
293
+ (entries) => {
294
+ if (entries[0].isIntersecting && !isLoading && hasMore) {
295
+ getLogs(currentPage + 1);
296
+ }
297
+ },
298
+ { threshold: 1.0 },
299
+ );
300
+
301
+ document.addEventListener('DOMContentLoaded', () => {
302
+ const scrollAnchor = document.getElementById('scroll-anchor');
303
+ if (scrollAnchor) {
304
+ observer.observe(scrollAnchor);
305
+ }
306
+ });
307
+
308
+ function matchesFilter(log) {
309
+ // Check types
310
+ if (!selectedLogTypes['all'] && !selectedLogTypes[log.type]) {
311
+ return false;
312
+ }
313
+
314
+ // Check search text
315
+ if (text !== '') {
316
+ const matches =
317
+ log.message.toLowerCase().includes(text) ||
318
+ log.trace?.toLowerCase().includes(text) ||
319
+ JSON.stringify(log.context || {})
320
+ .toLowerCase()
321
+ .includes(text);
322
+
323
+ if (!matches) return false;
324
+ }
325
+
326
+ return true;
327
+ }
328
+
329
+ function handleWsInsert(log) {
330
+ if (matchesFilter(log)) {
331
+ logs.unshift(log);
332
+ checkElementsVisibility();
333
+ renderLogs();
334
+ }
335
+ }
336
+
337
+ function handleWsUpdate(updatedLog) {
338
+ const idx = logs.findIndex((l) => l._id === updatedLog._id);
339
+ if (idx > -1) {
340
+ logs.splice(idx, 1);
341
+ }
342
+
343
+ if (matchesFilter(updatedLog)) {
344
+ logs.unshift(updatedLog);
345
+ }
346
+
347
+ checkElementsVisibility();
250
348
  renderLogs();
349
+ checkAndUpdatePopup();
350
+ }
351
+
352
+ function handleWsDelete(id) {
353
+ const idx = logs.findIndex((l) => l._id === id);
354
+ if (idx > -1) {
355
+ logs.splice(idx, 1);
356
+ checkElementsVisibility();
357
+ renderLogs();
358
+ checkAndUpdatePopup();
359
+ }
251
360
  }
@@ -21,8 +21,8 @@ function showLogDetails(log) {
21
21
  )}.   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,38 @@ 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();
60
79
  break;
61
80
  case 'insert':
62
- getLogs();
81
+ if (currentPage === 1) {
82
+ handleWsInsert(data['data']);
83
+ }
63
84
  break;
64
85
  case 'update':
65
- getLogs();
86
+ handleWsUpdate(data['data']);
66
87
  break;
67
88
  case 'delete':
68
- getLogs();
89
+ handleWsDelete(data['data']._id);
69
90
  break;
70
91
  }
71
92
  }
72
93
  };
94
+
95
+ setTimeout(() => {
96
+ if (!connected) connectWebSocket(); // fix for Safari browser
97
+ }, 300);
73
98
  }
99
+
74
100
  function sendMessage(message) {
75
101
  socket.send(JSON.stringify(message));
76
102
  }
@@ -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,7 @@ 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 (LogService.options && (LogService.options.join || LogService.options.join === undefined)) {
201
258
  log = await connection.findOne(LogService.Log, {
202
259
  where: {
203
260
  type: data.type,