LogBook.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import fs from "fs";
  2. export type Log = {
  3. timestamp: number;
  4. message: string;
  5. type?: "info" | "success" | "error" | "debug";
  6. category?: string;
  7. data?: Record<string, any>;
  8. };
  9. export type LogFilters = {
  10. include: Partial<Omit<Log, "timestamp">>[];
  11. exclude: Partial<Omit<Log, "timestamp">>[];
  12. };
  13. export type LogOutputOptions = Record<
  14. "timestamp" | "title" | "type" | "message" | "data" | "color",
  15. boolean
  16. > &
  17. Partial<LogFilters>;
  18. export type LogOutputs = {
  19. console: LogOutputOptions;
  20. file: LogOutputOptions;
  21. memory: { enabled: boolean } & Partial<LogFilters>;
  22. };
  23. export default class LogBook {
  24. private logs: Log[];
  25. private outputs: LogOutputs;
  26. private stream: fs.WriteStream;
  27. /**
  28. * Log Book
  29. */
  30. public constructor(file = "logs/backend.log") {
  31. this.logs = [];
  32. this.outputs = {
  33. console: {
  34. timestamp: true,
  35. title: true,
  36. type: false,
  37. message: true,
  38. data: false,
  39. color: true,
  40. exclude: [
  41. // {
  42. // category: "jobs",
  43. // type: "success"
  44. // },
  45. // {
  46. // type: "debug"
  47. // }
  48. ]
  49. },
  50. file: {
  51. timestamp: true,
  52. title: true,
  53. type: true,
  54. message: true,
  55. data: false,
  56. color: false
  57. },
  58. memory: {
  59. enabled: false
  60. }
  61. };
  62. this.stream = fs.createWriteStream(file, { flags: "a" });
  63. }
  64. /**
  65. * log - Add log
  66. *
  67. * @param log - Log message or object
  68. */
  69. public log(log: string | Omit<Log, "timestamp">) {
  70. const logObject: Log = {
  71. timestamp: Date.now(),
  72. ...(typeof log === "string" ? { message: log } : log)
  73. };
  74. const exclude = {
  75. console: false,
  76. file: false,
  77. memory: false
  78. };
  79. Object.entries(logObject).forEach(([key, value]) => {
  80. Object.entries(this.outputs).forEach(([outputName, output]) => {
  81. if (
  82. (output.include &&
  83. output.include.length > 0 &&
  84. output.include.filter(
  85. // @ts-ignore
  86. filter => filter[key] === value
  87. ).length === 0) ||
  88. (output.exclude &&
  89. output.exclude.filter(
  90. // @ts-ignore
  91. filter => filter[key] === value
  92. ).length > 0)
  93. )
  94. // @ts-ignore
  95. exclude[outputName] = true;
  96. });
  97. });
  98. const title =
  99. (logObject.data && logObject.data.jobName) ||
  100. logObject.category ||
  101. undefined;
  102. if (!exclude.memory && this.outputs.memory.enabled)
  103. this.logs.push(logObject);
  104. if (!exclude.console)
  105. console.log(this.formatMessage(logObject, title, "console"));
  106. if (!exclude.file)
  107. this.stream.write(
  108. `${this.formatMessage(logObject, title, "file")}\n`
  109. );
  110. }
  111. /**
  112. * formatMessage - Format log to string
  113. *
  114. * @param log - Log
  115. * @param title - Log title
  116. * @param destination - Message destination
  117. * @returns Formatted log string
  118. */
  119. private formatMessage(
  120. log: Log,
  121. title: string | undefined,
  122. destination: "console" | "file"
  123. ): string {
  124. const centerString = (string: string, length: number) => {
  125. const spaces = Array(
  126. Math.floor((length - Math.max(0, string.length)) / 2)
  127. ).join(" ");
  128. return `| ${spaces}${string}${spaces}${
  129. string.length % 2 === 0 ? "" : " "
  130. } `;
  131. };
  132. let message = "";
  133. if (this.outputs[destination].color)
  134. switch (log.type) {
  135. case "success":
  136. message += "\x1b[32m";
  137. break;
  138. case "error":
  139. message += "\x1b[31m";
  140. break;
  141. case "debug":
  142. message += "\x1b[33m";
  143. break;
  144. case "info":
  145. default:
  146. message += "\x1b[36m";
  147. break;
  148. }
  149. if (this.outputs[destination].timestamp)
  150. message += `| ${new Date(log.timestamp).toISOString()} `;
  151. if (this.outputs[destination].title)
  152. message += centerString(title ? title.substring(0, 20) : "", 24);
  153. if (this.outputs[destination].type)
  154. message += centerString(
  155. log.type ? log.type.toUpperCase() : "INFO",
  156. 10
  157. );
  158. if (this.outputs[destination].message) message += `| ${log.message} `;
  159. if (this.outputs[destination].data)
  160. message += `| ${JSON.stringify(log.data)} `;
  161. if (this.outputs[destination].color) message += "\x1b[0m";
  162. return message;
  163. }
  164. /**
  165. * updateOutput - Update output settings
  166. *
  167. * @param output - Output name
  168. * @param key - Output key to update
  169. * @param action - Update action
  170. * @param values - Updated value
  171. */
  172. public async updateOutput(
  173. output: "console" | "file" | "memory",
  174. key: keyof LogOutputOptions | "enabled",
  175. action: "set" | "add" | "reset",
  176. values?: any
  177. ) {
  178. switch (key) {
  179. case "include":
  180. case "exclude": {
  181. if (action === "set" || action === "add") {
  182. if (!values) throw new Error("No filters provided");
  183. const filters = Array.isArray(values) ? values : [values];
  184. if (action === "set") this.outputs[output][key] = filters;
  185. if (action === "add")
  186. this.outputs[output][key] = [
  187. ...(this.outputs[output][key] || []),
  188. ...filters
  189. ];
  190. } else if (action === "reset") {
  191. this.outputs[output][key] = [];
  192. } else
  193. throw new Error(
  194. `Action "${action}" not found for ${key} in ${output}`
  195. );
  196. break;
  197. }
  198. case "enabled": {
  199. if (output === "memory" && action === "set")
  200. this.outputs[output][key] = values;
  201. else
  202. throw new Error(
  203. `Action "${action}" not found for ${key} in ${output}`
  204. );
  205. break;
  206. }
  207. default: {
  208. if (output !== "memory" && action === "set") {
  209. if (!values) throw new Error("No value provided");
  210. this.outputs[output][key] = values;
  211. } else
  212. throw new Error(
  213. `Action "${action}" not found for ${key} in ${output}`
  214. );
  215. }
  216. }
  217. }
  218. }