Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/backend/logger.ts
Views: 687
/*1Debug logger for any node.js server.23There is used both by the hub(s) and project(s).45This is an implementation of basically how winston works for us,6but using the vastly simpler super-popular debug module.7*/89// setting env var must come *BEFORE* debug is loaded the first time10process.env.DEBUG_HIDE_DATE = "yes"; // since we supply it ourselves11// otherwise, maybe stuff like this works: (debug as any).inspectOpts["hideDate"] = true;1213import debug, { Debugger } from "debug";14import { mkdirSync, createWriteStream, statSync, ftruncate } from "fs";15import { format } from "util";16import { dirname, join } from "path";17import { logs } from "./data";1819const MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024; // 20MB2021const COCALC = debug("cocalc");2223let _trimLogFileSizePath = "";24export function trimLogFileSize() {25// THIS JUST DOESN'T REALLY WORK!26return;2728if (!_trimLogFileSizePath) return;29let stats;30try {31stats = statSync(_trimLogFileSizePath);32} catch(_) {33// this happens if the file doesn't exist, which is fine since "trimming" it would be a no-op34return;35}36if (stats.size > MAX_FILE_SIZE_BYTES) {37const fileStream = createWriteStream(_trimLogFileSizePath, { flags: "r+" });38fileStream.on("open", (fd) => {39ftruncate(fd, MAX_FILE_SIZE_BYTES, (truncateErr) => {40if (truncateErr) {41console.error(truncateErr);42return;43}44fileStream.close();45});46});47}48}4950function myFormat(...args): string {51if (args.length > 1 && typeof args[0] == "string" && !args[0].includes("%")) {52// This is something where we didn't use printf formatting.53const v: string[] = [];54for (const x of args) {55try {56v.push(typeof x == "object" ? JSON.stringify(x) : `${x}`);57} catch (_) {58// better to not crash everything just for logging59v.push(`${x}`);60}61}62return v.join(" ");63}64// use printf formatting.65return format(...args);66}6768function defaultTransports(): { console?: boolean; file?: string } {69if (process.env.SMC_TEST) {70return {};71} else if (process.env.COCALC_DOCKER) {72return { file: "/var/log/hub/log" };73} else if (process.env.NODE_ENV == "production") {74return { console: true };75} else {76return { file: join(logs, "log") };77}78}7980function initTransports() {81if (!process.env.DEBUG) {82// console.log("DEBUG is not set, so not setting up debug logging transport");83return;84}85const transports = defaultTransports();86if (process.env.DEBUG_CONSOLE) {87transports.console =88process.env.DEBUG_CONSOLE != "no" && process.env.DEBUG_CONSOLE != "false";89}90if (process.env.DEBUG_FILE != null) {91transports.file = process.env.DEBUG_FILE;92}93let fileStream;94if (transports.file) {95const { file } = transports;96// ensure directory exists97mkdirSync(dirname(file), { recursive: true });98// create the file stream; using a stream ensures99// that everything is written in the right order with100// no corruption/collision between different logging.101// We use append mode because we mainly watch the file log102// when doing dev, and nextjs constantly restarts the process.103fileStream = createWriteStream(file, {104flags: "a",105});106_trimLogFileSizePath = file;107trimLogFileSize();108}109let firstLog: boolean = true;110COCALC.log = (...args) => {111if (!transports.file && !transports.console) return;112if (firstLog && transports.file) {113const announce = `***\n\nLogging to "${transports.file}"${114transports.console ? " and console.log" : ""115} via the debug module\nwith DEBUG='${116process.env.DEBUG117}'.\nUse DEBUG_FILE='path' and DEBUG_CONSOLE=[yes|no] to override.\nUsing DEBUG='cocalc:*,-cocalc:silly:*' to control log levels.\n\n***`;118console.log(announce);119if (transports.file) {120// the file transport121fileStream.write(announce);122}123firstLog = false;124}125// Similar as in debug source code, except I stuck a timestamp126// at the beginning, which I like... except also aware of127// non-printf formatting.128const line = `${new Date().toISOString()}: ${myFormat(...args)}\n`;129130if (transports.console) {131// the console transport:132console.log(line);133}134if (transports.file) {135// the file transport136fileStream.write(line);137}138};139}140141initTransports();142143const DEBUGGERS = {144error: COCALC.extend("error"),145warn: COCALC.extend("warn"),146info: COCALC.extend("info"),147http: COCALC.extend("http"),148verbose: COCALC.extend("verbose"),149debug: COCALC.extend("debug"),150silly: COCALC.extend("silly"),151};152153type Level = keyof typeof DEBUGGERS;154155const LEVELS: Level[] = [156"error",157"warn",158"info",159"http",160"verbose",161"debug",162"silly",163];164165class Logger {166private name: string;167private debuggers: { [level: string]: Debugger } = {};168169constructor(name: string) {170this.name = name;171for (const level of LEVELS) {172this.debuggers[level] = DEBUGGERS[level].extend(name);173this[level] = (...args) => {174this.counter(level);175// @ts-ignore176this.debuggers[level](...args);177};178}179}180181public isEnabled(level: Level): boolean {182return this.debuggers[level].enabled;183}184185public extend(name: string) {186return new Logger(`${this.name}:${name}`);187}188189private counter(level: Level): void {190if (counter == null) return;191counter.labels(this.name, level).inc(1);192}193}194195export interface WinstonLogger {196error: Function;197warn: Function;198info: Function;199http: Function;200verbose: Function;201debug: Function;202silly: Function;203extend: (name: string) => WinstonLogger;204isEnabled: (level: Level) => boolean;205}206207const cache: { [name: string]: WinstonLogger } = {};208export default function getLogger(name: string): WinstonLogger {209if (cache[name] != null) {210return cache[name];211}212// smash it over since we build Logger pretty generically so typescript213// doesn't get it. But we care that all *client* code uses the WinstonLogger214// interface.215return (cache[name] = new Logger(name) as unknown as WinstonLogger);216}217218export { getLogger };219220let counter: any = undefined;221export function setCounter(f) {222counter = f;223}224225226