Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 27 additions & 82 deletions src/metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ describe('Metrics functionality', () => {
cacheCapacity: 10,
cacheTTL: 60000,
metricsConfig: {
cacheId: 'test_cache',
registry,
prefix: 'test_prefix',
labels: { test_label: 'test_value' },
Expand All @@ -55,100 +54,88 @@ describe('Metrics functionality', () => {

const metrics = await registry.metrics();
expect(metrics).to.include(
'test_prefix_hits_total{cache_id="test_cache",test_label="test_value"} 1',
'test_prefix_hits_total{test_label="test_value"} 1',
);
expect(metrics).to.include(
'test_prefix_misses_total{cache_id="test_cache",test_label="test_value"} 1',
'test_prefix_misses_total{test_label="test_value"} 1',
);
});

it('should track put operations', async () => {
const cache = new PromiseCache<string, string>({
cacheCapacity: 10,
cacheTTL: 60000,
metricsConfig: {
cacheId: 'test_cache',
registry,
prefix: 'test_prefix',
},
metricsConfig: { registry, prefix: 'test_prefix' },
});

await cache.put('key1', Promise.resolve('value1'));
await cache.put('key2', Promise.resolve('value2'));

const metrics = await registry.metrics();
expect(metrics).to.include(
'test_prefix_puts_total{cache_id="test_cache"} 2',
);
expect(metrics).to.include('test_prefix_puts_total 2');
});

it('should track cache size', async () => {
const cache = new PromiseCache<string, string>({
cacheCapacity: 10,
cacheTTL: 60000,
metricsConfig: { cacheId: 'test_cache', registry },
metricsConfig: { registry, prefix: 'test_prefix' },
});

await cache.put('key1', Promise.resolve('value1'));
await cache.put('key2', Promise.resolve('value2'));

const metrics = await registry.metrics();
expect(metrics).to.include('promise_cache_size{cache_id="test_cache"} 2');
expect(metrics).to.include('test_prefix_size 2');
});

it('should track remove operations', async () => {
const cache = new PromiseCache<string, string>({
cacheCapacity: 10,
cacheTTL: 60000,
metricsConfig: { cacheId: 'test_cache', registry },
metricsConfig: { registry, prefix: 'test_prefix' },
});

await cache.put('key1', Promise.resolve('value1'));
cache.remove('key1');

const metrics = await registry.metrics();
expect(metrics).to.include(
'promise_cache_removes_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include('test_prefix_removes_total 1');
});

it('should track clear operations', async () => {
const cache = new PromiseCache<string, string>({
cacheCapacity: 10,
cacheTTL: 60000,
metricsConfig: { cacheId: 'test_cache', registry },
metricsConfig: { registry, prefix: 'test_prefix' },
});

await cache.put('key1', Promise.resolve('value1'));
cache.clear();

const metrics = await registry.metrics();
expect(metrics).to.include(
'promise_cache_clears_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include('test_prefix_clears_total 1');
});

it('should track eviction operations when called directly', async () => {
const cache = new PromiseCache<string, string>({
cacheCapacity: 2,
cacheTTL: 60000,
metricsConfig: { cacheId: 'test_cache', registry },
metricsConfig: { registry, prefix: 'test_prefix' },
});

const metrics = cache.getMetrics();
metrics!.recordEviction();

const metricsOutput = await registry.metrics();
expect(metricsOutput).to.include(
'promise_cache_evictions_total{cache_id="test_cache"} 1',
);
expect(metricsOutput).to.include('test_prefix_evictions_total 1');
});

it('should provide access to metrics objects', () => {
const cache = new PromiseCache<string, string>({
cacheCapacity: 10,
cacheTTL: 60000,
metricsConfig: { cacheId: 'test_cache', registry },
metricsConfig: { registry, prefix: 'test_prefix' },
});

const metrics = cache.getMetrics();
Expand All @@ -170,11 +157,7 @@ describe('Metrics functionality', () => {
const cache = new ReadThroughPromiseCache<string, string>({
cacheParams: { cacheCapacity: 10, cacheTTL: 60000 },
readThroughFunction: async (key: string) => `value-${key}`,
metricsConfig: {
cacheId: 'test_cache',
registry,
prefix: 'test_rt_cache_1',
},
metricsConfig: { registry, prefix: 'test_rt_cache_1' },
});

// First access should be a miss
Expand All @@ -184,12 +167,8 @@ describe('Metrics functionality', () => {
await cache.get('key1');

const metrics = await registry.metrics();
expect(metrics).to.include(
'test_rt_cache_1_hits_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include(
'test_rt_cache_1_misses_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include('test_rt_cache_1_hits_total 1');
expect(metrics).to.include('test_rt_cache_1_misses_total 1');
});

it('should remove failed promises from cache in getWithStatus', async () => {
Expand All @@ -201,11 +180,7 @@ describe('Metrics functionality', () => {
}
return `value-${key}`;
},
metricsConfig: {
cacheId: 'test_cache',
registry,
prefix: 'test_rt_cache_3',
},
metricsConfig: { registry, prefix: 'test_rt_cache_3' },
});

try {
Expand Down Expand Up @@ -234,74 +209,46 @@ describe('Metrics functionality', () => {
const cache = new ReadThroughPromiseCache<string, string>({
cacheParams: { cacheCapacity: 10, cacheTTL: 60000 },
readThroughFunction: async (key: string) => `value-${key}`,
metricsConfig: { cacheId: 'test_cache' },
metricsConfig: { prefix: 'test_cache' },
});

await cache.get('key1');

const metrics = await register.metrics();
expect(metrics).to.include(
'promise_cache_misses_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include('test_cache_misses_total 1');
});

it('should handle undefined prefix in metricsConfig', async () => {
it('should default to prefix "promise_cache" for a zero length prefix', async () => {
const cache = new ReadThroughPromiseCache<string, string>({
cacheParams: { cacheCapacity: 10, cacheTTL: 60000 },
readThroughFunction: async (key: string) => `value-${key}`,
metricsConfig: { cacheId: 'test_cache', registry },
});

await cache.get('key1');

const metrics = await registry.metrics();
expect(metrics).to.include(
'promise_cache_misses_total{cache_id="test_cache"} 1',
);
});

it('should handle a zero length prefix', async () => {
const cache = new ReadThroughPromiseCache<string, string>({
cacheParams: { cacheCapacity: 10, cacheTTL: 60000 },
readThroughFunction: async (key: string) => `value-${key}`,
metricsConfig: { cacheId: 'test_cache', prefix: '' },
metricsConfig: { prefix: '' },
});

await cache.get('key1');

const metrics = await register.metrics();
expect(metrics).to.include(
'promise_cache_misses_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include('promise_cache_misses_total 1');
});

it('should handle empty registry in metricsConfig', async () => {
const cache = new ReadThroughPromiseCache<string, string>({
cacheParams: { cacheCapacity: 10, cacheTTL: 60000 },
readThroughFunction: async (key: string) => `value-${key}`,
metricsConfig: {
cacheId: 'test_cache',
prefix: 'test_rt_cache_empty_registry',
},
metricsConfig: { prefix: 'test_rt_cache_empty_registry' },
});

await cache.get('key1');

const metrics = await register.metrics();
expect(metrics).to.include(
'test_rt_cache_empty_registry_misses_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include('test_rt_cache_empty_registry_misses_total 1');
});

it('should provide access to metrics objects', () => {
const cache = new ReadThroughPromiseCache<string, string>({
cacheParams: { cacheCapacity: 10, cacheTTL: 60000 },
readThroughFunction: async (key: string) => `value-${key}`,
metricsConfig: {
cacheId: 'test_cache',
registry,
prefix: 'test_rt_cache_metrics_obj',
},
metricsConfig: { registry, prefix: 'test_rt_cache_metrics_obj' },
});

const metrics = cache.getMetrics();
Expand All @@ -323,12 +270,10 @@ describe('Metrics functionality', () => {

describe('CacheMetrics constructor', () => {
it('should create metrics with default values', async () => {
const cacheMetrics = new CacheMetrics({ cacheId: 'test_cache' });
const cacheMetrics = new CacheMetrics({ prefix: 'test_cache' });
cacheMetrics.recordHit();
const metrics = await register.metrics();
expect(metrics).to.include(
'promise_cache_hits_total{cache_id="test_cache"} 1',
);
expect(metrics).to.include('test_cache_hits_total 1');
});
});
});
10 changes: 3 additions & 7 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import { Counter, Gauge, Registry, register } from 'prom-client';
// Resulting metrics will be of the form:
// <prefix ?? "promise_cache">_<metric name>{cache_id=<cacheId>, <label name 1>=<label value 1>, ..., <label name N>=<label value N>} <metric value>
export interface CacheMetricsConfig {
cacheId: string;
prefix: string;
registry?: Registry;
prefix?: string;
labels?: Record<string, string>;
}

Expand All @@ -39,11 +38,8 @@ export class CacheMetrics {

constructor(config: CacheMetricsConfig) {
const registry = config.registry || register;
this.prefix =
config.prefix != undefined && config.prefix.length > 0
? config.prefix
: 'promise_cache';
this.defaultLabels = { cache_id: config.cacheId, ...config.labels };
this.prefix = config.prefix || 'promise_cache';
this.defaultLabels = { ...config.labels };
const labelNames = Object.keys(this.defaultLabels);

this.hitCounter = new Counter({
Expand Down
2 changes: 1 addition & 1 deletion src/promiseCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('PromiseCache class', () => {
it('constructor takes a capacity that is not exceeded by excessive puts', async () => {
const cache = new PromiseCache<string, string>({
cacheCapacity: 1,
cacheTTL: 60,
cacheTTLMillis: 60,
});
cache.put('1', Promise.resolve('one'));
cache.put('2', Promise.resolve('two'));
Expand Down
32 changes: 25 additions & 7 deletions src/promiseCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,35 @@
import { Cache, EphemeralCache } from '@alexsasharegan/simple-cache';
import { CacheMetrics, CacheMetricsConfig } from './metrics';

export interface CacheParams {
cacheCapacity: number;
cacheTTL: number;
metricsConfig?: CacheMetricsConfig;
}
export type CacheParams =
| {
cacheCapacity: number;
/** Time-to-live in milliseconds (deprecated, use cacheTTLMillis instead) */
cacheTTL: number;
cacheTTLMillis?: never;
metricsConfig?: CacheMetricsConfig;
}
| {
cacheCapacity: number;
cacheTTL?: never;
/** Time-to-live in milliseconds */
cacheTTLMillis: number;
metricsConfig?: CacheMetricsConfig;
};
export class PromiseCache<K, V> {
private readonly cache: Cache<string, Promise<V>>;
protected readonly metrics?: CacheMetrics;

constructor({ cacheCapacity, cacheTTL, metricsConfig }: CacheParams) {
this.cache = EphemeralCache<string, Promise<V>>(cacheCapacity, cacheTTL);
constructor({
cacheCapacity,
cacheTTL,
cacheTTLMillis,
metricsConfig,
}: CacheParams) {
this.cache = EphemeralCache<string, Promise<V>>(
cacheCapacity,
cacheTTLMillis ?? cacheTTL,
);

if (metricsConfig !== undefined) {
this.metrics = new CacheMetrics(metricsConfig);
Expand Down