Commit 19b18a50 by Xianquan Committed by GitHub

chore: remove logger package (#6979)

parent a1ea7c40
...@@ -1653,43 +1653,6 @@ importers: ...@@ -1653,43 +1653,6 @@ importers:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.22.1 version: 4.22.1
sdk/logger:
dependencies:
'@logtape/logtape':
specifier: ^2
version: 2.0.2
'@logtape/pretty':
specifier: ^2
version: 2.0.2(@logtape/logtape@2.0.2)
'@opentelemetry/api':
specifier: ^1.9.0
version: 1.9.0
'@opentelemetry/api-logs':
specifier: ^0.203.0
version: 0.203.0
'@opentelemetry/exporter-logs-otlp-http':
specifier: ^0.203.0
version: 0.203.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources':
specifier: ^2.0.1
version: 2.5.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs':
specifier: ^0.203.0
version: 0.203.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions':
specifier: ^1.39.0
version: 1.39.0
devDependencies:
'@types/node':
specifier: 'catalog:'
version: 20.17.24
tsdown:
specifier: 'catalog:'
version: 0.21.10(typescript@5.9.3)
typescript:
specifier: 'catalog:'
version: 5.9.3
sdk/otel: sdk/otel:
dependencies: dependencies:
'@logtape/logtape': '@logtape/logtape':
# @fastgpt-sdk/logger
FastGPT 的通用日志 SDK,基于 `@logtape/logtape`,内置:
- Console pretty log
- OpenTelemetry OTLP log sink
- `AsyncLocalStorage` 上下文透传
- 兼容 FastGPT 现有 `getLogger(...).info('msg', {...})` 调用风格
## 安装
```bash
pnpm add @fastgpt-sdk/logger
```
## 快速开始
```ts
import { configureLogger, getLogger, withContext } from '@fastgpt-sdk/logger';
await configureLogger({
defaultCategory: ['my-app'],
console: {
enabled: true,
level: 'info'
},
otel: {
enabled: true,
serviceName: 'my-app',
url: 'http://localhost:4318/v1/logs',
level: 'info'
},
sensitiveProperties: ['password', 'token']
});
const logger = getLogger(['my-app', 'worker']);
await withContext({ requestId: 'req_123' }, async () => {
logger.info('worker started', { jobId: 'job_001' });
});
```
## API
### `configureLogger(options)`
```ts
type LoggerConfigureOptions = {
defaultCategory?: readonly string[];
console?: boolean | { enabled?: boolean; level?: LogLevel };
otel?: false | {
enabled?: boolean;
level?: LogLevel;
serviceName: string;
url?: string;
loggerName?: string;
};
sensitiveProperties?: readonly string[];
contextLocalStorage?: AsyncLocalStorage<Record<string, unknown>>;
loggers?: Config['loggers'];
};
```
- `defaultCategory`: 默认 category,不传时默认 `['system']`
- `console`: 控制台输出配置
- `otel`: OpenTelemetry 输出配置;启用时需要 `serviceName`
- `sensitiveProperties`: 命中这些字段的日志不会发往 OTEL sink
- `contextLocalStorage`: 可注入自己的上下文存储实例
- `loggers`: 可覆盖默认的 LogTape logger 路由配置
### `getLogger(category?)`
```ts
const logger = getLogger(['app', 'api']);
logger.info('request completed', { status: 200 });
logger.error('request failed', { error });
```
`category` 不需要预注册;传任意字符串数组即可。
### `withContext(context, fn)`
```ts
await withContext({ requestId: 'req_123' }, async () => {
logger.info('handling request');
});
```
## Env Helper
如果项目已经使用环境变量管理日志配置,也可以直接用 SDK 提供的转换助手:
```ts
import { configureLoggerFromEnv } from '@fastgpt-sdk/logger';
await configureLoggerFromEnv({
env: process.env,
defaultCategory: ['my-app'],
defaultServiceName: 'my-app',
sensitiveProperties: ['password', 'token']
});
```
支持读取这些环境变量:
- `LOG_ENABLE_CONSOLE`
- `LOG_CONSOLE_LEVEL`
- `LOG_ENABLE_OTEL`
- `LOG_OTEL_LEVEL`
- `LOG_OTEL_SERVICE_NAME`
- `LOG_OTEL_URL`
## OpenTelemetry
启用 `otel` 后,SDK 会创建 OTLP HTTP log exporter。
```ts
await configureLogger({
otel: {
enabled: true,
serviceName: 'my-app',
loggerName: 'my-app',
url: 'http://localhost:4318/v1/logs'
}
});
```
也可以直接从 `@fastgpt-sdk/logger` 导入 `getOpenTelemetrySink` 进行更细粒度集成。
## FastGPT 迁移建议
如果你在 FastGPT 仓库内部使用:
- 通用能力放到 `sdk/logger`
- 业务分类常量继续放在 service 侧
- service 侧保留一层 `configureLoggerFromEnv` 风格的兼容封装
{
"name": "@fastgpt-sdk/logger",
"private": false,
"version": "0.1.2",
"description": "FastGPT SDK for logger",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"sideEffects": false,
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"prepublishOnly": "pnpm build"
},
"keywords": [
"logger",
"opentelemetry"
],
"author": "FastGPT",
"repository": {
"type": "git",
"url": "https://github.com/labring/FastGPT.git",
"directory": "sdk/logger"
},
"homepage": "https://github.com/labring/FastGPT",
"bugs": {
"url": "https://github.com/labring/FastGPT/issues"
},
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=20.19.0",
"pnpm": "10.x"
},
"license": "Apache-2.0",
"dependencies": {
"@logtape/logtape": "^2",
"@logtape/pretty": "^2",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.203.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
"@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-logs": "^0.203.0",
"@opentelemetry/semantic-conventions": "^1.39.0"
},
"devDependencies": {
"@types/node": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
}
}
import { AsyncLocalStorage } from 'node:async_hooks';
import { configure, dispose, getLogger as getLogtapeLogger } from '@logtape/logtape';
import { createLoggers } from './loggers';
import { createSinks } from './sinks';
import type { LogCategory, LoggerConfigureOptions, LoggerContext } from './types';
let configured = false;
let configurePromise: Promise<void> | null = null;
let defaultCategory: LogCategory = ['system'];
export async function configureLogger(options: LoggerConfigureOptions = {}) {
if (configured) return;
if (configurePromise) return configurePromise;
configurePromise = (async () => {
defaultCategory = options.defaultCategory ?? defaultCategory;
const { sinks, composedSinks } = await createSinks({
console: options.console,
otel: options.otel,
sensitiveProperties: options.sensitiveProperties
});
const loggers = options.loggers ?? createLoggers({ composedSinks });
const contextLocalStorage =
options.contextLocalStorage ?? new AsyncLocalStorage<LoggerContext>();
await configure({
contextLocalStorage,
loggers,
sinks
});
configured = true;
})();
try {
await configurePromise;
} catch (error) {
configurePromise = null;
throw error;
}
}
export async function disposeLogger() {
if (configurePromise) {
try {
await configurePromise;
} catch {
configurePromise = null;
return;
}
}
if (!configured) return;
await dispose();
configured = false;
configurePromise = null;
}
export function getLogger(category: LogCategory = defaultCategory) {
const logger = getLogtapeLogger(category);
return new Proxy(logger, {
get(target, prop, receiver) {
const fn = Reflect.get(target, prop, receiver);
if (typeof fn !== 'function') return fn;
return (...args: unknown[]) => {
if (args.length === 0) return fn.call(target);
const [firstArg, secondArg] = args;
if (args.length === 1) {
return fn.call(target, firstArg);
}
if (typeof firstArg === 'string') {
if (
typeof secondArg === 'object' &&
secondArg &&
'verbose' in secondArg &&
typeof secondArg.verbose === 'boolean' &&
!secondArg.verbose
) {
const { verbose: _verbose, ...properties } = secondArg as Record<string, unknown> & {
verbose?: boolean;
};
return fn.call(target, firstArg, properties);
}
return fn.call(target, `${firstArg}: {*}`, secondArg);
}
if (typeof firstArg === 'object') {
return fn.call(target, firstArg);
}
return fn.apply(target, args);
};
}
});
}
import type { LogLevel } from '@logtape/logtape';
import { configureLogger } from './client';
import type { LogCategory, LoggerConfigureOptions } from './types';
export type LoggerEnvValue = string | boolean | number | undefined;
export type LoggerEnv = Record<string, LoggerEnvValue>;
export type LoggerConfigureFromEnvOptions = {
env?: LoggerEnv;
defaultCategory?: LogCategory;
defaultServiceName?: string;
defaultLoggerName?: string;
defaultConsoleEnabled?: boolean;
defaultConsoleLevel?: LogLevel;
defaultOtelEnabled?: boolean;
defaultOtelLevel?: LogLevel;
defaultOtelUrl?: string;
sensitiveProperties?: readonly string[];
};
const logLevels = new Set<LogLevel>(['trace', 'debug', 'info', 'warning', 'error', 'fatal']);
function parseBoolean(value: LoggerEnvValue, defaultValue: boolean) {
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
if (typeof value !== 'string' || !value) return defaultValue;
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return defaultValue;
}
function parseLogLevel(value: LoggerEnvValue, defaultValue: LogLevel): LogLevel {
if (typeof value !== 'string') return defaultValue;
return logLevels.has(value as LogLevel) ? (value as LogLevel) : defaultValue;
}
function parseString(value: LoggerEnvValue): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
export function createLoggerOptionsFromEnv(
options: LoggerConfigureFromEnvOptions = {}
): LoggerConfigureOptions {
const env = options.env ?? process.env;
const defaultServiceName = options.defaultServiceName ?? 'app';
const serviceName = parseString(env.LOG_OTEL_SERVICE_NAME) ?? defaultServiceName;
const loggerName =
parseString(env.LOG_OTEL_LOGGER_NAME) ?? options.defaultLoggerName ?? serviceName;
return {
defaultCategory: options.defaultCategory,
console: {
enabled: parseBoolean(env.LOG_ENABLE_CONSOLE, options.defaultConsoleEnabled ?? true),
level: parseLogLevel(env.LOG_CONSOLE_LEVEL, options.defaultConsoleLevel ?? 'trace')
},
otel: parseBoolean(env.LOG_ENABLE_OTEL, options.defaultOtelEnabled ?? false)
? {
serviceName,
loggerName,
url:
parseString(env.LOG_OTEL_URL) ??
options.defaultOtelUrl ??
'http://localhost:4318/v1/logs',
level: parseLogLevel(env.LOG_OTEL_LEVEL, options.defaultOtelLevel ?? 'info')
}
: false,
sensitiveProperties: options.sensitiveProperties
};
}
export async function configureLoggerFromEnv(options: LoggerConfigureFromEnvOptions = {}) {
return configureLogger(createLoggerOptionsFromEnv(options));
}
import { SeverityNumber } from '@opentelemetry/api-logs';
export function mapLevelToSeverityNumber(level: string): number {
switch (level) {
case 'trace':
return SeverityNumber.TRACE;
case 'debug':
return SeverityNumber.DEBUG;
case 'info':
return SeverityNumber.INFO;
case 'warning':
return SeverityNumber.WARN;
case 'error':
return SeverityNumber.ERROR;
case 'fatal':
return SeverityNumber.FATAL;
default:
return SeverityNumber.UNSPECIFIED;
}
}
export { configureLogger, disposeLogger, getLogger } from './client';
export { withContext, withCategoryPrefix } from '@logtape/logtape';
export { getOpenTelemetrySink } from './otel';
export type {
BodyFormatter,
ExceptionAttributeMode,
ObjectRenderer,
OpenTelemetrySink,
OpenTelemetrySinkOptions
} from './otel';
export type {
ConsoleLoggerOptions,
LogCategory,
LoggerConfig,
LoggerConfigureOptions,
LoggerContext,
LoggerSinkId,
OtelLoggerOptions
} from './types';
export { configureLoggerFromEnv, createLoggerOptionsFromEnv } from './env';
export type { LoggerConfigureFromEnvOptions, LoggerEnv } from './env';
import type { LogTapeConfig, LoggerSinkId } from './types';
type LoggerConfig = LogTapeConfig['loggers'];
type CreateLoggersOptions = {
composedSinks: LoggerSinkId[];
};
export function createLoggers({ composedSinks }: CreateLoggersOptions): LoggerConfig {
const metaSinks: LoggerSinkId[] = composedSinks.includes('console') ? ['console'] : composedSinks;
return [
{
category: [],
lowestLevel: 'trace',
sinks: composedSinks
},
...(metaSinks.length === 0
? []
: [
{
category: ['logtape', 'meta'],
lowestLevel: 'fatal' as const,
parentSinks: 'override' as const,
sinks: metaSinks
}
])
];
}
import type { LogLevel, LogRecord } from '@logtape/logtape';
import { getConsoleSink, withFilter } from '@logtape/logtape';
import { getPrettyFormatter } from '@logtape/pretty';
import { mapLevelToSeverityNumber } from './helpers';
import { getOpenTelemetrySink } from './otel';
import type {
ConsoleLoggerOptions,
LogTapeConfig,
LoggerConfigureOptions,
LoggerSinkId,
OtelLoggerOptions
} from './types';
type SinkConfig = LogTapeConfig<string>['sinks'];
type CreateSinksOptions = Pick<LoggerConfigureOptions, 'console' | 'otel' | 'sensitiveProperties'>;
type CreateSinksResult = {
sinks: SinkConfig;
composedSinks: LoggerSinkId[];
};
const defaultConsoleOptions: Required<ConsoleLoggerOptions> = {
enabled: true,
level: 'trace'
};
const defaultOtelOptions = {
enabled: false,
level: 'info' as LogLevel
};
function normalizeConsoleOptions(
options?: boolean | ConsoleLoggerOptions
): Required<ConsoleLoggerOptions> {
if (typeof options === 'boolean') {
return {
...defaultConsoleOptions,
enabled: options
};
}
return {
enabled: options?.enabled ?? defaultConsoleOptions.enabled,
level: options?.level ?? defaultConsoleOptions.level
};
}
function normalizeOtelOptions(options?: false | OtelLoggerOptions) {
if (!options) {
return {
...defaultOtelOptions,
serviceName: undefined,
url: undefined,
loggerName: undefined
};
}
return {
enabled: options.enabled ?? true,
level: options.level ?? defaultOtelOptions.level,
serviceName: options.serviceName,
url: options.url,
loggerName: options.loggerName ?? options.serviceName
};
}
function pad(value: number) {
return value.toString().padStart(2, '0');
}
function formatTimestamp(timestamp: number | Date) {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
date.getHours()
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
export async function createSinks(options: CreateSinksOptions): Promise<CreateSinksResult> {
const consoleOptions = normalizeConsoleOptions(options.console);
const otelOptions = normalizeOtelOptions(options.otel);
const sensitiveProperties = options.sensitiveProperties ?? [];
const sinkConfig = {
bufferSize: 8192,
flushInterval: 5000,
nonBlocking: true,
lazy: true
} as const;
const sinks: SinkConfig = {};
const composedSinks: LoggerSinkId[] = [];
const levelFilter = (record: LogRecord, level: LogLevel) => {
return mapLevelToSeverityNumber(record.level) >= mapLevelToSeverityNumber(level);
};
if (consoleOptions.enabled) {
sinks.console = withFilter(
getConsoleSink({
...sinkConfig,
formatter: getPrettyFormatter({
icons: false,
level: 'ABBR',
wordWrap: false,
messageColor: null,
categoryColor: null,
timestampColor: null,
levelStyle: 'reset',
messageStyle: 'reset',
categoryStyle: 'reset',
timestampStyle: 'reset',
categorySeparator: ':',
timestamp: formatTimestamp,
inspectOptions: { depth: 5 }
})
}),
(record) => levelFilter(record, consoleOptions.level)
);
composedSinks.push('console');
}
if (otelOptions.enabled) {
if (!otelOptions.serviceName) {
throw new Error('`otel.serviceName` is required when OpenTelemetry logging is enabled');
}
sinks.otel = withFilter(
getOpenTelemetrySink({
serviceName: otelOptions.serviceName,
loggerName: otelOptions.loggerName,
otlpExporterConfig: otelOptions.url ? { url: otelOptions.url } : undefined
}),
(record) => {
const properties = record.properties ?? {};
return (
levelFilter(record, otelOptions.level) &&
!sensitiveProperties.some((property) => property in properties)
);
}
);
composedSinks.push('otel');
}
return { sinks, composedSinks };
}
import type { AsyncLocalStorage } from 'node:async_hooks';
import type { Config, LogLevel } from '@logtape/logtape';
export type LogCategory = readonly string[];
export type LoggerContext = Record<string, unknown>;
export type LoggerSinkId = 'console' | 'otel';
type FilterId = string;
export type LogTapeConfig<S extends string = LoggerSinkId, F extends string = FilterId> = Config<
S,
F
>;
export type LoggerConfig = LogTapeConfig['loggers'];
export type ConsoleLoggerOptions = {
enabled?: boolean;
level?: LogLevel;
};
export type OtelLoggerOptions = {
enabled?: boolean;
level?: LogLevel;
serviceName: string;
url?: string;
loggerName?: string;
};
export type LoggerConfigureOptions = {
console?: boolean | ConsoleLoggerOptions;
otel?: false | OtelLoggerOptions;
contextLocalStorage?: AsyncLocalStorage<LoggerContext>;
loggers?: LoggerConfig;
sensitiveProperties?: readonly string[];
defaultCategory?: LogCategory;
};
{
"compilerOptions": {
"module": "esnext",
"target": "es2022",
"moduleResolution": "bundler",
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"strict": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
"noEmit": true
}
}
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: 'src/index.ts',
format: 'esm',
dts: {
enabled: true,
sourcemap: false
},
outExtensions() {
return {
dts: '.d.ts',
js: '.js'
};
}
});
...@@ -2,15 +2,9 @@ import { defineConfig } from 'tsdown'; ...@@ -2,15 +2,9 @@ import { defineConfig } from 'tsdown';
export default defineConfig({ export default defineConfig({
entry: 'src/index.ts', entry: 'src/index.ts',
format: ['esm', 'cjs'],
dts: { dts: {
enabled: true, enabled: true,
sourcemap: false sourcemap: false
}, },
outExtensions({ format }) { outExtensions: () => ({ js: '.js', dts: '.d.ts' })
return {
js: format === 'cjs' ? '.cjs' : '.js',
dts: '.d.ts'
};
}
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment