diff --git a/lib/handlers/prom_default.js b/lib/handlers/prom_default.js new file mode 100644 index 00000000..f8edc898 --- /dev/null +++ b/lib/handlers/prom_default.js @@ -0,0 +1,10 @@ +/* Emulated PromQL Query Handler */ + +async function handler (req, res) { + req.log.debug('GET /api/v1/*') + const resp = {"status": "success", "data": {}}; + res.send(resp) + return +} + +module.exports = handler diff --git a/lib/handlers/prom_query.js b/lib/handlers/prom_query.js new file mode 100644 index 00000000..23714727 --- /dev/null +++ b/lib/handlers/prom_query.js @@ -0,0 +1,35 @@ +/* Emulated PromQL Query Handler */ + +const { p2l } = require('@qxip/promql2logql'); +const empty = '{"status" : "success", "data" : {"resultType" : "scalar", "result" : []}}'; // to be removed + +async function handler (req, res) { + req.log.debug('GET /loki/api/v1/query') + const resp = { streams: [] } + if (!req.query.query) { + res.send(resp) + return + } + /* remove newlines */ + req.query.query = req.query.query.replace(/\n/g, ' ') + /* transpile to logql */ + try { + req.query.query = p2l(req.query.query); + } catch(e) { + req.log.error({ err }) + res.send(empty) + } + /* scan fingerprints */ + /* TODO: handle time tag + direction + limit to govern the query output */ + try { + await this.instantQueryScan( + req.query, + { res: res.raw } + ) + } catch (err) { + req.log.error({ err }) + res.send(empty) + } +} + +module.exports = handler diff --git a/lib/handlers/prom_query_range.js b/lib/handlers/prom_query_range.js new file mode 100644 index 00000000..0fc4c8dc --- /dev/null +++ b/lib/handlers/prom_query_range.js @@ -0,0 +1,45 @@ +/* Emulated PromQL Query Handler */ +/* + Converts PromQL to LogQL queries, accepts the following parameters in the query-string: + query: a PromQL query + limit: max number of entries to return + start: the start time for the query, as a nanosecond Unix epoch (nanoseconds since 1970) + end: the end time for the query, as a nanosecond Unix epoch (nanoseconds since 1970) + direction: forward or backward, useful when specifying a limit + regexp: a regex to filter the returned results, will eventually be rolled into the query language +*/ + +const { p2l } = require('@qxip/promql2logql') + +async function handler (req, res) { + req.log.debug('GET /api/v1/query_range') + const resp = { streams: [] } + if (!req.query.query) { + res.send(resp) + return + } + /* remove newlines */ + req.query.query = req.query.query.replace(/\n/g, ' ') + if (!req.query.query) { + res.code(400).send('invalid query') + return + } + // Convert PromQL to LogQL and execute + try { + req.query.query = p2l(req.query.query) + await this.scanFingerprints( + { + ...req.query, + start: parseInt(req.query.start) * 1e9, + end: parseInt(req.query.end) * 1e9 + }, + { res: res.raw } + ) + res.sent = true + } catch (err) { + req.log.error({ err }) + res.send(resp) + } +} + +module.exports = handler diff --git a/lib/handlers/promlabel.js b/lib/handlers/promlabel.js new file mode 100644 index 00000000..6a296a93 --- /dev/null +++ b/lib/handlers/promlabel.js @@ -0,0 +1,25 @@ +/* Label Handler */ +/* + For retrieving the names of the labels one can query on. + Responses looks like this: +{ + "values": [ + "instance", + "job", + ... + ] +} +*/ + +async function handler (req, res) { + await require('./label.js')({ + ...req, + query: { + ...req.query, + start: req.query.start ? parseInt(req.query.start) * 1e9 : undefined, + end: req.query.end ? parseInt(req.query.end) * 1e9 : undefined + } + }, res) +} + +module.exports = handler diff --git a/lib/handlers/promlabel_values.js b/lib/handlers/promlabel_values.js new file mode 100644 index 00000000..e20622ff --- /dev/null +++ b/lib/handlers/promlabel_values.js @@ -0,0 +1,25 @@ +/* Label Value Handler */ +/* + For retrieving the label values one can query on. + Responses looks like this: + { + "values": [ + "default", + "cortex-ops", + ... + ] +} +*/ + +async function handler (req, res) { + await require('./label_values.js')({ + ...req, + query: { + ...req.query, + start: req.query.start ? parseInt(req.query.start) * 1e9 : undefined, + end: req.query.end ? parseInt(req.query.end) * 1e9 : undefined + } + }, res) +} + +module.exports = handler diff --git a/package-lock.json b/package-lock.json index b27b5d3b..35526563 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@apla/clickhouse": "^1.6.4", "@cloki/clickhouse-sql": "1.2.2", "@qxip/plugnplay": "^3.3.1", + "@qxip/promql2logql": "^1.0.3", "axios": "^0.21.4", "bnf": "^1.0.1", "date-fns": "^2.27.0", @@ -45,7 +46,8 @@ "yaml": "^1.10.2" }, "bin": { - "cloki": "cloki.js" + "cloki": "qryn.js", + "qryn": "qryn.js" }, "devDependencies": { "casual": "^1.6.2", @@ -1344,6 +1346,26 @@ "node": ">=6.0.0" } }, + "node_modules/@qxip/promql-parser-js": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@qxip/promql-parser-js/-/promql-parser-js-0.1.1.tgz", + "integrity": "sha512-/jEZBk7I3oH3ETTSWKaYbHSNkMVkOFouQmYEOiesz4wBPTPRKedLYO808hh2wAUzv8lUqluMY53dRhXyk0ZAZg==" + }, + "node_modules/@qxip/promql2logql": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@qxip/promql2logql/-/promql2logql-1.0.3.tgz", + "integrity": "sha512-phcI012MxbxXsfGAYS4aNtzPBfXMXa88J48ylry2pL55yRK20bx2Mjs0WP5LjqxlnGgfim3p9Wnn8zyCZ1zGdw==", + "dependencies": { + "@qxip/promql-parser-js": "^0.1.1", + "jsonic": "^1.0.1", + "squirrelly": "^8.0.8" + } + }, + "node_modules/@qxip/promql2logql/node_modules/jsonic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsonic/-/jsonic-1.0.1.tgz", + "integrity": "sha512-6GitEN4plTuB/I1o9kDZl7Pgc+DvFG1BG88IqaUz4eQglCA1uAgxWdXhLNA6ffaYsmzPjOysDpp6CYTwRiuXLw==" + }, "node_modules/@qxip/to-file": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@qxip/to-file/-/to-file-0.2.1.tgz", @@ -10490,6 +10512,17 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "node_modules/squirrelly": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/squirrelly/-/squirrelly-8.0.8.tgz", + "integrity": "sha512-7dyZJ9Gw86MmH0dYLiESsjGOTj6KG8IWToTaqBuB6LwPI+hyNb6mbQaZwrfnAQ4cMDnSWMUvX/zAYDLTSWLk/w==", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/squirrellyjs/squirrelly?sponsor=1" + } + }, "node_modules/stack-utils": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", @@ -11568,6 +11601,20 @@ "node": ">=0.10.0" } }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/uglify-js": { "version": "3.14.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.4.tgz", @@ -13067,6 +13114,28 @@ "rimraf": "^3.0.2" } }, + "@qxip/promql-parser-js": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@qxip/promql-parser-js/-/promql-parser-js-0.1.1.tgz", + "integrity": "sha512-/jEZBk7I3oH3ETTSWKaYbHSNkMVkOFouQmYEOiesz4wBPTPRKedLYO808hh2wAUzv8lUqluMY53dRhXyk0ZAZg==" + }, + "@qxip/promql2logql": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@qxip/promql2logql/-/promql2logql-1.0.3.tgz", + "integrity": "sha512-phcI012MxbxXsfGAYS4aNtzPBfXMXa88J48ylry2pL55yRK20bx2Mjs0WP5LjqxlnGgfim3p9Wnn8zyCZ1zGdw==", + "requires": { + "@qxip/promql-parser-js": "^0.1.1", + "jsonic": "^1.0.1", + "squirrelly": "^8.0.8" + }, + "dependencies": { + "jsonic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsonic/-/jsonic-1.0.1.tgz", + "integrity": "sha512-6GitEN4plTuB/I1o9kDZl7Pgc+DvFG1BG88IqaUz4eQglCA1uAgxWdXhLNA6ffaYsmzPjOysDpp6CYTwRiuXLw==" + } + } + }, "@qxip/to-file": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@qxip/to-file/-/to-file-0.2.1.tgz", @@ -20136,6 +20205,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "squirrelly": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/squirrelly/-/squirrelly-8.0.8.tgz", + "integrity": "sha512-7dyZJ9Gw86MmH0dYLiESsjGOTj6KG8IWToTaqBuB6LwPI+hyNb6mbQaZwrfnAQ4cMDnSWMUvX/zAYDLTSWLk/w==" + }, "stack-utils": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", @@ -20955,6 +21029,13 @@ } } }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "peer": true + }, "uglify-js": { "version": "3.14.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.4.tgz", diff --git a/package.json b/package.json index 544e491a..574e41e4 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "xxhash-wasm": "^0.4.2", "yaml": "^1.10.2", "json-stable-stringify": "^1.0.1", + "@qxip/promql2logql": "^1.0.10", "@qxip/influx-line-protocol-parser": "^0.2.1" }, "devDependencies": { diff --git a/qryn.js b/qryn.js index 39a903af..c9ae83f5 100755 --- a/qryn.js +++ b/qryn.js @@ -389,6 +389,23 @@ fastify.get('/prometheus/api/v1/rules', require('./lib/handlers/alerts/prom_get_ 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)) +/* PROMQETHEUS API EMULATION */ +const handlerPromQueryRange = require('./lib/handlers/prom_query_range.js').bind(this) +fastify.get('/api/v1/query_range', handlerPromQueryRange) +const handlerPromQuery = require('./lib/handlers/prom_query.js').bind(this) +fastify.get('/api/v1/query', handlerPromQuery) +const handlerPromLabel = require('./lib/handlers/promlabel.js').bind(this) +const handlerPromLabelValues = require('./lib/handlers/promlabel_values.js').bind(this) +fastify.get('/api/v1/labels', handlerPromLabel) // piggyback on qryn labels +fastify.get('/api/v1/label/:name/values', handlerPromLabelValues) // piggyback on qryn values +fastify.post('/api/v1/labels', handlerPromLabel) // piggyback on qryn labels +fastify.post('/api/v1/label/:name/values', handlerPromLabelValues) // piggyback on qryn values +const handlerPromDefault = require('./lib/handlers/prom_default.js').bind(this) +fastify.get('/api/v1/metadata', handlerPromDefault) // default handler TBD +fastify.get('/api/v1/rules', handlerPromDefault) // default handler TBD +fastify.get('/api/v1/query_exemplars', handlerPromDefault) // default handler TBD +fastify.get('/api/v1/status/buildinfo', handlerPromDefault) // default handler TBD + /* INFLUX WRITE Handlers */ const handlerInfluxWrite = require('./lib/handlers/influx_write.js').bind(this) fastify.post('/write', handlerInfluxWrite)