diff --git a/README.md b/README.md index cbe213e..4f860b2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,33 @@ +# esm-logging +> This README is a stub. Working on it. Currently stabilizing the build + environment after that I'll make it nice around here. -* [Logging Cookbook](doc/logging-cookbook.md) +A quasi-port of the Python standard library logging module to ECMAScript. +# Why? + +First of, because logging is important. It is important for debugging purposes, +leading to faster and more resilient development, for traceability leading to +better security. Most logging libraries I've discovered didn't satisfy me, +introduced weird concepts and all in all just weren't great. Other programming +language ecosystems offer way nicer logging facilities. Take Rust for example, +or... Python! Python has PEP, giving it a very structured approach towards +implementing new features and that's also how its logging facilities came to be +([PEP 282](https://peps.python.org/pep-0282/)). Python's logging facilities are +implemented by the [logging]() module, which is part of the standard library and +has been since 2002. It was originally authored by Vinay Sajip + +# Roadmap + +- do a quasi-port of the logging module with minimal amount of adaption +- add documentation +- add support for asynchronous calls +- implement Open Cybersecurity Framework (OCSF) formatter +- implement (Browser) local storage handler as a replacement for file handler + +# Usage + +For the time being, please check out my [CI +service](https://bitbucket.org/byteb4rb1e/esm-logging/pipelines), for an idea on +how to build this. diff --git a/TODO b/TODO new file mode 100644 index 0000000..c4f96b7 --- /dev/null +++ b/TODO @@ -0,0 +1,75 @@ +# TODO List for esm-logging + +This is a poor-man's issue tracker. I am not primarily a GitHub user so don't +want to commit to their issue tracking feature, but my primary SVC service +provider (Bitbucket) only offers paid integration into their issue tracker +(Jira). I don't have the time (and patience) at the moment to analyze the best +approach, so this file will have to suffice. + +It's a very simple concept: Track any issues (features, bugfixes, hotfixes) in +here, assign a sequential number to it and use that number when branching. + +I will try to develop a format so that I can parse the file later on, should I +decide to migrate to a real issue tracker. It's probably going to be Bugzilla, +but for that my html-theme-ref project needs to stabilize first. + +## Format Specification + +The file uses Markdown conventions for formatting headers and other text block +entitities, but SHOULD NOT be considered a Markdown file. That's why it has no +definitive file extension. + +Each issue entry follows a structured format for easier parsing and future +migration. Issues MUST be **appended** to this file and never moved, to +preserve Git diffing. + +### Issue Format + +``` + +ID: [ISSUE-NUMBER] +Type: [feature/bugfix/hotfix] +Title: [Short title] +Status: [open/in-progress/done] +Priority: [low/medium/high] +Created: [YYYY-MM-DD] +Description: [Detailed explanation] + +--- +``` + +- ISSUE-NUMBERs must be sequential +- truncation of description must be indentended so that every line starts at the + same column +- issues must be started with two LF +- issues must be terminated with two LF, then `---` +- issues may have a free-text field (epilog), which must be started with two LF. + +## Issues + +ID: 1 +Type: feature +Title: string formatting utilities +Status: in-progress +Priority: high +Created: 2025-05-01 +Description: implement utilities for formatting strings. The formatting should + be inspired by Python 3K PEP 3101 in addition to their standard + library utilities starting from ver. 3.7. Optimizations should + focus on V8 support. + +--- + +ID: 2 +Type: feature +Title: describe development workflow in CONTRIBUTING.md +Status: open +Priority: medium +Created: 2025-05-01 +Description: It's a good idea to describe the development workflow, including + branching strategies earlier on, so that if someone is interested + in forking, they can pick up right away. It's not meant for + contributions though. I'm currently not interested in external + contributions. + +--- diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 9a9a7a3..f11a6e3 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -26,6 +26,7 @@ definitions: - build/debug/**/* - build/debug/* script: + - make clean - make build/debug CI=1 - step: &build-release name: Build (Release) @@ -35,6 +36,7 @@ definitions: - build/release/**/* - build/release/* script: + - make clean - make build/release CI=1 - step: &build-doc name: Build (Doc) @@ -44,6 +46,7 @@ definitions: - build/doc/**/* - build/doc/* script: + - make clean - make build/doc CI=1 - step: &dist name: Package @@ -52,6 +55,7 @@ definitions: artifacts: - dist/* script: + - rm -rvf test-reports/ - make dist CI=1 pipelines: default: diff --git a/package-lock.json b/package-lock.json index 11517e4..81eebd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "administratrix/esm-logging", + "name": "@administratrix/esm-logging", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "administratrix/esm-logging", + "name": "@administratrix/esm-logging", "version": "1.0.0", "license": "UNLICENSED", "devDependencies": { @@ -14,7 +14,7 @@ "jest-junit": "^16.0.0", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "typedoc": "^0.27.9", + "typedoc": "^0.28.3", "typescript": "^5.8.3" } }, @@ -543,15 +543,64 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "1.27.2", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", - "integrity": "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.3.0.tgz", + "integrity": "sha512-frvArO0+s5Viq68uSod5SieLPVM2cLpXoQ1e07lURwgADXpL/MOypM7jPz9otks0g2DIe2YedDAeVrDyYJZRxA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^1.27.2", - "@shikijs/types": "^1.27.2", - "@shikijs/vscode-textmate": "^10.0.1" + "@shikijs/engine-oniguruma": "^3.3.0", + "@shikijs/langs": "^3.3.0", + "@shikijs/themes": "^3.3.0", + "@shikijs/types": "^3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/@istanbuljs/load-nyc-config": { @@ -927,24 +976,44 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", - "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.3.0.tgz", + "integrity": "sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1" + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.3.0.tgz", + "integrity": "sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.3.0.tgz", + "integrity": "sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" } }, "node_modules/@shikijs/types": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", - "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.3.0.tgz", + "integrity": "sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.1", + "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, @@ -1384,14 +1453,13 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1576,6 +1644,46 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1745,6 +1853,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -1782,9 +1897,9 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, @@ -1869,6 +1984,13 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -1922,16 +2044,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -1972,12 +2084,22 @@ "node": ">=8" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/fsevents": { "version": "2.3.3", @@ -2048,22 +2170,24 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2156,25 +2280,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2332,6 +2437,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -2351,6 +2472,30 @@ "node": ">=10" } }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -3201,16 +3346,29 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/mkdirp": { @@ -3277,16 +3435,6 @@ "node": ">=8" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -3358,6 +3506,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -3387,16 +3542,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3414,6 +3559,33 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3631,11 +3803,17 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sisteransi": { "version": "1.0.5", @@ -3710,6 +3888,25 @@ } }, "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", @@ -3724,6 +3921,42 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3737,6 +3970,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -3811,6 +4058,30 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -3975,38 +4246,29 @@ } }, "node_modules/typedoc": { - "version": "0.27.9", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.9.tgz", - "integrity": "sha512-/z585740YHURLl9DN2jCWe6OW7zKYm6VoQ93H0sxZ1cwHQEQrUn5BJrEnkWhfzUdyO+BLGjnKUZ9iz9hKloFDw==", + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.3.tgz", + "integrity": "sha512-5svOCTfXvVSh6zbZKSQluZhR8yN2tKpTeHZxlmWpE6N5vc3R8k/jhg9nnD6n5tN9/ObuQTojkONrOxFdUFUG9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^1.24.0", + "@gerrit0/mini-shiki": "^3.2.2", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.6.1" + "yaml": "^2.7.1" }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 18" + "node": ">= 18", + "pnpm": ">= 10" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" } }, - "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/typedoc/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -4141,6 +4403,25 @@ } }, "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", @@ -4158,12 +4439,69 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "ISC" + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -4179,6 +4517,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -4245,6 +4590,28 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 5bcf26d..f63284f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "administratrix/esm-logging", + "name": "@administratrix/esm-logging", "version": "1.0.0", "description": "port of Python standard library logging module", "main": "lib/index.js", @@ -24,7 +24,12 @@ "jest-junit": "^16.0.0", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "typedoc": "^0.27.9", + "typedoc": "^0.28.3", "typescript": "^5.8.3" + }, + "overrides": { + "jest": { + "glob": "^11.0.1" + } } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..594d01d --- /dev/null +++ b/src/config.ts @@ -0,0 +1,202 @@ + +import { MANAGER } from './manager'; +import { ValueError } from './helper/error'; +import { STYLES, Formatter } from './formatter'; +import { StreamHandler, FileHandler, Handler } from './handler'; +import { LogLevel } from './log-level'; + +//--------------------------------------------------------------------------- +// Configuration classes and functions +//--------------------------------------------------------------------------- + +/** + * options for basic configuration of logging module + */ +export interface BasicConfigOptions { + /* + * Specifies that a FileHandler be created, using the specified filename, + * rather than a StreamHandler. + */ + filename?: string; + + /** + * Specifies the mode to open the file, if filename is specified (if + * filemode is unspecified, it defaults to 'a') + */ + filemode?: string; + + /** + * Use the specified format string for the handler. + */ + format?: string; + + /** + * Use the specified date/time format. + * + */ + datefmt?: string; + + /** + * If a format string is specified, use this to specify the type of format + * string (possible values '%', '{', '$', for %-formatting, + * :meth:`str.format` and :class:`string.Template`- defaults to '%'). + * + * TODO: switch to enum + */ + style?: string; + + /** + * Set the root logger level to the specified level. + */ + level?: LogLevel; + + /** + * Use the specified stream to initialize the StreamHandler. Note that this + * argument is incompatible with 'filename' - if both are present, 'stream' + * is ignored. + * + * TODO: + */ + stream?: any; + + /** + * If specified, this should be an iterable of already created handlers, + * which will be added to the root logger. Any handler in the list which + * does not have a formatter assigned will be assigned the formatter created + * in this function. + */ + handlers?: Handler[]; + + /** + * If this keyword is specified as true, any existing handlers attached to + * the root logger are removed and closed, before carrying out the + * configuration as specified by the other arguments. + */ + force?: boolean; + + /** + * If specified together with a filename, this encoding is passed to the + * created FileHandler, causing it to be used when the file is opened. + */ + encoding?: string; + + /** + * If specified together with a filename, this value is + * passed to the created FileHandler, causing it to be used + * when the file is opened in text mode. If not specified, + * the default value is `backslashreplace`. + */ + errors?: string|null; +} + +/** + * Do basic configuration for the logging system. + * + * This function does nothing if the root logger already has handlers + * configured, unless the keyword argument *force* is set to ``True``. + * It is a convenience method intended for use by simple scripts + * to do one-shot configuration of the logging package. + * + * The default behaviour is to create a StreamHandler which writes to + * sys.stderr, set a formatter using the BASIC_FORMAT format string, and + * add the handler to the root logger. + * + * A number of optional keyword arguments may be specified, which can alter + * the default behaviour. + * + * Note that you could specify a stream created using open(filename, mode) + * rather than passing the filename and mode in. However, it should be + * remembered that StreamHandler does not close its stream (since it may be + * using sys.stdout or sys.stderr), whereas FileHandler closes its stream + * when the handler is closed. + * + * TODO: refactor logic, there apparently is some redundancy in the original + * code + */ +export function basicConfig(options: BasicConfigOptions) { + const force = options.force ?? false; + var encoding = options.encoding ?? undefined; + var errors: string|undefined = options.errors ?? 'backslashreplace'; + var handlers = options.handlers ?? []; + const filename = options.filename ?? null; + const stream = options.stream ?? null; + const filemode = options.filemode ?? 'a'; + const dateformat = options.filemode ?? null; + const style = options.filemode ?? '%'; + const level = options.level ?? null; + + if (!Object.keys(STYLES).includes(style)) { + throw new ValueError( + `style must be one of: ${Object.keys(STYLES).join(', ')}` + ); + } + + if (force) { + for (var i = 0; i < MANAGER.root.handlers.length; i += 1) { + let h: Handler = MANAGER.root.handlers[i]; + MANAGER.root.removeHandler(h); + h.close(); + } + } + + if (handlers.length == 0) { + if (handlers === null && stream && filename) { + throw new ValueError( + "'stream' and 'filename' should not be specified together" + ); + } + + else if (stream || filename) { + throw new ValueError( + "'stream' or 'filename' should not be specified together" + + "with 'handlers'" + ); + } + + if (handlers === null) { + var h: Handler; + + if (filename) { + if (filemode.match('b')) { errors = undefined } + else { encoding = 'utf-8' } + + h = new FileHandler({ + filename: filename, + filemode: filemode, + 'encoding': encoding, + errors: errors + }); + } + + else { h = new StreamHandler(stream) } + + handlers = [h]; + } + + for (var i = 0; i < handlers.length; i += 1) { + let h = handlers[i]; + + if (h.formatter === null) { + h.formatter = new Formatter({ + fmt: options.format ?? STYLES[style][1], + datefmt: dateformat, + style: style + }); + } + + MANAGER.root.addHandler(h); + } + + if (level !== null) { MANAGER.root.setLevel(level) } + + if (options) { + // runtime interface guard, please let me stay. 🥺 + // the interface does not allow for additional members, but the + // runtime environment has no concept of interfaces. We can stick to + // the original implementation + const keys = Object.keys(options).join(', '); + + throw new ValueError(`Unrecognised argument(s): ${keys}`); + } + } +} diff --git a/src/filter.ts b/src/filter.ts new file mode 100644 index 0000000..a6c26b2 --- /dev/null +++ b/src/filter.ts @@ -0,0 +1,114 @@ +import { LogRecord } from './log-record'; + +//--------------------------------------------------------------------------- +// Filter classes and functions +//--------------------------------------------------------------------------- + +export type FilterCallable = (record: LogRecord) => boolean; + +/** + * Filter instances are used to perform arbitrary filtering of LogRecords. + * + * Loggers and Handlers can optionally use Filter instances to filter records as + * desired. The base filter class only allows events which are below a certain + * point in the logger hierarchy. For example, a filter initialized with "A.B" + * will allow events logged by loggers "A.B", initialized with the empty string, + * all events are passed. + */ +export class Filter { + public readonly scope: string; + public readonly slen: number; + + /** + * Initialize with the name of the logger which ,together with its children, + * will have its events allowed through the filter. If no name is specified, + * allow every event. + * + * @param name - name of logging scope + */ + constructor(scope: string) { + this.scope = scope ?? ''; + this.slen = this.scope.length; + } + + /** + * Inspect a record, if it should be logged. + * + * Returns true if the record should be logged, or false otherwise. If + * deemed appropriate, the record may be modified in-place. + * + * @param - scope of log record to inspect + * @param - log record to inspect + */ + filter(record: LogRecord): boolean { + if (this.slen == 0 || this.scope == record.scope) { return true } + else if (!record.scope.substring(0, this.slen)) { return false } + return (record.scope[this.slen] == '.') + } +} + +export class Filterer { + filters: Filter[] = []; + + /** + * Add the specified filter to this handler. + * + * @param filter + */ + addFilter(filter: Filter) { + if (!this.filters.includes(filter)) { this.filters.push(filter) } + } + + /** + * Remove the specified filter from this handler. + * + * @param filter + */ + removeFilter(filter: Filter) { + if (this.filters.includes(filter)) { + this.filters.splice(this.filters.indexOf(filter), 1) + } + } + + /** + * Determine if a record is loggable by consulting all the filters. + * + * The default is to allow the record to be logged; any filter can veto this + * by returning a false value. + * If a filter attached to a handler returns a log record instance, then + * that instance is used in place of the original log record in any further + * processing of the event by that handler. + * If a filter returns any other true value, the original log record is used + * in any further processing of the event by that handler. + * + * If none of the filters return false values, this method returns a log + * record. + * + * If any of the filters return a false value, this method returns a false + * value. + * + * @param filter + */ + filter(record: LogRecord): LogRecord|null { + + for (var i = 0; i < this.filters.length; i += 1) { + let result: boolean|LogRecord = false; + + let filter = this.filters[i]; + + if (typeof (filter as Filter).filter == 'function') { + result = (filter as Filter).filter(record) + } + else { + result = (filter as unknown as FilterCallable)(record) + } + + if (!result) { return null } + + if ((result as any) instanceof LogRecord) { record = result as unknown as LogRecord } + } + + return record + } +} + diff --git a/src/formatter.ts b/src/formatter.ts new file mode 100644 index 0000000..7e0197c --- /dev/null +++ b/src/formatter.ts @@ -0,0 +1,202 @@ +import { MyError, ValueError } from './helper/error'; +import { LogRecord } from './log-record'; + +//--------------------------------------------------------------------------- +// Formatter classes and functions +//--------------------------------------------------------------------------- + +export interface PercentFormatterStyleOptions { + fmt?: string, + defaults: {[key: string]: any}; +} + +class PercentFormatterStyle { + public static defaultFormat = '%(message)s'; + public static asctimeFormat = '%(asctime)s'; + public static asctimeSearch = '%(asctime)'; + public static validationPattern = + /%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]/; + + private fmt: string; + private defaults: {[key: string]: any}; + + constructor(options: PercentFormatterStyleOptions) { + this.fmt = options.fmt ?? PercentFormatterStyle.defaultFormat; + this.defaults = options.defaults; + } + + usesTime(): boolean { + return this.fmt.match(PercentFormatterStyle.asctimeFormat) ? true : false + } + + /** + * Validate the input format, ensure it matches the correct style + */ + validate() { + if (!PercentFormatterStyle.validationPattern.test(this.fmt)) { + throw new ValueError( + `Invalid format '${this.fmt}' for ` + + `'${PercentFormatterStyle.defaultFormat[0]}'` + ) + } + } + + protected _format(record: LogRecord): string { + var defaults = this.defaults; + var values: {[key: string]: any}|null; + if (defaults) { values = {...this.defaults, ...Object.entries(record)} } + else { values = Object.entries(record) } + //TODO: implement formatting + return 'would do some formatting'; + } + + format(record: LogRecord): string { + try { + return this._format(record) + } + catch (e) { + throw new ValueError(`formatting field not found in record: ${e}`) + } + } +} + +const BASIC_FORMAT = '%(level)s:%(name)s:%(message)s'; + +export const STYLES: {[key: string]: [{ new(options: PercentFormatterStyleOptions): PercentFormatterStyle}, string]} = { + '%': [PercentFormatterStyle, BASIC_FORMAT], +} + +export interface FormatterOptions { + fmt?: string + datefmt?: any + style?: string + validate?: boolean + defaults?: {[key: string]: any} +} + +/** + * Formatter instances are used to convert a LogRecord to text. + * + * Formatters need to know how a LogRecord is constructed. They are + * responsible for converting a LogRecord to (usually) a string which can + * be interpreted by either a human or an external system. The base Formatter + * allows a formatting string to be specified. If none is supplied, the + * style-dependent default value, "%(message)s", "{message}", or + * "${message}", is used. + * + * The Formatter can be initialized with a format string which makes use of + * knowledge of the LogRecord attributes - e.g. the default value mentioned + * above makes use of the fact that the user's message and arguments are pre- + * formatted into a LogRecord's message attribute. Currently, the useful + * attributes in a LogRecord are described by: + * + * %(name)s Name of the logger (logging channel) + * %(levelno)s Numeric logging level for the message (DEBUG, INFO, + * WARNING, ERROR, CRITICAL) + * %(levelname)s Text logging level for the message ("DEBUG", "INFO", + * "WARNING", "ERROR", "CRITICAL") + * %(pathname)s Full pathname of the source file where the logging + * call was issued (if available) + * %(filename)s Filename portion of pathname + * %(module)s Module (name portion of filename) + * %(lineno)d Source line number where the logging call was issued + * (if available) + * %(funcName)s Function name + * %(created)f Time when the LogRecord was created (time.time_ns() / 1e9 + * return value) + * %(asctime)s Textual time when the LogRecord was created + * %(msecs)d Millisecond portion of the creation time + * %(relativeCreated)d Time in milliseconds when the LogRecord was created, + * relative to the time the logging module was loaded + * (typically at application startup time) + * %(thread)d Thread ID (if available) + * %(threadName)s Thread name (if available) + * %(taskName)s Task name (if available) + * %(process)d Process ID (if available) + * %(message)s The result of record.getMessage(), computed just as + * the record is emitted + */ +export class Formatter { + public static defaultTimeFormat = '%Y-%M'; + public static defaultMsecFormat = '%s,%30d'; + + protected style: any; + protected fmt: string; + protected datefmt: any; + + /** + * Initialize the formatter with specified format strings. + * + * Initialize the formatter either with the specified format string, or a + * default as described above. Allow for specialized date formatting with + * the optional datefmt argument. If datefmt is omitted, you get an + * ISO8601-like (or RFC 3339-like) format. + * + * Use a style parameter of '%', '{' or '$' to specify that you want to + * use one of %-formatting, :meth:`str.format` (``{}``) formatting or + * :class:`string.Template` formatting in your format string. + */ + constructor(options?: FormatterOptions) { + options = options ?? {}; + var style = options.style ?? '%'; + var validate = options.validate ?? true; + + if (!Object.keys(STYLES).includes(style ?? '')) { + throw new ValueError(`style must be one of: ${Object.keys(STYLES).join(', ')}`) + } + + this.style = new STYLES[style][0]({ + fmt: options.fmt, + defaults: options.defaults ?? {} + }); + + if (validate) { this.style.validate() } + + this.fmt = this.style.fmt; + + this.datefmt = options.datefmt; + } + + /** + * Return the creation time of the specified LogRecord as formatted text. + * + * This method should be called from format() by a formatter which + * wants to make use of a formatted time. This method can be overridden + * in formatters to provide for any specific requirement, but the + * basic behaviour is as follows: if datefmt (a string) is specified, + * it is used with time.strftime() to format the creation time of the + * record. Otherwise, an ISO8601-like (or RFC 3339-like) format is used. + * The resulting string is returned. This function uses a user-configurable + * function to convert the creation time to a tuple. By default, + * time.localtime() is used; to change this for a particular formatter + * instance, set the 'converter' attribute to a function with the same + * signature as time.localtime() or time.gmtime(). To change it for all + * formatters, for example if you want all logging times to be shown in GMT, + * set the 'converter' attribute in the Formatter class. + */ + formatTime(record: LogRecord, datefmt?: any): string { + + //TODO: record.created + if (datefmt) { + //TODO: time.strftime + } + else { + //TODO: time.strftime + } + + return 'some time'; + } + + /** + * Format and return the specified exception information as a string. + + * This default implementation just uses + * traceback.print_exception() + */ + formatError(ei: MyError): string { + //TODO + return 'some error'; + } +} + +export const DEFAULT_FORMATTER = new Formatter(); diff --git a/src/handler.ts b/src/handler.ts new file mode 100644 index 0000000..c75db06 --- /dev/null +++ b/src/handler.ts @@ -0,0 +1,199 @@ +import * as stream from 'stream'; + +import { LogLevel, checkLevel, NOTSET } from './log-level'; +import { LogRecord } from './log-record'; +import { Formatter, DEFAULT_FORMATTER } from './formatter'; +import { Filterer } from './filter'; +import { NotImplementedError } from './helper/error'; + +if (typeof window === 'undefined') { + const stream = require('stream'); +} +else { + const stream = require('./helper/stream'); +} + +//--------------------------------------------------------------------------- +// Handler classes and functions +//---------------------------------------------------------------------------- + +type Handlers = {[key: string]: Handler}; + +/** + * map of handler names to handlers + */ +const HANDLERS: Handlers = {}; + +/** + * added to allow handlers to be removed in reverse order of initialization + */ +const HANDLER_LIST: WeakRef[] = []; + +/** + * Add a handler to the internal cleanup list using a weak reference. + * + * @param handler - + */ +function addHandlerRef(handler: Handler) { + HANDLER_LIST.push(new WeakRef(handler)); +} + +/** + * Get a handler with the specified *name*, or None if there isn't one with + * that name. + */ +export function getHandlerByName(name: string): Handler|null { + return HANDLERS[name] ?? null +} + +/** + * Return all known handler names as an immutable set + */ +export function getHandlerNames(): Handlers { return Object.freeze(HANDLERS) } + +/** + * Handler instances dispatch logging events to specific destinations. + * + * The base handler class. Acts as a placeholder which defines the Handler + * interface. Handlers can optionally use Formatter instances to format + * records as desired. By default, no formatter is specified; in this case, + * the 'raw' message as determined by record.message is logged. + */ +export class Handler extends Filterer { + + protected _scope: string|null = null; + protected _formatter: Formatter|null = null; + protected _level: number; + protected _closed: boolean = false; + + /** + * Initializes the instance - basically setting the formatter to None + * and the filter list to empty + */ + constructor(level?: LogLevel) { + super(); + this._level = checkLevel(level ?? NOTSET); + // Add the handler to the global HANDLER_LIST (for cleanup on shutdown) + addHandlerRef(this); + } + + get level(): number { return this._level } + set level(level: LogLevel|string) { this.level = checkLevel(level) } + + get scope(): string|null { return this._scope } + set scope(scope: string) { this._scope = scope } + get closed(): boolean { return this._closed } + + /** + * Format the specified record. + * + * If a formatter is set, use it. Otherwise, use the default formatter for + * the module. + */ + format(record: LogRecord) { + var fmt: Formatter|null = null; + + if (this.formatter) { fmt = this.formatter } + else { fmt = DEFAULT_FORMATTER } + } + + /** + * Do whatever it takes to actually log the specified logging record. + * + * This version is intended to be implemented by subclasses and so raises a + * NotImplementedError. + */ + emit(record: LogRecord) { + throw new NotImplementedError( + 'emit must be implemented by Handler subclass' + ) + } + + /** + * Conditionally emit the specfied logging record. + * + * Emission depends on filters which may have been added to the handler. + * Wrap the actual emission of the record with acquisition/release of the + * I/O thread lock. + */ + handle(record: LogRecord) { + var rv = this.filter(record); + if ((rv as any) instanceof LogRecord) { + record = rv as unknown as LogRecord + } + if (rv) { + //locking here + this.emit(record) + } + } + + /** + * Tidy up any resources used by the handler + * + * This version removes the handler from an internal map of handlers, which + * is used for handler lookup by scope. Subclasses should ensure that this + * gets called from overriden close() methods. + */ + close() { + this._closed = true; + + if (this.scope && Object.keys(HANDLERS).includes(this.scope)) { + delete HANDLERS[this.scope] + } + } + + /** + * Handle errors which occur during an emit() call. + * + * This method should be called from handlers when an exception is + * encountered during an emit() call. If raiseExceptions is false, + * exceptions get silently ignored. This is what is mostly wanted + * for a logging system - most users will not care about errors in + * the logging system, they are more interested in application errors. + * You could, however, replace this with a custom handler if you wish. + * The record which was being processed is passed in to this method. + */ + handleError(record: LogRecord) { + throw new NotImplementedError( + 'still need to find portable way for stacktracing...' + ) + } + + set formatter(fmt: Formatter) { this._formatter = fmt } +} + +export interface FileHandlerOptions { + filename: string + filemode?: string + encoding?: string + errors?: string +} + +/** + * A handler class which writes logging records, appropriately formatted, + to a stream. Note that this class does not close the stream, as + sys.stdout or sys.stderr may be used. + */ +export class StreamHandler extends Handler { + constructor(stream?: stream.Writable) { + super(); + } +} + +export class FileHandler extends StreamHandler { + constructor(options: FileHandlerOptions) { + super(); + } +} + +/** + * This class is like a StreamHandler using sys.stderr, but always uses + * whatever sys.stderr is currently set to rather than the value of + * sys.stderr at handler construction time. + */ + export class StderrHandler extends Handler { + /** + * Initialize the handler. + */ + constructor(level: LogLevel) { super(level) } +} diff --git a/src/index.ts b/src/index.ts index e5f0039..604c678 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,1321 +87,26 @@ * * @module logging */ -import { - NotImplementedError, - MyError, - ValueError, - KeyError, - StackTrace -} from './helper/error'; -import { MillisecondsSinceUnixEpoch } from './helper/datetime'; -import * as stream from 'stream'; -if (typeof window === 'undefined') { - const stream = require('stream'); -} -else { - const stream = require('./helper/stream'); -} +export * as config from './config'; +export * as filter from './filter'; +export * as formatter from './formatter'; +export * as handler from './handler'; +// screw community conventions, whoever came up with the idea of aliasing +// imports in pascal case, or camel case doesn't seem to care about naming +// collisions. I'm sticking to snake case as this avoids naming collisions. +export * as log_level from './log-level'; +export * as log_record from './log-record'; +export * as logger from './logger'; +export * as manager from './manager'; -/*--------------------------------------------------------------------------- - Level related stuff - --------------------------------------------------------------------------- - Default levels and level names, these can be replaced with any positive set - of values having corresponding names. There is a pseudo-level, NOTSET, which - is only really there as a lower limit for user-defined levels. Handlers and - loggers are initialized with NOTSET so that they will log all messages, even - at user-defined levels. -*/ -export type LogLevel = number; -/** - * An indication that something unexpected happened, or that a problem might - * occur in the near future (e.g. ‘disk space low’). The software is still - * working as expected. - */ -export const CRITICAL = 50; -export const FATAL = CRITICAL; -/** - * Due to a more serious problem, the software has not been able to perform some - * function. - */ -export const ERROR = 40; -/** - * An indication that something unexpected happened, or that a problem might - * occur in the near future (e.g. ‘disk space low’). The software is still - * working as expected. - */ -export const WARNING = 30; -export const WARN = WARNING; -/** - * Confirmation that things are working as expected. - */ -export const INFO = 20; -/** - * Detailed information, typically only of interest to a developer trying to - * diagnose a problem. - */ -export const DEBUG = 10; -/** - * When set on a logger, indicates that ancestor loggers are to be consulted to - * determine the effective level. If that still resolves to NOTSET, then all - * events are logged. When set on a handler, all events are handled. - */ -export const NOTSET = 0; -const LEVELTONAME: {[key: number]: string} = { - [CRITICAL]: 'CRITICAL', - [ERROR]: 'ERROR', - [WARNING]: 'WARNING', - [INFO]: 'INFO', - [DEBUG]: 'DEBUG', - [NOTSET]: 'NOTSET' -} -const NAMETOLEVEL: {[key: string]: number} = { - CRITICAL: CRITICAL, - ERROR: ERROR, - WARNING: WARNING, - INFO: INFO, - DEBUG: DEBUG, - NOTSET: NOTSET, -} - -function getLevelNamesMapping() { - return Object.assign({}, NAMETOLEVEL); -} - -/** - * Return the textual or numeric representation of logging level 'level' - * - * @param level - */ -export function getLevelName(level: string|number): string|number { - var result: string|number = LEVELTONAME[level as number]; - if (result !== undefined) { return result } - result = NAMETOLEVEL[level as string]; - if (result !== undefined) { return result } - return `Level ${level}`; -} - -/** - * Associate 'levelName' with 'level' - * - * @param level - * @param levelName - */ -export function addLevelName(level: number, levelName: string) { - LEVELTONAME[level] = levelName; - NAMETOLEVEL[levelName] = level; -} - -function checkLevel(level: number|string): number { - var rv: number; - - if (typeof level == 'number') { rv = level } - - else if (typeof level == 'string') { - if (!Object.keys(NAMETOLEVEL).includes(level as string)) { - throw new Error(`Unknown level: ${level}`) - } - - rv = NAMETOLEVEL[level] - } - - else { - throw new Error(`Level not a number or valid string: ${level}`) - } - - return rv -} - -export type ExecutionInfo = [string, Error, StackTrace]; - -//--------------------------------------------------------------------------- -// The logging record -//--------------------------------------------------------------------------- - -/** - * options for instantiating a new log record - */ -export interface LogRecordOptions { - /** - * The numeric level of the logging event (such as 10 for DEBUG, 20 for - * INFO, etc). Note that this is converted to two attributes of the - * LogRecord: levelno for the numeric value and levelname for the - * corresponding level name. - */ - level: number, - file?: string, - /** - * The line number in the source file where the logging call was made. - */ - lno?: number, - /** - * The event description message, which can be a %-format string with - * placeholders for variable data, or an arbitrary object (see Using - * arbitrary objects as messages). - */ - msg: string, - /** - * Variable data to merge into the msg argument to obtain the event - * description. - */ - args?: any[], -} - -export type LogRecordFactory = { (name: string, options: LogRecordOptions): LogRecord }; - -/** - * LogRecord instances are created every time something is logged. They contain - * all the information pertinent to the event being logged. The main - * information parssed in is msg and args, which are combined using str(msg) % - * args to create the message field of the record. The record also includes - * information such as when the record was created, the source line where the - * logging call was made, and any exception information to be logged. - */ -export class LogRecord { - public readonly levelno: LogLevel; - public readonly levelname: string|LogLevel; - public readonly scope: string; - - public readonly created: MillisecondsSinceUnixEpoch = Date.now(); - - constructor(scope: string, options: LogRecordOptions) { - this.levelno = options.level; - this.levelname = getLevelName(options.level); - this.scope = scope; - } -} - -var logRecordFactory = (scope: string, options: LogRecordOptions) => { - return new LogRecord(scope, options) -}; - -/** - * Define which class use when instantiating log records. - * - * @param factory - A callable which will be called to instantiate a log record. - * Pass a clojure, if your factory is a class already. - */ -export function setLogRecordFactory(factory: LogRecordFactory) { - logRecordFactory = factory -} - -export function getLogRecordFactory(): LogRecordFactory { - return logRecordFactory -} - -//--------------------------------------------------------------------------- -// Formatter classes and functions -//--------------------------------------------------------------------------- - -export interface PercentFormatterStyleOptions { - fmt?: string, - defaults: {[key: string]: any}; -} - -class PercentFormatterStyle { - public static defaultFormat = '%(message)s'; - public static asctimeFormat = '%(asctime)s'; - public static asctimeSearch = '%(asctime)'; - public static validationPattern = - /%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]/; - - private fmt: string; - private defaults: {[key: string]: any}; - - constructor(options: PercentFormatterStyleOptions) { - this.fmt = options.fmt ?? PercentFormatterStyle.defaultFormat; - this.defaults = options.defaults; - } - - usesTime(): boolean { - return this.fmt.match(PercentFormatterStyle.asctimeFormat) ? true : false - } - - /** - * Validate the input format, ensure it matches the correct style - */ - validate() { - if (!PercentFormatterStyle.validationPattern.test(this.fmt)) { - throw new ValueError( - `Invalid format '${this.fmt}' for ` + - `'${PercentFormatterStyle.defaultFormat[0]}'` - ) - } - } - - protected _format(record: LogRecord): string { - var defaults = this.defaults; - var values: {[key: string]: any}|null; - if (defaults) { values = {...this.defaults, ...Object.entries(record)} } - else { values = Object.entries(record) } - //TODO: implement formatting - return 'would do some formatting'; - } - - format(record: LogRecord): string { - try { - return this._format(record) - } - catch (e) { - throw new ValueError(`formatting field not found in record: ${e}`) - } - } -} - -const BASIC_FORMAT = '%(level)s:%(name)s:%(message)s'; - -const STYLES: {[key: string]: [{ new(options: PercentFormatterStyleOptions): PercentFormatterStyle}, string]} = { - '%': [PercentFormatterStyle, BASIC_FORMAT], -} - -export interface FormatterOptions { - fmt?: string - datefmt?: any - style?: string - validate?: boolean - defaults?: {[key: string]: any} -} - -/** - * Formatter instances are used to convert a LogRecord to text. - * - * Formatters need to know how a LogRecord is constructed. They are - * responsible for converting a LogRecord to (usually) a string which can - * be interpreted by either a human or an external system. The base Formatter - * allows a formatting string to be specified. If none is supplied, the - * style-dependent default value, "%(message)s", "{message}", or - * "${message}", is used. - * - * The Formatter can be initialized with a format string which makes use of - * knowledge of the LogRecord attributes - e.g. the default value mentioned - * above makes use of the fact that the user's message and arguments are pre- - * formatted into a LogRecord's message attribute. Currently, the useful - * attributes in a LogRecord are described by: - * - * %(name)s Name of the logger (logging channel) - * %(levelno)s Numeric logging level for the message (DEBUG, INFO, - * WARNING, ERROR, CRITICAL) - * %(levelname)s Text logging level for the message ("DEBUG", "INFO", - * "WARNING", "ERROR", "CRITICAL") - * %(pathname)s Full pathname of the source file where the logging - * call was issued (if available) - * %(filename)s Filename portion of pathname - * %(module)s Module (name portion of filename) - * %(lineno)d Source line number where the logging call was issued - * (if available) - * %(funcName)s Function name - * %(created)f Time when the LogRecord was created (time.time_ns() / 1e9 - * return value) - * %(asctime)s Textual time when the LogRecord was created - * %(msecs)d Millisecond portion of the creation time - * %(relativeCreated)d Time in milliseconds when the LogRecord was created, - * relative to the time the logging module was loaded - * (typically at application startup time) - * %(thread)d Thread ID (if available) - * %(threadName)s Thread name (if available) - * %(taskName)s Task name (if available) - * %(process)d Process ID (if available) - * %(message)s The result of record.getMessage(), computed just as - * the record is emitted - */ -export class Formatter { - public static defaultTimeFormat = '%Y-%M'; - public static defaultMsecFormat = '%s,%30d'; - - protected style: any; - protected fmt: string; - protected datefmt: any; - - /** - * Initialize the formatter with specified format strings. - * - * Initialize the formatter either with the specified format string, or a - * default as described above. Allow for specialized date formatting with - * the optional datefmt argument. If datefmt is omitted, you get an - * ISO8601-like (or RFC 3339-like) format. - * - * Use a style parameter of '%', '{' or '$' to specify that you want to - * use one of %-formatting, :meth:`str.format` (``{}``) formatting or - * :class:`string.Template` formatting in your format string. - */ - constructor(options?: FormatterOptions) { - options = options ?? {}; - var style = options.style ?? '%'; - var validate = options.validate ?? true; - - if (!Object.keys(STYLES).includes(style ?? '')) { - throw new ValueError(`style must be one of: ${Object.keys(STYLES).join(', ')}`) - } - - this.style = new STYLES[style][0]({ - fmt: options.fmt, - defaults: options.defaults ?? {} - }); - - if (validate) { this.style.validate() } - - this.fmt = this.style.fmt; - - this.datefmt = options.datefmt; - } - - /** - * Return the creation time of the specified LogRecord as formatted text. - * - * This method should be called from format() by a formatter which - * wants to make use of a formatted time. This method can be overridden - * in formatters to provide for any specific requirement, but the - * basic behaviour is as follows: if datefmt (a string) is specified, - * it is used with time.strftime() to format the creation time of the - * record. Otherwise, an ISO8601-like (or RFC 3339-like) format is used. - * The resulting string is returned. This function uses a user-configurable - * function to convert the creation time to a tuple. By default, - * time.localtime() is used; to change this for a particular formatter - * instance, set the 'converter' attribute to a function with the same - * signature as time.localtime() or time.gmtime(). To change it for all - * formatters, for example if you want all logging times to be shown in GMT, - * set the 'converter' attribute in the Formatter class. - */ - formatTime(record: LogRecord, datefmt?: any): string { - - //TODO: record.created - if (datefmt) { - //TODO: time.strftime - } - else { - //TODO: time.strftime - } - - return 'some time'; - } - - /** - * Format and return the specified exception information as a string. - - * This default implementation just uses - * traceback.print_exception() - */ - formatError(ei: MyError): string { - //TODO - return 'some error'; - } -} - -const DEFAULT_FORMATTER = new Formatter(); - -//--------------------------------------------------------------------------- -// Filter classes and functions -//--------------------------------------------------------------------------- - -export type FilterCallable = (record: LogRecord) => boolean; - -/** - * Filter instances are used to perform arbitrary filtering of LogRecords. - * - * Loggers and Handlers can optionally use Filter instances to filter records as - * desired. The base filter class only allows events which are below a certain - * point in the logger hierarchy. For example, a filter initialized with "A.B" - * will allow events logged by loggers "A.B", initialized with the empty string, - * all events are passed. - */ -export class Filter { - public readonly scope: string; - public readonly slen: number; - - /** - * Initialize with the name of the logger which ,together with its children, - * will have its events allowed through the filter. If no name is specified, - * allow every event. - * - * @param name - name of logging scope - */ - constructor(scope: string) { - this.scope = scope ?? ''; - this.slen = this.scope.length; - } - - /** - * Inspect a record, if it should be logged. - * - * Returns true if the record should be logged, or false otherwise. If - * deemed appropriate, the record may be modified in-place. - * - * @param - scope of log record to inspect - * @param - log record to inspect - */ - filter(record: LogRecord): boolean { - if (this.slen == 0 || this.scope == record.scope) { return true } - else if (!record.scope.substring(0, this.slen)) { return false } - return (record.scope[this.slen] == '.') - } -} - -export class Filterer { - filters: Filter[] = []; - - /** - * Add the specified filter to this handler. - * - * @param filter - */ - addFilter(filter: Filter) { - if (!this.filters.includes(filter)) { this.filters.push(filter) } - } - - /** - * Remove the specified filter from this handler. - * - * @param filter - */ - removeFilter(filter: Filter) { - if (this.filters.includes(filter)) { - this.filters.splice(this.filters.indexOf(filter), 1) - } - } - - /** - * Determine if a record is loggable by consulting all the filters. - * - * The default is to allow the record to be logged; any filter can veto this - * by returning a false value. - * If a filter attached to a handler returns a log record instance, then - * that instance is used in place of the original log record in any further - * processing of the event by that handler. - * If a filter returns any other true value, the original log record is used - * in any further processing of the event by that handler. - * - * If none of the filters return false values, this method returns a log - * record. - * - * If any of the filters return a false value, this method returns a false - * value. - * - * @param filter - */ - filter(record: LogRecord): LogRecord|null { - - for (var i = 0; i < this.filters.length; i += 1) { - let result: boolean|LogRecord = false; - - let filter = this.filters[i]; - - if (typeof (filter as Filter).filter == 'function') { - result = (filter as Filter).filter(record) - } - else { - result = (filter as unknown as FilterCallable)(record) - } - - if (!result) { return null } - - if ((result as any) instanceof LogRecord) { record = result as unknown as LogRecord } - } - - return record - } -} - -var throwErrors: boolean = true; - -//--------------------------------------------------------------------------- -// Handler classes and functions -//---------------------------------------------------------------------------- - -type Handlers = {[key: string]: Handler}; - -/** - * map of handler names to handlers - */ -const HANDLERS: Handlers = {}; - -/** - * added to allow handlers to be removed in reverse order of initialization - */ -const HANDLER_LIST: WeakRef[] = []; - -/** - * Add a handler to the internal cleanup list using a weak reference. - * - * @param handler - - */ -function addHandlerRef(handler: Handler) { - HANDLER_LIST.push(new WeakRef(handler)); -} - -/** - * Get a handler with the specified *name*, or None if there isn't one with - * that name. - */ -export function getHandlerByName(name: string): Handler|null { - return HANDLERS[name] ?? null -} - -/** - * Return all known handler names as an immutable set - */ -export function getHandlerNames(): Handlers { return Object.freeze(HANDLERS) } - -/** - * Handler instances dispatch logging events to specific destinations. - * - * The base handler class. Acts as a placeholder which defines the Handler - * interface. Handlers can optionally use Formatter instances to format - * records as desired. By default, no formatter is specified; in this case, - * the 'raw' message as determined by record.message is logged. - */ -export class Handler extends Filterer { - - protected _scope: string|null = null; - protected _formatter: Formatter|null = null; - protected _level: number; - protected _closed: boolean = false; - - /** - * Initializes the instance - basically setting the formatter to None - * and the filter list to empty - */ - constructor(level?: LogLevel) { - super(); - this._level = checkLevel(level ?? NOTSET); - // Add the handler to the global HANDLER_LIST (for cleanup on shutdown) - addHandlerRef(this); - } - - get level(): number { return this._level } - set level(level: LogLevel|string) { this.level = checkLevel(level) } - - get scope(): string|null { return this._scope } - set scope(scope: string) { this._scope = scope } - get closed(): boolean { return this._closed } - - /** - * Format the specified record. - * - * If a formatter is set, use it. Otherwise, use the default formatter for - * the module. - */ - format(record: LogRecord) { - var fmt: Formatter|null = null; - - if (this.formatter) { fmt = this.formatter } - else { fmt = DEFAULT_FORMATTER } - } - - /** - * Do whatever it takes to actually log the specified logging record. - * - * This version is intended to be implemented by subclasses and so raises a - * NotImplementedError. - */ - emit(record: LogRecord) { - throw new NotImplementedError( - 'emit must be implemented by Handler subclass' - ) - } - - /** - * Conditionally emit the specfied logging record. - * - * Emission depends on filters which may have been added to the handler. - * Wrap the actual emission of the record with acquisition/release of the - * I/O thread lock. - */ - handle(record: LogRecord) { - var rv = this.filter(record); - if ((rv as any) instanceof LogRecord) { - record = rv as unknown as LogRecord - } - if (rv) { - //locking here - this.emit(record) - } - } - - /** - * Tidy up any resources used by the handler - * - * This version removes the handler from an internal map of handlers, which - * is used for handler lookup by scope. Subclasses should ensure that this - * gets called from overriden close() methods. - */ - close() { - this._closed = true; - - if (this.scope && Object.keys(HANDLERS).includes(this.scope)) { - delete HANDLERS[this.scope] - } - } - - /** - * Handle errors which occur during an emit() call. - * - * This method should be called from handlers when an exception is - * encountered during an emit() call. If raiseExceptions is false, - * exceptions get silently ignored. This is what is mostly wanted - * for a logging system - most users will not care about errors in - * the logging system, they are more interested in application errors. - * You could, however, replace this with a custom handler if you wish. - * The record which was being processed is passed in to this method. - */ - handleError(record: LogRecord) { - throw new NotImplementedError( - 'still need to find portable way for stacktracing...' - ) - } - - set formatter(fmt: Formatter) { this._formatter = fmt } -} - -export interface FileHandlerOptions { - filename: string - filemode?: string - encoding?: string - errors?: string -} - -/** - * A handler class which writes logging records, appropriately formatted, - to a stream. Note that this class does not close the stream, as - sys.stdout or sys.stderr may be used. - */ -export class StreamHandler extends Handler { - constructor(stream?: stream.Writable) { - super(); - } -} - -export class FileHandler extends StreamHandler { - constructor(options: FileHandlerOptions) { - super(); - } -} - -/** - * This class is like a StreamHandler using sys.stderr, but always uses - * whatever sys.stderr is currently set to rather than the value of - * sys.stderr at handler construction time. - */ - export class StderrHandler extends Handler { - /** - * Initialize the handler. - */ - constructor(level: LogLevel) { super(level) } -} - -const DEFAULT_LAST_RESORT = new StderrHandler(WARNING); - -export var lastResort = DEFAULT_LAST_RESORT; - -//--------------------------------------------------------------------------- -// Manager classes and functions -//--------------------------------------------------------------------------- - -/** - * Placeholder instance - */ -export class Placeholder { - protected loggers: Logger[] = []; - - /** - * initialize with the specified logger being a child of this placeholder. - */ - constructor(logger: Logger) { this.push(logger) } - - /** - * add the specified logger as a child of this placeholder - */ - public push(logger: Logger) { - if (!this.loggers.includes(logger)) { this.loggers.push(logger) } - } -} - -/** - * There is [under normal circumstances] just one Manager intance, which holds - * the hierarchy of loggers. - */ -export class Manager { - public readonly root: RootLogger; - protected _disable: number = 0; - public emittedNoHandlerWarning: boolean = false; - protected loggers: {[key: string]: Logger} = {}; - protected _loggerClass: LoggerClass|null = null; - protected _logRecordFactory: LogRecordFactory|null = null; - - public get disable(): number { return this._disable } - - public set disable(level: LogLevel) { this._disable = checkLevel(level) } - - /** - * Initialize the manager with the root node of the logger hierarchy - */ - constructor(root: RootLogger) { - this.root = root; - } - - /** - * Get a logger with the specified name (scope name), creating it, if it - * does not yet exist. This name is a dot-separated hierarchical name, such - * as "a", "a.b", "a.b.c" or similar. - * - * If a PlaceHolder existed for the specified name [i.e. the logger didn't - * exist but a child of it did], replace it with the created logger and fix - * up the parent/child references which pointed to the placeholder to now - * point to the logger. - */ - getLogger(scope: string) { - var rv: null|Logger = null; - - if (typeof scope != 'string') { - - rv = this.loggers[scope]; - - if (rv instanceof Placeholder) { - var ph = rv; - rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET); - } - else { - rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET); - this.loggers[scope] = rv; - } - } - } - - /** - * Set the class to be used when instantiating a logger with this Manager. - */ - set loggerClass(class_: LoggerClass) { - if (class_ !== Logger) { - if (!(class_.prototype instanceof Logger)) { - throw new TypeError("logger not derived from logging.Logger: ") - } - } - - this._loggerClass = class_; - } - - /** - * Set the factory to be used when instantiating a log record with this - * Manager. - */ - set logRecordFactory(factory: LogRecordFactory) { - this._logRecordFactory = factory; - } - - /** - * clear the cache for all loggers in loggerDict - */ - public clear() { - Object.values(this.loggers).forEach((logger) => { - logger.clear() - }); - } -} - -//--------------------------------------------------------------------------- -// Logger classes and functions -//--------------------------------------------------------------------------- - -export type LoggerClass = { new(): Logger }; - -/** - * context of a logging event/trigger - */ -export interface LogOptions{ - /** - * - */ - excInfo: ExecutionInfo|Error|null, - /** - * - */ - extra: {[key: string]: any}|null, - /** - * - */ - stackInfo: boolean, - /** - * - */ - stackLevel: number -} - -const DEFAULT_LOG_OPTIONS: LogOptions = Object.freeze({ - excInfo: null, - extra: null, - stackInfo: false, - stackLevel: 1 -}); - -/** - * Instances of the logger class represent a single logging channel. A 'logging - * channel' indicates an area of an application. Exactly how an 'area' is - * defined is up to the application developer. Since an application can have any - * number of areas, logging channels are identified by a unique string. - * Application areas can be nested (e.g. an area of input process might include - * sub-areas "read CSV file", "read XLS files" and "read Gnumeric files"). To - * cater for this natural nesting, channel ames are organized into a namespace - * hierarchy where levels are separated by periods, much like the Java or Python - * package namespace. So in the instance given above, channel names might be - * "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for - * the sub-levels. - * There is no arbitrary limit to the depth of nesting. - */ -export class Logger extends Filterer { - public readonly scope: string; - public _level: number; - private _manager: Manager|null = null; - public readonly parent: Logger|null = null; - public readonly propagate: boolean = true; - public readonly handlers: Handler[] = []; - public readonly disabled: boolean = false; - private cache: {[key: number]: boolean} = {}; - - /** - * Initialize the logger with a name and an optional level - * - * @param scope - - * @param level - - * @param manager - - */ - constructor( - scope: string, - level?: LogLevel, - ) { - super(); - - this.scope = scope; - this._level = checkLevel(level ?? NOTSET); - } - - public get level() { return this._level } - - public set level(level: LogLevel) { this._level = checkLevel(level) } - - public set manager(manager: Manager) { - if (this.manager) { - throw new ValueError('logger can only be assigned to manager once'); - } - } - - public setLevel(level: LogLevel) { - this.level = checkLevel(level); - - //this.manager.clearCache() - } - - /** - * Get the effective level for this logger. - * - * Loop through this logger and its parents in the logger hierarchy, looking - * for a non-zero logging level. Return the first one found. - */ - public getEffectiveLevel() { - var logger: Logger|null = this; - - while (logger) { - if (logger.level) { return logger.level } - logger = logger.parent; - } - - return NOTSET; - } - - /** - * Is this logger enabled for level 'level'? - */ - public isEnabledFor(level: LogLevel): boolean { - if (this.disabled) { return false } - - if (this.cache[level] === undefined && this.manager && this.manager.disable < level) { - return this.cache[level] = ( - level >= this.getEffectiveLevel() - ); - } - - return this.cache[level] = false; - } - - /** - * Log 'msg % args' with severity 'DEBUG' - * - * To pass exception information, use the keyword argument exc_info with - * a true value, e.g. - * - * ``` - * logger.debug("Houston, we have a thorny problem", { exc_info: true }) - * ``` - */ - public debug(msg: string, options?: LogOptions) { - if (this.isEnabledFor(DEBUG)) { this._log(DEBUG, msg, options) } - } - - /** - * A factory method which can be overriden in subclasses to create - * specialized LogRecords. - * - * - */ - protected makeRecord( - name: string, - level: LogLevel, - msg: string, - options: LogOptions, - ): LogRecord { - - var recordOptions: LogRecordOptions = { - level: level, - msg: msg, - }; - - var rv = logRecordFactory(name, recordOptions); - - if (options.extra !== null) { - Object.entries(options.extra!).forEach((item) => { - - var [k, v] = item; - - if (['message', 'asctime'].includes(k as string) || - (rv as {[key: string]: any}).keys().includes(k as string)) { - throw new KeyError('attempt to overwrite ${k} in LogRecord') - } - - (rv as any)[k] = options.extra![k as string] as any - }) - } - - return rv - } - - /** - * Low-level logging routine which creates a LogRecord and then calls the - * handlers of this logger to handle the record. - */ - protected _log(level: LogLevel, msg: string, options?: LogOptions) { - options = options ?? DEFAULT_LOG_OPTIONS; - options = { ...DEFAULT_LOG_OPTIONS, ...options }; - - var sinfo=null; - - if (options!.excInfo !== null) { - if (options!.excInfo instanceof Error) { - var excInfo: ExecutionInfo = [ - typeof options!.excInfo, - options!.excInfo, - options!.excInfo.stack! - ] - } - else if (!(options!.excInfo instanceof Array)) { - throw new NotImplementedError("would try to get the callee stack from the system. Probably will use stacktrace.js as this needs to be implemented browser-specific."); - } - } - - var record = this.makeRecord(this.scope, level, msg, options) - } - - /** - * Call the handlers for the specified record. - * - * This method is used for unpickled records received from a socket, as well - * as those created locally. Logger-level filtering is applied. - */ - protected handle(scope: string, record: LogRecord) { - if (this.disabled) { return } - var maybeRecord = this.filter(record); - if (!maybeRecord) { return } - if ((maybeRecord as any) instanceof LogRecord) { record = maybeRecord } - this.callHandlers(record) - } - - /** - * Pass a record to all relevant handlers. - * - * Loop through all handlers for this logger and its parents n the logger - * hierarchy. If no handler was found, output a one-off error message to - * sys.stderr. Stop searching up the hierarchy whenever a logger with the - * "propagate" attribute set to zero is found - that will be the last logger - * whose handlers are called. - */ - protected callHandlers(record: LogRecord) { - var c: Logger|null = this; - var found = 0; - - while (c) { - for (var i = 0; i < c.handlers.length; i += 1) { - let hdlr = c.handlers[i]; - - found = found + 1; - - if (record.levelno >= hdlr.level) { hdlr.handle(record) } - } - - if (!c.propagate) { c = null } - else { c = c.parent } - } - - if (found == 0) { - if (lastResort) { - if (record.levelno >= lastResort.level) { - lastResort.handle(record) - } - else if (throwErrors && (this.manager && !this.manager.emittedNoHandlerWarning)) { - console.error( - `No handlers could be found for logger ${this.scope}` - ); - - this.manager.emittedNoHandlerWarning = true; - } - } - } - } - - public clear() { - for (var property in this.cache) delete this.cache[property]; - } - - /** - * Remove the specified handler from this logger. - */ - public addHandler(hdlr: Handler) { - const i = this.handlers.indexOf(hdlr); - if (i === -1) { this.handlers.push(hdlr) } - } - - /** - * Remove the specified handler from this logger. - */ - public removeHandler(hdlr: Handler) { - const i = this.handlers.indexOf(hdlr); - if (i !== -1) { delete this.handlers[i] } - } -} - -/** - * A root logger is not that different to any other logger, except that it must - * have a logging level and there is only one instance of in a manager's - * hierarchy. - */ -class RootLogger extends Logger { - - constructor(level: LogLevel) { - super('root', level); - } -} - -var loggerClass = Logger; - -/** - * root logger (singleton) - */ -const ROOT = new RootLogger(WARNING); - -/** - * log manager (singleton) - */ -const MANAGER = new Manager(ROOT); - -//--------------------------------------------------------------------------- -// Configuration classes and functions -//--------------------------------------------------------------------------- - -/** - * options for basic configuration of logging module - */ -export interface BasicConfigOptions { - /* - * Specifies that a FileHandler be created, using the specified filename, - * rather than a StreamHandler. - */ - filename?: string; - - /** - * Specifies the mode to open the file, if filename is specified (if - * filemode is unspecified, it defaults to 'a') - */ - filemode?: string; - - /** - * Use the specified format string for the handler. - */ - format?: string; - - /** - * Use the specified date/time format. - * - */ - datefmt?: string; - - /** - * If a format string is specified, use this to specify the type of format - * string (possible values '%', '{', '$', for %-formatting, - * :meth:`str.format` and :class:`string.Template`- defaults to '%'). - * - * TODO: switch to enum - */ - style?: string; - - /** - * Set the root logger level to the specified level. - */ - level?: LogLevel; - - /** - * Use the specified stream to initialize the StreamHandler. Note that this - * argument is incompatible with 'filename' - if both are present, 'stream' - * is ignored. - * - * TODO: - */ - stream?: any; - - /** - * If specified, this should be an iterable of already created handlers, - * which will be added to the root logger. Any handler in the list which - * does not have a formatter assigned will be assigned the formatter created - * in this function. - */ - handlers?: Handler[]; - - /** - * If this keyword is specified as true, any existing handlers attached to - * the root logger are removed and closed, before carrying out the - * configuration as specified by the other arguments. - */ - force?: boolean; - - /** - * If specified together with a filename, this encoding is passed to the - * created FileHandler, causing it to be used when the file is opened. - */ - encoding?: string; - - /** - * If specified together with a filename, this value is - * passed to the created FileHandler, causing it to be used - * when the file is opened in text mode. If not specified, - * the default value is `backslashreplace`. - */ - errors?: string|null; -} - -/** - * Do basic configuration for the logging system. - * - * This function does nothing if the root logger already has handlers - * configured, unless the keyword argument *force* is set to ``True``. - * It is a convenience method intended for use by simple scripts - * to do one-shot configuration of the logging package. - * - * The default behaviour is to create a StreamHandler which writes to - * sys.stderr, set a formatter using the BASIC_FORMAT format string, and - * add the handler to the root logger. - * - * A number of optional keyword arguments may be specified, which can alter - * the default behaviour. - * - * Note that you could specify a stream created using open(filename, mode) - * rather than passing the filename and mode in. However, it should be - * remembered that StreamHandler does not close its stream (since it may be - * using sys.stdout or sys.stderr), whereas FileHandler closes its stream - * when the handler is closed. - * - * TODO: refactor logic, there apparently is some redundancy in the original - * code - */ -export function basicConfig(options: BasicConfigOptions) { - const force = options.force ?? false; - var encoding = options.encoding ?? undefined; - var errors: string|undefined = options.errors ?? 'backslashreplace'; - var handlers = options.handlers ?? []; - const filename = options.filename ?? null; - const stream = options.stream ?? null; - const filemode = options.filemode ?? 'a'; - const dateformat = options.filemode ?? null; - const style = options.filemode ?? '%'; - const level = options.level ?? null; - - if (!Object.keys(STYLES).includes(style)) { - throw new ValueError( - `style must be one of: ${Object.keys(STYLES).join(', ')}` - ); - } - - if (force) { - for (var i = 0; i < MANAGER.root.handlers.length; i += 1) { - let h: Handler = MANAGER.root.handlers[i]; - MANAGER.root.removeHandler(h); - h.close(); - } - } - - if (handlers.length == 0) { - if (handlers === null && stream && filename) { - throw new ValueError( - "'stream' and 'filename' should not be specified together" - ); - } - - else if (stream || filename) { - throw new ValueError( - "'stream' or 'filename' should not be specified together" + - "with 'handlers'" - ); - } - - if (handlers === null) { - var h: Handler; - - if (filename) { - if (filemode.match('b')) { errors = undefined } - else { encoding = 'utf-8' } - - h = new FileHandler({ - filename: filename, - filemode: filemode, - 'encoding': encoding, - errors: errors - }); - } - - else { h = new StreamHandler(stream) } - - handlers = [h]; - } - - for (var i = 0; i < handlers.length; i += 1) { - let h = handlers[i]; - - if (h.formatter === null) { - h.formatter = new Formatter({ - fmt: options.format ?? STYLES[style][1], - datefmt: dateformat, - style: style - }); - } - - MANAGER.root.addHandler(h); - } - - if (level !== null) { MANAGER.root.setLevel(level) } - - if (options) { - // runtime interface guard, please let me stay. 🥺 - // the interface does not allow for additional members, but the - // runtime environment has no concept of interfaces. We can stick to - // the original implementation - const keys = Object.keys(options).join(', '); - - throw new ValueError(`Unrecognised argument(s): ${keys}`); - } - } -} diff --git a/src/log-level.ts b/src/log-level.ts new file mode 100644 index 0000000..23014ba --- /dev/null +++ b/src/log-level.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------- + Level related stuff + --------------------------------------------------------------------------- + + Default levels and level names, these can be replaced with any positive set + of values having corresponding names. There is a pseudo-level, NOTSET, which + is only really there as a lower limit for user-defined levels. Handlers and + loggers are initialized with NOTSET so that they will log all messages, even + at user-defined levels. +*/ + +export type LogLevel = number; + +/** + * An indication that something unexpected happened, or that a problem might + * occur in the near future (e.g. ‘disk space low’). The software is still + * working as expected. + */ +export const CRITICAL = 50; +export const FATAL = CRITICAL; + +/** + * Due to a more serious problem, the software has not been able to perform some + * function. + */ +export const ERROR = 40; + +/** + * An indication that something unexpected happened, or that a problem might + * occur in the near future (e.g. ‘disk space low’). The software is still + * working as expected. + */ +export const WARNING = 30; +export const WARN = WARNING; + +/** + * Confirmation that things are working as expected. + */ +export const INFO = 20; + +/** + * Detailed information, typically only of interest to a developer trying to + * diagnose a problem. + */ +export const DEBUG = 10; + +/** + * When set on a logger, indicates that ancestor loggers are to be consulted to + * determine the effective level. If that still resolves to NOTSET, then all + * events are logged. When set on a handler, all events are handled. + */ +export const NOTSET = 0; + +const LEVELTONAME: {[key: number]: string} = { + [CRITICAL]: 'CRITICAL', + [ERROR]: 'ERROR', + [WARNING]: 'WARNING', + [INFO]: 'INFO', + [DEBUG]: 'DEBUG', + [NOTSET]: 'NOTSET' +} + +const NAMETOLEVEL: {[key: string]: number} = { + CRITICAL: CRITICAL, + ERROR: ERROR, + WARNING: WARNING, + INFO: INFO, + DEBUG: DEBUG, + NOTSET: NOTSET, +} + +function getLevelNamesMapping() { + return Object.assign({}, NAMETOLEVEL); +} + +/** + * Return the textual or numeric representation of logging level 'level' + * + * @param level + */ +export function getLevelName(level: string|number): string|number { + var result: string|number = LEVELTONAME[level as number]; + if (result !== undefined) { return result } + result = NAMETOLEVEL[level as string]; + if (result !== undefined) { return result } + return `Level ${level}`; +} + +/** + * Associate 'levelName' with 'level' + * + * @param level + * @param levelName + */ +export function addLevelName(level: number, levelName: string) { + LEVELTONAME[level] = levelName; + NAMETOLEVEL[levelName] = level; +} + +export function checkLevel(level: number|string): number { + var rv: number; + + if (typeof level == 'number') { rv = level } + + else if (typeof level == 'string') { + if (!Object.keys(NAMETOLEVEL).includes(level as string)) { + throw new Error(`Unknown level: ${level}`) + } + + rv = NAMETOLEVEL[level] + } + + else { + throw new Error(`Level not a number or valid string: ${level}`) + } + + return rv +} diff --git a/src/log-record.ts b/src/log-record.ts new file mode 100644 index 0000000..6c6f335 --- /dev/null +++ b/src/log-record.ts @@ -0,0 +1,77 @@ +import { getLevelName, LogLevel } from './log-level'; +import { MillisecondsSinceUnixEpoch } from './helper/datetime'; + +//--------------------------------------------------------------------------- +// The logging record +//--------------------------------------------------------------------------- + +/** + * options for instantiating a new log record + */ +export interface LogRecordOptions { + /** + * The numeric level of the logging event (such as 10 for DEBUG, 20 for + * INFO, etc). Note that this is converted to two attributes of the + * LogRecord: levelno for the numeric value and levelname for the + * corresponding level name. + */ + level: number, + file?: string, + /** + * The line number in the source file where the logging call was made. + */ + lno?: number, + /** + * The event description message, which can be a %-format string with + * placeholders for variable data, or an arbitrary object (see Using + * arbitrary objects as messages). + */ + msg: string, + /** + * Variable data to merge into the msg argument to obtain the event + * description. + */ + args?: any[], +} + +export type LogRecordFactory = { (name: string, options: LogRecordOptions): LogRecord }; + +/** + * LogRecord instances are created every time something is logged. They contain + * all the information pertinent to the event being logged. The main + * information parssed in is msg and args, which are combined using str(msg) % + * args to create the message field of the record. The record also includes + * information such as when the record was created, the source line where the + * logging call was made, and any exception information to be logged. + */ +export class LogRecord { + public readonly levelno: LogLevel; + public readonly levelname: string|LogLevel; + public readonly scope: string; + + public readonly created: MillisecondsSinceUnixEpoch = Date.now(); + + constructor(scope: string, options: LogRecordOptions) { + this.levelno = options.level; + this.levelname = getLevelName(options.level); + this.scope = scope; + } +} + +export var logRecordFactory = (scope: string, options: LogRecordOptions) => { + return new LogRecord(scope, options) +}; + +/** + * Define which class use when instantiating log records. + * + * @param factory - A callable which will be called to instantiate a log record. + * Pass a clojure, if your factory is a class already. + */ +export function setLogRecordFactory(factory: LogRecordFactory) { + logRecordFactory = factory +} + +export function getLogRecordFactory(): LogRecordFactory { + return logRecordFactory +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..8cdff06 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,324 @@ +import { + LogLevel, + DEBUG, + NOTSET, + WARNING, + checkLevel, +} from './log-level'; +import { + LogRecord, + logRecordFactory, + LogRecordOptions, +} from './log-record'; +import { Handler, StderrHandler } from './handler'; +import { + NotImplementedError, + KeyError, + ValueError, + StackTrace, +} from './helper/error'; +import { Manager } from './manager'; +import { Filterer } from './filter'; + +//--------------------------------------------------------------------------- +// Logger classes and functions +//--------------------------------------------------------------------------- + +export type ExecutionInfo = [string, Error, StackTrace]; + +export var throwErrors: boolean = true; + +export const DEFAULT_LAST_RESORT = new StderrHandler(WARNING); + +export var lastResort = DEFAULT_LAST_RESORT; + +export type LoggerClass = { new(): Logger }; + +/** + * context of a logging event/trigger + */ +export interface LogOptions{ + /** + * + */ + excInfo: ExecutionInfo|Error|null, + /** + * + */ + extra: {[key: string]: any}|null, + /** + * + */ + stackInfo: boolean, + /** + * + */ + stackLevel: number +} + +const DEFAULT_LOG_OPTIONS: LogOptions = Object.freeze({ + excInfo: null, + extra: null, + stackInfo: false, + stackLevel: 1 +}); + +/** + * Instances of the logger class represent a single logging channel. A 'logging + * channel' indicates an area of an application. Exactly how an 'area' is + * defined is up to the application developer. Since an application can have any + * number of areas, logging channels are identified by a unique string. + * Application areas can be nested (e.g. an area of input process might include + * sub-areas "read CSV file", "read XLS files" and "read Gnumeric files"). To + * cater for this natural nesting, channel ames are organized into a namespace + * hierarchy where levels are separated by periods, much like the Java or Python + * package namespace. So in the instance given above, channel names might be + * "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for + * the sub-levels. + * There is no arbitrary limit to the depth of nesting. + */ +export class Logger extends Filterer { + public readonly scope: string; + public _level: number; + private _manager: Manager|null = null; + public readonly parent: Logger|null = null; + public readonly propagate: boolean = true; + public readonly handlers: Handler[] = []; + public readonly disabled: boolean = false; + private cache: {[key: number]: boolean} = {}; + + /** + * Initialize the logger with a name and an optional level + * + * @param scope - + * @param level - + * @param manager - + */ + constructor( + scope: string, + level?: LogLevel, + ) { + super(); + + this.scope = scope; + this._level = checkLevel(level ?? NOTSET); + } + + public get level() { return this._level } + + public set level(level: LogLevel) { this._level = checkLevel(level) } + + public set manager(manager: Manager) { + if (this.manager) { + throw new ValueError('logger can only be assigned to manager once'); + } + } + + public setLevel(level: LogLevel) { + this.level = checkLevel(level); + + //this.manager.clearCache() + } + + /** + * Get the effective level for this logger. + * + * Loop through this logger and its parents in the logger hierarchy, looking + * for a non-zero logging level. Return the first one found. + */ + public getEffectiveLevel() { + var logger: Logger|null = this; + + while (logger) { + if (logger.level) { return logger.level } + logger = logger.parent; + } + + return NOTSET; + } + + /** + * Is this logger enabled for level 'level'? + */ + public isEnabledFor(level: LogLevel): boolean { + if (this.disabled) { return false } + + if (this.cache[level] === undefined && this.manager && this.manager.disable < level) { + return this.cache[level] = ( + level >= this.getEffectiveLevel() + ); + } + + return this.cache[level] = false; + } + + /** + * Log 'msg % args' with severity 'DEBUG' + * + * To pass exception information, use the keyword argument exc_info with + * a true value, e.g. + * + * ``` + * logger.debug("Houston, we have a thorny problem", { exc_info: true }) + * ``` + */ + public debug(msg: string, options?: LogOptions) { + if (this.isEnabledFor(DEBUG)) { this._log(DEBUG, msg, options) } + } + + /** + * A factory method which can be overriden in subclasses to create + * specialized LogRecords. + * + * + */ + protected makeRecord( + name: string, + level: LogLevel, + msg: string, + options: LogOptions, + ): LogRecord { + + var recordOptions: LogRecordOptions = { + level: level, + msg: msg, + }; + + var rv = logRecordFactory(name, recordOptions); + + if (options.extra !== null) { + Object.entries(options.extra!).forEach((item) => { + + var [k, v] = item; + + if (['message', 'asctime'].includes(k as string) || + (rv as {[key: string]: any}).keys().includes(k as string)) { + throw new KeyError('attempt to overwrite ${k} in LogRecord') + } + + (rv as any)[k] = options.extra![k as string] as any + }) + } + + return rv + } + + /** + * Low-level logging routine which creates a LogRecord and then calls the + * handlers of this logger to handle the record. + */ + protected _log(level: LogLevel, msg: string, options?: LogOptions) { + options = options ?? DEFAULT_LOG_OPTIONS; + options = { ...DEFAULT_LOG_OPTIONS, ...options }; + + var sinfo=null; + + if (options!.excInfo !== null) { + if (options!.excInfo instanceof Error) { + var excInfo: ExecutionInfo = [ + typeof options!.excInfo, + options!.excInfo, + options!.excInfo.stack! + ] + } + else if (!(options!.excInfo instanceof Array)) { + throw new NotImplementedError("would try to get the callee stack from the system. Probably will use stacktrace.js as this needs to be implemented browser-specific."); + } + } + + var record = this.makeRecord(this.scope, level, msg, options) + } + + /** + * Call the handlers for the specified record. + * + * This method is used for unpickled records received from a socket, as well + * as those created locally. Logger-level filtering is applied. + */ + protected handle(scope: string, record: LogRecord) { + if (this.disabled) { return } + var maybeRecord = this.filter(record); + if (!maybeRecord) { return } + if ((maybeRecord as any) instanceof LogRecord) { record = maybeRecord } + this.callHandlers(record) + } + + /** + * Pass a record to all relevant handlers. + * + * Loop through all handlers for this logger and its parents n the logger + * hierarchy. If no handler was found, output a one-off error message to + * sys.stderr. Stop searching up the hierarchy whenever a logger with the + * "propagate" attribute set to zero is found - that will be the last logger + * whose handlers are called. + */ + protected callHandlers(record: LogRecord) { + var c: Logger|null = this; + var found = 0; + + while (c) { + for (var i = 0; i < c.handlers.length; i += 1) { + let hdlr = c.handlers[i]; + + found = found + 1; + + if (record.levelno >= hdlr.level) { hdlr.handle(record) } + } + + if (!c.propagate) { c = null } + else { c = c.parent } + } + + if (found == 0) { + if (lastResort) { + if (record.levelno >= lastResort.level) { + lastResort.handle(record) + } + else if (throwErrors && (this.manager && !this.manager.emittedNoHandlerWarning)) { + console.error( + `No handlers could be found for logger ${this.scope}` + ); + + this.manager.emittedNoHandlerWarning = true; + } + } + } + } + + public clear() { + for (var property in this.cache) delete this.cache[property]; + } + + /** + * Remove the specified handler from this logger. + */ + public addHandler(hdlr: Handler) { + const i = this.handlers.indexOf(hdlr); + if (i === -1) { this.handlers.push(hdlr) } + } + + /** + * Remove the specified handler from this logger. + */ + public removeHandler(hdlr: Handler) { + const i = this.handlers.indexOf(hdlr); + if (i !== -1) { delete this.handlers[i] } + } +} + +/** + * A root logger is not that different to any other logger, except that it must + * have a logging level and there is only one instance of in a manager's + * hierarchy. + */ +export class RootLogger extends Logger { + + constructor(level: LogLevel) { + super('root', level); + } +} + +/** + * root logger (singleton) + */ +export const ROOT = new RootLogger(WARNING); diff --git a/src/manager.ts b/src/manager.ts new file mode 100644 index 0000000..c891849 --- /dev/null +++ b/src/manager.ts @@ -0,0 +1,127 @@ +import { + Logger, + LoggerClass, + RootLogger, + ROOT, +} from './logger'; +import { LogRecordFactory } from './log-record'; +import { + LogLevel, + NOTSET, + checkLevel, +} from './log-level' + + +//--------------------------------------------------------------------------- +// Manager classes and functions +//--------------------------------------------------------------------------- + +var loggerClass = Logger; + +/** + * Placeholder instance + */ +class Placeholder { + protected loggers: Logger[] = []; + + /** + * initialize with the specified logger being a child of this placeholder. + */ + constructor(logger: Logger) { this.push(logger) } + + /** + * add the specified logger as a child of this placeholder + */ + public push(logger: Logger) { + if (!this.loggers.includes(logger)) { this.loggers.push(logger) } + } +} + + +/** + * There is [under normal circumstances] just one Manager intance, which holds + * the hierarchy of loggers. + */ +export class Manager { + public readonly root: RootLogger; + protected _disable: number = 0; + public emittedNoHandlerWarning: boolean = false; + protected loggers: {[key: string]: Logger} = {}; + protected _loggerClass: LoggerClass|null = null; + protected _logRecordFactory: LogRecordFactory|null = null; + + public get disable(): number { return this._disable } + + public set disable(level: LogLevel) { this._disable = checkLevel(level) } + + /** + * Initialize the manager with the root node of the logger hierarchy + */ + constructor(root: RootLogger) { + this.root = root; + } + + /** + * Get a logger with the specified name (scope name), creating it, if it + * does not yet exist. This name is a dot-separated hierarchical name, such + * as "a", "a.b", "a.b.c" or similar. + * + * If a PlaceHolder existed for the specified name [i.e. the logger didn't + * exist but a child of it did], replace it with the created logger and fix + * up the parent/child references which pointed to the placeholder to now + * point to the logger. + */ + getLogger(scope: string) { + var rv: null|Logger = null; + + if (typeof scope != 'string') { + + rv = this.loggers[scope]; + + if (rv instanceof Placeholder) { + var ph = rv; + rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET); + } + else { + rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET); + this.loggers[scope] = rv; + } + } + } + + /** + * Set the class to be used when instantiating a logger with this Manager. + */ + set loggerClass(class_: LoggerClass) { + if (class_ !== Logger) { + if (!(class_.prototype instanceof Logger)) { + throw new TypeError("logger not derived from logging.Logger: ") + } + } + + this._loggerClass = class_; + } + + /** + * Set the factory to be used when instantiating a log record with this + * Manager. + */ + set logRecordFactory(factory: LogRecordFactory) { + this._logRecordFactory = factory; + } + + /** + * clear the cache for all loggers in loggerDict + */ + public clear() { + Object.values(this.loggers).forEach((logger) => { + logger.clear() + }); + } +} + + +/** + * log manager (singleton) + */ +export const MANAGER = new Manager(ROOT); diff --git a/src/polyfill/regexp.ts b/src/polyfill/regexp.ts new file mode 100644 index 0000000..ca50ad4 --- /dev/null +++ b/src/polyfill/regexp.ts @@ -0,0 +1,11 @@ +/** + * Polyfill for `RegExp.escape`, ensuring compatibility with environments + * that do not yet support this method. + * + * @see src/types/regexp.d.ts For the TypeScript type declaration. + */ +if (!RegExp.escape) { + RegExp.escape = function (str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }; +} diff --git a/src/types/regexp.d.ts b/src/types/regexp.d.ts new file mode 100644 index 0000000..4274c95 --- /dev/null +++ b/src/types/regexp.d.ts @@ -0,0 +1,8 @@ +interface RegExpConstructor { + /** + * @see + * {@link https://tc39.es/proposal-regex-escaping/#sec-regexp.escape + * | ECMAScript Stage 4 Draft} + */ + escape?(str: string): string; +} diff --git a/src/util/regexp.ts b/src/util/regexp.ts new file mode 100644 index 0000000..c01f820 --- /dev/null +++ b/src/util/regexp.ts @@ -0,0 +1,26 @@ +export type SubstitutionCallable = (match: RegExpExecArray|null) => string; + + +/** + * Return the string obtained by replacing the leftmost non-overlapping + * occurrences of the pattern in `input` by the `substitution`. `substitution` + * can be either a string or a callable; if a string, backslash escapes in it + * are processed. If it is a callable, it's passed the `RegExpExecArray` object + * and must return a substitution string to be used. + */ +export function substitute( + pattern: RegExp, + input: string, + substitution: string|SubstitutionCallable, +): string { + return input.replace(pattern, (match, ...groups) => { + const execArray = pattern.exec(match); + + if (typeof substitution === "function") return substitution(execArray); + + return substitution.replace( + /\\(\d+)/g, + (_, index) => execArray?.[Number(index)] ?? '' + ); + }); +} diff --git a/src/util/string.ts b/src/util/string.ts new file mode 100644 index 0000000..0e17209 --- /dev/null +++ b/src/util/string.ts @@ -0,0 +1,100 @@ +/** + * TODO: Monitor ECMAScript Stage 4 Draft adoption. + * Once officially standardized, remove the polyfill. + * + * @see src/types/regexp.d.ts for more information. + */ +import '../polyfill/regexp'; + +/** + * Constants used for ctype-style character classification. + * + * Includes: + * - `WHITESPACE`: Common whitespace characters. + * - `ASCII_LOWERCASE`: Lowercase ASCII letters. + * - `ASCII_UPPERCASE`: Uppercase ASCII letters. + * - `ASCII_LETTERS`: Combined uppercase and lowercase letters. + * - `DIGITS`: Numeric digits (0-9). + * - `HEXDIGITS`: Hexadecimal digits (0-9, a-f, A-F). + * - `OCTDIGITS`: Octal digits (0-7). + * - `PUNCTUATION`: A regex pattern for common punctuation characters. + * - `PRINTABLE`: All printable ASCII characters. + */ +export const WHITESPACE = ' \t\n\r\v\f'; +export const ASCII_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'; +export const ASCII_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +export const ASCII_LETTERS = ASCII_LOWERCASE + ASCII_UPPERCASE; +export const DIGITS = '0123456789'; +export const HEXDIGITS = DIGITS + 'abcdef' + 'abcdef'; +export const OCTDIGITS = '01234567'; +export const PUNCTUATION = new RegExp("!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~"); +export const PRINTABLE = DIGITS + ASCII_LETTERS + PUNCTUATION + WHITESPACE; + + +export interface TemplateOptions { + /** + * @remarks + * overrides of the default template options must ensure the delimiter is + * escaped through the `RegExp.escape` method. This is due to performance + * reasons as to not require escaping on every `Template` construction. + */ + delimiter: string, + /** + * TODO: write block comment + */ + bracedIdPattern?: string + /** + * @see + * {@link https://tc39.es/ecma262/multipage/text-processing.html#sec-regexp-constructor} + */ + flags: string +} + + +export const DEFAULT_TEMPLATE_OPTIONS: TemplateOptions = { + delimiter: RegExp.escape!('$'), + flags: 'i', +} + + +/** + * A string class for supporting $-substitutions + */ +export class Template { + /** + * '[a-z]' matches to non-ASCII letters when used with IGNORECASE, but + * without the ASCII flag. We can't add re.ASCII to flags because of + * backward compatibility. So we use the ?a local flag and [a-z] pattern. + * See https://bugs.python.org/issue31672 + */ + protected static idPattern: string = '[_a-z][_a-z0-9]*'; + protected readonly pattern: RegExp; + protected readonly template: string; + + static createPattern( + delimiter: string, + bracedIdPattern?: string, + ): string { + var pattern: string; + pattern = `${delimiter}(?:`; + pattern += `(?${delimiter})`; + pattern += `|(?${Template.idPattern})`; + pattern += `|{{(?${bracedIdPattern ?? Template.idPattern})}}`; + pattern += '|(?)'; + pattern += ')'; + + return pattern; + } + + constructor(template: string, options?: TemplateOptions) { + options = options ?? DEFAULT_TEMPLATE_OPTIONS; + + this.template = template; + + this.pattern = new RegExp( + Template.createPattern(options.delimiter, options.bracedIdPattern), + options.flags + ); + } +} + diff --git a/tests/log-level.test.ts b/tests/log-level.test.ts new file mode 100644 index 0000000..2a7d386 --- /dev/null +++ b/tests/log-level.test.ts @@ -0,0 +1,73 @@ +import {expect, jest, test} from '@jest/globals'; +import * as log_level from '../src/log-level'; + +describe('Logger', () => { + it('can be instantiated', () => { + //const logger = new log_level.Logger('test', 0); + }) +}); + +describe('getLevelName', () => { + it('numeric to textual representation of built-ins', () => { + expect( + log_level.getLevelName(log_level.CRITICAL) + ).toBe('CRITICAL'); + expect( + log_level.getLevelName(log_level.FATAL) + ).toBe('CRITICAL'); + expect( + log_level.getLevelName(log_level.ERROR) + ).toBe('ERROR'); + expect( + log_level.getLevelName(log_level.WARNING) + ).toBe('WARNING'); + expect( + log_level.getLevelName(log_level.WARN) + ).toBe('WARNING'); + expect( + log_level.getLevelName(log_level.INFO) + ).toBe('INFO'); + expect( + log_level.getLevelName(log_level.DEBUG) + ).toBe('DEBUG'); + expect( + log_level.getLevelName(log_level.NOTSET) + ).toBe('NOTSET'); + }); + + it('textual to numeric representation of built-ins', () => { + expect( + log_level.getLevelName('CRITICAL') + ).toBe(log_level.CRITICAL); + expect( + log_level.getLevelName('FATAL') + ).toBe(`Level FATAL`); + expect( + log_level.getLevelName('ERROR') + ).toBe(log_level.ERROR); + expect( + log_level.getLevelName('WARNING') + ).toBe(log_level.WARNING); + expect( + log_level.getLevelName('WARN') + ).toBe('Level WARN'); + expect( + log_level.getLevelName('INFO') + ).toBe(log_level.INFO); + expect( + log_level.getLevelName('DEBUG') + ).toBe(log_level.DEBUG); + expect( + log_level.getLevelName('NOTSET') + ).toBe(log_level.NOTSET); + }); +}); + + +describe('addLevelName', () => { + it('numeric to textual representation of built-ins', () => { + log_level.addLevelName(80, 'FOOBAR'); + expect(log_level.getLevelName(80)).toBe('FOOBAR'); + expect(log_level.getLevelName('FOOBAR')).toBe(80); + }) +}); diff --git a/tests/test.ts b/tests/test.ts deleted file mode 100644 index 208acdc..0000000 --- a/tests/test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {expect, jest, test} from '@jest/globals'; - -describe('Logger', () => { - it('can be instantiated', () => { - //const logger = new logging.Logger('test', 0); - }) -}); - -describe('getLevelName', () => { - var logging: any; - - beforeEach(() => { - // there are a couple of singletons, which I'm not yet sure if they need - // to be reloaded for every test case - logging = require('../src'); - }); - - it('numeric to textual representation of built-ins', () => { - expect( - logging.getLevelName(logging.CRITICAL) - ).toBe('CRITICAL'); - expect( - logging.getLevelName(logging.FATAL) - ).toBe('CRITICAL'); - expect( - logging.getLevelName(logging.ERROR) - ).toBe('ERROR'); - expect( - logging.getLevelName(logging.WARNING) - ).toBe('WARNING'); - expect( - logging.getLevelName(logging.WARN) - ).toBe('WARNING'); - expect( - logging.getLevelName(logging.INFO) - ).toBe('INFO'); - expect( - logging.getLevelName(logging.DEBUG) - ).toBe('DEBUG'); - expect( - logging.getLevelName(logging.NOTSET) - ).toBe('NOTSET'); - }); - - it('textual to numeric representation of built-ins', () => { - expect( - logging.getLevelName('CRITICAL') - ).toBe(logging.CRITICAL); - expect( - logging.getLevelName('FATAL') - ).toBe(`Level FATAL`); - expect( - logging.getLevelName('ERROR') - ).toBe(logging.ERROR); - expect( - logging.getLevelName('WARNING') - ).toBe(logging.WARNING); - expect( - logging.getLevelName('WARN') - ).toBe('Level WARN'); - expect( - logging.getLevelName('INFO') - ).toBe(logging.INFO); - expect( - logging.getLevelName('DEBUG') - ).toBe(logging.DEBUG); - expect( - logging.getLevelName('NOTSET') - ).toBe(logging.NOTSET); - }); -}); - - -describe('addLevelName', () => { - var logging: any; - - beforeEach(() => { - logging = require('../src'); - }); - - it('numeric to textual representation of built-ins', () => { - logging.addLevelName(80, 'FOOBAR'); - expect(logging.getLevelName(80)).toBe('FOOBAR'); - expect(logging.getLevelName('FOOBAR')).toBe(80); - }) -}); diff --git a/tsconfig.json b/tsconfig.json index 700b8f0..e3c3f14 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "lib": ["esnext.weakref", "dom"] }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/types/**/*.d.ts" ] }