UNPKG

log-vault

Version:

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

1,217 lines (1,216 loc) 47.7 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: 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("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: { transitional: { silentJSONParsing: true, forcedJSONParsing: true, clarifyTimeoutError: false }, adapter: ["xhr", "http", "fetch"], transformRequest: [], transformResponse: [], timeout: 100, xsrfCookieName: "XSRF-TOKEN", xsrfHeaderName: "X-XSRF-TOKEN", maxContentLength: -1, maxBodyLength: -1, env: {}, headers: { Accept: "application/json, text/plain, */*", "User-Agent": "axios/1.7.7", "Accept-Encoding": "gzip, compress, deflate, br" }, method: "get", url: "http://localhost:0000" }, 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: { adapter: ["xhr", "http", "fetch"], env: {}, headers: { Accept: "application/json, text/plain, */*", "Accept-Encoding": "gzip, compress, deflate, br", "User-Agent": "axios/1.7.7" }, maxBodyLength: -1, maxContentLength: -1, method: "get", timeout: 100, transformRequest: [], transformResponse: [], transitional: { clarifyTimeoutError: false, forcedJSONParsing: true, silentJSONParsing: true }, url: "http://localhost:0000", xsrfCookieName: "XSRF-TOKEN", xsrfHeaderName: "X-XSRF-TOKEN" }, 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.stringMatching(defaults_1.defaultTimestampRegexp) } ]); }); 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]] }); }); });