Skip to content

Commit

Permalink
Merge pull request #1 from metrico/master
Browse files Browse the repository at this point in the history
qryn client implantation
  • Loading branch information
lmangani authored Jun 27, 2024
2 parents 7f09c2f + 61a262f commit 9fc2e4d
Show file tree
Hide file tree
Showing 15 changed files with 562 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
package-lock.json
.vscode
46 changes: 46 additions & 0 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const {QrynClient,Metric, Stream} = require('../src');

async function main() {
try {
const client = new QrynClient({
baseUrl: process.env['QYRN_URL'],
auth: {
username: process.env['QYRN_LOGIN'],
password: process.env['QRYN_PASSWORD']
},
timeout: 5000
})
// Create and push Loki streams
const stream1 = client.createStream({ job: 'job1', env: 'prod' });
stream1.addEntry(Date.now(), 'Log message 1');
stream1.addEntry(Date.now(), 'Log message 2');

const stream2 = new Stream({ job: 'job2', env: 'dev' });
stream2.addEntry(Date.now(), 'Log message 3');

const lokiResponse = await client.loki.push([stream1, stream2]);
console.log('Loki push successful:', lokiResponse);

// Create and push Prometheus metrics


const memoryUsed = client.createMetric({ name: 'memory_use_test_134', labels: {
foo:"bar"
}});
memoryUsed.addSample(1024 * 1024 * 100);
memoryUsed.addSample(105, Date.now() + 60000);

const cpuUsed = new Metric('cpu_test_1234', { server: 'web-1' });

cpuUsed.addSample(1024 * 1024 * 100);
cpuUsed.addSample(105, Date.now() + 60000);

const promResponse = await client.prom.push([memoryUsed, cpuUsed]);
console.log('Prometheus push successful:', promResponse);

} catch (error) {
console.error('An error occurred:', error);
}
}

main();
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "qryn-client",
"version": "1.0.0",
"description": "A client library for interacting with qryn, a high-performance observability backend.",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "tzachiSh",
"license": "MIT",
"dependencies": {
"protobufjs": "^7.3.2",
"snappy": "^7.2.2"
},
"devDependencies": {},
"repository": {
"type": "git",
"url": "https://github.com/username/qryn-client.git"
},
"keywords": [
"qryn",
"client",
"observability",
"monitoring",
"logging"
],
"bugs": {
"url": "https://github.com/username/qryn-client/issues"
},
"homepage": "https://github.com/username/qryn-client#readme",
"engines": {
"node": ">=18.0.0"
}
}
46 changes: 46 additions & 0 deletions src/clients/loki.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { QrynError } = require('../types');
const { Stream } = require('../models');
const Http = require('../services/http');

class Loki {
/**
* Create a new Loki instance.
* @param {Http} service - The HTTP service to use for requests.
*/
constructor(service) {
this.service = service;
}

/**
* Push streams to Loki.
* @param {Stream[]} streams - An array of Stream instances to push.
* @returns {Promise<Object>} The response from the Loki API.
* @throws {QrynError} If the push fails or if the input is invalid.
*/
async push(streams) {
if (!Array.isArray(streams) || !streams.every(s => s instanceof Stream)) {
throw new QrynError('Streams must be an array of Stream instances');
}

const payload = {
streams: streams.map(s => ({
labels: s.labels,
entries: s.entries
}))
};

try {
return await this.service.request('/loki/api/v1/push', {
method: 'POST',
body: JSON.stringify(payload)
});
} catch (error) {
if (error instanceof QrynError) {
throw error;
}
throw new QrynError(`Loki push failed: ${error.message}`, error.statusCode);
}
}
}

module.exports = Loki;
50 changes: 50 additions & 0 deletions src/clients/prometheus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const Http = require('../services/http')
const Protobuff = require('../services/protobuff')
const path = require('path');
const {Metric} = require('../models')
const {QrynError} = require('../types')

class Prometheus {
/**
* Create a new PrometheusRemoteWrite instance.
* @param {Http} service - The HTTP service for making requests.
*/
constructor(service) {
this.service = service;
this.protobufHandler = new Protobuff();
}

async push(metrics) {
if (!Array.isArray(metrics) || !metrics.every(m => m instanceof Metric)) {
throw new QrynError('Metrics must be an array of Metric instances');
}


const timeseries = metrics.map(metric => metric.toTimeSeries());
const writeRequest = { timeseries };

const buffer = this.protobufHandler.encodeWriteRequest(writeRequest);
const compressedBuffer = await this.protobufHandler.compressBuffer(buffer);



try {
return await this.service.request('/api/v1/prom/remote/write', {
method: 'POST',
headers: {
'Content-Type': 'application/x-protobuf',
'Content-Encoding': 'snappy',
'X-Prometheus-Remote-Write-Version': '0.1.0'
},
body: compressedBuffer
});
} catch (error) {
if (error instanceof QrynError) {
throw error;
}
throw new QrynError(`Prometheus Remote Write push failed: ${error.message}`, error.statusCode);
}
}
}

module.exports = Prometheus;
66 changes: 66 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const {Stream, Metric} = require('./models')
const PrometheusClient = require('./clients/prometheus')
const LokiClient = require('./clients/loki')
const Http = require('./services/http')



class Auth{
constructor({username, password}){
this.username = username;
this.password = password;
}
}


/**
* Main client for qryn operations.
*/
class QrynClient {
/**
* Create a QrynClient.
* @param {Object} config - The configuration object.
* @param {string} [config.baseUrl='http://localhost:3100'] - The base URL for the qryn server.
* @param {Auth} [config.auth] - The base Auth for the qryn server.
* @param {number} [config.timeout=5000] - The timeout for requests in milliseconds.
* @param {Object} [config.headers={}] - Additional headers to send with requests.
*/
constructor(config) {
if (typeof config !== 'object' || config === null) {
throw new QrynError('Config must be a non-null object');
}
const auth = config.auth
const baseUrl = config.baseUrl || 'http://localhost:3100';
const timeout = config.timeout || 5000;
const headers = {
'Content-Type': 'application/json',
...config.headers
};
const http = new Http(baseUrl, timeout, headers, auth);
this.prom = new PrometheusClient(http);
this.loki = new LokiClient(http);
}

/**
* Create a new Stream instance.
* @param {Object} labels - The labels for the stream.
* @returns {Stream} A new Stream instance.
*/
createStream(labels) {
return new Stream(labels);
}

/**
* Create a new Metric instance.
* @param {string} name - The name of the metric.
* @param {Object} [labels={}] - Optional labels for the metric.
* @returns {Metric} A new Metric instance.
*/
createMetric({ name, labels = {} }) {
return new Metric(name, labels);
}

// Add more methods for other qryn operations as needed
}

module.exports = { QrynClient, Stream, Metric };
7 changes: 7 additions & 0 deletions src/models/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const Metric = require("./metric");
const Stream = require("./stream");

module.exports = {
Metric,
Stream
}
29 changes: 29 additions & 0 deletions src/models/metric.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class Metric {
constructor(name, labels = {}) {
this.name = name;
this.labels = labels;
this.samples = [];
}

addSample(value, timestamp = Date.now()) {
if (typeof value !== 'number') {
throw new Error('Value must be a number');
}
if (typeof timestamp !== 'number') {
throw new Error('Timestamp must be a number');
}

this.samples.push({ value, timestamp });
}

toTimeSeries() {
return {
labels: [
{ name: '__name__', value: this.name },
...Object.entries(this.labels).map(([name, value]) => ({ name, value }))
],
samples: this.samples
};
}
}
module.exports = Metric;
29 changes: 29 additions & 0 deletions src/models/stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class Stream {
constructor(labels) {
if (typeof labels !== 'object' || labels === null) {
throw new Error('Labels must be a non-null object');
}
this.labels = this.formatLabels(labels);
this.entries = [];
}

formatLabels(labels) {
return '{' + Object.entries(labels)
.map(([key, value]) => `${key}="${value}"`)
.join(',') + '}';
}

addEntry(timestamp, line) {
timestamp = new Date(timestamp).toISOString()
this.entries.push({ ts: timestamp, line: line });
}

toJSON() {
return {
labels: this.labels,
entries: this.entries
};
}
}

module.exports = Stream
Loading

0 comments on commit 9fc2e4d

Please sign in to comment.