@viplance/nestjs-logger 0.3.7 → 0.4.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.
@@ -12,9 +12,7 @@ const logTypes = Object.keys(selectedLogTypes).filter((key) => key !== `all`);
12
12
  let logs = [];
13
13
  let text = "";
14
14
 
15
- window.addEventListener("load", async () => {
16
- getLogs();
17
- });
15
+ connectWebSocket();
18
16
 
19
17
  document.addEventListener(`click`, (e) => {
20
18
  const target = e.target;
@@ -69,7 +67,14 @@ document.addEventListener(`click`, (e) => {
69
67
  (target.classList?.contains(`row`) && target.id);
70
68
 
71
69
  if (logId) {
72
- const log = logs.find((log) => log._id === logId);
70
+ let id = logId;
71
+
72
+ try {
73
+ if (Number(id) > 0) id = Number(id); // SQL DB numeric index
74
+ } catch (e) {}
75
+
76
+ const log = logs.find((log) => log._id === id);
77
+
73
78
  showLogDetails(log);
74
79
  }
75
80
  });
@@ -133,10 +138,10 @@ function getLogHtmlElement(log) {
133
138
  </div>`;
134
139
  }
135
140
 
136
- function renderLogs() {
141
+ function renderLogs(logList = logs) {
137
142
  let html = "";
138
143
 
139
- logs
144
+ logList
140
145
  .filter((log) => {
141
146
  return selectedLogTypes["all"] || selectedLogTypes[log.type];
142
147
  })
@@ -158,50 +163,83 @@ function renderLogs() {
158
163
  document.getElementById("logs").innerHTML = html;
159
164
  }
160
165
 
166
+ async function checkElementsVisibility(logList = logs) {
167
+ if (logList.length === 0) {
168
+ document.getElementById("no-logs").style.display = "block";
169
+ document.getElementById("search").style.display = "none";
170
+ document.querySelector(".table-header").style.display = "none";
171
+ document.querySelector("nav").style.display = "none";
172
+ } else {
173
+ document.getElementById("no-logs").style.display = "none";
174
+ document.getElementById("search").style.display = "inline-block";
175
+ document.querySelector(".table-header").style.display = "flex";
176
+ document.querySelector("nav").style.display = "flex";
177
+ }
178
+ }
179
+
161
180
  async function getLogs() {
162
181
  const { origin, pathname, search } = window.location;
182
+ const searchParams = new URLSearchParams(search);
183
+ const key = searchParams.get("key");
184
+
185
+ if (!!socket) {
186
+ socket.send(
187
+ JSON.stringify({
188
+ action: "getLogs",
189
+ key,
190
+ })
191
+ );
192
+ } else {
193
+ const res = await fetch(`${origin}${pathname}api${search}`);
163
194
 
164
- const res = await fetch(`${origin}${pathname}api${search}`);
195
+ if (res.ok) {
196
+ logs = await res.json();
165
197
 
166
- if (res.ok) {
167
- logs = await res.json();
198
+ checkElementsVisibility();
168
199
 
169
- if (logs.length === 0) {
170
- document.getElementById("no-logs").style.display = "block";
171
- document.querySelector(".table-header").style.display = "none";
172
- document.querySelector("nav").style.display = "none";
200
+ renderLogs();
173
201
  } else {
174
- document.getElementById("no-logs").style.display = "none";
175
- document.querySelector(".table-header").style.display = "flex";
176
- document.querySelector("nav").style.display = "flex";
202
+ alert("An error occurred while fetching logs.");
177
203
  }
178
-
179
- renderLogs();
180
- } else {
181
- alert("An error occurred while fetching logs.");
182
204
  }
183
205
  }
184
206
 
185
- async function deleteLog(id) {
207
+ async function deleteLog(_id) {
186
208
  if (!confirm("Are you sure? It can't be undone.")) return;
187
209
 
188
210
  const { origin, pathname, search: searchParams } = window.location;
189
211
 
190
212
  const searchParamsWithId = new URLSearchParams(searchParams);
191
- searchParamsWithId.set("id", id);
192
-
193
- const res = await fetch(
194
- `${origin}${pathname}api?${searchParamsWithId.toString()}`,
195
- {
196
- method: "DELETE",
197
- }
198
- );
199
-
200
- if (res.ok) {
213
+ const key = searchParamsWithId.get("key");
214
+
215
+ if (!!socket) {
216
+ socket.send(
217
+ JSON.stringify({
218
+ action: "delete",
219
+ key,
220
+ data: {
221
+ _id,
222
+ },
223
+ })
224
+ );
201
225
  closePopup();
202
226
  getLogs();
203
227
  } else {
204
- alert("An error occurred while deleting log.");
228
+ searchParamsWithId.set("id", _id);
229
+
230
+ const res = await fetch(
231
+ `${origin}${pathname}api?${searchParamsWithId.toString()}`,
232
+ {
233
+ method: "DELETE",
234
+ }
235
+ );
236
+
237
+ if (res.ok) {
238
+ closePopup();
239
+ getLogs();
240
+ } else {
241
+ alert("An error occurred while deleting log.");
242
+ }
205
243
  }
206
244
  }
207
245
 
@@ -1,11 +1,22 @@
1
1
  function showLogDetails(log) {
2
2
  const popup = document.getElementById(`popup`);
3
+ const context = getObject(log.context);
4
+ const breadcrumbs = getObject(log.breadcrumbs);
5
+
6
+ const timeInfo =
7
+ log.updatedAt === log.createdAt
8
+ ? getDate(log.updatedAt)
9
+ : `Updated: ${getDate(log.updatedAt)}.&nbsp;&nbsp;&nbsp;First seen: ${getDate(
10
+ log.createdAt
11
+ )}`;
3
12
 
4
13
  popup.innerHTML = `
5
14
  <div class="content center">
6
15
  <div class="container">
7
- <h2 class="popup-title ${log.type}">${log.type}: ${log.message}</h2>
8
- <div class="mt-05">${getDate(log.updatedAt)}</div>
16
+ <h2 class="popup-title ${log.type}">${log.type}: ${log.message} (${
17
+ log.count
18
+ })</h2>
19
+ <div class="mt-05">${timeInfo}</div>
9
20
  ${
10
21
  log.trace
11
22
  ? `
@@ -15,18 +26,18 @@ function showLogDetails(log) {
15
26
  : ""
16
27
  }
17
28
  ${
18
- log.context
29
+ context
19
30
  ? `
20
31
  <h3 class="mt-15">Context</h3>
21
- <p>${jsonViewer(log.context)}</p>
32
+ <p>${jsonViewer(context)}</p>
22
33
  `
23
34
  : ""
24
35
  }
25
36
  ${
26
- log.breadcrumbs && log.breadcrumbs.length > 0
37
+ breadcrumbs && breadcrumbs.length > 0
27
38
  ? `
28
39
  <h3 class="mt-15">Breadcrumbs</h3>
29
- <p>${jsonViewer(log.breadcrumbs)}</p>
40
+ <p>${jsonViewer(breadcrumbs)}</p>
30
41
  `
31
42
  : ""
32
43
  }
@@ -42,6 +53,14 @@ function showLogDetails(log) {
42
53
  popup.style.display = "block";
43
54
  }
44
55
 
56
+ function getObject(context) {
57
+ if (typeof context === "string") {
58
+ return JSON.parse(context);
59
+ }
60
+
61
+ return context;
62
+ }
63
+
45
64
  function getTrace(trace) {
46
65
  return trace.replace(new RegExp(String.fromCharCode(10), "g"), "<br />");
47
66
  }
@@ -0,0 +1,83 @@
1
+ // WebSocket connection
2
+ let socket;
3
+ let frozen = false;
4
+
5
+ async function connectWebSocket() {
6
+ const { hostname, origin, pathname, search } = window.location;
7
+
8
+ const res = await fetch(`${origin}${pathname}settings${search}`);
9
+
10
+ if (!res.ok) {
11
+ alert('An error occurred while fetching settings.');
12
+ return;
13
+ }
14
+
15
+ const settings = await res.json();
16
+
17
+ if (!settings.websocket) {
18
+ getLogs();
19
+ document.getElementById('refresh').style.display = 'block';
20
+ }
21
+
22
+ document.getElementById('freeze').style.display = 'block';
23
+
24
+ if (!settings.websocket?.port) {
25
+ alert('WebSocket port is not configured.');
26
+ return;
27
+ }
28
+
29
+ const socketUrl = `ws://${hostname}:${settings.websocket.port}`;
30
+ socket = new WebSocket(socketUrl, 'json');
31
+
32
+ socket.onerror = (error) => {
33
+ console.error(error);
34
+ };
35
+
36
+ socket.onopen = (event) => {
37
+ getLogs();
38
+ };
39
+
40
+ socket.onclose = (event) => {
41
+ console.log(event);
42
+ setTimeout(connectWebSocket, 5000);
43
+ };
44
+
45
+ socket.onmessage = (event) => {
46
+ const data = JSON.parse(event.data.toString());
47
+
48
+ if (data['action'] && !frozen) {
49
+ switch (data['action']) {
50
+ case 'list':
51
+ logs = data['data'];
52
+ checkElementsVisibility(logs);
53
+ renderLogs(logs);
54
+ break;
55
+ case 'insert':
56
+ getLogs();
57
+ break;
58
+ case 'update':
59
+ getLogs();
60
+ return;
61
+ case 'delete':
62
+ return;
63
+ }
64
+ }
65
+ };
66
+ }
67
+ function sendMessage(message) {
68
+ socket.send(JSON.stringify(message));
69
+ }
70
+
71
+ function toggleFreeze() {
72
+ frozen = !frozen;
73
+ const button = document.querySelector('#freeze button');
74
+ button.innerHTML = frozen ? 'Frozen' : 'Freeze';
75
+
76
+ if (frozen) {
77
+ button.classList.remove('white');
78
+ button.classList.add('light');
79
+ } else {
80
+ button.classList.remove('light');
81
+ button.classList.add('white');
82
+ }
83
+ }
@@ -190,6 +190,11 @@ nav ul li:hover {
190
190
  font-size: 1rem;
191
191
  }
192
192
 
193
+ #refresh,
194
+ #freeze {
195
+ display: none;
196
+ }
197
+
193
198
  /* table */
194
199
  .table-header,
195
200
  .row {
@@ -1,13 +1,17 @@
1
- import { EntitySchema } from "typeorm";
1
+ import { DataSourceOptions, EntitySchema } from "typeorm";
2
2
 
3
- export function createLogEntity(name: string) {
3
+ export function createLogEntity(
4
+ name: string,
5
+ dbType: DataSourceOptions["type"] | "memory"
6
+ ) {
4
7
  return new EntitySchema({
5
8
  name,
6
9
  columns: {
7
10
  _id: {
8
- type: String,
11
+ type: dbType === "mongodb" ? String : Number,
9
12
  objectId: true,
10
13
  primary: true,
14
+ generated: true,
11
15
  },
12
16
  type: { type: String },
13
17
  message: { type: String },
package/src/log.module.ts CHANGED
@@ -8,12 +8,19 @@ import querystring from "node:querystring";
8
8
  import { ApplicationConfig } from "@nestjs/core";
9
9
  import { join } from "node:path";
10
10
  import { LogAccessGuard } from "./guards/access.guard";
11
+ import { WsService } from "./services/ws.service";
11
12
 
12
13
  @Global()
13
14
  @Module({
14
15
  imports: [TypeOrmModule],
15
- providers: [ApplicationConfig, LogAccessGuard, LogService, MemoryDbService],
16
- exports: [TypeOrmModule, LogService, MemoryDbService],
16
+ providers: [
17
+ ApplicationConfig,
18
+ LogAccessGuard,
19
+ LogService,
20
+ MemoryDbService,
21
+ WsService,
22
+ ],
23
+ exports: [TypeOrmModule, LogService, MemoryDbService, WsService],
17
24
  })
18
25
  export class LogModule {
19
26
  public static async init(
@@ -23,6 +30,7 @@ export class LogModule {
23
30
  app.resolve(LogService);
24
31
 
25
32
  const logService: LogService = await app.resolve(LogService);
33
+ const wsService: WsService = await app.resolve(WsService);
26
34
  const logAccessGuard: LogAccessGuard = await app.get(LogAccessGuard);
27
35
 
28
36
  if (options) {
@@ -38,6 +46,26 @@ export class LogModule {
38
46
 
39
47
  const httpAdapter = app.getHttpAdapter();
40
48
 
49
+ // frontend settings endpoint
50
+ httpAdapter.get(
51
+ join(options.path, "settings"),
52
+ async (req: any, res: any) => {
53
+ logAccessGuard.canActivate(req);
54
+
55
+ const result: { [key: string]: any } = {};
56
+
57
+ if (options?.websocket) {
58
+ result.websocket = {
59
+ namespace: options.websocket?.namespace,
60
+ port: options.websocket?.port,
61
+ host: options.websocket?.host || req.headers?.host.split(":")[0],
62
+ };
63
+ }
64
+
65
+ res.json(result);
66
+ }
67
+ );
68
+
41
69
  // get all logs endpoint
42
70
  httpAdapter.get(join(options.path, "api"), async (req: any, res: any) => {
43
71
  logAccessGuard.canActivate(req);
@@ -60,6 +88,11 @@ export class LogModule {
60
88
  res.json(await logService.delete(params.id.toString()));
61
89
  }
62
90
  );
91
+
92
+ // set up WebSocket connection
93
+ if (options?.websocket) {
94
+ wsService.setupConnection(options.websocket, options.key);
95
+ }
63
96
  }
64
97
 
65
98
  if (options?.database) {
@@ -1,4 +1,9 @@
1
- import { Injectable, LoggerService, Scope } from "@nestjs/common";
1
+ import {
2
+ Injectable,
3
+ LoggerService,
4
+ OnApplicationShutdown,
5
+ Scope,
6
+ } from "@nestjs/common";
2
7
  import { MemoryDbService } from "./memory-db.service";
3
8
  import { defaultTable } from "../defaults";
4
9
  import { Context, LogModuleOptions, LogType } from "../types";
@@ -11,24 +16,40 @@ import {
11
16
  import { createLogEntity } from "../entities/log.entity";
12
17
  import { ExecutionContextHost } from "@nestjs/core/helpers/execution-context-host";
13
18
  import { setInterval } from "timers";
19
+ import { entity2table } from "../utils/entity2table";
20
+ import { WsService } from "./ws.service";
21
+ import { Subscription } from "rxjs";
14
22
 
15
23
  @Injectable({ scope: Scope.TRANSIENT })
16
- export class LogService implements LoggerService {
24
+ export class LogService implements LoggerService, OnApplicationShutdown {
17
25
  static connection: DataSource;
18
26
  static options: LogModuleOptions;
19
- static Log: EntitySchema = createLogEntity(defaultTable);
27
+ static Log: EntitySchema = createLogEntity(defaultTable, "memory");
20
28
  static timer: ReturnType<typeof setInterval>;
29
+ static subscription: Subscription;
21
30
 
22
31
  breadcrumbs: any[] = [];
23
32
 
24
- constructor(private readonly memoryDbService: MemoryDbService) {}
33
+ constructor(
34
+ private readonly memoryDbService: MemoryDbService,
35
+ private readonly wsService: WsService
36
+ ) {}
37
+
38
+ onApplicationShutdown() {
39
+ if (LogService.timer) {
40
+ clearInterval(LogService.timer);
41
+ }
42
+ }
25
43
 
26
44
  async connectDb(options: LogModuleOptions): Promise<DataSource> {
27
45
  LogService.Log = createLogEntity(
28
- options.database?.collection || options.database?.table || defaultTable
46
+ options.database?.collection || options.database?.table || defaultTable,
47
+ options.database?.type || "mongodb"
29
48
  );
30
49
 
31
- this.setOptions(options);
50
+ if (!LogService.options) {
51
+ this.setOptions(options);
52
+ }
32
53
 
33
54
  const dataSourceOptions = {
34
55
  type: options.database?.type,
@@ -39,9 +60,22 @@ export class LogService implements LoggerService {
39
60
  } as DataSourceOptions;
40
61
 
41
62
  LogService.connection = new DataSource(dataSourceOptions);
42
-
43
63
  await LogService.connection.initialize();
44
64
 
65
+ if (dataSourceOptions.type !== "mongodb") {
66
+ const queryRunner = LogService.connection.createQueryRunner();
67
+
68
+ try {
69
+ await queryRunner.connect();
70
+
71
+ const table = entity2table(LogService.Log);
72
+
73
+ await queryRunner.createTable(table, true);
74
+ } finally {
75
+ await queryRunner.release();
76
+ }
77
+ }
78
+
45
79
  if (LogService.timer) {
46
80
  clearInterval(LogService.timer);
47
81
  }
@@ -53,6 +87,24 @@ export class LogService implements LoggerService {
53
87
 
54
88
  setOptions(options: LogModuleOptions) {
55
89
  LogService.options = options;
90
+
91
+ if (options.websocket && !LogService.subscription) {
92
+ LogService.subscription = this.wsService.onMessage.subscribe(
93
+ async (message) => {
94
+ switch (message.action) {
95
+ case "getLogs":
96
+ this.wsService.sendMessage({
97
+ action: "list",
98
+ data: await this.getAll(),
99
+ });
100
+ break;
101
+ case "delete":
102
+ this.delete(message.data._id);
103
+ break;
104
+ }
105
+ }
106
+ );
107
+ }
56
108
  }
57
109
 
58
110
  addBreadcrumb(breadcrumb: any) {
@@ -121,8 +173,12 @@ export class LogService implements LoggerService {
121
173
  });
122
174
  }
123
175
 
124
- async delete(id: string) {
125
- return this.getConnection().delete(LogService.Log, id);
176
+ async delete(_id: string) {
177
+ this.wsService.sendMessage({
178
+ action: "delete",
179
+ data: { _id },
180
+ });
181
+ return this.getConnection().delete(LogService.Log, _id);
126
182
  }
127
183
 
128
184
  private async smartInsert(data: {
@@ -146,16 +202,31 @@ export class LogService implements LoggerService {
146
202
  const context = data.context ? this.parseContext(data.context) : undefined;
147
203
 
148
204
  if (log) {
149
- return await connection.update(LogService.Log, log._id, {
205
+ const updatedLog = {
150
206
  context,
151
207
  trace: data.trace,
152
208
  breadcrumbs: this.breadcrumbs,
153
209
  count: log.count + 1,
154
210
  updatedAt: currentDate,
211
+ };
212
+
213
+ this.wsService.sendMessage({
214
+ action: "update",
215
+ data: { ...log, ...updatedLog },
155
216
  });
217
+
218
+ await connection.update(LogService.Log, log["_id"], {
219
+ context,
220
+ trace: data.trace,
221
+ breadcrumbs: this.breadcrumbs,
222
+ count: log.count + 1,
223
+ updatedAt: currentDate,
224
+ });
225
+
226
+ return { ...log, ...updatedLog };
156
227
  }
157
228
 
158
- return await connection.insert(LogService.Log, {
229
+ const insertedLog = {
159
230
  type: data.type,
160
231
  message: data.message,
161
232
  context,
@@ -164,7 +235,27 @@ export class LogService implements LoggerService {
164
235
  count: 1,
165
236
  createdAt: currentDate,
166
237
  updatedAt: currentDate,
238
+ };
239
+
240
+ const res = await connection.insert(LogService.Log, insertedLog);
241
+ const _id = this.getNewObjectId(res);
242
+
243
+ this.wsService.sendMessage({
244
+ action: "insert",
245
+ data: { _id, ...insertedLog },
167
246
  });
247
+
248
+ return { _id, ...insertedLog };
249
+ }
250
+
251
+ private getNewObjectId(result: any): string | number {
252
+ if (result.identifiers) {
253
+ return result.identifiers[0]._id;
254
+ }
255
+
256
+ console.log(result);
257
+
258
+ return result._id;
168
259
  }
169
260
 
170
261
  private getConnection(): EntityManager {
@@ -0,0 +1 @@
1
+ declare module "ws";
@@ -0,0 +1,110 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import WebSocket, { WebSocketServer } from "ws";
3
+ import { LogModuleOptions } from "../types";
4
+ import { Subject } from "rxjs";
5
+
6
+ @Injectable()
7
+ export class WsService {
8
+ public onMessage: Subject<any> = new Subject();
9
+ private ws: WebSocket | null = null;
10
+ private connected: boolean = false;
11
+ private connectionTimeout: number = 500;
12
+ private options: LogModuleOptions["websocket"] = {
13
+ port: 8080,
14
+ host: "localhost",
15
+ };
16
+ private key: string = "";
17
+
18
+ setupConnection(options: LogModuleOptions["websocket"], key = "") {
19
+ this.options = {
20
+ ...this.options,
21
+ ...options,
22
+ };
23
+ this.key = key;
24
+
25
+ // Set up Web Socket server
26
+ if (this.ws) {
27
+ return;
28
+ }
29
+
30
+ const wsServer = new WebSocketServer({
31
+ retryCount: 1,
32
+ reconnectInterval: 1,
33
+ handshakeTimeout: this.connectionTimeout,
34
+ port: this.options?.port,
35
+ });
36
+
37
+ console.log(
38
+ `Logs WebSocket server is listening on port ${this.options.port}`
39
+ );
40
+
41
+ wsServer.on("error", this.handleError);
42
+ wsServer.on("open", () => this.handleOpenConnection());
43
+ wsServer.on("ping", () => this.ping(this.ws));
44
+ wsServer.on("close", () => this.closeConnection(this.ws));
45
+ wsServer.on("message", this.handleMessage);
46
+ wsServer.on("connection", (connection: WebSocket) => {
47
+ this.ws = connection;
48
+ connection.onmessage = this.handleMessage;
49
+ });
50
+ }
51
+
52
+ sendMessage(message: any) {
53
+ this.ws?.send(JSON.stringify(message));
54
+ }
55
+
56
+ private handleError = () => {
57
+ const serverUrl = this.getServerUrl();
58
+ console.error(`Server ${serverUrl} is not available.`);
59
+
60
+ setTimeout(this.setupConnection, this.connectionTimeout);
61
+ };
62
+
63
+ private closeConnection = (connection: any) => {
64
+ clearTimeout(connection.pingTimeout);
65
+
66
+ if (this.connected) {
67
+ console.log("Connection has been closed by server.");
68
+ this.connected = false;
69
+ this.handleError();
70
+ }
71
+ };
72
+
73
+ private ping = (connection: any) => {
74
+ console.log("Ping remote server.");
75
+ clearTimeout(connection.pingTimeout);
76
+
77
+ connection.pingTimeout = setTimeout(() => {
78
+ connection.terminate();
79
+ }, 30000 + this.connectionTimeout);
80
+ };
81
+
82
+ private handleMessage = (message: any) => {
83
+ try {
84
+ const data = JSON.parse((message.data || message).toString());
85
+
86
+ if (this.key !== "" && data.key !== this.key) {
87
+ throw new Error("WebSocket unauthorized");
88
+ }
89
+
90
+ if (this.options)
91
+ if (data.action) {
92
+ this.onMessage.next(data);
93
+ }
94
+ } catch (err) {
95
+ console.error(err);
96
+ }
97
+ };
98
+
99
+ private getServerUrl = (): string => {
100
+ return `${this.options?.secure ? "wss" : "ws"}://${this.options?.host}:${
101
+ this.options?.port
102
+ }`;
103
+ };
104
+
105
+ private handleOpenConnection = async () => {
106
+ this.connected = true;
107
+ const serverUrl = this.getServerUrl();
108
+ console.log(`${serverUrl} has been connected.`);
109
+ };
110
+ }
@@ -12,4 +12,10 @@ export type LogModuleOptions = {
12
12
  table?: string;
13
13
  collection?: string;
14
14
  };
15
+ websocket?: {
16
+ port?: number;
17
+ namespace?: string;
18
+ host?: string;
19
+ secure?: boolean;
20
+ };
15
21
  };