@holz/core 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/holz-core.js +69 -0
- package/dist/holz-core.js.map +1 -0
- package/dist/holz-core.umd.cjs +2 -0
- package/dist/holz-core.umd.cjs.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/logger.test.ts +57 -0
- package/src/backends/test.ts +5 -0
- package/src/index.ts +5 -0
- package/src/logger.ts +55 -0
- package/src/operators/__tests__/combine.test.ts +29 -0
- package/src/operators/__tests__/filter.test.ts +21 -0
- package/src/operators/combine.ts +22 -0
- package/src/operators/filter.ts +28 -0
- package/src/types.ts +62 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
var t = /* @__PURE__ */ ((s) => (s.Error = "error", s.Warn = "warn", s.Info = "info", s.Debug = "debug", s))(t || {});
|
|
2
|
+
class e {
|
|
3
|
+
constructor(r, o) {
|
|
4
|
+
this.processor = r, this.origin = o;
|
|
5
|
+
}
|
|
6
|
+
static create(r) {
|
|
7
|
+
return new e(r, []);
|
|
8
|
+
}
|
|
9
|
+
/** Extend the logger to attach a class or module name to logs. */
|
|
10
|
+
namespace(r) {
|
|
11
|
+
return new e(this.processor, this.origin.concat(r));
|
|
12
|
+
}
|
|
13
|
+
/** Log a frequent and verbose progress update. */
|
|
14
|
+
debug(r, o) {
|
|
15
|
+
this.forwardLog(t.Debug, r, o);
|
|
16
|
+
}
|
|
17
|
+
/** Log a high-level progress update. */
|
|
18
|
+
info(r, o) {
|
|
19
|
+
this.forwardLog(t.Info, r, o);
|
|
20
|
+
}
|
|
21
|
+
/** Log something concerning. */
|
|
22
|
+
warn(r, o) {
|
|
23
|
+
this.forwardLog(t.Warn, r, o);
|
|
24
|
+
}
|
|
25
|
+
/** Log a critical failure. */
|
|
26
|
+
error(r, o) {
|
|
27
|
+
this.forwardLog(t.Error, r, o);
|
|
28
|
+
}
|
|
29
|
+
forwardLog(r, o, c = {}) {
|
|
30
|
+
this.processor.processLog({
|
|
31
|
+
message: o,
|
|
32
|
+
level: r,
|
|
33
|
+
origin: this.origin,
|
|
34
|
+
context: c
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const a = e.create;
|
|
39
|
+
class i {
|
|
40
|
+
constructor(r) {
|
|
41
|
+
this.processors = r;
|
|
42
|
+
}
|
|
43
|
+
processLog(r) {
|
|
44
|
+
this.processors.forEach((o) => {
|
|
45
|
+
o.processLog(r);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function h(s) {
|
|
50
|
+
return new i(s);
|
|
51
|
+
}
|
|
52
|
+
class n {
|
|
53
|
+
constructor(r, o) {
|
|
54
|
+
this.predicate = r, this.processor = o;
|
|
55
|
+
}
|
|
56
|
+
processLog(r) {
|
|
57
|
+
this.predicate(r) && this.processor.processLog(r);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function g(s, r) {
|
|
61
|
+
return new n(s, r);
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
t as LogLevel,
|
|
65
|
+
h as combine,
|
|
66
|
+
a as createLogger,
|
|
67
|
+
g as filter
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=holz-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"holz-core.js","sources":["../src/types.ts","../src/logger.ts","../src/operators/combine.ts","../src/operators/filter.ts"],"sourcesContent":["/**\n * A logger is made up of log processors. These processors take structured\n * logs and do something with them. Examples are logging backends (console,\n * file, uploads) and operators (filtering, transforming, aggregating).\n */\nexport interface LogProcessor {\n /** Do something with a log message. */\n processLog(log: Log): void;\n}\n\n/** A log message where variables are carried as structured data. */\nexport interface Log {\n /** The verbatim log message. Should not contain interpolated data. */\n readonly message: string;\n\n /** Log severity. */\n readonly level: LogLevel;\n\n /**\n * Where the log message originated. Usually starts with a library or app name,\n * followed by something more specific.\n *\n * @example ['logger-library', 'ConsoleBackend']\n */\n readonly origin: ReadonlyArray<string>;\n\n /**\n * Key-value pairs that provide additional context for the log message. If\n * you're tempted to interpolate data into the message, consider using log\n * context instead.\n *\n * These values must be JSON serializable.\n *\n * Because it's easy to accidentally include unsuitable log context (e.g.\n * redux state, PII) nested objects are not allowed. The restriction doesn't\n * make it impossible, but it makes it harder to miss during code review.\n *\n * @example { userId: 123, reason: 'disconnect' }\n */\n readonly context: LogContext;\n}\n\nexport enum LogLevel {\n /** Something critical failed and we can't continue. */\n Error = 'error',\n\n /** Something is concerning, but we can keep going. */\n Warn = 'warn',\n\n /** High-level progress updates. */\n Info = 'info',\n\n /** Extremely verbose progress updates (usually hidden). */\n Debug = 'debug',\n}\n\nexport type LogContext = Record<\n string,\n JsonPrimitive | ReadonlyArray<JsonPrimitive>\n>;\n\ntype JsonPrimitive = string | number | boolean | null | undefined;\n","import type { LogProcessor, LogContext } from './types';\nimport { LogLevel } from './types';\n\nclass Logger {\n static create(backend: LogProcessor): Logger {\n return new Logger(backend, []);\n }\n\n private constructor(\n private processor: LogProcessor,\n private origin: ReadonlyArray<string>\n ) {\n // empty\n }\n\n /** Extend the logger to attach a class or module name to logs. */\n namespace(owner: string): Logger {\n return new Logger(this.processor, this.origin.concat(owner));\n }\n\n /** Log a frequent and verbose progress update. */\n debug(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Debug, message, context);\n }\n\n /** Log a high-level progress update. */\n info(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Info, message, context);\n }\n\n /** Log something concerning. */\n warn(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Warn, message, context);\n }\n\n /** Log a critical failure. */\n error(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Error, message, context);\n }\n\n private forwardLog(\n level: LogLevel,\n message: string,\n context: LogContext = {}\n ) {\n this.processor.processLog({\n message,\n level,\n origin: this.origin,\n context,\n });\n }\n}\n\nexport const createLogger = Logger.create;\n","import type { Log, LogProcessor } from '../types';\n\nclass CombinedLogProcessor implements LogProcessor {\n constructor(private processors: Array<LogProcessor>) {\n // empty\n }\n\n processLog(log: Log) {\n this.processors.forEach((processor) => {\n processor.processLog(log);\n });\n }\n}\n\n/**\n * Combine several log processors into a single log processor. Each one is\n * called in sequence. Useful for sending a single log to multiple log\n * destinations.\n */\nexport default function combine(processors: Array<LogProcessor>): LogProcessor {\n return new CombinedLogProcessor(processors);\n}\n","import type { Log, LogProcessor } from '../types';\n\nclass LogFilter implements LogProcessor {\n constructor(\n private predicate: (log: Log) => boolean,\n private processor: LogProcessor\n ) {}\n\n processLog(log: Log) {\n if (this.predicate(log)) {\n this.processor.processLog(log);\n }\n }\n}\n\n/**\n * Filter logs based on a filter function. If the function returns true, the\n * log is kept and forwarded onto the next processor, otherwise it is\n * discarded.\n */\nexport default function filter(\n /** Returns `true` to forward the log. */\n predicate: (log: Log) => boolean,\n /** Where to send the log if it passes the filter. */\n processor: LogProcessor\n): LogProcessor {\n return new LogFilter(predicate, processor);\n}\n"],"names":["LogLevel","Logger","processor","origin","backend","owner","message","context","level","createLogger","CombinedLogProcessor","processors","log","combine","LogFilter","predicate","filter"],"mappings":"AA0CY,IAAAA,sBAAAA,OAEVA,EAAA,QAAQ,SAGRA,EAAA,OAAO,QAGPA,EAAA,OAAO,QAGPA,EAAA,QAAQ,SAXEA,IAAAA,KAAA,CAAA,CAAA;ACvCZ,MAAMC,EAAO;AAAA,EAKH,YACEC,GACAC,GACR;AAFQ,SAAA,YAAAD,GACA,KAAA,SAAAC;AAAA,EAGV;AAAA,EATA,OAAO,OAAOC,GAA+B;AAC3C,WAAO,IAAIH,EAAOG,GAAS,CAAA,CAAE;AAAA,EAC/B;AAAA;AAAA,EAUA,UAAUC,GAAuB;AACxB,WAAA,IAAIJ,EAAO,KAAK,WAAW,KAAK,OAAO,OAAOI,CAAK,CAAC;AAAA,EAC7D;AAAA;AAAA,EAGA,MAAMC,GAAiBC,GAAsB;AAC3C,SAAK,WAAWP,EAAS,OAAOM,GAASC,CAAO;AAAA,EAClD;AAAA;AAAA,EAGA,KAAKD,GAAiBC,GAAsB;AAC1C,SAAK,WAAWP,EAAS,MAAMM,GAASC,CAAO;AAAA,EACjD;AAAA;AAAA,EAGA,KAAKD,GAAiBC,GAAsB;AAC1C,SAAK,WAAWP,EAAS,MAAMM,GAASC,CAAO;AAAA,EACjD;AAAA;AAAA,EAGA,MAAMD,GAAiBC,GAAsB;AAC3C,SAAK,WAAWP,EAAS,OAAOM,GAASC,CAAO;AAAA,EAClD;AAAA,EAEQ,WACNC,GACAF,GACAC,IAAsB,CAAA,GACtB;AACA,SAAK,UAAU,WAAW;AAAA,MACxB,SAAAD;AAAA,MACA,OAAAE;AAAA,MACA,QAAQ,KAAK;AAAA,MACb,SAAAD;AAAA,IAAA,CACD;AAAA,EACH;AACF;AAEO,MAAME,IAAeR,EAAO;ACpDnC,MAAMS,EAA6C;AAAA,EACjD,YAAoBC,GAAiC;AAAjC,SAAA,aAAAA;AAAA,EAEpB;AAAA,EAEA,WAAWC,GAAU;AACd,SAAA,WAAW,QAAQ,CAACV,MAAc;AACrC,MAAAA,EAAU,WAAWU,CAAG;AAAA,IAAA,CACzB;AAAA,EACH;AACF;AAOA,SAAwBC,EAAQF,GAA+C;AACtE,SAAA,IAAID,EAAqBC,CAAU;AAC5C;ACnBA,MAAMG,EAAkC;AAAA,EACtC,YACUC,GACAb,GACR;AAFQ,SAAA,YAAAa,GACA,KAAA,YAAAb;AAAA,EACP;AAAA,EAEH,WAAWU,GAAU;AACf,IAAA,KAAK,UAAUA,CAAG,KACf,KAAA,UAAU,WAAWA,CAAG;AAAA,EAEjC;AACF;AAOwB,SAAAI,EAEtBD,GAEAb,GACc;AACP,SAAA,IAAIY,EAAUC,GAAWb,CAAS;AAC3C;"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(t,s){typeof exports=="object"&&typeof module<"u"?s(exports):typeof define=="function"&&define.amd?define(["exports"],s):(t=typeof globalThis<"u"?globalThis:t||self,s(t["holz-core"]={}))})(this,function(t){"use strict";var s=(e=>(e.Error="error",e.Warn="warn",e.Info="info",e.Debug="debug",e))(s||{});class i{constructor(r,o){this.processor=r,this.origin=o}static create(r){return new i(r,[])}namespace(r){return new i(this.processor,this.origin.concat(r))}debug(r,o){this.forwardLog(s.Debug,r,o)}info(r,o){this.forwardLog(s.Info,r,o)}warn(r,o){this.forwardLog(s.Warn,r,o)}error(r,o){this.forwardLog(s.Error,r,o)}forwardLog(r,o,d={}){this.processor.processLog({message:o,level:r,origin:this.origin,context:d})}}const n=i.create;class c{constructor(r){this.processors=r}processLog(r){this.processors.forEach(o=>{o.processLog(r)})}}function f(e){return new c(e)}class a{constructor(r,o){this.predicate=r,this.processor=o}processLog(r){this.predicate(r)&&this.processor.processLog(r)}}function u(e,r){return new a(e,r)}t.LogLevel=s,t.combine=f,t.createLogger=n,t.filter=u,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
|
|
2
|
+
//# sourceMappingURL=holz-core.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"holz-core.umd.cjs","sources":["../src/types.ts","../src/logger.ts","../src/operators/combine.ts","../src/operators/filter.ts"],"sourcesContent":["/**\n * A logger is made up of log processors. These processors take structured\n * logs and do something with them. Examples are logging backends (console,\n * file, uploads) and operators (filtering, transforming, aggregating).\n */\nexport interface LogProcessor {\n /** Do something with a log message. */\n processLog(log: Log): void;\n}\n\n/** A log message where variables are carried as structured data. */\nexport interface Log {\n /** The verbatim log message. Should not contain interpolated data. */\n readonly message: string;\n\n /** Log severity. */\n readonly level: LogLevel;\n\n /**\n * Where the log message originated. Usually starts with a library or app name,\n * followed by something more specific.\n *\n * @example ['logger-library', 'ConsoleBackend']\n */\n readonly origin: ReadonlyArray<string>;\n\n /**\n * Key-value pairs that provide additional context for the log message. If\n * you're tempted to interpolate data into the message, consider using log\n * context instead.\n *\n * These values must be JSON serializable.\n *\n * Because it's easy to accidentally include unsuitable log context (e.g.\n * redux state, PII) nested objects are not allowed. The restriction doesn't\n * make it impossible, but it makes it harder to miss during code review.\n *\n * @example { userId: 123, reason: 'disconnect' }\n */\n readonly context: LogContext;\n}\n\nexport enum LogLevel {\n /** Something critical failed and we can't continue. */\n Error = 'error',\n\n /** Something is concerning, but we can keep going. */\n Warn = 'warn',\n\n /** High-level progress updates. */\n Info = 'info',\n\n /** Extremely verbose progress updates (usually hidden). */\n Debug = 'debug',\n}\n\nexport type LogContext = Record<\n string,\n JsonPrimitive | ReadonlyArray<JsonPrimitive>\n>;\n\ntype JsonPrimitive = string | number | boolean | null | undefined;\n","import type { LogProcessor, LogContext } from './types';\nimport { LogLevel } from './types';\n\nclass Logger {\n static create(backend: LogProcessor): Logger {\n return new Logger(backend, []);\n }\n\n private constructor(\n private processor: LogProcessor,\n private origin: ReadonlyArray<string>\n ) {\n // empty\n }\n\n /** Extend the logger to attach a class or module name to logs. */\n namespace(owner: string): Logger {\n return new Logger(this.processor, this.origin.concat(owner));\n }\n\n /** Log a frequent and verbose progress update. */\n debug(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Debug, message, context);\n }\n\n /** Log a high-level progress update. */\n info(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Info, message, context);\n }\n\n /** Log something concerning. */\n warn(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Warn, message, context);\n }\n\n /** Log a critical failure. */\n error(message: string, context?: LogContext) {\n this.forwardLog(LogLevel.Error, message, context);\n }\n\n private forwardLog(\n level: LogLevel,\n message: string,\n context: LogContext = {}\n ) {\n this.processor.processLog({\n message,\n level,\n origin: this.origin,\n context,\n });\n }\n}\n\nexport const createLogger = Logger.create;\n","import type { Log, LogProcessor } from '../types';\n\nclass CombinedLogProcessor implements LogProcessor {\n constructor(private processors: Array<LogProcessor>) {\n // empty\n }\n\n processLog(log: Log) {\n this.processors.forEach((processor) => {\n processor.processLog(log);\n });\n }\n}\n\n/**\n * Combine several log processors into a single log processor. Each one is\n * called in sequence. Useful for sending a single log to multiple log\n * destinations.\n */\nexport default function combine(processors: Array<LogProcessor>): LogProcessor {\n return new CombinedLogProcessor(processors);\n}\n","import type { Log, LogProcessor } from '../types';\n\nclass LogFilter implements LogProcessor {\n constructor(\n private predicate: (log: Log) => boolean,\n private processor: LogProcessor\n ) {}\n\n processLog(log: Log) {\n if (this.predicate(log)) {\n this.processor.processLog(log);\n }\n }\n}\n\n/**\n * Filter logs based on a filter function. If the function returns true, the\n * log is kept and forwarded onto the next processor, otherwise it is\n * discarded.\n */\nexport default function filter(\n /** Returns `true` to forward the log. */\n predicate: (log: Log) => boolean,\n /** Where to send the log if it passes the filter. */\n processor: LogProcessor\n): LogProcessor {\n return new LogFilter(predicate, processor);\n}\n"],"names":["LogLevel","Logger","processor","origin","backend","owner","message","context","level","createLogger","CombinedLogProcessor","processors","log","combine","LogFilter","predicate","filter"],"mappings":"oOA0CY,IAAAA,GAAAA,IAEVA,EAAA,MAAQ,QAGRA,EAAA,KAAO,OAGPA,EAAA,KAAO,OAGPA,EAAA,MAAQ,QAXEA,IAAAA,GAAA,CAAA,CAAA,ECvCZ,MAAMC,CAAO,CAKH,YACEC,EACAC,EACR,CAFQ,KAAA,UAAAD,EACA,KAAA,OAAAC,CAGV,CATA,OAAO,OAAOC,EAA+B,CAC3C,OAAO,IAAIH,EAAOG,EAAS,CAAA,CAAE,CAC/B,CAUA,UAAUC,EAAuB,CACxB,OAAA,IAAIJ,EAAO,KAAK,UAAW,KAAK,OAAO,OAAOI,CAAK,CAAC,CAC7D,CAGA,MAAMC,EAAiBC,EAAsB,CAC3C,KAAK,WAAWP,EAAS,MAAOM,EAASC,CAAO,CAClD,CAGA,KAAKD,EAAiBC,EAAsB,CAC1C,KAAK,WAAWP,EAAS,KAAMM,EAASC,CAAO,CACjD,CAGA,KAAKD,EAAiBC,EAAsB,CAC1C,KAAK,WAAWP,EAAS,KAAMM,EAASC,CAAO,CACjD,CAGA,MAAMD,EAAiBC,EAAsB,CAC3C,KAAK,WAAWP,EAAS,MAAOM,EAASC,CAAO,CAClD,CAEQ,WACNC,EACAF,EACAC,EAAsB,CAAA,EACtB,CACA,KAAK,UAAU,WAAW,CACxB,QAAAD,EACA,MAAAE,EACA,OAAQ,KAAK,OACb,QAAAD,CAAA,CACD,CACH,CACF,CAEa,MAAAE,EAAeR,EAAO,OCpDnC,MAAMS,CAA6C,CACjD,YAAoBC,EAAiC,CAAjC,KAAA,WAAAA,CAEpB,CAEA,WAAWC,EAAU,CACd,KAAA,WAAW,QAASV,GAAc,CACrCA,EAAU,WAAWU,CAAG,CAAA,CACzB,CACH,CACF,CAOA,SAAwBC,EAAQF,EAA+C,CACtE,OAAA,IAAID,EAAqBC,CAAU,CAC5C,CCnBA,MAAMG,CAAkC,CACtC,YACUC,EACAb,EACR,CAFQ,KAAA,UAAAa,EACA,KAAA,UAAAb,CACP,CAEH,WAAWU,EAAU,CACf,KAAK,UAAUA,CAAG,GACf,KAAA,UAAU,WAAWA,CAAG,CAEjC,CACF,CAOwB,SAAAI,EAEtBD,EAEAb,EACc,CACP,OAAA,IAAIY,EAAUC,EAAWb,CAAS,CAC3C"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@holz/core",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A structured and composable logger",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/holz-core.umd.cjs",
|
|
7
|
+
"module": "./dist/holz-core.js",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/PsychoLlama/holz",
|
|
12
|
+
"directory": "packages/holz-core"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"require": "./dist/holz-core.umd.cjs",
|
|
17
|
+
"import": "./dist/holz-core.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"author": "Jesse Gibson",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"keywords": [
|
|
31
|
+
"structured",
|
|
32
|
+
"composable",
|
|
33
|
+
"logger"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"prepare": "vite build --sourcemap",
|
|
37
|
+
"test:unit": "vitest --color --passWithNoTests",
|
|
38
|
+
"test:coverage": "run test:unit --coverage"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@vitest/coverage-c8": "0.28.5",
|
|
42
|
+
"vite": "^4.0.0",
|
|
43
|
+
"vitest": "^0.28.5"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import TestBackend from '../backends/test';
|
|
2
|
+
import { createLogger } from '../logger';
|
|
3
|
+
import { LogLevel } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('Logger', () => {
|
|
6
|
+
it('sends structured logs to the log processor', () => {
|
|
7
|
+
const backend = new TestBackend();
|
|
8
|
+
const logger = createLogger(backend);
|
|
9
|
+
|
|
10
|
+
logger.info('Hello world', { audience: 'testers' });
|
|
11
|
+
|
|
12
|
+
expect(backend.processLog).toHaveBeenCalledOnce();
|
|
13
|
+
expect(backend.processLog).toHaveBeenCalledWith({
|
|
14
|
+
message: 'Hello world',
|
|
15
|
+
level: 'info',
|
|
16
|
+
origin: [],
|
|
17
|
+
context: { audience: 'testers' },
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
[LogLevel.Debug, 'Look, a dead fly', { urgency: 'high' }],
|
|
23
|
+
[LogLevel.Info, 'I am not a window cleaner!', { state: 'panic' }],
|
|
24
|
+
[LogLevel.Warn, 'There are irregularities in the pension fund', {}],
|
|
25
|
+
[LogLevel.Error, 'Leadership has taken a dive', { windows: 'open' }],
|
|
26
|
+
])('correctly processes %s log messages', (level, message, context) => {
|
|
27
|
+
const backend = new TestBackend();
|
|
28
|
+
const logger = createLogger(backend);
|
|
29
|
+
|
|
30
|
+
logger[level](message, context);
|
|
31
|
+
expect(backend.processLog).toHaveBeenCalledWith({
|
|
32
|
+
message,
|
|
33
|
+
level,
|
|
34
|
+
context,
|
|
35
|
+
origin: [],
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('namespace', () => {
|
|
40
|
+
it('extends the origin of the logger', () => {
|
|
41
|
+
const backend = new TestBackend();
|
|
42
|
+
const logger = createLogger(backend)
|
|
43
|
+
.namespace('signaling')
|
|
44
|
+
.namespace('socket');
|
|
45
|
+
|
|
46
|
+
logger.info('opening socket');
|
|
47
|
+
|
|
48
|
+
expect(backend.processLog).toHaveBeenCalledOnce();
|
|
49
|
+
expect(backend.processLog).toHaveBeenCalledWith({
|
|
50
|
+
message: 'opening socket',
|
|
51
|
+
level: 'info',
|
|
52
|
+
origin: ['signaling', 'socket'],
|
|
53
|
+
context: {},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { LogProcessor, LogContext } from './types';
|
|
2
|
+
import { LogLevel } from './types';
|
|
3
|
+
|
|
4
|
+
class Logger {
|
|
5
|
+
static create(backend: LogProcessor): Logger {
|
|
6
|
+
return new Logger(backend, []);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
private constructor(
|
|
10
|
+
private processor: LogProcessor,
|
|
11
|
+
private origin: ReadonlyArray<string>
|
|
12
|
+
) {
|
|
13
|
+
// empty
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Extend the logger to attach a class or module name to logs. */
|
|
17
|
+
namespace(owner: string): Logger {
|
|
18
|
+
return new Logger(this.processor, this.origin.concat(owner));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Log a frequent and verbose progress update. */
|
|
22
|
+
debug(message: string, context?: LogContext) {
|
|
23
|
+
this.forwardLog(LogLevel.Debug, message, context);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Log a high-level progress update. */
|
|
27
|
+
info(message: string, context?: LogContext) {
|
|
28
|
+
this.forwardLog(LogLevel.Info, message, context);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Log something concerning. */
|
|
32
|
+
warn(message: string, context?: LogContext) {
|
|
33
|
+
this.forwardLog(LogLevel.Warn, message, context);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Log a critical failure. */
|
|
37
|
+
error(message: string, context?: LogContext) {
|
|
38
|
+
this.forwardLog(LogLevel.Error, message, context);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private forwardLog(
|
|
42
|
+
level: LogLevel,
|
|
43
|
+
message: string,
|
|
44
|
+
context: LogContext = {}
|
|
45
|
+
) {
|
|
46
|
+
this.processor.processLog({
|
|
47
|
+
message,
|
|
48
|
+
level,
|
|
49
|
+
origin: this.origin,
|
|
50
|
+
context,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const createLogger = Logger.create;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import TestBackend from '../../backends/test';
|
|
2
|
+
import { createLogger } from '../../logger';
|
|
3
|
+
import combine from '../combine';
|
|
4
|
+
|
|
5
|
+
describe('combine operator', () => {
|
|
6
|
+
it('combines several log processors backends into one', () => {
|
|
7
|
+
const b1 = new TestBackend();
|
|
8
|
+
const b2 = new TestBackend();
|
|
9
|
+
const backend = combine([b1, b2]);
|
|
10
|
+
const logger = createLogger(backend);
|
|
11
|
+
|
|
12
|
+
logger.info('tee message');
|
|
13
|
+
|
|
14
|
+
expect(b1.processLog).toHaveBeenCalledWith(
|
|
15
|
+
expect.objectContaining({ message: 'tee message' })
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
expect(b2.processLog).toHaveBeenCalledWith(
|
|
19
|
+
expect.objectContaining({ message: 'tee message' })
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('works even if you provide no logging backend', () => {
|
|
24
|
+
const backend = combine([]);
|
|
25
|
+
const logger = createLogger(backend);
|
|
26
|
+
|
|
27
|
+
expect(() => logger.info('no backend')).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import TestBackend from '../../backends/test';
|
|
2
|
+
import { createLogger } from '../../logger';
|
|
3
|
+
import { LogLevel } from '../../types';
|
|
4
|
+
import filter from '../filter';
|
|
5
|
+
|
|
6
|
+
describe('filter operator', () => {
|
|
7
|
+
it('filters out logs that do not match the predicate', () => {
|
|
8
|
+
const backend = new TestBackend();
|
|
9
|
+
const logger = createLogger(
|
|
10
|
+
filter((log) => log.level !== LogLevel.Debug, backend)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
logger.info('keep me');
|
|
14
|
+
logger.debug('discard');
|
|
15
|
+
|
|
16
|
+
expect(backend.processLog).toHaveBeenCalledOnce();
|
|
17
|
+
expect(backend.processLog).toHaveBeenCalledWith(
|
|
18
|
+
expect.objectContaining({ message: 'keep me' })
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Log, LogProcessor } from '../types';
|
|
2
|
+
|
|
3
|
+
class CombinedLogProcessor implements LogProcessor {
|
|
4
|
+
constructor(private processors: Array<LogProcessor>) {
|
|
5
|
+
// empty
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
processLog(log: Log) {
|
|
9
|
+
this.processors.forEach((processor) => {
|
|
10
|
+
processor.processLog(log);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Combine several log processors into a single log processor. Each one is
|
|
17
|
+
* called in sequence. Useful for sending a single log to multiple log
|
|
18
|
+
* destinations.
|
|
19
|
+
*/
|
|
20
|
+
export default function combine(processors: Array<LogProcessor>): LogProcessor {
|
|
21
|
+
return new CombinedLogProcessor(processors);
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Log, LogProcessor } from '../types';
|
|
2
|
+
|
|
3
|
+
class LogFilter implements LogProcessor {
|
|
4
|
+
constructor(
|
|
5
|
+
private predicate: (log: Log) => boolean,
|
|
6
|
+
private processor: LogProcessor
|
|
7
|
+
) {}
|
|
8
|
+
|
|
9
|
+
processLog(log: Log) {
|
|
10
|
+
if (this.predicate(log)) {
|
|
11
|
+
this.processor.processLog(log);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Filter logs based on a filter function. If the function returns true, the
|
|
18
|
+
* log is kept and forwarded onto the next processor, otherwise it is
|
|
19
|
+
* discarded.
|
|
20
|
+
*/
|
|
21
|
+
export default function filter(
|
|
22
|
+
/** Returns `true` to forward the log. */
|
|
23
|
+
predicate: (log: Log) => boolean,
|
|
24
|
+
/** Where to send the log if it passes the filter. */
|
|
25
|
+
processor: LogProcessor
|
|
26
|
+
): LogProcessor {
|
|
27
|
+
return new LogFilter(predicate, processor);
|
|
28
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A logger is made up of log processors. These processors take structured
|
|
3
|
+
* logs and do something with them. Examples are logging backends (console,
|
|
4
|
+
* file, uploads) and operators (filtering, transforming, aggregating).
|
|
5
|
+
*/
|
|
6
|
+
export interface LogProcessor {
|
|
7
|
+
/** Do something with a log message. */
|
|
8
|
+
processLog(log: Log): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A log message where variables are carried as structured data. */
|
|
12
|
+
export interface Log {
|
|
13
|
+
/** The verbatim log message. Should not contain interpolated data. */
|
|
14
|
+
readonly message: string;
|
|
15
|
+
|
|
16
|
+
/** Log severity. */
|
|
17
|
+
readonly level: LogLevel;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where the log message originated. Usually starts with a library or app name,
|
|
21
|
+
* followed by something more specific.
|
|
22
|
+
*
|
|
23
|
+
* @example ['logger-library', 'ConsoleBackend']
|
|
24
|
+
*/
|
|
25
|
+
readonly origin: ReadonlyArray<string>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Key-value pairs that provide additional context for the log message. If
|
|
29
|
+
* you're tempted to interpolate data into the message, consider using log
|
|
30
|
+
* context instead.
|
|
31
|
+
*
|
|
32
|
+
* These values must be JSON serializable.
|
|
33
|
+
*
|
|
34
|
+
* Because it's easy to accidentally include unsuitable log context (e.g.
|
|
35
|
+
* redux state, PII) nested objects are not allowed. The restriction doesn't
|
|
36
|
+
* make it impossible, but it makes it harder to miss during code review.
|
|
37
|
+
*
|
|
38
|
+
* @example { userId: 123, reason: 'disconnect' }
|
|
39
|
+
*/
|
|
40
|
+
readonly context: LogContext;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export enum LogLevel {
|
|
44
|
+
/** Something critical failed and we can't continue. */
|
|
45
|
+
Error = 'error',
|
|
46
|
+
|
|
47
|
+
/** Something is concerning, but we can keep going. */
|
|
48
|
+
Warn = 'warn',
|
|
49
|
+
|
|
50
|
+
/** High-level progress updates. */
|
|
51
|
+
Info = 'info',
|
|
52
|
+
|
|
53
|
+
/** Extremely verbose progress updates (usually hidden). */
|
|
54
|
+
Debug = 'debug',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type LogContext = Record<
|
|
58
|
+
string,
|
|
59
|
+
JsonPrimitive | ReadonlyArray<JsonPrimitive>
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
type JsonPrimitive = string | number | boolean | null | undefined;
|