11import { snakeCase } from 'lodash' ;
22import { OnEvent } from 'src/decorators' ;
3- import { ImmichWorker , JobStatus } from 'src/enum' ;
4- import { ArgOf , ArgsOf } from 'src/repositories/event.repository' ;
3+ import { ImmichTelemetry , ImmichWorker , JobStatus , QueueName } from 'src/enum' ;
4+ import { ArgOf } from 'src/repositories/event.repository' ;
55import { BaseService } from 'src/services/base.service' ;
66
7+ const QUEUE_METRICS_POLLING_INTERVAL = 5000 ;
8+
79export class TelemetryService extends BaseService {
10+ private queueWaitingCounts = new Map < string , number > ( ) ;
11+ private queuePausedCounts = new Map < string , number > ( ) ;
12+ private queueDelayedCounts = new Map < string , number > ( ) ;
13+ private queueActiveCounts = new Map < string , number > ( ) ;
14+ private pollingInterval ?: NodeJS . Timeout ;
15+
816 @OnEvent ( { name : 'AppBootstrap' , workers : [ ImmichWorker . Api ] } )
917 async onBootstrap ( ) : Promise < void > {
1018 const userCount = await this . userRepository . getCount ( ) ;
1119 this . telemetryRepository . api . addToGauge ( 'immich.users.total' , userCount ) ;
20+
21+ const { telemetry } = this . configRepository . getEnv ( ) ;
22+ if ( telemetry . metrics . has ( ImmichTelemetry . Job ) ) {
23+ // Register observable gauges for queued metrics
24+ this . registerQueuedMetrics ( ) ;
25+
26+ // Start polling queue statistics
27+ await this . updateQueuedMetrics ( ) ;
28+ this . pollingInterval = setInterval ( ( ) => {
29+ void this . updateQueuedMetrics ( ) ;
30+ } , QUEUE_METRICS_POLLING_INTERVAL ) ;
31+ }
32+ }
33+
34+ @OnEvent ( { name : 'AppShutdown' } )
35+ onShutdown ( ) : void {
36+ if ( this . pollingInterval ) {
37+ clearInterval ( this . pollingInterval ) ;
38+ }
39+ }
40+
41+ private registerQueuedMetrics ( ) : void {
42+ for ( const queueName of Object . values ( QueueName ) ) {
43+ const queueKey = snakeCase ( queueName ) ;
44+
45+ this . telemetryRepository . jobs . setObservableGauge (
46+ `immich.queues.${ queueKey } .waiting` ,
47+ ( ) => this . queueWaitingCounts . get ( queueKey ) ?? 0 ,
48+ { description : `Number of waiting jobs in ${ queueName } queue` } ,
49+ ) ;
50+
51+ this . telemetryRepository . jobs . setObservableGauge (
52+ `immich.queues.${ queueKey } .paused` ,
53+ ( ) => this . queuePausedCounts . get ( queueKey ) ?? 0 ,
54+ { description : `Number of paused jobs in ${ queueName } queue` } ,
55+ ) ;
56+
57+ this . telemetryRepository . jobs . setObservableGauge (
58+ `immich.queues.${ queueKey } .delayed` ,
59+ ( ) => this . queueDelayedCounts . get ( queueKey ) ?? 0 ,
60+ { description : `Number of delayed jobs in ${ queueName } queue` } ,
61+ ) ;
62+
63+ this . telemetryRepository . jobs . setObservableGauge (
64+ `immich.queues.${ queueKey } .active` ,
65+ ( ) => this . queueActiveCounts . get ( queueKey ) ?? 0 ,
66+ { description : `Number of active jobs in ${ queueName } queue` } ,
67+ ) ;
68+ }
69+ }
70+
71+ private async updateQueuedMetrics ( ) : Promise < void > {
72+ await Promise . all (
73+ Object . values ( QueueName ) . map ( async ( queueName ) => {
74+ try {
75+ const stats = await this . jobRepository . getJobCounts ( queueName ) ;
76+ const queueKey = snakeCase ( queueName ) ;
77+ this . queueWaitingCounts . set ( queueKey , stats . waiting ) ;
78+ this . queuePausedCounts . set ( queueKey , stats . paused ) ;
79+ this . queueDelayedCounts . set ( queueKey , stats . delayed ) ;
80+ this . queueActiveCounts . set ( queueKey , stats . active ) ;
81+ } catch ( error ) {
82+ this . logger . debug ( `Failed to update queued metrics for ${ queueName } : ${ error } ` ) ;
83+ }
84+ } ) ,
85+ ) ;
1286 }
1387
1488 @OnEvent ( { name : 'UserCreate' } )
@@ -26,12 +100,6 @@ export class TelemetryService extends BaseService {
26100 this . telemetryRepository . api . addToGauge ( `immich.users.total` , 1 ) ;
27101 }
28102
29- @OnEvent ( { name : 'JobStart' } )
30- onJobStart ( ...[ queueName ] : ArgsOf < 'JobStart' > ) {
31- const queueMetric = `immich.queues.${ snakeCase ( queueName ) } .active` ;
32- this . telemetryRepository . jobs . addToGauge ( queueMetric , 1 ) ;
33- }
34-
35103 @OnEvent ( { name : 'JobSuccess' } )
36104 onJobSuccess ( { job, response } : ArgOf < 'JobSuccess' > ) {
37105 if ( response && Object . values ( JobStatus ) . includes ( response as JobStatus ) ) {
@@ -46,12 +114,6 @@ export class TelemetryService extends BaseService {
46114 this . telemetryRepository . jobs . addToCounter ( jobMetric , 1 ) ;
47115 }
48116
49- @OnEvent ( { name : 'JobComplete' } )
50- onJobComplete ( ...[ queueName ] : ArgsOf < 'JobComplete' > ) {
51- const queueMetric = `immich.queues.${ snakeCase ( queueName ) } .active` ;
52- this . telemetryRepository . jobs . addToGauge ( queueMetric , - 1 ) ;
53- }
54-
55117 @OnEvent ( { name : 'QueueStart' } )
56118 onQueueStart ( { name } : ArgOf < 'QueueStart' > ) {
57119 this . telemetryRepository . jobs . addToCounter ( `immich.queues.${ snakeCase ( name ) } .started` , 1 ) ;
0 commit comments