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
JavaScript
"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();
});
});