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 { 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 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 { 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 };