Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
ac48b11
Share the webhook URL guard for other outbound requests
jamby77 Oct 8, 2026
faabdb1
Match only loopback addresses in the outbound URL guard
jamby77 Oct 8, 2026
5f8f1c8
Parse LMCache counters from Prometheus text
jamby77 Oct 8, 2026
f898b3c
Turn LMCache counters into per-minute deltas
jamby77 Oct 8, 2026
c3508d9
Reject inherited and non-finite metrics in the delta tracker
jamby77 Oct 8, 2026
0bc5ccd
Link LMCache engines by scrape URL or OTLP engine id
jamby77 Oct 8, 2026
181f130
Never send a cleared or stale scrape credential to a new URL
jamby77 Oct 8, 2026
6bdd021
Store per-minute LMCache samples and serve hit rate history
jamby77 Oct 8, 2026
0865ad6
Scrape LMCache counters from linked engines
jamby77 Oct 8, 2026
b4aa93c
Fall back to the default scrape interval for invalid values
jamby77 Oct 8, 2026
c2e6fac
Route LMCache OTLP metrics to linked KV cache engines
jamby77 Oct 8, 2026
898c913
Alert on low LMCache hit rate and KV cache eviction risk
jamby77 Oct 9, 2026
a9da862
Never alert for disabled KV cache engines
jamby77 Oct 9, 2026
6abf0b4
Re-export KV cache hit rate and footprint metrics
jamby77 Oct 9, 2026
6abe07e
Clear hit rate gauges when a connection's last engine is removed
jamby77 Oct 9, 2026
10c2e09
Delete engine samples on removal and reset baselines on disable or UR…
jamby77 Oct 9, 2026
e564f2c
Skip DNS for IP-literal outbound URLs and always block loopback
jamby77 Oct 9, 2026
f1f6485
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 9, 2026
31e6c56
Block IPv4-mapped IPv6 SSRF targets and require unique engine names
jamby77 Oct 9, 2026
ae5a147
Reserve engine names while a create or rename is in flight
jamby77 Oct 9, 2026
41edfd9
Tighten outbound URL checks and KV cache ingest edge cases
jamby77 Oct 9, 2026
46cdf33
Drop stale scrape errors for engines changed mid-fetch
jamby77 Oct 9, 2026
14c20df
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 9, 2026
c4d1e57
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 9, 2026
a2ff0bf
Serialize KV cache engine and sample writes
jamby77 Oct 9, 2026
49fdc63
Harden KV cache counter deltas and outbound URL guard
jamby77 Oct 13, 2026
578ecd7
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 13, 2026
cc6b5f5
Fix KV engine registry races and stuck hit-rate alerts
jamby77 Oct 14, 2026
707ce16
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 20, 2026
2818149
Fix KV engine removal races and reject null in KV DTOs
jamby77 Oct 20, 2026
ec05919
Pin KV scrape addresses and restore webhook URL checks
jamby77 Oct 20, 2026
3bd51e8
Tidy KV cache series keys, OTLP ids and alert hysteresis
jamby77 Oct 20, 2026
d7cd65a
Clear pending-remove mark when a KV cache engine delete fails
jamby77 Oct 20, 2026
74a120e
Fold late KV cache observations into the open minute
jamby77 Oct 20, 2026
612e4bf
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 20, 2026
fdca83c
Trigger KV cache removal listeners through the registry in tests
jamby77 Oct 20, 2026
071d209
Skip the unevictable KV cache check when TTL data is missing
jamby77 Oct 20, 2026
57e5924
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 20, 2026
14b0da8
Clear KV cache eviction alerts when disabled or LMCache is gone
jamby77 Oct 20, 2026
54275b7
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 20, 2026
928e2c5
Reject engine saves that land after a delete
jamby77 Oct 20, 2026
39270ad
Reject out-of-range scrape statuses instead of crashing
jamby77 Oct 21, 2026
3619349
Block private scrape targets in cloud and more metadata endpoints
jamby77 Oct 21, 2026
56aa769
Keep scrape URL credentials out of storage and responses
jamby77 Oct 21, 2026
2671431
Refuse KV cache engines for missing or removed connections
jamby77 Oct 21, 2026
fc90685
Rebaseline KV cache counters when the series start time moves
jamby77 Oct 21, 2026
7c6da06
Skip the DNS pre-check for scrapes so the timeout covers lookups
jamby77 Oct 21, 2026
3e2a7d9
Allow loopback answers in guardedLookup outside production
jamby77 Oct 21, 2026
3e9404a
Sum KV cache footprint gauges across dtypes for each model
jamby77 Oct 21, 2026
47e72cd
Clear exported KV cache series when the license lapses
jamby77 Oct 21, 2026
17312b9
Fail closed on unknown resolveDns values in the outbound URL guard
jamby77 Oct 21, 2026
ae0b9fa
Compute the KV cache range hit rate from SQL totals
jamby77 Oct 21, 2026
65b9882
Throttle KV cache engine row writes from scrape results
jamby77 Oct 21, 2026
82c6f6f
Expire KV cache counter baselines after an hour idle
jamby77 Oct 21, 2026
b00d060
Expire KV cache engine removal tombstones after an hour
jamby77 Oct 21, 2026
dcaa9dc
Share the OTLP engine id pattern and no-metrics message
jamby77 Oct 21, 2026
4de26dc
Use the shared encryption service accessor for scrape auth headers
jamby77 Oct 21, 2026
6eeefa0
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 21, 2026
5d1565e
Treat all of 0.0.0.0/8 like loopback in the outbound URL guard
jamby77 Oct 21, 2026
9ca0a04
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 21, 2026
3d4d06a
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 21, 2026
f726070
Skip the KV cache bytes gauge when the byte estimate is unknown
jamby77 Oct 21, 2026
9c28c11
Keep the KV evicting alert armed when the LMCache share is unknown
jamby77 Oct 21, 2026
67d9d3b
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 21, 2026
e275243
Reject engine saves whose write lands after a delete
jamby77 Oct 21, 2026
1390009
Merge branch 'kv-cache/footprint' into kv-cache/engines
jamby77 Oct 21, 2026
722f986
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 21, 2026
180d725
Await in-flight KV sample saves when flushing with nothing to drain
jamby77 Oct 21, 2026
ffb8a18
Parse KV_CACHE_SCRAPE_BLOCK_PRIVATE leniently and fail closed on unkn…
jamby77 Oct 21, 2026
45dc452
Accept underscores in KV cache scrape URL hostnames
jamby77 Oct 21, 2026
971604f
Match private IP patterns only on IP literals in the outbound URL guard
jamby77 Oct 21, 2026
dadf61d
Harden KV alert evaluation against overlap, partial and unknown snaps…
jamby77 Oct 21, 2026
938f6cd
Validate the KV scrape interval and surface undecryptable auth headers
jamby77 Oct 21, 2026
7892a6b
Sum KV sample counters through a shared helper over METRIC_FIELDS
jamby77 Oct 21, 2026
5420d97
Bucket KV hit-rate chart samples in storage across the whole range
jamby77 Oct 21, 2026
3df2117
Keep Valkey metrics of an LMCache-tagged OTLP resource on the Valkey …
jamby77 Oct 21, 2026
dc490fa
Let a failed KV engine sample delete be retried by its connection
jamby77 Oct 21, 2026
16ace9a
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 21, 2026
6eced4d
Hide blocked IPs from outbound URL errors and match localhost exactly
jamby77 Oct 21, 2026
cc94a12
Pin webhook delivery to the validated address and refuse redirects
jamby77 Oct 21, 2026
e3fc1dd
Send KV eviction risk webhooks without blocking footprint collection
jamby77 Oct 21, 2026
57a82eb
Report an undecryptable KV auth header when a scrape URL changes
jamby77 Oct 21, 2026
a4ffd09
Keep a URL change's probe status over an old-URL scrape result
jamby77 Oct 21, 2026
691d4a8
Mask query values in KV engine scrape URLs returned by the API
jamby77 Oct 21, 2026
5bcaa58
Label KV cache Prometheus series with the connection id
jamby77 Oct 21, 2026
3ff96db
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 21, 2026
3f05869
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 21, 2026
71d6799
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 21, 2026
5b37557
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 21, 2026
5ffebe8
Merge kv-cache/footprint into kv-cache/engines
jamby77 Oct 21, 2026
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
8 changes: 8 additions & 0 deletions apps/api/src/common/interfaces/storage-port.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,12 @@ export interface KvCacheFootprintQuery extends KvCacheRangeQuery {
export interface KvCacheSampleQuery extends KvCacheRangeQuery {
engineId?: string;
modelName?: string;
bucketMs?: number;
}

export interface KvCacheSampleTotals {
requestedTokens: number;
hitTokens: number;
}

export interface MemorySnapshotQueryOptions {
Expand Down Expand Up @@ -655,7 +661,9 @@ export interface StoragePort {
pruneOldKvCacheFootprintSnapshots(cutoffTimestamp: number, connectionId?: string): Promise<number>;
saveKvCacheEngineSamples(samples: KvCacheEngineSample[]): Promise<void>;
getKvCacheEngineSamples(query: KvCacheSampleQuery): Promise<KvCacheEngineSample[]>;
getKvCacheSampleTotals(query: Omit<KvCacheSampleQuery, 'limit' | 'bucketMs'>): Promise<KvCacheSampleTotals>;
pruneOldKvCacheEngineSamples(cutoffTimestamp: number, connectionId?: string): Promise<number>;
deleteKvCacheEngineSamples(engineId: string): Promise<void>;
saveKvCacheEngine(engine: StoredKvCacheEngine): Promise<StoredKvCacheEngine>;
getKvCacheEngines(connectionId?: string): Promise<StoredKvCacheEngine[]>;
getKvCacheEngine(id: string): Promise<StoredKvCacheEngine | null>;
Expand Down
294 changes: 294 additions & 0 deletions apps/api/src/common/utils/__tests__/outbound-url-guard.spec.ts

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions apps/api/src/common/utils/outbound-url-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { BadRequestException, Logger } from '@nestjs/common';
import { promises as dns } from 'dns';
import { isIP, type LookupFunction } from 'net';

export interface OutboundUrlOptions {
label: string;
allowPrivateNetworks?: boolean;
resolveDns?: 'always' | 'production' | 'never';
}

export class BlockedAddressError extends Error {}

const logger = new Logger('OutboundUrlGuard');

const BLOCKED_IP_PATTERNS = [
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\./,
/^169\.254\./,
/^::1?$/,
/^0\./,
/^fe[89ab][0-9a-f]:/i,
/^f[cd][0-9a-f]{2}:/i,
];
Comment thread
jamby77 marked this conversation as resolved.

const ALWAYS_BLOCKED = [/^169\.254\./, /^fe[89ab][0-9a-f]:/i, /^fd00:ec2::254$/, /^100\.100\.100\.200$/, /^192\.0\.0\.192$/];

const LOOPBACK = [/^127\./, /^0\./, /^::1?$/];

const MAPPED_DOTTED = /^::ffff:(?:0:)?(\d{1,3}(?:\.\d{1,3}){3})$/;
const MAPPED_HEX = /^::ffff:(?:0:)?([0-9a-f]{1,4}):([0-9a-f]{1,4})$/;

const bareHost = (hostname: string) => hostname.replace(/^\[|\]$/g, '').toLowerCase();

function canonicalIp(ip: string): string {
const address = bareHost(ip);
if (isIP(address) !== 6) return address;
try {
return bareHost(new URL(`http://[${address}]`).hostname);
} catch {
return address;
}
}

function unmapIpv4(address: string): string {
const dotted = MAPPED_DOTTED.exec(address);
if (dotted) return dotted[1];
const hex = MAPPED_HEX.exec(address);
if (!hex) return address;
const high = parseInt(hex[1], 16);
const low = parseInt(hex[2], 16);
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
}

const capitalize = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);

const isLocalhostName = (hostname: string) => /(^|\.)localhost\.?$/.test(hostname);

function blockedAddressMessage(label: string, hostname: string, address: string): string {
logger.warn(`${capitalize(label)} ${hostname} resolves to blocked IP address ${address}`);
return `${capitalize(label)} resolves to a blocked address`;
}

export function isBlockedIp(ip: string, allowPrivateNetworks = false): boolean {
const address = unmapIpv4(canonicalIp(ip));
if (ALWAYS_BLOCKED.some((pattern) => pattern.test(address))) return true;
if (allowPrivateNetworks) return false;
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(address));
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

function isLoopback(ip: string): boolean {
const address = unmapIpv4(canonicalIp(ip));
return LOOPBACK.some((pattern) => pattern.test(address));
}

export async function assertSafeOutboundUrl(rawUrl: string, options: OutboundUrlOptions): Promise<URL> {
const { label, allowPrivateNetworks, resolveDns = 'always' } = options;
try {
const parsed = new URL(rawUrl);

if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new BadRequestException('Only HTTP and HTTPS protocols are allowed');
}

if (parsed.username || parsed.password) {
logger.warn(`${capitalize(label)} contains credentials, consider using custom headers instead: ${parsed.hostname}`);
}

const isProduction = process.env.NODE_ENV === 'production';
const host = unmapIpv4(bareHost(parsed.hostname));
const isLocalhost = host === 'localhost' || LOOPBACK.some((pattern) => pattern.test(host));

if (isLocalhost && (!isProduction || allowPrivateNetworks)) {
logger.debug(`Allowing localhost ${label} in ${process.env.NODE_ENV || 'development'} mode: ${rawUrl}`);
return parsed;
}

if (parsed.hostname === 'localhost' || parsed.hostname === '0.0.0.0') {
throw new BadRequestException(`Cannot use localhost or 0.0.0.0 as ${label} in production`);
}

if (isIP(host) && isBlockedIp(host, allowPrivateNetworks)) {
throw new BadRequestException(`Cannot use private IP addresses as ${label}`);
}

if (!allowPrivateNetworks && (parsed.hostname.startsWith('127.') || isLocalhostName(host))) {
throw new BadRequestException('Suspicious hostname detected');
}

if (!isIP(host) && resolveDns !== 'never' && (resolveDns !== 'production' || isProduction)) {
try {
const answers = await dns.lookup(host, { all: true, verbatim: true });
for (const { address: addr } of answers) {
if (isBlockedIp(addr, allowPrivateNetworks)) {
throw new BadRequestException(blockedAddressMessage(label, host, addr));
}
}
} catch (dnsError: any) {
if (dnsError instanceof BadRequestException) {
throw dnsError;
}
logger.warn(`Failed to resolve DNS for ${label}: ${parsed.hostname}`);
throw new BadRequestException(`Failed to resolve ${label} hostname`);
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

return parsed;
} catch (error) {
if (error instanceof BadRequestException) {
throw error;
}
throw new BadRequestException(`Invalid ${label}`);
}
}

export function guardedLookup(options: Omit<OutboundUrlOptions, 'resolveDns'>): LookupFunction {
const { label, allowPrivateNetworks } = options;
return (hostname, lookupOptions, callback) => {
dns.lookup(hostname, { family: lookupOptions.family, hints: lookupOptions.hints, all: true, verbatim: true }).then(
(answers) => {
const allowLoopback = allowPrivateNetworks || process.env.NODE_ENV !== 'production';
const blocked = answers.find(({ address }) => isBlockedIp(address, allowPrivateNetworks) && !(allowLoopback && isLoopback(address)));
if (blocked) {
callback(new BlockedAddressError(blockedAddressMessage(label, hostname, blocked.address)), '', 0);
} else if (answers.length === 0) {
callback(Object.assign(new Error(`getaddrinfo ENOTFOUND ${hostname}`), { code: 'ENOTFOUND' }), '', 0);
} else if (lookupOptions.all) {
(callback as unknown as (error: null, addresses: typeof answers) => void)(null, answers);
} else {
callback(null, answers[0].address, answers[0].family);
}
},
(error: NodeJS.ErrnoException) => callback(error, '', 0),
);
};
}
58 changes: 58 additions & 0 deletions apps/api/src/common/utils/pinned-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { request as httpRequest, type IncomingMessage } from 'http';
import { request as httpsRequest } from 'https';
import type { LookupFunction } from 'net';
import { Readable } from 'stream';

export interface PinnedRequestInit {
method?: string;
headers: Record<string, string>;
body?: string;
signal: AbortSignal;
lookup?: LookupFunction;
}

export class UnexpectedStatusError extends Error {}

const NULL_BODY_STATUSES = new Set([204, 205, 304]);

function toResponse(message: IncomingMessage): Response {
const headers = new Headers();
for (const [name, value] of Object.entries(message.headers)) {
if (value === undefined) continue;
for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item);
}
const status = message.statusCode ?? 502;
if (status < 200 || status > 599) throw new UnexpectedStatusError(`unexpected HTTP status ${status}`);
if (NULL_BODY_STATUSES.has(status)) {
message.resume();
return new Response(null, { status, headers });
}
return new Response(Readable.toWeb(message) as ReadableStream<Uint8Array>, { status, headers });
}

export const pinnedRequest = (url: string, init: PinnedRequestInit): Promise<Response> =>
new Promise((resolve, reject) => {
const target = new URL(url);
const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
const headers = init.body === undefined ? init.headers : { 'Content-Length': String(Buffer.byteLength(init.body)), ...init.headers };
const req = send(
target,
{
method: init.method ?? 'GET',
agent: false,
headers,
signal: init.signal,
...(init.lookup ? { lookup: init.lookup } : {}),
},
(message) => {
try {
resolve(toResponse(message));
} catch (error) {
message.destroy();
reject(error);
}
},
);
req.on('error', reject);
req.end(init.body);
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const clean: IngestResult = {
unmapped_metric: 0,
invalid_value: 0,
cardinality_limit: 0,
unknown_engine: 0,
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ describe('OtelMetricsIngestService', () => {
unmapped_metric: 1,
invalid_value: 1,
cardinality_limit: 0,
unknown_engine: 0,
});
expect(toPartialSuccess(result)).toEqual({
rejectedDataPoints: 8,
Expand Down Expand Up @@ -420,4 +421,78 @@ describe('OtelMetricsIngestService discovery', () => {
expect(discovered.list(NOW_MS)).toEqual([]);
expect(result.dropped.unknown_instance).toBe(1);
});

describe('KV cache routing', () => {
const lmcacheSum = (name: string): OtlpMetric => ({
name,
sum: { dataPoints: [{ attributes: [], asInt: '5' }, { attributes: [], asInt: '6' }], aggregationTemporality: 2 },
});

function buildWithSink(sinkResult = { accepted: 2, dropped: { unmapped_metric: 3 } as Record<string, number> }) {
const registry = { findByHostPort: jest.fn() } as unknown as ConnectionRegistry;
const store = new ExternalMetricsStore();
const applySpy = jest.spyOn(store, 'apply');
const sink = { ingest: jest.fn().mockReturnValue(sinkResult) };
const service = new OtelMetricsIngestService(registry, store, undefined, undefined, sink);
return { service, registry, applySpy, sink };
}

it('routes a resource with the engine attribute to the sink', () => {
const { service, registry, applySpy, sink } = buildWithSink();
const metrics = [lmcacheSum('lmcache:num_hit_tokens_total')];
const result = service.ingest(resource([str('betterdb.lmcache.engine', 'eng-1')], metrics), NOW_MS);
expect(sink.ingest).toHaveBeenCalledWith({ 'betterdb.lmcache.engine': 'eng-1' }, metrics, NOW_MS);
expect(registry.findByHostPort).not.toHaveBeenCalled();
expect(applySpy).not.toHaveBeenCalled();
expect(result.accepted).toBe(2);
expect(result.dropped.unmapped_metric).toBe(3);
});

it('routes a resource whose metrics are all lmcache without the attribute', () => {
const { service, sink } = buildWithSink();
service.ingest(resource([], [lmcacheSum('lmcache:num_hit_tokens_total')]), NOW_MS);
expect(sink.ingest).toHaveBeenCalledTimes(1);
});

it('drops an lmcache resource as unknown_engine when no sink is present', () => {
const { service } = build();
const result = service.ingest(
resource([str('betterdb.lmcache.engine', 'eng-1')], [lmcacheSum('lmcache:num_hit_tokens_total')]),
NOW_MS,
);
expect(result.dropped.unknown_engine).toBe(2);
expect(result.accepted).toBe(0);
});

it('keeps Valkey metrics of a tagged resource on the Valkey path and routes the rest to the sink', () => {
const { service, registry, sink } = buildWithSink({ accepted: 2, dropped: {} });
(registry.findByHostPort as jest.Mock).mockReturnValue({ id: 'ext', connectionType: 'external' });
const kv = lmcacheSum('lmcache:num_hit_tokens_total');
const result = service.ingest(
resource([str('betterdb.lmcache.engine', 'eng-1'), ...identity], [gauge('redis.memory.used', 1024), kv]),
NOW_MS,
);
expect(sink.ingest).toHaveBeenCalledWith(expect.objectContaining({ 'betterdb.lmcache.engine': 'eng-1' }), [kv], NOW_MS);
expect(registry.findByHostPort).toHaveBeenCalledWith('cache.internal', 6379);
expect(result.accepted).toBe(3);
});

it('does not call the sink for a tagged resource that only carries Valkey metrics', () => {
const { service, registry, sink } = buildWithSink();
(registry.findByHostPort as jest.Mock).mockReturnValue({ id: 'ext', connectionType: 'external' });
const result = service.ingest(
resource([str('betterdb.lmcache.engine', 'eng-1'), ...identity], [gauge('valkey.memory.used', 1024)]),
NOW_MS,
);
expect(sink.ingest).not.toHaveBeenCalled();
expect(result.accepted).toBe(1);
});

it('keeps the Valkey path when a sink is present', () => {
const { service, sink } = buildWithSink();
const result = service.ingest(resource([], [gauge('redis.memory.used', 1)]), NOW_MS);
expect(sink.ingest).not.toHaveBeenCalled();
expect(result.dropped.unidentified).toBe(1);
});
});
});
18 changes: 18 additions & 0 deletions apps/api/src/external-metrics/kv-cache-otlp-sink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { DropReason, OtlpMetric } from './otlp-metrics-types';

export const KV_CACHE_OTLP_SINK = Symbol('KV_CACHE_OTLP_SINK');
export const LMCACHE_ENGINE_ATTRIBUTE = 'betterdb.lmcache.engine';

export interface KvCacheOtlpSinkResult {
accepted: number;
dropped: Partial<Record<DropReason, number>>;
}

export interface KvCacheOtlpSink {
ingest(resourceAttributes: Record<string, string>, metrics: OtlpMetric[], nowMs: number): KvCacheOtlpSinkResult;
}

export function isKvCacheResource(resourceAttributes: Record<string, string>, metrics: OtlpMetric[]): boolean {
if (resourceAttributes[LMCACHE_ENGINE_ATTRIBUTE] !== undefined) return true;
return metrics.length > 0 && metrics.every((metric) => (metric.name ?? '').startsWith('lmcache:'));
}
Loading
Loading