1515 * limitations under the License.
1616 */
1717
18- import { Logger , LogLevel , LogLevelString } from '@firebase/logger' ;
18+ import { Logger , LogHandler , LogLevel , LogLevelString } from '@firebase/logger' ;
1919
2020import { SDK_VERSION } from '../core/version' ;
2121import { formatJSON } from '../platform/format_json' ;
2222
23+ import { generateUniqueDebugId } from './debug_uid' ;
24+
2325export { LogLevel , LogLevelString } ;
2426
2527const logClient = new Logger ( '@firebase/firestore' ) ;
28+ const defaultLogHandler = logClient . logHandler ;
29+ let logBuffer : LogBuffer | undefined ;
2630
2731// Helper methods are needed because variables can't be exported as read/write
2832export function getLogLevel ( ) : LogLevel {
@@ -41,20 +45,37 @@ export function getLogLevel(): LogLevel {
4145 * <li>`error` to log errors only.</li>
4246 * <li><code>`silent` to turn off logging.</li>
4347 * </ul>
48+ * @param includeContext - If set to a positive value, the logger will buffer
49+ * all log messages (of all log levels) and log the most recent messages
50+ * when a message of `logLevel` is seen. This is useful if you want to get
51+ * debug logging from the SDK leading up to a warning or error, but do not
52+ * always want debug log verbosity. This param specifies how many messages
53+ * to buffer.
4454 */
45- export function setLogLevel ( logLevel : LogLevelString ) : void {
55+ export function setLogLevel (
56+ logLevel : LogLevelString ,
57+ includeContext : number = 0
58+ ) : void {
4659 logClient . setLogLevel ( logLevel ) ;
60+
61+ if ( includeContext > 0 ) {
62+ logBuffer = new LogBuffer ( includeContext ) ;
63+ logClient . logHandler = bufferingLogHandler ;
64+ } else {
65+ logBuffer = undefined ;
66+ logClient . logHandler = defaultLogHandler ;
67+ }
4768}
4869
4970export function logDebug ( msg : string , ...obj : unknown [ ] ) : void {
50- if ( logClient . logLevel <= LogLevel . DEBUG ) {
71+ if ( logBuffer || logClient . logLevel <= LogLevel . DEBUG ) {
5172 const args = obj . map ( argToString ) ;
5273 logClient . debug ( `Firestore (${ SDK_VERSION } ): ${ msg } ` , ...args ) ;
5374 }
5475}
5576
5677export function logError ( msg : string , ...obj : unknown [ ] ) : void {
57- if ( logClient . logLevel <= LogLevel . ERROR ) {
78+ if ( logBuffer || logClient . logLevel <= LogLevel . ERROR ) {
5879 const args = obj . map ( argToString ) ;
5980 logClient . error ( `Firestore (${ SDK_VERSION } ): ${ msg } ` , ...args ) ;
6081 }
@@ -64,7 +85,7 @@ export function logError(msg: string, ...obj: unknown[]): void {
6485 * @internal
6586 */
6687export function logWarn ( msg : string , ...obj : unknown [ ] ) : void {
67- if ( logClient . logLevel <= LogLevel . WARN ) {
88+ if ( logBuffer || logClient . logLevel <= LogLevel . WARN ) {
6889 const args = obj . map ( argToString ) ;
6990 logClient . warn ( `Firestore (${ SDK_VERSION } ): ${ msg } ` , ...args ) ;
7091 }
@@ -85,3 +106,163 @@ function argToString(obj: unknown): string | unknown {
85106 }
86107 }
87108}
109+
110+ class LogBuffer {
111+ private _buffer : Array < { level : LogLevel ; now : string ; args : unknown [ ] } > ;
112+ private _numTruncated : number = 0 ;
113+
114+ constructor ( readonly bufferSize : number ) {
115+ this . _buffer = [ ] ;
116+ this . _numTruncated = 0 ;
117+ }
118+
119+ /**
120+ * Clear the log buffer
121+ */
122+ clear ( ) : void {
123+ this . _buffer = [ ] ;
124+ this . _numTruncated = 0 ;
125+ }
126+
127+ /**
128+ * Add a new log message to the buffer. If the buffer will exceed
129+ * the allocated buffer size, then remove the oldest message from
130+ * the buffer.
131+ * @param level
132+ * @param now
133+ * @param args
134+ */
135+ add ( level : LogLevel , now : string , args : unknown [ ] ) : void {
136+ this . _buffer . push ( {
137+ level,
138+ now,
139+ args
140+ } ) ;
141+
142+ if ( this . _buffer . length > this . bufferSize ) {
143+ // remove the first (oldest) element
144+ this . _buffer . shift ( ) ;
145+ this . _numTruncated ++ ;
146+ }
147+ }
148+
149+ /**
150+ * Returns the number of old log messages that have been
151+ * truncated from the log to maintain buffer size.
152+ */
153+ get numTruncated ( ) : number {
154+ return this . _numTruncated ;
155+ }
156+
157+ get first ( ) : { level : LogLevel ; now : string ; args : unknown [ ] } | undefined {
158+ return this . _buffer [ 0 ] ;
159+ }
160+
161+ /**
162+ * Iterate from oldest to newest.
163+ */
164+ [ Symbol . iterator ] ( ) : Iterator < {
165+ level : LogLevel ;
166+ now : string ;
167+ args : unknown [ ] ;
168+ } > {
169+ let currentIndex = 0 ;
170+ // Create a snapshot of the buffer for iteration.
171+ // This ensures that if the buffer is modified while iterating (e.g., by adding new logs),
172+ // the iterator will continue to iterate over the state of the buffer as it was when iteration began.
173+ // It also means you iterate from the oldest to the newest log.
174+ const bufferSnapshot = [ ...this . _buffer ] ;
175+
176+ return {
177+ next : ( ) : IteratorResult < {
178+ level : LogLevel ;
179+ now : string ;
180+ args : unknown [ ] ;
181+ } > => {
182+ if ( currentIndex < bufferSnapshot . length ) {
183+ return { value : bufferSnapshot [ currentIndex ++ ] , done : false } ;
184+ } else {
185+ return { value : undefined , done : true } ;
186+ }
187+ }
188+ } ;
189+ }
190+ }
191+
192+ /**
193+ * By default, `console.debug` is not displayed in the developer console (in
194+ * chrome). To avoid forcing users to have to opt-in to these logs twice
195+ * (i.e. once for firebase, and once in the console), we are sending `DEBUG`
196+ * logs to the `console.log` function.
197+ */
198+ const ConsoleMethod = {
199+ [ LogLevel . DEBUG ] : 'log' ,
200+ [ LogLevel . VERBOSE ] : 'log' ,
201+ [ LogLevel . INFO ] : 'info' ,
202+ [ LogLevel . WARN ] : 'warn' ,
203+ [ LogLevel . ERROR ] : 'error'
204+ } ;
205+
206+ /**
207+ * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR
208+ * messages on to their corresponding console counterparts (if the log method
209+ * is supported by the current log level)
210+ */
211+ const bufferingLogHandler : LogHandler = ( instance , logType , ...args ) : void => {
212+ const now = new Date ( ) . toISOString ( ) ;
213+
214+ // Fail-safe. This is never expected to be true, but if it is,
215+ // it's not important enough to throw.
216+ if ( ! logBuffer ) {
217+ defaultLogHandler ( instance , logType , args ) ;
218+ return ;
219+ }
220+
221+ // Buffer any messages less than the current logLevel
222+ if ( logType < instance . logLevel ) {
223+ logBuffer ! . add ( logType , now , args ) ;
224+ return ;
225+ }
226+
227+ // create identifier that associates all of the associated
228+ // context messages with the log message that caused the
229+ // flush of the logBuffer
230+ const id = generateUniqueDebugId ( ) ;
231+
232+ // Optionally write a log message stating if any log messages
233+ // were skipped.
234+ if ( logBuffer . first ) {
235+ writeLog ( instance , id , LogLevel . INFO , logBuffer . first . now , [
236+ `... ${ logBuffer . numTruncated } log messages skipped ...`
237+ ] ) ;
238+ }
239+
240+ // If here, write the log buffer contents as context
241+ for ( const logInfo of logBuffer ) {
242+ writeLog ( instance , id , logInfo . level , logInfo . now , logInfo . args ) ;
243+ }
244+ logBuffer . clear ( ) ;
245+
246+ // Now write the target log message.
247+ writeLog ( instance , id , logType , now , args ) ;
248+ } ;
249+
250+ function writeLog (
251+ instance : Logger ,
252+ id : string ,
253+ logType : LogLevel ,
254+ now : string ,
255+ args : unknown [ ]
256+ ) : void {
257+ const method = ConsoleMethod [ logType as keyof typeof ConsoleMethod ] ;
258+ if ( method ) {
259+ console [ method as 'log' | 'info' | 'warn' | 'error' ] (
260+ `[${ now } ] (context: ${ id } ) ${ instance . name } :` ,
261+ ...args
262+ ) ;
263+ } else {
264+ throw new Error (
265+ `Attempted to log a message with an invalid logType (value: ${ logType } )`
266+ ) ;
267+ }
268+ }
0 commit comments