persistent logging
This commit is contained in:
parent
2986afb3d4
commit
356ebd2137
@ -33,7 +33,7 @@ window.addEventListener("load", () => {
|
||||
System.import("../wxBackend")
|
||||
.then((wxMessaging: any) => {
|
||||
// Export as global for debugger
|
||||
(window as any).wxMessaging = wxMessaging;
|
||||
(window as any).wx = wxMessaging;
|
||||
wxMessaging.wxMain();
|
||||
}).catch((e: Error) => {
|
||||
console.error("Loading Taler wallet background page failed.", e);
|
||||
|
186
src/logging.ts
Normal file
186
src/logging.ts
Normal file
@ -0,0 +1,186 @@
|
||||
/*
|
||||
This file is part of TALER
|
||||
(C) 2016 Inria
|
||||
|
||||
TALER is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configurable logging.
|
||||
*
|
||||
* @author Florian Dold
|
||||
*/
|
||||
|
||||
import {Store, QueryRoot} from "./query";
|
||||
|
||||
export type Level = "error" | "debug" | "info" | "warn";
|
||||
|
||||
function makeInfo() {
|
||||
return console.info.bind(console, "%o");
|
||||
}
|
||||
|
||||
function makeWarn() {
|
||||
return console.warn.bind(console, "%o");
|
||||
}
|
||||
|
||||
function makeError() {
|
||||
return console.error.bind(console, "%o");
|
||||
}
|
||||
|
||||
function makeDebug() {
|
||||
return console.log.bind(console, "%o");
|
||||
}
|
||||
|
||||
export async function log(msg: string, level: Level = "info"): Promise<void> {
|
||||
let ci = getCallInfo(2);
|
||||
return record(level, msg, ci.file, ci.line, ci.column);
|
||||
}
|
||||
|
||||
function getCallInfo(level: number) {
|
||||
// see https://github.com/v8/v8/wiki/Stack-Trace-API
|
||||
let stack = Error().stack;
|
||||
if (!stack) {
|
||||
return unknownFrame;
|
||||
}
|
||||
let lines = stack.split("\n");
|
||||
return parseStackLine(lines[level + 1]);
|
||||
}
|
||||
|
||||
interface Frame {
|
||||
file?: string;
|
||||
method?: string;
|
||||
column?: number;
|
||||
line?: number;
|
||||
}
|
||||
|
||||
const unknownFrame: Frame = {
|
||||
file: "(unknown)",
|
||||
method: "(unknown)",
|
||||
line: 0,
|
||||
column: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Adapted from https://github.com/errwischt/stacktrace-parser.
|
||||
*/
|
||||
function parseStackLine(stackLine: string): Frame {
|
||||
const chrome = /^\s*at (?:(?:(?:Anonymous function)?|((?:\[object object\])?\S+(?: \[as \S+\])?)) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
|
||||
const gecko = /^(?:\s*([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i;
|
||||
const node = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
|
||||
let parts;
|
||||
|
||||
if ((parts = gecko.exec(stackLine))) {
|
||||
let f: Frame = {
|
||||
file: parts[3],
|
||||
method: parts[1] || "(unknown)",
|
||||
line: +parts[4],
|
||||
column: parts[5] ? +parts[5] : undefined,
|
||||
};
|
||||
return f;
|
||||
} else if ((parts = chrome.exec(stackLine))) {
|
||||
let f: Frame = {
|
||||
file: parts[2],
|
||||
method: parts[1] || "(unknown)",
|
||||
line: +parts[3],
|
||||
column: parts[4] ? +parts[4] : undefined,
|
||||
};
|
||||
return f;
|
||||
} else if ((parts = node.exec(stackLine))) {
|
||||
let f: Frame = {
|
||||
file: parts[2],
|
||||
method: parts[1] || "(unknown)",
|
||||
line: +parts[3],
|
||||
column: parts[4] ? +parts[4] : undefined,
|
||||
};
|
||||
return f;
|
||||
}
|
||||
return unknownFrame;
|
||||
}
|
||||
|
||||
|
||||
let db: IDBDatabase|undefined = undefined;
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
level: string;
|
||||
msg: string;
|
||||
source: string|undefined;
|
||||
col: number|undefined;
|
||||
line: number|undefined;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export async function getLogs(): Promise<LogEntry[]> {
|
||||
if (!db) {
|
||||
db = await openLoggingDb();
|
||||
}
|
||||
return await new QueryRoot(db).iter(logsStore).toArray();
|
||||
}
|
||||
|
||||
export async function record(level: Level, msg: string, source?: string, line?: number, col?: number): Promise<void> {
|
||||
if (typeof indexedDB === "undefined") {
|
||||
return;
|
||||
}
|
||||
if (!db) {
|
||||
db = await openLoggingDb();
|
||||
}
|
||||
|
||||
let count = await new QueryRoot(db).count(logsStore);
|
||||
|
||||
console.log("count is", count);
|
||||
|
||||
if (count > 1000) {
|
||||
await new QueryRoot(db).deleteIf(logsStore, (e, i) => (i < 200));
|
||||
}
|
||||
|
||||
let entry: LogEntry = {
|
||||
timestamp: new Date().getTime(),
|
||||
level,
|
||||
msg,
|
||||
source,
|
||||
line,
|
||||
col,
|
||||
};
|
||||
await new QueryRoot(db).put(logsStore, entry);
|
||||
}
|
||||
|
||||
const loggingDbVersion = 1;
|
||||
|
||||
const logsStore: Store<LogEntry> = new Store<LogEntry>("logs");
|
||||
|
||||
export function openLoggingDb(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open("taler-logging", loggingDbVersion);
|
||||
req.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
req.onsuccess = (e) => {
|
||||
resolve(req.result);
|
||||
};
|
||||
req.onupgradeneeded = (e) => {
|
||||
const db = req.result;
|
||||
if (e.oldVersion != 0) {
|
||||
try {
|
||||
db.deleteObjectStore("logs");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
db.createObjectStore("logs", {keyPath: "id", autoIncrement: true});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const info = makeInfo();
|
||||
export const debug = makeDebug();
|
||||
export const warn = makeWarn();
|
||||
export const error = makeError();
|
36
src/pages/logs.html
Normal file
36
src/pages/logs.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Taler Wallet: Logs</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../style/lang.css">
|
||||
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
|
||||
|
||||
<link rel="icon" href="/img/icon.png">
|
||||
|
||||
<script src="/src/vendor/URI.js"></script>
|
||||
<script src="/src/vendor/react.js"></script>
|
||||
<script src="/src/vendor/react-dom.js"></script>
|
||||
|
||||
<!-- i18n -->
|
||||
<script src="/src/vendor/jed.js"></script>
|
||||
<script src="/src/i18n.js"></script>
|
||||
<script src="/src/i18n/strings.js"></script>
|
||||
|
||||
<script src="/src/vendor/system-csp-production.src.js"></script>
|
||||
<script src="/src/module-trampoline.js"></script>
|
||||
|
||||
<style>
|
||||
.tree-item {
|
||||
margin: 2em;
|
||||
border-radius: 5px;
|
||||
border: 1px solid gray;
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
76
src/pages/logs.tsx
Normal file
76
src/pages/logs.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
This file is part of TALER
|
||||
(C) 2016 Inria
|
||||
|
||||
TALER is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Show wallet logs.
|
||||
*
|
||||
* @author Florian Dold
|
||||
*/
|
||||
|
||||
import {LogEntry, getLogs} from "src/logging";
|
||||
|
||||
interface LogViewProps {
|
||||
log: LogEntry;
|
||||
}
|
||||
|
||||
class LogView extends React.Component<LogViewProps, void> {
|
||||
render(): JSX.Element {
|
||||
let e = this.props.log;
|
||||
return (
|
||||
<div className="tree-item">
|
||||
<ul>
|
||||
<li>id: {e.id || "unknown"}</li>
|
||||
<li>msg: {e.msg}</li>
|
||||
<li>file: {e.source || "(unknown)"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface LogsState {
|
||||
logs: LogEntry[]|undefined;
|
||||
}
|
||||
|
||||
class Logs extends React.Component<any, LogsState> {
|
||||
constructor() {
|
||||
super();
|
||||
this.update();
|
||||
this.state = {} as any;
|
||||
}
|
||||
|
||||
async update() {
|
||||
let logs = await getLogs();
|
||||
this.setState({logs});
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
let logs = this.state.logs;
|
||||
if (!logs) {
|
||||
return <span>...</span>;
|
||||
}
|
||||
return (
|
||||
<div className="tree-item">
|
||||
Logs:
|
||||
{logs.map(e => <LogView log={e} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function main() {
|
||||
ReactDOM.render(<Logs />, document.getElementById("container")!);
|
||||
}
|
@ -492,6 +492,9 @@ function WalletDebug(props: any) {
|
||||
<button onClick={openExtensionPage("/src/pages/tree.html")}>
|
||||
show tree
|
||||
</button>
|
||||
<button onClick={openExtensionPage("/src/pages/logs.html")}>
|
||||
show logs
|
||||
</button>
|
||||
<br />
|
||||
<button onClick={confirmReset}>
|
||||
reset
|
||||
|
41
src/query.ts
41
src/query.ts
@ -38,9 +38,9 @@ export interface JoinLeftResult<L,R> {
|
||||
export class Store<T> {
|
||||
name: string;
|
||||
validator?: (v: T) => T;
|
||||
storeParams: IDBObjectStoreParameters;
|
||||
storeParams?: IDBObjectStoreParameters;
|
||||
|
||||
constructor(name: string, storeParams: IDBObjectStoreParameters,
|
||||
constructor(name: string, storeParams?: IDBObjectStoreParameters,
|
||||
validator?: (v: T) => T) {
|
||||
this.name = name;
|
||||
this.validator = validator;
|
||||
@ -450,6 +450,43 @@ export class QueryRoot implements PromiseLike<void> {
|
||||
return new IterQueryStream(this, store.name, {});
|
||||
}
|
||||
|
||||
count<T>(store: Store<T>): Promise<number> {
|
||||
const {resolve, promise} = openPromise();
|
||||
|
||||
const doCount = (tx: IDBTransaction) => {
|
||||
const s = tx.objectStore(store.name);
|
||||
const req = s.count();
|
||||
req.onsuccess = () => {
|
||||
resolve(req.result);
|
||||
};
|
||||
}
|
||||
|
||||
this.addWork(doCount, store.name, false);
|
||||
return Promise.resolve()
|
||||
.then(() => this.finish())
|
||||
.then(() => promise);
|
||||
|
||||
}
|
||||
|
||||
deleteIf<T>(store: Store<T>, predicate: (x: T, n: number) => boolean): QueryRoot {
|
||||
const doDeleteIf = (tx: IDBTransaction) => {
|
||||
const s = tx.objectStore(store.name);
|
||||
const req = s.openCursor();
|
||||
let n = 0;
|
||||
req.onsuccess = () => {
|
||||
let cursor: IDBCursorWithValue = req.result;
|
||||
if (cursor) {
|
||||
if (predicate(cursor.value, n++)) {
|
||||
cursor.delete();
|
||||
}
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
};
|
||||
this.addWork(doDeleteIf, store.name, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
iterIndex<S extends IDBValidKey,T>(index: Index<S,T>,
|
||||
only?: S): QueryStream<T> {
|
||||
this.stores.add(index.storeName);
|
||||
|
@ -30,6 +30,7 @@ import { Notifier } from "./types";
|
||||
import { Contract } from "./types";
|
||||
import MessageSender = chrome.runtime.MessageSender;
|
||||
import { ChromeBadge } from "./chromeBadge";
|
||||
import * as logging from "./logging";
|
||||
|
||||
"use strict";
|
||||
|
||||
@ -413,6 +414,7 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea
|
||||
// Useful for debugging ...
|
||||
export let wallet: Wallet | undefined = undefined;
|
||||
export let badge: ChromeBadge | undefined = undefined;
|
||||
export let log = logging.log;
|
||||
|
||||
// Rate limit cache for executePayment operations, to break redirect loops
|
||||
let rateLimitCache: { [n: number]: number } = {};
|
||||
@ -422,6 +424,10 @@ function clearRateLimitCache() {
|
||||
}
|
||||
|
||||
export function wxMain() {
|
||||
window.onerror = (m, source, lineno, colno, error) => {
|
||||
logging.record("error", m + error, source || "(unknown)", lineno || 0, colno || 0);
|
||||
}
|
||||
|
||||
chrome.browserAction.setBadgeText({ text: "" });
|
||||
badge = new ChromeBadge();
|
||||
|
||||
|
@ -16,14 +16,14 @@
|
||||
"src/checkable.ts",
|
||||
"decl/lib.es6.d.ts",
|
||||
"src/chromeBadge.ts",
|
||||
"decl/urijs/URIjs.d.ts",
|
||||
"src/cryptoApi-test.ts",
|
||||
"decl/urijs/URIjs.d.ts",
|
||||
"src/components.ts",
|
||||
"decl/systemjs/systemjs.d.ts",
|
||||
"src/emscriptif-test.ts",
|
||||
"decl/systemjs/systemjs.d.ts",
|
||||
"src/cryptoApi.ts",
|
||||
"decl/react-global.d.ts",
|
||||
"src/helpers-test.ts",
|
||||
"decl/react-global.d.ts",
|
||||
"src/cryptoLib.ts",
|
||||
"src/types-test.ts",
|
||||
"decl/chrome/chrome.d.ts",
|
||||
@ -32,6 +32,7 @@
|
||||
"src/emscriptif.ts",
|
||||
"src/helpers.ts",
|
||||
"src/http.ts",
|
||||
"src/logging.ts",
|
||||
"src/query.ts",
|
||||
"src/taler-wallet-lib.ts",
|
||||
"src/types.ts",
|
||||
@ -46,6 +47,7 @@
|
||||
"src/pages/show-db.ts",
|
||||
"src/pages/confirm-contract.tsx",
|
||||
"src/pages/confirm-create-reserve.tsx",
|
||||
"src/pages/logs.tsx",
|
||||
"src/pages/tree.tsx",
|
||||
"src/popup/popup.tsx"
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user