logger.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import {Logger} from 'ts-log';
  18. import {BaseConfidential} from './confidential';
  19. import LogType = threema.LogType;
  20. import LogLevel = threema.LogLevel;
  21. import LogRecord = threema.LogRecord;
  22. type LogFunction = (message?: any, ...args: any[]) => void;
  23. // Supported log (level) types
  24. const LOG_TYPES: LogType[] = ['debug', 'trace', 'info', 'warn', 'error'];
  25. // Types allowed for serialisation
  26. const ALLOWED_TYPES: any[] = [Boolean, Number, String, Array];
  27. /**
  28. * Forwards log records to one or more loggers.
  29. */
  30. export class TeeLogger implements Logger {
  31. private readonly loggers: Logger[];
  32. public readonly debug: LogFunction;
  33. public readonly trace: LogFunction;
  34. public readonly info: LogFunction;
  35. public readonly warn: LogFunction;
  36. public readonly error: LogFunction;
  37. constructor(loggers: Logger[]) {
  38. this.loggers = loggers;
  39. // Bind log level type methods
  40. for (const type of LOG_TYPES) {
  41. this[type] = this.forward.bind(this, type);
  42. }
  43. }
  44. /**
  45. * Forward a log record to each underlying logger.
  46. * @param type The log record level type.
  47. * @param message The log message.
  48. * @param args Further arguments of the log record.
  49. */
  50. private forward(type: LogType, message?: any, ...args: any[]): void {
  51. for (const log of this.loggers) {
  52. log[type](message, ...args);
  53. }
  54. }
  55. }
  56. /**
  57. * Filters log messages depending on the applied log level.
  58. *
  59. * Wraps a normal logger and forwards all log records that have not been
  60. * filtered by the log level.
  61. */
  62. export class LevelLogger implements Logger {
  63. public readonly logger: Logger;
  64. public readonly level: LogLevel;
  65. public readonly debug: LogFunction = this.noop;
  66. public readonly trace: LogFunction = this.noop;
  67. public readonly info: LogFunction = this.noop;
  68. public readonly warn: LogFunction = this.noop;
  69. public readonly error: LogFunction = this.noop;
  70. constructor(logger: Logger, level: LogLevel) {
  71. this.logger = logger;
  72. this.level = level;
  73. // Bind corresponding method to log level type, if enabled
  74. // noinspection FallThroughInSwitchStatementJS
  75. switch (level) {
  76. case 'debug':
  77. this.debug = this.logger.debug.bind(this.logger);
  78. this.trace = this.logger.trace.bind(this.logger);
  79. case 'info':
  80. this.info = this.logger.info.bind(this.logger);
  81. case 'warn':
  82. this.warn = this.logger.warn.bind(this.warn);
  83. case 'error':
  84. this.error = this.logger.error.bind(this.error);
  85. default:
  86. break;
  87. }
  88. }
  89. private noop(): void {
  90. // noop
  91. }
  92. }
  93. /**
  94. * Adds a prefix before forwarding log records to another logger.
  95. */
  96. export class TagLogger implements Logger {
  97. public readonly logger: Logger;
  98. public readonly debug: LogFunction;
  99. public readonly trace: LogFunction;
  100. public readonly info: LogFunction;
  101. public readonly warn: LogFunction;
  102. public readonly error: LogFunction;
  103. constructor(logger: Logger, ...tag: string[]) {
  104. this.logger = logger;
  105. // Apply a tag to each log level type method of the logger
  106. for (const type of LOG_TYPES) {
  107. this[type] = logger[type].bind(logger, ...tag);
  108. }
  109. }
  110. }
  111. /**
  112. * Forwards all log records to another logger while unveiling confidential
  113. * log records.
  114. */
  115. export class UnveilLogger implements Logger {
  116. public readonly logger: Logger;
  117. public readonly debug: LogFunction;
  118. public readonly trace: LogFunction;
  119. public readonly info: LogFunction;
  120. public readonly warn: LogFunction;
  121. public readonly error: LogFunction;
  122. constructor(logger: Logger) {
  123. this.logger = logger;
  124. // Bind log level type methods
  125. for (const type of LOG_TYPES) {
  126. this[type] = this.unveil.bind(this, type);
  127. }
  128. }
  129. private unveil(type: LogType, ...args: any[]): void {
  130. args = args.map((item) => item instanceof BaseConfidential ? item.uncensored : item);
  131. this.logger[type](...args);
  132. }
  133. }
  134. /**
  135. * Forwards all log records to the default `Console` logger.
  136. */
  137. export class ConsoleLogger implements Logger {
  138. // tslint:disable:no-console
  139. public readonly debug: LogFunction = console.debug;
  140. public readonly trace: LogFunction = console.trace;
  141. public readonly info: LogFunction = console.info;
  142. public readonly warn: LogFunction = console.warn;
  143. public readonly error: LogFunction = console.error;
  144. // tslint:enable:no-console
  145. }
  146. /**
  147. * Stores log records in memory.
  148. *
  149. * A limit can be provided which results in a circular memory buffer, where old
  150. * log records are being continuously dropped in case the limit would be
  151. * exceeded by a new log record.
  152. *
  153. * Since serialisation can be expensive, this holds references to objects
  154. * until explicit serialisation is being requested.
  155. *
  156. * Note: This logger will serialise confidential log arguments censored.
  157. */
  158. export class MemoryLogger implements Logger {
  159. private readonly records: LogRecord[] = [];
  160. public readonly limit: number = 0;
  161. public readonly debug: LogFunction;
  162. public readonly trace: LogFunction;
  163. public readonly info: LogFunction;
  164. public readonly warn: LogFunction;
  165. public readonly error: LogFunction;
  166. constructor(limit: number = 0) {
  167. this.limit = limit;
  168. // Bind log level type methods
  169. for (const type of LOG_TYPES) {
  170. this[type] = this.append.bind(this, type);
  171. }
  172. }
  173. /**
  174. * Append a log record to the memory buffer.
  175. *
  176. * Drops the oldest log record if the log record limit would be exceeded.
  177. *
  178. * @param type The log record level type.
  179. * @param message The log message.
  180. * @param args Further arguments of the log record
  181. */
  182. private append(type: LogType, message?: any, ...args: any[]): void {
  183. // Remove oldest record if needed
  184. if (this.limit > 0 && this.records.length >= this.limit) {
  185. this.records.shift();
  186. }
  187. // Add newest record
  188. this.records.push([Date.now(), type, message, ...args]);
  189. }
  190. /**
  191. * Get a copy of all currently logged records. Strips any style formatting
  192. * of the log tags.
  193. *
  194. * Important: Objects implementing the `Confidential` interface will be
  195. * returned as is.
  196. */
  197. public getRecords(): LogRecord[] {
  198. return this.records.map(([date, type, message, ...args]: LogRecord) => {
  199. // Trim first message (tag)
  200. if (message !== null && message !== undefined && message.constructor === String) {
  201. message = message.trim();
  202. }
  203. return [date, type, message, ...args];
  204. });
  205. }
  206. /**
  207. * Replacer function for serialising log records to JSON.
  208. *
  209. * A recursive filter will be applied:
  210. *
  211. * - the types `null`, `string`, `number` and `boolean` will be returned
  212. * unmodified,
  213. * - an object implementing the `Confidential` interface will be returned
  214. * sanitised,
  215. * - an `Error` instance will be left as is,
  216. * - the binary types `Uint8Array` and `Blob` will only return meta
  217. * information about the content, and
  218. * - everything else will return the value's type instead of the value
  219. * itself.
  220. */
  221. public static replacer(key: string, value: any): any {
  222. // Handle `null` and `undefined` early
  223. if (value === null || value === undefined) {
  224. return value;
  225. }
  226. // Apply filter to confidential data
  227. if (value instanceof BaseConfidential) {
  228. return value.censored();
  229. }
  230. // Allowed (standard) types
  231. for (const allowedType of ALLOWED_TYPES) {
  232. if (value.constructor === allowedType) {
  233. return value;
  234. }
  235. }
  236. // Allow exceptions
  237. if (value instanceof Error) {
  238. return value.toString();
  239. }
  240. // Filter binary data
  241. if (value instanceof ArrayBuffer) {
  242. return `[ArrayBuffer: length=${value.byteLength}]`;
  243. }
  244. if (value instanceof Uint8Array) {
  245. return `[Uint8Array: length=${value.byteLength}, offset=${value.byteOffset}]`;
  246. }
  247. if (value instanceof Blob) {
  248. return `[Blob: length=${value.size}, type=${value.type}]`;
  249. }
  250. // Plain object
  251. if (value.constructor === Object) {
  252. return value;
  253. }
  254. // Not listed
  255. return `[${value.constructor.name}]`;
  256. }
  257. }