wallet-core/src/logging.ts

310 lines
7.0 KiB
TypeScript
Raw Normal View History

2016-11-18 04:09:04 +01:00
/*
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. Allows to log persistently to a database.
2016-11-18 04:09:04 +01:00
*/
2017-05-28 01:10:54 +02:00
import {
QueryRoot,
Store,
openPromise,
} from "./query";
2016-11-18 04:09:04 +01:00
/**
* Supported log levels.
*/
2016-11-18 04:09:04 +01:00
export type Level = "error" | "debug" | "info" | "warn";
// Right now, our debug/info/warn/debug loggers just use the console based
// loggers. This might change in the future.
2016-11-18 04:09:04 +01:00
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");
}
/**
* Log a message using the configurable logger.
*/
2016-11-18 04:09:04 +01:00
export async function log(msg: string, level: Level = "info"): Promise<void> {
2017-05-28 01:10:54 +02:00
const ci = getCallInfo(2);
2016-11-19 16:33:29 +01:00
return record(level, msg, undefined, ci.file, ci.line, ci.column);
2016-11-18 04:09:04 +01:00
}
function getCallInfo(level: number) {
// see https://github.com/v8/v8/wiki/Stack-Trace-API
2017-05-28 01:10:54 +02:00
const stack = Error().stack;
2016-11-18 04:09:04 +01:00
if (!stack) {
return unknownFrame;
}
2017-05-28 01:10:54 +02:00
const lines = stack.split("\n");
2016-11-18 04:09:04 +01:00
return parseStackLine(lines[level + 1]);
}
interface Frame {
column?: number;
2017-05-28 01:10:54 +02:00
file?: string;
2016-11-18 04:09:04 +01:00
line?: number;
2017-05-28 01:10:54 +02:00
method?: string;
2016-11-18 04:09:04 +01:00
}
const unknownFrame: Frame = {
2017-05-28 01:10:54 +02:00
column: 0,
2016-11-18 04:09:04 +01:00
file: "(unknown)",
line: 0,
2017-05-28 01:10:54 +02:00
method: "(unknown)",
2016-11-18 04:09:04 +01:00
};
/**
* Adapted from https://github.com/errwischt/stacktrace-parser.
*/
function parseStackLine(stackLine: string): Frame {
2017-05-28 01:10:54 +02:00
// tslint:disable-next-line:max-line-length
2016-11-18 04:09:04 +01:00
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;
2017-05-28 01:10:54 +02:00
parts = gecko.exec(stackLine);
if (parts) {
const f: Frame = {
column: parts[5] ? +parts[5] : undefined,
2016-11-18 04:09:04 +01:00
file: parts[3],
line: +parts[4],
2017-05-28 01:10:54 +02:00
method: parts[1] || "(unknown)",
2016-11-18 04:09:04 +01:00
};
return f;
2017-05-28 01:10:54 +02:00
}
parts = chrome.exec(stackLine);
if (parts) {
const f: Frame = {
column: parts[4] ? +parts[4] : undefined,
2016-11-18 04:09:04 +01:00
file: parts[2],
line: +parts[3],
2017-05-28 01:10:54 +02:00
method: parts[1] || "(unknown)",
2016-11-18 04:09:04 +01:00
};
return f;
2017-05-28 01:10:54 +02:00
}
parts = node.exec(stackLine);
if (parts) {
const f: Frame = {
column: parts[4] ? +parts[4] : undefined,
2016-11-18 04:09:04 +01:00
file: parts[2],
line: +parts[3],
2017-05-28 01:10:54 +02:00
method: parts[1] || "(unknown)",
2016-11-18 04:09:04 +01:00
};
return f;
}
2017-05-28 01:10:54 +02:00
2016-11-18 04:09:04 +01:00
return unknownFrame;
}
2017-05-28 01:10:54 +02:00
let db: IDBDatabase|undefined;
2016-11-18 04:09:04 +01:00
/**
* A structured log entry as stored in the database.
*/
2016-11-18 04:09:04 +01:00
export interface LogEntry {
/**
* Soure code column where the error occured.
*/
2017-05-28 01:10:54 +02:00
col?: number;
/**
* Additional detail for the log statement.
*/
2017-05-28 01:10:54 +02:00
detail?: string;
/**
* Id of the log entry, used as primary
* key for the database.
*/
2017-05-28 01:10:54 +02:00
id?: number;
/**
* Log level, see [[Level}}.
*/
2016-11-18 04:09:04 +01:00
level: string;
/**
* Line where the log was created from.
*/
2017-05-28 01:10:54 +02:00
line?: number;
/**
* The actual log message.
*/
2016-11-18 04:09:04 +01:00
msg: string;
/**
* The source file where the log enctry
* was created from.
*/
2016-11-19 16:33:29 +01:00
source?: string;
/**
* Time when the log entry was created.
*/
2017-05-28 01:10:54 +02:00
timestamp: number;
2016-11-18 04:09:04 +01:00
}
/**
* Get all logs. Only use for debugging, since this returns all logs ever made
* at once without pagination.
*/
2016-11-18 04:09:04 +01:00
export async function getLogs(): Promise<LogEntry[]> {
if (!db) {
db = await openLoggingDb();
}
return await new QueryRoot(db).iter(logsStore).toArray();
}
2016-11-18 04:39:31 +01:00
/**
* 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.
*/
2016-11-18 04:37:10 +01:00
let barrier: any;
/**
* Record an exeption in the log.
*/
2016-11-19 16:33:29 +01:00
export async function recordException(msg: string, e: any): Promise<void> {
let stack: string|undefined;
let frame: Frame|undefined;
try {
stack = e.stack;
if (stack) {
2017-05-28 01:10:54 +02:00
const lines = stack.split("\n");
2016-11-19 16:33:29 +01:00
frame = parseStackLine(lines[1]);
}
} catch (e) {
// ignore
}
if (!frame) {
frame = unknownFrame;
}
return record("error", e.toString(), stack, frame.file, frame.line, frame.column);
}
/**
* Record a log entry in the database.
*/
2017-05-28 01:10:54 +02:00
export async function record(level: Level,
msg: string,
detail?: string,
source?: string,
line?: number,
col?: number): Promise<void> {
2016-11-18 04:09:04 +01:00
if (typeof indexedDB === "undefined") {
return;
}
2016-11-18 04:37:10 +01:00
let myBarrier: any;
if (barrier) {
2017-05-28 01:10:54 +02:00
const p = barrier.promise;
2016-11-18 04:37:10 +01:00
myBarrier = barrier = openPromise();
await p;
} else {
myBarrier = barrier = openPromise();
2016-11-18 04:09:04 +01:00
}
2016-11-18 04:37:10 +01:00
try {
if (!db) {
db = await openLoggingDb();
}
2016-11-18 04:09:04 +01:00
2017-05-28 01:10:54 +02:00
const count = await new QueryRoot(db).count(logsStore);
2016-11-18 04:09:04 +01:00
2016-11-18 04:37:10 +01:00
if (count > 1000) {
await new QueryRoot(db).deleteIf(logsStore, (e, i) => (i < 200));
}
2016-11-18 04:09:04 +01:00
2017-05-28 01:10:54 +02:00
const entry: LogEntry = {
col,
detail,
2016-11-18 04:37:10 +01:00
level,
2017-05-28 01:10:54 +02:00
line,
2016-11-18 04:37:10 +01:00
msg,
source,
2017-05-28 01:10:54 +02:00
timestamp: new Date().getTime(),
2016-11-18 04:37:10 +01:00
};
await new QueryRoot(db).put(logsStore, entry);
} finally {
await Promise.resolve().then(() => myBarrier.resolve());
}
2016-11-18 04:09:04 +01:00
}
const loggingDbVersion = 1;
const logsStore: Store<LogEntry> = new Store<LogEntry>("logs");
/**
* Get a handle to the IndexedDB used to store
* logs.
*/
2016-11-18 04:09:04 +01:00
export function openLoggingDb(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
2016-11-18 04:09:04 +01:00
const req = indexedDB.open("taler-logging", loggingDbVersion);
req.onerror = (e) => {
reject(e);
};
req.onsuccess = (e) => {
resolve(req.result);
};
req.onupgradeneeded = (e) => {
2017-05-28 01:10:54 +02:00
const resDb = req.result;
if (e.oldVersion !== 0) {
2016-11-18 04:09:04 +01:00
try {
2017-05-28 01:10:54 +02:00
resDb.deleteObjectStore("logs");
2016-11-18 04:09:04 +01:00
} catch (e) {
console.error(e);
}
}
2017-05-28 01:10:54 +02:00
resDb.createObjectStore("logs", {keyPath: "id", autoIncrement: true});
2016-11-18 04:09:04 +01:00
};
});
}
/**
* Log a message at severity info.
*/
2016-11-18 04:09:04 +01:00
export const info = makeInfo();
/**
* Log a message at severity debug.
*/
2016-11-18 04:09:04 +01:00
export const debug = makeDebug();
/**
* Log a message at severity warn.
*/
2016-11-18 04:09:04 +01:00
export const warn = makeWarn();
/**
* Log a message at severity error.
*/
2016-11-18 04:09:04 +01:00
export const error = makeError();