diff --git a/package-lock.json b/package-lock.json index 3ce49ae84..f455b5153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2540,6 +2540,11 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, "body-parser": { "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", @@ -12996,6 +13001,14 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "prom-client": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.3.0.tgz", + "integrity": "sha512-OqSf5WOvpGZXkfqPXUHNHpjrbEE/q8jxjktO0i7zg1cnULAtf0ET67/J5R4e4iA4MZx2260tzTzSFSWgMdTZmQ==", + "requires": { + "tdigest": "^0.1.1" + } + }, "promise-events": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/promise-events/-/promise-events-0.1.4.tgz", @@ -14712,6 +14725,14 @@ } } }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, "teeny-request": { "version": "3.11.3", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", diff --git a/package.json b/package.json index b93bb3726..fbec722f1 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "minimatch": "^3.0.4", "probot": "^7.4.0", "probot-config": "^1.0.1", + "prom-client": "^11.3.0", "raven": "^2.6.4" }, "devDependencies": { diff --git a/src/index.ts b/src/index.ts index ae36b0599..6a1e3487d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,15 +4,25 @@ import { WorkerContext } from './models' import Raven from 'raven' import { RepositoryWorkers } from './repository-workers' import sentryStream from 'bunyan-sentry-stream' +import { Headers } from 'probot/lib/github' import { RepositoryReference, PullRequestReference } from './github-models' import myAppId from './myappid' +import { metricsReporter } from './metrics' async function getWorkerContext (options: {app: Application, context: Context, installationId: number}): Promise { const { app, context, installationId } = options const config = await loadConfig(context) const log = app.log const createGitHubAPI = async () => { - return app.auth(installationId, log) + const github = await app.auth(installationId, log) + const { owner, repo } = options.context.repo() + github.hook.after('request', (response, requestOptions) => { + const responseHeaders = response.headers as Headers + const rateLimit = Number(responseHeaders['x-ratelimit-remaining']) + const apiVersion = requestOptions.query ? 'v4' : 'v3' + metricsReporter.rateLimitGauge.set({ owner, repo, apiVersion }, rateLimit) + }) + return github } return { createGitHubAPI, @@ -90,6 +100,14 @@ export = (app: Application) => { }) } + // Get an express router to expose new HTTP endpoints + const router = app.route('/') + + // Add a new route + router.get('/metrics', (req: any, res: any) => { + res.send(metricsReporter.outputMetrics()) + }) + app.on([ 'pull_request.opened', 'pull_request.edited', diff --git a/src/metrics.ts b/src/metrics.ts new file mode 100644 index 000000000..5c9883b8e --- /dev/null +++ b/src/metrics.ts @@ -0,0 +1,44 @@ +import promClient, { Gauge, labelValues } from 'prom-client' + +const makeMockGauge = (name: String): Partial => ({ + set: (labels: labelValues | number, value: number) => {} // tslint:disable-line:no-empty +}) + +class MetricsReporter { + public rateLimitGauge: Gauge + public queueSizeGauge: Gauge + + constructor (public enabled: boolean) { + this.enabled = enabled + + if (this.enabled) { + promClient.collectDefaultMetrics() + + this.rateLimitGauge = new promClient.Gauge({ + name: 'probot_auto_merge_github_rate_limit_remaining', + help: + 'Github rate limit as reported by the `x-ratelimit-remaining` header', + labelNames: ['owner', 'repo', 'apiVersion'] + }) + + this.queueSizeGauge = new promClient.Gauge({ + name: 'probot_auto_merge_pr_queue_size', + help: 'Number of PRs in the Probot Auto Merge queue', + labelNames: ['owner', 'repo'] + }) + } else { + this.rateLimitGauge = makeMockGauge('rate limit') as Gauge + this.queueSizeGauge = makeMockGauge('queue size') as Gauge + } + } + + outputMetrics (): string { + return this.enabled ? promClient.register.metrics() : 'nothing' + } +} + +const metricsReporter = new MetricsReporter( + process.env.NODE_ENV === 'production' +) + +export { metricsReporter } diff --git a/src/repository-worker.ts b/src/repository-worker.ts index 817fe1912..98ba46352 100644 --- a/src/repository-worker.ts +++ b/src/repository-worker.ts @@ -2,10 +2,12 @@ import { WaitQueue } from './WaitQueue' import { RepositoryReference, PullRequestReference } from './github-models' import { handlePullRequest, PullRequestContext } from './pull-request-handler' import { WorkerContext } from './models' +import { metricsReporter } from './metrics' export class RepositoryWorker { private waitQueue: WaitQueue private context: WorkerContext + public sendMetricsInterval?: NodeJS.Timeout constructor ( public repository: RepositoryReference, @@ -17,7 +19,24 @@ export class RepositoryWorker { this.waitQueue = new WaitQueue( (pullRequestNumber: number) => `${pullRequestNumber}`, this.handlePullRequestNumber.bind(this), - onDrain + () => { + this.sendMetricsInterval && clearInterval(this.sendMetricsInterval) + this.sendQueueSizeMetrics() + onDrain() + } + ) + + metricsReporter.enabled && + (this.sendMetricsInterval = setInterval( + this.sendQueueSizeMetrics.bind(this), + 1000 + )) + } + + private sendQueueSizeMetrics (): void { + metricsReporter.queueSizeGauge.set( + { owner: this.repository.owner, repo: this.repository.repo }, + this.waitQueue.getQueuedTasks().length ) } diff --git a/test/mock.ts b/test/mock.ts index 2fb8ea0b4..ce79d0206 100644 --- a/test/mock.ts +++ b/test/mock.ts @@ -78,7 +78,15 @@ export function createPullRequestInfo (pullRequestInfo?: Partial): GitHubAPI { return { - ...options + ...options, + hook: { + before: (when: string, cb: () => void) => { + return + }, + after: (when: string, cb: () => void) => { + return + } + } } as GitHubAPI }