@logtail/node
Version:
Better Stack Node.js logger (formerly Logtail)
210 lines • 9.16 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const stream_1 = require("stream");
const nock_1 = __importDefault(require("nock"));
const types_1 = require("@logtail/types");
const node_1 = require("./node");
/**
* Create a log with a random string / current date
*/
function getRandomLog(message) {
return {
dt: new Date(),
level: types_1.LogLevel.Info,
message,
};
}
describe("node tests", () => {
it("should echo log if logtail sends 20x status code", async () => {
(0, nock_1.default)("https://in.logs.betterstack.com").post("/").reply(201);
const message = String(Math.random());
const expectedLog = getRandomLog(message);
const node = new node_1.Node("valid source token", { throwExceptions: true });
const echoedLog = await node.log(message);
expect(echoedLog.message).toEqual(expectedLog.message);
});
it("should throw error if logtail sends non 200 status code", async () => {
(0, nock_1.default)("https://in.logs.betterstack.com").post("/").reply(401);
const node = new node_1.Node("invalid source token", { throwExceptions: true });
const message = String(Math.random);
await expect(node.log(message)).rejects.toThrow();
});
it("should warn and echo log even with circular reference as context", async () => {
(0, nock_1.default)("https://in.logs.betterstack.com").post("/").reply(201);
let circularContext = { foo: { value: 42 } };
circularContext.foo.bar = circularContext;
// Mock console warnings
const originalConsoleWarn = console.warn;
console.warn = jest.fn();
const message = String(Math.random());
const expectedLog = getRandomLog(message);
const node = new node_1.Node("valid source token", { throwExceptions: true });
const echoedLog = await node.log(message, types_1.LogLevel.Info, circularContext);
expect(echoedLog.message).toEqual(expectedLog.message);
expect(console.warn.mock.calls).toHaveLength(1);
expect(console.warn.mock.calls[0][0]).toBe("[Logtail] Found a circular reference when serializing logs. Please do not use circular references in your logs.");
console.warn = originalConsoleWarn;
});
it("should enable piping logs to a writable stream", async () => {
// Create a writable stream
const writeStream = new stream_1.Writable({
write(chunk, encoding, callback) {
// Will be a buffered JSON string -- parse
const log = JSON.parse(chunk.toString());
// Expect the log to match the message
expect(log.message).toEqual(message);
callback();
},
});
// Fixtures
const logtail = new node_1.Node("test", { throwExceptions: true });
logtail.pipe(writeStream);
const message = "This should be streamed";
// Mock the sync method by simply returning the same logs
logtail.setSync(async (logs) => logs);
// Fire a log event
await logtail.log(message);
});
it("should pipe logs to a writable file stream", async () => {
// Create a temporary file name
const temp = path.join(os.tmpdir(), `logtail_${Math.random()}`);
// Create a write stream based on that temp file
const writeStream = fs.createWriteStream(temp);
// Create a Pass-through stream, to ensure multiplexing works
const passThrough = new stream_1.PassThrough();
// Pass write stream to Better Stack
const logtail = new node_1.Node("test", { throwExceptions: true });
logtail.pipe(passThrough).pipe(writeStream);
// Mock the sync method by simply returning the same logs
logtail.setSync(async (logs) => logs);
// Create messages
const messages = ["message 1", "message 2"];
// Log messages
await Promise.all(messages.map((msg) => logtail.log(msg)));
writeStream.on("finish", () => {
// Get the stored data, and translate back to JSON
const data = fs
.readFileSync(temp)
.toString()
.trim()
.split("\n")
.map((line) => JSON.parse(line));
// Messages should match
for (let i = 0; i < messages.length; i++) {
expect(data[i].message).toEqual(messages[i]);
}
});
writeStream.end();
});
it("should drop logs when queue limit is exceeded", async () => {
let requestCount = 0;
(0, nock_1.default)("https://in.logs.betterstack.com")
.post("/")
.times(10)
.delay(500) // 500ms delay
.reply(201, () => {
requestCount++;
});
const node = new node_1.Node("test-token", {
syncMax: 2, // Only 2 concurrent batches
syncQueuedMax: 3, // Only 3 batches can be queued
batchSize: 2, // Two logs per batch
ignoreExceptions: true, // Do not print the exceptions to console during test
retryCount: 0,
});
// Send 24 logs rapidly
const promises = [];
for (let i = 0; i < 24; i++) {
promises.push(node.log(`Message ${i}`));
}
// Wait a bit for queue processing
await new Promise((resolve) => setTimeout(resolve, 100));
// 2 immediate batches of 2
expect(requestCount).toBe(2);
// which haven't synced yet
expect(node.synced).toBe(0);
// 24 logs in total - 2 immediate batches of 2 - 3 queued batches of 2
expect(node.dropped).toBe(14);
// Wait for the queue to finish
await Promise.all(promises);
// 2 immediate batches of 2 + 3 queued batches of 2
expect(requestCount).toBe(5);
expect(node.synced).toBe(10);
// 24 in total - 5 done batches of 2
expect(node.dropped).toBe(14);
});
it("should timeout when request takes longer than configured timeout", async () => {
// Mock a slow endpoint
(0, nock_1.default)("https://in.logs.betterstack.com")
.post("/")
.delay(300) // 300ms delay
.reply(201);
const node = new node_1.Node("test-token", {
timeout: 200, // 200ms timeout
throwExceptions: true,
retryCount: 0,
});
// This should timeout
await expect(node.log("Test message")).rejects.toThrow("Request timeout after 200ms");
});
it("should complete successfully when request is within timeout", async () => {
// Mock a normal endpoint
(0, nock_1.default)("https://in.logs.betterstack.com")
.post("/")
.delay(100) // 100ms delay
.reply(201);
const node = new node_1.Node("test-token", {
timeout: 1000, // 1s timeout
throwExceptions: true,
retryCount: 0,
});
// This should succeed
const result = await node.log("Test message");
expect(result).toHaveProperty("message", "Test message");
});
it("should work without timeout when timeout is 0", async () => {
// Mock a slow endpoint
(0, nock_1.default)("https://in.logs.betterstack.com")
.post("/")
.delay(100) // 100ms delay
.reply(200);
const node = new node_1.Node("test-token", {
timeout: 0, // No timeout
throwExceptions: true,
retryCount: 0,
});
// This should complete despite the delay
const result = await node.log("Test message");
expect(result).toHaveProperty("message", "Test message");
});
});
//# sourceMappingURL=node.test.js.map