mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 12:05:19 +02:00
enhance: 旧Loggerを拡張して構造化ログを直接受け取れるように (#17780)
* enhance: 旧Loggerを拡張して構造化ログを直接受け取れるように * fix lint
This commit is contained in:
@@ -5,9 +5,12 @@
|
||||
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { logManager } from './logging/logging-runtime.js';
|
||||
import type { LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
|
||||
import type { LogEntryInput, LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
|
||||
import type { Keyword } from 'color-convert';
|
||||
|
||||
// 旧APIのdataは表示用の任意値を受け取り、Errorや配列も既存呼び出しで使用されています。
|
||||
type LegacyData = Record<string, any> | null;
|
||||
|
||||
/**
|
||||
* ロガー名の階層と従来の公開APIを提供する薄い窓口です。
|
||||
* 出力条件の判断や整形はLogManagerとLogBackendへ委譲します。
|
||||
@@ -52,6 +55,15 @@ export default class Logger {
|
||||
});
|
||||
}
|
||||
|
||||
/** level別メソッドの構造化入力にlevelとLoggerのcontextを付けて渡します。 */
|
||||
@bindThis
|
||||
private logStructured(level: LogLevel, input: LogEntryInput): void {
|
||||
this.write({
|
||||
...input,
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
/** 構造化ログをLoggerのcontext付きでLogManagerへ渡します。 */
|
||||
@bindThis
|
||||
public write(input: LogWriteInput): void {
|
||||
@@ -62,24 +74,35 @@ export default class Logger {
|
||||
}
|
||||
|
||||
/** 処理を継続できない状況を記録します。 */
|
||||
public error(input: LogEntryInput): void;
|
||||
public error(error: Error, data?: LegacyData, important?: boolean): void;
|
||||
public error(message: string, data?: LegacyData, important?: boolean): void;
|
||||
public error(errorOrMessage: string | Error, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public error(x: string | Error, data?: Record<string, any> | null, important = false): void {
|
||||
public error(x: LogEntryInput | string | Error, data?: LegacyData, important = false): void {
|
||||
if (x instanceof Error) {
|
||||
// エラー本体も第2引数へ残し、従来どおりスタックなどを確認できるようにします。
|
||||
data = data ?? {};
|
||||
data.e = x;
|
||||
this.log('error', x.toString(), data, important, undefined, x);
|
||||
} else if (typeof x === 'object') {
|
||||
this.log('error', `${(x as any).message ?? (x as any).name ?? x}`, data, important);
|
||||
} else if (typeof x === 'string') {
|
||||
this.log('error', x, data, important);
|
||||
} else {
|
||||
this.log('error', `${x}`, data, important);
|
||||
this.logStructured('error', x);
|
||||
}
|
||||
}
|
||||
|
||||
/** 処理は継続できるものの、改善が必要な状況を記録します。 */
|
||||
public warn(input: LogEntryInput): void;
|
||||
public warn(message: string): void;
|
||||
public warn(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public warn(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('warn', message, data, important);
|
||||
public warn(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('warn', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('warn', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 処理が成功したことを、従来のDONE表示で記録します。 */
|
||||
@@ -89,14 +112,40 @@ export default class Logger {
|
||||
}
|
||||
|
||||
/** 開発者向けの調査情報を記録します。 */
|
||||
public debug(input: LogEntryInput): void;
|
||||
public debug(message: string): void;
|
||||
public debug(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public debug(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('debug', message, data, important);
|
||||
public debug(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('debug', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('debug', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 通常の動作状況を記録します。 */
|
||||
public info(input: LogEntryInput): void;
|
||||
public info(message: string): void;
|
||||
public info(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public info(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('info', message, data, important);
|
||||
public info(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('info', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('info', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 致命的な状況を構造化ログとして記録します。 */
|
||||
public fatal(input: LogEntryInput): void;
|
||||
public fatal(message: string): void;
|
||||
@bindThis
|
||||
public fatal(inputOrMessage: LogEntryInput | string): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.logStructured('fatal', { message: inputOrMessage });
|
||||
} else {
|
||||
this.logStructured('fatal', inputOrMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,15 +52,19 @@ export type LogTraceContext = {
|
||||
/** LogManagerが出力直前に呼び出すTrace Context取得処理です。 */
|
||||
export type LogTraceContextProvider = () => LogTraceContext | undefined;
|
||||
|
||||
/** ロガーの呼び出し側が構造化ログとして指定する入力です。 */
|
||||
export type LogWriteInput = {
|
||||
readonly level: LogLevel;
|
||||
/** level別のLoggerメソッドが受け取る構造化ログの共通入力です。 */
|
||||
export type LogEntryInput = {
|
||||
readonly message: string;
|
||||
readonly eventName?: string;
|
||||
readonly attributes?: Readonly<Record<string, unknown>>;
|
||||
readonly error?: unknown;
|
||||
};
|
||||
|
||||
/** 動的なlevelを含めてLogManagerへ渡す構造化ログの入力です。 */
|
||||
export type LogWriteInput = LogEntryInput & {
|
||||
readonly level: LogLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* ロガー名を構成する一要素です。
|
||||
* 色は見やすい形式での表示だけに使い、ログの意味には影響させません。
|
||||
|
||||
@@ -76,6 +76,91 @@ describe('Logger', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
test('supports structured input through every level-specific method', () => {
|
||||
const logger = new Logger('root').createSubLogger('child');
|
||||
const error = new Error('broken');
|
||||
const input = {
|
||||
eventName: 'example.failed',
|
||||
message: 'failed',
|
||||
attributes: { id: 'id' },
|
||||
error,
|
||||
};
|
||||
|
||||
logger.debug(input);
|
||||
logger.info(input);
|
||||
logger.warn(input);
|
||||
logger.error(input);
|
||||
logger.fatal(input);
|
||||
|
||||
expect(mocks.write.mock.calls.map(([entry]) => entry.level)).toEqual([
|
||||
'debug',
|
||||
'info',
|
||||
'warn',
|
||||
'error',
|
||||
'fatal',
|
||||
]);
|
||||
for (const [entry] of mocks.write.mock.calls) {
|
||||
expect(entry).toMatchObject({
|
||||
...input,
|
||||
context: [
|
||||
{ name: 'root', color: undefined },
|
||||
{ name: 'child', color: undefined },
|
||||
],
|
||||
});
|
||||
expect(entry).not.toHaveProperty('compatibility');
|
||||
}
|
||||
});
|
||||
|
||||
test('level-specific methods own the level even for runtime-invalid input', () => {
|
||||
const logger = new Logger('root');
|
||||
|
||||
// @ts-expect-error level is selected by the method rather than the input object
|
||||
logger.warn({ level: 'error', message: 'warning' });
|
||||
|
||||
expect(mocks.write.mock.calls[0][0]).toMatchObject({
|
||||
level: 'warn',
|
||||
message: 'warning',
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves the legacy string signatures for non-error levels', () => {
|
||||
const logger = new Logger('root');
|
||||
logger.debug('debug', { source: 'debug' }, true);
|
||||
logger.info('info', null, true);
|
||||
logger.warn('warn', { source: 'warn' });
|
||||
|
||||
expect(mocks.write.mock.calls.map(([entry]) => entry.compatibility)).toEqual([
|
||||
{ legacyLevel: undefined, important: true, data: { source: 'debug' } },
|
||||
{ legacyLevel: undefined, important: true, data: null },
|
||||
{ legacyLevel: undefined, important: false, data: { source: 'warn' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('records a fatal string through the structured API', () => {
|
||||
new Logger('root').fatal('fatal message');
|
||||
|
||||
expect(mocks.write).toHaveBeenCalledWith({
|
||||
level: 'fatal',
|
||||
message: 'fatal message',
|
||||
context: [{ name: 'root', color: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves the legacy error string signature', () => {
|
||||
new Logger('root').error('failed', { requestId: 'request' }, true);
|
||||
|
||||
expect(mocks.write).toHaveBeenCalledWith({
|
||||
level: 'error',
|
||||
message: 'failed',
|
||||
context: [{ name: 'root', color: undefined }],
|
||||
compatibility: {
|
||||
legacyLevel: undefined,
|
||||
important: true,
|
||||
data: { requestId: 'request' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('uses Error.toString and adds the Error to existing data', () => {
|
||||
const logger = new Logger('root');
|
||||
const error = new TypeError('broken');
|
||||
|
||||
Reference in New Issue
Block a user