Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2f38be0af | |||
| 9a61a3a9af | |||
| dbcfeba16d | |||
| d70954f3ef |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-20 - 3.2.1 - fix(destination-buffer)
|
||||
return entries in chronological order (oldest-first) and adjust pagination semantics
|
||||
|
||||
- Change getEntries to return the most recent entries in chronological (oldest-first) order instead of newest-first
|
||||
- Adjust pagination to compute slice indices from the newest end (start = max(0, len - limit - offset), end = len - offset)
|
||||
- Update tests to expect chronological ordering and clarified pagination examples
|
||||
- Modified files: ts_destination_buffer/classes.destinationbuffer.ts, test/test.destination-buffer.node.ts
|
||||
|
||||
## 2026-02-19 - 3.2.0 - feat(destination-buffer)
|
||||
add SmartlogDestinationBuffer in-memory circular buffer destination with query/filter/pagination and tests
|
||||
|
||||
- Introduce SmartlogDestinationBuffer: an in-memory circular buffer implementing ILogDestination with configurable maxEntries (default 2000).
|
||||
- Expose APIs: handleLog, getEntries (supports level filtering, search, since timestamp, limit/offset pagination, newest-first ordering), getEntryCount, and clear.
|
||||
- Add package export './destination-buffer' and module entry points (index and plugins) to expose the new destination.
|
||||
- Add tests covering storage, filtering by level and search, pagination, eviction when maxEntries exceeded, clearing, and default maxEntries behavior.
|
||||
|
||||
## 2026-02-14 - 3.1.11 - fix(destination-receiver)
|
||||
return full webrequest response from SmartlogDestinationReceiver and migrate to WebrequestClient; update tests, dependencies, docs, and npmextra metadata
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartlog",
|
||||
"version": "3.1.11",
|
||||
"version": "3.2.1",
|
||||
"private": false,
|
||||
"description": "A minimalistic, distributed, and extensible logging tool supporting centralized log management.",
|
||||
"keywords": [
|
||||
@@ -30,6 +30,7 @@
|
||||
"./destination-devtools": "./dist_ts_destination_devtools/index.js",
|
||||
"./destination-file": "./dist_ts_destination_file/index.js",
|
||||
"./destination-local": "./dist_ts_destination_local/index.js",
|
||||
"./destination-buffer": "./dist_ts_destination_buffer/index.js",
|
||||
"./destination-receiver": "./dist_ts_destination_receiver/index.js",
|
||||
"./receiver": "./dist_ts_receiver/index.js"
|
||||
},
|
||||
|
||||
120
test/test.destination-buffer.node.ts
Normal file
120
test/test.destination-buffer.node.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartlogDestinationBuffer } from '../ts_destination_buffer/index.js';
|
||||
import type { ILogPackage, TLogLevel } from '../ts_interfaces/index.js';
|
||||
|
||||
const createMockLogPackage = (level: TLogLevel, message: string): ILogPackage => {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
type: 'log',
|
||||
level,
|
||||
message,
|
||||
context: {
|
||||
environment: 'test',
|
||||
runtime: 'node',
|
||||
zone: 'test-zone',
|
||||
},
|
||||
correlation: {
|
||||
id: '123',
|
||||
type: 'none',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
let buffer: SmartlogDestinationBuffer;
|
||||
|
||||
tap.test('should create a buffer destination instance', async () => {
|
||||
buffer = new SmartlogDestinationBuffer({ maxEntries: 100 });
|
||||
expect(buffer).toBeTruthy();
|
||||
expect(buffer.getEntryCount()).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should store log entries via handleLog', async () => {
|
||||
await buffer.handleLog(createMockLogPackage('info', 'Hello world'));
|
||||
await buffer.handleLog(createMockLogPackage('error', 'Something failed'));
|
||||
await buffer.handleLog(createMockLogPackage('warn', 'Watch out'));
|
||||
|
||||
expect(buffer.getEntryCount()).toEqual(3);
|
||||
});
|
||||
|
||||
tap.test('should retrieve entries in chronological order (oldest-first)', async () => {
|
||||
const entries = buffer.getEntries();
|
||||
expect(entries.length).toEqual(3);
|
||||
expect(entries[0].message).toEqual('Hello world');
|
||||
expect(entries[2].message).toEqual('Watch out');
|
||||
});
|
||||
|
||||
tap.test('should filter entries by level', async () => {
|
||||
const errorEntries = buffer.getEntries({ level: 'error' });
|
||||
expect(errorEntries.length).toEqual(1);
|
||||
expect(errorEntries[0].message).toEqual('Something failed');
|
||||
|
||||
const multiLevel = buffer.getEntries({ level: ['info', 'warn'] });
|
||||
expect(multiLevel.length).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('should filter entries by search string', async () => {
|
||||
const results = buffer.getEntries({ search: 'hello' });
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].message).toEqual('Hello world');
|
||||
});
|
||||
|
||||
tap.test('should support limit and offset pagination', async () => {
|
||||
// limit=2, offset=0 → last 2 entries in chronological order
|
||||
const page1 = buffer.getEntries({ limit: 2, offset: 0 });
|
||||
expect(page1.length).toEqual(2);
|
||||
expect(page1[0].message).toEqual('Something failed');
|
||||
expect(page1[1].message).toEqual('Watch out');
|
||||
|
||||
// limit=2, offset=2 → skip 2 from end, return up to 2
|
||||
const page2 = buffer.getEntries({ limit: 2, offset: 2 });
|
||||
expect(page2.length).toEqual(1);
|
||||
expect(page2[0].message).toEqual('Hello world');
|
||||
});
|
||||
|
||||
tap.test('should filter by since timestamp', async () => {
|
||||
const now = Date.now();
|
||||
const freshBuffer = new SmartlogDestinationBuffer();
|
||||
|
||||
const oldPkg = createMockLogPackage('info', 'Old message');
|
||||
oldPkg.timestamp = now - 60000;
|
||||
await freshBuffer.handleLog(oldPkg);
|
||||
|
||||
const newPkg = createMockLogPackage('info', 'New message');
|
||||
newPkg.timestamp = now;
|
||||
await freshBuffer.handleLog(newPkg);
|
||||
|
||||
const results = freshBuffer.getEntries({ since: now - 1000 });
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].message).toEqual('New message');
|
||||
});
|
||||
|
||||
tap.test('should enforce circular buffer max entries', async () => {
|
||||
const smallBuffer = new SmartlogDestinationBuffer({ maxEntries: 5 });
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await smallBuffer.handleLog(createMockLogPackage('info', `Message ${i}`));
|
||||
}
|
||||
|
||||
expect(smallBuffer.getEntryCount()).toEqual(5);
|
||||
|
||||
// Should have kept the latest 5 (messages 5-9), returned chronologically
|
||||
const entries = smallBuffer.getEntries({ limit: 10 });
|
||||
expect(entries[0].message).toEqual('Message 5');
|
||||
expect(entries[4].message).toEqual('Message 9');
|
||||
});
|
||||
|
||||
tap.test('should clear all entries', async () => {
|
||||
expect(buffer.getEntryCount()).toBeGreaterThan(0);
|
||||
buffer.clear();
|
||||
expect(buffer.getEntryCount()).toEqual(0);
|
||||
expect(buffer.getEntries().length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should use default maxEntries of 2000', async () => {
|
||||
const defaultBuffer = new SmartlogDestinationBuffer();
|
||||
// Just verify it was created without error
|
||||
expect(defaultBuffer).toBeTruthy();
|
||||
expect(defaultBuffer.getEntryCount()).toEqual(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartlog',
|
||||
version: '3.1.11',
|
||||
version: '3.2.1',
|
||||
description: 'A minimalistic, distributed, and extensible logging tool supporting centralized log management.'
|
||||
}
|
||||
|
||||
67
ts_destination_buffer/classes.destinationbuffer.ts
Normal file
67
ts_destination_buffer/classes.destinationbuffer.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { ILogDestination, ILogPackage, TLogLevel } from '../dist_ts_interfaces/index.js';
|
||||
|
||||
export interface IDestinationBufferOptions {
|
||||
maxEntries?: number;
|
||||
}
|
||||
|
||||
export interface IBufferQueryOptions {
|
||||
level?: TLogLevel | TLogLevel[];
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
since?: number;
|
||||
}
|
||||
|
||||
export class SmartlogDestinationBuffer implements ILogDestination {
|
||||
private logPackages: ILogPackage[] = [];
|
||||
private maxEntries: number;
|
||||
|
||||
constructor(options?: IDestinationBufferOptions) {
|
||||
this.maxEntries = options?.maxEntries ?? 2000;
|
||||
}
|
||||
|
||||
public async handleLog(logPackage: ILogPackage): Promise<void> {
|
||||
this.logPackages.push(logPackage);
|
||||
if (this.logPackages.length > this.maxEntries) {
|
||||
this.logPackages.shift();
|
||||
}
|
||||
}
|
||||
|
||||
public getEntries(options?: IBufferQueryOptions): ILogPackage[] {
|
||||
const limit = options?.limit ?? 100;
|
||||
const offset = options?.offset ?? 0;
|
||||
|
||||
let results = this.logPackages;
|
||||
|
||||
// Filter by level
|
||||
if (options?.level) {
|
||||
const levels = Array.isArray(options.level) ? options.level : [options.level];
|
||||
results = results.filter((pkg) => levels.includes(pkg.level));
|
||||
}
|
||||
|
||||
// Filter by search (message content)
|
||||
if (options?.search) {
|
||||
const searchLower = options.search.toLowerCase();
|
||||
results = results.filter((pkg) => pkg.message.toLowerCase().includes(searchLower));
|
||||
}
|
||||
|
||||
// Filter by timestamp
|
||||
if (options?.since) {
|
||||
results = results.filter((pkg) => pkg.timestamp >= options.since);
|
||||
}
|
||||
|
||||
// Return most recent `limit` entries in chronological order (oldest-first)
|
||||
// offset skips from the newest end
|
||||
const start = Math.max(0, results.length - limit - offset);
|
||||
const end = results.length - offset;
|
||||
return results.slice(Math.max(0, start), Math.max(0, end));
|
||||
}
|
||||
|
||||
public getEntryCount(): number {
|
||||
return this.logPackages.length;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.logPackages = [];
|
||||
}
|
||||
}
|
||||
1
ts_destination_buffer/index.ts
Normal file
1
ts_destination_buffer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SmartlogDestinationBuffer, type IDestinationBufferOptions, type IBufferQueryOptions } from './classes.destinationbuffer.js';
|
||||
3
ts_destination_buffer/plugins.ts
Normal file
3
ts_destination_buffer/plugins.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as smartlogInterfaces from '../dist_ts_interfaces/index.js';
|
||||
|
||||
export { smartlogInterfaces };
|
||||
Reference in New Issue
Block a user