Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/backend/logger.ts
5796 views
1
/*
2
Debug logger for any node.js server.
3
4
There is used both by the hub(s) and project(s).
5
6
This is an implementation of basically how winston works for us,
7
but using the vastly simpler super-popular debug module.
8
*/
9
10
// setting env var must come *BEFORE* debug is loaded the first time
11
process.env.DEBUG_HIDE_DATE = "yes"; // since we supply it ourselves
12
// otherwise, maybe stuff like this works: (debug as any).inspectOpts["hideDate"] = true;
13
14
import debug, { Debugger } from "debug";
15
import { mkdirSync, createWriteStream, statSync, ftruncate } from "fs";
16
import { format, inspect } from "util";
17
import { dirname, join } from "path";
18
import { logs } from "./data";
19
20
const MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024; // 20MB
21
22
const COCALC = debug("cocalc");
23
24
let _trimLogFileSizePath = "";
25
export function trimLogFileSize() {
26
// THIS JUST DOESN'T REALLY WORK!
27
return;
28
29
if (!_trimLogFileSizePath) return;
30
let stats;
31
try {
32
stats = statSync(_trimLogFileSizePath);
33
} catch (_) {
34
// this happens if the file doesn't exist, which is fine since "trimming" it would be a no-op
35
return;
36
}
37
if (stats.size > MAX_FILE_SIZE_BYTES) {
38
const fileStream = createWriteStream(_trimLogFileSizePath, { flags: "r+" });
39
fileStream.on("open", (fd) => {
40
ftruncate(fd, MAX_FILE_SIZE_BYTES, (truncateErr) => {
41
if (truncateErr) {
42
console.error(truncateErr);
43
return;
44
}
45
fileStream.close();
46
});
47
});
48
}
49
}
50
51
function myFormat(...args): string {
52
if (args.length > 1 && typeof args[0] == "string" && !args[0].includes("%")) {
53
const v: string[] = [];
54
for (const x of args) {
55
try {
56
// Use util.inspect for better object representation
57
v.push(
58
typeof x == "object"
59
? inspect(x, { depth: 4, breakLength: 120 })
60
: `${x}`,
61
);
62
} catch (_) {
63
v.push(`${x}`);
64
}
65
}
66
return v.join(" ");
67
}
68
return format(...args);
69
}
70
71
function defaultTransports(): { console?: boolean; file?: string } {
72
if (process.env.SMC_TEST) {
73
return {};
74
} else if (process.env.COCALC_DOCKER) {
75
return { file: "/var/log/hub/log" };
76
} else if (process.env.NODE_ENV == "production") {
77
return { console: true };
78
} else {
79
return { file: join(logs, "log") };
80
}
81
}
82
83
function initTransports() {
84
if (!process.env.DEBUG) {
85
// console.log("DEBUG is not set, so not setting up debug logging transport");
86
return;
87
}
88
const transports = defaultTransports();
89
if (process.env.DEBUG_CONSOLE) {
90
transports.console =
91
process.env.DEBUG_CONSOLE != "no" && process.env.DEBUG_CONSOLE != "false";
92
}
93
if (process.env.DEBUG_FILE != null) {
94
transports.file = process.env.DEBUG_FILE;
95
}
96
let fileStream;
97
if (transports.file) {
98
const { file } = transports;
99
// ensure directory exists
100
mkdirSync(dirname(file), { recursive: true });
101
// create the file stream; using a stream ensures
102
// that everything is written in the right order with
103
// no corruption/collision between different logging.
104
// We use append mode because we mainly watch the file log
105
// when doing dev, and nextjs constantly restarts the process.
106
fileStream = createWriteStream(file, {
107
flags: "a",
108
});
109
_trimLogFileSizePath = file;
110
trimLogFileSize();
111
}
112
let firstLog: boolean = true;
113
COCALC.log = (...args) => {
114
if (!transports.file && !transports.console) return;
115
if (firstLog && transports.file) {
116
const announce = `***\n\nLogging to "${transports.file}"${
117
transports.console ? " and console.log" : ""
118
} via the debug module\nwith DEBUG='${
119
process.env.DEBUG
120
}'.\nUse DEBUG_FILE='path' and DEBUG_CONSOLE=[yes|no] to override.\nUsing e.g., something like DEBUG='cocalc:*,-cocalc:silly:*' to control log levels.\n\n***`;
121
console.log(announce);
122
if (transports.file) {
123
// the file transport
124
fileStream.write(announce);
125
}
126
firstLog = false;
127
}
128
// Similar as in debug source code, except I stuck a timestamp
129
// at the beginning, which I like... except also aware of
130
// non-printf formatting.
131
const line = `${new Date().toISOString()} (${process.pid}):${myFormat(...args)}\n`;
132
133
if (transports.console) {
134
// the console transport:
135
console.log(line);
136
}
137
if (transports.file) {
138
// the file transport
139
fileStream.write(line);
140
}
141
};
142
}
143
144
initTransports();
145
146
const DEBUGGERS = {
147
error: COCALC.extend("error"),
148
warn: COCALC.extend("warn"),
149
info: COCALC.extend("info"),
150
http: COCALC.extend("http"),
151
verbose: COCALC.extend("verbose"),
152
debug: COCALC.extend("debug"),
153
silly: COCALC.extend("silly"),
154
};
155
156
type Level = keyof typeof DEBUGGERS;
157
158
const LEVELS: Level[] = [
159
"error",
160
"warn",
161
"info",
162
"http",
163
"verbose",
164
"debug",
165
"silly",
166
];
167
168
class Logger {
169
private name: string;
170
private debuggers: { [level: string]: Debugger } = {};
171
172
constructor(name: string) {
173
this.name = name;
174
for (const level of LEVELS) {
175
this.debuggers[level] = DEBUGGERS[level].extend(name);
176
this[level] = (...args) => {
177
this.counter(level);
178
// @ts-ignore
179
this.debuggers[level](...args);
180
};
181
}
182
}
183
184
public isEnabled(level: Level): boolean {
185
return this.debuggers[level].enabled;
186
}
187
188
public extend(name: string) {
189
return new Logger(`${this.name}:${name}`);
190
}
191
192
private counter(level: Level): void {
193
if (counter == null) return;
194
counter.labels(this.name, level).inc(1);
195
}
196
}
197
198
export interface WinstonLogger {
199
error: Function;
200
warn: Function;
201
info: Function;
202
http: Function;
203
verbose: Function;
204
debug: Function;
205
silly: Function;
206
extend: (name: string) => WinstonLogger;
207
isEnabled: (level: Level) => boolean;
208
}
209
210
const cache: { [name: string]: WinstonLogger } = {};
211
export default function getLogger(name: string): WinstonLogger {
212
if (cache[name] != null) {
213
return cache[name];
214
}
215
// smash it over since we build Logger pretty generically so typescript
216
// doesn't get it. But we care that all *client* code uses the WinstonLogger
217
// interface.
218
return (cache[name] = new Logger(name) as unknown as WinstonLogger);
219
}
220
221
export { getLogger };
222
223
let counter: any = undefined;
224
export function setCounter(f) {
225
counter = f;
226
}
227
228