1// Copyright 2023 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15import { LogSource } from '../log-source'; 16import { LogEntry, Level } from '../shared/interfaces'; 17import { timeFormat } from '../shared/time-format'; 18 19export class BrowserLogSource extends LogSource { 20 private originalMethods = { 21 log: console.log, 22 info: console.info, 23 warn: console.warn, 24 error: console.error, 25 debug: console.debug, 26 }; 27 28 constructor(sourceName = 'Browser Console') { 29 super(sourceName); 30 } 31 32 private getFileInfo(): string | null { 33 const error = new Error(); 34 const stackLines = error.stack?.split('\n').slice(1); // Skip the error message itself 35 36 for (const line of stackLines || []) { 37 const regex = /(?:\()?(.*?)(?:\?[^:]+)?:(\d+):(\d+)(?:\))?/; 38 const match = regex.exec(line); 39 40 if (match) { 41 const [, filePath, lineNumber] = match; 42 43 // Ignore non-.js or .ts files 44 if ( 45 (!filePath.endsWith('.js') && !filePath.endsWith('.ts')) || 46 filePath.includes('browser-log-source.ts') 47 ) { 48 continue; 49 } 50 51 let fileName; 52 53 if (filePath) { 54 const segments = filePath?.split('/'); 55 const lastSegment = segments?.pop(); 56 const withoutQuery = lastSegment?.split('?')[0]; 57 const withoutFragment = withoutQuery?.split('#')[0]; 58 fileName = withoutFragment; 59 } 60 61 if (fileName && lineNumber) { 62 return `${fileName}:${lineNumber}`; 63 } 64 } 65 } 66 67 return null; 68 } 69 70 private formatMessage(args: IArguments | string[] | object[]): string { 71 if (args.length > 0) { 72 let msg = String(args[0]); 73 const subs = Array.prototype.slice.call(args, 1); 74 let subIndex = 0; 75 msg = msg.replace(/%s|%d|%i|%f/g, (match) => { 76 if (subIndex < subs.length) { 77 const replacement = subs[subIndex++]; 78 switch (match) { 79 case '%s': 80 // Check if replacement is an object and stringify it 81 return typeof replacement === 'object' 82 ? JSON.stringify(replacement) 83 : String(replacement); 84 case '%d': 85 case '%i': 86 return parseInt(replacement, 10); 87 case '%f': 88 return parseFloat(replacement); 89 default: 90 return replacement; 91 } 92 } 93 return match; 94 }); 95 // Handle remaining arguments that were not replaced in the string 96 if (subs.length > subIndex) { 97 const remaining = subs 98 .slice(subIndex) 99 .map((arg) => 100 typeof arg === 'object' 101 ? JSON.stringify(arg, null, 0) 102 : String(arg), 103 ) 104 .join(' '); 105 msg += ` ${remaining}`; 106 } 107 return msg; 108 } 109 return ''; 110 } 111 112 private publishFormattedLogEntry( 113 level: Level, 114 originalArgs: IArguments | string[] | object[], 115 ): void { 116 const formattedMessage = this.formatMessage(originalArgs); 117 const fileInfo = this.getFileInfo(); 118 119 const logEntry: LogEntry = { 120 level: level, 121 timestamp: new Date(), 122 fields: [ 123 { key: 'level', value: level }, 124 { key: 'time', value: timeFormat.format(new Date()) }, 125 { key: 'message', value: formattedMessage }, 126 { key: 'file', value: fileInfo || '' }, 127 ], 128 }; 129 130 this.publishLogEntry(logEntry); 131 } 132 133 start(): void { 134 console.log = (...args: string[] | object[]) => { 135 this.publishFormattedLogEntry(Level.INFO, args); 136 this.originalMethods.log(...args); 137 }; 138 139 console.info = (...args: string[] | object[]) => { 140 this.publishFormattedLogEntry(Level.INFO, args); 141 this.originalMethods.info(...args); 142 }; 143 144 console.warn = (...args: string[] | object[]) => { 145 this.publishFormattedLogEntry(Level.WARNING, args); 146 this.originalMethods.warn(...args); 147 }; 148 149 console.error = (...args: string[] | object[]) => { 150 this.publishFormattedLogEntry(Level.ERROR, args); 151 this.originalMethods.error(...args); 152 }; 153 154 console.debug = (...args: string[] | object[]) => { 155 this.publishFormattedLogEntry(Level.DEBUG, args); 156 this.originalMethods.debug(...args); 157 }; 158 } 159 160 stop(): void { 161 console.log = this.originalMethods.log; 162 console.info = this.originalMethods.info; 163 console.warn = this.originalMethods.warn; 164 console.error = this.originalMethods.error; 165 console.debug = this.originalMethods.debug; 166 } 167} 168