Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/commons/Logger.ts
1028 views
1
// eslint-disable-next-line max-classes-per-file
2
import ILog, { ILogData } from '@secret-agent/interfaces/ILog';
3
import { inspect } from 'util';
4
5
const hasBeenLoggedSymbol = Symbol.for('hasBeenLogged');
6
7
let logId = 0;
8
class Log implements ILog {
9
public readonly level: string = process.env.DEBUG ? 'stats' : 'error';
10
private readonly module: string;
11
private readonly logLevel: number;
12
private readonly boundContext: any = {};
13
14
constructor(module: NodeModule, boundContext?: any) {
15
this.logLevel = logLevels.indexOf(this.level);
16
this.module = module ? extractPathFromModule(module) : '';
17
if (boundContext) this.boundContext = boundContext;
18
}
19
20
public stats(action: string, data?: ILogData): number {
21
return this.log('stats', action, data);
22
}
23
24
public info(action: string, data?: ILogData): number {
25
return this.log('info', action, data);
26
}
27
28
public warn(action: string, data?: ILogData): number {
29
return this.log('warn', action, data);
30
}
31
32
public error(action: string, data?: ILogData): number {
33
return this.log('error', action, data);
34
}
35
36
public createChild(module, boundContext?: any): ILog {
37
return new Log(module, {
38
...this.boundContext,
39
...boundContext,
40
});
41
}
42
43
public flush(): void {
44
// no-op
45
}
46
47
private log(level: LogLevel, action: string, data?: ILogData): number {
48
let logData: object;
49
let sessionId: string = this.boundContext.sessionId;
50
let parentId: number;
51
const mergedData = { ...data, context: this.boundContext };
52
if (mergedData) {
53
for (const [key, val] of Object.entries(mergedData)) {
54
if (key === 'parentLogId') parentId = val as number;
55
else if (key === 'sessionId') sessionId = val as string;
56
else {
57
if (!logData) logData = {};
58
logData[key] = val;
59
}
60
}
61
}
62
logId += 1;
63
const id = logId;
64
const entry: ILogEntry = {
65
id,
66
sessionId,
67
parentId,
68
timestamp: new Date(),
69
action,
70
data: logData,
71
level,
72
module: this.module,
73
};
74
const printToConsole = logLevels.indexOf(level) >= this.logLevel;
75
if (printToConsole) {
76
const printablePath = entry.module
77
.replace('.js', '')
78
.replace('.ts', '')
79
.replace('build/', '');
80
81
const { error, printData } = translateToPrintable(entry.data);
82
83
if (level === 'warn' || level === 'error') {
84
printData.sessionId = sessionId;
85
printData.sessionName = loggerSessionIdNames.get(sessionId) ?? undefined;
86
}
87
88
const params = Object.keys(printData).length ? [printData] : [];
89
if (error) params.push(error);
90
const useColors =
91
process.env.NODE_DISABLE_COLORS !== 'true' && process.env.NODE_DISABLE_COLORS !== '1';
92
// eslint-disable-next-line no-console
93
console.log(
94
`${entry.timestamp.toISOString()} ${entry.level.toUpperCase()} [${printablePath}] ${
95
entry.action
96
}`,
97
...params.map(x => inspect(x, false, null, useColors)),
98
);
99
}
100
LogEvents.broadcast(entry);
101
return id;
102
}
103
}
104
105
function translateValueToPrintable(value: any, depth = 0): any {
106
if (value === undefined || value === null) return;
107
if (value instanceof Error) {
108
return value.toString();
109
}
110
if ((value as any).toJSON) {
111
return (value as any).toJSON();
112
}
113
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
114
return value;
115
}
116
117
if (depth > 2) return value;
118
119
if (typeof value === 'object') {
120
if (Array.isArray(value)) {
121
return value.map(x => translateValueToPrintable(x, depth + 1));
122
}
123
const result: any = {};
124
for (const [key, subValue] of Object.entries(value)) {
125
result[key] = translateValueToPrintable(subValue, depth + 1);
126
}
127
return result;
128
}
129
}
130
131
function translateToPrintable(
132
data: any,
133
result?: { error?: Error; printData: any },
134
): { error?: Error; printData: any } {
135
result ??= { printData: {} };
136
const { printData } = result;
137
for (const [key, value] of Object.entries(data)) {
138
if (value instanceof Error) {
139
Object.defineProperty(value, hasBeenLoggedSymbol, {
140
enumerable: false,
141
value: true,
142
});
143
result.error = value;
144
continue;
145
}
146
const printable = translateValueToPrintable(value);
147
if (!printable) continue;
148
printData[key] = printable;
149
}
150
return result;
151
}
152
153
const logLevels = ['stats', 'info', 'warn', 'error'];
154
155
let logCreator = (module: NodeModule): { log: ILog } => {
156
const log: ILog = new Log(module);
157
158
return {
159
log,
160
};
161
};
162
163
export default function logger(module: NodeModule): ILogBuilder {
164
return logCreator(module);
165
}
166
167
let idCounter = 0;
168
169
const loggerSessionIdNames = new Map<string, string>();
170
171
class LogEvents {
172
private static subscriptions: { [id: number]: (log: ILogEntry) => any } = {};
173
174
public static unsubscribe(subscriptionId: number): void {
175
delete LogEvents.subscriptions[subscriptionId];
176
}
177
178
public static subscribe(onLogFn: (log: ILogEntry) => any): number {
179
idCounter += 1;
180
const id = idCounter;
181
LogEvents.subscriptions[id] = onLogFn;
182
return id;
183
}
184
185
public static broadcast(entry: ILogEntry): void {
186
Object.values(LogEvents.subscriptions).forEach(x => x(entry));
187
}
188
}
189
190
export { Log, LogEvents, loggerSessionIdNames, hasBeenLoggedSymbol };
191
192
export function injectLogger(builder: (module: NodeModule) => ILogBuilder): void {
193
logCreator = builder;
194
}
195
196
export interface ILogEntry {
197
id: number;
198
timestamp: Date;
199
action: string;
200
module: string;
201
sessionId?: string;
202
parentId?: number;
203
data?: any;
204
level: LogLevel;
205
}
206
207
type LogLevel = 'stats' | 'info' | 'warn' | 'error';
208
209
interface ILogBuilder {
210
log: ILog;
211
}
212
213
function extractPathFromModule(module: NodeModule): string {
214
const fullPath = typeof module === 'string' ? module : module.filename || module.id || '';
215
return fullPath.replace(/^(.*)\/secret-agent\/(.*)$/, '$2');
216
}
217
218