Browse Source
Feature/add health check endpoints (#1886)
* Add health check endpoints
* Update changelog
pull/1887/head
Thomas Kaul
2 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with
125 additions and
1 deletions
-
CHANGELOG.md
-
apps/api/src/app/app.module.ts
-
apps/api/src/app/health/health.controller.ts
-
apps/api/src/app/health/health.module.ts
-
apps/api/src/app/health/health.service.ts
-
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
-
apps/api/src/services/data-provider/coingecko/coingecko.service.ts
-
apps/api/src/services/data-provider/data-provider.service.ts
-
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
-
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
-
apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
-
apps/api/src/services/data-provider/manual/manual.service.ts
-
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
-
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
|
|
@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 |
|
|
|
### Added |
|
|
|
|
|
|
|
- Added a fallback to historical market data if a data provider does not provide live data |
|
|
|
- Added a general health check endpoint |
|
|
|
- Added health check endpoints for data providers |
|
|
|
|
|
|
|
### Changed |
|
|
|
|
|
|
|
|
|
@ -24,6 +24,7 @@ import { CacheModule } from './cache/cache.module'; |
|
|
|
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; |
|
|
|
import { ExportModule } from './export/export.module'; |
|
|
|
import { FrontendMiddleware } from './frontend.middleware'; |
|
|
|
import { HealthModule } from './health/health.module'; |
|
|
|
import { ImportModule } from './import/import.module'; |
|
|
|
import { InfoModule } from './info/info.module'; |
|
|
|
import { LogoModule } from './logo/logo.module'; |
|
|
@ -57,6 +58,7 @@ import { UserModule } from './user/user.module'; |
|
|
|
ExchangeRateModule, |
|
|
|
ExchangeRateDataModule, |
|
|
|
ExportModule, |
|
|
|
HealthModule, |
|
|
|
ImportModule, |
|
|
|
InfoModule, |
|
|
|
LogoModule, |
|
|
|
|
|
@ -0,0 +1,44 @@ |
|
|
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; |
|
|
|
import { |
|
|
|
Controller, |
|
|
|
Get, |
|
|
|
HttpException, |
|
|
|
Param, |
|
|
|
UseInterceptors |
|
|
|
} from '@nestjs/common'; |
|
|
|
|
|
|
|
import { HealthService } from './health.service'; |
|
|
|
import { DataSource } from '@prisma/client'; |
|
|
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
|
|
|
|
|
|
|
@Controller('health') |
|
|
|
export class HealthController { |
|
|
|
public constructor(private readonly healthService: HealthService) {} |
|
|
|
|
|
|
|
@Get() |
|
|
|
public async getHealth() {} |
|
|
|
|
|
|
|
@Get('data-provider/:dataSource') |
|
|
|
@UseInterceptors(TransformDataSourceInRequestInterceptor) |
|
|
|
public async getHealthOfDataProvider( |
|
|
|
@Param('dataSource') dataSource: DataSource |
|
|
|
) { |
|
|
|
if (!DataSource[dataSource]) { |
|
|
|
throw new HttpException( |
|
|
|
getReasonPhrase(StatusCodes.NOT_FOUND), |
|
|
|
StatusCodes.NOT_FOUND |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const hasResponse = await this.healthService.hasResponseFromDataProvider( |
|
|
|
dataSource |
|
|
|
); |
|
|
|
|
|
|
|
if (hasResponse !== true) { |
|
|
|
throw new HttpException( |
|
|
|
getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE), |
|
|
|
StatusCodes.SERVICE_UNAVAILABLE |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -0,0 +1,13 @@ |
|
|
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; |
|
|
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|
|
|
import { Module } from '@nestjs/common'; |
|
|
|
|
|
|
|
import { HealthController } from './health.controller'; |
|
|
|
import { HealthService } from './health.service'; |
|
|
|
|
|
|
|
@Module({ |
|
|
|
controllers: [HealthController], |
|
|
|
imports: [ConfigurationModule, DataProviderModule], |
|
|
|
providers: [HealthService] |
|
|
|
}) |
|
|
|
export class HealthModule {} |
|
|
@ -0,0 +1,14 @@ |
|
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; |
|
|
|
import { Injectable } from '@nestjs/common'; |
|
|
|
import { DataSource } from '@prisma/client'; |
|
|
|
|
|
|
|
@Injectable() |
|
|
|
export class HealthService { |
|
|
|
public constructor( |
|
|
|
private readonly dataProviderService: DataProviderService |
|
|
|
) {} |
|
|
|
|
|
|
|
public async hasResponseFromDataProvider(aDataSource: DataSource) { |
|
|
|
return this.dataProviderService.checkQuote(aDataSource); |
|
|
|
} |
|
|
|
} |
|
|
@ -7,7 +7,7 @@ import { |
|
|
|
} from '@ghostfolio/api/services/interfaces/interfaces'; |
|
|
|
import { DATE_FORMAT } from '@ghostfolio/common/helper'; |
|
|
|
import { Granularity } from '@ghostfolio/common/types'; |
|
|
|
import { Injectable, Logger } from '@nestjs/common'; |
|
|
|
import { Injectable } from '@nestjs/common'; |
|
|
|
import { DataSource, SymbolProfile } from '@prisma/client'; |
|
|
|
import { format, isAfter, isBefore, parse } from 'date-fns'; |
|
|
|
|
|
|
@ -110,6 +110,10 @@ export class AlphaVantageService implements DataProviderInterface { |
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
public getTestSymbol() { |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
|
const result = await this.alphaVantage.data.search(aQuery); |
|
|
|
|
|
|
|
|
|
@ -160,6 +160,10 @@ export class CoinGeckoService implements DataProviderInterface { |
|
|
|
return results; |
|
|
|
} |
|
|
|
|
|
|
|
public getTestSymbol() { |
|
|
|
return 'bitcoin'; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
|
let items: LookupItem[] = []; |
|
|
|
|
|
|
|
|
|
@ -38,6 +38,24 @@ export class DataProviderService { |
|
|
|
}) ?? {}; |
|
|
|
} |
|
|
|
|
|
|
|
public async checkQuote(dataSource: DataSource) { |
|
|
|
const dataProvider = this.getDataProvider(dataSource); |
|
|
|
const symbol = dataProvider.getTestSymbol(); |
|
|
|
|
|
|
|
const quotes = await this.getQuotes([ |
|
|
|
{ |
|
|
|
dataSource, |
|
|
|
symbol |
|
|
|
} |
|
|
|
]); |
|
|
|
|
|
|
|
if (quotes[symbol]?.marketPrice > 0) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
public async getDividends({ |
|
|
|
dataSource, |
|
|
|
from, |
|
|
|
|
|
@ -172,6 +172,10 @@ export class EodHistoricalDataService implements DataProviderInterface { |
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
public getTestSymbol() { |
|
|
|
return 'AAPL.US'; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
|
const searchResult = await this.getSearchResult(aQuery); |
|
|
|
|
|
|
|
|
|
@ -143,6 +143,10 @@ export class GoogleSheetsService implements DataProviderInterface { |
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
public getTestSymbol() { |
|
|
|
return 'INDEXSP:.INX'; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
|
const items = await this.prismaService.symbolProfile.findMany({ |
|
|
|
select: { |
|
|
|
|
|
@ -40,5 +40,7 @@ export interface DataProviderInterface { |
|
|
|
aSymbols: string[] |
|
|
|
): Promise<{ [symbol: string]: IDataProviderResponse }>; |
|
|
|
|
|
|
|
getTestSymbol(): string; |
|
|
|
|
|
|
|
search(aQuery: string): Promise<{ items: LookupItem[] }>; |
|
|
|
} |
|
|
|
|
|
@ -163,6 +163,10 @@ export class ManualService implements DataProviderInterface { |
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
public getTestSymbol() { |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
|
let items = await this.prismaService.symbolProfile.findMany({ |
|
|
|
select: { |
|
|
|
|
|
@ -113,6 +113,10 @@ export class RapidApiService implements DataProviderInterface { |
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
public getTestSymbol() { |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
|
return { items: [] }; |
|
|
|
} |
|
|
|
|
|
@ -167,6 +167,7 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
if (aSymbols.length <= 0) { |
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
const yahooFinanceSymbols = aSymbols.map((symbol) => |
|
|
|
this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(symbol) |
|
|
|
); |
|
|
@ -251,6 +252,10 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public getTestSymbol() { |
|
|
|
return 'AAPL'; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
|
const items: LookupItem[] = []; |
|
|
|
|
|
|
|