persistent logging

This commit is contained in:
Florian Dold 2016-11-18 04:09:04 +01:00
parent 2986afb3d4
commit 356ebd2137
8 changed files with 352 additions and 6 deletions

View File

@ -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
View 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
View 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
View 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")!);
}

View File

@ -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

View File

@ -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);

View File

@ -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();

View File

@ -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"
]