Compare commits

...

16 Commits

Author SHA1 Message Date
3de5f368ef Add typescript declarations 2023-11-22 22:27:54 +01:00
e0b51625d8 Make it ESM only 2023-11-22 22:18:40 +01:00
153aca0ccb Add missing await to addAdapter init 2021-05-19 11:41:14 +02:00
7ca0c4fd72 Some performance optimizations 2021-05-18 14:10:59 +02:00
8e183ac1a5 Add adapter based control of log level 2021-05-18 09:10:06 +02:00
feed4626e6 Fix bugf 2021-05-09 22:33:47 +02:00
9d4e521619 Fix error with non existing close on adapter 2021-05-08 22:53:12 +02:00
7d75f65dd3 Bind functions of Logging to this 2021-05-08 22:28:06 +02:00
eeed068ddd Add exports 2021-05-08 22:24:00 +02:00
6daf815ea8 Change name of class 2021-05-08 22:18:44 +02:00
357b98c69a Changing name again 2021-05-08 22:15:56 +02:00
96d7808f35 V3 2021-05-08 21:47:00 +02:00
176d37249d Making filename configurable after creation 2020-09-30 17:28:32 +02:00
bcff79fc90 Making filename output optional 2020-09-30 16:46:16 +02:00
b92caf6468 Add ESModule support 2020-08-26 11:55:57 +02:00
f34800d725 Use private fields 2020-05-18 17:06:18 +02:00
14 changed files with 5598 additions and 1108 deletions

2
.gitignore vendored
View File

@ -2,4 +2,4 @@ node_modules/
logs/
yarn.lock
out/
.history/
esm/

2
.vscode/launch.json vendored
View File

@ -9,7 +9,7 @@
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}\\out\\test.js",
"program": "${workspaceFolder}/out/test.js",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}

View File

@ -1,29 +1,29 @@
# Logging
Simple logging module, that supports terminal coloring and different plugins
# Getting Started
## Getting Started
``` javascript
const Logging = require("@hibas123/logging").Logging;
```javascript
const Logging = require("@hibas123/logging").default;
Logging.log("Hello there");
```
There are different Logging levels, that also apply terminal coloring:
``` javascript
Logging.debug("Debug message")
Logging.log("Log message")
Logging.warning("Warning")
Logging.error(new Error("To less creativity"))
Logging.error("Just an simple message as error")
Logging.errorMessage("Nearly the same as error")
```javascript
Logging.debug("Debug message");
Logging.log("Log message");
Logging.warning("Warning");
Logging.error(new Error("To less creativity"));
Logging.error("Just an simple message as error");
Logging.errorMessage("Nearly the same as error");
```
All Logging types except the simple error take as many arguments as you want. These will be joined with spaces and serialized with the node util.inspect function.
# Setup
## Setup
This logging module doesn't require any setup per default, but sometimes it makes sense to configure a few things.
@ -31,57 +31,66 @@ For example can you disable the console output. This may be helpful, if you inse
Also you can set a name. All messages that are send with this instance are prefixed by this name.
``` javascript
const CustomLogging = new LoggingBase(name | {
name: "custom", // default undefined
console: false // default true
});
```javascript
const CustomLogging = new LoggingBase(
name |
{
name: "custom", // default undefined
console: false, // default true
}
);
```
# Plugins
## Plugins
There is a Plugin API available, that makes is possible to add custom Logging Adapter.
``` javascript
```javascript
const Demo = new LoggingBase("Demo");
Demo.addAdapter(new DemoAdapter({ color: "rainbow" }));
```
The adapters need to provide a very simple Interface:
``` typescript
```typescript
interface Adapter {
init(observable: ObservableInterface<Message>, name?: string): void | Promise<void>;
init(): void | Promise<void>;
flush(sync: true): void;
flush(sync: false): void | Promise<void>;
onMessage(message: Message): void;
flush(sync: true): void;
flush(sync: false): void | Promise<void>;
}
interface Message {
type: LoggingTypes;
name?:string;
text: {
raw: string[],
formatted: string[]
};
date: Date;
file: string;
type: LoggingTypes;
names?: string[];
text: IFormatted;
date: Date;
file: string;
}
export interface IFormatted {
color?: IColors;
bgcolor?: IColors;
bold?: boolean;
italic?: boolean;
blink?: boolean;
underscore?: boolean;
content: string;
}
enum LoggingTypes {
Log,
Warning,
Error,
Debug
Debug,
}
```
The `ObservableInterface` comes from `@hibas123/utils`. It provides a very simple api for subscribing and unsubscribing from the message events.
## License
More Details on Observable [git](https://git.stamm.me/OpenServer/Utils) or [npm](https://www.npmjs.com/package/@hibas123/utils)
# License
MIT
Copyright (c) 2018 Fabian Stamm
@ -90,4 +99,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

54
benchmark.js Normal file
View File

@ -0,0 +1,54 @@
const { LoggingBase } = require("./out/index.js");
let results = {};
function benchmark(name, count, runner) {
console.profile(name);
const start = process.hrtime.bigint()
runner(count);
const diffNS = process.hrtime.bigint() - start;
const diffMS = Number(diffNS / 1000n / 1000n);
console.profileEnd(name)
results[name]= {
count,
time: diffMS,
timePerI: (diffMS / count).toFixed(4)
}
}
benchmark("simple", 10000000, (cnt) => {
const l = new LoggingBase({
console: false
});
for (let i = 0; i < cnt; i++) {
l.log("simple log")
}
})
benchmark("complex", 1000000, (cnt) => {
const l = new LoggingBase({
console: false
});
for (let i = 0; i < cnt; i++) {
l.log("complex log", {
a: 1,
b: {
c:"test"
}
})
}
})
benchmark("very long", 10000000, (cnt) => {
const l = new LoggingBase({
console: false
});
const longText = "complex log".repeat(100);
for (let i = 0; i < cnt; i++) {
l.log(longText)
}
})
console.table(results)

8
deno_build.js Normal file
View File

@ -0,0 +1,8 @@
const pkg = JSON.parse(await Deno.readTextFile("package.json"));
const meta = JSON.parse(await Deno.readTextFile("meta.json"));
meta.version = pkg.version;
await Deno.copyFile("README.md", "esm/README.md");
await Deno.writeTextFile("meta.json", JSON.stringify(meta, undefined, 3));

16
meta.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "logging",
"version": "3.1.2",
"description": "",
"author": "Fabian Stamm <dev@fabianstamm.de>",
"contributors": [],
"hooks": {
"prepublish": "deno_build.js"
},
"root": "esm",
"files": [
"esm/**/*.ts",
"esm/**/*.js",
"esm/README.md"
]
}

5215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,18 @@
{
"name": "@hibas123/logging",
"version": "2.4.4",
"version": "4.0.1",
"description": "",
"main": "out/index.js",
"types": "out/index.d.ts",
"type": "module",
"main": "esm/index.js",
"types": "esm/index.d.ts",
"module": "esm/index.js",
"scripts": {
"prepublish": "tsc",
"prepublishOnly": "npm run build",
"build": "tsc",
"dev": "nodemon -e ts --exec ts-node src/test.ts"
"test": "tsc && node esm/test.js",
"postpublish": "denreg publish",
"bench": "tsc && node benchmark.js",
"prof": "tsc && ndb --prof benchmark.js"
},
"repository": {
"type": "git",
@ -17,17 +22,15 @@
"license": "MIT",
"files": [
"src/",
"out/",
"esm/",
"tsconfig.json",
"readme.md"
],
"devDependencies": {
"concurrently": "^5.1.0",
"nodemon": "^2.0.3",
"ts-node": "^8.8.2",
"typescript": "^3.8.3"
},
"dependencies": {
"@hibas123/utils": "^2.2.4"
"concurrently": "^6.0.2",
"ndb": "^1.1.5",
"nodemon": "^2.0.7",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
}
}
}

View File

@ -1,36 +1,19 @@
import { Observable, ObservableInterface } from "@hibas123/utils";
import { ConsoleAdapter } from "./consolewriter";
import inspect from "./inspect";
import { ConsoleAdapter } from "./consolewriter.js";
import inspect from "./inspect.js";
import {
Adapter,
LoggingTypes,
Message,
FormatConfig,
DefaultFormatConfig,
Format,
FormatTypes,
Colors,
FormattedText,
FormattedLine,
} from "./types";
FormatConfig,
Formatted,
ILoggingInterface,
LoggingTypes,
Message,
} from "./types.js";
const browser = typeof window !== "undefined";
export function removeColors(text: string) {
text = text.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
""
);
// let index = text.indexOf("\x1b");
// while (index >= 0) {
// text = text.substring(0, index) + text.substring(index + 5, text.length);
// index = text.indexOf("\x1b");
// }
return text;
}
export interface LoggingBaseOptions {
export interface ILoggingOptions {
/**
* Name will be prefixed on Console output and added to logfiles, if not specified here
*/
@ -39,258 +22,110 @@ export interface LoggingBaseOptions {
* Prints output to console
*/
console: boolean;
}
const adapterCache = new WeakMap<Adapter, number>();
class AdapterSet {
change = new Observable<{ type: "add" | "remove"; adapter: Adapter }>();
adapters: Set<Adapter> = new Set();
addAdapter(adapter: Adapter) {
if (!this.adapters.has(adapter)) {
this.adapters.add(adapter);
this.change.send({
type: "add",
adapter: adapter,
});
}
}
}
const consoleAdapter = new ConsoleAdapter();
declare var process: { cwd: () => string };
const PROJECT_ROOT = typeof process !== "undefined" ? process.cwd() : undefined;
export class LoggingBase {
private _formatMap: FormatConfig = new DefaultFormatConfig();
public set formatMap(value: FormatConfig) {
this._formatMap = value;
}
private adapterSet: AdapterSet;
private adapter_init: Promise<void>[] = [];
private timerMap = new Map<string, { name: string; start: any }>();
private messageObservable = new Observable<Message>();
protected _name: string;
private _logLevel = LoggingTypes.Debug;
get logLevel() {
return this._logLevel;
}
set logLevel(value: LoggingTypes) {
this._logLevel = value;
}
get name() {
return this._name;
}
constructor(
options?: Partial<LoggingBaseOptions> | string,
adapterSet?: AdapterSet
) {
let opt: Partial<LoggingBaseOptions>;
if (!options) opt = {};
else if (typeof options === "string") {
opt = { name: options };
} else {
opt = options;
}
let config: LoggingBaseOptions = {
name: undefined,
console: true,
...opt,
};
if (config.name) this._name = config.name;
for (let key in this) {
if (typeof this[key] === "function")
this[key] = (<any>this[key]).bind(this);
}
if (adapterSet) {
this.adapterSet = adapterSet;
this.adapterSet.adapters.forEach((a) => this.initAdapter(a));
} else {
this.adapterSet = new AdapterSet();
}
this.adapterSet.change.subscribe((change) => {
if (change.type === "add") {
this.initAdapter(change.adapter);
}
});
if (config.console) {
this.addAdapter(consoleAdapter);
}
//Binding function to this
this.debug = this.debug.bind(this);
this.log = this.log.bind(this);
this.warn = this.warn.bind(this);
this.warning = this.warning.bind(this);
this.error = this.error.bind(this);
this.errorMessage = this.errorMessage.bind(this);
this.flush = this.flush.bind(this);
}
/**
* Can be used to override function from super class
* @param child New child logging instance
* Enables printing of calling file
*/
protected postGetChild(child: LoggingBase) {}
resolve_filename: boolean;
}
/**
* Creates a new logging instance, with the adapters liked together.
* @param name Name/Prefix of the new child. The actual name will resolve as "<parent-name>/<name>"
*/
getChild(name: string) {
let lg = new LoggingBase(
{
console: false,
name: this.name ? this.name + "/" + name : name,
},
this.adapterSet
);
return lg;
}
export interface INativeFunctions {
startTimer(): any;
endTimer(start: any): number;
}
private initAdapter(adapter: Adapter) {
let cached = adapterCache.get(adapter) || 0;
adapterCache.set(adapter, cached + 1);
let prms = Promise.resolve(
adapter.init(this.messageObservable.getPublicApi())
);
this.adapter_init.push(prms);
}
addAdapter(adapter: Adapter) {
this.adapterSet.addAdapter(adapter);
}
flush(sync: true): void;
flush(sync: false): Promise<void>;
flush(sync: boolean): void | Promise<void> {
if (sync) {
this.adapterSet.adapters.forEach((elm) => elm.flush(true));
} else {
let adapters: (void | Promise<void>)[] = [];
this.adapterSet.adapters.forEach((elm) =>
adapters.push(elm.flush(false))
);
return Promise.all(adapters).then(() => {});
}
}
private $closed = false;
public close() {
if (this.$closed) return;
this.$closed = true;
this.adapterSet.adapters.forEach((adapter) => {
let cached = adapterCache.get(adapter);
if (cached) {
cached--;
if (cached <= 0) {
adapterCache.delete(adapter);
adapter.close();
} else adapterCache.set(adapter, cached);
}
adapter.close ? adapter.close() : undefined;
});
this.adapterSet = undefined;
this.messageObservable.close();
}
public waitForSetup() {
return Promise.all(this.adapter_init);
}
debug(...message: any[]) {
if (this._logLevel <= LoggingTypes.Debug)
this.message(LoggingTypes.Debug, message);
}
log(...message: any[]) {
if (this._logLevel <= LoggingTypes.Log)
this.message(LoggingTypes.Log, message);
}
warning(...message: any[]) {
if (this._logLevel <= LoggingTypes.Warning)
this.message(LoggingTypes.Warning, message);
}
warn(...message: any[]) {
if (this._logLevel <= LoggingTypes.Warning)
this.message(LoggingTypes.Warning, message);
}
error(error: Error | string) {
if (this._logLevel > LoggingTypes.Error) return;
if (!error)
error = "Empty ERROR was passed, so no informations available";
if (typeof error === "string") {
let e = new Error("This is a fake error, to get a stack trace");
this.message(LoggingTypes.Error, [error, "\n", e.stack]);
} else {
this.message(
LoggingTypes.Error,
[error.message, "\n", error.stack],
getCallerFromExisting(error)
);
}
}
errorMessage(...message: any[]) {
if (this._logLevel <= LoggingTypes.Error)
this.message(LoggingTypes.Error, message);
}
protected getCurrentTime(): any {
export const DefaultNativeFunctions = {
startTimer: () => {
if (browser && window.performance && window.performance.now) {
return window.performance.now();
} else {
return Date.now();
}
}
/**
* The time difference in milliseconds (fractions allowed!)
* @param start Start time from getCurrentTime
*/
protected getTimeDiff(start: any) {
},
endTimer: (start: any) => {
if (browser && window.performance && window.performance.now) {
return window.performance.now() - start;
} else {
return Date.now() - start;
}
},
} as INativeFunctions;
const consoleAdapter = new ConsoleAdapter();
declare var process: { cwd: () => string };
const PROJECT_ROOT = typeof process !== "undefined" ? process.cwd() : undefined;
const InitialisedAdapters = Symbol("@hibas123/logging:initialisedAdapters");
export abstract class LoggingInterface implements ILoggingInterface {
#names: string[];
#timerMap = new Map<string, { name: string; start: any }>();
get names() {
return [...this.#names];
}
time(id?: string, name = id) {
if (!id) {
id = Math.floor(Math.random() * 899999 + 100000).toString();
}
protected abstract message(
type: LoggingTypes,
names: string[],
message: any[],
caller?: { file: string; line: number; column?: number }
): void;
this.timerMap.set(id, {
name,
start: this.getCurrentTime(),
constructor(names: string[]) {
this.#names = names;
for (const key in this) {
if (typeof this[key] === "function") {
this[key] = (this[key] as never as Function).bind(this);
}
}
}
debug(...message: any[]) {
this.message(LoggingTypes.Debug, this.#names, message);
}
log(...message: any[]) {
this.message(LoggingTypes.Log, this.#names, message);
}
warning(...message: any[]) {
this.message(LoggingTypes.Warn, this.#names, message);
}
warn(...message: any[]) {
this.message(LoggingTypes.Warn, this.#names, message);
}
error(error: Error | string, ...message: any[]) {
if (!error)
error = "Empty ERROR was passed, so no informations available";
if (typeof error === "string") {
let e = new Error("This is a fake error, to get a stack trace");
this.message(LoggingTypes.Error, this.#names, [
error,
...message,
"\n",
e.stack,
"\n",
]);
} else {
this.message(
LoggingTypes.Error,
this.#names,
[error.message, "\n", error.stack, "\n", ...message],
getCallerFromExisting(error)
);
}
}
time(id?: string) {
if (!id) id = Math.floor(Math.random() * 899999 + 100000).toString();
this.#timerMap.set(id, {
name: id,
start: LoggingBase.nativeFunctions.startTimer(),
});
return {
@ -300,169 +135,256 @@ export class LoggingBase {
}
timeEnd(id: string) {
let timer = this.timerMap.get(id);
let timer = this.#timerMap.get(id);
if (timer) {
let diff = this.getTimeDiff(timer.start);
this.message(LoggingTypes.Debug, [
withColor(Colors.GREEN, `[${timer.name}]`),
let diff = LoggingBase.nativeFunctions.endTimer(timer.start);
this.message(LoggingTypes.Debug, this.#names, [
Format.green(`[${timer.name}]`),
`->`,
withColor(Colors.BLUE, diff.toFixed(4)),
Format.blue(diff.toFixed(4)),
"ms",
]);
return diff;
}
return -1;
}
private message(
abstract getChild(name: string): ILoggingInterface;
}
export class LoggingBase extends LoggingInterface {
private static [InitialisedAdapters] = new Map<Adapter, number>();
public static nativeFunctions: INativeFunctions = DefaultNativeFunctions;
static DecoupledLogging = class extends LoggingInterface {
#lg: LoggingBase;
constructor(names: string[], lg: LoggingBase) {
super(names);
this.#lg = lg;
}
message(...params: [any, any, any, any]) {
this.#lg.message(...params);
}
getChild(name: string) {
return new LoggingBase.DecoupledLogging(
[...this.names, name],
this.#lg
);
}
};
#closed = false;
#logLevel = LoggingTypes.Debug;
#resolve_filename: boolean;
#format_map: FormatConfig = new DefaultFormatConfig();
#adapters = new Set<Adapter>();
set logLevel(level: LoggingTypes) {
this.#logLevel = level;
}
get logLevel() {
return this.#logLevel;
}
constructor(options?: Partial<ILoggingOptions>) {
super(options?.name ? [options.name] : []);
options = {
console: true,
resolve_filename: true,
...options,
};
if (options.console) {
this.addAdapter(consoleAdapter);
}
}
async addAdapter(adapter: Adapter) {
const init = adapter.init();
const add = () => {
this.#adapters.add(adapter);
if (LoggingBase[InitialisedAdapters].has(adapter)) {
LoggingBase[InitialisedAdapters].set(
adapter,
LoggingBase[InitialisedAdapters].get(adapter) + 1
);
} else {
LoggingBase[InitialisedAdapters].set(adapter, 1);
}
};
if (!init) {
add();
} else {
await Promise.resolve(init).then(add);
}
}
getChild(name: string): ILoggingInterface {
return new LoggingBase.DecoupledLogging([...this.names, name], this);
}
protected message(
type: LoggingTypes,
names: string[],
message: any[],
caller?: { file: string; line: number; column?: number }
) {
if (this.$closed) return;
if (this.#logLevel > type) return;
if (this.#closed) {
//TODO: Maybe error?
return;
}
let date = new Date().toISOString().replace(/T/, " ").replace(/\..+/, "");
const date = new Date();
const isoStr = date.toISOString();
const date_str = isoStr.substring(0, 10) + " " + isoStr.substring(11, 19);
let file_raw = caller;
if (!file_raw) {
try {
file_raw = getCallerFile();
} catch (err) {
file_raw = {
file: "<unknown>",
line: 0,
column: 0,
};
let file: string | undefined = undefined;
if (this.#resolve_filename) {
let file_raw = caller;
if (!file_raw) {
try {
file_raw = getCallerFile();
} catch (err) {
file_raw = {
file: "<unknown>",
line: 0,
column: 0,
};
}
}
if (
PROJECT_ROOT &&
file_raw.file &&
file_raw.file.startsWith(PROJECT_ROOT)
) {
let newF = file_raw.file.substring(PROJECT_ROOT.length);
if (newF.startsWith("/") || newF.startsWith("\\"))
newF = newF.substring(1);
file_raw.file = newF;
}
file = `${file_raw.file || "<unknown>"}:${file_raw.line}:${
file_raw.column || 0
}`;
}
if (
PROJECT_ROOT &&
file_raw.file &&
file_raw.file.startsWith(PROJECT_ROOT)
) {
let newF = file_raw.file.substring(PROJECT_ROOT.length);
if (newF.startsWith("/") || newF.startsWith("\\"))
newF = newF.substring(1);
file_raw.file = newF;
}
let file = `${file_raw.file || "<unknown>"}:${file_raw.line}:${
file_raw.column || 0
}`;
let type_str = LoggingTypes[type].toUpperCase().padEnd(5, " ");
let type_format: Format[] = [];
let type_format: Formatted;
switch (type) {
case LoggingTypes.Log:
type_format = this._formatMap.log;
type_format = this.#format_map.log;
break;
case LoggingTypes.Error:
type_format = this._formatMap.error;
type_format = this.#format_map.error;
break;
case LoggingTypes.Debug:
type_format = this._formatMap.debug;
type_format = this.#format_map.debug;
break;
case LoggingTypes.Warning:
type_format = this._formatMap.warning;
case LoggingTypes.Warn:
type_format = this.#format_map.warning;
break;
}
const prefix: FormattedText[] = [];
const a = (text: string, formats: Format[] = []) => {
prefix.push({
text,
formats,
});
};
a("[");
a(date, this._formatMap.date);
a("][");
a(type_str, type_format);
if (file_raw.file) {
a("][");
a(file, this._formatMap.file);
}
a("]: ");
let raw: string[] = [];
const formatted: FormattedLine[] = [];
let line: FormattedLine;
const newLine = () => {
if (line && line.length > 0) {
formatted.push(line);
raw.push(line.map((e) => e.text).join(""));
}
line = [...prefix];
};
newLine();
message.forEach((e, i) => {
let formats: Format[] = [];
if (typeof e !== "string") {
if (e && typeof e === "object") {
if (e[colorSymbol]) {
formats.push({
type: FormatTypes.COLOR,
color: e[colorSymbol],
});
e = e.value;
}
const nameFormatted: Formatted<string>[] = [];
if (names.length > 0) {
nameFormatted.push(new Formatted("["));
for (let i = 0; i < names.length; i++) {
nameFormatted.push(new Formatted(names[i], this.#format_map.names));
if (i < names.length - 1) {
nameFormatted.push(this.#format_map.names_delimiter);
}
if (typeof e !== "string")
e = inspect(e, {
colors: true,
showHidden: true,
depth: 3,
}) as string;
}
nameFormatted.push(new Formatted("]"));
}
let linePrefix = [
new Formatted("["),
new Formatted(date_str, this.#format_map.date),
new Formatted("]["),
new Formatted(type_str, type_format),
...(file
? [new Formatted("]["), new Formatted(file, this.#format_map.file)]
: []),
new Formatted("]"),
...nameFormatted,
new Formatted(": "),
];
let formattedMessage: Formatted<string>[] = [...linePrefix];
message.forEach((msg, idx) => {
let format: Formatted;
if (msg instanceof Formatted) {
format = msg;
msg = msg.content;
} else {
format = new Formatted();
}
removeColors(e)
.split("\n")
.map((text, index, { length }) => {
line.push({ text, formats });
if (index < length - 1) {
newLine();
}
});
if (!e.endsWith("\n") && i < message.length - 1) {
line.push({ text: " ", formats: [] });
if (typeof msg !== "string") {
msg = inspect(msg, {
colors: false, //TODO: Maybe change when changing the removeColors to return formatted text?
showHidden: true,
depth: 3,
}) as string;
}
// removeColors(msg) // Remove colors is uncommented for now, since there are no real benefits of having it and it reduces performance
msg.split("\n").forEach((text, index, { length }) => {
if (index != length - 1) {
formattedMessage.push(
new Formatted(text + "\n", format),
...linePrefix
);
} else {
formattedMessage.push(new Formatted(text, format));
}
});
formattedMessage.push(new Formatted(" "));
});
newLine();
let msg: Message = {
date: new Date(),
date,
file,
name: this._name,
text: {
raw,
formatted,
},
names,
text: formattedMessage,
type,
};
this.messageObservable.send(msg);
this.#adapters.forEach((adapter) => {
if (adapter.level <= type) {
adapter.onMessage(msg);
}
});
}
}
const colorSymbol = Symbol("color");
async close() {
if (this.#closed) return;
this.#closed = true;
export interface ColorFormat {
[colorSymbol]: Colors;
value: any;
}
export function withColor(color: Colors, value: any): ColorFormat {
return {
[colorSymbol]: color,
value,
};
for (const adapter of this.#adapters) {
const cnt = LoggingBase[InitialisedAdapters].get(adapter);
if (!cnt) {
//TODO: should not happen!
} else {
if (cnt <= 1) {
if (adapter.close) await adapter.close();
LoggingBase[InitialisedAdapters].delete(adapter);
} else {
LoggingBase[InitialisedAdapters].set(adapter, cnt - 1);
}
}
}
}
}
function getStack() {
@ -507,9 +429,11 @@ function getCallerFile() {
return { file: undefined, line: 0 };
}
function getCallerFromExisting(
err: Error
): { file: string; line: number; column?: number } {
function getCallerFromExisting(err: Error): {
file: string;
line: number;
column?: number;
} {
if (!err || !err.stack) return { file: "NOFILE", line: 0 };
let lines = err.stack.split("\n");
lines.shift(); // removing first line
@ -537,3 +461,13 @@ function getCallerFromExisting(
}
}
}
export function removeColors(text: string) {
text = text.replace(
// Putting regex here directly instead of externally actually improves performance. The cause of that is not clear for now.
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
""
);
return text;
}

View File

@ -1,24 +1,30 @@
import { ObservableInterface } from "@hibas123/utils";
import { Colors } from "./index";
import { Colors } from "./index.js";
import {
Adapter,
Message,
FormattedLine,
TerminalFormats,
FormatTypes,
} from "./types";
Formatted,
IFormatted,
LoggingTypes,
} from "./types.js";
const browser = typeof window !== "undefined";
declare const Deno: any;
declare const process: any;
const browser = typeof window !== "undefined" && typeof Deno === "undefined";
const deno = typeof Deno !== "undefined";
const NodeJS = typeof process !== undefined;
export class ConsoleAdapter implements Adapter {
level: LoggingTypes = LoggingTypes.Debug;
constructor(private colors: boolean = true) {}
init(observable: ObservableInterface<Message>) {
observable.subscribe(this.onMessage.bind(this));
}
init() {}
flush() {}
setLevel(level: LoggingTypes) {
this.level = level;
}
// TODO: Check if required!
// private escape(text: string): string {
// return text
@ -26,102 +32,161 @@ export class ConsoleAdapter implements Adapter {
// .replace(/%c/g, "%%c")
// }
private formatLine(line: FormattedLine): [string, string[] | undefined] {
private format(
formatted: IFormatted<string>[]
): [string, string[] | undefined] {
let text = "";
let style_formats: string[] = [];
if (!browser) {
for (let part of line) {
// NodeJS or Deno
for (const format of formatted) {
let formats = "";
for (let format of part.formats) {
switch (format.type) {
case FormatTypes.BOLD:
formats += TerminalFormats.Bold;
if (format.bold) {
formats += TerminalFormats.Bold;
}
if (format.blink) {
formats += TerminalFormats.Blink;
}
if (format.underscore) {
formats += TerminalFormats.Underscore;
}
if (format.color) {
switch (format.color) {
case Colors.RED:
formats += TerminalFormats.FgRed;
break;
case FormatTypes.UNDERSCORE:
formats += TerminalFormats.Underscore;
case Colors.GREEN:
formats += TerminalFormats.FgGreen;
break;
case FormatTypes.BLINK:
formats += TerminalFormats.Blink;
case Colors.YELLOW:
formats += TerminalFormats.FgYellow;
break;
case FormatTypes.COLOR:
switch (format.color) {
case Colors.RED:
formats += TerminalFormats.FgRed;
break;
case Colors.GREEN:
formats += TerminalFormats.FgGreen;
break;
case Colors.YELLOW:
formats += TerminalFormats.FgYellow;
break;
case Colors.BLUE:
formats += TerminalFormats.FgBlue;
break;
case Colors.MAGENTA:
formats += TerminalFormats.FgMagenta;
break;
case Colors.CYAN:
formats += TerminalFormats.FgCyan;
break;
case Colors.WHITE:
formats += TerminalFormats.FgWhite;
break;
}
case Colors.BLUE:
formats += TerminalFormats.FgBlue;
break;
case Colors.MAGENTA:
formats += TerminalFormats.FgMagenta;
break;
case Colors.CYAN:
formats += TerminalFormats.FgCyan;
break;
case Colors.WHITE:
formats += TerminalFormats.FgWhite;
break;
}
}
text += formats + part.text + TerminalFormats.Reset;
if (format.bgcolor) {
switch (format.bgcolor) {
case Colors.RED:
formats += TerminalFormats.BgRed;
break;
case Colors.GREEN:
formats += TerminalFormats.BgGreen;
break;
case Colors.YELLOW:
formats += TerminalFormats.BgYellow;
break;
case Colors.BLUE:
formats += TerminalFormats.BgBlue;
break;
case Colors.MAGENTA:
formats += TerminalFormats.BgMagenta;
break;
case Colors.CYAN:
formats += TerminalFormats.BgCyan;
break;
case Colors.WHITE:
formats += TerminalFormats.BgWhite;
break;
}
}
text += formats + format.content + TerminalFormats.Reset;
// (formats.length > 0 ? TerminalFormats.Reset : ""); //TODO: Benchmark if this is better
}
} else {
for (let part of line) {
for (const format of formatted) {
let styles: string[] = [];
let resetStyles: string[] = [];
for (let format of part.formats) {
switch (format.type) {
case FormatTypes.BOLD:
styles.push("font-weight: bold;");
resetStyles.push("font-weight: unset");
if (format.bold) {
styles.push("font-weight: bold;");
resetStyles.push("font-weight: unset");
}
if (format.underscore) {
styles.push("text-decoration: underline");
resetStyles.push("text-decoration: unset");
}
if (format.blink) {
styles.push("text-decoration: blink");
resetStyles.push("text-decoration: unset");
}
if (format.color) {
let color = "";
switch (format.color) {
case Colors.RED:
color = "red";
break;
case FormatTypes.UNDERSCORE:
styles.push("text-decoration: underline");
resetStyles.push("text-decoration: unset");
case Colors.GREEN:
color = "green";
break;
case FormatTypes.BLINK:
styles.push("text-decoration: blink");
resetStyles.push("text-decoration: unset");
case Colors.YELLOW:
color = "gold";
break;
case FormatTypes.COLOR:
let color = "";
switch (format.color) {
case Colors.RED:
color = "red";
break;
case Colors.GREEN:
color = "green";
break;
case Colors.YELLOW:
color = "gold";
break;
case Colors.BLUE:
color = "blue";
break;
case Colors.MAGENTA:
color = "magenta";
break;
case Colors.CYAN:
color = "cyan";
break;
case Colors.WHITE:
color = "white";
break;
}
styles.push("color: " + color);
resetStyles.push("color: unset");
case Colors.BLUE:
color = "blue";
break;
case Colors.MAGENTA:
color = "magenta";
break;
case Colors.CYAN:
color = "cyan";
break;
case Colors.WHITE:
color = "white";
break;
}
styles.push("color: " + color);
resetStyles.push("color: unset");
}
text += "%c" + part.text.replace(/%c/g, "%%c") + "%c";
if (format.bgcolor) {
let color = "";
switch (format.bgcolor) {
case Colors.RED:
color = "red";
break;
case Colors.GREEN:
color = "green";
break;
case Colors.YELLOW:
color = "gold";
break;
case Colors.BLUE:
color = "blue";
break;
case Colors.MAGENTA:
color = "magenta";
break;
case Colors.CYAN:
color = "cyan";
break;
case Colors.WHITE:
color = "white";
break;
}
styles.push("background-color: " + color);
resetStyles.push("background-color: unset");
}
text += "%c" + format.content.replace(/%c/g, "%%c") + "%c";
style_formats.push(styles.join(";"), resetStyles.join(";"));
}
}
@ -130,34 +195,26 @@ export class ConsoleAdapter implements Adapter {
}
onMessage(message: Message) {
let lines = message.text.formatted;
let prefix = "";
if (message.name) prefix = `[${message.name}]=>`;
if (browser) {
if (this.colors) {
let formats: string[] = [];
let text = lines
.map((line) => {
let [t, fmts] = this.formatLine(line);
formats.push(...fmts);
return prefix + t;
})
.join("\n");
// console.log(formats);
const [text, formats] = this.format(message.text);
console.log(text, ...formats);
} else {
console.log(message.text.raw.join("\n"));
console.log(Formatted.strip(message.text));
}
} else {
if (this.colors) {
lines.forEach((line) => {
let [text] = this.formatLine(line);
console.log(prefix + text);
});
const text = this.colors
? this.format(message.text)[0]
: Formatted.strip(message.text);
if (deno) {
//TODO: Deno specific thing
console.log(text);
} else if (typeof process !== "undefined") {
//NodeJS
process.stdout.write(text + "\n");
} else {
message.text.raw.forEach((line) => console.log(prefix + line));
console.log(text);
}
}
}

View File

@ -1,27 +1,23 @@
import { LoggingBase } from "./base";
export { ConsoleAdapter } from "./consolewriter";
export {
LoggingBase,
LoggingBaseOptions,
removeColors,
withColor,
} from "./base";
import { LoggingBase } from "./base.js";
export { LoggingBase } from "./base.js";
export { ConsoleAdapter } from "./consolewriter.js";
export { ILoggingOptions, INativeFunctions } from "./base.js";
export {
Adapter,
LoggingTypes,
Message,
FormatConfig,
FormattedLine,
DefaultFormatConfig as DefaultColorMap,
FormattedText,
Colors,
IColors as Colors,
Format,
FormatTypes,
TerminalFormats,
} from "./types";
Formatted,
IFormatted,
ILoggingInterface,
ILoggingTimer,
} from "./types.js";
export { ObservableInterface } from "@hibas123/utils";
const Logging = new LoggingBase();
export let Logging: LoggingBase = undefined;
Logging = new LoggingBase();
export default Logging;

View File

@ -1,20 +1,19 @@
import { Logging, LoggingBase, LoggingTypes, Colors, withColor } from ".";
import Logging from "./index.js";
import { LoggingBase, LoggingTypes, Format } from "./index.js";
Logging.log("test");
Logging.log("i", "am", { a: "an" }, 1000);
Logging.error(new Error("fehler 001"));
Logging.debug("Some Debug infos");
Logging.errorMessage("i", "am", "an", "error");
Logging.error("i", "am", "an", "error");
Logging.log(
"\x1b[31m\x1b[31m\x1b[31m\x1b[31m\x1b[31m\x1b[31m TEST \x1b[31m\x1b[31m\x1b[31m"
);
Logging.log(
withColor(Colors.MAGENTA, "This text should be magenta!"),
"This not!"
);
Logging.log(withColor(Colors.MAGENTA, { somekey: "Some value" }));
Logging.log(Format.magenta("This text should be magenta!"), "This not!");
Logging.log(Format.magenta({ somekey: "Some value" }));
let err = new Error();
if (typeof err.stack !== "string") console.log("Stacktrace invalid", err.stack);
@ -23,10 +22,10 @@ let cus = new LoggingBase({ name: "test" });
cus.log("Hello from custom Logger");
cus.log("This has some %c symbols inside of it!");
let cus2 = new LoggingBase("test2");
let cus2 = new LoggingBase({ name: "test2" });
cus2.log("Hello from custom Logger 2");
let cus22 = new LoggingBase("test2");
let cus22 = new LoggingBase({ name: "test2" });
cus22.log("Hello from custom Logger 22");
cus2.log("Hello from custom Logger 2");
cus22.log("Hello from custom Logger 22");
@ -47,7 +46,9 @@ Logging.warn("This should not be there 3");
Logging.warning("This should not be there 4");
Logging.error("This should be there 1");
Logging.errorMessage("This should be there 2");
Logging.error("This should be there 2");
Logging.logLevel = LoggingTypes.Debug;
const c1 = Logging.getChild("child-level-1");
@ -57,23 +58,15 @@ const c2 = c1.getChild("child-level-2");
c2.log("Hello from Child 2");
c2.addAdapter({
init: (obs) =>
obs.subscribe((msg) =>
console.log(
"Adapter adden on child level 2: ",
"---",
msg.name,
msg.text.raw
)
),
flush: () => undefined,
close: () => undefined,
});
c2.log("MSG from C2");
c1.log("MSG from C1");
Logging.log("MSG from root");
const timer = Logging.time("timer1", "Test Timer");
const timer = Logging.time("timer1");
setTimeout(() => timer.end(), 1000);
const withoutFile = new LoggingBase({
resolve_filename: false,
});
withoutFile.log("This should not have a file attached");

View File

@ -1,9 +1,7 @@
import { ObservableInterface } from "@hibas123/utils";
export enum LoggingTypes {
Debug,
Log,
Warning,
Warn,
Error,
}
@ -34,14 +32,7 @@ export const TerminalFormats = {
BgWhite: "\x1b[47m",
};
export enum FormatTypes {
COLOR,
BOLD,
UNDERSCORE,
BLINK,
}
export enum Colors {
export enum IColors {
NONE,
RED,
GREEN,
@ -52,70 +43,194 @@ export enum Colors {
WHITE,
}
export interface FormatConfig {
error: Format[];
warning: Format[];
log: Format[];
debug: Format[];
export interface IFormatted<T = any> {
color?: IColors;
bgcolor?: IColors;
bold?: boolean;
italic?: boolean;
blink?: boolean;
underscore?: boolean;
date: Format[];
file: Format[];
content: T;
}
function colorFormat(color: Colors) {
return {
type: FormatTypes.COLOR,
color,
};
export class Formatted<T = any> implements IFormatted<T> {
static strip(formatted: IFormatted<string> | IFormatted<string>[]) {
if (Array.isArray(formatted)) {
return formatted.reduce((p, c) => p + c.content, "");
} else {
return formatted.content;
}
}
constructor(content?: T, apply?: Formatted) {
if (content instanceof Formatted) {
this.color = content.color;
this.bgcolor = content.bgcolor;
this.bold = content.bold;
this.italic = content.italic;
this.blink = content.blink;
this.underscore = content.underscore;
this.content = content.content;
} else {
this.content = content;
}
if (apply) {
this.color = apply.color;
this.bgcolor = apply.bgcolor;
this.bold = apply.bold;
this.italic = apply.italic;
this.blink = apply.blink;
this.underscore = apply.underscore;
}
}
color?: IColors;
bgcolor?: IColors;
bold?: boolean;
italic?: boolean;
blink?: boolean;
underscore?: boolean;
content: T;
_color(c: IColors) {
this.color = c;
return this;
}
_bgcolor(c: IColors) {
this.bgcolor = c;
return this;
}
_bold(active = true) {
this.bold = active;
return this;
}
_italic(active = true) {
this.italic = active;
return this;
}
_blink(active = true) {
this.blink = active;
return this;
}
}
const boldFormat = {
type: FormatTypes.BOLD,
const AddColorFormat = (color: IColors) => (content: any) => {
if (content instanceof Formatted) {
content.color = color;
return content;
} else {
const d = new Formatted(content);
d.color = color;
return d;
}
};
const AddBGColorFormat = (color: IColors) => (content: any) => {
if (content instanceof Formatted) {
content.bgcolor = color;
return content;
} else {
const d = new Formatted(content);
d.bgcolor = color;
return d;
}
};
export const Format = {
none: AddColorFormat(IColors.NONE),
red: AddColorFormat(IColors.RED),
green: AddColorFormat(IColors.GREEN),
yellow: AddColorFormat(IColors.YELLOW),
blue: AddColorFormat(IColors.BLUE),
magenta: AddColorFormat(IColors.MAGENTA),
cyan: AddColorFormat(IColors.CYAN),
white: AddColorFormat(IColors.WHITE),
bgnone: AddBGColorFormat(IColors.NONE),
bgred: AddBGColorFormat(IColors.RED),
bggreen: AddBGColorFormat(IColors.GREEN),
bgyellow: AddBGColorFormat(IColors.YELLOW),
bgblue: AddBGColorFormat(IColors.BLUE),
bgmagenta: AddBGColorFormat(IColors.MAGENTA),
bgcyan: AddBGColorFormat(IColors.CYAN),
bgwhite: AddBGColorFormat(IColors.WHITE),
bold: (content: any) => {
if (content instanceof Formatted) {
content.bold = true;
return content;
} else {
const d = new Formatted(content);
d.bold = true;
return d;
}
},
italic: (content: any) => {
if (content instanceof Formatted) {
content.italic = true;
return content;
} else {
const d = new Formatted(content);
d.italic = true;
return d;
}
},
};
export interface FormatConfig {
error: Formatted;
warning: Formatted;
log: Formatted;
debug: Formatted;
date: Formatted;
file: Formatted;
names: Formatted;
names_delimiter: Formatted;
}
export class DefaultFormatConfig implements FormatConfig {
error = [colorFormat(Colors.RED), boldFormat];
warning = [colorFormat(Colors.YELLOW), boldFormat];
log = [colorFormat(Colors.NONE), boldFormat];
debug = [colorFormat(Colors.CYAN), boldFormat];
error = new Formatted()._color(IColors.RED)._bold();
warning = new Formatted()._color(IColors.YELLOW)._bold();
log = new Formatted()._color(IColors.NONE)._bold();
debug = new Formatted()._color(IColors.CYAN)._bold();
date = [colorFormat(Colors.NONE)];
file = [colorFormat(Colors.NONE)];
date = new Formatted()._color(IColors.NONE);
file = new Formatted()._color(IColors.NONE);
names = new Formatted()._bold()._color(IColors.CYAN);
names_delimiter = new Formatted(" -> ")._bold();
}
export interface Format {
type: FormatTypes;
color?: Colors;
}
export interface FormattedText {
text: string;
formats: Format[];
}
export type FormattedLine = FormattedText[];
export interface Message {
type: LoggingTypes;
name?: string;
text: {
raw: string[];
formatted: FormattedLine[];
};
names?: string[];
text: IFormatted<string>[];
date: Date;
file: string;
}
export interface Adapter {
readonly level: LoggingTypes;
/**
* This function initialises the Adapter. It might be called multiple times, when added to multiple instances
* @param observable An observable to subscribe to messages
*/
init(observable: ObservableInterface<Message>): void | Promise<void>;
init(): void | Promise<void>;
flush(sync: true): void;
flush(sync: false): void | Promise<void>;
setLevel(level: LoggingTypes): void;
onMessage(message: Message): void;
/**
* When a close function is available, it will be called when no logging instance references it anymore.
*
@ -123,3 +238,19 @@ export interface Adapter {
*/
close?(): void;
}
export interface ILoggingTimer {
end: () => number;
}
export interface ILoggingInterface {
debug(...message: any[]): void;
log(...message: any[]): void;
warn(...message: any[]): void;
error(...message: any[]): void;
time(name?: string): ILoggingTimer;
timeEnd(id: string): number;
getChild(name: string): ILoggingInterface;
}

View File

@ -1,19 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "node",
"outDir": "esm",
"noImplicitAny": false,
"sourceMap": true,
"outDir": "out",
"declaration": true,
"typeRoots": [
"node_modules/@types"
]
"declaration": true
},
"exclude": [
"node_modules"
],
"include": [
"src"
]
}
"exclude": ["node_modules"],
"include": ["src"]
}