Compare commits
13 Commits
90d7798791
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e8ee446d1 | |||
| bf96303bf9 | |||
| 1546973475 | |||
| 5e167b7fb4 | |||
| ebcd7880fb | |||
| 70233cc941 | |||
| 66568b5e0f | |||
| ff0f797d20 | |||
| 9ebac95c27 | |||
| 7bb80989c4 | |||
| a6aff1a5b9 | |||
| c957150879 | |||
| f405c0bc5a |
+1
-1
@@ -3,7 +3,7 @@ import tsParser from '@typescript-eslint/parser'
|
|||||||
import eslintJs from '@eslint/js'
|
import eslintJs from '@eslint/js'
|
||||||
import eslintTs from 'typescript-eslint'
|
import eslintTs from 'typescript-eslint'
|
||||||
|
|
||||||
const tsFiles = ['src/**/*.ts']
|
const tsFiles = ['src/**/*.ts', 'test/**/*.ts']
|
||||||
|
|
||||||
const customTypescriptConfig = {
|
const customTypescriptConfig = {
|
||||||
files: tsFiles,
|
files: tsFiles,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@jdbernard/logging",
|
"name": "@jdbernard/logging",
|
||||||
"version": "2.3.3",
|
"version": "2.5.0",
|
||||||
"description": "Simple Javascript logging service.",
|
"description": "Simple Javascript logging service.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -1,21 +1,78 @@
|
|||||||
import type { LogAppender } from './log-appender'
|
import type { LogAppender } from './log-appender'
|
||||||
import { LogLevel, type LogMessage } from './log-message'
|
import { LogLevel, type LogMessage } from './log-message'
|
||||||
|
import { clamp } from './util'
|
||||||
|
|
||||||
|
export interface BufferSizeBounds {
|
||||||
|
target: number
|
||||||
|
max: number
|
||||||
|
}
|
||||||
|
|
||||||
export class BufferLogAppender implements LogAppender {
|
export class BufferLogAppender implements LogAppender {
|
||||||
public threshold: LogLevel
|
public threshold: LogLevel
|
||||||
public buffer: LogMessage[]
|
public buffer: LogMessage[]
|
||||||
|
|
||||||
constructor(buffer?: LogMessage[], threshold?: LogLevel) {
|
private _size?: BufferSizeBounds
|
||||||
this.buffer = buffer ?? []
|
|
||||||
|
public get size() {
|
||||||
|
return this._size === undefined ? undefined : { ...this._size }
|
||||||
|
}
|
||||||
|
|
||||||
|
public set size(s: {target: number, max?: number} | undefined) {
|
||||||
|
if (s === undefined) {
|
||||||
|
this._size = undefined
|
||||||
|
} else {
|
||||||
|
const target = clamp(s.target, { min: 1 })
|
||||||
|
this._size = {
|
||||||
|
target,
|
||||||
|
max: s.max === undefined
|
||||||
|
? clamp(target * 1.2, { min: target + 1 })
|
||||||
|
: clamp(s.max, { min: target + 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(threshold?: LogLevel, size?: {target: number, max?: number}) {
|
||||||
|
if (size !== undefined && typeof size === 'object') {
|
||||||
|
this.size = size
|
||||||
|
} else {
|
||||||
|
this._size = { target: 1000, max: 1200 }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buffer = []
|
||||||
this.threshold = threshold ?? LogLevel.ALL
|
this.threshold = threshold ?? LogLevel.ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
public appendMessage(msg: LogMessage): void {
|
public appendMessage(msg: LogMessage): void {
|
||||||
if (this.threshold && msg.level < this.threshold) return
|
if (this.threshold && msg.level < this.threshold) return
|
||||||
else this.buffer.push(msg)
|
|
||||||
|
this.buffer.push(msg)
|
||||||
|
|
||||||
|
if (this._size !== undefined) {
|
||||||
|
|
||||||
|
if (this._size.max <= this._size.target) {
|
||||||
|
// This should be impossible, so if it happens, I want to know.
|
||||||
|
const oldSize = this.size
|
||||||
|
this.size = { target: this._size.target }
|
||||||
|
this.buffer.push({
|
||||||
|
scope: '@jdbernard/js-logging/buffer-log-appender.ts',
|
||||||
|
level: LogLevel.ERROR,
|
||||||
|
msg: {
|
||||||
|
msg: 'BufferLogAppender misconfigured: max size was not greater ' +
|
||||||
|
'than target size. Reconfiguring.',
|
||||||
|
oldSize,
|
||||||
|
newSize: this.size
|
||||||
|
},
|
||||||
|
ts: new Date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.buffer.length > this._size.max) {
|
||||||
|
this.buffer = this.buffer.slice(-this._size.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearBuffer(): void {
|
public clearBuffer(): void {
|
||||||
this.buffer.length = 0
|
this.buffer = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -10,3 +10,14 @@ export function omit(
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clamp(
|
||||||
|
val: number,
|
||||||
|
bounds?: { min?: number, max?: number},
|
||||||
|
allowFloats?: boolean ): number {
|
||||||
|
let clamped = val
|
||||||
|
if (!allowFloats) clamped = Math.floor(clamped)
|
||||||
|
if (bounds?.min !== undefined) clamped = Math.max(bounds?.min, clamped)
|
||||||
|
if (bounds?.max !== undefined) clamped = Math.min(bounds?.max, clamped)
|
||||||
|
return clamped
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,74 +1,227 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
import { describe, expect, test } from 'bun:test'
|
||||||
import { BufferLogAppender, LogLevel, LogMessage } from "../src";
|
import {
|
||||||
|
BufferLogAppender,
|
||||||
|
LogLevel,
|
||||||
|
type LogMessage,
|
||||||
|
} from '../src'
|
||||||
|
|
||||||
function makeMsg(overrides: Partial<LogMessage> = {}): LogMessage {
|
function makeMsg(overrides: Partial<LogMessage> = {}): LogMessage {
|
||||||
return {
|
return {
|
||||||
scope: "test-scope",
|
scope: 'test-scope',
|
||||||
level: LogLevel.INFO,
|
level: LogLevel.INFO,
|
||||||
msg: "test message",
|
msg: 'test message',
|
||||||
stacktrace: "",
|
stacktrace: '',
|
||||||
ts: new Date(),
|
ts: new Date(),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("BufferLogAppender", () => {
|
describe('BufferLogAppender', () => {
|
||||||
test("defaults to empty buffer and ALL threshold", () => {
|
test('defaults to empty buffer, ALL threshold, and bounded size', () => {
|
||||||
const appender = new BufferLogAppender();
|
const appender = new BufferLogAppender()
|
||||||
|
|
||||||
expect(appender.buffer).toEqual([]);
|
expect(appender.buffer).toEqual([])
|
||||||
expect(appender.threshold).toBe(LogLevel.ALL);
|
expect(appender.threshold).toBe(LogLevel.ALL)
|
||||||
});
|
expect(appender.size).toEqual({ target: 1000, max: 1200 })
|
||||||
|
})
|
||||||
|
|
||||||
test("accepts initial buffer", () => {
|
test('accepts threshold and size constructor options', () => {
|
||||||
const existing: LogMessage[] = [makeMsg({ msg: "pre-existing" })];
|
const appender = new BufferLogAppender(LogLevel.WARN, {
|
||||||
const appender = new BufferLogAppender(existing);
|
target: 20,
|
||||||
|
max: 30,
|
||||||
|
})
|
||||||
|
|
||||||
expect(appender.buffer.length).toBe(1);
|
expect(appender.threshold).toBe(LogLevel.WARN)
|
||||||
expect(appender.buffer[0].msg).toBe("pre-existing");
|
expect(appender.size).toEqual({ target: 20, max: 30 })
|
||||||
});
|
})
|
||||||
|
|
||||||
test("appends messages to buffer", () => {
|
test('appends messages to buffer', () => {
|
||||||
const appender = new BufferLogAppender();
|
const appender = new BufferLogAppender()
|
||||||
|
|
||||||
appender.appendMessage(makeMsg({ msg: "first" }));
|
appender.appendMessage(makeMsg({ msg: 'first' }))
|
||||||
appender.appendMessage(makeMsg({ msg: "second" }));
|
appender.appendMessage(makeMsg({ msg: 'second' }))
|
||||||
|
|
||||||
expect(appender.buffer.length).toBe(2);
|
expect(appender.buffer.length).toBe(2)
|
||||||
expect(appender.buffer[0].msg).toBe("first");
|
expect(appender.buffer[0].msg).toBe('first')
|
||||||
expect(appender.buffer[1].msg).toBe("second");
|
expect(appender.buffer[1].msg).toBe('second')
|
||||||
});
|
})
|
||||||
|
|
||||||
test("respects threshold", () => {
|
test('respects threshold', () => {
|
||||||
const appender = new BufferLogAppender(undefined, LogLevel.WARN);
|
const appender = new BufferLogAppender(LogLevel.WARN)
|
||||||
|
|
||||||
appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: "dropped" }));
|
appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: 'dropped' }))
|
||||||
appender.appendMessage(makeMsg({ level: LogLevel.WARN, msg: "kept" }));
|
appender.appendMessage(makeMsg({ level: LogLevel.WARN, msg: 'kept' }))
|
||||||
appender.appendMessage(makeMsg({ level: LogLevel.ERROR, msg: "also kept" }));
|
appender.appendMessage(makeMsg({ level: LogLevel.ERROR, msg: 'also kept' }))
|
||||||
|
|
||||||
expect(appender.buffer.length).toBe(2);
|
expect(appender.buffer.length).toBe(2)
|
||||||
expect(appender.buffer[0].msg).toBe("kept");
|
expect(appender.buffer[0].msg).toBe('kept')
|
||||||
expect(appender.buffer[1].msg).toBe("also kept");
|
expect(appender.buffer[1].msg).toBe('also kept')
|
||||||
});
|
})
|
||||||
|
|
||||||
test("threshold of ALL (0) does not cause falsy check to drop messages", () => {
|
test('threshold of ALL does not drop messages', () => {
|
||||||
const appender = new BufferLogAppender(undefined, LogLevel.ALL);
|
const appender = new BufferLogAppender(LogLevel.ALL)
|
||||||
|
|
||||||
appender.appendMessage(makeMsg({ level: LogLevel.ALL, msg: "level all" }));
|
appender.appendMessage(makeMsg({ level: LogLevel.ALL, msg: 'level all' }))
|
||||||
appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: "level info" }));
|
appender.appendMessage(makeMsg({ level: LogLevel.INFO, msg: 'level info' }))
|
||||||
|
|
||||||
// Both should be kept — ALL (0) should not be treated as falsy
|
expect(appender.buffer.length).toBe(2)
|
||||||
expect(appender.buffer.length).toBe(2);
|
expect(appender.buffer[0].msg).toBe('level all')
|
||||||
expect(appender.buffer[0].msg).toBe("level all");
|
expect(appender.buffer[1].msg).toBe('level info')
|
||||||
expect(appender.buffer[1].msg).toBe("level info");
|
})
|
||||||
});
|
|
||||||
|
|
||||||
test("clearBuffer empties the buffer", () => {
|
test('clearBuffer empties the buffer and reassigns the array', () => {
|
||||||
const appender = new BufferLogAppender();
|
const appender = new BufferLogAppender()
|
||||||
appender.appendMessage(makeMsg({ msg: "to clear" }));
|
appender.appendMessage(makeMsg({ msg: 'to clear' }))
|
||||||
expect(appender.buffer.length).toBe(1);
|
const existing = appender.buffer
|
||||||
|
|
||||||
appender.clearBuffer();
|
appender.clearBuffer()
|
||||||
expect(appender.buffer.length).toBe(0);
|
|
||||||
});
|
expect(appender.buffer).toEqual([])
|
||||||
});
|
expect(appender.buffer).not.toBe(existing)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('size bounds', () => {
|
||||||
|
test('derives max as 120 percent of target by default', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, { target: 20 })
|
||||||
|
|
||||||
|
expect(appender.size).toEqual({ target: 20, max: 24 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps max at least one greater than target', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 20,
|
||||||
|
max: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(appender.size).toEqual({ target: 20, max: 21 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizes fractional and too-small target values', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, { target: 0.5 })
|
||||||
|
|
||||||
|
expect(appender.size).toEqual({ target: 1, max: 2 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizes fractional max values', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 3,
|
||||||
|
max: 4.9,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(appender.size).toEqual({ target: 3, max: 4 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can be disabled for unbounded buffering', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 2,
|
||||||
|
max: 3,
|
||||||
|
})
|
||||||
|
appender.size = undefined
|
||||||
|
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
appender.appendMessage(makeMsg({ msg: `message ${i}` }))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(appender.size).toBeUndefined()
|
||||||
|
expect(appender.buffer.map((message) => message.msg)).toEqual([
|
||||||
|
'message 1',
|
||||||
|
'message 2',
|
||||||
|
'message 3',
|
||||||
|
'message 4',
|
||||||
|
'message 5',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns size as a defensive copy', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 3,
|
||||||
|
max: 4,
|
||||||
|
})
|
||||||
|
const size = appender.size
|
||||||
|
|
||||||
|
if (size !== undefined) {
|
||||||
|
size.max = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(appender.size).toEqual({ target: 3, max: 4 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buffer trimming', () => {
|
||||||
|
test('allows buffer to grow until max is exceeded', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 3,
|
||||||
|
max: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
appender.appendMessage(makeMsg({ msg: `message ${i}` }))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(appender.buffer.map((message) => message.msg)).toEqual([
|
||||||
|
'message 1',
|
||||||
|
'message 2',
|
||||||
|
'message 3',
|
||||||
|
'message 4',
|
||||||
|
'message 5',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('batch trims to target when max is exceeded', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 3,
|
||||||
|
max: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 1; i <= 6; i++) {
|
||||||
|
appender.appendMessage(makeMsg({ msg: `message ${i}` }))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(appender.buffer.map((message) => message.msg)).toEqual([
|
||||||
|
'message 4',
|
||||||
|
'message 5',
|
||||||
|
'message 6',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reassigns buffer array when trimming', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 2,
|
||||||
|
max: 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
appender.appendMessage(makeMsg({ msg: 'first' }))
|
||||||
|
appender.appendMessage(makeMsg({ msg: 'second' }))
|
||||||
|
appender.appendMessage(makeMsg({ msg: 'third' }))
|
||||||
|
const existing = appender.buffer
|
||||||
|
appender.appendMessage(makeMsg({ msg: 'fourth' }))
|
||||||
|
|
||||||
|
expect(appender.buffer).not.toBe(existing)
|
||||||
|
expect(appender.buffer.map((message) => message.msg)).toEqual([
|
||||||
|
'third',
|
||||||
|
'fourth',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('repairs invalid internal bounds before trimming', () => {
|
||||||
|
const appender = new BufferLogAppender(LogLevel.ALL, {
|
||||||
|
target: 3,
|
||||||
|
max: 4,
|
||||||
|
})
|
||||||
|
const unsafeAppender = appender as unknown as {
|
||||||
|
_size: { target: number, max: number }
|
||||||
|
}
|
||||||
|
unsafeAppender._size = { target: 3, max: 2 }
|
||||||
|
|
||||||
|
appender.appendMessage(makeMsg({ msg: 'first' }))
|
||||||
|
|
||||||
|
expect(appender.size).toEqual({ target: 3, max: 4 })
|
||||||
|
expect(appender.buffer.length).toBe(2)
|
||||||
|
expect(appender.buffer[1].level).toBe(LogLevel.ERROR)
|
||||||
|
expect(appender.buffer[1].msg).toMatchObject({
|
||||||
|
oldSize: { target: 3, max: 2 },
|
||||||
|
newSize: { target: 3, max: 4 },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
import { clamp } from '../src/util'
|
||||||
|
|
||||||
|
describe('utils', () => {
|
||||||
|
describe('clamp', () => {
|
||||||
|
test("doesn't alter integers within the given range", () => {
|
||||||
|
expect(clamp(5, { min: 0, max: 10 })).toBe(5)
|
||||||
|
expect(clamp(-8, { min: -100, max: 100 })).toBe(-8)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps values at the range boundaries', () => {
|
||||||
|
expect(clamp(0, { min: 0, max: 10 })).toBe(0)
|
||||||
|
expect(clamp(10, { min: 0, max: 10 })).toBe(10)
|
||||||
|
expect(clamp(-100, { min: -100, max: 100 })).toBe(-100)
|
||||||
|
expect(clamp(100, { min: -100, max: 100 })).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('raises values below the minimum', () => {
|
||||||
|
expect(clamp(-1, { min: 0, max: 10 })).toBe(0)
|
||||||
|
expect(clamp(-101, { min: -100, max: 100 })).toBe(-100)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lowers values above the maximum', () => {
|
||||||
|
expect(clamp(11, { min: 0, max: 10 })).toBe(10)
|
||||||
|
expect(clamp(101, { min: -100, max: 100 })).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('floors floats by default', () => {
|
||||||
|
expect(clamp(1.5, { min: 0, max: 2 })).toBe(1)
|
||||||
|
expect(clamp(1.999, { min: 0, max: 2 })).toBe(1)
|
||||||
|
expect(clamp(-0.3, { min: -2, max: 2 })).toBe(-1)
|
||||||
|
expect(clamp(-1.1, { min: -2, max: 2 })).toBe(-2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('floors before applying bounds', () => {
|
||||||
|
expect(clamp(10.5, { min: 0, max: 10 })).toBe(10)
|
||||||
|
expect(clamp(-10.5, { min: -10, max: 10 })).toBe(-10)
|
||||||
|
expect(clamp(1.2, { min: 1.5 })).toBe(1.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('preserves floats when allowed', () => {
|
||||||
|
expect(clamp(1.5, { min: 0, max: 2 }, true)).toBe(1.5)
|
||||||
|
expect(clamp(2.5, { min: 0, max: 2 }, true)).toBe(2)
|
||||||
|
expect(clamp(-0.5, { min: 0, max: 2 }, true)).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('supports one-sided bounds', () => {
|
||||||
|
expect(clamp(-1, { min: 0 })).toBe(0)
|
||||||
|
expect(clamp(11, { min: 0 })).toBe(11)
|
||||||
|
expect(clamp(-1, { max: 10 })).toBe(-1)
|
||||||
|
expect(clamp(11, { max: 10 })).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clamps infinite values to the range boundaries', () => {
|
||||||
|
expect(clamp(Infinity, { min: 0, max: 10 })).toBe(10)
|
||||||
|
expect(clamp(-Infinity, { min: 0, max: 10 })).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es2016",
|
"target": "es2016",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
|
|||||||
Reference in New Issue
Block a user