Skip to content

Commit

Permalink
Added prom reader
Browse files Browse the repository at this point in the history
  • Loading branch information
TzachiSh committed Sep 24, 2024
1 parent 7b1597c commit d850548
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 9 deletions.
114 changes: 113 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<a href="https://qryn.cloud" target="_blank"><img src='https://user-images.githubusercontent.com/1423657/218816262-e0e8d7ad-44d0-4a7d-9497-0d383ed78b83.png' width=170></a>

# qryn-client
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion example/collector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
38 changes: 38 additions & 0 deletions example/read.js
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
142 changes: 141 additions & 1 deletion src/clients/prometheus.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<QrynResponse>} 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<QrynResponse>} 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<QrynResponse>} 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<QrynResponse>} 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<QrynResponse>} 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<QrynResponse>} 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 {
/**
Expand Down Expand Up @@ -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 = {
Expand Down
13 changes: 11 additions & 2 deletions src/services/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/types/qrynResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit d850548

Please sign in to comment.