LogBook.ts 5.8 KB

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