@dxos/log 0.4.9 → 0.4.10-main.068c3d8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/log",
3
- "version": "0.4.9",
3
+ "version": "0.4.10-main.068c3d8",
4
4
  "description": "Logger",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -24,8 +24,8 @@
24
24
  "lodash.omit": "^4.5.0",
25
25
  "lodash.pick": "^4.4.0",
26
26
  "lodash.pickby": "^4.6.0",
27
- "@dxos/node-std": "0.4.9",
28
- "@dxos/util": "0.4.9"
27
+ "@dxos/node-std": "0.4.10-main.068c3d8",
28
+ "@dxos/util": "0.4.10-main.068c3d8"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@swc-node/sourcemap-support": "^0.2.0",
package/src/context.ts CHANGED
@@ -64,7 +64,7 @@ export const getContextFromEntry = (entry: LogEntry): Record<string, any> | unde
64
64
 
65
65
  if (entry.error) {
66
66
  const errorContext = (entry.error as any).context;
67
- context = Object.assign(context ?? {}, { error: entry.error.stack, ...errorContext });
67
+ context = Object.assign(context ?? {}, { error: entry.error, ...errorContext });
68
68
  }
69
69
 
70
70
  return context && Object.keys(context).length > 0 ? context : undefined;
package/src/decorators.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import chalk from 'chalk';
5
6
  import { inspect } from 'node:util';
6
7
 
7
8
  import type { LogMethods } from './log';
@@ -14,8 +15,11 @@ export const createMethodLogDecorator =
14
15
  (arg0?: never, arg1?: never, meta?: CallMetadata): MethodDecorator =>
15
16
  (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
16
17
  const method = descriptor.value!;
18
+ const methodName = propertyKey as string;
17
19
  descriptor.value = function (this: any, ...args: any) {
18
20
  const combinedMeta = {
21
+ F: '',
22
+ L: 0,
19
23
  ...(meta ?? {}),
20
24
  S: this as any,
21
25
  } as CallMetadata;
@@ -28,43 +32,117 @@ export const createMethodLogDecorator =
28
32
 
29
33
  if (isThenable(result)) {
30
34
  const id = nextPromiseId++;
31
- log.info(`${propertyKey as string}(${formattedArgs}) => Promise { #${id} }`, {}, combinedMeta);
35
+ logAsyncBegin(log, methodName, formattedArgs, id, combinedMeta);
32
36
  result.then(
33
37
  (resolvedValue) => {
34
- if (resolvedValue !== undefined) {
35
- log.info(
36
- `✅ resolve #${id} ${(performance.now() - startTime).toFixed(0)}ms => ${inspect(
37
- resolvedValue,
38
- false,
39
- 1,
40
- true,
41
- )}`,
42
- {},
43
- combinedMeta,
44
- );
45
- } else {
46
- log.info(`✅ resolve #${id} ${(performance.now() - startTime).toFixed(0)}ms`, {}, combinedMeta);
47
- }
38
+ logAsyncResolved(log, methodName, resolvedValue, id, startTime, combinedMeta);
48
39
  },
49
40
  (err) => {
50
- log.info(`🔥 reject #${id} #${(performance.now() - startTime).toFixed(0)}ms => ${err}`, {}, combinedMeta);
41
+ logAsyncRejected(log, methodName, err, id, startTime, combinedMeta);
51
42
  },
52
43
  );
53
44
  } else {
54
- log.info(
55
- `${propertyKey as string}(${formattedArgs}) => ${inspect(result, false, 1, true)}`,
56
- {},
57
- combinedMeta,
58
- );
45
+ logSyncCall(log, methodName, formattedArgs, result, combinedMeta);
59
46
  }
60
47
 
61
48
  return result;
62
- } catch (err) {
63
- log.error(`${propertyKey as string}(${formattedArgs}) 🔥 ${err}`, {}, combinedMeta);
49
+ } catch (err: any) {
50
+ logSyncError(log, methodName, formattedArgs, err, combinedMeta);
64
51
  throw err;
65
52
  }
66
53
  };
67
- Object.defineProperty(descriptor.value, 'name', { value: (propertyKey as string) + '$log' });
54
+ Object.defineProperty(descriptor.value, 'name', { value: methodName + '$log' });
68
55
  };
69
56
 
70
57
  const isThenable = (obj: any): obj is Promise<unknown> => obj && typeof obj.then === 'function';
58
+
59
+ const logSyncCall = (
60
+ log: LogMethods,
61
+ methodName: string,
62
+ formattedArgs: string,
63
+ result: unknown,
64
+ combinedMeta: CallMetadata,
65
+ ) => {
66
+ log.info(
67
+ `.${formatFunction(methodName)} (${formattedArgs}) ${chalk.gray('resolve')} ${inspect(result, false, 1, true)}`,
68
+ {},
69
+ combinedMeta,
70
+ );
71
+ };
72
+
73
+ const logSyncError = (
74
+ log: LogMethods,
75
+ methodName: string,
76
+ formattedArgs: string,
77
+ err: Error,
78
+ combinedMeta: CallMetadata,
79
+ ) => {
80
+ log.error(`.${formatFunction(methodName)} (${formattedArgs}) 🔥 ${err}`, {}, combinedMeta);
81
+ };
82
+
83
+ const logAsyncBegin = (
84
+ log: LogMethods,
85
+ methodName: string,
86
+ formattedArgs: string,
87
+ promiseId: number,
88
+ combinedMeta: CallMetadata,
89
+ ) => {
90
+ log.info(
91
+ `.${formatFunction(methodName)} ↴ (${formattedArgs}) ${chalk.gray('=>')} ${formatPromise(promiseId)}`,
92
+ {},
93
+ combinedMeta,
94
+ );
95
+ };
96
+
97
+ const logAsyncResolved = (
98
+ log: LogMethods,
99
+ methodName: string,
100
+ resolvedValue: unknown | undefined,
101
+ promiseId: number,
102
+ startTime: number,
103
+ combinedMeta: CallMetadata,
104
+ ) => {
105
+ if (resolvedValue !== undefined) {
106
+ log.info(
107
+ `.${formatFunction(methodName)} ↲ ${greenCheck} ${chalk.gray('resolve')} ${formatPromise(promiseId)} ${formatTimeElapsed(startTime)} ${chalk.gray('=>')} ${inspect(
108
+ resolvedValue,
109
+ false,
110
+ 1,
111
+ true,
112
+ )}`,
113
+ {},
114
+ combinedMeta,
115
+ );
116
+ } else {
117
+ log.info(
118
+ `.${formatFunction(methodName)} ↲ ${greenCheck} ${chalk.gray('resolve')} ${formatPromise(promiseId)} ${formatTimeElapsed(startTime)}`,
119
+ {},
120
+ combinedMeta,
121
+ );
122
+ }
123
+ };
124
+
125
+ const logAsyncRejected = (
126
+ log: LogMethods,
127
+ methodName: string,
128
+ err: Error,
129
+ promiseId: number,
130
+ startTime: number,
131
+ combinedMeta: CallMetadata,
132
+ ) => {
133
+ log.info(
134
+ `.${formatFunction(methodName)} ↲ 🔥 ${chalk.gray('reject')} ${formatPromise(promiseId)} ${formatTimeElapsed(startTime)} ${chalk.gray('=>')} ${err}`,
135
+ {},
136
+ combinedMeta,
137
+ );
138
+ };
139
+
140
+ const greenCheck = chalk.green('✔');
141
+
142
+ const formatTimeElapsed = (startTime: number) => chalk.gray(`${(performance.now() - startTime).toFixed(0)}ms`);
143
+
144
+ const COLOR_FUNCTION = [220, 220, 170] as const;
145
+
146
+ const formatFunction = (name: string) => chalk.bold(chalk.rgb(...COLOR_FUNCTION)(name));
147
+
148
+ const formatPromise = (id: number) => chalk.blue(`Promise#${id}`);
package/src/log.ts CHANGED
@@ -71,7 +71,7 @@ const createLog = (): LogImp => {
71
71
  log.error = (...params) => processLog(LogLevel.ERROR, ...params);
72
72
 
73
73
  // Catch only shows error message, not stacktrace.
74
- log.catch = (error: Error | any, context, meta) => processLog(LogLevel.ERROR, error.stack, context, meta, error);
74
+ log.catch = (error: Error | any, context, meta) => processLog(LogLevel.ERROR, error.message, context, meta, error);
75
75
 
76
76
  // Show break.
77
77
  log.break = () => log.info('——————————————————————————————————————————————————');
@@ -58,7 +58,7 @@ const APP_BROWSER_PROCESSOR: LogProcessor = (config, entry) => {
58
58
  const filename = getRelativeFilename(entry.meta.F);
59
59
  const filepath = `${LOG_BROWSER_PREFIX.replace(/\/$/, '')}/${filename}`;
60
60
  // TODO(burdon): Line numbers not working for app link, even with colons.
61
- // https://stackoverflow.com/a/54459820/2804332
61
+ // https://stackoverflow.com/a/54459820/2804332
62
62
  link = `${filepath}#L${entry.meta.L}`;
63
63
  }
64
64
 
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export const getRelativeFilename = (filename: string) => {
6
+ // TODO(burdon): Hack uses "packages" as an anchor (pre-parse NX?)
7
+ // Including `packages/` part of the path so that excluded paths (e.g. from dist) are clickable in vscode.
8
+ const match = filename.match(/.+\/(packages\/.+\/.+)/);
9
+ if (match) {
10
+ const [, filePath] = match;
11
+ return filePath;
12
+ }
13
+
14
+ return filename;
15
+ };
@@ -8,6 +8,7 @@ import { inspect } from 'node:util';
8
8
 
9
9
  import { getPrototypeSpecificInstanceId } from '@dxos/util';
10
10
 
11
+ import { getRelativeFilename } from './common';
11
12
  import { type LogConfig, LogLevel, shortLevelName } from '../config';
12
13
  import { getContextFromEntry, type LogProcessor, shouldLog } from '../context';
13
14
 
@@ -24,18 +25,6 @@ export const truncate = (text?: string, length = 0, right = false) => {
24
25
  return right ? str.padStart(length, ' ') : str.padEnd(length, ' ');
25
26
  };
26
27
 
27
- const getRelativeFilename = (filename: string) => {
28
- // TODO(burdon): Hack uses "packages" as an anchor (pre-parse NX?)
29
- // Including `packages/` part of the path so that excluded paths (e.g. from dist) are clickable in vscode.
30
- const match = filename.match(/.+\/(packages\/.+\/.+)/);
31
- if (match) {
32
- const [, filePath] = match;
33
- return filePath;
34
- }
35
-
36
- return filename;
37
- };
38
-
39
28
  // TODO(burdon): Optional package name.
40
29
  // TODO(burdon): Show exceptions on one line.
41
30
  export type FormatParts = {
@@ -53,10 +42,9 @@ export type Formatter = (config: LogConfig, parts: FormatParts) => (string | und
53
42
 
54
43
  export const DEFAULT_FORMATTER: Formatter = (
55
44
  config,
56
- { path, line, timestamp, level, message, context, error, scope },
57
- ) => {
45
+ { path, line, level, message, context, error, scope },
46
+ ): string[] => {
58
47
  const column = config.options?.formatter?.column;
59
-
60
48
  const filepath = path !== undefined && line !== undefined ? chalk.grey(`${path}:${line}`) : undefined;
61
49
 
62
50
  let instance;
@@ -86,11 +74,13 @@ export const DEFAULT_FORMATTER: Formatter = (
86
74
  ];
87
75
  };
88
76
 
89
- export const SHORT_FORMATTER: Formatter = (config, { path, level, message }) => [
90
- chalk.grey(truncate(path, 16, true)), // NOTE: Breaks terminal linking.
91
- chalk[LEVEL_COLORS[level]](shortLevelName[level]),
92
- message,
93
- ];
77
+ export const SHORT_FORMATTER: Formatter = (config, { path, level, message }) => {
78
+ return [
79
+ chalk.grey(truncate(path, 16, true)), // NOTE: Breaks terminal linking.
80
+ chalk[LEVEL_COLORS[level]](shortLevelName[level]),
81
+ message,
82
+ ];
83
+ };
94
84
 
95
85
  // TODO(burdon): Config option.
96
86
  const formatter = DEFAULT_FORMATTER;
@@ -5,35 +5,78 @@
5
5
  import { appendFileSync, mkdirSync, openSync } from 'node:fs';
6
6
  import { dirname } from 'node:path';
7
7
 
8
- import { jsonify } from '@dxos/util';
8
+ import { jsonlogify } from '@dxos/util';
9
9
 
10
- import { LogLevel } from '../config';
11
- import { type LogProcessor, getContextFromEntry } from '../context';
10
+ import { getRelativeFilename } from './common';
11
+ import { type LogFilter, LogLevel } from '../config';
12
+ import { type LogProcessor, getContextFromEntry, shouldLog } from '../context';
12
13
 
13
- export const createFileProcessor = ({ path, levels }: { path: string; levels: LogLevel[] }): LogProcessor => {
14
+ // Amount of time to retry writing after encountering EAGAIN before giving up.
15
+ const EAGAIN_MAX_DURATION = 1000;
16
+ /**
17
+ * Create a file processor.
18
+ * @param path - Path to log file to create or append to, or existing open file descriptor e.g. stdout.
19
+ * @param levels - Log levels to process. Takes preference over Filters.
20
+ * @param filters - Filters to apply.
21
+ */
22
+ export const createFileProcessor = ({
23
+ pathOrFd,
24
+ levels,
25
+ filters,
26
+ }: {
27
+ pathOrFd: string | number;
28
+ levels: LogLevel[];
29
+ filters?: LogFilter[];
30
+ }): LogProcessor => {
14
31
  let fd: number | undefined;
15
32
 
16
33
  return (config, entry) => {
17
- if (!levels.includes(entry.level)) {
34
+ if (levels.length > 0 && !levels.includes(entry.level)) {
18
35
  return;
19
36
  }
20
- if (!fd) {
37
+ if (!shouldLog(entry, filters)) {
38
+ return;
39
+ }
40
+ if (typeof pathOrFd === 'number') {
41
+ fd = pathOrFd;
42
+ } else {
21
43
  try {
22
- mkdirSync(dirname(path));
44
+ mkdirSync(dirname(pathOrFd));
23
45
  } catch {}
24
- fd = openSync(path, 'w');
46
+ fd = openSync(pathOrFd, 'w');
25
47
  }
26
48
 
27
49
  const record = {
28
50
  ...entry,
29
51
  timestamp: Date.now(),
30
- meta: {
31
- file: entry.meta?.F,
32
- line: entry.meta?.L,
33
- },
34
- context: jsonify(getContextFromEntry(entry)),
52
+ ...(entry.meta ? { meta: { file: getRelativeFilename(entry.meta.F), line: entry.meta.L } } : {}),
53
+ context: jsonlogify(getContextFromEntry(entry)),
35
54
  };
36
- appendFileSync(fd, JSON.stringify(record) + '\n');
55
+ let retryTS: number = 0;
56
+
57
+ // Retry writing if EAGAIN is encountered.
58
+ //
59
+ // Node may set stdout and stderr to non-blocking. https://github.com/nodejs/node/issues/42826
60
+ // This can cause EAGAIN errors when writing to them.
61
+ // In order to not drop logs, make log methods asynchronous, or deal with buffering/delayed writes, spin until write succeeds.
62
+
63
+ while (true) {
64
+ try {
65
+ return appendFileSync(fd, JSON.stringify(record) + '\n');
66
+ } catch (err: any) {
67
+ if (err.code !== 'EAGAIN') {
68
+ throw err;
69
+ }
70
+ if (retryTS === 0) {
71
+ retryTS = performance.now();
72
+ } else {
73
+ if (performance.now() - retryTS > EAGAIN_MAX_DURATION) {
74
+ console.log(`could not write after ${EAGAIN_MAX_DURATION}ms of EAGAIN failures, giving up`);
75
+ throw err;
76
+ }
77
+ }
78
+ }
79
+ }
37
80
  };
38
81
  };
39
82
 
@@ -47,6 +90,6 @@ const getLogFilePath = () => {
47
90
  };
48
91
 
49
92
  export const FILE_PROCESSOR: LogProcessor = createFileProcessor({
50
- path: getLogFilePath(),
93
+ pathOrFd: getLogFilePath(),
51
94
  levels: [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.TRACE],
52
95
  });