/*
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. Allows to log persistently to a database.
*/
import {
QueryRoot,
Store,
} from "./query";
import { openPromise } from "./promiseUtils";
/**
* Supported log levels.
*/
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.
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.
*/
export async function log(msg: string, level: Level = "info"): Promise {
const 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
const stack = Error().stack;
if (!stack) {
return unknownFrame;
}
const lines = stack.split("\n");
return parseStackLine(lines[level + 1]);
}
interface Frame {
column?: number;
file?: string;
line?: number;
method?: string;
}
const unknownFrame: Frame = {
column: 0,
file: "(unknown)",
line: 0,
method: "(unknown)",
};
/**
* Adapted from https://github.com/errwischt/stacktrace-parser.
*/
function parseStackLine(stackLine: string): Frame {
// tslint:disable-next-line:max-line-length
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;
parts = gecko.exec(stackLine);
if (parts) {
const f: Frame = {
column: parts[5] ? +parts[5] : undefined,
file: parts[3],
line: +parts[4],
method: parts[1] || "(unknown)",
};
return f;
}
parts = chrome.exec(stackLine);
if (parts) {
const f: Frame = {
column: parts[4] ? +parts[4] : undefined,
file: parts[2],
line: +parts[3],
method: parts[1] || "(unknown)",
};
return f;
}
parts = node.exec(stackLine);
if (parts) {
const f: Frame = {
column: parts[4] ? +parts[4] : undefined,
file: parts[2],
line: +parts[3],
method: parts[1] || "(unknown)",
};
return f;
}
return unknownFrame;
}
let db: IDBDatabase|undefined;
/**
* A structured log entry as stored in the database.
*/
export interface LogEntry {
/**
* Soure code column where the error occured.
*/
col?: number;
/**
* Additional detail for the log statement.
*/
detail?: string;
/**
* Id of the log entry, used as primary
* key for the database.
*/
id?: number;
/**
* Log level, see [[Level}}.
*/
level: string;
/**
* Line where the log was created from.
*/
line?: number;
/**
* The actual log message.
*/
msg: string;
/**
* The source file where the log enctry
* was created from.
*/
source?: string;
/**
* Time when the log entry was created.
*/
timestamp: number;
}
/**
* Get all logs. Only use for debugging, since this returns all logs ever made
* at once without pagination.
*/
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;
/**
* Record an exeption in the log.
*/
export async function recordException(msg: string, e: any): Promise {
let stack: string|undefined;
let frame: Frame|undefined;
try {
stack = e.stack;
if (stack) {
const 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);
}
/**
* Cache for reports. Also used when something is so broken that we can't even
* access the database.
*/
const reportCache: { [reportId: string]: any } = {};
/**
* Get a UUID that does not use cryptographically secure randomness.
* Formatted as RFC4122 version 4 UUID.
*/
function getInsecureUuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c: string) => {
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* Store a report and return a unique identifier to retrieve it later.
*/
export async function storeReport(report: any): Promise {
const uid = getInsecureUuid();
reportCache[uid] = report;
return uid;
}
/**
* Retrieve a report by its unique identifier.
*/
export async function getReport(reportUid: string): Promise {
return reportCache[reportUid];
}
/**
* Record a log entry in the database.
*/
export async function record(level: Level,
msg: string,
detail?: string,
source?: string,
line?: number,
col?: number): Promise {
if (typeof indexedDB === "undefined") {
console.log("can't access DB for logging in this context");
console.log("log was", { level, msg, detail, source, line, col });
return;
}
let myBarrier: any;
if (barrier) {
const p = barrier.promise;
myBarrier = barrier = openPromise();
await p;
} else {
myBarrier = barrier = openPromise();
}
try {
if (!db) {
db = await openLoggingDb();
}
const count = await new QueryRoot(db).count(logsStore);
if (count > 1000) {
await new QueryRoot(db).deleteIf(logsStore, (e, i) => (i < 200));
}
const entry: LogEntry = {
col,
detail,
level,
line,
msg,
source,
timestamp: new Date().getTime(),
};
await new QueryRoot(db).put(logsStore, entry);
} finally {
await Promise.resolve().then(() => myBarrier.resolve());
}
}
const loggingDbVersion = 2;
const logsStore: Store = new Store("logs");
/**
* Get a handle to the IndexedDB used to 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 resDb = req.result;
if (e.oldVersion !== 0) {
try {
resDb.deleteObjectStore("logs");
} catch (e) {
console.error(e);
}
}
resDb.createObjectStore("logs", { keyPath: "id", autoIncrement: true });
resDb.createObjectStore("reports", { keyPath: "uid", autoIncrement: false });
};
});
}
/**
* Log a message at severity info.
*/
export const info = makeInfo();
/**
* Log a message at severity debug.
*/
export const debug = makeDebug();
/**
* Log a message at severity warn.
*/
export const warn = makeWarn();
/**
* Log a message at severity error.
*/
export const error = makeError();