prometheus-remote-write
Version:
Send samples to prometheus via remote_write from NodeJS
169 lines (151 loc) • 4.43 kB
JavaScript
const SnappyJS = require("snappyjs");
const protobuf = require("protobufjs");
const btoa = (s) => Buffer.from(s, "binary").toString("base64");
const prom = require("./prom");
const __holder = {
type: null,
};
const kv = (o) =>
typeof o === "object"
? Object.entries(o).map((e) => ({
name: e[0],
value: e[1],
}))
: undefined;
/** Loads protocol definition, cache it */
async function loadProto(options) {
if (__holder.root) {
return __holder.type;
}
if (options?.proto) {
const root = await protobuf.load(options?.proto);
if (options?.verbose) {
console.info("Loaded protocol definitions", options?.proto, root);
}
const WriteRequest = root.lookupType("prometheus.WriteRequest");
__holder.type = WriteRequest;
return WriteRequest;
}
return prom.prometheus.WriteRequest;
}
/** Serializes JSON as protobuf buffer */
async function serialize(payload, options) {
const type = await loadProto(options);
const errMsg = type.verify(payload);
if (errMsg) {
throw new Error(errMsg);
}
const buffer = type.encode(payload).finish();
return buffer;
}
async function deserialize(buffer, options) {
const type = await loadProto(options);
return type.decode(buffer);
}
/**
* Sends metrics over HTTP(s)
*
* @param {import("./types").Timeseries | import("./types").Timeseries[]} timeseries
* @param {import("./types").Options} options
* @return {Promise<import("./types").Result>}
*/
async function pushTimeseries(timeseries, options) {
const orig = timeseries;
// Brush up a little
timeseries = !Array.isArray(timeseries) ? [timeseries] : timeseries;
// Nothing to do
if (timeseries.length === 0) {
return {
status: 200,
statusText: "OK",
};
}
const start1 = Date.now();
const writeRequest = {
timeseries: timeseries.map((t) => ({
labels: Array.isArray(t.labels)
? [t.labels, ...(kv(options?.labels) || [])]
: kv({
...options?.labels,
...t.labels,
}),
samples: t.samples.map((s) => ({
value: s.value,
timestamp: s.timestamp ? s.timestamp : Date.now(),
})),
})),
};
const buffer = await serialize(writeRequest, options?.proto);
const logger = options?.console || console;
const start2 = Date.now();
if (options?.timing) {
logger.info("Serialized in", start2 - start1, "ms");
}
if (options?.url) {
/** @type import("./types").MinimalFetch */
let fetch = options.fetch || require("node-fetch").default;
return fetch(options?.url, {
method: "POST",
headers: {
"Content-Type": "application/x-protobuf",
...(options?.auth?.username && options?.auth?.password
? {
Authorization: "Basic " + btoa(options?.auth.username + ":" + options?.auth?.password),
}
: undefined),
...(options.headers || {}),
},
body: SnappyJS.compress(buffer),
timeout: options.timeout,
}).then(async (r) => {
const text = await r.text();
if (options?.verbose && r.status != 200 && r.status != 204) {
logger.warn("Failed to send write request, error", r.status + " " + r.statusText + " " + text, writeRequest);
} else if (options?.verbose && !options?.timing) {
logger.info("Write request sent", r.status + " " + r.statusText + " " + text, writeRequest);
} else if (options?.verbose && options?.timing) {
logger.info(
"Write request sent",
r.status + " " + r.statusText + " in",
Date.now() - start2,
"ms",
writeRequest
);
}
return {
status: r.status,
statusText: r.statusText,
errorMessage: r.status !== 200 ? text : undefined,
};
});
} else {
return {
status: 400,
statusText: "Bad request",
errorMessage: "No endpoint configured",
};
}
}
/**
* Sends metrics over HTTP(s)
*
* @param {Record<string,number>} metrics
* @param {import("./types").Options=} options
* @return {Promise<import("./types").Result>}
*/
async function pushMetrics(metrics, options) {
return pushTimeseries(
Object.entries(metrics).map((c) => ({
labels: { __name__: c[0] },
samples: [{ value: c[1] }],
})),
options
);
}
module.exports = {
serialize,
deserialize,
loadProto,
pushTimeseries,
pushMetrics,
};