change rageshake.ts
to be more similar to the matrix-js version
This commit is contained in:
parent
0aa29f775c
commit
60ed54d6d3
1 changed files with 91 additions and 90 deletions
|
@ -39,6 +39,7 @@ limitations under the License.
|
||||||
// purge on startup to prevent logs from accumulating.
|
// purge on startup to prevent logs from accumulating.
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
// the frequency with which we flush to indexeddb
|
// the frequency with which we flush to indexeddb
|
||||||
const FLUSH_RATE_MS = 30 * 1000;
|
const FLUSH_RATE_MS = 30 * 1000;
|
||||||
|
@ -46,6 +47,11 @@ const FLUSH_RATE_MS = 30 * 1000;
|
||||||
// the length of log data we keep in indexeddb (and include in the reports)
|
// the length of log data we keep in indexeddb (and include in the reports)
|
||||||
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
|
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
|
||||||
|
|
||||||
|
type LogFunction = (
|
||||||
|
...args: (Error | DOMException | object | string)[]
|
||||||
|
) => void;
|
||||||
|
type LogFunctionName = "log" | "info" | "warn" | "error";
|
||||||
|
|
||||||
// A class which monkey-patches the global console and stores log lines.
|
// A class which monkey-patches the global console and stores log lines.
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
|
@ -54,42 +60,40 @@ interface LogEntry {
|
||||||
index?: number;
|
index?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Cursor {
|
|
||||||
id: string;
|
|
||||||
ts: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ConsoleLogger {
|
export class ConsoleLogger {
|
||||||
logs = "";
|
private logs = "";
|
||||||
|
private originalFunctions: { [key in LogFunctionName]?: LogFunction } = {};
|
||||||
|
|
||||||
monkeyPatch(consoleObj: Console): void {
|
public monkeyPatch(consoleObj: Console): void {
|
||||||
// Monkey-patch console logging
|
// Monkey-patch console logging
|
||||||
|
const consoleFunctionsToLevels = {
|
||||||
const consoleFunctionsToLevels: { [level: string]: string } = {
|
|
||||||
log: "I",
|
log: "I",
|
||||||
info: "I",
|
info: "I",
|
||||||
warn: "W",
|
warn: "W",
|
||||||
error: "E",
|
error: "E",
|
||||||
};
|
};
|
||||||
|
Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
|
||||||
Object.keys(consoleFunctionsToLevels).forEach(
|
const level = consoleFunctionsToLevels[fnName];
|
||||||
(fnName: "log" | "info" | "warn" | "error") => {
|
const originalFn = consoleObj[fnName].bind(consoleObj);
|
||||||
const level = consoleFunctionsToLevels[fnName];
|
this.originalFunctions[fnName] = originalFn;
|
||||||
const originalFn = consoleObj[fnName].bind(consoleObj);
|
consoleObj[fnName] = (...args) => {
|
||||||
consoleObj[fnName] = (...args: unknown[]) => {
|
this.log(level, ...args);
|
||||||
this.log(level, ...args);
|
originalFn(...args);
|
||||||
originalFn(...args);
|
};
|
||||||
};
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// these functions get overwritten by the monkey patch
|
public bypassRageshake(
|
||||||
error(...args: unknown[]): void {}
|
fnName: LogFunctionName,
|
||||||
warn(...args: unknown[]): void {}
|
...args: (Error | DOMException | object | string)[]
|
||||||
info(...args: unknown[]): void {}
|
): void {
|
||||||
|
this.originalFunctions[fnName](...args);
|
||||||
|
}
|
||||||
|
|
||||||
log(level: string, ...args: unknown[]): void {
|
public log(
|
||||||
|
level: string,
|
||||||
|
...args: (Error | DOMException | object | string)[]
|
||||||
|
): void {
|
||||||
// We don't know what locale the user may be running so use ISO strings
|
// We don't know what locale the user may be running so use ISO strings
|
||||||
const ts = new Date().toISOString();
|
const ts = new Date().toISOString();
|
||||||
|
|
||||||
|
@ -100,21 +104,7 @@ export class ConsoleLogger {
|
||||||
} else if (arg instanceof Error) {
|
} else if (arg instanceof Error) {
|
||||||
return arg.message + (arg.stack ? `\n${arg.stack}` : "");
|
return arg.message + (arg.stack ? `\n${arg.stack}` : "");
|
||||||
} else if (typeof arg === "object") {
|
} else if (typeof arg === "object") {
|
||||||
try {
|
return JSON.stringify(arg, getCircularReplacer());
|
||||||
return JSON.stringify(arg);
|
|
||||||
} catch (e) {
|
|
||||||
// In development, it can be useful to log complex cyclic
|
|
||||||
// objects to the console for inspection. This is fine for
|
|
||||||
// the console, but default `stringify` can't handle that.
|
|
||||||
// We workaround this by using a special replacer function
|
|
||||||
// to only log values of the root object and avoid cycles.
|
|
||||||
return JSON.stringify(arg, (key, value) => {
|
|
||||||
if (key && typeof value === "object") {
|
|
||||||
return "<object>";
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return arg;
|
return arg;
|
||||||
}
|
}
|
||||||
|
@ -135,10 +125,10 @@ export class ConsoleLogger {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve log lines to flush to disk.
|
* Retrieve log lines to flush to disk.
|
||||||
* @param {boolean} keepLogs True to not delete logs after flushing. Defaults to false.
|
* @param {boolean} keepLogs True to not delete logs after flushing.
|
||||||
* @return {string} \n delimited log lines to flush.
|
* @return {string} \n delimited log lines to flush.
|
||||||
*/
|
*/
|
||||||
flush(keepLogs = false): string {
|
public flush(keepLogs?: boolean): string {
|
||||||
// The ConsoleLogger doesn't care how these end up on disk, it just
|
// The ConsoleLogger doesn't care how these end up on disk, it just
|
||||||
// flushes them to the caller.
|
// flushes them to the caller.
|
||||||
if (keepLogs) {
|
if (keepLogs) {
|
||||||
|
@ -152,26 +142,22 @@ export class ConsoleLogger {
|
||||||
|
|
||||||
// A class which stores log lines in an IndexedDB instance.
|
// A class which stores log lines in an IndexedDB instance.
|
||||||
export class IndexedDBLogStore {
|
export class IndexedDBLogStore {
|
||||||
index = 0;
|
private index = 0;
|
||||||
db: IDBDatabase = null;
|
private db: IDBDatabase = null;
|
||||||
flushPromise: Promise<void> = null;
|
private flushPromise: Promise<void> = null;
|
||||||
flushAgainPromise: Promise<void> = null;
|
private flushAgainPromise: Promise<void> = null;
|
||||||
indexedDB: IDBFactory;
|
private id: string;
|
||||||
logger: ConsoleLogger;
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
constructor(indexedDB: IDBFactory, logger: ConsoleLogger) {
|
constructor(private indexedDB: IDBFactory, private logger: ConsoleLogger) {
|
||||||
this.indexedDB = indexedDB;
|
this.id = "instance-" + randomString(16);
|
||||||
this.logger = logger;
|
|
||||||
this.id = "instance-" + Math.random() + Date.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Promise} Resolves when the store is ready.
|
* @return {Promise} Resolves when the store is ready.
|
||||||
*/
|
*/
|
||||||
connect(): Promise<void> {
|
public connect(): Promise<void> {
|
||||||
const req = this.indexedDB.open("logs");
|
const req = this.indexedDB.open("logs");
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.onsuccess = (event: Event) => {
|
req.onsuccess = (event: Event) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.db = event.target.result;
|
this.db = event.target.result;
|
||||||
|
@ -180,16 +166,16 @@ export class IndexedDBLogStore {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
req.onerror = (event: Event) => {
|
req.onerror = (event) => {
|
||||||
const err =
|
const err =
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
"Failed to open log database: " + event.target.error.name;
|
"Failed to open log database: " + event.target.error.name;
|
||||||
this.logger.error(err);
|
logger.error(err);
|
||||||
reject(new Error(err));
|
reject(new Error(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
// First time: Setup the object store
|
// First time: Setup the object store
|
||||||
req.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
req.onupgradeneeded = (event) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const db = event.target.result;
|
const db = event.target.result;
|
||||||
const logObjStore = db.createObjectStore("logs", {
|
const logObjStore = db.createObjectStore("logs", {
|
||||||
|
@ -231,7 +217,7 @@ export class IndexedDBLogStore {
|
||||||
*
|
*
|
||||||
* @return {Promise} Resolved when the logs have been flushed.
|
* @return {Promise} Resolved when the logs have been flushed.
|
||||||
*/
|
*/
|
||||||
flush(): Promise<void> {
|
public flush(): Promise<void> {
|
||||||
// check if a flush() operation is ongoing
|
// check if a flush() operation is ongoing
|
||||||
if (this.flushPromise) {
|
if (this.flushPromise) {
|
||||||
if (this.flushAgainPromise) {
|
if (this.flushAgainPromise) {
|
||||||
|
@ -267,7 +253,7 @@ export class IndexedDBLogStore {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
txn.onerror = (event) => {
|
txn.onerror = (event) => {
|
||||||
this.logger.error("Failed to flush logs : ", event);
|
logger.error("Failed to flush logs : ", event);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
reject(new Error("Failed to write logs: " + event.target.errorCode));
|
reject(new Error("Failed to write logs: " + event.target.errorCode));
|
||||||
};
|
};
|
||||||
|
@ -290,13 +276,12 @@ export class IndexedDBLogStore {
|
||||||
* log ID). The objects have said log ID in an "id" field and "lines" which
|
* log ID). The objects have said log ID in an "id" field and "lines" which
|
||||||
* is a big string with all the new-line delimited logs.
|
* is a big string with all the new-line delimited logs.
|
||||||
*/
|
*/
|
||||||
|
public async consume(): Promise<LogEntry[]> {
|
||||||
async consume(): Promise<Object[]> {
|
|
||||||
const db = this.db;
|
const db = this.db;
|
||||||
|
|
||||||
// Returns: a string representing the concatenated logs for this ID.
|
// Returns: a string representing the concatenated logs for this ID.
|
||||||
// Stops adding log fragments when the size exceeds maxSize
|
// Stops adding log fragments when the size exceeds maxSize
|
||||||
function fetchLogs(id: string, maxSize: number): Promise<string[]> {
|
function fetchLogs(id: string, maxSize: number): Promise<string> {
|
||||||
const objectStore = db
|
const objectStore = db
|
||||||
.transaction("logs", "readonly")
|
.transaction("logs", "readonly")
|
||||||
.objectStore("logs");
|
.objectStore("logs");
|
||||||
|
@ -314,28 +299,29 @@ export class IndexedDBLogStore {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const cursor = event.target.result;
|
const cursor = event.target.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
resolve(lines.split("\n"));
|
resolve(lines);
|
||||||
return; // end of results
|
return; // end of results
|
||||||
}
|
}
|
||||||
lines = cursor.value.lines + lines;
|
lines = cursor.value.lines + lines;
|
||||||
if (lines.length >= maxSize) {
|
if (lines.length >= maxSize) {
|
||||||
resolve(lines.split("\n"));
|
resolve(lines);
|
||||||
} else {
|
} else {
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns: A sorted array of log IDs. (newest first)
|
// Returns: A sorted array of log IDs. (newest first)
|
||||||
function fetchLogIds(): Promise<string[]> {
|
function fetchLogIds(): Promise<string[]> {
|
||||||
// To gather all the log IDs, query for all records in logslastmod.
|
// To gather all the log IDs, query for all records in logslastmod.
|
||||||
const o = db
|
const o = db
|
||||||
.transaction("logslastmod", "readonly")
|
.transaction("logslastmod", "readonly")
|
||||||
.objectStore("logslastmod");
|
.objectStore("logslastmod");
|
||||||
return selectQuery(o, undefined, (cursor: Cursor) => {
|
return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => {
|
||||||
return {
|
return {
|
||||||
id: cursor.id,
|
id: cursor.value.id,
|
||||||
ts: cursor.ts,
|
ts: cursor.value.ts,
|
||||||
};
|
};
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
// Sort IDs by timestamp (newest first)
|
// Sort IDs by timestamp (newest first)
|
||||||
|
@ -346,8 +332,9 @@ export class IndexedDBLogStore {
|
||||||
.map((a) => a.id);
|
.map((a) => a.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function deleteLogs(id: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
function deleteLogs(id: number): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
const txn = db.transaction(["logs", "logslastmod"], "readwrite");
|
const txn = db.transaction(["logs", "logslastmod"], "readwrite");
|
||||||
const o = txn.objectStore("logs");
|
const o = txn.objectStore("logs");
|
||||||
// only load the key path, not the data which may be huge
|
// only load the key path, not the data which may be huge
|
||||||
|
@ -380,14 +367,11 @@ export class IndexedDBLogStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
const allLogIds = await fetchLogIds();
|
const allLogIds = await fetchLogIds();
|
||||||
let removeLogIds: string[] = [];
|
let removeLogIds = [];
|
||||||
const logs = [];
|
const logs: LogEntry[] = [];
|
||||||
let size = 0;
|
let size = 0;
|
||||||
for (let i = 0; i < allLogIds.length; i++) {
|
for (let i = 0; i < allLogIds.length; i++) {
|
||||||
const lines: string[] = await fetchLogs(
|
const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size);
|
||||||
allLogIds[i],
|
|
||||||
MAX_LOG_SIZE - size
|
|
||||||
);
|
|
||||||
|
|
||||||
// always add the log file: fetchLogs will truncate once the maxSize we give it is
|
// always add the log file: fetchLogs will truncate once the maxSize we give it is
|
||||||
// exceeded, so we'll go over the max but only by one fragment's worth.
|
// exceeded, so we'll go over the max but only by one fragment's worth.
|
||||||
|
@ -407,22 +391,22 @@ export class IndexedDBLogStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (removeLogIds.length > 0) {
|
if (removeLogIds.length > 0) {
|
||||||
this.logger.log("Removing logs: ", removeLogIds);
|
logger.log("Removing logs: ", removeLogIds);
|
||||||
// Don't await this because it's non-fatal if we can't clean up
|
// Don't await this because it's non-fatal if we can't clean up
|
||||||
// logs.
|
// logs.
|
||||||
Promise.all(removeLogIds.map((id) => deleteLogs(id))).then(
|
Promise.all(removeLogIds.map((id) => deleteLogs(id))).then(
|
||||||
() => {
|
() => {
|
||||||
this.logger.log(`Removed ${removeLogIds.length} old logs.`);
|
logger.log(`Removed ${removeLogIds.length} old logs.`);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
this.logger.error(err);
|
logger.error(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return logs;
|
return logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateLogEntry(lines: string): LogEntry {
|
private generateLogEntry(lines: string): LogEntry {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
lines: lines,
|
lines: lines,
|
||||||
|
@ -430,7 +414,7 @@ export class IndexedDBLogStore {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
generateLastModifiedTime(): Cursor {
|
private generateLastModifiedTime(): { id: string; ts: number } {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
@ -448,22 +432,22 @@ export class IndexedDBLogStore {
|
||||||
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||||
* resultMapper.
|
* resultMapper.
|
||||||
*/
|
*/
|
||||||
function selectQuery(
|
function selectQuery<T>(
|
||||||
store: IDBObjectStore,
|
store: IDBObjectStore,
|
||||||
keyRange: IDBKeyRange,
|
keyRange: IDBKeyRange,
|
||||||
resultMapper: (arg: Cursor) => Cursor
|
resultMapper: (cursor: IDBCursorWithValue) => T
|
||||||
): Promise<Cursor[]> {
|
): Promise<T[]> {
|
||||||
const query = store.openCursor(keyRange);
|
const query = store.openCursor(keyRange);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const results: Cursor[] = [];
|
const results = [];
|
||||||
query.onerror = (event: Event) => {
|
query.onerror = (event) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
reject(new Error("Query failed: " + event.target.errorCode));
|
reject(new Error("Query failed: " + event.target.errorCode));
|
||||||
};
|
};
|
||||||
// collect results
|
// collect results
|
||||||
query.onsuccess = (event: Event) => {
|
query.onsuccess = (event) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const cursor = event.target.result?.value;
|
const cursor = event.target.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
resolve(results);
|
resolve(results);
|
||||||
return; // end of results
|
return; // end of results
|
||||||
|
@ -569,13 +553,30 @@ export async function getLogsForReport(): Promise<LogEntry[]> {
|
||||||
if (global.mx_rage_store) {
|
if (global.mx_rage_store) {
|
||||||
// flush most recent logs
|
// flush most recent logs
|
||||||
await global.mx_rage_store.flush();
|
await global.mx_rage_store.flush();
|
||||||
return (await global.mx_rage_store.consume()) as LogEntry[];
|
return global.mx_rage_store.consume();
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
lines: global.mx_rage_logger.flush(true),
|
lines: global.mx_rage_logger.flush(true),
|
||||||
id: "-",
|
id: "-",
|
||||||
},
|
},
|
||||||
] as LogEntry[];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StringifyReplacer = (this: any, key: string, value: any) => any;
|
||||||
|
|
||||||
|
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
|
||||||
|
// Injects `<$ cycle-trimmed $>` wherever it cuts a cyclical object relationship
|
||||||
|
const getCircularReplacer = (): StringifyReplacer => {
|
||||||
|
const seen = new WeakSet();
|
||||||
|
return (key: string, value: any): any => {
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return "<$ cycle-trimmed $>";
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue