Browse Source

Feature/extend health check endpoint by database and cache operations (#4188)

* Extend health check endpoint by database and cache operations

* Update changelog
pull/4197/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
e4968dbea7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 20
      apps/api/src/app/health/health.controller.ts
  3. 4
      apps/api/src/app/health/health.module.ts
  4. 27
      apps/api/src/app/health/health.service.ts
  5. 21
      apps/api/src/app/redis-cache/redis-cache.service.ts

1
CHANGELOG.md

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the health check endpoint to include database and cache operations (experimental)
- Refactored various `lodash` functions with native JavaScript equivalents
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.1.0` to `6.2.1`

20
apps/api/src/app/health/health.controller.ts

@ -3,13 +3,14 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import {
Controller,
Get,
HttpCode,
HttpException,
HttpStatus,
Param,
Res,
UseInterceptors
} from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { HealthService } from './health.service';
@ -19,9 +20,20 @@ export class HealthController {
public constructor(private readonly healthService: HealthService) {}
@Get()
@HttpCode(HttpStatus.OK)
public getHealth() {
return { status: getReasonPhrase(StatusCodes.OK) };
public async getHealth(@Res() response: Response) {
const databaseServiceHealthy = await this.healthService.isDatabaseHealthy();
const redisCacheServiceHealthy =
await this.healthService.isRedisCacheHealthy();
if (databaseServiceHealthy && redisCacheServiceHealthy) {
return response
.status(HttpStatus.OK)
.json({ status: getReasonPhrase(StatusCodes.OK) });
} else {
return response
.status(HttpStatus.SERVICE_UNAVAILABLE)
.json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) });
}
}
@Get('data-enhancer/:name')

4
apps/api/src/app/health/health.module.ts

@ -1,6 +1,8 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common';
@ -12,6 +14,8 @@ import { HealthService } from './health.service';
imports: [
DataEnhancerModule,
DataProviderModule,
PropertyModule,
RedisCacheModule,
TransformDataSourceInRequestModule
],
providers: [HealthService]

27
apps/api/src/app/health/health.service.ts

@ -1,5 +1,8 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { DataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@ -8,7 +11,9 @@ import { DataSource } from '@prisma/client';
export class HealthService {
public constructor(
private readonly dataEnhancerService: DataEnhancerService,
private readonly dataProviderService: DataProviderService
private readonly dataProviderService: DataProviderService,
private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService
) {}
public async hasResponseFromDataEnhancer(aName: string) {
@ -18,4 +23,24 @@ export class HealthService {
public async hasResponseFromDataProvider(aDataSource: DataSource) {
return this.dataProviderService.checkQuote(aDataSource);
}
public async isDatabaseHealthy() {
try {
await this.propertyService.getByKey(PROPERTY_CURRENCIES);
return true;
} catch {
return false;
}
}
public async isRedisCacheHealthy() {
try {
const isHealthy = await this.redisCacheService.isHealthy();
return isHealthy;
} catch {
return false;
}
}
}

21
apps/api/src/app/redis-cache/redis-cache.service.ts

@ -7,6 +7,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { Milliseconds } from 'cache-manager';
import { RedisCache } from 'cache-manager-redis-yet';
import { createHash } from 'crypto';
import ms from 'ms';
@Injectable()
export class RedisCacheService {
@ -59,6 +60,26 @@ export class RedisCacheService {
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
}
public async isHealthy() {
try {
const client = this.cache.store.client;
const isHealthy = await Promise.race([
client.ping(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error('Redis health check timeout')),
ms('2 seconds')
)
)
]);
return isHealthy === 'PONG';
} catch (error) {
return false;
}
}
public async remove(key: string) {
return this.cache.del(key);
}

Loading…
Cancel
Save