Merge branch 'feature/8' into dev
This commit is contained in:
commit
1d5ded82ba
4 changed files with 246 additions and 55 deletions
2
TODO
2
TODO
|
|
@ -121,7 +121,7 @@ Content-Type: application/issue
|
||||||
ID: 8
|
ID: 8
|
||||||
Type: feature
|
Type: feature
|
||||||
Title: implement browser-compatible Handler output
|
Title: implement browser-compatible Handler output
|
||||||
Status: in-progress
|
Status: done
|
||||||
Priority: high
|
Priority: high
|
||||||
Created: 2026-03-13
|
Created: 2026-03-13
|
||||||
Relationships: dependsOn:5
|
Relationships: dependsOn:5
|
||||||
|
|
|
||||||
119
src/handler.ts
119
src/handler.ts
|
|
@ -1,17 +1,9 @@
|
||||||
import * as stream from 'stream';
|
import { LogLevel, checkLevel, NOTSET, WARNING, ERROR } from './log-level';
|
||||||
|
|
||||||
import { LogLevel, checkLevel, NOTSET } from './log-level';
|
|
||||||
import { LogRecord } from './log-record';
|
import { LogRecord } from './log-record';
|
||||||
import { Formatter, DEFAULT_FORMATTER } from './formatter';
|
import { Formatter, DEFAULT_FORMATTER } from './formatter';
|
||||||
import { Filterer } from './filter';
|
import { Filterer } from './filter';
|
||||||
import { NotImplementedError } from './helper/error';
|
import { NotImplementedError } from './helper/error';
|
||||||
|
import { Writable, DEFAULT_STREAM, StderrWritable } from './helper/stream';
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
const stream = require('stream');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const stream = require('./helper/stream');
|
|
||||||
}
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
// Handler classes and functions
|
// Handler classes and functions
|
||||||
|
|
@ -115,14 +107,13 @@ export class Handler extends Filterer {
|
||||||
* I/O thread lock.
|
* I/O thread lock.
|
||||||
*/
|
*/
|
||||||
handle(record: LogRecord) {
|
handle(record: LogRecord) {
|
||||||
var rv = this.filter(record);
|
const rv = this.filter(record);
|
||||||
|
if (!rv) { return }
|
||||||
|
let filtered = record;
|
||||||
if ((rv as any) instanceof LogRecord) {
|
if ((rv as any) instanceof LogRecord) {
|
||||||
record = rv as unknown as LogRecord
|
filtered = rv as unknown as LogRecord
|
||||||
}
|
|
||||||
if (rv) {
|
|
||||||
//locking here
|
|
||||||
this.emit(record)
|
|
||||||
}
|
}
|
||||||
|
this.emit(filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,23 +135,73 @@ export class Handler extends Filterer {
|
||||||
* Handle errors which occur during an emit() call.
|
* Handle errors which occur during an emit() call.
|
||||||
*
|
*
|
||||||
* This method should be called from handlers when an exception is
|
* This method should be called from handlers when an exception is
|
||||||
* encountered during an emit() call. If raiseExceptions is false,
|
* encountered during an emit() call.
|
||||||
* 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) {
|
handleError(record: LogRecord) {
|
||||||
throw new NotImplementedError(
|
try {
|
||||||
'still need to find portable way for stacktracing...'
|
console.error('--- Logging error ---');
|
||||||
)
|
console.error('Error in handler for record:', record.scope, record.msg);
|
||||||
|
}
|
||||||
|
catch (_) {
|
||||||
|
// silently ignore errors in error handling
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get formatter(): Formatter|null { return this._formatter }
|
get formatter(): Formatter|null { return this._formatter }
|
||||||
set formatter(fmt: Formatter) { this._formatter = fmt }
|
set formatter(fmt: Formatter) { this._formatter = fmt }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler class which writes logging records, appropriately formatted,
|
||||||
|
* to a stream. Note that this class does not close the stream, as
|
||||||
|
* the default stream may be shared.
|
||||||
|
*/
|
||||||
|
export class StreamHandler extends Handler {
|
||||||
|
protected stream: Writable;
|
||||||
|
|
||||||
|
constructor(stream?: Writable) {
|
||||||
|
super();
|
||||||
|
this.stream = stream ?? DEFAULT_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(record: LogRecord) {
|
||||||
|
try {
|
||||||
|
const msg = this.format(record);
|
||||||
|
this.stream.write(msg);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.handleError(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler class which writes logging records to the browser console,
|
||||||
|
* mapping log levels to the appropriate console method.
|
||||||
|
*
|
||||||
|
* This is the primary handler for browser environments.
|
||||||
|
*/
|
||||||
|
export class ConsoleHandler extends Handler {
|
||||||
|
emit(record: LogRecord) {
|
||||||
|
try {
|
||||||
|
const msg = this.format(record);
|
||||||
|
|
||||||
|
if (record.levelno >= ERROR) {
|
||||||
|
console.error(msg);
|
||||||
|
}
|
||||||
|
else if (record.levelno >= WARNING) {
|
||||||
|
console.warn(msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.handleError(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileHandlerOptions {
|
export interface FileHandlerOptions {
|
||||||
filename: string
|
filename: string
|
||||||
filemode?: string
|
filemode?: string
|
||||||
|
|
@ -168,20 +209,13 @@ export interface FileHandlerOptions {
|
||||||
errors?: 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 {
|
export class FileHandler extends StreamHandler {
|
||||||
constructor(options: FileHandlerOptions) {
|
constructor(options: FileHandlerOptions) {
|
||||||
super();
|
super();
|
||||||
|
throw new NotImplementedError(
|
||||||
|
'FileHandler is not available in browser environments. ' +
|
||||||
|
'Use ConsoleHandler or a storage-backed handler instead.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,8 +225,15 @@ export class FileHandler extends StreamHandler {
|
||||||
* sys.stderr at handler construction time.
|
* sys.stderr at handler construction time.
|
||||||
*/
|
*/
|
||||||
export class StderrHandler extends Handler {
|
export class StderrHandler extends Handler {
|
||||||
/**
|
|
||||||
* Initialize the handler.
|
|
||||||
*/
|
|
||||||
constructor(level: LogLevel) { super(level) }
|
constructor(level: LogLevel) { super(level) }
|
||||||
|
|
||||||
|
emit(record: LogRecord) {
|
||||||
|
try {
|
||||||
|
const msg = this.format(record);
|
||||||
|
console.error(msg);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.handleError(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Minimal writable stream interface for browser compatibility.
|
||||||
|
*
|
||||||
|
* This abstracts over Node.js streams and browser console output,
|
||||||
|
* providing a common write target for handlers.
|
||||||
|
*/
|
||||||
|
export interface Writable {
|
||||||
|
write(data: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
export type MillisecondsSinceUnixEpoch = number;
|
/**
|
||||||
|
* A Writable backed by console.log.
|
||||||
|
*/
|
||||||
|
export class ConsoleWritable implements Writable {
|
||||||
|
write(data: string): void {
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Writable backed by console.error (stderr equivalent).
|
||||||
|
*/
|
||||||
|
export class StderrWritable implements Writable {
|
||||||
|
write(data: string): void {
|
||||||
|
console.error(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default output stream. Uses console.error to match Python's default
|
||||||
|
* of writing to sys.stderr.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_STREAM: Writable = new StderrWritable();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
import {expect, jest, test} from '@jest/globals';
|
import {expect, jest, test} from '@jest/globals';
|
||||||
import { Handler, StreamHandler, StderrHandler } from '../../src/handler';
|
import {
|
||||||
|
Handler,
|
||||||
|
StreamHandler,
|
||||||
|
ConsoleHandler,
|
||||||
|
StderrHandler,
|
||||||
|
FileHandler,
|
||||||
|
} from '../../src/handler';
|
||||||
import { Formatter, DEFAULT_FORMATTER } from '../../src/formatter';
|
import { Formatter, DEFAULT_FORMATTER } from '../../src/formatter';
|
||||||
import { LogRecord } from '../../src/log-record';
|
import { LogRecord } from '../../src/log-record';
|
||||||
import { DEBUG, WARNING, ERROR, NOTSET } from '../../src/log-level';
|
import { Writable } from '../../src/helper/stream';
|
||||||
|
import { DEBUG, INFO, WARNING, ERROR, CRITICAL, NOTSET } from '../../src/log-level';
|
||||||
|
|
||||||
function makeRecord(level: number, msg: string): LogRecord {
|
function makeRecord(level: number, msg: string): LogRecord {
|
||||||
return new LogRecord('test', { level, msg });
|
return new LogRecord('test', { level, msg });
|
||||||
}
|
}
|
||||||
|
|
@ -50,15 +58,6 @@ describe('Handler', () => {
|
||||||
expect(typeof result).toBe('string');
|
expect(typeof result).toBe('string');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uses assigned formatter', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
const fmt = new Formatter({ fmt: '%(message)s' });
|
|
||||||
h.formatter = fmt;
|
|
||||||
const record = makeRecord(DEBUG, 'hello');
|
|
||||||
const result = h.format(record);
|
|
||||||
expect(typeof result).toBe('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns a string', () => {
|
test('returns a string', () => {
|
||||||
const h = new Handler();
|
const h = new Handler();
|
||||||
const record = makeRecord(WARNING, 'test message');
|
const record = makeRecord(WARNING, 'test message');
|
||||||
|
|
@ -82,6 +81,103 @@ describe('Handler', () => {
|
||||||
expect(h.closed).toBe(true);
|
expect(h.closed).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleError', () => {
|
||||||
|
test('does not throw', () => {
|
||||||
|
const h = new Handler();
|
||||||
|
const record = makeRecord(DEBUG, 'test');
|
||||||
|
expect(() => h.handleError(record)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('StreamHandler', () => {
|
||||||
|
test('writes formatted output to the stream', () => {
|
||||||
|
const written: string[] = [];
|
||||||
|
const stream: Writable = { write: (data: string) => { written.push(data) } };
|
||||||
|
const h = new StreamHandler(stream);
|
||||||
|
const record = makeRecord(WARNING, 'stream test');
|
||||||
|
h.emit(record);
|
||||||
|
expect(written.length).toBe(1);
|
||||||
|
expect(written[0]).toContain('stream test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses default stream when none provided', () => {
|
||||||
|
const h = new StreamHandler();
|
||||||
|
const record = makeRecord(DEBUG, 'default stream');
|
||||||
|
expect(() => h.emit(record)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls handleError on emit failure', () => {
|
||||||
|
const stream: Writable = {
|
||||||
|
write: () => { throw new Error('write failed') }
|
||||||
|
};
|
||||||
|
const h = new StreamHandler(stream);
|
||||||
|
const record = makeRecord(DEBUG, 'fail');
|
||||||
|
expect(() => h.emit(record)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ConsoleHandler', () => {
|
||||||
|
let origLog: typeof console.log;
|
||||||
|
let origWarn: typeof console.warn;
|
||||||
|
let origError: typeof console.error;
|
||||||
|
let logged: string[];
|
||||||
|
let warned: string[];
|
||||||
|
let errored: string[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logged = [];
|
||||||
|
warned = [];
|
||||||
|
errored = [];
|
||||||
|
origLog = console.log;
|
||||||
|
origWarn = console.warn;
|
||||||
|
origError = console.error;
|
||||||
|
console.log = (...args: any[]) => { logged.push(args.join(' ')) };
|
||||||
|
console.warn = (...args: any[]) => { warned.push(args.join(' ')) };
|
||||||
|
console.error = (...args: any[]) => { errored.push(args.join(' ')) };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
console.log = origLog;
|
||||||
|
console.warn = origWarn;
|
||||||
|
console.error = origError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses console.error for ERROR level', () => {
|
||||||
|
const h = new ConsoleHandler();
|
||||||
|
h.emit(makeRecord(ERROR, 'error msg'));
|
||||||
|
expect(errored.length).toBe(1);
|
||||||
|
expect(errored[0]).toContain('error msg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses console.error for CRITICAL level', () => {
|
||||||
|
const h = new ConsoleHandler();
|
||||||
|
h.emit(makeRecord(CRITICAL, 'critical msg'));
|
||||||
|
expect(errored.length).toBe(1);
|
||||||
|
expect(errored[0]).toContain('critical msg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses console.warn for WARNING level', () => {
|
||||||
|
const h = new ConsoleHandler();
|
||||||
|
h.emit(makeRecord(WARNING, 'warn msg'));
|
||||||
|
expect(warned.length).toBe(1);
|
||||||
|
expect(warned[0]).toContain('warn msg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses console.log for INFO level', () => {
|
||||||
|
const h = new ConsoleHandler();
|
||||||
|
h.emit(makeRecord(INFO, 'info msg'));
|
||||||
|
expect(logged.length).toBe(1);
|
||||||
|
expect(logged[0]).toContain('info msg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses console.log for DEBUG level', () => {
|
||||||
|
const h = new ConsoleHandler();
|
||||||
|
h.emit(makeRecord(DEBUG, 'debug msg'));
|
||||||
|
expect(logged.length).toBe(1);
|
||||||
|
expect(logged[0]).toContain('debug msg');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('StderrHandler', () => {
|
describe('StderrHandler', () => {
|
||||||
|
|
@ -89,4 +185,27 @@ describe('StderrHandler', () => {
|
||||||
const h = new StderrHandler(ERROR);
|
const h = new StderrHandler(ERROR);
|
||||||
expect(h.level).toBe(ERROR);
|
expect(h.level).toBe(ERROR);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('emits via console.error', () => {
|
||||||
|
const errored: string[] = [];
|
||||||
|
const origError = console.error;
|
||||||
|
console.error = (...args: any[]) => { errored.push(args.join(' ')) };
|
||||||
|
try {
|
||||||
|
const h = new StderrHandler(DEBUG);
|
||||||
|
h.emit(makeRecord(WARNING, 'stderr test'));
|
||||||
|
expect(errored.length).toBe(1);
|
||||||
|
expect(errored[0]).toContain('stderr test');
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
console.error = origError;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FileHandler', () => {
|
||||||
|
test('throws NotImplementedError on construction', () => {
|
||||||
|
expect(() => new FileHandler({
|
||||||
|
filename: 'test.log'
|
||||||
|
})).toThrow('not available in browser');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue