strogger
Version:
📊 A modern structured logging library with functional programming, duck-typing, and comprehensive third-party integrations
201 lines • 8.41 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createElasticsearchTransport = void 0;
const types_1 = require("../types");
const errors_1 = require("../utils/errors");
const base_transport_1 = require("./base-transport");
const createElasticsearchTransport = (options = {}) => {
const transportName = "Elasticsearch";
try {
let minLevel = options.level ?? types_1.LogLevel.INFO;
// Note: formatter is not used in this transport as we format logs directly for Elasticsearch
const url = options.url || process.env.ELASTICSEARCH_URL || "http://localhost:9200";
const username = options.username || process.env.ELASTICSEARCH_USERNAME;
const password = options.password || process.env.ELASTICSEARCH_PASSWORD;
const apiKey = options.apiKey || process.env.ELASTICSEARCH_API_KEY;
const index = options.index || process.env.ELASTICSEARCH_INDEX || "logs";
const indexPattern = options.indexPattern ||
process.env.ELASTICSEARCH_INDEX_PATTERN ||
"logs-{YYYY.MM.DD}";
const batchSize = options.batchSize || 10;
const flushInterval = options.flushInterval || 5000;
const timeout = options.timeout || 30000;
// Validate required configuration
if (!apiKey && (!username || !password)) {
throw (0, errors_1.createDetailedError)("ELASTICSEARCH_MISSING_AUTH", transportName, {
message: "Elasticsearch transport requires either API key or username/password",
});
}
let batch = [];
let flushTimer = null;
const getIndexName = () => {
if (indexPattern?.includes("{YYYY.MM.DD}")) {
const date = new Date();
const dateStr = date.toISOString().split("T")[0]?.replace(/-/g, ".");
if (!dateStr) {
throw (0, errors_1.createDetailedError)("ELASTICSEARCH_MISSING_DATE", transportName, {
message: "Date string is missing",
});
}
return indexPattern.replace("{YYYY.MM.DD}", dateStr);
}
return typeof index === "string" && index.length > 0 ? index : "logs";
};
const getAuthHeaders = () => {
if (apiKey) {
return {
Authorization: `ApiKey ${apiKey}`,
};
}
if (username && password) {
const auth = Buffer.from(`${username}:${password}`).toString("base64");
return {
Authorization: `Basic ${auth}`,
};
}
return {};
};
const sendToElasticsearch = async (entries) => {
try {
const indexName = getIndexName();
const bulkUrl = `${url}/_bulk`;
// Create bulk payload
const bulkPayload = `${entries
.map((entry) => {
const doc = {
message: entry.message,
level: getElasticsearchLevel(entry.level),
timestamp: new Date(entry.timestamp).toISOString(),
service: process.env.SERVICE_NAME || "unknown",
...entry.context,
...(entry.error && {
error_name: entry.error.name,
error_message: entry.error.message,
error_stack: entry.error.stack,
}),
...(entry.metadata && { metadata: entry.metadata }),
};
// Bulk format: action + document
return `${JSON.stringify({ index: { _index: indexName } })}\n${JSON.stringify(doc)}`;
})
.join("\n")}\n`;
const response = await fetch(bulkUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-ndjson",
...getAuthHeaders(),
},
body: bulkPayload,
signal: AbortSignal.timeout(timeout),
});
if (!response.ok) {
const responseText = await response
.text()
.catch(() => "Unable to read response body");
throw (0, errors_1.createDetailedError)("ELASTICSEARCH_API_ERROR", transportName, {
status: response.status,
statusText: response.statusText,
url: bulkUrl,
responseBody: responseText,
});
}
// Check for individual document errors in bulk response
const responseData = await response.json().catch(() => null);
if (responseData?.errors && Array.isArray(responseData.items)) {
const errors = responseData.items
.filter((item) => {
const index = item.index;
return index?.error;
})
.map((item) => {
const index = item.index;
return index?.error;
})
.slice(0, 3); // Show first 3 errors
if (errors?.length > 0) {
console.warn("[ELASTICSEARCH] Some documents failed to index:", errors);
}
}
}
catch (error) {
(0, errors_1.handleTransportError)(error, transportName, true);
}
};
const getElasticsearchLevel = (level) => {
switch (level) {
case types_1.LogLevel.DEBUG:
return "debug";
case types_1.LogLevel.INFO:
return "info";
case types_1.LogLevel.WARN:
return "warn";
case types_1.LogLevel.ERROR:
return "error";
case types_1.LogLevel.FATAL:
return "fatal";
default:
return "info";
}
};
const flush = async () => {
if (batch.length === 0)
return;
const entriesToSend = [...batch];
batch = [];
await sendToElasticsearch(entriesToSend);
};
const startFlushTimer = () => {
if (flushTimer)
return;
flushTimer = setInterval(() => {
flush().catch((error) => {
(0, errors_1.handleTransportError)(error, transportName, true);
});
}, flushInterval);
};
// Start the flush timer
startFlushTimer();
return {
log: async (entry) => {
if (!(0, base_transport_1.shouldLog)(entry.level, minLevel))
return;
batch.push(entry);
if (batch.length >= batchSize) {
await flush();
}
},
setLevel: (level) => {
minLevel = level;
},
getLevel: () => minLevel,
// Elasticsearch specific methods
flush: async () => {
await flush();
},
close: async () => {
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
await flush();
},
// Get current configuration
getConfig: () => ({
url,
index,
indexPattern,
batchSize,
flushInterval,
timeout,
}),
// Get current index name
getCurrentIndex: () => getIndexName(),
};
}
catch (error) {
(0, errors_1.handleTransportError)(error, transportName, false);
throw error; // Re-throw for proper error handling
}
};
exports.createElasticsearchTransport = createElasticsearchTransport;
//# sourceMappingURL=elasticsearch-transport.js.map