diff --git a/src/backend/common/AbstractComponent.ts b/src/backend/common/AbstractComponent.ts index 1f61d147..3d0da933 100644 --- a/src/backend/common/AbstractComponent.ts +++ b/src/backend/common/AbstractComponent.ts @@ -50,6 +50,7 @@ export default abstract class AbstractComponent extends AbstractInitializable { protected db: DbConcrete; protected componentRepo!: DrizzleComponentRepository; protected dbComponent!: ComponentSelect; + componentId!: number; protected retentionOpts: RetentionOptions; protected componentType: 'source' | 'client'; @@ -100,6 +101,7 @@ export default abstract class AbstractComponent extends AbstractInitializable { uid: this.config.id ?? this.config.name ?? name, name: this.config.name ?? name }); + this.componentId = this.dbComponent.id; return true; } diff --git a/src/backend/common/database/drizzle/repositories/PlayHistoricalRepository.ts b/src/backend/common/database/drizzle/repositories/PlayHistoricalRepository.ts index 2523044e..4c0e600f 100644 --- a/src/backend/common/database/drizzle/repositories/PlayHistoricalRepository.ts +++ b/src/backend/common/database/drizzle/repositories/PlayHistoricalRepository.ts @@ -283,6 +283,13 @@ export class DrizzlePlayHistoricalRepository extends DrizzleBaseRepository<'play where, })) as PlayHistoricalSelect[]).map(x => ({...x, play: hydratePlaySelect(x)})); } + + public getPlayCountByComponent = async () => { + + const res = await this.db.all(sql`select componentId, count(*) from plays_historical p +group by componentId;`); + return res; + } } export const buildPlayHistoricalWhere = (args: PlayWhereOpts): WhereClause<'playsHistorical'> => { diff --git a/src/backend/common/database/drizzle/repositories/PlayRepository.ts b/src/backend/common/database/drizzle/repositories/PlayRepository.ts index 6e4bbc4f..2177f2c2 100644 --- a/src/backend/common/database/drizzle/repositories/PlayRepository.ts +++ b/src/backend/common/database/drizzle/repositories/PlayRepository.ts @@ -690,6 +690,21 @@ export class DrizzlePlayRepository extends DrizzleBaseRepository<'plays'> { with: buildPlayWith(qWith) })) as PlayWith<'queueStates'>[]).map(x => ({...x, play: hydratePlaySelect(x)})); } + + public getComponentPlayCountByState = async (componentId?: string) => { + + const res = await this.db.all(sql`select state, count(*) from plays p +where componentId = ${componentId ?? this.componentId} +group by state;`); + return res; + } + + public getPlayCountByState = async () => { + + const res = await this.db.all(sql`select state,componentId, count(*) from plays p +group by state,componentId;`); + return res; + } } export const getTemporallyCloseDateCompareOp = (play: PlayObject, opts: {bufferTime?: number, useCompleted?: boolean} = {}): CompareDateOp => { diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index 336d3c3d..6c63ad38 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -11,6 +11,8 @@ import { DeadLetterScrobble, LeveledLogData, LogOutputConfig, + PLAY_CLIENT_STATE, + PLAY_SOURCE_STATE, PlayObject, SOURCE_SOT, SOURCE_SOT_TYPES, @@ -35,9 +37,10 @@ import ScrobbleSources from "../sources/ScrobbleSources.js"; import ScrobbleClients from "../scrobblers/ScrobbleClients.js"; import prom from 'prom-client'; import { SimpleError } from "../common/errors/MSErrors.js"; -import { QueryPlaysOpts } from "../common/database/drizzle/repositories/PlayRepository.js"; +import { DrizzlePlayRepository, QueryPlaysOpts } from "../common/database/drizzle/repositories/PlayRepository.js"; import { playSelectToDeadScrobble } from "../common/database/drizzle/entityUtils.js"; import AbstractHistoricalScrobbleClient from "../scrobblers/AbstractHistoricalScrobbleClient.js"; +import { DrizzlePlayHistoricalRepository } from "../common/database/drizzle/repositories/PlayHistoricalRepository.js"; const maxBufferSize = 300; const output: Record> = {}; @@ -588,12 +591,71 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro } }); + + let playRepo: DrizzlePlayRepository, + playHistoricalRepo: DrizzlePlayHistoricalRepository; + + const sourcePlays = new prom.Gauge({ + name: 'multiscrobbler_source_plays', + help: 'Count of stored plays by state for Sources', + labelNames: ['name', 'type'], + async collect() { + const res = await playRepo.getPlayCountByState(); + for(const source of scrobbleSources.sources) { + const relevant = res.filter(x => x['componentId'] === source.componentId); + for(const s of PLAY_SOURCE_STATE) { + const rel = relevant.find(x => x['state'] === s); + const count = rel === undefined ? 0 : rel['count(*)']; + this.labels({name: source.getSafeExternalName(), type: s}).set(count); + } + } + } + }); + const clientPlays = new prom.Gauge({ + name: 'multiscrobbler_client_plays', + help: 'Count of stored plays by state for Clients', + labelNames: ['name', 'type'], + async collect() { + const res = await playRepo.getPlayCountByState(); + for(const client of scrobbleClients.clients) { + const relevant = res.filter(x => x['componentId'] === client.componentId); + for(const s of PLAY_CLIENT_STATE) { + const rel = relevant.find(x => x['state'] === s); + const count = rel === undefined ? 0 : rel['count(*)']; + this.labels({name: client.getSafeExternalName(), type: s}).set(count); + } + } + } + }); + const clientHistoricalPlays = new prom.Gauge({ + name: 'multiscrobbler_client_historical_plays', + help: 'Count of stored historical plays for Clients', + labelNames: ['name', 'type'], + async collect() { + const res = await playHistoricalRepo.getPlayCountByComponent(); + for(const client of scrobbleClients.clients) { + if(client instanceof AbstractHistoricalScrobbleClient) { + const relevant = res.filter(x => x['componentId'] === client.componentId); + for(const rel of relevant) { + this.labels({name: client.getSafeExternalName()}).set(rel['count(*)']); + } + } + } + } + }); + if(process.env.PROMETHEUS_FULL === 'true') { prom.collectDefaultMetrics(); } app.get('/api/metrics', async (req, res) => { + if(playRepo === undefined) { + const db = await getRoot().items.db(); + playRepo = new DrizzlePlayRepository(db); + playHistoricalRepo = new DrizzlePlayHistoricalRepository(db); + } + const metricsString = await prom.register.metrics(); return res .status(200) diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index 22c8f5ea..069b1ae7 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -622,4 +622,14 @@ export type TypesAreEqual = (() => G extends T ? 1 : 2) extends (() => G extends U ? 1 : 2) ? Y : N; -export type MBID = `${string}-${string}-${string}-${string}-${string}` \ No newline at end of file +export type MBID = `${string}-${string}-${string}-${string}-${string}` + +// ['queued','discovered','discarded','scrobbled','failed','duped'] +export type PlayStateCommon = 'queued' |'discarded' | 'failed'; +export const PLAY_STATE_COMMON: PlayStateCommon[] = ['queued', 'discarded', 'failed']; +export type PlaySourceState = PlayStateCommon | 'discovered'; +export const PLAY_SOURCE_STATE: PlaySourceState[] = [...PLAY_STATE_COMMON, 'discovered']; +export type PlayClientState = PlayStateCommon | 'duped' | 'scrobbled'; +export const PLAY_CLIENT_STATE = [...PLAY_STATE_COMMON, 'duped', 'scrobbled']; +export type PlayState = PlaySourceState | PlayClientState; +export const PLAY_STATES = Array.from(new Set(...PLAY_CLIENT_STATE, ...PLAY_SOURCE_STATE));