218 lines
6.4 KiB
TypeScript
218 lines
6.4 KiB
TypeScript
import type { OptionDefinition } from "command-line-args";
|
|
import { readFile } from "node:fs/promises";
|
|
import fs from "node:fs";
|
|
import cmdline from "command-line-args";
|
|
import yaml from "js-yaml";
|
|
import { compileQuery, Query } from "./query.js";
|
|
|
|
export interface Configuration {
|
|
bindHost: string;
|
|
port: number;
|
|
configFile: string;
|
|
watchFile: boolean;
|
|
prometheusURL: string;
|
|
queriesAllow: string[];
|
|
variables: AnyVariable[];
|
|
queriesCompiled: Query[];
|
|
labelsAllow: LabelAllow[];
|
|
}
|
|
|
|
interface LabelAllow {
|
|
name: string;
|
|
values?: string[];
|
|
}
|
|
|
|
interface BaseVariable<T extends string> {
|
|
name: string;
|
|
varType: T
|
|
}
|
|
|
|
export interface LiteralVariable extends BaseVariable<"literal"> {
|
|
text: string;
|
|
}
|
|
|
|
export function isAnyVariable(o: any): o is AnyVariable {
|
|
return (
|
|
typeof o === "object" &&
|
|
typeof o.name === "string" &&
|
|
typeof o.varType === "string"
|
|
);
|
|
}
|
|
|
|
interface ArrayLikeVariable<T extends string> extends BaseVariable<`${T}_array`> {
|
|
items: string[];
|
|
separator: string;
|
|
}
|
|
|
|
export type AnyOfItemsVariable = ArrayLikeVariable<"any_of">;
|
|
export type AllOfItemsVariable = ArrayLikeVariable<"all_of">;
|
|
|
|
export interface PatternVariable extends BaseVariable<"pattern"> {
|
|
pattern: RegExp;
|
|
}
|
|
|
|
export type AnyVariable = LiteralVariable | AnyOfItemsVariable | AllOfItemsVariable | PatternVariable;
|
|
|
|
|
|
const optionDefinitions: OptionDefinition[] = [
|
|
{ name: "bind-host", alias: "b", type: String, defaultValue: "0.0.0.0" },
|
|
{ name: "port", alias: "p", type: Number, defaultValue: 9091 },
|
|
{ name: "config-file", alias: "c", type: String },
|
|
{ name: "watch-file", alias: "w", type: Boolean, defaultValue: false },
|
|
{ name: "prometheus-url", alias: "u", type: String },
|
|
];
|
|
|
|
const cli = cmdline(optionDefinitions);
|
|
|
|
if (!cli["prometheus-url"]) {
|
|
throw new Error("--prometheus-url is required.");
|
|
}
|
|
|
|
const config: Configuration = {
|
|
bindHost: cli["bind-host"],
|
|
port: cli.port,
|
|
configFile: cli["config-file"],
|
|
watchFile: cli["watch-file"],
|
|
prometheusURL: cli["prometheus-url"],
|
|
queriesAllow: [],
|
|
variables: [],
|
|
queriesCompiled: [],
|
|
labelsAllow: [],
|
|
};
|
|
|
|
function arrayProp(propName: string, target: any): string[] | undefined {
|
|
if (!(propName in target)) {
|
|
return;
|
|
}
|
|
if (!Array.isArray(target[propName])) {
|
|
throw new Error(`property '${propName}' should be an array.`);
|
|
}
|
|
const badElementIndex = target[propName].findIndex(v => typeof v !== "string" && typeof v !== "number");
|
|
if (badElementIndex !== -1) {
|
|
throw new Error(`all elements of '${propName}' should be strings or numbers. offending element index is ${badElementIndex}.`);
|
|
}
|
|
return target[propName];
|
|
}
|
|
|
|
function loadConfigFileVariables(data: object): AnyVariable[] {
|
|
const vars: AnyVariable[] = [];
|
|
for (const [name, value] of Object.entries(data)) {
|
|
if (typeof value === "string" || typeof value === "number") {
|
|
vars.push({
|
|
name,
|
|
varType: "literal",
|
|
text: `${value}`
|
|
});
|
|
continue;
|
|
}
|
|
if (typeof value === "object") {
|
|
try {
|
|
let separator = "|";
|
|
if (typeof value.separator === "string") {
|
|
separator = value.separator;
|
|
}
|
|
|
|
let items = arrayProp("any_of", value);
|
|
if (items) {
|
|
vars.push({
|
|
name,
|
|
varType: "any_of_array",
|
|
items,
|
|
separator,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
items = arrayProp("all_of", value);
|
|
if (items) {
|
|
vars.push({
|
|
name,
|
|
varType: "all_of_array",
|
|
items,
|
|
separator,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (typeof value.pattern === "string") {
|
|
let flags = "";
|
|
if(typeof value.flags === "string") {
|
|
flags = value.flags;
|
|
}
|
|
|
|
vars.push({
|
|
name,
|
|
varType: "pattern",
|
|
pattern: new RegExp(value.pattern, flags),
|
|
});
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`failed to parse '${name}' variable: ${e}`);
|
|
}
|
|
}
|
|
}
|
|
return vars;
|
|
}
|
|
|
|
export async function loadConfigFile(filePath: string): Promise<void> {
|
|
const doc = yaml.load(await readFile(filePath, "utf8")) as {
|
|
queries_allow?: any[],
|
|
variables?: object,
|
|
labels_allow?: object,
|
|
};
|
|
|
|
if (typeof doc.variables !== "object") {
|
|
throw new Error("'variables' defined in config file, but not a dictionary.");
|
|
}
|
|
|
|
if (doc.variables) {
|
|
config.variables = loadConfigFileVariables(doc.variables);
|
|
}
|
|
|
|
if (typeof doc.labels_allow === "object") {
|
|
config.labelsAllow = [];
|
|
for(const [key, value] of Object.entries(doc.labels_allow)) {
|
|
if (typeof value === "boolean" && value) {
|
|
config.labelsAllow.push({ name: key });
|
|
} else {
|
|
const values = arrayProp(key, doc.labels_allow)!;
|
|
config.labelsAllow.push({ name: key, values });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(doc.queries_allow)) {
|
|
throw new Error("Config file format error: no 'queries_allow' property, or not an array.");
|
|
}
|
|
|
|
config.queriesAllow = [];
|
|
for (const item of doc.queries_allow) {
|
|
if (typeof item === "string") {
|
|
config.queriesAllow.push(item);
|
|
}
|
|
}
|
|
|
|
const queries = [];
|
|
for (const q of config.queriesAllow) {
|
|
queries.push(compileQuery(q, config.variables));
|
|
}
|
|
config.queriesCompiled = queries;
|
|
}
|
|
|
|
await loadConfigFile(config.configFile);
|
|
|
|
if (config.watchFile) {
|
|
fs.watchFile(config.configFile, async (curr, prev) => {
|
|
if (curr.mtimeMs === prev.mtimeMs) {
|
|
return;
|
|
}
|
|
console.log("[watch-file] Config file was updated. Re-reading configuration...");
|
|
try {
|
|
await loadConfigFile(config.configFile);
|
|
} catch (e) {
|
|
console.error("Failed to read config file", config.configFile, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
export { config };
|