Browse Source

Merge branch 'main' into task/i18n-ukrainian-single-words

pull/6978/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
e94552f485
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 4
      apps/api/src/app/admin/admin.controller.ts
  3. 4
      apps/api/src/app/auth/auth.module.ts
  4. 4
      apps/api/src/app/auth/google.strategy.ts
  5. 9
      apps/api/src/app/auth/oidc.strategy.ts
  6. 6
      apps/api/src/app/auth/web-auth.service.ts
  7. 7
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
  8. 12
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  9. 4
      apps/api/src/app/health/health.controller.ts
  10. 4
      apps/api/src/app/import/import.controller.ts
  11. 7
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  12. 6
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  13. 7
      apps/api/src/app/portfolio/portfolio.service.ts
  14. 6
      apps/api/src/app/redis-cache/redis-cache.service.ts
  15. 14
      apps/api/src/app/subscription/subscription.controller.ts
  16. 9
      apps/api/src/app/subscription/subscription.service.ts
  17. 4
      apps/api/src/app/symbol/symbol.service.ts
  18. 12
      apps/api/src/events/asset-profile-changed.listener.ts
  19. 7
      apps/api/src/events/portfolio-changed.listener.ts
  20. 4
      apps/api/src/interceptors/performance-logging/performance-logging.service.ts
  21. 18
      apps/api/src/main.ts
  22. 8
      apps/api/src/middlewares/html-template.middleware.ts
  23. 6
      apps/api/src/services/benchmark/benchmark.service.ts
  24. 8
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  25. 7
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  26. 6
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  27. 31
      apps/api/src/services/data-provider/data-provider.service.ts
  28. 21
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  29. 13
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  30. 12
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  31. 4
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  32. 9
      apps/api/src/services/data-provider/manual/manual.service.ts
  33. 6
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  34. 23
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  35. 19
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  36. 24
      apps/api/src/services/fetch/fetch.service.ts
  37. 8
      apps/api/src/services/i18n/i18n.service.ts
  38. 4
      apps/api/src/services/prisma/prisma.service.ts
  39. 42
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  40. 15
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  41. 17
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  42. 55
      apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts
  43. 9
      apps/api/src/services/twitter-bot/twitter-bot.service.ts

1
CHANGELOG.md

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Refactored the backend logging to use the instance-based `Logger`
- Improved the language localization for Ukrainian (`uk`) - Improved the language localization for Ukrainian (`uk`)
### Fixed ### Fixed

4
apps/api/src/app/admin/admin.controller.ts

@ -58,6 +58,8 @@ import { AdminService } from './admin.service';
@Controller('admin') @Controller('admin')
export class AdminController { export class AdminController {
private readonly logger = new Logger(AdminController.name);
public constructor( public constructor(
private readonly adminService: AdminService, private readonly adminService: AdminService,
private readonly apiService: ApiService, private readonly apiService: ApiService,
@ -260,7 +262,7 @@ export class AdminController {
`Could not parse the market price for ${symbol} (${dataSource})` `Could not parse the market price for ${symbol} (${dataSource})`
); );
} catch (error) { } catch (error) {
Logger.error(error, 'AdminController'); this.logger.error(error);
throw new HttpException(error.message, StatusCodes.BAD_REQUEST); throw new HttpException(error.message, StatusCodes.BAD_REQUEST);
} }

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

@ -50,6 +50,8 @@ import { OidcStrategy } from './oidc.strategy';
configurationService: ConfigurationService, configurationService: ConfigurationService,
fetchService: FetchService fetchService: FetchService
) => { ) => {
const logger = new Logger('OidcStrategy');
const isOidcEnabled = configurationService.get( const isOidcEnabled = configurationService.get(
'ENABLE_FEATURE_AUTH_OIDC' 'ENABLE_FEATURE_AUTH_OIDC'
); );
@ -101,7 +103,7 @@ import { OidcStrategy } from './oidc.strategy';
tokenURL = manualTokenUrl || config.token_endpoint; tokenURL = manualTokenUrl || config.token_endpoint;
userInfoURL = manualUserInfoUrl || config.userinfo_endpoint; userInfoURL = manualUserInfoUrl || config.userinfo_endpoint;
} catch (error) { } catch (error) {
Logger.error(error, 'OidcStrategy'); logger.error(error);
throw new Error('Failed to fetch OIDC configuration from issuer'); throw new Error('Failed to fetch OIDC configuration from issuer');
} }
} }

4
apps/api/src/app/auth/google.strategy.ts

@ -10,6 +10,8 @@ import { AuthService } from './auth.service';
@Injectable() @Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
private readonly logger = new Logger(GoogleStrategy.name);
public constructor( public constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
configurationService: ConfigurationService configurationService: ConfigurationService
@ -40,7 +42,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
done(null, { jwt }); done(null, { jwt });
} catch (error) { } catch (error) {
Logger.error(error, 'GoogleStrategy'); this.logger.error(error);
done(error, false); done(error, false);
} }
} }

9
apps/api/src/app/auth/oidc.strategy.ts

@ -15,6 +15,8 @@ import { OidcStateStore } from './oidc-state.store';
@Injectable() @Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
private readonly logger = new Logger(OidcStrategy.name);
private static readonly stateStore = new OidcStateStore(); private static readonly stateStore = new OidcStateStore();
public constructor( public constructor(
@ -52,9 +54,8 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
}); });
if (!thirdPartyId) { if (!thirdPartyId) {
Logger.error( this.logger.error(
`Missing subject identifier in OIDC response from ${issuer}`, `Missing subject identifier in OIDC response from ${issuer}`
'OidcStrategy'
); );
throw new Error('Missing subject identifier in OIDC response'); throw new Error('Missing subject identifier in OIDC response');
@ -62,7 +63,7 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
return { jwt }; return { jwt };
} catch (error) { } catch (error) {
Logger.error(error, 'OidcStrategy'); this.logger.error(error);
throw error; throw error;
} }
} }

6
apps/api/src/app/auth/web-auth.service.ts

@ -33,6 +33,8 @@ import ms from 'ms';
@Injectable() @Injectable()
export class WebAuthService { export class WebAuthService {
private readonly logger = new Logger(WebAuthService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly deviceService: AuthDeviceService, private readonly deviceService: AuthDeviceService,
@ -103,7 +105,7 @@ export class WebAuthService {
verification = await verifyRegistrationResponse(opts); verification = await verifyRegistrationResponse(opts);
} catch (error) { } catch (error) {
Logger.error(error, 'WebAuthService'); this.logger.error(error);
throw new InternalServerErrorException(error.message); throw new InternalServerErrorException(error.message);
} }
@ -210,7 +212,7 @@ export class WebAuthService {
verification = await verifyAuthenticationResponse(opts); verification = await verifyAuthenticationResponse(opts);
} catch (error) { } catch (error) {
Logger.error(error, 'WebAuthService'); this.logger.error(error);
throw new InternalServerErrorException({ error: error.message }); throw new InternalServerErrorException({ error: error.message });
} }

7
apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

@ -17,6 +17,8 @@ import { isNumber } from 'lodash';
@Injectable() @Injectable()
export class BenchmarksService { export class BenchmarksService {
private readonly logger = new Logger(BenchmarksService.name);
public constructor( public constructor(
private readonly benchmarkService: BenchmarkService, private readonly benchmarkService: BenchmarkService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
@ -96,12 +98,11 @@ export class BenchmarksService {
})?.marketPrice; })?.marketPrice;
if (!marketPriceAtStartDate) { if (!marketPriceAtStartDate) {
Logger.error( this.logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) at ${format( `No historical market data has been found for ${symbol} (${dataSource}) at ${format(
startDate, startDate,
DATE_FORMAT DATE_FORMAT
)}`, )}`
'BenchmarkService'
); );
return { marketData }; return { marketData };

12
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -34,6 +34,8 @@ import { Big } from 'big.js';
@Injectable() @Injectable()
export class GhostfolioService { export class GhostfolioService {
private readonly logger = new Logger(GhostfolioService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
@ -99,7 +101,7 @@ export class GhostfolioService {
return result; return result;
} catch (error) { } catch (error) {
Logger.error(error, 'GhostfolioService'); this.logger.error(error);
throw error; throw error;
} }
@ -141,7 +143,7 @@ export class GhostfolioService {
return result; return result;
} catch (error) { } catch (error) {
Logger.error(error, 'GhostfolioService'); this.logger.error(error);
throw error; throw error;
} }
@ -183,7 +185,7 @@ export class GhostfolioService {
return result; return result;
} catch (error) { } catch (error) {
Logger.error(error, 'GhostfolioService'); this.logger.error(error);
throw error; throw error;
} }
@ -271,7 +273,7 @@ export class GhostfolioService {
return results; return results;
} catch (error) { } catch (error) {
Logger.error(error, 'GhostfolioService'); this.logger.error(error);
throw error; throw error;
} }
@ -348,7 +350,7 @@ export class GhostfolioService {
return results; return results;
} catch (error) { } catch (error) {
Logger.error(error, 'GhostfolioService'); this.logger.error(error);
throw error; throw error;
} }

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

@ -24,6 +24,8 @@ import { HealthService } from './health.service';
@Controller('health') @Controller('health')
export class HealthController { export class HealthController {
private readonly logger = new Logger(HealthController.name);
public constructor( public constructor(
private readonly aiService: AiService, private readonly aiService: AiService,
private readonly healthService: HealthService private readonly healthService: HealthService
@ -61,7 +63,7 @@ export class HealthController {
.json({ status: getReasonPhrase(StatusCodes.OK) }); .json({ status: getReasonPhrase(StatusCodes.OK) });
} }
} catch (error) { } catch (error) {
Logger.error(error, 'HealthController'); this.logger.error(error);
} }
return response return response

4
apps/api/src/app/import/import.controller.ts

@ -31,6 +31,8 @@ import { ImportService } from './import.service';
@Controller('import') @Controller('import')
export class ImportController { export class ImportController {
private readonly logger = new Logger(ImportController.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly importService: ImportService, private readonly importService: ImportService,
@ -81,7 +83,7 @@ export class ImportController {
return { activities }; return { activities };
} catch (error) { } catch (error) {
Logger.error(error, ImportController); this.logger.error(error);
throw new HttpException( throw new HttpException(
{ {

7
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -62,6 +62,8 @@ import { isNumber, sortBy, sum, uniqBy } from 'lodash';
export abstract class PortfolioCalculator { export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false; protected static readonly ENABLE_LOGGING = false;
protected readonly logger = new Logger(PortfolioCalculator.name);
protected accountBalanceItems: HistoricalDataItem[]; protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[]; protected activities: PortfolioOrder[];
@ -1119,12 +1121,11 @@ export abstract class PortfolioCalculator {
if (cachedPortfolioSnapshot) { if (cachedPortfolioSnapshot) {
this.snapshot = cachedPortfolioSnapshot; this.snapshot = cachedPortfolioSnapshot;
Logger.debug( this.logger.debug(
`Fetched portfolio snapshot from cache in ${( `Fetched portfolio snapshot from cache in ${(
(performance.now() - startTimeTotal) / (performance.now() - startTimeTotal) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
'PortfolioCalculator'
); );
if (isCachedPortfolioSnapshotExpired) { if (isCachedPortfolioSnapshotExpired) {

6
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -11,7 +11,6 @@ import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { import {
addMilliseconds, addMilliseconds,
@ -96,9 +95,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
currentPosition.timeWeightedInvestmentWithCurrencyEffect currentPosition.timeWeightedInvestmentWithCurrencyEffect
); );
} else if (!currentPosition.quantity.eq(0)) { } else if (!currentPosition.quantity.eq(0)) {
Logger.warn( this.logger.warn(
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, `Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`
'PortfolioCalculator'
); );
hasErrors = true; hasErrors = true;

7
apps/api/src/app/portfolio/portfolio.service.ts

@ -108,6 +108,8 @@ const europeMarkets = require('../../assets/countries/europe-markets.json');
@Injectable() @Injectable()
export class PortfolioService { export class PortfolioService {
private readonly logger = new Logger(PortfolioService.name);
public constructor( public constructor(
private readonly accountBalanceService: AccountBalanceService, private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService, private readonly accountService: AccountService,
@ -619,9 +621,8 @@ export class PortfolioService {
symbolProfileMap[getAssetProfileIdentifier({ dataSource, symbol })]; symbolProfileMap[getAssetProfileIdentifier({ dataSource, symbol })];
if (!assetProfile) { if (!assetProfile) {
Logger.warn( this.logger.warn(
`Asset profile not found for ${symbol} (${dataSource})`, `Asset profile not found for ${symbol} (${dataSource})`
'PortfolioService'
); );
continue; continue;

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

@ -10,6 +10,8 @@ import { createHash, randomUUID } from 'node:crypto';
@Injectable() @Injectable()
export class RedisCacheService { export class RedisCacheService {
private readonly logger = new Logger(RedisCacheService.name);
private client: Keyv; private client: Keyv;
public constructor( public constructor(
@ -27,7 +29,7 @@ export class RedisCacheService {
}; };
this.client.on('error', (error) => { this.client.on('error', (error) => {
Logger.error(error, 'RedisCacheService'); this.logger.error(error);
}); });
} }
@ -101,7 +103,7 @@ export class RedisCacheService {
return true; return true;
} catch (error) { } catch (error) {
Logger.error(error?.message, 'RedisCacheService'); this.logger.error(error?.message);
return false; return false;
} finally { } finally {

14
apps/api/src/app/subscription/subscription.controller.ts

@ -33,6 +33,8 @@ import { SubscriptionService } from './subscription.service';
@Controller('subscription') @Controller('subscription')
export class SubscriptionController { export class SubscriptionController {
private readonly logger = new Logger(SubscriptionController.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
@ -80,9 +82,8 @@ export class SubscriptionController {
value: JSON.stringify(coupons) value: JSON.stringify(coupons)
}); });
Logger.log( this.logger.log(
`Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}`, `Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}`
'SubscriptionController'
); );
return { return {
@ -101,9 +102,8 @@ export class SubscriptionController {
); );
if (userId) { if (userId) {
Logger.log( this.logger.log(
`Subscription for user '${userId}' has been created via Stripe`, `Subscription for user '${userId}' has been created via Stripe`
'SubscriptionController'
); );
} }
@ -126,7 +126,7 @@ export class SubscriptionController {
user: this.request.user user: this.request.user
}); });
} catch (error) { } catch (error) {
Logger.error(error, 'SubscriptionController'); this.logger.error(error);
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST), getReasonPhrase(StatusCodes.BAD_REQUEST),

9
apps/api/src/app/subscription/subscription.service.ts

@ -24,6 +24,8 @@ import Stripe from 'stripe';
@Injectable() @Injectable()
export class SubscriptionService { export class SubscriptionService {
private readonly logger = new Logger(SubscriptionService.name);
private stripe: Stripe; private stripe: Stripe;
public constructor( public constructor(
@ -166,9 +168,8 @@ export class SubscriptionService {
error instanceof Prisma.PrismaClientKnownRequestError && error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002' error.code === 'P2002'
) { ) {
Logger.log( this.logger.log(
`Stripe Checkout Session '${session.id}' has already been redeemed`, `Stripe Checkout Session '${session.id}' has already been redeemed`
'SubscriptionService'
); );
} else { } else {
throw error; throw error;
@ -177,7 +178,7 @@ export class SubscriptionService {
return session.client_reference_id; return session.client_reference_id;
} catch (error) { } catch (error) {
Logger.error(error, 'SubscriptionService'); this.logger.error(error);
} }
} }

4
apps/api/src/app/symbol/symbol.service.ts

@ -15,6 +15,8 @@ import { format, subDays } from 'date-fns';
@Injectable() @Injectable()
export class SymbolService { export class SymbolService {
private readonly logger = new Logger(SymbolService.name);
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService private readonly marketDataService: MarketDataService
@ -119,7 +121,7 @@ export class SymbolService {
results.items = items; results.items = items;
return results; return results;
} catch (error) { } catch (error) {
Logger.error(error, 'SymbolService'); this.logger.error(error);
throw error; throw error;
} }

12
apps/api/src/events/asset-profile-changed.listener.ts

@ -15,6 +15,8 @@ import { AssetProfileChangedEvent } from './asset-profile-changed.event';
@Injectable() @Injectable()
export class AssetProfileChangedListener { export class AssetProfileChangedListener {
private readonly logger = new Logger(AssetProfileChangedListener.name);
private static readonly DEBOUNCE_DELAY = ms('5 seconds'); private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>(); private debounceTimers = new Map<string, NodeJS.Timeout>();
@ -67,10 +69,7 @@ export class AssetProfileChangedListener {
dataSource: DataSource; dataSource: DataSource;
symbol: string; symbol: string;
}) { }) {
Logger.log( this.logger.log(`Asset profile of ${symbol} (${dataSource}) has changed`);
`Asset profile of ${symbol} (${dataSource}) has changed`,
'AssetProfileChangedListener'
);
if ( if (
this.configurationService.get( this.configurationService.get(
@ -84,10 +83,7 @@ export class AssetProfileChangedListener {
const existingCurrencies = this.exchangeRateDataService.getCurrencies(); const existingCurrencies = this.exchangeRateDataService.getCurrencies();
if (!existingCurrencies.includes(currency)) { if (!existingCurrencies.includes(currency)) {
Logger.log( this.logger.log(`New currency ${currency} has been detected`);
`New currency ${currency} has been detected`,
'AssetProfileChangedListener'
);
await this.exchangeRateDataService.initialize(); await this.exchangeRateDataService.initialize();
} }

7
apps/api/src/events/portfolio-changed.listener.ts

@ -8,6 +8,8 @@ import { PortfolioChangedEvent } from './portfolio-changed.event';
@Injectable() @Injectable()
export class PortfolioChangedListener { export class PortfolioChangedListener {
private readonly logger = new Logger(PortfolioChangedListener.name);
private static readonly DEBOUNCE_DELAY = ms('5 seconds'); private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>(); private debounceTimers = new Map<string, NodeJS.Timeout>();
@ -35,10 +37,7 @@ export class PortfolioChangedListener {
} }
private async processPortfolioChanged({ userId }: { userId: string }) { private async processPortfolioChanged({ userId }: { userId: string }) {
Logger.log( this.logger.log(`Portfolio of user '${userId}' has changed`);
`Portfolio of user '${userId}' has changed`,
'PortfolioChangedListener'
);
await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId }); await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId });
} }

4
apps/api/src/interceptors/performance-logging/performance-logging.service.ts

@ -2,6 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
@Injectable() @Injectable()
export class PerformanceLoggingService { export class PerformanceLoggingService {
private readonly logger = new Logger(PerformanceLoggingService.name);
public logPerformance({ public logPerformance({
className, className,
methodName, methodName,
@ -13,7 +15,7 @@ export class PerformanceLoggingService {
}) { }) {
const endTime = performance.now(); const endTime = performance.now();
Logger.debug( this.logger.debug(
`Completed execution of ${methodName}() in ${((endTime - startTime) / 1000).toFixed(3)} seconds`, `Completed execution of ${methodName}() in ${((endTime - startTime) / 1000).toFixed(3)} seconds`,
className className
); );

18
apps/api/src/main.ts

@ -23,6 +23,8 @@ import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
const logger = new Logger('Bootstrap');
async function bootstrap() { async function bootstrap() {
// Respect HTTP_PROXY / HTTPS_PROXY / NO_PROXY for outbound HTTP requests // Respect HTTP_PROXY / HTTPS_PROXY / NO_PROXY for outbound HTTP requests
setGlobalDispatcher(new EnvHttpProxyAgent()); setGlobalDispatcher(new EnvHttpProxyAgent());
@ -114,20 +116,20 @@ async function bootstrap() {
address = `${host}:${addressObject.port}`; address = `${host}:${addressObject.port}`;
} }
Logger.log(`Listening at http://${address}`); logger.log(`Listening at http://${address}`);
Logger.log(''); logger.log('');
}); });
} }
function logLogo() { function logLogo() {
Logger.log(' ________ __ ____ ___'); logger.log(' ________ __ ____ ___');
Logger.log(' / ____/ /_ ____ _____/ /_/ __/___ / (_)___'); logger.log(' / ____/ /_ ____ _____/ /_/ __/___ / (_)___');
Logger.log(' / / __/ __ \\/ __ \\/ ___/ __/ /_/ __ \\/ / / __ \\'); logger.log(' / / __/ __ \\/ __ \\/ ___/ __/ /_/ __ \\/ / / __ \\');
Logger.log('/ /_/ / / / / /_/ (__ ) /_/ __/ /_/ / / / /_/ /'); logger.log('/ /_/ / / / / /_/ (__ ) /_/ __/ /_/ / / / /_/ /');
Logger.log( logger.log(
`\\____/_/ /_/\\____/____/\\__/_/ \\____/_/_/\\____/ ${environment.version}` `\\____/_/ /_/\\____/____/\\__/_/ \\____/_/_/\\____/ ${environment.version}`
); );
Logger.log(''); logger.log('');
} }
bootstrap(); bootstrap();

8
apps/api/src/middlewares/html-template.middleware.ts

@ -92,6 +92,8 @@ const locales = {
@Injectable() @Injectable()
export class HtmlTemplateMiddleware implements NestMiddleware { export class HtmlTemplateMiddleware implements NestMiddleware {
private readonly logger = new Logger(HtmlTemplateMiddleware.name);
private indexHtmlMap: { [languageCode: string]: string } = {}; private indexHtmlMap: { [languageCode: string]: string } = {};
public constructor(private readonly i18nService: I18nService) { public constructor(private readonly i18nService: I18nService) {
@ -107,11 +109,7 @@ export class HtmlTemplateMiddleware implements NestMiddleware {
{} {}
); );
} catch (error) { } catch (error) {
Logger.error( this.logger.error('Failed to initialize index HTML map', error);
'Failed to initialize index HTML map',
error,
'HTMLTemplateMiddleware'
);
} }
} }

6
apps/api/src/services/benchmark/benchmark.service.ts

@ -28,6 +28,8 @@ import { BenchmarkValue } from './interfaces/benchmark-value.interface';
@Injectable() @Injectable()
export class BenchmarkService { export class BenchmarkService {
private readonly logger = new Logger(BenchmarkService.name);
private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS'; private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS';
public constructor( public constructor(
@ -87,7 +89,7 @@ export class BenchmarkService {
const { benchmarks, expiration }: BenchmarkValue = const { benchmarks, expiration }: BenchmarkValue =
JSON.parse(cachedBenchmarkValue); JSON.parse(cachedBenchmarkValue);
Logger.debug('Fetched benchmarks from cache', 'BenchmarkService'); this.logger.debug('Fetched benchmarks from cache');
if (isAfter(new Date(), new Date(expiration))) { if (isAfter(new Date(), new Date(expiration))) {
this.calculateAndCacheBenchmarks({ this.calculateAndCacheBenchmarks({
@ -227,7 +229,7 @@ export class BenchmarkService {
private async calculateAndCacheBenchmarks({ private async calculateAndCacheBenchmarks({
enableSharing = false enableSharing = false
}): Promise<BenchmarkResponse['benchmarks']> { }): Promise<BenchmarkResponse['benchmarks']> {
Logger.debug('Calculate benchmarks', 'BenchmarkService'); this.logger.debug('Calculate benchmarks');
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({ const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({
enableSharing enableSharing

8
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -29,6 +29,8 @@ import { format, fromUnixTime, getUnixTime } from 'date-fns';
@Injectable() @Injectable()
export class CoinGeckoService implements DataProviderInterface, OnModuleInit { export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
private readonly logger = new Logger(CoinGeckoService.name);
private apiUrl: string; private apiUrl: string;
private headers: HeadersInit = {}; private headers: HeadersInit = {};
@ -88,7 +90,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'CoinGeckoService'); this.logger.error(message);
} }
return response; return response;
@ -214,7 +216,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'CoinGeckoService'); this.logger.error(message);
} }
return response; return response;
@ -262,7 +264,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'CoinGeckoService'); this.logger.error(message);
} }
return { items }; return { items };

7
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -11,6 +11,8 @@ import { countries } from 'countries-list';
@Injectable() @Injectable()
export class TrackinsightDataEnhancerService implements DataEnhancerInterface { export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private readonly logger = new Logger(TrackinsightDataEnhancerService.name);
private static baseUrl = 'https://www.trackinsight.com/data-api'; private static baseUrl = 'https://www.trackinsight.com/data-api';
private static countriesMapping = { private static countriesMapping = {
'Russian Federation': 'Russia', 'Russian Federation': 'Russia',
@ -209,9 +211,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return undefined; return undefined;
}) })
.catch(({ message }) => { .catch(({ message }) => {
Logger.error( this.logger.error(
`Failed to search Trackinsight symbol for ${symbol} (${message})`, `Failed to search Trackinsight symbol for ${symbol} (${message})`
'TrackinsightDataEnhancerService'
); );
return undefined; return undefined;

6
apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts

@ -23,6 +23,8 @@ import type { Price } from 'yahoo-finance2/esm/src/modules/quoteSummary-iface';
@Injectable() @Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
private readonly logger = new Logger(YahooFinanceDataEnhancerService.name);
private readonly yahooFinance = new YahooFinance({ private readonly yahooFinance = new YahooFinance({
suppressNotices: ['yahooSurvey'] suppressNotices: ['yahooSurvey']
}); });
@ -123,7 +125,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
response.url = url; response.url = url;
} }
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceDataEnhancerService'); this.logger.error(error);
} }
return response; return response;
@ -266,7 +268,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
`No data found, ${aSymbol} (${this.getName()}) may be delisted` `No data found, ${aSymbol} (${this.getName()}) may be delisted`
); );
} else { } else {
Logger.error(error, 'YahooFinanceService'); this.logger.error(error);
} }
} }

31
apps/api/src/services/data-provider/data-provider.service.ts

@ -41,6 +41,8 @@ import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error';
@Injectable() @Injectable()
export class DataProviderService implements OnModuleInit { export class DataProviderService implements OnModuleInit {
private readonly logger = new Logger(DataProviderService.name);
private dataProviderMapping: { [dataProviderName: string]: string }; private dataProviderMapping: { [dataProviderName: string]: string };
public constructor( public constructor(
@ -129,7 +131,7 @@ export class DataProviderService implements OnModuleInit {
); );
} }
} catch (error) { } catch (error) {
Logger.error(error, 'DataProviderService'); this.logger.error(error);
throw error; throw error;
} }
@ -391,7 +393,7 @@ export class DataProviderService implements OnModuleInit {
return r; return r;
}, {}); }, {});
} catch (error) { } catch (error) {
Logger.error(error, 'DataProviderService'); this.logger.error(error);
} finally { } finally {
return response; return response;
} }
@ -503,7 +505,7 @@ export class DataProviderService implements OnModuleInit {
result[symbol] = data; result[symbol] = data;
} }
} catch (error) { } catch (error) {
Logger.error(error, 'DataProviderService'); this.logger.error(error);
throw error; throw error;
} }
@ -567,13 +569,12 @@ export class DataProviderService implements OnModuleInit {
const numberOfItemsInCache = Object.keys(response)?.length; const numberOfItemsInCache = Object.keys(response)?.length;
if (numberOfItemsInCache) { if (numberOfItemsInCache) {
Logger.debug( this.logger.debug(
`Fetched ${numberOfItemsInCache} quote${ `Fetched ${numberOfItemsInCache} quote${
numberOfItemsInCache > 1 ? 's' : '' numberOfItemsInCache > 1 ? 's' : ''
} from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed( } from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed(
3 3
)} seconds`, )} seconds`
'DataProviderService'
); );
} }
@ -684,14 +685,13 @@ export class DataProviderService implements OnModuleInit {
} }
} }
Logger.debug( this.logger.debug(
`Fetched ${symbolsChunk.length} quote${ `Fetched ${symbolsChunk.length} quote${
symbolsChunk.length > 1 ? 's' : '' symbolsChunk.length > 1 ? 's' : ''
} from ${dataSource} in ${( } from ${dataSource} in ${(
(performance.now() - startTimeDataSource) / (performance.now() - startTimeDataSource) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
'DataProviderService'
); );
try { try {
@ -722,15 +722,18 @@ export class DataProviderService implements OnModuleInit {
await Promise.all(promises); await Promise.all(promises);
Logger.debug('--------------------------------------------------------'); this.logger.debug(
Logger.debug( '--------------------------------------------------------'
);
this.logger.debug(
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${( `Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
(performance.now() - startTimeTotal) / (performance.now() - startTimeTotal) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
'DataProviderService' );
this.logger.debug(
'========================================================'
); );
Logger.debug('========================================================');
return response; return response;
} }

21
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -37,6 +37,8 @@ import { isNumber } from 'lodash';
export class EodHistoricalDataService export class EodHistoricalDataService
implements DataProviderInterface, OnModuleInit implements DataProviderInterface, OnModuleInit
{ {
private readonly logger = new Logger(EodHistoricalDataService.name);
private apiKey: string; private apiKey: string;
private readonly URL = 'https://eodhistoricaldata.com/api'; private readonly URL = 'https://eodhistoricaldata.com/api';
@ -127,12 +129,11 @@ export class EodHistoricalDataService
return response; return response;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format( `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from, from,
DATE_FORMAT DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`, )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
'EodHistoricalDataService'
); );
return {}; return {};
@ -172,9 +173,8 @@ export class EodHistoricalDataService
marketPrice: adjusted_close marketPrice: adjusted_close
}; };
} else { } else {
Logger.error( this.logger.error(
`Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`, `Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`
'EodHistoricalDataService'
); );
} }
@ -292,9 +292,8 @@ export class EodHistoricalDataService
dataSource: this.getName() dataSource: this.getName()
}; };
} else { } else {
Logger.error( this.logger.error(
`Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`, `Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`
'EodHistoricalDataService'
); );
} }
} }
@ -311,7 +310,7 @@ export class EodHistoricalDataService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'EodHistoricalDataService'); this.logger.error(message);
} }
return {}; return {};
@ -465,7 +464,7 @@ export class EodHistoricalDataService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'EodHistoricalDataService'); this.logger.error(message);
} }
return searchResult; return searchResult;

13
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -49,6 +49,8 @@ import { uniqBy } from 'lodash';
export class FinancialModelingPrepService export class FinancialModelingPrepService
implements DataProviderInterface, OnModuleInit implements DataProviderInterface, OnModuleInit
{ {
private readonly logger = new Logger(FinancialModelingPrepService.name);
private static countriesMapping = { private static countriesMapping = {
'Korea (the Republic of)': 'South Korea', 'Korea (the Republic of)': 'South Korea',
'Russian Federation': 'Russia', 'Russian Federation': 'Russia',
@ -265,7 +267,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'FinancialModelingPrepService'); this.logger.error(message);
} }
return response; return response;
@ -325,12 +327,11 @@ export class FinancialModelingPrepService
return response; return response;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format( `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from, from,
DATE_FORMAT DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`, )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
'FinancialModelingPrepService'
); );
return {}; return {};
@ -518,7 +519,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'FinancialModelingPrepService'); this.logger.error(message);
} }
return response; return response;
@ -638,7 +639,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'FinancialModelingPrepService'); this.logger.error(message);
} }
return { items }; return { items };

12
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -33,6 +33,8 @@ import { StatusCodes } from 'http-status-codes';
@Injectable() @Injectable()
export class GhostfolioService implements DataProviderInterface { export class GhostfolioService implements DataProviderInterface {
private readonly logger = new Logger(GhostfolioService.name);
private readonly URL = environment.production private readonly URL = environment.production
? 'https://ghostfol.io/api' ? 'https://ghostfol.io/api'
: `${this.configurationService.get('ROOT_URL')}/api`; : `${this.configurationService.get('ROOT_URL')}/api`;
@ -89,7 +91,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} }
Logger.error(message, 'GhostfolioService'); this.logger.error(message);
} }
return assetProfile; return assetProfile;
@ -154,7 +156,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} }
Logger.error(message, 'GhostfolioService'); this.logger.error(message);
} }
return dividends; return dividends;
@ -211,7 +213,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} }
Logger.error(error.message, 'GhostfolioService'); this.logger.error(error.message);
throw new Error( throw new Error(
`Could not get historical market data for ${symbol} (${this.getName()}) from ${format( `Could not get historical market data for ${symbol} (${this.getName()}) from ${format(
@ -283,7 +285,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} }
Logger.error(message, 'GhostfolioService'); this.logger.error(message);
} }
return quotes; return quotes;
@ -338,7 +340,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} }
Logger.error(message, 'GhostfolioService'); this.logger.error(message);
} }
return searchResult; return searchResult;

4
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -24,6 +24,8 @@ import { GoogleSpreadsheet } from 'google-spreadsheet';
@Injectable() @Injectable()
export class GoogleSheetsService implements DataProviderInterface { export class GoogleSheetsService implements DataProviderInterface {
private readonly logger = new Logger(GoogleSheetsService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
@ -144,7 +146,7 @@ export class GoogleSheetsService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error(error, 'GoogleSheetsService'); this.logger.error(error);
} }
return {}; return {};

9
apps/api/src/services/data-provider/manual/manual.service.ts

@ -31,6 +31,8 @@ import { addDays, format, isBefore } from 'date-fns';
@Injectable() @Injectable()
export class ManualService implements DataProviderInterface { export class ManualService implements DataProviderInterface {
private readonly logger = new Logger(ManualService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService, private readonly fetchService: FetchService,
@ -181,9 +183,8 @@ export class ManualService implements DataProviderInterface {
}); });
return { marketPrice, symbol }; return { marketPrice, symbol };
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`, `Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`
'ManualService'
); );
return { symbol, marketPrice: undefined }; return { symbol, marketPrice: undefined };
} }
@ -216,7 +217,7 @@ export class ManualService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error(error, 'ManualService'); this.logger.error(error);
} }
return {}; return {};

6
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -26,6 +26,8 @@ import { format } from 'date-fns';
@Injectable() @Injectable()
export class RapidApiService implements DataProviderInterface { export class RapidApiService implements DataProviderInterface {
private readonly logger = new Logger(RapidApiService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService private readonly fetchService: FetchService
@ -122,7 +124,7 @@ export class RapidApiService implements DataProviderInterface {
}; };
} }
} catch (error) { } catch (error) {
Logger.error(error, 'RapidApiService'); this.logger.error(error);
} }
return {}; return {};
@ -167,7 +169,7 @@ export class RapidApiService implements DataProviderInterface {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'RapidApiService'); this.logger.error(message);
return undefined; return undefined;
} }

23
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -41,6 +41,8 @@ import { SearchQuoteNonYahoo } from 'yahoo-finance2/esm/src/modules/search';
@Injectable() @Injectable()
export class YahooFinanceService implements DataProviderInterface { export class YahooFinanceService implements DataProviderInterface {
private readonly logger = new Logger(YahooFinanceService.name);
private readonly yahooFinance = new YahooFinance({ private readonly yahooFinance = new YahooFinance({
suppressNotices: ['yahooSurvey'] suppressNotices: ['yahooSurvey']
}); });
@ -105,12 +107,11 @@ export class YahooFinanceService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format( `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from, from,
DATE_FORMAT DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`, )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
'YahooFinanceService'
); );
return {}; return {};
@ -198,12 +199,9 @@ export class YahooFinanceService implements DataProviderInterface {
try { try {
quotes = await this.yahooFinance.quote(yahooFinanceSymbols); quotes = await this.yahooFinance.quote(yahooFinanceSymbols);
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceService'); this.logger.error(error);
Logger.warn( this.logger.warn('Fallback to yahooFinance.quoteSummary()');
'Fallback to yahooFinance.quoteSummary()',
'YahooFinanceService'
);
quotes = await this.getQuotesWithQuoteSummary(yahooFinanceSymbols); quotes = await this.getQuotesWithQuoteSummary(yahooFinanceSymbols);
} }
@ -229,7 +227,7 @@ export class YahooFinanceService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceService'); this.logger.error(error);
return {}; return {};
} }
@ -334,7 +332,7 @@ export class YahooFinanceService implements DataProviderInterface {
}); });
} }
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceService'); this.logger.error(error);
} }
return { items }; return { items };
@ -365,10 +363,7 @@ export class YahooFinanceService implements DataProviderInterface {
.filter( .filter(
(result): result is PromiseFulfilledResult<QuoteSummaryResult> => { (result): result is PromiseFulfilledResult<QuoteSummaryResult> => {
if (result.status === 'rejected') { if (result.status === 'rejected') {
Logger.error( this.logger.error(`Could not get quote summary: ${result.reason}`);
`Could not get quote summary: ${result.reason}`,
'YahooFinanceService'
);
return false; return false;
} }

19
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -30,6 +30,8 @@ import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interfa
@Injectable() @Injectable()
export class ExchangeRateDataService { export class ExchangeRateDataService {
private readonly logger = new Logger(ExchangeRateDataService.name);
private currencies: string[] = []; private currencies: string[] = [];
private currencyPairs: DataGatheringItem[] = []; private currencyPairs: DataGatheringItem[] = [];
private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; private derivedCurrencyFactors: { [currencyPair: string]: number } = {};
@ -110,9 +112,8 @@ export class ExchangeRateDataService {
previousExchangeRate; previousExchangeRate;
if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) { if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) {
Logger.error( this.logger.error(
`No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`, `No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`
'ExchangeRateDataService'
); );
} }
} else { } else {
@ -253,9 +254,8 @@ export class ExchangeRateDataService {
} }
// Fallback with error, if currencies are not available // Fallback with error, if currencies are not available
Logger.error( this.logger.error(
`No exchange rate has been found for ${aFromCurrency}${aToCurrency}`, `No exchange rate has been found for ${aFromCurrency}${aToCurrency}`
'ExchangeRateDataService'
); );
return aValue; return aValue;
@ -341,12 +341,11 @@ export class ExchangeRateDataService {
return factor * aValue; return factor * aValue;
} }
Logger.error( this.logger.error(
`No exchange rate has been found for ${aFromCurrency}${aToCurrency} at ${format( `No exchange rate has been found for ${aFromCurrency}${aToCurrency} at ${format(
aDate, aDate,
DATE_FORMAT DATE_FORMAT
)}`, )}`
'ExchangeRateDataService'
); );
return undefined; return undefined;
@ -483,7 +482,7 @@ export class ExchangeRateDataService {
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`;
} }
Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); this.logger.error(`${errorMessage}.`);
} }
} }
} }

24
apps/api/src/services/fetch/fetch.service.ts

@ -15,6 +15,8 @@ import { WebFetchRoute } from './interfaces/web-fetch-route.interface';
@Injectable() @Injectable()
export class FetchService implements OnModuleInit { export class FetchService implements OnModuleInit {
private readonly logger = new Logger(FetchService.name);
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token']; private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds'); private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds');
@ -39,7 +41,7 @@ export class FetchService implements OnModuleInit {
const url = input instanceof Request ? input.url : input.toString(); const url = input instanceof Request ? input.url : input.toString();
const urlRedacted = this.redactUrl(url); const urlRedacted = this.redactUrl(url);
Logger.debug(`${method} ${urlRedacted}`, 'FetchService'); this.logger.debug(`${method} ${urlRedacted}`);
if (method === 'GET') { if (method === 'GET') {
const webFetchRoute = this.getMatchingWebFetchRoute(url); const webFetchRoute = this.getMatchingWebFetchRoute(url);
@ -60,15 +62,11 @@ export class FetchService implements OnModuleInit {
return await globalThis.fetch(input, init); return await globalThis.fetch(input, init);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
Logger.error( this.logger.error(
`${method} ${urlRedacted} failed: [${error.name}] ${error.message}`, `${method} ${urlRedacted} failed: [${error.name}] ${error.message}`
'FetchService'
); );
} else { } else {
Logger.error( this.logger.error(`${method} ${urlRedacted} failed: ${String(error)}`);
`${method} ${urlRedacted} failed: ${String(error)}`,
'FetchService'
);
} }
throw error; throw error;
@ -145,10 +143,7 @@ export class FetchService implements OnModuleInit {
} }
} }
Logger.debug( this.logger.debug(`Routed ${this.redactUrl(url)} via web fetch tool`);
`Routed ${this.redactUrl(url)} via web fetch tool`,
'FetchService'
);
return new Response(body, { return new Response(body, {
headers: webFetchRoute.responseContentType headers: webFetchRoute.responseContentType
@ -159,11 +154,10 @@ export class FetchService implements OnModuleInit {
return undefined; return undefined;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Web fetch tool failed for ${this.redactUrl(url)}: ${ `Web fetch tool failed for ${this.redactUrl(url)}: ${
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
}`, }`
'FetchService'
); );
return undefined; return undefined;

8
apps/api/src/services/i18n/i18n.service.ts

@ -7,6 +7,8 @@ import { join } from 'node:path';
@Injectable() @Injectable()
export class I18nService implements OnModuleInit { export class I18nService implements OnModuleInit {
private readonly logger = new Logger(I18nService.name);
private localesPath = join(__dirname, 'assets', 'locales'); private localesPath = join(__dirname, 'assets', 'locales');
private translations: { [locale: string]: cheerio.CheerioAPI } = {}; private translations: { [locale: string]: cheerio.CheerioAPI } = {};
@ -26,7 +28,7 @@ export class I18nService implements OnModuleInit {
const $ = this.translations[languageCode]; const $ = this.translations[languageCode];
if (!$) { if (!$) {
Logger.warn(`Translation not found for locale '${languageCode}'`); this.logger.warn(`Translation not found for locale '${languageCode}'`);
} }
let translatedText = $( let translatedText = $(
@ -36,7 +38,7 @@ export class I18nService implements OnModuleInit {
).text(); ).text();
if (!translatedText) { if (!translatedText) {
Logger.warn( this.logger.warn(
`Translation not found for id '${id}' in locale '${languageCode}'` `Translation not found for id '${id}' in locale '${languageCode}'`
); );
} }
@ -60,7 +62,7 @@ export class I18nService implements OnModuleInit {
this.parseXml(xmlData); this.parseXml(xmlData);
} }
} catch (error) { } catch (error) {
Logger.error(error, 'I18nService'); this.logger.error(error);
} }
} }

4
apps/api/src/services/prisma/prisma.service.ts

@ -14,6 +14,8 @@ export class PrismaService
extends PrismaClient extends PrismaClient
implements OnModuleInit, OnModuleDestroy implements OnModuleInit, OnModuleDestroy
{ {
private readonly logger = new Logger(PrismaService.name);
public constructor(configService: ConfigService) { public constructor(configService: ConfigService) {
const adapter = new PrismaPg({ const adapter = new PrismaPg({
connectionString: configService.get<string>('DATABASE_URL') connectionString: configService.get<string>('DATABASE_URL')
@ -43,7 +45,7 @@ export class PrismaService
try { try {
await this.$connect(); await this.$connect();
} catch (error) { } catch (error) {
Logger.error(error, 'PrismaService'); this.logger.error(error);
} }
} }

42
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -32,6 +32,8 @@ import { DataGatheringService } from './data-gathering.service';
@Injectable() @Injectable()
@Processor(DATA_GATHERING_QUEUE) @Processor(DATA_GATHERING_QUEUE)
export class DataGatheringProcessor { export class DataGatheringProcessor {
private readonly logger = new Logger(DataGatheringProcessor.name);
public constructor( public constructor(
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
@ -51,16 +53,14 @@ export class DataGatheringProcessor {
const { dataSource, symbol } = job.data; const { dataSource, symbol } = job.data;
try { try {
Logger.log( this.logger.log(
`Asset profile data gathering has been started for ${symbol} (${dataSource})`, `Asset profile data gathering has been started for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
await this.dataGatheringService.gatherAssetProfiles([job.data]); await this.dataGatheringService.gatherAssetProfiles([job.data]);
Logger.log( this.logger.log(
`Asset profile data gathering has been completed for ${symbol} (${dataSource})`, `Asset profile data gathering has been completed for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
} catch (error) { } catch (error) {
if (error instanceof AssetProfileDelistedError) { if (error instanceof AssetProfileDelistedError) {
@ -74,18 +74,14 @@ export class DataGatheringProcessor {
} }
); );
Logger.log( this.logger.log(
`Asset profile data gathering has been discarded for ${symbol} (${dataSource})`, `Asset profile data gathering has been discarded for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
return job.discard(); return job.discard();
} }
Logger.error( this.logger.error(error);
error,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
);
throw error; throw error;
} }
@ -105,12 +101,11 @@ export class DataGatheringProcessor {
try { try {
let currentDate = parseISO(date as unknown as string); let currentDate = parseISO(date as unknown as string);
Logger.log( this.logger.log(
`Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format( `Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format(
currentDate, currentDate,
DATE_FORMAT DATE_FORMAT
)}${force ? ' (forced update)' : ''}`, )}${force ? ' (forced update)' : ''}`
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
); );
const historicalData = await this.dataProviderService.getHistoricalRaw({ const historicalData = await this.dataProviderService.getHistoricalRaw({
@ -167,12 +162,11 @@ export class DataGatheringProcessor {
await this.marketDataService.updateMany({ data }); await this.marketDataService.updateMany({ data });
} }
Logger.log( this.logger.log(
`Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format( `Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format(
currentDate, currentDate,
DATE_FORMAT DATE_FORMAT
)}`, )}`
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
); );
} catch (error) { } catch (error) {
if (error instanceof AssetProfileDelistedError) { if (error instanceof AssetProfileDelistedError) {
@ -186,18 +180,14 @@ export class DataGatheringProcessor {
} }
); );
Logger.log( this.logger.log(
`Historical market data gathering has been discarded for ${symbol} (${dataSource})`, `Historical market data gathering has been discarded for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
); );
return job.discard(); return job.discard();
} }
Logger.error( this.logger.error(error);
error,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
);
throw error; throw error;
} }

15
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -34,6 +34,8 @@ import ms, { StringValue } from 'ms';
@Injectable() @Injectable()
export class DataGatheringService { export class DataGatheringService {
private readonly logger = new Logger(DataGatheringService.name);
public constructor( public constructor(
@Inject('DataEnhancers') @Inject('DataEnhancers')
private readonly dataEnhancers: DataEnhancerInterface[], private readonly dataEnhancers: DataEnhancerInterface[],
@ -145,7 +147,7 @@ export class DataGatheringService {
}); });
} }
} catch (error) { } catch (error) {
Logger.error(error, 'DataGatheringService'); this.logger.error(error);
} finally { } finally {
return undefined; return undefined;
} }
@ -187,12 +189,11 @@ export class DataGatheringService {
symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol
}); });
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Failed to enhance data for ${symbol} (${ `Failed to enhance data for ${symbol} (${
assetProfile.dataSource assetProfile.dataSource
}) by ${dataEnhancer.getName()}`, }) by ${dataEnhancer.getName()}`,
error, error
'DataGatheringService'
); );
} }
} }
@ -256,11 +257,7 @@ export class DataGatheringService {
} }
}); });
} catch (error) { } catch (error) {
Logger.error( this.logger.error(`${symbol}: ${error?.meta?.cause}`, error);
`${symbol}: ${error?.meta?.cause}`,
error,
'DataGatheringService'
);
if (assetProfileIdentifiers.length === 1) { if (assetProfileIdentifiers.length === 1) {
throw error; throw error;

17
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts

@ -21,6 +21,8 @@ import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue
@Injectable() @Injectable()
@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) @Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE)
export class PortfolioSnapshotProcessor { export class PortfolioSnapshotProcessor {
private readonly logger = new Logger(PortfolioSnapshotProcessor.name);
public constructor( public constructor(
private readonly accountBalanceService: AccountBalanceService, private readonly accountBalanceService: AccountBalanceService,
private readonly activitiesService: ActivitiesService, private readonly activitiesService: ActivitiesService,
@ -41,9 +43,8 @@ export class PortfolioSnapshotProcessor {
try { try {
const startTime = performance.now(); const startTime = performance.now();
Logger.log( this.logger.log(
`Portfolio snapshot calculation of user '${job.data.userId}' has been started`, `Portfolio snapshot calculation of user '${job.data.userId}' has been started`
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
); );
const { activities } = const { activities } =
@ -72,12 +73,11 @@ export class PortfolioSnapshotProcessor {
const snapshot = await portfolioCalculator.computeSnapshot(); const snapshot = await portfolioCalculator.computeSnapshot();
Logger.log( this.logger.log(
`Portfolio snapshot calculation of user '${job.data.userId}' has been completed in ${( `Portfolio snapshot calculation of user '${job.data.userId}' has been completed in ${(
(performance.now() - startTime) / (performance.now() - startTime) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
); );
const expiration = addMilliseconds( const expiration = addMilliseconds(
@ -101,10 +101,7 @@ export class PortfolioSnapshotProcessor {
return snapshot; return snapshot;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(error);
error,
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
);
throw new Error(error); throw new Error(error);
} }

55
apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts

@ -27,6 +27,8 @@ import { format, subDays } from 'date-fns';
@Injectable() @Injectable()
@Processor(STATISTICS_GATHERING_QUEUE) @Processor(STATISTICS_GATHERING_QUEUE)
export class StatisticsGatheringProcessor { export class StatisticsGatheringProcessor {
private readonly logger = new Logger(StatisticsGatheringProcessor.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService, private readonly fetchService: FetchService,
@ -35,10 +37,7 @@ export class StatisticsGatheringProcessor {
@Process(GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME) @Process(GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME)
public async gatherDockerHubPullsStatistics() { public async gatherDockerHubPullsStatistics() {
Logger.log( this.logger.log('Docker Hub pulls statistics gathering has been started');
'Docker Hub pulls statistics gathering has been started',
'StatisticsGatheringProcessor'
);
const dockerHubPulls = await this.countDockerHubPulls(); const dockerHubPulls = await this.countDockerHubPulls();
@ -47,17 +46,13 @@ export class StatisticsGatheringProcessor {
value: String(dockerHubPulls) value: String(dockerHubPulls)
}); });
Logger.log( this.logger.log('Docker Hub pulls statistics gathering has been completed');
'Docker Hub pulls statistics gathering has been completed',
'StatisticsGatheringProcessor'
);
} }
@Process(GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME) @Process(GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME)
public async gatherGitHubContributorsStatistics() { public async gatherGitHubContributorsStatistics() {
Logger.log( this.logger.log(
'GitHub contributors statistics gathering has been started', 'GitHub contributors statistics gathering has been started'
'StatisticsGatheringProcessor'
); );
const gitHubContributors = await this.countGitHubContributors(); const gitHubContributors = await this.countGitHubContributors();
@ -67,18 +62,14 @@ export class StatisticsGatheringProcessor {
value: String(gitHubContributors) value: String(gitHubContributors)
}); });
Logger.log( this.logger.log(
'GitHub contributors statistics gathering has been completed', 'GitHub contributors statistics gathering has been completed'
'StatisticsGatheringProcessor'
); );
} }
@Process(GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME) @Process(GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME)
public async gatherGitHubStargazersStatistics() { public async gatherGitHubStargazersStatistics() {
Logger.log( this.logger.log('GitHub stargazers statistics gathering has been started');
'GitHub stargazers statistics gathering has been started',
'StatisticsGatheringProcessor'
);
const gitHubStargazers = await this.countGitHubStargazers(); const gitHubStargazers = await this.countGitHubStargazers();
@ -87,9 +78,8 @@ export class StatisticsGatheringProcessor {
value: String(gitHubStargazers) value: String(gitHubStargazers)
}); });
Logger.log( this.logger.log(
'GitHub stargazers statistics gathering has been completed', 'GitHub stargazers statistics gathering has been completed'
'StatisticsGatheringProcessor'
); );
} }
@ -100,18 +90,14 @@ export class StatisticsGatheringProcessor {
); );
if (!monitorId) { if (!monitorId) {
Logger.log( this.logger.log(
`Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`, `Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`
'StatisticsGatheringProcessor'
); );
return; return;
} }
Logger.log( this.logger.log('Uptime statistics gathering has been started');
'Uptime statistics gathering has been started',
'StatisticsGatheringProcessor'
);
const uptime = await this.getUptime(monitorId); const uptime = await this.getUptime(monitorId);
@ -120,10 +106,7 @@ export class StatisticsGatheringProcessor {
value: String(uptime) value: String(uptime)
}); });
Logger.log( this.logger.log('Uptime statistics gathering has been completed');
'Uptime statistics gathering has been completed',
'StatisticsGatheringProcessor'
);
} }
private async countDockerHubPulls(): Promise<number> { private async countDockerHubPulls(): Promise<number> {
@ -139,7 +122,7 @@ export class StatisticsGatheringProcessor {
return pull_count; return pull_count;
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - DockerHub'); this.logger.error(error);
throw error; throw error;
} }
@ -169,7 +152,7 @@ export class StatisticsGatheringProcessor {
value value
}); });
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - GitHub'); this.logger.error(error);
throw error; throw error;
} }
@ -188,7 +171,7 @@ export class StatisticsGatheringProcessor {
return stargazers_count; return stargazers_count;
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - GitHub'); this.logger.error(error);
throw error; throw error;
} }
@ -217,7 +200,7 @@ export class StatisticsGatheringProcessor {
return data.attributes.availability / 100; return data.attributes.availability / 100;
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - Better Stack'); this.logger.error(error);
throw error; throw error;
} }

9
apps/api/src/services/twitter-bot/twitter-bot.service.ts

@ -16,6 +16,8 @@ import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
@Injectable() @Injectable()
export class TwitterBotService implements OnModuleInit { export class TwitterBotService implements OnModuleInit {
private readonly logger = new Logger(TwitterBotService.name);
private twitterClient: TwitterApiReadWrite; private twitterClient: TwitterApiReadWrite;
public constructor( public constructor(
@ -71,13 +73,12 @@ export class TwitterBotService implements OnModuleInit {
const { data: createdTweet } = const { data: createdTweet } =
await this.twitterClient.v2.tweet(status); await this.twitterClient.v2.tweet(status);
Logger.log( this.logger.log(
`Fear & Greed Index has been posted: https://x.com/ghostfolio_/status/${createdTweet.id}`, `Fear & Greed Index has been posted: https://x.com/ghostfolio_/status/${createdTweet.id}`
'TwitterBotService'
); );
} }
} catch (error) { } catch (error) {
Logger.error(error, 'TwitterBotService'); this.logger.error(error);
} }
} }

Loading…
Cancel
Save