From fd0900e0533ad2f56a1546f55f1e92a48c366b2e Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:00:07 +0000 Subject: [PATCH] feat: add stats endpoint with cache stats --- src/extractor/ExtractorRegistry.test.ts | 7 +++++++ src/extractor/ExtractorRegistry.ts | 7 +++++++ src/index.ts | 8 ++++++++ src/source/Source.test.ts | 10 ++++++++++ src/source/Source.ts | 7 +++++++ src/utils/Fetcher.test.ts | 7 +++++++ src/utils/Fetcher.ts | 12 +++++++++++- 7 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/source/Source.test.ts diff --git a/src/extractor/ExtractorRegistry.test.ts b/src/extractor/ExtractorRegistry.test.ts index 7ea76e5..4d34654 100644 --- a/src/extractor/ExtractorRegistry.test.ts +++ b/src/extractor/ExtractorRegistry.test.ts @@ -54,4 +54,11 @@ describe('ExtractorRegistry', () => { const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked-2.html'), CountryCode.de, 'a title!'); expect(urlResults).toMatchSnapshot(); }); + + test('stats returns something', async () => { + const stats = extractorRegistry.stats(); + + expect(stats).toHaveProperty('urlResultCache'); + expect(stats.urlResultCache).toBeTruthy(); + }); }); diff --git a/src/extractor/ExtractorRegistry.ts b/src/extractor/ExtractorRegistry.ts index aa6df83..fbbae2a 100644 --- a/src/extractor/ExtractorRegistry.ts +++ b/src/extractor/ExtractorRegistry.ts @@ -18,9 +18,16 @@ export class ExtractorRegistry { this.urlResultCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 4096 }) }), secondary: new Keyv(new KeyvSqlite(`sqlite://${getCacheDir()}/webstreamr-extractor-cache.sqlite`)), + stats: true, }); } + public stats() { + return { + urlResultCache: this.urlResultCache.stats, + }; + }; + public async handle(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise { const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url)); if (!extractor) { diff --git a/src/index.ts b/src/index.ts index c50d24f..23ef41e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -139,6 +139,14 @@ addon.get('/live', async (req: Request, res: Response) => { } }); +addon.get('/stats', async (_req: Request, res: Response) => { + res.json({ + extractorRegistry: extractorRegistry.stats(), + fetcher: fetcher.stats(), + sources: Source.stats(), + }); +}); + const port = parseInt(envGet('PORT') || '51546'); addon.listen(port, () => { logger.info(`Add-on Repository URL: http://127.0.0.1:${port}/manifest.json`); diff --git a/src/source/Source.test.ts b/src/source/Source.test.ts new file mode 100644 index 0000000..e7058ec --- /dev/null +++ b/src/source/Source.test.ts @@ -0,0 +1,10 @@ +import { Source } from './Source'; + +describe('Source', () => { + test('stats returns something', async () => { + const stats = Source.stats(); + + expect(stats).toHaveProperty('sourceResultCache'); + expect(stats.sourceResultCache).toBeTruthy(); + }); +}); diff --git a/src/source/Source.ts b/src/source/Source.ts index 16e3c3a..5ddd4fd 100644 --- a/src/source/Source.ts +++ b/src/source/Source.ts @@ -28,10 +28,17 @@ export abstract class Source { private static readonly sourceResultCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }), secondary: new Keyv(new KeyvSqlite(`sqlite://${getCacheDir()}/webstreamr-source-cache.sqlite`)), + stats: true, }); protected abstract handleInternal(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])>; + public static stats() { + return { + sourceResultCache: Source.sourceResultCache.stats, + }; + }; + public async handle(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])> { const cacheKey = `${this.id}_${id.toString()}`; diff --git a/src/utils/Fetcher.test.ts b/src/utils/Fetcher.test.ts index 4b81a49..cb5ea44 100644 --- a/src/utils/Fetcher.test.ts +++ b/src/utils/Fetcher.test.ts @@ -258,4 +258,11 @@ describe('fetch', () => { await expect(fetcher.text(ctx, new URL('https://too-many-recent-timeouts-url.test/'), { timeout: 10, timeoutsCountThrow: 2 })).rejects.toBeInstanceOf(TimeoutError); await expect(fetcher.text(ctx, new URL('https://too-many-recent-timeouts-url.test/'), { timeout: 10, timeoutsCountThrow: 2 })).rejects.toBeInstanceOf(TooManyTimeoutsError); }); + + test('stats returns something', async () => { + const stats = fetcher.stats(); + + expect(stats).toHaveProperty('httpCache'); + expect(stats.httpCache).toBeTruthy(); + }); }); diff --git a/src/utils/Fetcher.ts b/src/utils/Fetcher.ts index 3fd4545..145fa33 100644 --- a/src/utils/Fetcher.ts +++ b/src/utils/Fetcher.ts @@ -63,7 +63,11 @@ export class Fetcher { private readonly logger: winston.Logger; - private readonly httpCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 2048 }) }) }); + private readonly httpCache = new Cacheable({ + primary: new Keyv({ store: new CacheableMemory({ lruSize: 2048 }) }), + stats: true, + }); + private readonly rateLimitedCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }) }); private readonly semaphores = new Map(); private readonly hostUserAgentMap = new Map(); @@ -76,6 +80,12 @@ export class Fetcher { this.logger = logger; } + public stats() { + return { + httpCache: this.httpCache.stats, + }; + }; + public async text(ctx: Context, url: URL, init?: CustomRequestInit): Promise { return (await this.cachedFetch(ctx, url, init)).body; };