UNPKG

log-vault

Version:

A generator of Winston logger instance with pre-defined configurable transports and formats and extra functionality.

1,237 lines (1,236 loc) 48.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const strip_color_1 = __importDefault(require("strip-color")); const transports_1 = require("winston/lib/winston/transports"); const LogVault_1 = require("./LogVault"); const node_child_process_1 = require("node:child_process"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const defaults_1 = require("./defaults"); const _1 = require("."); const winston_mongodb_1 = require("winston-mongodb"); const winston_loki_1 = __importDefault(require("winston-loki")); const transports_2 = require("./transports"); const wait_1 = require("./test-files/util/wait"); describe("console transport: logging", () => { let output; let logger; beforeEach(() => { output = ""; jest.clearAllMocks(); }); afterEach(() => { if (logger) { const consoleTransport = getConsoleTransport(logger); logger.exceptions.unhandle(consoleTransport); logger.rejections.unhandle(consoleTransport); process.removeAllListeners("uncaughtException"); process.removeAllListeners("unhandledRejection"); } }); it("logger.info single string", () => { const { logger } = new LogVault_1.LogVault().withConsole(); const spy = getConsoleSpy(logger); logger.info("A log message"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: A log message"); }); it("logger.log single string", () => { const { logger } = new LogVault_1.LogVault().withConsole(); const spy = getConsoleSpy(logger); logger.log({ level: "warn", message: "A log message" }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("warn: A log message"); }); it("logger.info several string messages", () => { const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.info("First", "Second"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: First\n[\n 'Second'\n]"); }); it("logger.log several string messages", () => { const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.log({ level: "info", message: "First", extra: "second" }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: First\n[\n {\n extra: 'second'\n }\n]"); }); it("logger.info an object", () => { const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.info({ foo: "bar" }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: {\n foo: 'bar'\n}"); }); it("logger.info different entities", () => { const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.info("this is an object:", { some: "data" }, [1, 2]); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: this is an object:\n[\n {\n some: 'data'\n },\n [\n 1,\n 2\n ]\n]"); }); it("logger.info circular object", () => { const circular = { b: 2, }; circular.b = circular; const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.info(circular); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: {\n b: '...[Truncated]'\n}"); }); it("logger error", () => { const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.error("Error"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("error: Error"); }); it("logger warn", () => { const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.warn("Warn"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("warn: Warn"); }); it("logger info", () => { const logVault = new LogVault_1.LogVault().withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.info("Data"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: Data"); }); it("logger http", () => { const logVault = new LogVault_1.LogVault({ level: "silly" }).withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.http("Data"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("http: Data"); }); it("logger debug", () => { const logVault = new LogVault_1.LogVault({ level: "silly" }).withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.debug("Data"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("debug: Data"); }); it("logger silly", () => { const logVault = new LogVault_1.LogVault({ level: "silly" }).withConsole(); logger = logVault.logger; const spy = getConsoleSpy(logger); logger.silly("Data"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("silly: Data"); }); it("console: mask sensitive field: password", () => { const { logger } = new LogVault_1.LogVault().withConsole(); const spy = getConsoleSpy(logger); logger.info({ user: "username", password: "P@ssw0rd", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: {\n user: 'username',\n password: '...[Masked]'\n}"); }); it("console: mask mongodb connection string by default", () => { const { logger } = new LogVault_1.LogVault().withConsole(); const spy = getConsoleSpy(logger); logger.info(`Failed to connect to MongoDB at mongodb+srv://username:db_password@cluster0.knoxu.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0`); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: Failed to connect to MongoDB at mongodb+srv://...[Masked]:...[Masked]@cluster0.knoxu.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"); }); it("console: mask postgres connection string by default", () => { const { logger } = new LogVault_1.LogVault().withConsole(); const spy = getConsoleSpy(logger); logger.info(`postgresql://root:secret@dbname.dwktu38uygj.eu-north-1.rds.amazonaws.com:5432/postgres`); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: postgresql://...[Masked]:...[Masked]@dbname.dwktu38uygj.eu-north-1.rds.amazonaws.com:5432/postgres"); }); it("console: do not mask any field", () => { const { logger } = new LogVault_1.LogVault({ maskOptions: { fields: [] }, }).withConsole(); const spy = getConsoleSpy(logger); logger.info({ user: "username", password: "P@ssw0rd", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: {\n user: 'username',\n password: 'P@ssw0rd'\n}"); }); it("console: mask values in extra object", () => { const logVault = new LogVault_1.LogVault().withConsole(); const { logger } = logVault; const spy = getConsoleSpy(logger); logger.http("An error occured", { request: { headers: { "content-type": "application/json", }, body: { login: "sdjkjasdh", password: "P@ssw0rd", }, }, response: { header: { id: null, status: "ERROR", }, error: { code: "INPDATAFORMAT", message: "Input data format is incorrect", }, }, }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("http: An error occured\n" + "[\n" + " {\n" + " request: {\n" + " headers: {\n" + " 'content-type': 'application/json'\n" + " },\n" + " body: {\n" + " login: 'sdjkjasdh',\n" + " password: '...[Masked]'\n" + " }\n" + " },\n" + " response: {\n" + " header: {\n" + " id: null,\n" + " status: 'ERROR'\n" + " },\n" + " error: {\n" + " code: 'INPDATAFORMAT',\n" + " message: 'Input data format is incorrect'\n" + " }\n" + " }\n" + " }\n" + "]"); }); it("console: do not output LogOptions", () => { const { logger } = new LogVault_1.LogVault().withConsole(); const spy = getConsoleSpy(logger); logger.info("something", new _1.LogOptions({ meta: { key: "value" } })); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual("info: something"); }); it("capture console: log a single string", () => { const logVault = new LogVault_1.LogVault().withConsole().captureConsole(); const { logger } = logVault; const spy = getConsoleSpy(logger); console.log("logging with console.log"); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual("info: logging with console.log"); }); it("numeric string, avoid parsing non-JSON strings, fix for max safe integer message value", async () => { const logVault = new LogVault_1.LogVault().withConsole().captureConsole(); const { logger } = logVault; const spy = getConsoleSpy(logger); const xx = "3383814258104171532"; console.log("xx:", xx); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual("info: xx:\n[\n '3383814258104171532'\n]"); }); it("capture console: log different entities", () => { const logVault = new LogVault_1.LogVault().withConsole().captureConsole(); const { logger } = logVault; const spy = getConsoleSpy(logger); console.log("this is an object:", { some: "data" }, [1, 2]); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual("info: this is an object:\n[\n {\n some: 'data'\n },\n [\n 1,\n 2\n ]\n]"); }); function getConsoleTransport(logger) { const consoleTransport = logger.transports.find((t) => t instanceof transports_1.Console); if (!consoleTransport) throw new Error("Couldn't assign the console transport"); return consoleTransport; } function getConsoleSpy(logger) { const consoleTransport = getConsoleTransport(logger); return jest.spyOn(consoleTransport, "log").mockImplementation((data) => { const decolorized = (0, strip_color_1.default)(data[Symbol.for("message")]); const matched = decolorized.match(/^\d{2}\s[A-z]{3}\s\d{4}\s\d{2}:\d{2}:\d{2}\s\([+-]*\d{2}:\d{2}\)\s(.*)$/s); if (!matched) throw new Error("No match found in output"); output = matched[1]; }); } }); describe("console transport:catching exceptions and rejections", () => { it("catch a simple exception with console", () => { const buf = (0, node_child_process_1.execSync)("ts-node ./src/test-files/textError.ts"); const out = formatOutput(buf); expect(out).toEqual({ name: "Error", message: "An error occur", stack: expect.stringMatching(/Error:\sAn\serror\soccur\n/s), }); }); it("catch an axios error with console", () => { const buf = (0, node_child_process_1.execSync)("ts-node ./src/test-files/axiosError.ts"); const out = formatOutput(buf); expect(out).toEqual({ message: "timeout of 100ms exceeded", name: "AxiosError", stack: expect.stringMatching(/AxiosError:\stimeout\sof\s100ms\sexceeded\n/s), config: expect.any(Object), code: "ECONNABORTED", }); }); it("catch a simple exception with console capturing enabled", () => { const buf = (0, node_child_process_1.execSync)("ts-node ./src/test-files/textError.ts"); const out = formatOutput(buf); expect(out).toEqual({ name: "Error", message: "An error occur", stack: expect.stringMatching(/Error:\sAn\serror\soccur\n/s), }); }); it("mask fields in exception", () => { const buf = (0, node_child_process_1.execSync)("ts-node ./src/test-files/errorWithSensitiveField.ts"); const out = formatOutput(buf); expect(out).toEqual({ name: "Error", message: '{"user":"test@mail.com","password":"...[Masked]"}', stack: expect.stringMatching(/Error:\s{"user":"test@mail.com","password":"secret"}\n/s), }); }); function formatOutput(buf) { const decolorized = (0, strip_color_1.default)(buf.toString()); const matched = decolorized.match(/^\d{2}\s[A-z]{3}\s\d{4}\s\d{2}:\d{2}:\d{2}\s\([+-]*\d{2}:\d{2}\)\serror:\s(.*)$/s); if (!matched) throw new Error("No match found in output"); let obj; eval("obj=" + matched[1]); return obj; } }); describe("files transport", () => { let logger; beforeEach(() => { (0, node_fs_1.rmSync)((0, node_path_1.resolve)("./", "logs"), { recursive: true, force: true }); }); afterEach(() => { if (logger) { const fileErrorTransport = getFileErrorTransport(logger); logger.exceptions.unhandle(fileErrorTransport); logger.rejections.unhandle(fileErrorTransport); process.removeAllListeners("uncaughtException"); process.removeAllListeners("unhandledRejection"); } }); afterAll(() => { (0, node_fs_1.rmSync)((0, node_path_1.resolve)("./", "logs"), { recursive: true, force: true }); }); it("log to file: single string", async () => { logger = new LogVault_1.LogVault().withFiles().logger; logger.info("A log record"); const parsed = await readLogFile("http"); expect(parsed).toEqual([ { extra: [], level: "info", message: "A log record", meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: curcular values", async () => { logger = new LogVault_1.LogVault().withFiles().logger; const circular = { a: "b", content: "", }; circular.content = circular; logger.warn(circular); const parsed = await readLogFile("http"); expect(parsed).toEqual([ { level: "warn", message: { a: "b", content: "...[Truncated]" }, meta: { environment: "test", process: "log-vault", project: "log-vault", }, extra: [], timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: extra arguments", async () => { logger = new LogVault_1.LogVault().withFiles().logger; logger.info("This is a test", { a: 1 }); const parsed = await readLogFile("http"); expect(parsed).toEqual([ { extra: [ { a: 1, }, ], level: "info", message: "This is a test", meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: truncate object nested prop", async () => { const logger = new LogVault_1.LogVault().withFiles().logger; logger.info({ deep: { some: { obj: { deep: { deep: "nested" } } } }, not_nested: "value", }); const parsed = await readLogFile("http"); expect(parsed).toEqual([ { extra: [], level: "info", message: { deep: { some: { obj: { deep: "...[Truncated]", }, }, }, not_nested: "value", }, meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: shrink long strings", async () => { const logger = new LogVault_1.LogVault().withFiles().logger; logger.info("a".repeat(4096)); const parsed = await readLogFile("http"); expect(parsed).toEqual([ { level: "info", message: "a".repeat(2048) + "...", meta: { environment: "test", process: "log-vault", project: "log-vault", }, extra: [], timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: custom meta", async () => { const logger = new LogVault_1.LogVault().withFiles().logger; logger.info("A log record", new _1.LogOptions({ meta: { myCustomKey: "value" } }), "something else"); const parsed = await readLogFile("http"); expect(parsed).toEqual([ { extra: ["something else"], level: "info", message: "A log record", meta: { environment: "test", myCustomKey: "value", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: catch simple exception", async () => { (0, node_child_process_1.execSync)("ts-node ./src/test-files/textErrorWithFiles.ts"); const parsed = await readLogFile("error"); expect(parsed).toEqual([ { level: "error", message: { message: "An error occur", name: "Error", stack: expect.stringMatching(/Error:\sAn\serror\soccur\n/s), }, meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: catch axios exception", async () => { (0, node_child_process_1.execSync)("ts-node ./src/test-files/axiosErrorWithFiles.ts"); const parsed = await readLogFile("error"); expect(parsed).toEqual([ { level: "error", message: { code: "ECONNABORTED", config: expect.any(Object), message: "timeout of 100ms exceeded", name: "AxiosError", stack: expect.stringMatching(/AxiosError:\stimeout\sof\s100ms\sexceeded\n/s), }, meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.any(String), }, ]); }); it("log to file: mask sensitive field: password", async () => { const { logger } = new LogVault_1.LogVault().withFiles(); logger.info({ user: "username", password: "P@ssw0rd", }); const parsed = await readLogFile("http"); expect(parsed).toEqual([ { extra: [], level: "info", message: { password: "...[Masked]", user: "username" }, meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: capture a single string", async () => { const logVault = new LogVault_1.LogVault().withFiles().captureConsole(); console.log("logging with console.log"); const parsed = await readLogFile("http"); logVault.uncaptureConsole(); expect(parsed).toEqual([ { extra: [], level: "info", message: "logging with console.log", meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); it("log to file: capture different entities", async () => { const logVault = new LogVault_1.LogVault().withFiles().captureConsole(); console.log("this is an object:", { some: "data" }, [1, 2]); const parsed = await readLogFile("http"); logVault.uncaptureConsole(); expect(parsed).toEqual([ { extra: [{ some: "data" }, [1, 2]], level: "info", message: "this is an object:", meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }, ]); }); function parseFileContent(content) { let str = "[" + content.replaceAll(/\n}\n{/g, "\n},\n{"); str = str.slice(0, -1) + "]"; return JSON.parse(str); } function getLogFileName(level) { const parts = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(); return `${level}-${parts[4].value}-${parts[0].value}-${parts[2].value}.log`; } async function readLogFile(level) { await (0, wait_1.wait)(50); const logFileName = getLogFileName(level); const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)("./logs", logFileName), { encoding: "utf-8", }); const parsed = parseFileContent(content); return parsed; } function getFileErrorTransport(logger) { var _a; const transport = (_a = logger.transports) === null || _a === void 0 ? void 0 : _a.find((transport) => transport instanceof transports_1.DailyRotateFile && transport.handleExceptions); if (!transport) throw new Error("Couldn't unhandle files transport"); return transport; } }); describe("mongo transport", () => { let logger; let logVault; let output = ""; let spy; beforeEach(() => { logVault = new LogVault_1.LogVault() .withMongo({ db: "mongodb+srv://user:pass@cluster0.zooxqjl.mongodb.net/?retryWrites=true&w=majority", }) .captureConsole(); logger = logVault.logger; const mongo = logger.transports.find((t) => t instanceof winston_mongodb_1.MongoDB); if (!mongo) throw new Error("Failed to instantiate Mongo connection"); spy = jest.spyOn(mongo, "log").mockImplementation((data) => { output = data; }); }); afterEach(() => { if (logger) { const transport = logger.transports.find((t) => t instanceof winston_mongodb_1.MongoDB); logVault.uncaptureConsole(); if (transport) { logger.exceptions.unhandle(transport); logger.rejections.unhandle(transport); process.removeAllListeners("uncaughtException"); process.removeAllListeners("unhandledRejection"); } } }); it("log to mongo: single message", async () => { logger.info("Single message"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ message: "Single message", level: "info", timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), meta: { project: "log-vault", process: "log-vault", environment: "test", }, }); }); it("log to mongo: curcular values", async () => { const circular = { a: "b", content: "", }; circular.content = circular; logger.warn(circular); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "warn", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: '{\n "a": "b",\n "content": "...[Truncated]"\n}', }); }); it("log to mongo: extra arguments", async () => { logger.info("This is a test", { a: 1 }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: '[\n "This is a test",\n {\n "a": 1\n }\n]', }); }); it("log to mongo: truncate object nested prop", async () => { logger.info({ deep: { some: { obj: { deep: { deep: "nested" } } } }, not_nested: "value", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: "{\n" + ' "deep": {\n' + ' "some": {\n' + ' "obj": {\n' + ' "deep": "...[Truncated]"\n' + " }\n" + " }\n" + " },\n" + ' "not_nested": "value"\n' + "}", }); }); it("log to mongo: shrink long strings", async () => { logger.info("a".repeat(4096)); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ level: "info", message: "a".repeat(2048) + "...", meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }); }); it("log to mongo: custom meta", async () => { logger.info("A log record", new _1.LogOptions({ meta: { myCustomKey: "value" } }), "something else"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", myCustomKey: "value", }, message: '[\n "A log record",\n "something else"\n]', }); }); it("log to mongo: mask sensitive field: password", async () => { logger.info({ user: "username", password: "P@ssw0rd", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: '{\n "user": "username",\n "password": "...[Masked]"\n}', }); }); it("log to mongo: capture a single string", async () => { console.log("logging with console.log"); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: "logging with console.log", }); }); it("log to mongo: capture different entities", async () => { console.log("this is an object:", { some: "data" }, [1, 2]); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: "[\n" + ' "this is an object:",\n' + " {\n" + ' "some": "data"\n' + " },\n" + " [\n" + " 1,\n" + " 2\n" + " ]\n" + "]", }); }); it("log to mongo: nested object", async () => { logger.info("An error occured", { request: { headers: { "content-type": "application/json", }, body: { login: "sdjkjasdh", password: "P@ssw0rd", }, }, response: { header: { id: null, status: "ERROR", }, error: { code: "INPDATAFORMAT", message: "Input data format is incorrect", }, }, }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test" }, message: "[\n" + ' "An error occured",\n' + " {\n" + ' "request": {\n' + ' "headers": {\n' + ' "content-type": "application/json"\n' + " },\n" + ' "body": {\n' + ' "login": "sdjkjasdh",\n' + ' "password": "...[Masked]"\n' + " }\n" + " },\n" + ' "response": {\n' + ' "header": {\n' + ' "id": null,\n' + ' "status": "ERROR"\n' + " },\n" + ' "error": {\n' + ' "code": "INPDATAFORMAT",\n' + ' "message": "Input data format is incorrect"\n' + " }\n" + " }\n" + " }\n" + "]", }); }); }); describe("loki transport", () => { let output; let logger; let logVault; let spy; beforeEach(() => { jest.clearAllMocks(); logVault = new LogVault_1.LogVault().withLoki().captureConsole(); logger = logVault.logger; const loki = logger.transports.find((t) => t instanceof winston_loki_1.default); if (!loki) throw new Error("Couldn't assign Loki transport"); spy = jest.spyOn(loki, "log").mockImplementation((data) => { output = data; }); }); afterEach(() => { if (logger) { logVault.uncaptureConsole(); const transport = logger.transports.find((t) => t instanceof winston_loki_1.default); if (transport) { logger.exceptions.unhandle(transport); logger.rejections.unhandle(transport); process.removeAllListeners("uncaughtException"); process.removeAllListeners("unhandledRejection"); } } }); it("log to loki: single message", async () => { logger.info("Log record"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault" }, content: "Log record" }, [_1.MESSAGE]: '{"meta":{"process":"log-vault"},"content":"Log record"}', }); }); it("log to loki: curcular values", async () => { const circular = { a: "b", content: "", }; circular.content = circular; logger.warn(circular); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "warn", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault" }, content: '{\n "a": "b",\n "content": "...[Truncated]"\n}', }, [_1.MESSAGE]: '{"meta":{"process":"log-vault"},"content":"{\\n \\"a\\": \\"b\\",\\n \\"content\\": \\"...[Truncated]\\"\\n}"}', }); }); it("log to loki: extra arguments", async () => { logger.info("This is a test", { a: 1 }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault" }, content: ["This is a test", { a: 1 }], }, [_1.MESSAGE]: '{"meta":{"process":"log-vault"},"content":["This is a test",{"a":1}]}', }); }); it("log to mongo: truncate object nested prop", async () => { logger.info({ deep: { some: { obj: { deep: { deep: "nested" } } } }, not_nested: "value", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault" }, content: "{\n" + ' "deep": {\n' + ' "some": {\n' + ' "obj": {\n' + ' "deep": "...[Truncated]"\n' + " }\n" + " }\n" + " },\n" + ' "not_nested": "value"\n' + "}", }, [_1.MESSAGE]: '{"meta":{"process":"log-vault"},"content":"{\\n \\"deep\\": {\\n \\"some\\": {\\n \\"obj\\": {\\n \\"deep\\": \\"...[Truncated]\\"\\n }\\n }\\n },\\n \\"not_nested\\": \\"value\\"\\n}"}', }); }); it("log to loki: shrink long strings", async () => { logger.info("a".repeat(4096)); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ level: "info", // message: "a".repeat(2048) + "...", labels: { project: "log-vault", environment: "test" }, message: { content: "a".repeat(2048) + "...", meta: { process: "log-vault" }, }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), [_1.MESSAGE]: JSON.stringify({ meta: { process: "log-vault" }, content: "a".repeat(2048) + "...", }), }); }); it("log to loki: custom meta", async () => { logger.info("A log record", new _1.LogOptions({ meta: { myCustomKey: "value" } }), "something else"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault", myCustomKey: "value" }, content: ["A log record", "something else"], }, [_1.MESSAGE]: '{"meta":{"process":"log-vault","myCustomKey":"value"},"content":["A log record","something else"]}', }); }); it("log to loki: mask sensitive field: password", async () => { logger.info({ user: "username", password: "P@ssw0rd", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault" }, content: '{\n "user": "username",\n "password": "...[Masked]"\n}', }, [_1.MESSAGE]: '{"meta":{"process":"log-vault"},"content":"{\\n \\"user\\": \\"username\\",\\n \\"password\\": \\"...[Masked]\\"\\n}"}', }); }); it("log to loki: capture a single string", async () => { console.log("logging with console.log"); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault" }, content: "logging with console.log", }, [_1.MESSAGE]: '{"meta":{"process":"log-vault"},"content":"logging with console.log"}', }); }); it("log to loki: capture different entities", async () => { console.log("this is an object:", { some: "data" }, [1, 2]); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", labels: { project: "log-vault", environment: "test" }, message: { meta: { process: "log-vault" }, content: ["this is an object:", { some: "data" }, [1, 2]], }, [_1.MESSAGE]: '{"meta":{"process":"log-vault"},"content":["this is an object:",{"some":"data"},[1,2]]}', }); }); }); describe("notifications transport", () => { let logger; let logVault; let output = ""; let spy; beforeEach(() => { logVault = new LogVault_1.LogVault().withNotifications().captureConsole(); logger = logVault.logger; const notificationsTransport = logger.transports.find((t) => t instanceof transports_2.NotificationsTransport); if (!notificationsTransport) throw new Error("Failed to instantiate Mongo connection"); spy = jest .spyOn(notificationsTransport, "log") .mockImplementation((data) => { output = data; return Promise.resolve(); }); }); afterEach(() => { if (logger) { logVault.uncaptureConsole(); const transport = logger.transports.find((t) => t instanceof transports_2.NotificationsTransport); if (transport) { logger.exceptions.unhandle(transport); logger.rejections.unhandle(transport); process.removeAllListeners("uncaughtException"); process.removeAllListeners("unhandledRejection"); } } }); it("notifications transport: single string message", async () => { logger.info("A single string message"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test" }, message: "A single string message", }); }); it("notifications transport: curcular values", async () => { const circular = { a: "b", content: "", }; circular.content = circular; logger.warn(circular); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "warn", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: { a: "b", content: "...[Truncated]" }, }); }); it("notifications transport: extra arguments", async () => { logger.info("This is a test", { a: 1 }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: ["This is a test", { a: 1 }], }); }); it("notifications transport: truncate object nested prop", async () => { logger.info({ deep: { some: { obj: { deep: { deep: "nested" } } } }, not_nested: "value", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: { deep: { some: { obj: { deep: "...[Truncated]" } } }, not_nested: "value", }, }); }); it("notifications transport: shrink long strings", async () => { logger.info("a".repeat(4096)); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ level: "info", message: "a".repeat(2048) + "...", meta: { environment: "test", process: "log-vault", project: "log-vault", }, timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), }); }); it("notifications transport: custom meta", async () => { logger.info("A log record", new _1.LogOptions({ meta: { myCustomKey: "value" } }), "something else"); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", myCustomKey: "value", }, message: ["A log record", "something else"], }); }); it("notifications transport: mask sensitive field: password", async () => { logger.info({ user: "username", password: "P@ssw0rd", }); expect(spy).toHaveBeenCalledTimes(1); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: { user: "username", password: "...[Masked]" }, }); }); it("notifications transport: capture a single string", async () => { console.log("logging with console.log"); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: "logging with console.log", }); }); it("notifications transport: capture different entities", async () => { console.log("this is an object:", { some: "data" }, [1, 2]); expect(spy).toHaveBeenCalledTimes(1); logVault.uncaptureConsole(); expect(output).toEqual({ timestamp: expect.stringMatching(defaults_1.defaultTimestampRegexp), level: "info", meta: { project: "log-vault", process: "log-vault", environment: "test", }, message: ["this is an object:", { some: "data" }, [1, 2]], }); }); }); describe("fix: empty log message on catched error", () => { let output; let logger; let logVault; let spy; beforeEach(() => { jest.clearAllMocks(); logVault = new LogVault_1.LogVault().withLoki(); logger = logVault.logger; const loki = logger.transports.find((t) => t instanceof winston_loki_1.default); if (!loki) throw new Error("Couldn't assign Loki transport"); spy = jest.spyOn(loki, "log").mockImplementation((data) => { output = data; }); }); afterEach(() => { if (logger) { const transport = logger.transports.find((t) => t instanceof winston_loki_1.default); if (transport) { logger.exceptions.unhandle(transport); logger.rejections.unhandle(transport); process.removeAllListeners("uncaughtException"); process.removeAllListeners("unhandledRejection"); } } }); it("catch and log an error", async () => { try { throw new Error("Test error"); } catch (e) { logger.warn(e); } expect(spy).toHaveBeenCalledTimes(1); const content = output.message.content; expect(content[0]).toEqual("Test error"); expect(content[1].stack).toBeDefined(); }); });