You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
8.6 KiB

  1. 'use strict';
  2. import { ExtensionContext, ExtensionMode, OutputChannel, Uri, window } from 'vscode';
  3. import { TraceLevel } from './configuration';
  4. import { getCorrelationContext, getNextCorrelationId } from './system';
  5. const emptyStr = '';
  6. const extensionOutputChannelName = 'GitLens';
  7. const ConsolePrefix = `[${extensionOutputChannelName}]`;
  8. export { TraceLevel } from './configuration';
  9. export interface LogCorrelationContext {
  10. readonly correlationId?: number;
  11. readonly prefix: string;
  12. exitDetails?: string;
  13. }
  14. export class Logger {
  15. static output: OutputChannel | undefined;
  16. static customLoggableFn: ((o: object) => string | undefined) | undefined;
  17. static configure(context: ExtensionContext, level: TraceLevel, loggableFn?: (o: any) => string | undefined) {
  18. this.customLoggableFn = loggableFn;
  19. this._isDebugging = context.extensionMode === ExtensionMode.Development;
  20. this.level = level;
  21. }
  22. private static _isDebugging: boolean;
  23. static get isDebugging() {
  24. return this._isDebugging;
  25. }
  26. private static _level: TraceLevel = TraceLevel.Silent;
  27. static get level() {
  28. return this._level;
  29. }
  30. static set level(value: TraceLevel) {
  31. this._level = value;
  32. if (value === TraceLevel.Silent) {
  33. if (this.output != null) {
  34. this.output.dispose();
  35. this.output = undefined;
  36. }
  37. } else {
  38. this.output = this.output ?? window.createOutputChannel(extensionOutputChannelName);
  39. }
  40. }
  41. static debug(message: string, ...params: any[]): void;
  42. static debug(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void;
  43. static debug(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void {
  44. if (this.level !== TraceLevel.Debug && !Logger.isDebugging) return;
  45. let message;
  46. if (typeof contextOrMessage === 'string') {
  47. message = contextOrMessage;
  48. } else {
  49. message = params.shift();
  50. if (contextOrMessage != null) {
  51. message = `${contextOrMessage.prefix} ${message ?? emptyStr}`;
  52. }
  53. }
  54. if (Logger.isDebugging) {
  55. console.log(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params);
  56. }
  57. if (this.output != null && this.level === TraceLevel.Debug) {
  58. this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(true, params)}`);
  59. }
  60. }
  61. static error(ex: Error, message?: string, ...params: any[]): void;
  62. static error(ex: Error, context?: LogCorrelationContext, message?: string, ...params: any[]): void;
  63. static error(ex: Error, contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void {
  64. if (this.level === TraceLevel.Silent && !Logger.isDebugging) return;
  65. let message;
  66. if (contextOrMessage == null || typeof contextOrMessage === 'string') {
  67. message = contextOrMessage;
  68. } else {
  69. message = `${contextOrMessage.prefix} ${params.shift() ?? emptyStr}`;
  70. }
  71. if (message == null) {
  72. const stack = ex.stack;
  73. if (stack) {
  74. const match = /.*\s*?at\s(.+?)\s/.exec(stack);
  75. if (match != null) {
  76. message = match[1];
  77. }
  78. }
  79. }
  80. if (Logger.isDebugging) {
  81. console.error(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params, ex);
  82. }
  83. if (this.output != null && this.level !== TraceLevel.Silent) {
  84. this.output.appendLine(
  85. `${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}\n${ex?.toString()}`,
  86. );
  87. }
  88. }
  89. static getCorrelationContext() {
  90. return getCorrelationContext();
  91. }
  92. static getNewCorrelationContext(prefix: string): LogCorrelationContext {
  93. const correlationId = getNextCorrelationId();
  94. return {
  95. correlationId: correlationId,
  96. prefix: `[${correlationId}] ${prefix}`,
  97. };
  98. }
  99. static log(message: string, ...params: any[]): void;
  100. static log(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void;
  101. static log(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void {
  102. if (this.level !== TraceLevel.Verbose && this.level !== TraceLevel.Debug && !Logger.isDebugging) {
  103. return;
  104. }
  105. let message;
  106. if (typeof contextOrMessage === 'string') {
  107. message = contextOrMessage;
  108. } else {
  109. message = params.shift();
  110. if (contextOrMessage != null) {
  111. message = `${contextOrMessage.prefix} ${message ?? emptyStr}`;
  112. }
  113. }
  114. if (Logger.isDebugging) {
  115. console.log(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params);
  116. }
  117. if (this.output != null && (this.level === TraceLevel.Verbose || this.level === TraceLevel.Debug)) {
  118. this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`);
  119. }
  120. }
  121. static logWithDebugParams(message: string, ...params: any[]): void;
  122. static logWithDebugParams(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void;
  123. static logWithDebugParams(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void {
  124. if (this.level !== TraceLevel.Verbose && this.level !== TraceLevel.Debug && !Logger.isDebugging) {
  125. return;
  126. }
  127. let message;
  128. if (typeof contextOrMessage === 'string') {
  129. message = contextOrMessage;
  130. } else {
  131. message = params.shift();
  132. if (contextOrMessage != null) {
  133. message = `${contextOrMessage.prefix} ${message ?? emptyStr}`;
  134. }
  135. }
  136. if (Logger.isDebugging) {
  137. console.log(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params);
  138. }
  139. if (this.output != null && (this.level === TraceLevel.Verbose || this.level === TraceLevel.Debug)) {
  140. this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(true, params)}`);
  141. }
  142. }
  143. static warn(message: string, ...params: any[]): void;
  144. static warn(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void;
  145. static warn(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void {
  146. if (this.level === TraceLevel.Silent && !Logger.isDebugging) return;
  147. let message;
  148. if (typeof contextOrMessage === 'string') {
  149. message = contextOrMessage;
  150. } else {
  151. message = params.shift();
  152. if (contextOrMessage != null) {
  153. message = `${contextOrMessage.prefix} ${message ?? emptyStr}`;
  154. }
  155. }
  156. if (Logger.isDebugging) {
  157. console.warn(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params);
  158. }
  159. if (this.output != null && this.level !== TraceLevel.Silent) {
  160. this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`);
  161. }
  162. }
  163. static willLog(type: 'debug' | 'error' | 'log' | 'warn'): boolean {
  164. switch (type) {
  165. case 'debug':
  166. return this.level === TraceLevel.Debug || Logger.isDebugging;
  167. case 'error':
  168. case 'warn':
  169. return this.level !== TraceLevel.Silent || Logger.isDebugging;
  170. case 'log':
  171. return this.level === TraceLevel.Verbose || this.level === TraceLevel.Debug || Logger.isDebugging;
  172. default:
  173. return false;
  174. }
  175. }
  176. static showOutputChannel() {
  177. if (this.output == null) return;
  178. this.output.show();
  179. }
  180. static toLoggable(p: any, sanitize?: ((key: string, value: any) => any) | undefined) {
  181. if (typeof p !== 'object') return String(p);
  182. if (this.customLoggableFn != null) {
  183. const loggable = this.customLoggableFn(p);
  184. if (loggable != null) return loggable;
  185. }
  186. if (p instanceof Uri) return `Uri(${p.toString(true)})`;
  187. try {
  188. return JSON.stringify(p, sanitize);
  189. } catch {
  190. return '<error>';
  191. }
  192. }
  193. static toLoggableName(instance: Function | object) {
  194. let name: string;
  195. if (typeof instance === 'function') {
  196. if (instance.prototype == null || instance.prototype.constructor == null) {
  197. return instance.name;
  198. }
  199. name = instance.prototype.constructor.name ?? emptyStr;
  200. } else {
  201. name = instance.constructor?.name ?? emptyStr;
  202. }
  203. // Strip webpack module name (since I never name classes with an _)
  204. const index = name.indexOf('_');
  205. return index === -1 ? name : name.substr(index + 1);
  206. }
  207. private static get timestamp(): string {
  208. const now = new Date();
  209. return `[${now
  210. .toISOString()
  211. .replace(/T/, ' ')
  212. .replace(/\..+/, emptyStr)}:${`00${now.getUTCMilliseconds()}`.slice(-3)}]`;
  213. }
  214. private static toLoggableParams(debugOnly: boolean, params: any[]) {
  215. if (params.length === 0 || (debugOnly && this.level !== TraceLevel.Debug && !Logger.isDebugging)) {
  216. return emptyStr;
  217. }
  218. const loggableParams = params.map(p => this.toLoggable(p)).join(', ');
  219. return loggableParams.length !== 0 ? ` \u2014 ${loggableParams}` : emptyStr;
  220. }
  221. static gitOutput: OutputChannel | undefined;
  222. static logGitCommand(command: string, ex?: Error): void {
  223. if (this.level !== TraceLevel.Debug) return;
  224. if (this.gitOutput == null) {
  225. this.gitOutput = window.createOutputChannel(`${extensionOutputChannelName} (Git)`);
  226. }
  227. this.gitOutput.appendLine(`${this.timestamp} ${command}${ex != null ? `\n\n${ex.toString()}` : emptyStr}`);
  228. }
  229. }