From d850548b0e48861cc61eb3ffc400bca9e16481a6 Mon Sep 17 00:00:00 2001 From: TzachiSh Date: Tue, 24 Sep 2024 18:03:24 +0300 Subject: [PATCH] Added prom reader --- README.md | 114 +++++++++++++++++++++++++++++- example/collector.js | 2 +- example/index.js | 2 +- example/read.js | 38 ++++++++++ package.json | 2 +- src/clients/prometheus.js | 142 +++++++++++++++++++++++++++++++++++++- src/services/http.js | 13 +++- src/types/qrynResponse.js | 4 +- 8 files changed, 308 insertions(+), 9 deletions(-) create mode 100644 example/read.js diff --git a/README.md b/README.md index 29710e0..5c856a1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # qryn-client @@ -143,6 +142,60 @@ collector.on('error', error => { }); ``` +### Reading Metrics from Prometheus + +To read metrics from Prometheus, you can use the `createReader()` method of the `prom` object. It returns a `Read` instance that provides methods for querying and retrieving metrics. + +```javascript +const reader = client.prom.createReader({ + orgId: 'your-org-id' +}); + +// Retrieve the list of label names +reader.labels().then(labels => { + console.log('Label names:', labels.response.data); +}); + +// Retrieve the list of label values for a specific label name +reader.labelValues('job').then(values => { + console.log('Label values for "job":', values.response.data); +}); + +// Execute a PromQL query +const query = 'sum(rate(http_requests_total[5m]))'; +reader.query(query).then(result => { + console.log('Query result:', result.response.data); +}); + +// Execute a PromQL query over a range of time +const start = Math.floor(Date.now() / 1000) - (0.5 * 60 * 60); +const end = Math.floor(Date.now() / 1000); +const step = 60; +reader.queryRange(query, start, end, step).then(result => { + console.log('Query range result:', result.response.data); +}); + +// Retrieve the list of time series that match a specified label set +const match = { job: 'api-server' }; +reader.series(match, start, end).then(result => { + console.log('Series result:', result.response.data); +}); + +// Retrieve the currently loaded alerting and recording rules +reader.rules().then(result => { + console.log('Rules:', result.response.data); +}); +``` + +- Use `client.prom.createReader()` to create a new `Read` instance with the desired options. +- Use the methods provided by the `Read` instance to query and retrieve metrics from Prometheus. +- The `labels()` method retrieves the list of label names. +- The `labelValues()` method retrieves the list of label values for a specific label name. +- The `query()` method executes a PromQL query and retrieves the result. +- The `queryRange()` method executes a PromQL query over a range of time. +- The `series()` method retrieves the list of time series that match a specified label set. +- The `rules()` method retrieves the currently loaded alerting and recording rules. + ## Error Handling qryn-client provides error handling mechanisms to catch and handle errors that may occur during API requests. You can use the `.catch()` method to catch errors and implement fallback logic, such as using a backup client. @@ -282,6 +335,65 @@ Creates a new metric with the specified options and adds it to the collector. Returns a new `Metric` instance. +### Read + +#### `constructor(service, options)` + +Creates a new instance of Read. + +- `service` (object): The HTTP service for making requests. +- `options` (object): + - `orgId` (string): The organization ID to include in the request headers. + +#### `query(query)` + +Execute a PromQL query and retrieve the result. + +- `query` (string): The PromQL query string. + +Returns a promise that resolves to the response from the query endpoint. + +#### `queryRange(query, start, end, step)` + +Execute a PromQL query over a range of time. + +- `query` (string): The PromQL query string. +- `start` (number): The start timestamp in seconds. +- `end` (number): The end timestamp in seconds. +- `step` (string): The query resolution step width in duration format (e.g., '15s'). + +Returns a promise that resolves to the response from the query range endpoint. + +#### `labels()` + +Retrieve the list of label names. + +Returns a promise that resolves to the response from the labels endpoint. + +#### `labelValues(labelName)` + +Retrieve the list of label values for a specific label name. + +- `labelName` (string): The name of the label. + +Returns a promise that resolves to the response from the label values endpoint. + +#### `series(match, start, end)` + +Retrieve the list of time series that match a specified label set. + +- `match` (object): The label set to match. +- `start` (number): The start timestamp in seconds. +- `end` (number): The end timestamp in seconds. + +Returns a promise that resolves to the response from the series endpoint. + +#### `rules()` + +Retrieve the currently loaded alerting and recording rules. + +Returns a promise that resolves to the response from the rules endpoint. + ## Contributing Contributions to qryn-client are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on the [GitHub repository](https://github.com/metrico/qryn-client). diff --git a/example/collector.js b/example/collector.js index 7529bdb..01cd0b3 100644 --- a/example/collector.js +++ b/example/collector.js @@ -2,7 +2,7 @@ const { QrynClient, Metric, Stream, Collector } = require('../src'); async function main() { const client = new QrynClient({ - baseUrl: process.env['QYRN_URL'], + baseUrl: process.env['QYRN_WRITE_URL'], auth: { username: process.env['QYRN_LOGIN'], password: process.env['QRYN_PASSWORD'] diff --git a/example/index.js b/example/index.js index ee611c4..2492ba6 100644 --- a/example/index.js +++ b/example/index.js @@ -2,7 +2,7 @@ const {QrynClient,Metric, Stream} = require('../src'); async function main() { const client = new QrynClient({ - baseUrl: process.env['QYRN_URL'], + baseUrl: process.env['QYRN_WRITE_URL'], auth: { username: process.env['QYRN_LOGIN'], password: process.env['QRYN_PASSWORD'] diff --git a/example/read.js b/example/read.js new file mode 100644 index 0000000..4fa13f5 --- /dev/null +++ b/example/read.js @@ -0,0 +1,38 @@ +const {QrynClient,Metric, Stream} = require('../src'); + +async function main() { + const client = new QrynClient({ + baseUrl: process.env['QYRN_READ_URL'], + auth: { + username: process.env['QYRN_LOGIN'], + password: process.env['QRYN_PASSWORD'] + }, + timeout: 15000 + }) + const reader = client.prom.createReader({ + orgId: process.env['QYRN_ORG_ID'] + }) + + let metrics = await reader.labels().then( labels => { + let label = labels.response.data[1]; + return reader.labelValues(label).then( values => { + let value = values.response.data[0]; + let query = `{${label}="${value}"}`; + let start = Math.floor(Date.now() / 1000) - (0.5 * 60 * 60); + let end = Math.floor(Date.now() / 1000); + let step = 60 + return reader.queryRange(query,start,end,step).then(range => { + return range.response.data.result; + }).catch(err => { + console.log(err); + }) + }) + }) + console.log(metrics); + + + + +} + +main(); \ No newline at end of file diff --git a/package.json b/package.json index 0f52305..51f8bce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qryn-client", - "version": "1.0.7", + "version": "1.0.8", "description": "A client library for interacting with qryn, a high-performance observability backend.", "main": "src/index.js", "scripts": { diff --git a/src/clients/prometheus.js b/src/clients/prometheus.js index efe3a24..ad177e4 100644 --- a/src/clients/prometheus.js +++ b/src/clients/prometheus.js @@ -2,7 +2,138 @@ const Http = require('../services/http') const Protobuff = require('../services/protobuff') const path = require('path'); const {Metric} = require('../models') -const {QrynError} = require('../types') +const {QrynError} = require('../types'); + + +class Read { + constructor(service, options) { + this.service = service; + this.options = options; + } + + /** + * Execute a PromQL query and retrieve the result. + * @param {string} query - The PromQL query string. + * @returns {Promise} A promise that resolves to the response from the query endpoint. + * @throws {QrynError} If the query request fails. + */ + async query(query) { + return this.service.request('/api/v1/query', { + method: 'POST', + headers: this.headers(), + body: { query } + }).catch(error => { + if (error instanceof QrynError) { + throw error; + } + throw new QrynError(`Prometheus query failed: ${error.message}`, error.statusCode); + }); + } + + /** + * Execute a PromQL query over a range of time. + * @param {string} query - The PromQL query string. + * @param {number} start - The start timestamp in seconds. + * @param {number} end - The end timestamp in seconds. + * @param {string} step - The query resolution step width in duration format (e.g., '15s'). + * @returns {Promise} A promise that resolves to the response from the query range endpoint. + * @throws {QrynError} If the query range request fails. + */ + async queryRange(query, start, end, step) { + + return this.service.request('/api/v1/query_range', { + method: 'POST', + headers: this.headers(), + body: new URLSearchParams({query, start, end, step}) + }).catch(error => { + if (error instanceof QrynError) { + throw error; + } + throw new QrynError(`Prometheus query range failed: ${error.message}`, error.statusCode); + }); + } + + /** + * Retrieve the list of label names. + * @returns {Promise} A promise that resolves to the response from the labels endpoint. + * @throws {QrynError} If the labels request fails. + */ + async labels() { + return this.service.request('/api/v1/labels', { + method: 'GET', + headers: this.headers() + }).catch(error => { + if (error instanceof QrynError) { + throw error; + } + throw new QrynError(`Prometheus labels retrieval failed: ${error.message}`, error.statusCode); + }); + } + + /** + * Retrieve the list of label values for a specific label name. + * @param {string} labelName - The name of the label. + * @returns {Promise} A promise that resolves to the response from the label values endpoint. + * @throws {QrynError} If the label values request fails. + */ + async labelValues(labelName) { + return this.service.request(`/api/v1/label/${labelName}/values`, { + method: 'GET', + headers: this.headers() + }).catch(error => { + if (error instanceof QrynError) { + throw error; + } + throw new QrynError(`Prometheus label values retrieval failed: ${error.message}`, error.statusCode); + }); + } + + /** + * Retrieve the list of time series that match a specified label set. + * @param {Object} match - The label set to match. + * @param {number} start - The start timestamp in seconds. + * @param {number} end - The end timestamp in seconds. + * @returns {Promise} A promise that resolves to the response from the series endpoint. + * @throws {QrynError} If the series request fails. + */ + async series(match, start, end) { + return this.service.request('/api/v1/series', { + method: 'POST', + headers: this.headers(), + body: new URLSearchParams({ match, start, end }) + }).catch(error => { + if (error instanceof QrynError) { + throw error; + } + throw new QrynError(`Prometheus series retrieval failed: ${error.message}`, error.statusCode); + }); + } + + /** + * Retrieve the currently loaded alerting and recording rules. + * @returns {Promise} A promise that resolves to the response from the rules endpoint. + * @throws {QrynError} If the rules request fails. + */ + async rules() { + return this.service.request('/api/v1/rules', { + method: 'GET', + headers: this.headers() + }).catch(error => { + if (error instanceof QrynError) { + throw error; + } + throw new QrynError(`Prometheus rules retrieval failed: ${error.message}`, error.statusCode); + }); + } + + headers() { + let headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + if (this.options.orgId) headers['X-Scope-OrgID'] = this.options.orgId; + return headers; + } +} class Prometheus { /** @@ -56,6 +187,15 @@ class Prometheus { throw new QrynError(`Prometheus Remote Write push failed: ${error.message}`, error.statusCode); }); } + /** + * Create a new Read instance for reading metrics from Prometheus. + * @param {Object} options - Options for the read operation. + * @param {string} [options.orgId] - The organization ID to include in the request headers. + * @returns {Read} A new Read instance. + */ + createReader(options) { + return new Read(this.service, options); + } headers(options = {}) { let headers = { diff --git a/src/services/http.js b/src/services/http.js index 603be38..62dea26 100644 --- a/src/services/http.js +++ b/src/services/http.js @@ -43,6 +43,7 @@ class Http { async request(path, options = {}) { const url = new URL(path, this.baseUrl); const headers = { ...this.headers, ...options.headers }; + let res = {}; // Add Authorization header if basic auth is set if (this.basicAuth) { @@ -58,13 +59,21 @@ class Http { try { const response = await fetch(url.toString(), fetchOptions); + + if(headers['Content-Type'] === 'application/x-www-form-urlencoded'){ + res = await response.json(); + } + if (!response.ok) { - throw new QrynError(`HTTP error! status: ${response.status}`, response.status, path); + let message = `HTTP error! status: ${response.status}` + throw new QrynError(message, response.status, res, path); } - return new QrynResponse({}, response.status, response.headers, path) + return new QrynResponse(res, response.status, response.headers, path) } catch (error) { + if(error instanceof QrynError) + throw error; throw new QrynError(`Request failed: ${error.message} ${error?.cause?.message}`, 400, error.cause, path); } } diff --git a/src/types/qrynResponse.js b/src/types/qrynResponse.js index cf8e334..e50692b 100644 --- a/src/types/qrynResponse.js +++ b/src/types/qrynResponse.js @@ -9,8 +9,8 @@ class QrynResponse { * @param {Object} headers - The headers of the response. */ #headers; - constructor(data, status, headers, path) { - this.data = data; + constructor(res, status, headers, path) { + this.response = res; this.status = status; this.headers = headers; this.path = path;