mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
20 changed files with 419 additions and 156 deletions
@ -0,0 +1,36 @@ |
|||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; |
||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
||||
|
import { STATISTICS_GATHERING_QUEUE } from '@ghostfolio/common/config'; |
||||
|
|
||||
|
import { BullAdapter } from '@bull-board/api/bullAdapter'; |
||||
|
import { BullBoardModule } from '@bull-board/nestjs'; |
||||
|
import { BullModule } from '@nestjs/bull'; |
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { StatisticsGatheringProcessor } from './statistics-gathering.processor'; |
||||
|
import { StatisticsGatheringService } from './statistics-gathering.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
exports: [BullModule, StatisticsGatheringService], |
||||
|
imports: [ |
||||
|
...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' |
||||
|
? [ |
||||
|
BullBoardModule.forFeature({ |
||||
|
adapter: BullAdapter, |
||||
|
name: STATISTICS_GATHERING_QUEUE, |
||||
|
options: { |
||||
|
displayName: 'Statistics Gathering', |
||||
|
readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' |
||||
|
} |
||||
|
}) |
||||
|
] |
||||
|
: []), |
||||
|
BullModule.registerQueue({ |
||||
|
name: STATISTICS_GATHERING_QUEUE |
||||
|
}), |
||||
|
ConfigurationModule, |
||||
|
PropertyModule |
||||
|
], |
||||
|
providers: [StatisticsGatheringProcessor, StatisticsGatheringService] |
||||
|
}) |
||||
|
export class StatisticsGatheringQueueModule {} |
||||
@ -0,0 +1,212 @@ |
|||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
||||
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; |
||||
|
import { |
||||
|
GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME, |
||||
|
HEADER_KEY_TOKEN, |
||||
|
PROPERTY_BETTER_UPTIME_MONITOR_ID, |
||||
|
PROPERTY_DOCKER_HUB_PULLS, |
||||
|
PROPERTY_GITHUB_CONTRIBUTORS, |
||||
|
PROPERTY_GITHUB_STARGAZERS, |
||||
|
PROPERTY_UPTIME, |
||||
|
STATISTICS_GATHERING_QUEUE |
||||
|
} from '@ghostfolio/common/config'; |
||||
|
import { |
||||
|
DATE_FORMAT, |
||||
|
extractNumberFromString |
||||
|
} from '@ghostfolio/common/helper'; |
||||
|
|
||||
|
import { Process, Processor } from '@nestjs/bull'; |
||||
|
import { Injectable, Logger } from '@nestjs/common'; |
||||
|
import * as cheerio from 'cheerio'; |
||||
|
import { format, subDays } from 'date-fns'; |
||||
|
|
||||
|
@Injectable() |
||||
|
@Processor(STATISTICS_GATHERING_QUEUE) |
||||
|
export class StatisticsGatheringProcessor { |
||||
|
public constructor( |
||||
|
private readonly configurationService: ConfigurationService, |
||||
|
private readonly propertyService: PropertyService |
||||
|
) {} |
||||
|
|
||||
|
@Process(GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME) |
||||
|
public async gatherDockerHubPullsStatistics() { |
||||
|
Logger.log( |
||||
|
'Docker Hub pulls statistics gathering has been started', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
|
||||
|
const dockerHubPulls = await this.countDockerHubPulls(); |
||||
|
|
||||
|
await this.propertyService.put({ |
||||
|
key: PROPERTY_DOCKER_HUB_PULLS, |
||||
|
value: String(dockerHubPulls) |
||||
|
}); |
||||
|
|
||||
|
Logger.log( |
||||
|
'Docker Hub pulls statistics gathering has been completed', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Process(GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME) |
||||
|
public async gatherGitHubContributorsStatistics() { |
||||
|
Logger.log( |
||||
|
'GitHub contributors statistics gathering has been started', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
|
||||
|
const gitHubContributors = await this.countGitHubContributors(); |
||||
|
|
||||
|
await this.propertyService.put({ |
||||
|
key: PROPERTY_GITHUB_CONTRIBUTORS, |
||||
|
value: String(gitHubContributors) |
||||
|
}); |
||||
|
|
||||
|
Logger.log( |
||||
|
'GitHub contributors statistics gathering has been completed', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Process(GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME) |
||||
|
public async gatherGitHubStargazersStatistics() { |
||||
|
Logger.log( |
||||
|
'GitHub stargazers statistics gathering has been started', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
|
||||
|
const gitHubStargazers = await this.countGitHubStargazers(); |
||||
|
|
||||
|
await this.propertyService.put({ |
||||
|
key: PROPERTY_GITHUB_STARGAZERS, |
||||
|
value: String(gitHubStargazers) |
||||
|
}); |
||||
|
|
||||
|
Logger.log( |
||||
|
'GitHub stargazers statistics gathering has been completed', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Process(GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME) |
||||
|
public async gatherUptimeStatistics() { |
||||
|
Logger.log( |
||||
|
'Uptime statistics gathering has been started', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
|
||||
|
const uptime = await this.getUptime(); |
||||
|
|
||||
|
await this.propertyService.put({ |
||||
|
key: PROPERTY_UPTIME, |
||||
|
value: String(uptime) |
||||
|
}); |
||||
|
|
||||
|
Logger.log( |
||||
|
'Uptime statistics gathering has been completed', |
||||
|
'StatisticsGatheringProcessor' |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private async countDockerHubPulls(): Promise<number> { |
||||
|
try { |
||||
|
const { pull_count } = (await fetch( |
||||
|
'https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio', |
||||
|
{ |
||||
|
headers: { 'User-Agent': 'request' }, |
||||
|
signal: AbortSignal.timeout( |
||||
|
this.configurationService.get('REQUEST_TIMEOUT') |
||||
|
) |
||||
|
} |
||||
|
).then((res) => res.json())) as { pull_count: number }; |
||||
|
|
||||
|
return pull_count; |
||||
|
} catch (error) { |
||||
|
Logger.error(error, 'StatisticsGatheringProcessor - DockerHub'); |
||||
|
|
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async countGitHubContributors(): Promise<number> { |
||||
|
try { |
||||
|
const body = await fetch('https://github.com/ghostfolio/ghostfolio', { |
||||
|
signal: AbortSignal.timeout( |
||||
|
this.configurationService.get('REQUEST_TIMEOUT') |
||||
|
) |
||||
|
}).then((res) => res.text()); |
||||
|
|
||||
|
const $ = cheerio.load(body); |
||||
|
|
||||
|
const value = $( |
||||
|
'a[href="/ghostfolio/ghostfolio/graphs/contributors"] .Counter' |
||||
|
).text(); |
||||
|
|
||||
|
if (!value) { |
||||
|
throw new Error('Could not find the contributors count in the page'); |
||||
|
} |
||||
|
|
||||
|
return extractNumberFromString({ |
||||
|
value |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
Logger.error(error, 'StatisticsGatheringProcessor - GitHub'); |
||||
|
|
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async countGitHubStargazers(): Promise<number> { |
||||
|
try { |
||||
|
const { stargazers_count } = (await fetch( |
||||
|
'https://api.github.com/repos/ghostfolio/ghostfolio', |
||||
|
{ |
||||
|
headers: { 'User-Agent': 'request' }, |
||||
|
signal: AbortSignal.timeout( |
||||
|
this.configurationService.get('REQUEST_TIMEOUT') |
||||
|
) |
||||
|
} |
||||
|
).then((res) => res.json())) as { stargazers_count: number }; |
||||
|
|
||||
|
return stargazers_count; |
||||
|
} catch (error) { |
||||
|
Logger.error(error, 'StatisticsGatheringProcessor - GitHub'); |
||||
|
|
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async getUptime(): Promise<number> { |
||||
|
try { |
||||
|
const monitorId = await this.propertyService.getByKey<string>( |
||||
|
PROPERTY_BETTER_UPTIME_MONITOR_ID |
||||
|
); |
||||
|
|
||||
|
const { data } = await fetch( |
||||
|
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format( |
||||
|
subDays(new Date(), 90), |
||||
|
DATE_FORMAT |
||||
|
)}&to${format(new Date(), DATE_FORMAT)}`,
|
||||
|
{ |
||||
|
headers: { |
||||
|
[HEADER_KEY_TOKEN]: `Bearer ${this.configurationService.get( |
||||
|
'API_KEY_BETTER_UPTIME' |
||||
|
)}` |
||||
|
}, |
||||
|
signal: AbortSignal.timeout( |
||||
|
this.configurationService.get('REQUEST_TIMEOUT') |
||||
|
) |
||||
|
} |
||||
|
).then((res) => res.json()); |
||||
|
|
||||
|
return data.attributes.availability / 100; |
||||
|
} catch (error) { |
||||
|
Logger.error(error, 'StatisticsGatheringProcessor - Better Stack'); |
||||
|
|
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
import { |
||||
|
GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_PROCESS_JOB_OPTIONS, |
||||
|
GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME, |
||||
|
STATISTICS_GATHERING_QUEUE |
||||
|
} from '@ghostfolio/common/config'; |
||||
|
|
||||
|
import { InjectQueue } from '@nestjs/bull'; |
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { Queue } from 'bull'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class StatisticsGatheringService { |
||||
|
public constructor( |
||||
|
@InjectQueue(STATISTICS_GATHERING_QUEUE) |
||||
|
private readonly statisticsGatheringQueue: Queue |
||||
|
) {} |
||||
|
|
||||
|
public async addJobsToQueue() { |
||||
|
return Promise.all( |
||||
|
[ |
||||
|
GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME, |
||||
|
GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME |
||||
|
].map((jobName) => { |
||||
|
return this.statisticsGatheringQueue.add( |
||||
|
jobName, |
||||
|
{}, |
||||
|
{ |
||||
|
...GATHER_STATISTICS_PROCESS_JOB_OPTIONS, |
||||
|
jobId: jobName |
||||
|
} |
||||
|
); |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue