Skip to content

Commit 61178d5

Browse files
committed
Enable contextual debug logging for integration tests
1 parent a40528c commit 61178d5

File tree

2 files changed

+190
-6
lines changed

2 files changed

+190
-6
lines changed

packages/firestore/src/util/log.ts

Lines changed: 186 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
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

2020
import { SDK_VERSION } from '../core/version';
2121
import { formatJSON } from '../platform/format_json';
2222

23+
import { generateUniqueDebugId } from './debug_uid';
24+
2325
export { LogLevel, LogLevelString };
2426

2527
const 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
2832
export 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

4970
export 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

5677
export 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
*/
6687
export 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+
}

packages/firestore/test/integration/util/firebase_export.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222

2323
import { FirebaseApp, initializeApp } from '@firebase/app';
2424

25-
import { Firestore, initializeFirestore } from '../../../src';
25+
import { Firestore, initializeFirestore, setLogLevel } from '../../../src';
2626
import { PrivateSettings } from '../../../src/lite-api/settings';
2727

2828
// TODO(dimond): Right now we create a new app and Firestore instance for
2929
// every test and never clean them up. We may need to revisit.
3030
let appCount = 0;
3131

32+
// enable contextual debug logging
33+
setLogLevel('error', 100);
34+
3235
export function newTestApp(projectId: string, appName?: string): FirebaseApp {
3336
if (appName === undefined) {
3437
appName = 'test-app-' + appCount++;

0 commit comments

Comments
 (0)