code-server/packages/logger/src/logger.ts
Asher 4a80bcb42c
Make everything use active evals (#30)
* Add trace log level

* Use active eval to implement spdlog

* Split server/client active eval interfaces

Since all properties are *not* valid on both sides

* +200% fire resistance

* Implement exec using active evaluations

* Fully implement child process streams

* Watch impl, move child_process back to explicitly adding events

Automatically forwarding all events might be the right move, but wanna
think/discuss it a bit more because it didn't come out very cleanly.

* Would you like some args with that callback?

* Implement the rest of child_process using active evals

* Rampant memory leaks

Emit "kill" to active evaluations when client disconnects in order to
kill processes. Most likely won't be the final solution.

* Resolve some minor issues with output panel

* Implement node-pty with active evals

* Provide clearTimeout to vm sandbox

* Implement socket with active evals

* Extract some callback logic

Also remove some eval interfaces, need to re-think those.

* Implement net.Server and remainder of net.Socket using active evals

* Implement dispose for active evaluations

* Use trace for express requests

* Handle sending buffers through evaluation events

* Make event logging a bit more clear

* Fix some errors due to us not actually instantiating until connect/listen

* is this a commit message?

* We can just create the evaluator in the ctor

Not sure what I was thinking.

* memory leak for you, memory leak for everyone

* it's a ternary now

* Don't dispose automatically on close or error

The code may or may not be disposable at that point.

* Handle parsing buffers on the client side as well

* Remove unused protobuf

* Remove TypedValue

* Remove unused forkProvider and test

* Improve dispose pattern for active evals

* Socket calls close after error; no need to bind both

* Improve comment

* Comment is no longer wishy washy due to explicit boolean

* Simplify check for sendHandle and options

* Replace _require with __non_webpack_require__

Webpack will then replace this with `require` which we then provide to
the vm sandbox.

* Provide path.parse

* Prevent original-fs from loading

* Start with a pid of -1

vscode immediately checks the PID to see if the debug process launch
correctly, but of course we don't get the pid synchronously.

* Pass arguments to bootstrap-fork

* Fully implement streams

Was causing errors because internally the stream would set this.writing
to true and it would never become false, so subsequent messages would
never send.

* Fix serializing errors and streams emitting errors multiple times

* Was emitting close to data

* Fix missing path for spawned processes

* Move evaluation onDispose call

Now it's accurate and runs when the active evaluation has actually
disposed.

* Fix promisifying fs.exists

* Fix some active eval callback issues

* Patch existsSync in debug adapter
2019-02-19 10:17:03 -06:00

442 lines
10 KiB
TypeScript

/**
* Log level.
*/
export enum Level {
Trace,
Debug,
Info,
Warning,
Error,
}
/**
* A field to log.
*/
export class Field<T> {
public constructor(
public readonly identifier: string,
public readonly value: T,
) { }
/**
* Convert field to JSON.
*/
public toJSON(): object {
return {
identifier: this.identifier,
value: this.value,
};
}
}
/**
* Represents the time something takes.
*/
export class Time {
public constructor(
public readonly expected: number,
public readonly ms: number,
) { }
}
// `undefined` is allowed to make it easy to conditionally display a field.
// For example: `error && field("error", error)`
// tslint:disable-next-line no-any
export type FieldArray = Array<Field<any> | undefined>;
// Functions can be used to remove the need to perform operations when the
// logging level won't output the result anyway.
export type LogCallback = () => [string, ...FieldArray];
/**
* Creates a time field
*/
export const time = (expected: number): Time => {
return new Time(expected, Date.now());
};
export const field = <T>(name: string, value: T): Field<T> => {
return new Field(name, value);
};
/**
* This formats & builds text for logging.
* It should only be used to build one log item at a time since it stores the
* currently built items and appends to that.
*/
export abstract class Formatter {
protected format = "";
protected args = <string[]>[];
/**
* Add a tag.
*/
public abstract tag(name: string, color: string): void;
/**
* Add string or arbitrary variable.
*/
public abstract push(arg: string, color?: string, weight?: string): void;
public abstract push(arg: any): void; // tslint:disable-line no-any
// tslint:disable-next-line no-any
public abstract fields(fields: Array<Field<any>>): void;
/**
* Flush out the built arguments.
*/
public flush(): any[] { // tslint:disable-line no-any
const args = [this.format, ...this.args];
this.format = "";
this.args = [];
return args;
}
/**
* Get the format string for the value type.
*/
protected getType(arg: any): string { // tslint:disable-line no-any
switch (typeof arg) {
case "object":
return "%o";
case "number":
return "%d";
default:
return "%s";
}
}
}
/**
* Browser formatter.
*/
export class BrowserFormatter extends Formatter {
public tag(name: string, color: string): void {
this.format += `%c ${name} `;
this.args.push(
`border: 1px solid #222; background-color: ${color}; padding-top: 1px;`
+ " padding-bottom: 1px; font-size: 12px; font-weight: bold; color: white;"
+ (name.length === 4 ? "padding-left: 3px; padding-right: 4px;" : ""),
);
// A space to separate the tag from the title.
this.push(" ");
}
public push(arg: any, color: string = "inherit", weight: string = "normal"): void { // tslint:disable-line no-any
if (color || weight) {
this.format += "%c";
this.args.push(
(color ? `color: ${color};` : "") +
(weight ? `font-weight: ${weight};` : ""),
);
}
this.format += this.getType(arg);
this.args.push(arg);
}
// tslint:disable-next-line no-any
public fields(fields: Array<Field<any>>): void {
// tslint:disable-next-line no-console
console.groupCollapsed(...this.flush());
fields.forEach((field) => {
this.push(field.identifier, "#3794ff", "bold");
if (typeof field.value !== "undefined" && field.value.constructor && field.value.constructor.name) {
this.push(` (${field.value.constructor.name})`);
}
this.push(": ");
this.push(field.value);
// tslint:disable-next-line no-console
console.log(...this.flush());
});
// tslint:disable-next-line no-console
console.groupEnd();
}
}
/**
* Server (Node) formatter.
*/
export class ServerFormatter extends Formatter {
public tag(name: string, color: string): void {
const [r, g, b] = this.hexToRgb(color);
while (name.length < 5) {
name += " ";
}
this.format += "\u001B[1m";
this.format += `\u001B[38;2;${r};${g};${b}m${name} \u001B[0m`;
}
public push(arg: any, color?: string, weight?: string): void { // tslint:disable-line no-any
if (weight === "bold") {
this.format += "\u001B[1m";
}
if (color) {
const [r, g, b] = this.hexToRgb(color);
this.format += `\u001B[38;2;${r};${g};${b}m`;
}
this.format += this.getType(arg);
if (weight || color) {
this.format += "\u001B[0m";
}
this.args.push(arg);
}
// tslint:disable-next-line no-any
public fields(fields: Array<Field<any>>): void {
// tslint:disable-next-line no-any
const obj: { [key: string]: any} = {};
this.format += "\u001B[38;2;140;140;140m";
fields.forEach((field) => {
obj[field.identifier] = field.value;
});
this.args.push(JSON.stringify(obj));
console.log(...this.flush()); // tslint:disable-line no-console
}
/**
* Convert fully-formed hex to rgb.
*/
private hexToRgb(hex: string): [number, number, number] {
const integer = parseInt(hex.substr(1), 16);
return [
(integer >> 16) & 0xFF,
(integer >> 8) & 0xFF,
integer & 0xFF,
];
}
}
/**
* Class for logging.
*/
export class Logger {
public level = Level.Info;
private readonly nameColor?: string;
private muted: boolean = false;
public constructor(
private _formatter: Formatter,
private readonly name?: string,
private readonly defaultFields?: FieldArray,
) {
if (name) {
this.nameColor = this.hashStringToColor(name);
}
const envLevel = typeof global !== "undefined" && typeof global.process !== "undefined" ? global.process.env.LOG_LEVEL : process.env.LOG_LEVEL;
if (envLevel) {
switch (envLevel) {
case "trace": this.level = Level.Trace; break;
case "debug": this.level = Level.Debug; break;
case "info": this.level = Level.Info; break;
case "warn": this.level = Level.Warning; break;
case "error": this.level = Level.Error; break;
}
}
}
public set formatter(formatter: Formatter) {
this._formatter = formatter;
}
/**
* Supresses all output
*/
public mute(): void {
this.muted = true;
}
/**
* Outputs information.
*/
public info(fn: LogCallback): void;
public info(message: string, ...fields: FieldArray): void;
public info(message: LogCallback | string, ...fields: FieldArray): void {
this.handle({
type: "info",
message,
fields,
tagColor: "#008FBF",
level: Level.Info,
});
}
/**
* Outputs a warning.
*/
public warn(fn: LogCallback): void;
public warn(message: string, ...fields: FieldArray): void;
public warn(message: LogCallback | string, ...fields: FieldArray): void {
this.handle({
type: "warn",
message,
fields,
tagColor: "#FF9D00",
level: Level.Warning,
});
}
/**
* Outputs a trace message.
*/
public trace(fn: LogCallback): void;
public trace(message: string, ...fields: FieldArray): void;
public trace(message: LogCallback | string, ...fields: FieldArray): void {
this.handle({
type: "trace",
message,
fields,
tagColor: "#888888",
level: Level.Trace,
});
}
/**
* Outputs a debug message.
*/
public debug(fn: LogCallback): void;
public debug(message: string, ...fields: FieldArray): void;
public debug(message: LogCallback | string, ...fields: FieldArray): void {
this.handle({
type: "debug",
message,
fields,
tagColor: "#84009E",
level: Level.Debug,
});
}
/**
* Outputs an error.
*/
public error(fn: LogCallback): void;
public error(message: string, ...fields: FieldArray): void;
public error(message: LogCallback | string, ...fields: FieldArray): void {
this.handle({
type: "error",
message,
fields,
tagColor: "#B00000",
level: Level.Error,
});
}
/**
* Returns a sub-logger with a name.
* Each name is deterministically generated a color.
*/
public named(name: string, ...fields: FieldArray): Logger {
const l = new Logger(this._formatter, name, fields);
if (this.muted) {
l.mute();
}
return l;
}
/**
* Outputs a message.
*/
private handle(options: {
type: "trace" | "info" | "warn" | "debug" | "error";
message: string | LogCallback;
fields?: FieldArray;
level: Level;
tagColor: string;
}): void {
if (this.level > options.level || this.muted) {
return;
}
let passedFields = options.fields || [];
if (typeof options.message === "function") {
const values = options.message();
options.message = values.shift() as string;
passedFields = values as FieldArray;
}
const fields = (this.defaultFields
? passedFields.filter((f) => !!f).concat(this.defaultFields)
: passedFields.filter((f) => !!f)) as Array<Field<any>>; // tslint:disable-line no-any
const now = Date.now();
let times: Array<Field<Time>> = [];
const hasFields = fields && fields.length > 0;
if (hasFields) {
times = fields.filter((f) => f.value instanceof Time);
}
this._formatter.tag(options.type.toUpperCase(), options.tagColor);
if (this.name && this.nameColor) {
this._formatter.tag(this.name.toUpperCase(), this.nameColor);
}
this._formatter.push(options.message);
if (times.length > 0) {
times.forEach((time) => {
const diff = now - time.value.ms;
const expPer = diff / time.value.expected;
const min = 125 * (1 - expPer);
const max = 125 + min;
const green = expPer < 1 ? max : min;
const red = expPer >= 1 ? max : min;
this._formatter.push(` ${time.identifier}=`, "#3794ff");
this._formatter.push(`${diff}ms`, this.rgbToHex(red > 0 ? red : 0, green > 0 ? green : 0, 0));
});
}
// tslint:disable no-console
if (hasFields) {
this._formatter.fields(fields);
} else {
console.log(...this._formatter.flush());
}
// tslint:enable no-console
}
/**
* Hashes a string.
*/
private djb2(str: string): number {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
}
return hash;
}
/**
* Convert rgb to hex.
*/
private rgbToHex(r: number, g: number, b: number): string {
const integer = ((Math.round(r) & 0xFF) << 16)
+ ((Math.round(g) & 0xFF) << 8)
+ (Math.round(b) & 0xFF);
const str = integer.toString(16);
return "#" + "000000".substring(str.length) + str;
}
/**
* Generates a deterministic color from a string using hashing.
*/
private hashStringToColor(str: string): string {
const hash = this.djb2(str);
return this.rgbToHex(
(hash & 0xFF0000) >> 16,
(hash & 0x00FF00) >> 8,
hash & 0x0000FF,
);
}
}
export const logger = new Logger(
typeof process === "undefined" || typeof process.stdout === "undefined"
? new BrowserFormatter()
: new ServerFormatter(),
);