#!/usr/bin/env node
import pump from "pump";
import { debuglog, parseArgs } from "node:util";
import abstractTransportBuild from "pino-abstract-transport";

//#region src/cli/args.ts
const options = {
	"version": {
		type: "boolean",
		short: "v",
		help: `Print version number and exit`
	},
	"user": {
		type: "string",
		short: "u",
		help: "Loki username"
	},
	"password": {
		type: "string",
		short: "p",
		help: "Loki password"
	},
	"hostname": {
		type: "string",
		default: "http://localhost:3100",
		help: "URL for Loki"
	},
	"endpoint": {
		type: "string",
		default: "/loki/api/v1/push",
		help: "Path to the Loki push API"
	},
	"batch": {
		type: "boolean",
		default: true,
		short: "b",
		help: "Should logs be sent in batch mode"
	},
	"interval": {
		type: "string",
		default: "5",
		short: "i",
		help: "The interval at which batched logs are sent in seconds"
	},
	"timeout": {
		type: "string",
		default: "2000",
		short: "t",
		help: "Timeout for request to Loki"
	},
	"silenceErrors": {
		type: "boolean",
		default: false,
		short: "s",
		help: "If false, errors will be displayed in the console"
	},
	"replaceTimestamp": {
		type: "boolean",
		default: false,
		short: "r",
		help: "Replace pino logs timestamps with Date.now()"
	},
	"labels": {
		type: "string",
		short: "l",
		help: "Additional labels to be added to all Loki logs"
	},
	"convertArrays": {
		type: "boolean",
		default: false,
		help: "If true, arrays will be converted to objects"
	},
	"structuredMetaKey": {
		type: "string",
		default: "",
		help: "Key to use for structured metadata"
	},
	"propsLabels": {
		type: "string",
		help: "Fields in log line to convert to Loki labels (comma separated values)"
	},
	"pl": {
		type: "string",
		help: "Deprecated: Use --propsLabels instead. Fields in log line to convert to Loki labels (comma separated values)"
	},
	"no-stdout": {
		type: "boolean",
		default: false,
		help: "Disable output to stdout"
	},
	"headers": {
		type: "string",
		help: "Custom headers to be sent with the request to Loki (comma separated key=value pairs)"
	},
	"help": {
		type: "boolean",
		short: "h",
		default: false,
		help: "Print this help message and exit"
	}
};

//#endregion
//#region src/debug.ts
var debug_default = debuglog("pino-loki");

//#endregion
//#region src/constants.ts
const LokiLogLevel = {
	Info: "info",
	Debug: "debug",
	Error: "error",
	Warning: "warning",
	Critical: "critical"
};

//#endregion
//#region src/get.ts
/**
* Dynamically get a nested value from an array or object with a
* string.
*/
function get(value, path, defaultValue) {
	const segments = path.split(/[.[\]]/g);
	let current = value;
	for (const key of segments) {
		if (current === null) return defaultValue;
		if (current === void 0) return defaultValue;
		const unquotedKey = key.replace(/["']/g, "");
		if (unquotedKey.trim() === "") continue;
		current = current[unquotedKey];
	}
	if (current === void 0) return defaultValue;
	return current;
}

//#endregion
//#region src/format_mesage.ts
function formatLog(options$1) {
	const { log, logFormat } = options$1;
	if (logFormat && typeof logFormat === "string") return logFormat.replace(/{([^{}]+)}/g, (_match, p1) => get(log, p1) || "");
	if (logFormat && typeof logFormat === "function") return logFormat(options$1.log);
	throw new Error("Message format must be a string or a function. Received: " + typeof logFormat);
}

//#endregion
//#region src/log_builder.ts
const NANOSECONDS_LENGTH = 19;
/**
* Converts a Pino log to a Loki log
*/
var LogBuilder = class {
	#propsToLabels;
	#levelMap;
	constructor(options$1) {
		this.#propsToLabels = options$1?.propsToLabels || [];
		this.#levelMap = Object.assign({
			10: LokiLogLevel.Debug,
			20: LokiLogLevel.Debug,
			30: LokiLogLevel.Info,
			40: LokiLogLevel.Warning,
			50: LokiLogLevel.Error,
			60: LokiLogLevel.Critical
		}, options$1?.levelMap);
	}
	/**
	* Builds a timestamp string from a Pino log object.
	* @returns A string representing the timestamp in nanoseconds.
	*/
	#buildTimestamp(log, replaceTimestamp) {
		if (replaceTimestamp) return ((/* @__PURE__ */ new Date()).getTime() * 1e6).toString();
		const time = log.time || Date.now();
		const strTime = time.toString();
		if (strTime.length === NANOSECONDS_LENGTH) return strTime;
		const missingFactor = 10 ** (19 - strTime.length);
		return (time * missingFactor).toString();
	}
	/**
	* Stringify the log object. If convertArrays is true then it will convert
	* arrays to objects with indexes as keys.
	*/
	#stringifyLog(log, convertArrays) {
		return JSON.stringify(log, (_, value) => {
			if (!convertArrays) return value;
			if (Array.isArray(value)) return Object.fromEntries(value.map((value$1, index) => [index, value$1]));
			return value;
		});
	}
	#buildLabelsFromProps(log) {
		const labels = {};
		for (const prop of this.#propsToLabels) if (log[prop]) labels[prop] = log[prop];
		return labels;
	}
	/**
	* Convert a level to a human readable status
	*/
	statusFromLevel(level) {
		return this.#levelMap[level] || LokiLogLevel.Info;
	}
	/**
	* Build a loki log entry from a pino log
	*/
	build(options$1) {
		const status = this.statusFromLevel(options$1.log.level);
		const time = this.#buildTimestamp(options$1.log, options$1.replaceTimestamp);
		const propsLabels = this.#buildLabelsFromProps(options$1.log);
		const hostname = options$1.log.hostname;
		options$1.log.hostname = void 0;
		const structuredMetadata = options$1.structuredMetaKey ? options$1.log[options$1.structuredMetaKey] : void 0;
		const formattedMessage = options$1.logFormat ? formatLog({
			logFormat: options$1.logFormat,
			log: {
				...options$1.log,
				lokilevel: status
			}
		}) : this.#stringifyLog(options$1.log, options$1.convertArrays);
		return {
			stream: {
				level: status,
				hostname,
				...options$1.additionalLabels,
				...propsLabels
			},
			values: [structuredMetadata ? [
				time,
				formattedMessage,
				structuredMetadata
			] : [time, formattedMessage]]
		};
	}
};

//#endregion
//#region src/log_pusher.ts
var RequestError = class extends Error {
	responseBody;
	constructor(message, responseBody) {
		super(message);
		this.name = "RequestError";
		this.responseBody = responseBody;
	}
};
/**
* Responsible for pushing logs to Loki
*/
var LogPusher = class {
	#options;
	#logBuilder;
	constructor(options$1) {
		this.#options = options$1;
		this.#logBuilder = new LogBuilder({
			levelMap: options$1.levelMap,
			propsToLabels: options$1.propsToLabels
		});
	}
	/**
	* Handle push failures
	*/
	#handleFailure(err) {
		if (this.#options.silenceErrors === true) return;
		if (err instanceof RequestError) {
			console.error(err.message + "\n" + err.responseBody);
			return;
		}
		console.error("Got unknown error when trying to send log to Loki, error output:", err);
	}
	/**
	* Push one or multiples logs entries to Loki
	*/
	async push(logs) {
		if (!Array.isArray(logs)) logs = [logs];
		const lokiLogs = logs.map((log) => this.#logBuilder.build({
			log,
			replaceTimestamp: this.#options.replaceTimestamp,
			additionalLabels: this.#options.labels,
			convertArrays: this.#options.convertArrays,
			structuredMetaKey: this.#options.structuredMetaKey,
			logFormat: this.#options.logFormat
		}));
		debug_default(`[LogPusher] pushing ${lokiLogs.length} logs to Loki`);
		try {
			const response = await fetch(new URL(this.#options.endpoint ?? "loki/api/v1/push", this.#options.host), {
				method: "POST",
				signal: AbortSignal.timeout(this.#options.timeout ?? 3e4),
				headers: {
					...this.#options.headers,
					...this.#options.basicAuth && { Authorization: "Basic " + Buffer.from(`${this.#options.basicAuth.username}:${this.#options.basicAuth.password}`).toString("base64") },
					"Content-Type": "application/json"
				},
				body: JSON.stringify({ streams: lokiLogs })
			});
			if (!response.ok) throw new RequestError("Got error when trying to send log to loki", await response.text());
		} catch (err) {
			this.#handleFailure(err);
		}
		debug_default(`[LogPusher] pushed ${lokiLogs.length} logs to Loki`, { logs: lokiLogs });
	}
};

//#endregion
//#region src/index.ts
/**
* Resolves the options for the Pino Loki transport
*/
function resolveOptions(options$1) {
	return {
		...options$1,
		endpoint: options$1.endpoint ?? "loki/api/v1/push",
		timeout: options$1.timeout ?? 3e4,
		silenceErrors: options$1.silenceErrors ?? false,
		batching: options$1.batching ?? true,
		interval: options$1.interval ?? 5,
		replaceTimestamp: options$1.replaceTimestamp ?? false,
		propsToLabels: options$1.propsToLabels ?? [],
		convertArrays: options$1.convertArrays ?? false,
		structuredMetaKey: options$1.structuredMetaKey,
		logFormat: options$1.logFormat
	};
}
function pinoLoki(userOptions) {
	const options$1 = resolveOptions(userOptions);
	const logPusher = new LogPusher(options$1);
	debug_default(`[PinoLoki] initialized with options: ${JSON.stringify(options$1)}`);
	let batchInterval;
	let pinoLogBuffer = [];
	return abstractTransportBuild(async (source) => {
		if (options$1.batching) batchInterval = setInterval(async () => {
			debug_default(`Batch interval reached, sending ${pinoLogBuffer.length} logs to Loki`);
			if (pinoLogBuffer.length === 0) return;
			logPusher.push(pinoLogBuffer);
			pinoLogBuffer = [];
		}, options$1.interval * 1e3);
		for await (const obj of source) {
			if (options$1.batching) {
				pinoLogBuffer.push(obj);
				continue;
			}
			logPusher.push(obj);
		}
	}, { async close() {
		if (options$1.batching) {
			clearInterval(batchInterval);
			await logPusher.push(pinoLogBuffer);
		}
	} });
}

//#endregion
//#region src/cli/print_help.ts
function printHelp(opts) {
	console.log(`Usage: pino-loki [options]\n\nOptions:`);
	const flagsData = Object.entries(opts).map(([key, option]) => ({
		flags: (option.short ? `-${option.short}, --${key}` : `--${key}`) + (option.type === "boolean" ? "" : ` <value>`),
		help: option.help,
		default: option.default
	}));
	const maxFlagsWidth = Math.max(...flagsData.map((item) => item.flags.length));
	for (const { flags, help, default: defaultValue } of flagsData) {
		const paddedFlags = flags.padEnd(maxFlagsWidth + 2);
		const defaultText = defaultValue ? ` (default: ${defaultValue})` : "";
		console.log(`  ${paddedFlags}${help}${defaultText}`);
	}
	console.log(`\nExample:\n  pino-loki --hostname http://localhost:3100 --user myuser --password mypass --batch --interval 10`);
}

//#endregion
//#region package.json
var name = "pino-loki";
var type = "module";
var version = "2.6.0";
var packageManager = "pnpm@10.11.0";
var description = "A transport for pino that sends messages to Loki";
var author = "Julien Ripouteau <julien@ripouteau.com>";
var license = "MIT";
var funding = "https://github.com/sponsors/Julien-R44";
var homepage = "https://github.com/Julien-R44/pino-loki#readme";
var repository = {
	"type": "git",
	"url": "git+https://github.com/Julien-R44/pino-loki.git"
};
var bugs = { "url": "https://github.com/Julien-R44/pino-loki/issues" };
var keywords = [
	"pino",
	"pino-transport",
	"loki",
	"logging"
];
var exports = {
	".": {
		"import": "./dist/index.js",
		"require": "./dist/index.cjs"
	},
	"./package.json": "./package.json"
};
var main$1 = "./dist/index.cjs";
var module = "./dist/index.js";
var types = "./dist/index.d.cts";
var bin = { "pino-loki": "dist/cli.js" };
var files = ["dist"];
var scripts = {
	"build": "tsdown",
	"lint": "eslint .",
	"format": "prettier --write .",
	"prepublishOnly": "pnpm build",
	"release": "bumpp --commit --push --tag && pnpm publish",
	"quick:test": "node --experimental-strip-types bin/test.ts",
	"test": "c8 node --experimental-strip-types bin/test.ts",
	"typecheck": "tsc --noEmit",
	"checks": "pnpm typecheck && pnpm lint && pnpm test"
};
var dependencies = {
	"pino-abstract-transport": "^2.0.0",
	"pump": "^3.0.2"
};
var devDependencies = {
	"@japa/assert": "^4.0.1",
	"@japa/runner": "^4.2.0",
	"@japa/spec-reporter": "^1.3.3",
	"@julr/tooling-configs": "^4.0.0",
	"@types/node": "^22.15.29",
	"@types/pump": "^1.1.3",
	"bumpp": "^10.1.1",
	"c8": "^10.1.3",
	"dotenv": "^16.5.0",
	"eslint": "^9.28.0",
	"msw": "^2.8.7",
	"pino": "^9.7.0",
	"pino-pretty": "^13.0.0",
	"prettier": "^3.5.3",
	"tsdown": "^0.12.6",
	"typescript": "^5.8.3"
};
var prettier = "@julr/tooling-configs/prettier";
var package_default = {
	name,
	type,
	version,
	packageManager,
	description,
	author,
	license,
	funding,
	homepage,
	repository,
	bugs,
	keywords,
	exports,
	main: main$1,
	module,
	types,
	bin,
	files,
	scripts,
	dependencies,
	devDependencies,
	prettier
};

//#endregion
//#region src/cli/index.ts
function validateHeaders(headers) {
	const headerPairs = headers.split(",").map((pair) => pair.trim());
	const headerObject = {};
	for (const pair of headerPairs) {
		const [key, value] = pair.split("=").map((part) => part.trim());
		if (!key || !value) throw new Error(`Invalid header format: "${pair}". Expected format is "key=value".`);
		headerObject[key] = value;
	}
	return headerObject;
}
/**
* Create a PinoLokiOptionsContract from cli arguments
*/
const createPinoLokiConfigFromArgs = () => {
	const { values } = parseArgs({ options });
	if (values.help) {
		printHelp(options);
		process.exit(0);
	}
	if (values.version) {
		console.log(`v${package_default.version}`);
		process.exit(0);
	}
	const propsLabels = (values.propsLabels ?? values.pl ?? "").split(",").map((label) => label.trim()).filter(Boolean);
	const config = {
		host: values.hostname,
		endpoint: values.endpoint,
		timeout: values.timeout ? Number(values.timeout) : void 0,
		silenceErrors: values.silenceErrors,
		batching: values.batch,
		interval: values.interval ? Number(values.interval) : void 0,
		replaceTimestamp: values.replaceTimestamp,
		labels: values.labels ? JSON.parse(values.labels) : void 0,
		propsToLabels: propsLabels,
		structuredMetaKey: values.structuredMetaKey,
		convertArrays: values.convertArrays,
		headers: values.headers ? validateHeaders(values.headers) : void 0
	};
	if (values.user && values.password) config.basicAuth = {
		username: values.user,
		password: values.password
	};
	return config;
};
function main() {
	pump(process.stdin, pinoLoki(createPinoLokiConfigFromArgs()));
}
main();

//#endregion
export { createPinoLokiConfigFromArgs, validateHeaders };
//# sourceMappingURL=cli.js.map