@logtail/core
Version:
Better Stack logging core (formerly Logtail)
337 lines • 13 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("@logtail/types");
const tools_1 = require("@logtail/tools");
const serialize_error_1 = require("serialize-error");
// Set default options for Logtail
const defaultOptions = {
// Default sync endpoint (protocol + domain)
endpoint: "https://in.logs.betterstack.com",
// Maximum number of logs to sync in a single request to Better Stack
batchSize: 1000,
// Size of logs (in KiB) to trigger sync to Better Stack (0 to disable)
batchSizeKiB: 0,
// Max interval (in milliseconds) before a batch of logs proceeds to syncing
batchInterval: 1000,
// Maximum number of times to retry a failed sync request
retryCount: 3,
// Minimum number of milliseconds to wait before retrying a failed sync request
retryBackoff: 100,
// Maximum number of sync requests to make concurrently
syncMax: 5,
// Length of the checked window for logs burst protection in milliseconds (0 to disable)
burstProtectionMilliseconds: 5000,
// Maximum number of accepted logs in the specified time window (0 to disable)
burstProtectionMax: 10000,
// If true, errors when sending logs will be ignored
// Has precedence over throwExceptions
ignoreExceptions: false,
// If true, errors when sending logs will result in a thrown exception
throwExceptions: false,
// Maximum depth (number of attribute levels) of a context object
contextObjectMaxDepth: 50,
// Produce a warn log when context object max depth is reached
contextObjectMaxDepthWarn: true,
// Produce a warning when circular reference is found in context object
contextObjectCircularRefWarn: true,
// If true, all logs will be sent to standard console output functions (console.info, console.warn, ...)
sendLogsToConsoleOutput: false,
// If true, all logs will be sent to Better Stack
sendLogsToBetterStack: true,
// Function to be used to calculate size of logs in bytes (to evaluate batchSizeLimitKiB)
calculateLogSizeBytes: tools_1.calculateJsonLogSizeBytes,
};
/**
* Logtail core class for logging to the Better Stack service
*/
class Logtail {
/* CONSTRUCTOR */
/**
* Initializes a new Logtail instance
*
* @param sourceToken: string - Private source token for logging to Better Stack
* @param options?: ILogtailOptions - Optionally specify Logtail options
*/
constructor(sourceToken, options) {
// Middleware
this._middleware = [];
// Number of logs logged
this._countLogged = 0;
// Number of logs successfully synced with Logtail
this._countSynced = 0;
// Number of logs that failed to be synced to Logtail
this._countDropped = 0;
// First, check we have a valid source token
if (typeof sourceToken !== "string" || sourceToken === "") {
throw new Error("Logtail source token missing");
}
// Store the source token, to use for syncing with Better Stack
this._sourceToken = sourceToken;
// Merge default and user options
this._options = Object.assign(Object.assign({}, defaultOptions), options);
// Create a throttler, for sync operations
const throttle = (0, tools_1.makeThrottle)(this._options.syncMax);
// Sync after throttling
const throttler = throttle((logs) => {
return this._sync(logs);
});
// Burst protection for logging
this._logBurstProtection = (0, tools_1.makeBurstProtection)(this._options.burstProtectionMilliseconds, this._options.burstProtectionMax, "Logging");
this.log = this._logBurstProtection(this.log.bind(this));
// Create a batcher, for aggregating logs by buffer size/interval
const batcher = (0, tools_1.makeBatch)(this._options.batchSize, this._options.batchInterval, this._options.retryCount, this._options.retryBackoff, this._options.batchSizeKiB * 1024, this._options.calculateLogSizeBytes);
this._batch = batcher.initPusher((logs) => {
return throttler(logs);
});
this._flush = batcher.flush;
}
/* PUBLIC METHODS */
/**
* Flush batched logs to Logtail
*/
async flush() {
return this._flush();
}
/**
* Number of entries logged
*
* @returns number
*/
get logged() {
return this._countLogged;
}
/**
* Number of log entries synced with Better Stack
*
* @returns number
*/
get synced() {
return this._countSynced;
}
/**
* Number of entries dropped
*
* @returns number
*/
get dropped() {
return this._countDropped;
}
/**
* Log an entry, to be synced with Better Stack
*
* @param message: string - Log message
* @param level (LogLevel) - Level to log at (debug|info|warn|error)
* @param context: (Context) - Context (optional)
* @returns Promise<ILogtailLog> after syncing
*/
async log(message, level = types_1.LogLevel.Info, context = {}) {
// Wrap context in an object, if it's not already
if (typeof context !== "object") {
const wrappedContext = { extra: context };
context = wrappedContext;
}
if (context instanceof Error) {
const wrappedContext = { error: context };
context = wrappedContext;
}
if (this._options.sendLogsToConsoleOutput) {
switch (level) {
case "debug":
console.debug(message, context);
break;
case "info":
console.info(message, context);
break;
case "warn":
console.warn(message, context);
break;
case "error":
console.error(message, context);
break;
default:
console.log(`[${level.toUpperCase()}]`, message, context);
break;
}
}
// Check that we have a sync function
if (typeof this._sync !== "function") {
throw new Error("No Logtail logger sync function provided");
}
// Increment log count
this._countLogged++;
// Start building the log message
let log = Object.assign(Object.assign({
// Implicit date timestamp
dt: new Date(),
// Explicit level
level }, context), (message instanceof Error ? (0, serialize_error_1.serializeError)(message) : { message }));
let transformedLog = log;
for (const middleware of this._middleware) {
let newTransformedLog = await middleware(transformedLog);
if (newTransformedLog == null) {
// Don't push the log if it was filtered out in a middleware
return transformedLog;
}
transformedLog = newTransformedLog;
}
// Manually serialize the log data
transformedLog = this.serialize(transformedLog, this._options.contextObjectMaxDepth);
if (!this._options.sendLogsToBetterStack) {
// Return the resulting log before sending it
return transformedLog;
}
try {
// Push the log through the batcher, and sync
await this._batch(transformedLog);
// Increment sync count
this._countSynced++;
}
catch (e) {
// Increment dropped count
this._countDropped++;
// Catch any errors - re-throw if `ignoreExceptions` == false
if (!this._options.ignoreExceptions) {
if (this._options.throwExceptions) {
throw e;
}
else {
// Output to console
console.error(e);
}
}
}
// Return the resulting log
return transformedLog;
}
serialize(value, maxDepth, visitedObjects = new WeakSet()) {
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
return value;
}
else if (value instanceof Date) {
// Date instances can be invalid & toISOString() will fail
if (isNaN(value.getTime())) {
return value.toString();
}
return value.toISOString();
}
else if (value instanceof Error) {
return (0, serialize_error_1.serializeError)(value);
}
else if ((typeof value === "object" || Array.isArray(value)) && (maxDepth < 1 || visitedObjects.has(value))) {
if (visitedObjects.has(value)) {
if (this._options.contextObjectCircularRefWarn) {
console.warn(`[Logtail] Found a circular reference when serializing logs. Please do not use circular references in your logs.`);
}
return "<omitted circular reference>";
}
if (this._options.contextObjectMaxDepthWarn) {
console.warn(`[Logtail] Max depth of ${this._options.contextObjectMaxDepth} reached when serializing logs. Please do not use excessive object depth in your logs.`);
}
return `<omitted context beyond configured max depth: ${this._options.contextObjectMaxDepth}>`;
}
else if (Array.isArray(value)) {
visitedObjects.add(value);
const serializedArray = value.map((item) => this.serialize(item, maxDepth - 1, visitedObjects));
visitedObjects.delete(value);
return serializedArray;
}
else if (typeof value === "object") {
const serializedObject = {};
visitedObjects.add(value);
Object.entries(value).forEach((item) => {
const key = item[0];
const value = item[1];
const serializedValue = this.serialize(value, maxDepth - 1, visitedObjects);
if (serializedValue !== undefined) {
serializedObject[key] = serializedValue;
}
});
visitedObjects.delete(value);
return serializedObject;
}
else if (typeof value === "undefined") {
return undefined;
}
else {
return `<omitted unserializable ${typeof value}>`;
}
}
/**
*
* Debug level log, to be synced with Better Stack
*
* @param message: string - Log message
* @param context: (Pick<ILogtailLog, "context">) - Context (optional)
* @returns Promise<ILogtailLog> after syncing
*/
async debug(message, context = {}) {
return this.log(message, types_1.LogLevel.Debug, context);
}
/**
*
* Info level log, to be synced with Better Stack
*
* @param message: string - Log message
* @param context: (Pick<ILogtailLog, "context">) - Context (optional)
* @returns Promise<ILogtailLog> after syncing
*/
async info(message, context = {}) {
return this.log(message, types_1.LogLevel.Info, context);
}
/**
*
* Warning level log, to be synced with Better Stack
*
* @param message: string - Log message
* @param context: (Pick<ILogtailLog, "context">) - Context (optional)
* @returns Promise<ILogtailLog> after syncing
*/
async warn(message, context = {}) {
return this.log(message, types_1.LogLevel.Warn, context);
}
/**
*
* Warning level log, to be synced with Better Stack
*
* @param message: string - Log message
* @param context: (Pick<ILogtailLog, "context">) - Context (optional)
* @returns Promise<ILogtailLog> after syncing
*/
async error(message, context = {}) {
return this.log(message, types_1.LogLevel.Error, context);
}
/**
* Sets the sync method - i.e. the final step in the pipeline to get logs
* over to Better Stack
*
* @param fn - Pipeline function to use as sync method
*/
setSync(fn) {
this._sync = fn;
}
/**
* Add a middleware function to the logging pipeline
*
* @param fn - Function to add to the log pipeline
* @returns void
*/
use(fn) {
this._middleware.push(fn);
}
/**
* Remove a function from the pipeline
*
* @param fn - Pipeline function
* @returns void
*/
remove(fn) {
this._middleware = this._middleware.filter((p) => p !== fn);
}
}
class default_1 extends Logtail {
async log(message, level = types_1.LogLevel.Info, context = {}) {
return super.log(message, level, context);
}
}
exports.default = default_1;
//# sourceMappingURL=base.js.map