[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Add cache stats to prometheus metrics

FoxxMD (Mar 27, 2026, 6:45 PM UTC) 81885632 23467481

+106 -1
+106 -1
src/backend/common/Cache.ts
··· 20 20 import { loggerNoop } from './MaybeLogger.js'; 21 21 import { ListenProgressPositional, ListenProgressTS } from '../sources/PlayerState/ListenProgress.js'; 22 22 const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); 23 + import prom, { Gauge } from 'prom-client'; 23 24 24 25 dayjs.extend(utc) 25 26 dayjs.extend(isBetween); ··· 53 54 cacheClientScrobbles: Cacheable; 54 55 55 56 logger: Logger; 57 + 58 + cacheHits: Gauge; 59 + cacheMisses: Gauge; 60 + cacheSets: Gauge; 61 + cacheCount: Gauge; 62 + cacheVSize: Gauge; 56 63 57 64 constructor(logger: Logger, config: CacheConfigOptions = {}) { 58 65 this.logger = childLogger(logger, 'Cache'); ··· 97 104 98 105 this.regexCache = cacheFunctions(this.config.regex); 99 106 this.cacheTransform = new Cacheable({primary: initMemoryCache({lruSize: 500})}); 107 + this.cacheTransform.stats.enabled = true; 100 108 this.cacheClientScrobbles = new Cacheable({primary: initMemoryCache({lruSize: 100, ttl: '5m'})}); 109 + this.cacheClientScrobbles.stats.enabled = true; 101 110 } 102 111 103 112 init = async () => { 104 113 await this.initMetadataCache(); 105 114 await this.initScrobbleCache(); 106 115 await this.initAuthCache(); 116 + this.enableCollectors(); 107 117 //this.cacheTransform = await this.initCacheable({provider: false, memory: {lruSize: 500}}, 'transform'); 108 118 } 109 119 120 + protected enableCollectors = () => { 121 + 122 + const collectors: {cache: Cacheable, name: string}[] = [ 123 + { cache: this.cacheMetadata, name: 'metadata' }, 124 + { cache: this.cacheScrobble, name: 'queued_scrobbles' }, 125 + { cache: this.cacheTransform, name: 'transformer' }, 126 + { cache: this.cacheClientScrobbles, name: 'historical_scrobbles' } 127 + ]; 128 + 129 + this.cacheHits = new prom.Gauge({ 130 + name: 'multiscrobbler_cache_hits', 131 + help: 'cache hits', 132 + labelNames: ['cacheType', 'tier'], 133 + collect() { 134 + for(const set of collectors) { 135 + const [primary, secondary] = getStat(set.cache, 'hits'); 136 + this.labels({cacheType: set.name, tier: 'primary'}).set(primary); 137 + if(secondary !== undefined) { 138 + this.labels({cacheType: set.name, tier: 'secondary'}).set(secondary); 139 + } 140 + } 141 + 142 + } 143 + }); 144 + this.cacheMisses = new prom.Gauge({ 145 + name: 'multiscrobbler_cache_misses', 146 + help: 'cache misses', 147 + labelNames: ['cacheType', 'tier'], 148 + collect() { 149 + for(const set of collectors) { 150 + const [primary, secondary] = getStat(set.cache, 'misses'); 151 + this.labels({cacheType: set.name, tier: 'primary'}).set(primary); 152 + if(secondary !== undefined) { 153 + this.labels({cacheType: set.name, tier: 'secondary'}).set(secondary); 154 + } 155 + } 156 + 157 + } 158 + }); 159 + 160 + this.cacheMisses = new prom.Gauge({ 161 + name: 'multiscrobbler_cache_sets', 162 + help: 'cache sets', 163 + labelNames: ['cacheType', 'tier'], 164 + collect() { 165 + for(const set of collectors) { 166 + const [primary, secondary] = getStat(set.cache, 'sets'); 167 + this.labels({cacheType: set.name, tier: 'primary'}).set(primary); 168 + if(secondary !== undefined) { 169 + this.labels({cacheType: set.name, tier: 'secondary'}).set(secondary); 170 + } 171 + } 172 + 173 + } 174 + }); 175 + 176 + this.cacheCount = new prom.Gauge({ 177 + name: 'multiscrobbler_cache_count', 178 + help: 'number of keys in cache', 179 + labelNames: ['cacheType', 'tier'], 180 + collect() { 181 + for(const set of collectors) { 182 + const [primary] = getStat(set.cache, 'count', false); 183 + this.labels({cacheType: set.name, tier: 'primary'}).set(primary); 184 + } 185 + } 186 + }); 187 + 188 + this.cacheVSize = new prom.Gauge({ 189 + name: 'multiscrobbler_cache_vsize', 190 + help: 'estimated byte size of values in cache', 191 + labelNames: ['cacheType', 'tier'], 192 + collect() { 193 + for(const set of collectors) { 194 + const [primary] = getStat(set.cache, 'vsize', false); 195 + this.labels({cacheType: set.name, tier: 'primary'}).set(primary); 196 + } 197 + } 198 + }); 199 + } 200 + 110 201 protected initCacheable = async (config: CacheConfig, cacheFor: string) => { 111 202 112 203 let logger = childLogger(this.logger, cacheFor); ··· 146 237 if(secondaryCache !== undefined) { 147 238 cacheOpts.secondary = secondaryCache; 148 239 } 149 - return new Cacheable(cacheOpts); 240 + const cache = new Cacheable(cacheOpts); 241 + cache.stats.enabled = true; 242 + return cache; 150 243 151 244 } 152 245 ··· 200 293 memory.serialize = (data) => { 201 294 return clone(data) as string; 202 295 } 296 + memory.stats.enabled = true; 203 297 return memory; 204 298 } 205 299 ··· 278 372 throwOnErrors: true, 279 373 ...typesonMarshalling 280 374 }); 375 + cache.stats.enabled = true; 281 376 return [cache, flatCache]; 282 377 } catch (e) { 283 378 throw e; ··· 293 388 namespace: ns, 294 389 ...typesonMarshalling 295 390 }); 391 + kv.stats.enabled = true; 296 392 return kv; 297 393 } 298 394 ··· 315 411 const data = typeson.parseSync(str); 316 412 return data; 317 413 } 414 + } 415 + 416 + const getStat = (cache: Cacheable, statName: string, getSecondary: boolean = true): [number, number?] => { 417 + const primary = cache.stats[statName]; 418 + let secondary: number; 419 + if(getSecondary && cache.secondary !== undefined) { 420 + secondary = cache.secondary.stats[statName]; 421 + } 422 + return [primary, secondary]; 318 423 }