UNPKG

cloki

Version:

LogQL API with Clickhouse Backend

395 lines (348 loc) 12.1 kB
#!/usr/bin/env node /* * Loki API to Clickhouse Gateway * (C) 2018-2022 QXIP BV */ this.readonly = process.env.READONLY || false this.http_user = process.env.CLOKI_LOGIN || undefined this.http_password = process.env.CLOKI_PASSWORD || undefined require('./plugins/engine') const DATABASE = require('./lib/db/clickhouse') const UTILS = require('./lib/utils') const { EventEmitter } = require('events') /* ProtoBuf Helpers */ const fs = require('fs') const path = require('path') const protoBuff = require('protocol-buffers') const messages = protoBuff(fs.readFileSync('lib/loki.proto')) const protobufjs = require('protobufjs') const WriteRequest = protobufjs.loadSync(path.join(__dirname, 'lib/prompb.proto')).lookupType('WriteRequest') const logger = require('./lib/logger') /* Alerting */ const { startAlerting, stop } = require('./lib/db/alerting') const yaml = require('yaml') const { CLokiError } = require('./lib/handlers/errors') /* Fingerprinting */ this.fingerPrint = UTILS.fingerPrint this.toJSON = UTILS.toJSON /* Database this.bulk Helpers */ this.bulk = DATABASE.cache.bulk // samples this.bulk_labels = DATABASE.cache.bulk_labels // labels this.labels = DATABASE.cache.labels // in-memory labels /* Function Helpers */ this.labelParser = UTILS.labelParser const init = DATABASE.init this.reloadFingerprints = DATABASE.reloadFingerprints this.scanFingerprints = DATABASE.scanFingerprints this.instantQueryScan = DATABASE.instantQueryScan this.tempoQueryScan = DATABASE.tempoQueryScan this.scanMetricFingerprints = DATABASE.scanMetricFingerprints this.tempoQueryScan = DATABASE.tempoQueryScan this.scanClickhouse = DATABASE.scanClickhouse let profiler = null const shaper = { onParse: 0, onParsed: new EventEmitter(), shapeInterval: setInterval(() => { shaper.onParse = 0 shaper.onParsed.emit('parsed') }, 1000), /** * * @param size {number} * @returns {Promise<void>} */ register: async (size) => { while (shaper.onParse + size > 50e6) { await new Promise(resolve => { shaper.onParsed.once('parsed', resolve) }) } shaper.onParse += size }, stop: () => { shaper.shapeInterval && clearInterval(shaper.shapeInterval) shaper.shapeInterval = null shaper.onParsed.removeAllListeners('parsed') shaper.onParsed = null } } /** * * @param req {FastifyRequest} * @param limit {number} * @returns {number} */ function getContentLength (req, limit) { if (!req.headers['content-length'] || isNaN(parseInt(req.headers['content-length']))) { throw new CLokiError(400, 'Content-Length is required') } const res = parseInt(req.headers['content-length']) if (limit && res > limit) { throw new CLokiError(400, 'Request is too big') } return res } /** * * @param req {FastifyRequest} * @returns {Promise<string>} */ async function getContentBody (req) { let body = '' req.raw.on('data', data => { body += data.toString() }) await new Promise(resolve => req.raw.once('end', resolve)) return body } /** * * @param req {FastifyRequest} * @returns {Promise<void>} */ async function genericJSONParser (req) { try { const length = getContentLength(req, 1e9) if (req.routerPath === '/loki/api/v1/push' && length > 5e6) { return } await shaper.register(length) return JSON.parse(await getContentBody(req)) } catch (err) { err.statusCode = 400 throw err } } /** * * @param req {FastifyRequest} * @returns {Promise<void>} */ async function genericJSONOrYAMLParser (req) { try { const length = getContentLength(req, 1e9) if (req.routerPath === '/loki/api/v1/push' && length > 5e6) { return } await shaper.register(length) const body = await getContentBody(req) try { return JSON.parse(body) } catch (e) {} try { return yaml.parse(body) } catch (e) {} throw new Error('Unexpected request content-type') } catch (err) { err.statusCode = 400 throw err } } (async () => { if (!this.readonly) { await init(process.env.CLICKHOUSE_DB || 'cloki') await startAlerting() } if (!this.readonly && process.env.PROFILE) { const tag = JSON.stringify({ profiler_id: process.env.PROFILE, label: 'RAM usage' }) const fp = this.fingerPrint(tag) profiler = setInterval(() => { this.bulk_labels.add([[new Date().toISOString().split('T')[0], fp, tag, '']]) this.bulk.add([[fp, [['label', 'RAM usage'], ['profiler_id', process.env.PROFILE]], BigInt(Date.now()) * BigInt(1000000), process.memoryUsage().rss / 1024 / 1024, '' ]]) }, 1000) } })().catch((err) => { logger.error(err, 'Error starting cloki') process.exit(1) }) /* Fastify Helper */ const fastify = require('fastify')({ logger, requestTimeout: parseInt(process.env.FASTIFY_REQUESTTIMEOUT) || 0, maxRequestsPerSocket: parseInt(process.env.FASTIFY_MAXREQUESTS) || 0 }) fastify.register(require('fastify-url-data')) fastify.register(require('fastify-websocket')) /* CORS Helper */ const CORS = process.env.CORS_ALLOW_ORIGIN || '*' fastify.register(require('fastify-cors'), { origin: CORS }) fastify.after((err) => { if (err) { logger.error({ err }, 'Error creating http response') throw err } }) /* Enable Simple Authentication */ if (this.http_user && this.http_password) { function checkAuth (username, password, req, reply, done) { if (username === this.http_user && password === this.http_password) { done() } else { done(new Error('Unauthorized!: Wrong username/password.')) } } const validate = checkAuth.bind(this) fastify.register(require('fastify-basic-auth'), { validate, authenticate: true }) fastify.after(() => { fastify.addHook('preHandler', fastify.basicAuth) }) } fastify.addContentTypeParser('application/yaml', {}, async function (req, body, done) { try { const length = getContentLength(req, 5e6) await shaper.register(length) const json = yaml.parse(await getContentBody(req)) return json } catch (err) { err.statusCode = 400 throw err } }) try { const snappy = require('snappyjs') /* Protobuf Handler */ fastify.addContentTypeParser('application/x-protobuf', {}, async function (req, body, done) { try { const length = getContentLength(req, 5e6) await shaper.register(length) let body = new Uint8Array() req.raw.on('data', (data) => { body = new Uint8Array([...body, ...Uint8Array.from(data)]) }) await new Promise(resolve => req.raw.once('end', resolve)) // Prometheus Protobuf Write Handler if (req.url === '/api/v1/prom/remote/write') { let _data = await snappy.uncompress(body) _data = WriteRequest.decode(_data) _data.timeseries = _data.timeseries.map(s => ({ ...s, samples: s.samples.map(e => { const nanos = e.timestamp + '000000' return { ...e, timestamp: nanos } }) })) return _data // Loki Protobuf Push Handler } else { let _data = await snappy.uncompress(body) _data = messages.PushRequest.decode(Buffer.from(_data)) _data.streams = _data.streams.map(s => ({ ...s, entries: s.entries.map(e => { return { ...e, timestamp: BigInt(e.timestamp.seconds) * BigInt(1e9) + BigInt(e.timestamp.nanos) } }) })) return _data.streams } } catch (err) { logger.error({ err }, 'Error handling protobuf conversion') throw err } }) } catch (err) { logger.error({ err }, 'Protobuf ingesting is unsupported') } fastify.addContentTypeParser('application/json', {}, async function (req, body, done) { return await genericJSONParser(req) }) /* Null content-type handler for CH-MV HTTP PUSH */ fastify.addContentTypeParser('*', {}, async function (req, body, done) { return await genericJSONOrYAMLParser(req) }) /* 404 Handler */ const handler404 = require('./lib/handlers/404.js').bind(this) fastify.setNotFoundHandler(handler404) fastify.setErrorHandler(require('./lib/handlers/errors').handler.bind(this)) /* Hello cloki test API */ const handlerHello = require('./lib/handlers/ready').bind(this) fastify.get('/hello', handlerHello) fastify.get('/ready', handlerHello) /* Write Handler */ const handlerPush = require('./lib/handlers/push.js').bind(this) fastify.post('/loki/api/v1/push', handlerPush) /* Tempo Write Handler */ this.tempo_tagtrace = process.env.TEMPO_TAGTRACE || false const handlerTempoPush = require('./lib/handlers/tempo_push.js').bind(this) fastify.post('/tempo/api/push', handlerTempoPush) fastify.post('/api/v2/spans', handlerTempoPush) /* Tempo Traces Query Handler */ this.tempo_span = process.env.TEMPO_SPAN || 24 const handlerTempoTraces = require('./lib/handlers/tempo_traces.js').bind(this) fastify.get('/api/traces/:traceId', handlerTempoTraces) fastify.get('/api/traces/:traceId/:json', handlerTempoTraces) /* Tempo Tag Handlers */ const handlerTempoLabel = require('./lib/handlers/tags.js').bind(this) fastify.get('/api/search/tags', handlerTempoLabel) /* Tempo Tag Value Handler */ const handlerTempoLabelValues = require('./lib/handlers/tags_values.js').bind(this) fastify.get('/api/search/tag/:name/values', handlerTempoLabelValues) /* Telegraf HTTP Bulk handler */ const handlerTelegraf = require('./lib/handlers/telegraf.js').bind(this) fastify.post('/telegraf', handlerTelegraf) /* Query Handler */ const handlerQueryRange = require('./lib/handlers/query_range.js').bind(this) fastify.get('/loki/api/v1/query_range', handlerQueryRange) /* Label Handlers */ /* Label Value Handler via query (test) */ const handlerQuery = require('./lib/handlers/query.js').bind(this) fastify.get('/loki/api/v1/query', handlerQuery) /* Label Handlers */ const handlerLabel = require('./lib/handlers/label.js').bind(this) fastify.get('/loki/api/v1/label', handlerLabel) fastify.get('/loki/api/v1/labels', handlerLabel) /* Label Value Handler */ const handlerLabelValues = require('./lib/handlers/label_values.js').bind(this) fastify.get('/loki/api/v1/label/:name/values', handlerLabelValues) /* Series Placeholder - we do not track this as of yet */ const handlerSeries = require('./lib/handlers/series.js').bind(this) fastify.get('/loki/api/v1/series', handlerSeries) fastify.get('/loki/api/v1/tail', { websocket: true }, require('./lib/handlers/tail').bind(this)) /* ALERT MANAGER Handlers */ fastify.get('/api/prom/rules', require('./lib/handlers/alerts/get_rules').bind(this)) fastify.get('/api/prom/rules/:ns/:group', require('./lib/handlers/alerts/get_group').bind(this)) fastify.post('/api/prom/rules/:ns', require('./lib/handlers/alerts/post_group').bind(this)) fastify.delete('/api/prom/rules/:ns/:group', require('./lib/handlers/alerts/del_group').bind(this)) fastify.delete('/api/prom/rules/:ns', require('./lib/handlers/alerts/del_ns').bind(this)) fastify.get('/prometheus/api/v1/rules', require('./lib/handlers/alerts/prom_get_rules').bind(this)) /* PROMETHEUS REMOTE WRITE Handlers */ fastify.post('/api/v1/prom/remote/write', require('./lib/handlers/prom_push.js').bind(this)) fastify.post('/api/prom/remote/write', require('./lib/handlers/prom_push.js').bind(this)) /* CLOKI-VIEW Optional Handler */ if (fs.existsSync(path.join(__dirname, 'view/index.html'))) { fastify.register(require('fastify-static'), { root: path.join(__dirname, 'view'), prefix: '/' }) } // Run API Service fastify.listen( process.env.PORT || 3100, process.env.HOST || '0.0.0.0', (err, address) => { if (err) throw err logger.info('cLoki API up') fastify.log.info(`cloki API listening on ${address}`) } ) module.exports.stop = () => { shaper.stop() profiler && clearInterval(profiler) fastify.close() DATABASE.stop() require('./parser/transpiler').stop() stop() }