Skip to content

Commit a2bff3b

Browse files
authored
feat(logger): timestamp, pretty-print objects #337
BEFORE: info: [object Object] debug: request -> nvim_command debug: request -> nvim_list_wins debug: response -> nvim_command: null debug: response -> nvim_list_wins: [object Object],[object Object],[object Object],[object Object] ... AFTER: 2024-03-20 11:37:25 INF { matches: [ { nvimVersion: '0.10.0-dev-2657+g9765efb40f57', path: '/usr/local/bin/nvim', buildType: 'RelWithDebInfo', luaJitVersion: '2.1.1710088188' } ], invalid: [] } 2024-03-20 11:37:25 DBG request -> nvim_command 2024-03-20 11:37:25 DBG request -> nvim_list_wins 2024-03-20 11:37:25 DBG response -> nvim_command: null 2024-03-20 11:37:25 DBG response -> nvim_list_wins: [ { _events: [Object: null prototype] {}, _eventsCount: 0, _maxListeners: undefined, transport: '[Object]', data: 1003, logger: '[Object]', client: '[Object]', prefix: 'nvim_win_' }, { _events: [Object: null prototype] {}, _eventsCount: 0, _maxListeners: undefined, transport: '[Object]', data: 1002, logger: '[Object]', client: '[Object]', prefix: 'nvim_win_' }, ... ] ... 2024-03-20 11:37:25 DBG request -> nvim_buf_get_lines 2024-03-20 11:37:25 DBG response -> nvim_buf_get_lines: [ 'line1', 'line2' ]
1 parent 899a54f commit a2bff3b

File tree

10 files changed

+112
-33
lines changed

10 files changed

+112
-33
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ module.exports = {
4848
functions: 'ignore',
4949
},
5050
],
51+
// airbnb discourages "for..of", wtf? https://github.com/airbnb/javascript/issues/1271#issuecomment-548688952
52+
'no-restricted-syntax': 'off',
5153
'no-case-declarations': 'off',
5254
'no-prototype-builtins': 'off',
5355
'no-underscore-dangle': 'off',

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010

1111
env:
1212
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
13-
# Purpose of this (currently) is to exercise related codepaths.
13+
# Set NVIM_NODE_LOG_FILE to exercise related codepaths.
1414
NVIM_NODE_LOG_FILE: 'node-client.test.log'
1515

1616
# A workflow run is made up of one or more jobs that can run sequentially or in parallel

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
- fix: console.log() writes to RPC channel #202 #329
66
- feat: eliminate `which` dependency
77
- feat: improve logs + error messages
8-
- fix: always use custom logger if one is given
8+
- fix: always use custom logger if one is given #336
9+
- feat(logger): timestamp, pretty-print objects #337
910

1011
## [5.0.1](https://github.com/neovim/node-client/compare/v4.11.0...v5.0.1) (2024-03-01)
1112

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ Following is a complete, working example.
5959
const found = findNvim({ orderBy: 'desc', minVersion: '0.9.0' })
6060
console.log(found);
6161
const nvim_proc = child_process.spawn(found.matches[0].path, ['--clean', '--embed'], {});
62-
6362
const nvim = attach({ proc: nvim_proc });
63+
6464
nvim.command('vsp | vsp | vsp');
6565

6666
const windows = await nvim.windows;
@@ -81,7 +81,6 @@ Following is a complete, working example.
8181
const newLines = await buf.lines;
8282
assert.deepStrictEqual(newLines, ['line1', 'line2']);
8383

84-
// console.log('%O', nvim_proc);
8584
if (nvim_proc.disconnect) {
8685
nvim_proc.disconnect();
8786
}

packages/neovim/src/api/Base.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EventEmitter } from 'node:events';
22

33
import { Transport } from '../utils/transport';
4+
import { partialClone } from '../utils/util';
45
import { Logger, getLogger } from '../utils/logger';
56
import { VimValue } from '../types/VimValue';
67

@@ -22,8 +23,8 @@ const DO_REQUEST = Symbol('DO_REQUEST');
2223
// i.e. a plugin that detaches will affect all plugins registered on host
2324
// const EXCLUDED = ['nvim_buf_attach', 'nvim_buf_detach'];
2425

25-
// Instead of dealing with multiple inheritance (or lackof), just extend EE
26-
// Only the Neovim API class should use EE though
26+
// Instead of dealing with multiple inheritance (or lackof), just extend EventEmitter
27+
// Only the Neovim API class should use EventEmitter though
2728
export class BaseApi extends EventEmitter {
2829
protected transport: Transport;
2930

@@ -49,7 +50,6 @@ export class BaseApi extends EventEmitter {
4950

5051
this.setTransport(transport);
5152
this.data = data;
52-
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
5353
this.logger = logger || getLogger();
5454
this.client = client;
5555

@@ -73,7 +73,25 @@ export class BaseApi extends EventEmitter {
7373
[DO_REQUEST] = (name: string, args: any[] = []): Promise<any> =>
7474
new Promise((resolve, reject) => {
7575
this.transport.request(name, args, (err: any, res: any) => {
76-
this.logger.debug(`response -> ${name}: ${res}`);
76+
if (this.logger.level === 'debug') {
77+
// Avoid noisy logging of entire Buffer/Window/Tabpage.
78+
let logData: any;
79+
try {
80+
logData =
81+
res && typeof res === 'object'
82+
? partialClone(
83+
res,
84+
2,
85+
['logger', 'transport', 'client'],
86+
'[Object]'
87+
)
88+
: res;
89+
} catch {
90+
logData = String(res);
91+
}
92+
this.logger.debug(`response -> ${name}: %O`, logData);
93+
}
94+
7795
if (err) {
7896
reject(new Error(`${name}: ${err[1]}`));
7997
} else {
@@ -89,7 +107,7 @@ export class BaseApi extends EventEmitter {
89107
// Not possible for ExtType classes since they are only created after transport is ready
90108
await this._isReady;
91109

92-
this.logger.debug(`request -> ${name}`);
110+
this.logger.debug(`request -> ${name}`);
93111

94112
return this[DO_REQUEST](name, args).catch(err => {
95113
// XXX: Get a `*.ts stacktrace. If we re-throw `err` we get a `*.js` trace. tsconfig issue?

packages/neovim/src/attach/attach.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export function attach({
3737
}
3838

3939
if (writer && reader) {
40-
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
4140
const loggerInstance = options.logger || getLogger(); // lazy load to winston only if needed
4241
const neovim = new NeovimClient({ logger: loggerInstance });
4342
neovim.attach({

packages/neovim/src/plugin/autocmd.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ export function autocmd(name: string, options?: AutocmdOptions) {
2929
}
3030
});
3131

32-
const nameWithPattern = `${name}${
33-
options.pattern ? `:${options.pattern}` : ''
34-
}`;
32+
const nameWithPattern = `${name}${options.pattern ? `:${options.pattern}` : ''}`;
3533
Object.defineProperty(f, NVIM_METHOD_NAME, {
3634
value: `autocmd:${nameWithPattern}`,
3735
});

packages/neovim/src/testUtil.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export function startNvim(
3434
): [cp.ChildProcessWithoutNullStreams, NeovimClient | undefined] {
3535
const testFile = jest.expect.getState().testPath?.replace(/.*[\\/]/, '');
3636
const msg = `startNvim in test: ${testFile}`;
37-
// console.log(msg);
3837
if (process.env.NVIM_NODE_LOG_FILE) {
3938
const logfile = path.resolve(process.env.NVIM_NODE_LOG_FILE);
4039
fs.writeFileSync(logfile, `${msg}\n`, { flag: 'a' });

packages/neovim/src/utils/logger.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
11
import * as winston from 'winston';
2+
import { inspect } from 'node:util';
23

34
const level = process.env.NVIM_NODE_LOG_LEVEL || 'debug';
45

5-
export type Logger = Pick<winston.Logger, 'info' | 'warn' | 'error' | 'debug'>;
6+
export type Logger = Pick<
7+
winston.Logger,
8+
'info' | 'warn' | 'error' | 'debug' | 'level'
9+
>;
10+
11+
function getFormat(colorize: boolean) {
12+
return winston.format.combine(
13+
winston.format.splat(),
14+
winston.format.timestamp({
15+
format: 'YYYY-MM-DD HH:mm:ss',
16+
}),
17+
winston.format.printf(info => {
18+
let msg: string;
19+
try {
20+
msg =
21+
typeof info.message === 'object'
22+
? inspect(info.message, false, 2, colorize)
23+
: info.message;
24+
} catch {
25+
msg = info.message;
26+
}
27+
const lvl =
28+
info.level === 'debug' ? 'DBG' : info.level.slice(0, 3).toUpperCase();
29+
return `${info.timestamp} ${lvl} ${msg}`;
30+
})
31+
);
32+
}
633

734
function setupWinstonLogger(): Logger {
835
const logger = winston.createLogger({
@@ -14,31 +41,15 @@ function setupWinstonLogger(): Logger {
1441
new winston.transports.File({
1542
filename: process.env.NVIM_NODE_LOG_FILE,
1643
level,
17-
format: winston.format.combine(
18-
winston.format.splat(),
19-
winston.format.timestamp({
20-
format: 'YYYY-MM-DD HH:mm:ss',
21-
}),
22-
winston.format.errors({ stack: true }),
23-
winston.format.printf(info => {
24-
if (info.raw) {
25-
return info.message;
26-
}
27-
const lvl =
28-
info.level === 'debug'
29-
? 'DBG'
30-
: info.level.slice(0, 3).toUpperCase();
31-
return `${info.timestamp} ${lvl} ${info.message}`;
32-
})
33-
),
44+
format: getFormat(false),
3445
})
3546
);
3647
}
3748

3849
if (process.env.ALLOW_CONSOLE) {
3950
logger.add(
4051
new winston.transports.Console({
41-
format: winston.format.simple(),
52+
format: getFormat(true),
4253
})
4354
);
4455
}

packages/neovim/src/utils/util.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Clones an object (copies "own properties") until `depth`, where:
3+
* - depth=0 returns non-object value, or empty object (`{}` or `[]`).
4+
* - depth=1 returns `obj` with its immediate children (but not their children).
5+
* - depth=2 returns `obj` with its children and their children.
6+
* - and so on...
7+
*
8+
* TODO: node's `util.inspect()` function is better, but doesn't work in web browser?
9+
*
10+
* @param obj Object to clone.
11+
* @param depth
12+
* @param omitKeys Omit properties matching these names (at any depth).
13+
* @param replacement Replacement for object whose fields extend beyond `depth`, and properties matching `omitKeys`.
14+
*/
15+
export function partialClone(
16+
obj: any,
17+
depth: number = 3,
18+
omitKeys: string[] = [],
19+
replacement: any = undefined
20+
): any {
21+
// Base case: If input is not an object or has no children, return it.
22+
if (
23+
typeof obj !== 'object' ||
24+
obj === null ||
25+
Object.getOwnPropertyNames(obj).length === 0
26+
) {
27+
return obj;
28+
}
29+
30+
// Create a new object of the same type as the input object.
31+
const clonedObj = Array.isArray(obj) ? [] : {};
32+
33+
if (depth === 0) {
34+
return replacement || clonedObj;
35+
}
36+
37+
// Recursively clone properties of the input object
38+
for (const key of Object.keys(obj)) {
39+
if (omitKeys.includes(key)) {
40+
(clonedObj as any)[key] = replacement || (Array.isArray(obj) ? [] : {});
41+
} else if (Object.prototype.hasOwnProperty.call(obj, key)) {
42+
(clonedObj as any)[key] = partialClone(
43+
obj[key],
44+
depth - 1,
45+
omitKeys,
46+
replacement
47+
);
48+
}
49+
}
50+
51+
return clonedObj;
52+
}

0 commit comments

Comments
 (0)