/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see .
*/
import * as fs from "fs";
import * as os from "os";
import * as config from "./config";
import { getRequestOrigin } from "./forwarded";
import {
SessionContext,
AcsRequest,
CpeRequest,
CpeFault,
InformRequest,
Fault,
} from "./types";
const REOPEN_EVERY = 60000;
const LOG_FORMAT = config.get("LOG_FORMAT");
const ACCESS_LOG_FORMAT = config.get("ACCESS_LOG_FORMAT") || LOG_FORMAT;
const defaultMeta: { [name: string]: any } = {};
let LOG_SYSTEMD = false;
let ACCESS_LOG_SYSTEMD = false;
let LOG_FILE, ACCESS_LOG_FILE;
declare global {
/* eslint-disable-next-line @typescript-eslint/no-namespace */
namespace NodeJS {
export interface WritableStream {
fd?: number;
}
}
}
declare module "fs" {
interface WriteStream {
fd?: number;
}
}
let logStream = fs.createWriteStream(null, { fd: process.stderr.fd });
let logStat = fs.fstatSync(logStream.fd);
let accessLogStream = fs.createWriteStream(null, { fd: process.stdout.fd });
let accessLogStat = fs.fstatSync(accessLogStream.fd);
// Reopen if original files have been moved (e.g. logrotate)
function reopen(): void {
let counter = 1;
if (LOG_FILE) {
++counter;
fs.stat(LOG_FILE, (err, stat) => {
if (err && !err.message.startsWith("ENOENT:")) throw err;
if (!(stat && stat.dev === logStat.dev && stat.ino === logStat.ino)) {
logStream.end();
logStream = fs.createWriteStream(null, {
fd: fs.openSync(LOG_FILE, "a"),
});
logStat = fs.fstatSync(logStream.fd);
}
if (--counter === 0)
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
});
}
if (ACCESS_LOG_FILE) {
++counter;
fs.stat(ACCESS_LOG_FILE, (err, stat) => {
if (err && !err.message.startsWith("ENOENT:")) throw err;
if (
!(
stat &&
stat.dev === accessLogStat.dev &&
stat.ino === accessLogStat.ino
)
) {
accessLogStream.end();
accessLogStream = fs.createWriteStream(null, {
fd: fs.openSync(ACCESS_LOG_FILE, "a"),
});
accessLogStat = fs.fstatSync(accessLogStream.fd);
}
if (--counter === 0)
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
});
}
if (--counter === 0)
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
}
export function init(service: string, version: string): void {
defaultMeta.hostname = os.hostname();
defaultMeta.pid = process.pid;
defaultMeta.name = `genieacs-${service}`;
defaultMeta.version = version;
LOG_FILE = config.get(`${service.toUpperCase()}_LOG_FILE`);
ACCESS_LOG_FILE = config.get(`${service.toUpperCase()}_ACCESS_LOG_FILE`);
if (LOG_FILE) {
logStream = fs.createWriteStream(null, { fd: fs.openSync(LOG_FILE, "a") });
logStat = fs.fstatSync(logStream.fd);
}
if (ACCESS_LOG_FILE) {
accessLogStream = fs.createWriteStream(null, {
fd: fs.openSync(ACCESS_LOG_FILE, "a"),
});
accessLogStat = fs.fstatSync(accessLogStream.fd);
}
// Determine if logs are going to journald
const JOURNAL_STREAM = process.env["JOURNAL_STREAM"];
if (JOURNAL_STREAM) {
const [dev, inode] = JOURNAL_STREAM.split(":").map(parseInt);
LOG_SYSTEMD = logStat.dev === dev && logStat.ino === inode;
ACCESS_LOG_SYSTEMD =
accessLogStat.dev === dev && accessLogStat.ino === inode;
}
if (LOG_FILE || ACCESS_LOG_FILE)
// Can't use setInterval as we need all workers to cehck at the same time
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
}
export function close(): void {
accessLogStream.end();
logStream.end();
}
export function flatten(
details: Record,
): Record {
if (details.sessionContext) {
const sessionContext = details.sessionContext as SessionContext;
details.deviceId = sessionContext.deviceId;
details.remoteAddress = getRequestOrigin(
sessionContext.httpRequest,
).remoteAddress;
delete details.sessionContext;
}
if (details.exception) {
const err = details.exception as Error;
details.exceptionName = err.name;
details.exceptionMessage = err.message;
details.exceptionStack = err.stack;
delete details.exception;
}
if (details.task) {
details.taskId = details.task["_id"];
delete details.task;
}
if (details.rpc) {
const rpc = details.rpc as {
id: string;
acsRequest?: AcsRequest;
cpeRequest?: CpeRequest;
cpeFault?: CpeFault;
};
if (rpc.acsRequest) {
details.acsRequestId = rpc.id;
details.acsRequestName = rpc.acsRequest.name;
if (rpc.acsRequest["commandKey"])
details.acsRequestCommandKey = rpc.acsRequest["commandKey"];
} else if (rpc.cpeRequest) {
details.cpeRequestId = rpc.id;
if (rpc.cpeRequest.name === "Inform") {
details.informEvent = (rpc.cpeRequest as InformRequest).event.join(",");
details.informRetryCount = (rpc.cpeRequest as InformRequest).retryCount;
} else {
details.cpeRequestName = rpc.cpeRequest.name;
if (rpc.cpeRequest["commandKey"])
details.cpeRequestCommandKey = rpc.cpeRequest["commandKey"];
}
} else if (rpc.cpeFault) {
details.acsRequestId = rpc.id;
details.cpeFaultCode = rpc.cpeFault.detail.faultCode;
details.cpeFaultString = rpc.cpeFault.detail.faultString;
}
delete details.rpc;
}
if (details.fault) {
const fault = details.fault as Fault;
details.faultCode = fault.code;
details.faultMessage = fault.message;
delete details.fault;
}
// For genieacs-ui
if (details.context) {
details.remoteAddress = getRequestOrigin(
details.context["req"],
).remoteAddress;
if (details.context["state"].user)
details.user = details.context["state"].user.username;
delete details.context;
}
for (const [k, v] of Object.entries(details))
if (v == null) delete details[k];
return details;
}
function formatJson(
details: Record,
systemd: boolean,
): string {
if (systemd) {
let severity = "";
if (details.severity === "info") severity = "<6>";
else if (details.severity === "warn") severity = "<4>";
else if (details.severity === "error") severity = "<3>";
return `${severity}${JSON.stringify(flatten(details))}${os.EOL}`;
}
return `${JSON.stringify(flatten(details))}${os.EOL}`;
}
function formatSimple(
details: Record,
systemd: boolean,
): string {
const skip = {
user: true,
remoteAddress: true,
severity: true,
timestamp: true,
message: true,
deviceId: !!details.sessionContext,
};
flatten(details);
let remote = "";
if (details.remoteAddress) {
if (details.deviceId && skip["deviceId"])
remote = `${details.remoteAddress} ${details.deviceId}: `;
else if (details.user)
remote = `${details.user}@${details.remoteAddress}: `;
else remote = `${details.remoteAddress}: `;
}
const keys = Object.keys(details);
let meta = "";
const kv = [];
for (const k of keys)
if (!skip[k]) kv.push(`${k}=${JSON.stringify(details[k])}`);
if (kv.length) meta = `; ${kv.join(" ")}`;
if (systemd) {
let severity = "";
if (details.severity === "info") severity = "<6>";
else if (details.severity === "warn") severity = "<4>";
else if (details.severity === "error") severity = "<3>";
return `${severity}${remote}${details.message}${meta}${os.EOL}`;
}
return `${details.timestamp} [${(
details.severity as string
).toUpperCase()}] ${remote}${details.message}${meta}${os.EOL}`;
}
function log(details: Record): void {
details.timestamp = new Date().toISOString();
if (LOG_FORMAT === "json") {
details = Object.assign({}, defaultMeta, details);
logStream.write(formatJson(details, LOG_SYSTEMD));
} else {
logStream.write(formatSimple(details, LOG_SYSTEMD));
}
}
export function info(details: Record): void {
details.severity = "info";
log(details);
}
export function warn(details: Record): void {
details.severity = "warn";
log(details);
}
export function error(details: Record): void {
details.severity = "error";
log(details);
}
export function accessLog(details: Record): void {
details.timestamp = new Date().toISOString();
if (ACCESS_LOG_FORMAT === "json") {
Object.assign(details, defaultMeta);
accessLogStream.write(formatJson(details, ACCESS_LOG_SYSTEMD));
} else {
accessLogStream.write(formatSimple(details, ACCESS_LOG_SYSTEMD));
}
}
export function accessInfo(details: Record): void {
details.severity = "info";
accessLog(details);
}
export function accessWarn(details: Record): void {
details.severity = "warn";
accessLog(details);
}
export function accessError(details: Record): void {
details.severity = "error";
accessLog(details);
}