feat: initialize tests, docs and refactor
This commit is contained in:
parent
c297e3f4ef
commit
7a70ed28c3
16 changed files with 2096 additions and 468 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ config.log
|
||||||
config.status
|
config.status
|
||||||
autom4te.cache
|
autom4te.cache
|
||||||
/dist
|
/dist
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
4
README.md
Normal file
4
README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
|
||||||
|
* [Logging Cookbook](doc/logging-cookbook.md)
|
||||||
|
|
||||||
1
doc/README.md
Normal file
1
doc/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
The doc/README.md
|
||||||
37
doc/logging-cookbook.md
Normal file
37
doc/logging-cookbook.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Using logging in multiple modules
|
||||||
|
|
||||||
|
Multiple calls to `logging.getLogger('someLogger')` return a reference to the
|
||||||
|
same logger object. This is true not only within the same module, but also
|
||||||
|
across modules as long as it is in the same Python interpreter process. It is
|
||||||
|
true for references to the same object; additionally, application code can
|
||||||
|
define and configure a parent logger in one module and create (but not
|
||||||
|
configure) a child logger in a separate module, and all logger calls to the
|
||||||
|
child will pass up to the parent. Here is a main module:
|
||||||
|
|
||||||
|
``javascript
|
||||||
|
import * as logging from 'eslib/logging';
|
||||||
|
import * as my_module from './my_module';
|
||||||
|
|
||||||
|
// create logger with 'spam_application'
|
||||||
|
var logger = logging.getLogger('spam_application');
|
||||||
|
logger.setLevel(logging.DEBUG);
|
||||||
|
|
||||||
|
// create file handler which logs even debug messages
|
||||||
|
var fh = logging.FileHandler('spam.log')
|
||||||
|
fh.setLevel(logging.DEBUG);
|
||||||
|
|
||||||
|
// create console handler with a higher log level
|
||||||
|
var ch = logging.StreamHandler();
|
||||||
|
ch.setLevel(logging.ERROR);
|
||||||
|
|
||||||
|
// create formatter and add it to the handlers
|
||||||
|
var formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s');
|
||||||
|
fh.setFormatter(formatter);
|
||||||
|
ch.setFormatter(formatter);
|
||||||
|
|
||||||
|
// add the handlers to the logger
|
||||||
|
logger.addHandler(fh);
|
||||||
|
logger.addHandler(ch);
|
||||||
|
|
||||||
|
logger.info('creating an instance of auxiliary_module.Auxiliary')
|
||||||
|
```
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
testEnvironment: "node",
|
testEnvironment: "node",
|
||||||
transform: {
|
transform: {
|
||||||
"^.+.tsx?$": ["ts-jest", {}]
|
"^.+.tsx?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsconfig: 'tsconfig.debug.json'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
roots: ['./tests']
|
roots: ['./tests']
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
package-lock.json
generated
54
package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"typedoc": "^0.27.7",
|
"typedoc": "^0.27.7",
|
||||||
|
"typedoc-plugin-pages": "^1.1.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
}
|
}
|
||||||
|
|
@ -1539,6 +1540,12 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/compare-versions": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -3724,6 +3731,28 @@
|
||||||
"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"
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedoc-default-themes": {
|
||||||
|
"version": "0.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.10.2.tgz",
|
||||||
|
"integrity": "sha512-zo09yRj+xwLFE3hyhJeVHWRSPuKEIAsFK5r2u47KL/HBKqpwdUSanoaz5L34IKiSATFrjG5ywmIu98hPVMfxZg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"lunr": "^2.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typedoc-plugin-pages": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedoc-plugin-pages/-/typedoc-plugin-pages-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-pmCCp3G2aCeEWb829dcVe9Pl+pI5OsaqfyrkEutcAHZi6IMVfQ5G5NdrkIkFCGhJU/DY04rGrVdynWqnaO5/jg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"compare-versions": "^3.6.0",
|
||||||
|
"typedoc-default-themes": "^0.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typedoc/node_modules/brace-expansion": {
|
"node_modules/typedoc/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
|
@ -5156,6 +5185,12 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"compare-versions": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -6767,6 +6802,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"typedoc-default-themes": {
|
||||||
|
"version": "0.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.10.2.tgz",
|
||||||
|
"integrity": "sha512-zo09yRj+xwLFE3hyhJeVHWRSPuKEIAsFK5r2u47KL/HBKqpwdUSanoaz5L34IKiSATFrjG5ywmIu98hPVMfxZg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"lunr": "^2.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typedoc-plugin-pages": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedoc-plugin-pages/-/typedoc-plugin-pages-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-pmCCp3G2aCeEWb829dcVe9Pl+pI5OsaqfyrkEutcAHZi6IMVfQ5G5NdrkIkFCGhJU/DY04rGrVdynWqnaO5/jg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"compare-versions": "^3.6.0",
|
||||||
|
"typedoc-default-themes": "^0.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "5.7.3",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"typedoc": "^0.27.7",
|
"typedoc": "^0.27.7",
|
||||||
|
"typedoc-plugin-pages": "^1.1.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
2
src/datetime.ts
Normal file
2
src/datetime.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export type MillisecondsSinceUnixEpoch = number;
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
export * as logging from './logging';
|
||||||
export * as logging from './logging'
|
export * as datetime from './datetime';
|
||||||
|
export * as error from './error';
|
||||||
|
|
|
||||||
1
src/logging/README.md
Normal file
1
src/logging/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
sds
|
||||||
1383
src/logging/index.ts
1383
src/logging/index.ts
File diff suppressed because it is too large
Load diff
951
src/logging/logger.ts
Normal file
951
src/logging/logger.ts
Normal file
|
|
@ -0,0 +1,951 @@
|
||||||
|
/**
|
||||||
|
* This module defines functions and classes which implement a flexible event
|
||||||
|
* logging system for applications and libraries.
|
||||||
|
*
|
||||||
|
* The key benefit of having the logging API provided by a standard library
|
||||||
|
* module is that all Python modules can participate in logging, so your
|
||||||
|
* application log can include your own messages integrated with messages from
|
||||||
|
* third-party modules.
|
||||||
|
*
|
||||||
|
* Here’s a simple example of idiomatic usage:
|
||||||
|
*
|
||||||
|
* ```javascript
|
||||||
|
* // myapp.ts
|
||||||
|
* import * as logging from 'eslib/logging';
|
||||||
|
* import * as mylib from './mylib';
|
||||||
|
* const logger = logging.getLogger(__name__);
|
||||||
|
*
|
||||||
|
* function main() {
|
||||||
|
* logging.basicConfig({filename: 'myapp.log', level: logging.INFO});
|
||||||
|
* logger.info('Started');
|
||||||
|
* mylib.doSomething();
|
||||||
|
* logger.info('Finished');
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* main();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```javascript
|
||||||
|
* // mylib.ts
|
||||||
|
* import * as logging from 'eslib/logging';
|
||||||
|
* const logger = logging.getLogger(__name__);
|
||||||
|
*
|
||||||
|
* function do_something() {
|
||||||
|
* logger.info('Doing something')
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If you run `myapp.ts`, you should see this in myapp.log:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* INFO:__main__:Started
|
||||||
|
* INFO:mylib:Doing something
|
||||||
|
* INFO:__main__:Finished
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The key feature of this idiomatic usage is that the majority of code is
|
||||||
|
* simply creating a module level logger with `getLogger(__name__)`, and using
|
||||||
|
* that logger to do any needed logging. This is concise, while allowing
|
||||||
|
* downstream code fine-grained control if needed. Logged messages to the
|
||||||
|
* module-level logger get forwarded to handlers of loggers in higher-level
|
||||||
|
* modules, all the way up to the highest-level logger known as the root logger;
|
||||||
|
* this approach is known as hierarchical logging.
|
||||||
|
*
|
||||||
|
* For logging to be useful, it needs to be configured: setting the levels and
|
||||||
|
* destinations for each logger, potentially changing how specific modules log,
|
||||||
|
* often based on command-line arguments or application configuration. In most
|
||||||
|
* cases, like the one above, only the root logger needs to be so configured,
|
||||||
|
* since all the lower level loggers at module level eventually forward their
|
||||||
|
* messages to its handlers. basicConfig() provides a quick way to configure the
|
||||||
|
* root logger that handles many use cases.
|
||||||
|
*
|
||||||
|
* The module provides a lot of functionality and flexibility. If you are
|
||||||
|
* unfamiliar with logging, the best way to get to grips with it is to view the
|
||||||
|
* tutorials (see the links above and on the right).
|
||||||
|
*
|
||||||
|
* The basic classes defined by the module, together with their attributes and
|
||||||
|
* methods, are listed in the sections below.
|
||||||
|
*
|
||||||
|
* * Loggers expose the interface that application code directly uses.
|
||||||
|
* * Handlers send the log records (created by loggers) to the appropriate
|
||||||
|
* destination.
|
||||||
|
* * Filters provide a finer grained facility for determining which log records
|
||||||
|
* to output.
|
||||||
|
* * Formatters specify the layout of log records in the final output.
|
||||||
|
* @module logging
|
||||||
|
*/
|
||||||
|
import { NotImplementedError, MyError, ValueError, KeyError, StackTrace } from '../error';
|
||||||
|
|
||||||
|
export const CRITICAL = 50;
|
||||||
|
export const FATAL = CRITICAL;
|
||||||
|
export const ERROR = 40;
|
||||||
|
export const WARNING = 30;
|
||||||
|
export const WARN = WARNING;
|
||||||
|
export const INFO = 20;
|
||||||
|
export const DEBUG = 10;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogLevel = number;
|
||||||
|
|
||||||
|
export type ExecutionInfo = [string, Error, StackTrace];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* context of a logging event/trigger
|
||||||
|
*/
|
||||||
|
export interface LogOptions{
|
||||||
|
excInfo: ExecutionInfo|Error|null,
|
||||||
|
extra: {[key: string]: any}|null,
|
||||||
|
stackInfo: boolean,
|
||||||
|
stackLevel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* options for instantiating a new log record
|
||||||
|
*/
|
||||||
|
export interface LogRecordOptions {
|
||||||
|
level: number,
|
||||||
|
file?: string,
|
||||||
|
lno?: number,
|
||||||
|
msg: string,
|
||||||
|
args?: any[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogRecordFactory = { (name: string, options: LogRecordOptions): LogRecord };
|
||||||
|
|
||||||
|
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 type MillisecondsSinceUnixEpoch = number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 type FilterCallable = (record: LogRecord) => boolean;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULTLOGOPTIONS: LogOptions = {
|
||||||
|
excInfo: null,
|
||||||
|
extra: null,
|
||||||
|
stackInfo: false,
|
||||||
|
stackLevel: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoggerClass = { new(): Logger };
|
||||||
|
|
||||||
|
var throwErrors: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
public readonly manager: Manager|null = null;
|
||||||
|
public readonly parent_: Logger|null = null;
|
||||||
|
public readonly propagate: boolean = true;
|
||||||
|
public readonly handlers: any[] = [];
|
||||||
|
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, manager?: Manager) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.scope = scope;
|
||||||
|
|
||||||
|
this._level = checkLevel(level ?? NOTSET);
|
||||||
|
|
||||||
|
this.manager = manager ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get level() { return this._level }
|
||||||
|
|
||||||
|
public set level(level: LogLevel) { this._level = checkLevel(level) }
|
||||||
|
|
||||||
|
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 ?? DEFAULTLOGOPTIONS;
|
||||||
|
options = { ...DEFAULTLOGOPTIONS, ...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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, manager?: Manager) {
|
||||||
|
super('root', level, manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loggerClass = Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 rootLogger: 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(logger: RootLogger) {
|
||||||
|
this.rootLogger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, this)
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET, this)
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 basicFormat = '%(level)s:%(name)s:%(message)s';
|
||||||
|
|
||||||
|
const styles: {[key: string]: [{ new(options: PercentFormatterStyleOptions): PercentFormatterStyle}, string]} = {
|
||||||
|
'%': [PercentFormatterStyle, basicFormat],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormatterOptions {
|
||||||
|
fmt?: string
|
||||||
|
datefmt?: any
|
||||||
|
style?: string
|
||||||
|
validate?: boolean
|
||||||
|
defaults?: {[key: string]: any}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 handlers: {[key: string]: Handler} = {}; // map of handler names to
|
||||||
|
// handlers
|
||||||
|
const handlerList: WeakRef<Handler>[] = []; // added to allow handlers to be removed in reverse
|
||||||
|
// order of initialization
|
||||||
|
|
||||||
|
function addHandlerRef(handler: Handler) {
|
||||||
|
handlerList.push(new WeakRef(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFormatter = new Formatter();
|
||||||
|
|
||||||
|
export class Handler extends Filterer {
|
||||||
|
|
||||||
|
protected _scope: string|null = null;
|
||||||
|
protected formatter: Formatter|null = null;
|
||||||
|
protected _level: number;
|
||||||
|
protected _closed: boolean = false;
|
||||||
|
|
||||||
|
constructor(level: LogLevel) {
|
||||||
|
super();
|
||||||
|
this._level = checkLevel(level);
|
||||||
|
// Add the handler to the global handlerList (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 = defaultFormatter }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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...'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StderrHandler extends Handler {
|
||||||
|
constructor(level: LogLevel) {
|
||||||
|
super(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLastResort = new StderrHandler(WARNING);
|
||||||
|
export var lastResort = defaultLastResort;
|
||||||
2
src/stream.ts
Normal file
2
src/stream.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export type MillisecondsSinceUnixEpoch = number;
|
||||||
|
|
@ -1,7 +1,84 @@
|
||||||
import * as logging from '../src/logging';
|
import {expect, jest, test} from '@jest/globals';
|
||||||
|
|
||||||
describe('Logger', () => {
|
describe('Logger', () => {
|
||||||
it('can be instantiated', () => {
|
it('can be instantiated', () => {
|
||||||
//const logger = new logging.Logger('test', 0);
|
//const logger = new logging.Logger('test', 0);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getLevelName', () => {
|
||||||
|
var logging: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logging = require('../src/logging');
|
||||||
|
});
|
||||||
|
|
||||||
|
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/logging');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('numeric to textual representation of built-ins', () => {
|
||||||
|
logging.addLevelName(80, 'FOOBAR');
|
||||||
|
expect(logging.getLevelName(80)).toBe('FOOBAR');
|
||||||
|
expect(logging.getLevelName('FOOBAR')).toBe(80);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"outDir": "build/production",
|
"outDir": "build/production",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": { "*": ["node/modules/*"] },
|
"paths": { "*": ["node/modules/*"] },
|
||||||
"lib": ["esnext.weakref"]
|
"lib": ["esnext.weakref", "dom"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.ts"
|
||||||
|
|
|
||||||
4
typedoc.json
Normal file
4
typedoc.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"out": "build/doc",
|
||||||
|
"readme": "README.md"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue