LogBook.ts 5.8 KB

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