* Basic process metrics * Enable Prometheus by an environment variable * Code formatting * Documentation for Prometheus metrics * Link from README to documentation of Prometheus * Link from README to documentation of Prometheus * Link from README to documentation of Prometheus * Separate module for metrics + tests * Metrics limited by IP * Metrics are forbidded for all requets by default * Code refactoring * allowedIps passed as a string to PrometheusMetrics * Handle missing config * METRICS_PROMETHEUS_ALLOWED_IPS added to documentation * Log info about enabled metrics * Unused code removed * package-lock.json updated * prom-client updated to 11.1.2 * Code refactoring * Do not read IP address from X-Forwarder-For header
339 lines
9.7 KiB
JavaScript
339 lines
9.7 KiB
JavaScript
'use strict'
|
|
|
|
const { DOMParser } = require('xmldom')
|
|
const jp = require('jsonpath')
|
|
const path = require('path')
|
|
const xpath = require('xpath')
|
|
const yaml = require('js-yaml')
|
|
const Raven = require('raven')
|
|
|
|
const serverSecrets = require('./lib/server-secrets')
|
|
Raven.config(process.env.SENTRY_DSN || serverSecrets.sentry_dsn).install()
|
|
Raven.disableConsoleAlerts()
|
|
|
|
const { loadServiceClasses } = require('./services')
|
|
const { checkErrorResponse } = require('./lib/error-helper')
|
|
const analytics = require('./lib/analytics')
|
|
const config = require('./lib/server-config')
|
|
const GithubConstellation = require('./services/github/github-constellation')
|
|
const PrometheusMetrics = require('./lib/sys/prometheus-metrics')
|
|
const sysMonitor = require('./lib/sys/monitor')
|
|
const log = require('./lib/log')
|
|
const { makeMakeBadgeFn } = require('./lib/make-badge')
|
|
const { QuickTextMeasurer } = require('./lib/text-measurer')
|
|
const suggest = require('./lib/suggest')
|
|
const {
|
|
makeColorB,
|
|
makeLabel: getLabel,
|
|
makeBadgeData: getBadgeData,
|
|
setBadgeColor,
|
|
} = require('./lib/badge-data')
|
|
const {
|
|
makeHandleRequestFn,
|
|
clearRequestCache,
|
|
} = require('./lib/request-handler')
|
|
const { clearRegularUpdateCache } = require('./lib/regular-update')
|
|
const { makeSend } = require('./lib/result-sender')
|
|
const { escapeFormat } = require('./lib/path-helpers')
|
|
|
|
const serverStartTime = new Date(new Date().toGMTString())
|
|
|
|
const camp = require('camp').start({
|
|
documentRoot: path.join(__dirname, 'public'),
|
|
port: config.bind.port,
|
|
hostname: config.bind.address,
|
|
secure: config.ssl.isSecure,
|
|
cert: config.ssl.cert,
|
|
key: config.ssl.key,
|
|
})
|
|
|
|
const githubConstellation = new GithubConstellation({
|
|
persistence: config.persistence,
|
|
service: config.services.github,
|
|
})
|
|
const metrics = new PrometheusMetrics(config.metrics.prometheus)
|
|
const { apiProvider: githubApiProvider } = githubConstellation
|
|
|
|
function reset() {
|
|
clearRequestCache()
|
|
clearRegularUpdateCache()
|
|
}
|
|
|
|
async function stop() {
|
|
await githubConstellation.stop()
|
|
analytics.cancelAutosaving()
|
|
return new Promise(resolve => {
|
|
camp.close(resolve)
|
|
})
|
|
}
|
|
|
|
module.exports = {
|
|
camp,
|
|
reset,
|
|
stop,
|
|
}
|
|
|
|
log(`Server is starting up: ${config.baseUri}`)
|
|
|
|
let measurer
|
|
try {
|
|
measurer = new QuickTextMeasurer(config.font.path, config.font.fallbackPath)
|
|
} catch (e) {
|
|
console.log(`Unable to load fallback font. Using Helvetica-Bold instead.`)
|
|
measurer = new QuickTextMeasurer('Helvetica')
|
|
}
|
|
const makeBadge = makeMakeBadgeFn(measurer)
|
|
const cache = makeHandleRequestFn(makeBadge)
|
|
|
|
analytics.load()
|
|
analytics.scheduleAutosaving()
|
|
analytics.setRoutes(camp)
|
|
|
|
if (serverSecrets && serverSecrets.shieldsSecret) {
|
|
sysMonitor.setRoutes(camp)
|
|
}
|
|
|
|
githubConstellation.initialize(camp)
|
|
metrics.initialize(camp)
|
|
|
|
suggest.setRoutes(config.cors.allowedOrigin, githubApiProvider, camp)
|
|
|
|
camp.notfound(/\.(svg|png|gif|jpg|json)/, (query, match, end, request) => {
|
|
const format = match[1]
|
|
const badgeData = getBadgeData('404', query)
|
|
badgeData.text[1] = 'badge not found'
|
|
badgeData.colorscheme = 'red'
|
|
// Add format to badge data.
|
|
badgeData.format = format
|
|
const svg = makeBadge(badgeData)
|
|
makeSend(format, request.res, end)(svg)
|
|
})
|
|
|
|
camp.notfound(/.*/, (query, match, end, request) => {
|
|
end(null, { template: '404.html' })
|
|
})
|
|
|
|
// Vendors.
|
|
|
|
loadServiceClasses().forEach(serviceClass =>
|
|
serviceClass.register(
|
|
{ camp, handleRequest: cache, githubApiProvider },
|
|
{ handleInternalErrors: config.handleInternalErrors }
|
|
)
|
|
)
|
|
|
|
// User defined sources - JSON response
|
|
camp.route(
|
|
/^\/badge\/dynamic\/(json|xml|yaml)\.(svg|png|gif|jpg|json)$/,
|
|
cache({
|
|
queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'],
|
|
handler: function(query, match, sendBadge, request) {
|
|
const type = match[1]
|
|
const format = match[2]
|
|
const prefix = query.prefix || ''
|
|
const suffix = query.suffix || ''
|
|
const pathExpression = query.query
|
|
let requestOptions = {}
|
|
|
|
const badgeData = getBadgeData('custom badge', query)
|
|
|
|
if ((!query.uri && !query.url) || !query.query) {
|
|
setBadgeColor(badgeData, 'red')
|
|
badgeData.text[1] = !query.query
|
|
? 'no query specified'
|
|
: 'no url specified'
|
|
sendBadge(format, badgeData)
|
|
return
|
|
}
|
|
|
|
let url
|
|
try {
|
|
url = encodeURI(decodeURIComponent(query.url || query.uri))
|
|
} catch (e) {
|
|
setBadgeColor(badgeData, 'red')
|
|
badgeData.text[1] = 'malformed url'
|
|
sendBadge(format, badgeData)
|
|
return
|
|
}
|
|
|
|
switch (type) {
|
|
case 'json':
|
|
requestOptions = {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
json: true,
|
|
}
|
|
break
|
|
case 'xml':
|
|
requestOptions = {
|
|
headers: {
|
|
Accept: 'application/xml, text/xml',
|
|
},
|
|
}
|
|
break
|
|
case 'yaml':
|
|
requestOptions = {
|
|
headers: {
|
|
Accept:
|
|
'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain',
|
|
},
|
|
}
|
|
break
|
|
}
|
|
|
|
request(url, requestOptions, (err, res, data) => {
|
|
try {
|
|
if (
|
|
checkErrorResponse(badgeData, err, res, {
|
|
404: 'resource not found',
|
|
})
|
|
) {
|
|
return
|
|
}
|
|
|
|
badgeData.colorscheme = 'brightgreen'
|
|
|
|
let innerText = []
|
|
switch (type) {
|
|
case 'json':
|
|
data = typeof data === 'object' ? data : JSON.parse(data)
|
|
data = jp.query(data, pathExpression)
|
|
if (!data.length) {
|
|
throw Error('no result')
|
|
}
|
|
innerText = data
|
|
break
|
|
case 'xml':
|
|
data = new DOMParser().parseFromString(data)
|
|
data = xpath.select(pathExpression, data)
|
|
if (!data.length) {
|
|
throw Error('no result')
|
|
}
|
|
data.forEach((i, v) => {
|
|
innerText.push(
|
|
pathExpression.indexOf('@') + 1 ? i.value : i.firstChild.data
|
|
)
|
|
})
|
|
break
|
|
case 'yaml':
|
|
data = yaml.safeLoad(data)
|
|
data = jp.query(data, pathExpression)
|
|
if (!data.length) {
|
|
throw Error('no result')
|
|
}
|
|
innerText = data
|
|
break
|
|
}
|
|
badgeData.text[1] =
|
|
(prefix || '') + innerText.join(', ') + (suffix || '')
|
|
} catch (e) {
|
|
setBadgeColor(badgeData, 'lightgrey')
|
|
badgeData.text[1] = e.message
|
|
} finally {
|
|
sendBadge(format, badgeData)
|
|
}
|
|
})
|
|
},
|
|
})
|
|
)
|
|
|
|
// Any badge.
|
|
camp.route(
|
|
/^\/(:|badge\/)(([^-]|--)*?)-?(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/,
|
|
(data, match, end, ask) => {
|
|
const subject = escapeFormat(match[2])
|
|
const status = escapeFormat(match[4])
|
|
const color = escapeFormat(match[6])
|
|
const format = match[8]
|
|
|
|
analytics.noteRequest(data, match)
|
|
|
|
// Cache management - the badge is constant.
|
|
const cacheDuration = (3600 * 24 * 1) | 0 // 1 day.
|
|
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
|
if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) {
|
|
ask.res.statusCode = 304
|
|
ask.res.end() // not modified.
|
|
return
|
|
}
|
|
ask.res.setHeader('Last-Modified', serverStartTime.toGMTString())
|
|
|
|
// Badge creation.
|
|
try {
|
|
const badgeData = getBadgeData(subject, data)
|
|
badgeData.text[0] = getLabel(undefined, { label: subject })
|
|
badgeData.text[1] = status
|
|
badgeData.colorB = makeColorB(color, data)
|
|
badgeData.template = data.style
|
|
if (config.profiling.makeBadge) {
|
|
console.time('makeBadge total')
|
|
}
|
|
const svg = makeBadge(badgeData)
|
|
if (config.profiling.makeBadge) {
|
|
console.timeEnd('makeBadge total')
|
|
}
|
|
makeSend(format, ask.res, end)(svg)
|
|
} catch (e) {
|
|
log.error(e.stack)
|
|
const svg = makeBadge({
|
|
text: ['error', 'bad badge'],
|
|
colorscheme: 'red',
|
|
})
|
|
makeSend(format, ask.res, end)(svg)
|
|
}
|
|
}
|
|
)
|
|
|
|
// Production cache debugging.
|
|
let bitFlip = false
|
|
camp.route(/^\/flip\.svg$/, (data, match, end, ask) => {
|
|
const cacheSecs = 60
|
|
ask.res.setHeader('Cache-Control', `max-age=${cacheSecs}`)
|
|
const reqTime = new Date()
|
|
const date = new Date(+reqTime + cacheSecs * 1000).toGMTString()
|
|
ask.res.setHeader('Expires', date)
|
|
const badgeData = getBadgeData('flip', data)
|
|
bitFlip = !bitFlip
|
|
badgeData.text[1] = bitFlip ? 'on' : 'off'
|
|
badgeData.colorscheme = bitFlip ? 'brightgreen' : 'red'
|
|
const svg = makeBadge(badgeData)
|
|
makeSend('svg', ask.res, end)(svg)
|
|
})
|
|
|
|
// Any badge, old version.
|
|
camp.route(/^\/([^/]+)\/(.+).png$/, (data, match, end, ask) => {
|
|
const subject = match[1]
|
|
const status = match[2]
|
|
const color = data.color
|
|
|
|
// Cache management - the badge is constant.
|
|
const cacheDuration = (3600 * 24 * 1) | 0 // 1 day.
|
|
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
|
if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) {
|
|
ask.res.statusCode = 304
|
|
ask.res.end() // not modified.
|
|
return
|
|
}
|
|
ask.res.setHeader('Last-Modified', serverStartTime.toGMTString())
|
|
|
|
// Badge creation.
|
|
try {
|
|
const badgeData = { text: [subject, status] }
|
|
badgeData.colorscheme = color
|
|
const svg = makeBadge(badgeData)
|
|
makeSend('png', ask.res, end)(svg)
|
|
} catch (e) {
|
|
const svg = makeBadge({ text: ['error', 'bad badge'], colorscheme: 'red' })
|
|
makeSend('png', ask.res, end)(svg)
|
|
}
|
|
})
|
|
|
|
if (config.redirectUri) {
|
|
camp.route(/^\/$/, (data, match, end, ask) => {
|
|
ask.res.statusCode = 302
|
|
ask.res.setHeader('Location', config.redirectUri)
|
|
ask.res.end()
|
|
})
|
|
}
|