/*
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
*/
/**
* Configurable logging.
*
* @author Florian Dold
*/
import {Store, QueryRoot, openPromise} 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 {
let ci = getCallInfo(2);
return record(level, msg, undefined, 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;
detail?: string;
source?: string;
col?: number;
line?: number;
id?: number;
}
export async function getLogs(): Promise {
if (!db) {
db = await openLoggingDb();
}
return await new QueryRoot(db).iter(logsStore).toArray();
}
/**
* The barrier ensures that only one DB write is scheduled against the log db
* at the same time, so that the DB can stay responsive. This is a bit of a
* design problem with IndexedDB, it doesn't guarantee fairness.
*/
let barrier: any;
export async function recordException(msg: string, e: any): Promise {
let stack: string|undefined;
let frame: Frame|undefined;
try {
stack = e.stack;
if (stack) {
let lines = stack.split("\n");
frame = parseStackLine(lines[1]);
}
} catch (e) {
// ignore
}
if (!frame) {
frame = unknownFrame;
}
return record("error", e.toString(), stack, frame.file, frame.line, frame.column);
}
export async function record(level: Level, msg: string, detail?: string, source?: string, line?: number, col?: number): Promise {
if (typeof indexedDB === "undefined") {
return;
}
let myBarrier: any;
if (barrier) {
let p = barrier.promise;
myBarrier = barrier = openPromise();
await p;
} else {
myBarrier = barrier = openPromise();
}
try {
if (!db) {
db = await openLoggingDb();
}
let count = await new QueryRoot(db).count(logsStore);
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,
detail,
};
await new QueryRoot(db).put(logsStore, entry);
} finally {
await Promise.resolve().then(() => myBarrier.resolve());
}
}
const loggingDbVersion = 1;
const logsStore: Store = new Store("logs");
export function openLoggingDb(): Promise {
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();