mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-03 01:02:15 +08:00
V4.14.9 dev (#6555)
* feat: encapsulate logger (#6535) * feat: encapsulate logger * update engines --------- Co-authored-by: archer <545436317@qq.com> * next config * dev shell * Agent sandbox (#6532) * docs: switch to docs layout and apply black theme (#6533) * feat: add Gemini 3.1 models - Add gemini-3.1-pro-preview (released February 19, 2026) - Add gemini-3.1-flash-lite-preview (released March 3, 2026) Both models support: - 1M context window - 64k max response - Vision - Tool choice * docs: switch to docs layout and apply black theme - Change layout from notebook to docs - Update logo to icon + text format - Apply fumadocs black theme - Simplify global.css (keep only navbar and TOC styles) - Fix icon components to properly accept className props - Add mobile text overflow handling - Update Node engine requirement to >=20.x * doc * doc * lock * fix: ts * doc * doc --------- Co-authored-by: archer <archer@archerdeMac-mini.local> Co-authored-by: archer <545436317@qq.com> * Doc (#6493) * cloud doc * doc refactor * doc move * seo * remove doc * yml * doc * fix: tsconfig * fix: tsconfig * sandbox version (#6497) * sandbox version * add sandbox log * update lock * fix * fix: sandbox * doc * add console * i18n * sandbxo in agent * feat: agent sandbox * lock * feat: sandbox ui * sandbox check exists * env tempalte * doc * lock * sandbox in chat window * sandbox entry * fix: test * rename var * sandbox config tip * update sandbox lifecircle * update prompt * rename provider test * sandbox logger * yml --------- Co-authored-by: Archer <archer@fastgpt.io> Co-authored-by: archer <archer@archerdeMac-mini.local> * perf: sandbox error tip * Add sandbox limit and fix some issue (#6550) * sandbox in plan * fix: some issue * fix: test * editor default path * fix: comment * perf: sandbox worksapce * doc * perf: del sandbox * sandbox build * fix: test * fix: pr comment --------- Co-authored-by: Ryo <whoeverimf5@gmail.com> Co-authored-by: Archer <archer@fastgpt.io> Co-authored-by: archer <archer@archerdeMac-mini.local>
This commit is contained in:
@@ -27,6 +27,7 @@ export enum QueueNames {
|
||||
datasetSync = 'datasetSync',
|
||||
evaluation = 'evaluation',
|
||||
s3FileDelete = 's3FileDelete',
|
||||
collectionUpdate = 'collectionUpdate',
|
||||
|
||||
// Delete Queue
|
||||
datasetDelete = 'datasetDelete',
|
||||
|
||||
@@ -75,7 +75,8 @@ export const LogCategories = {
|
||||
LLM: ['ai', 'llm'],
|
||||
MODEL: ['ai', 'model'],
|
||||
OPTIMIZE_PROMPT: ['ai', 'optimize-prompt'],
|
||||
RERANK: ['ai', 'rerank']
|
||||
RERANK: ['ai', 'rerank'],
|
||||
SANDBOX: ['ai', 'sandbox']
|
||||
}),
|
||||
USER: Object.assign(['user'], {
|
||||
ACCOUNT: ['user', 'account'],
|
||||
|
||||
@@ -1,84 +1,13 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
import { configure, dispose, Logger } from '@logtape/logtape';
|
||||
import { configureLoggerFromEnv, disposeLogger, getLogger } from '@fastgpt-sdk/logger';
|
||||
import { env } from '../../env';
|
||||
import { createSinks } from './sinks';
|
||||
import { createLoggers } from './loggers';
|
||||
import { getLogger as getLogtapeLogger } from '@logtape/logtape';
|
||||
|
||||
let configured = false;
|
||||
export async function configureLogger() {
|
||||
if (configured) return;
|
||||
|
||||
const {
|
||||
LOG_ENABLE_CONSOLE,
|
||||
LOG_ENABLE_OTEL,
|
||||
LOG_OTEL_SERVICE_NAME,
|
||||
LOG_OTEL_URL,
|
||||
LOG_CONSOLE_LEVEL,
|
||||
LOG_OTEL_LEVEL
|
||||
} = env;
|
||||
|
||||
const { sinks, composedSinks } = await createSinks({
|
||||
enableConsole: LOG_ENABLE_CONSOLE,
|
||||
enableOtel: LOG_ENABLE_OTEL,
|
||||
otelServiceName: LOG_OTEL_SERVICE_NAME,
|
||||
otelUrl: LOG_OTEL_URL,
|
||||
consoleLevel: LOG_CONSOLE_LEVEL,
|
||||
otelLevel: LOG_OTEL_LEVEL
|
||||
});
|
||||
|
||||
const loggers = createLoggers({ composedSinks });
|
||||
|
||||
const contextLocalStorage = new AsyncLocalStorage<Record<string, unknown>>();
|
||||
|
||||
await configure({
|
||||
contextLocalStorage,
|
||||
loggers,
|
||||
sinks
|
||||
});
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
export async function disposeLogger() {
|
||||
if (!configured) return;
|
||||
|
||||
await dispose();
|
||||
configured = false;
|
||||
}
|
||||
|
||||
export function getLogger(category: readonly string[] = ['system']) {
|
||||
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 [f, s] = args;
|
||||
if (args.length === 1) {
|
||||
return fn.call(target, f);
|
||||
}
|
||||
if (typeof f === 'string') {
|
||||
if (
|
||||
typeof s === 'object' &&
|
||||
s &&
|
||||
'verbose' in s &&
|
||||
typeof s.verbose === 'boolean' &&
|
||||
!s.verbose
|
||||
) {
|
||||
delete s.verbose;
|
||||
return fn.call(target, f, s);
|
||||
}
|
||||
|
||||
return fn.call(target, `${f}: {*}`, s);
|
||||
}
|
||||
if (typeof f === 'object') {
|
||||
return fn.call(target, f);
|
||||
}
|
||||
return fn.apply(target, args);
|
||||
};
|
||||
}
|
||||
await configureLoggerFromEnv({
|
||||
env,
|
||||
defaultCategory: ['system'],
|
||||
defaultServiceName: 'fastgpt-client',
|
||||
sensitiveProperties: ['fastgpt']
|
||||
});
|
||||
}
|
||||
|
||||
export { disposeLogger, getLogger };
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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 const sensitiveProperties = ['fastgpt'] as const;
|
||||
@@ -1,4 +1,4 @@
|
||||
export { configureLogger, disposeLogger, getLogger } from './client';
|
||||
export { withContext, withCategoryPrefix } from '@logtape/logtape';
|
||||
export { withContext, withCategoryPrefix } from '@fastgpt-sdk/logger';
|
||||
export { LogCategories } from './categories';
|
||||
export type { LogCategory } from './categories';
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import type { Config, LogLevel } from '@logtape/logtape';
|
||||
import { moduleCategories } from './categories';
|
||||
|
||||
type SinkId = 'console' | 'jsonl' | 'otel';
|
||||
type FilterId = string;
|
||||
type LogTapeConfig<S extends string = SinkId, F extends string = FilterId> = Config<S, F>;
|
||||
type LoggerConfig = LogTapeConfig['loggers'];
|
||||
|
||||
type CreateLoggersOptions = {
|
||||
composedSinks: SinkId[];
|
||||
};
|
||||
|
||||
export function createLoggers(options: CreateLoggersOptions) {
|
||||
const { composedSinks } = options;
|
||||
|
||||
const loggers: LoggerConfig = [
|
||||
{
|
||||
category: [],
|
||||
lowestLevel: 'trace',
|
||||
sinks: ['console']
|
||||
},
|
||||
// logtape 内部日志
|
||||
{
|
||||
category: ['logtape', 'meta'],
|
||||
lowestLevel: 'fatal',
|
||||
parentSinks: 'override',
|
||||
sinks: ['console']
|
||||
},
|
||||
// 应用层日志
|
||||
{
|
||||
category: ['system'],
|
||||
lowestLevel: 'trace',
|
||||
parentSinks: 'override',
|
||||
sinks: composedSinks
|
||||
},
|
||||
// 错误层日志
|
||||
{
|
||||
category: ['error'],
|
||||
lowestLevel: 'error',
|
||||
parentSinks: 'override',
|
||||
sinks: composedSinks
|
||||
},
|
||||
// HTTP 层日志
|
||||
{
|
||||
category: ['http'],
|
||||
lowestLevel: 'trace',
|
||||
parentSinks: 'override',
|
||||
sinks: composedSinks
|
||||
},
|
||||
// 基础设施层日志
|
||||
{
|
||||
category: ['infra'],
|
||||
lowestLevel: 'trace',
|
||||
parentSinks: 'override',
|
||||
sinks: composedSinks
|
||||
},
|
||||
// 业务模块层日志
|
||||
...moduleCategories.map(
|
||||
(category) =>
|
||||
({
|
||||
category: [category],
|
||||
lowestLevel: 'trace' as const,
|
||||
parentSinks: 'override',
|
||||
sinks: composedSinks
|
||||
}) satisfies LoggerConfig[number]
|
||||
),
|
||||
// 事件层日志
|
||||
{
|
||||
category: ['event'],
|
||||
lowestLevel: 'trace',
|
||||
parentSinks: 'override',
|
||||
sinks: composedSinks
|
||||
}
|
||||
];
|
||||
|
||||
return loggers;
|
||||
}
|
||||
@@ -1,645 +0,0 @@
|
||||
import { getLogger, type Logger, type LogRecord, type Sink } from '@logtape/logtape';
|
||||
import { diag, type DiagLogger, DiagLogLevel } from '@opentelemetry/api';
|
||||
import {
|
||||
type AnyValue,
|
||||
type Logger as OTLogger,
|
||||
type LoggerProvider as LoggerProviderBase,
|
||||
type LogRecord as OTLogRecord,
|
||||
NOOP_LOGGER,
|
||||
SeverityNumber
|
||||
} from '@opentelemetry/api-logs';
|
||||
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
||||
import type { Resource } from '@opentelemetry/resources';
|
||||
import { defaultResource, resourceFromAttributes } from '@opentelemetry/resources';
|
||||
import { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';
|
||||
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
|
||||
import { inspect as nodeInspect } from 'util';
|
||||
|
||||
/**
|
||||
* Gets an environment variable value in Node.js.
|
||||
* @param name The environment variable name.
|
||||
* @returns The environment variable value, or undefined if not found.
|
||||
*/
|
||||
function getEnvironmentVariable(name: string): string | undefined {
|
||||
return process.env[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an OTLP endpoint is configured via environment variables or options.
|
||||
* Checks the following environment variables:
|
||||
* - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (logs-specific endpoint)
|
||||
* - `OTEL_EXPORTER_OTLP_ENDPOINT` (general OTLP endpoint)
|
||||
*
|
||||
* @param config Optional exporter configuration that may contain a URL.
|
||||
* @returns `true` if an endpoint is configured, `false` otherwise.
|
||||
*/
|
||||
type OtlpHttpExporterConfig = ConstructorParameters<typeof OTLPLogExporter>[0];
|
||||
|
||||
function hasOtlpEndpoint(config?: OtlpHttpExporterConfig): boolean {
|
||||
if (config?.url) return true;
|
||||
|
||||
const logsEndpoint = getEnvironmentVariable('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT');
|
||||
if (logsEndpoint) return true;
|
||||
|
||||
const endpoint = getEnvironmentVariable('OTEL_EXPORTER_OTLP_ENDPOINT');
|
||||
if (endpoint) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The OpenTelemetry logger provider.
|
||||
*/
|
||||
type ILoggerProvider = LoggerProviderBase & {
|
||||
/**
|
||||
* Flush all buffered data and shut down the LoggerProvider and all registered
|
||||
* LogRecordProcessor.
|
||||
*
|
||||
* Returns a promise which is resolved when all flushes are complete.
|
||||
*/
|
||||
shutdown?: () => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The way to render the object in the log record. If `"json"`,
|
||||
* the object is rendered as a JSON string. If `"inspect"`,
|
||||
* the object is rendered using `util.inspect` in Node.js.
|
||||
*/
|
||||
export type ObjectRenderer = 'json' | 'inspect';
|
||||
|
||||
type Message = (string | null | undefined)[];
|
||||
|
||||
/**
|
||||
* Custom `body` attribute formatter.
|
||||
*/
|
||||
export type BodyFormatter = (message: Message) => AnyValue;
|
||||
|
||||
/**
|
||||
* How to serialize `Error` objects in log attributes.
|
||||
*/
|
||||
export type ExceptionAttributeMode = 'semconv' | 'raw' | false;
|
||||
|
||||
/**
|
||||
* Base options shared by all OpenTelemetry sink configurations.
|
||||
*/
|
||||
interface OpenTelemetrySinkOptionsBase {
|
||||
/**
|
||||
* The way to render the message in the log record. If `"string"`,
|
||||
* the message is rendered as a single string with the values are
|
||||
* interpolated into the message. If `"array"`, the message is
|
||||
* rendered as an array of strings. `"string"` by default.
|
||||
*
|
||||
* Or even fully customizable with a {@link BodyFormatter} function.
|
||||
*/
|
||||
messageType?: 'string' | 'array' | BodyFormatter;
|
||||
|
||||
/**
|
||||
* The way to render the object in the log record. If `"json"`,
|
||||
* the object is rendered as a JSON string. If `"inspect"`,
|
||||
* the object is rendered using `util.inspect` in Node.js.
|
||||
* `"inspect"` by default.
|
||||
*/
|
||||
objectRenderer?: ObjectRenderer;
|
||||
|
||||
/**
|
||||
* How to serialize `Error` objects in log attributes.
|
||||
*
|
||||
* - `"semconv"` (default): Follow OpenTelemetry semantic conventions,
|
||||
* converting `Error` objects to `exception.type`, `exception.message`,
|
||||
* and `exception.stacktrace` attributes.
|
||||
* - `"raw"`: Serialize `Error` objects as JSON strings with fields like
|
||||
* `name`, `message`, `stack`, etc.
|
||||
* - `false`: Treat `Error` objects like regular objects without special
|
||||
* handling.
|
||||
*/
|
||||
exceptionAttributes?: ExceptionAttributeMode;
|
||||
|
||||
/**
|
||||
* Whether to log diagnostics. Diagnostic logs are logged to
|
||||
* the `["logtape", "meta", "otel"]` category.
|
||||
* Turned off by default.
|
||||
*/
|
||||
diagnostics?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an OpenTelemetry sink with a custom logger provider.
|
||||
* When using this configuration, you are responsible for setting up the
|
||||
* logger provider with appropriate exporters and processors.
|
||||
*/
|
||||
export interface OpenTelemetrySinkProviderOptions extends OpenTelemetrySinkOptionsBase {
|
||||
/**
|
||||
* The OpenTelemetry logger provider to use.
|
||||
*/
|
||||
loggerProvider: ILoggerProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an OpenTelemetry sink with automatic exporter creation.
|
||||
*/
|
||||
export interface OpenTelemetrySinkExporterOptions extends OpenTelemetrySinkOptionsBase {
|
||||
/**
|
||||
* The OpenTelemetry logger provider to use.
|
||||
* Must be undefined or omitted when using exporter options.
|
||||
*/
|
||||
loggerProvider?: undefined;
|
||||
|
||||
/**
|
||||
* The OpenTelemetry OTLP exporter configuration to use.
|
||||
*/
|
||||
otlpExporterConfig?: OtlpHttpExporterConfig;
|
||||
|
||||
/**
|
||||
* The service name to use. If not provided, the service name is
|
||||
* taken from the `OTEL_SERVICE_NAME` environment variable.
|
||||
*/
|
||||
serviceName?: string;
|
||||
|
||||
/**
|
||||
* An additional resource to merge with the default resource.
|
||||
*/
|
||||
additionalResource?: Resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an OpenTelemetry sink.
|
||||
*
|
||||
* This is a union type that accepts either:
|
||||
* - {@link OpenTelemetrySinkProviderOptions}: Provide your own `loggerProvider`
|
||||
* (recommended for production)
|
||||
* - {@link OpenTelemetrySinkExporterOptions}: Let the sink create an exporter
|
||||
* automatically based on environment variables
|
||||
*/
|
||||
export type OpenTelemetrySinkOptions =
|
||||
| OpenTelemetrySinkProviderOptions
|
||||
| OpenTelemetrySinkExporterOptions;
|
||||
|
||||
/**
|
||||
* A no-op logger provider that returns NOOP_LOGGER for all requests.
|
||||
* Used when no OTLP endpoint is configured to avoid repeated connection errors.
|
||||
*/
|
||||
const noopLoggerProvider: ILoggerProvider = {
|
||||
getLogger: () => NOOP_LOGGER
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the logger provider asynchronously.
|
||||
* This is used when the user doesn't provide a custom logger provider.
|
||||
*
|
||||
* If no OTLP endpoint is configured (via options or environment variables),
|
||||
* returns a noop logger provider to avoid repeated connection errors.
|
||||
*
|
||||
* @param options The exporter options.
|
||||
* @returns A promise that resolves to the initialized logger provider.
|
||||
*/
|
||||
async function initializeLoggerProvider(
|
||||
options: OpenTelemetrySinkExporterOptions
|
||||
): Promise<ILoggerProvider> {
|
||||
if (!hasOtlpEndpoint(options.otlpExporterConfig)) {
|
||||
return noopLoggerProvider;
|
||||
}
|
||||
|
||||
const resource = defaultResource().merge(
|
||||
resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: options.serviceName ?? getEnvironmentVariable('OTEL_SERVICE_NAME')
|
||||
}).merge(options.additionalResource ?? null)
|
||||
);
|
||||
|
||||
const otlpExporter = new OTLPLogExporter(options.otlpExporterConfig);
|
||||
const loggerProvider = new LoggerProvider({
|
||||
resource,
|
||||
processors: [
|
||||
// @ts-ignore: compatible with sdk-logs
|
||||
new SimpleLogRecordProcessor(otlpExporter)
|
||||
]
|
||||
});
|
||||
|
||||
return loggerProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a log record to the OpenTelemetry logger.
|
||||
* @param logger The OpenTelemetry logger.
|
||||
* @param record The LogTape log record.
|
||||
* @param options The sink options.
|
||||
*/
|
||||
function emitLogRecord(
|
||||
logger: OTLogger,
|
||||
record: LogRecord,
|
||||
options: OpenTelemetrySinkOptions
|
||||
): void {
|
||||
const objectRenderer = options.objectRenderer ?? 'inspect';
|
||||
const exceptionMode = options.exceptionAttributes ?? 'semconv';
|
||||
const { category, level, message, timestamp, properties } = record;
|
||||
const severityNumber = mapLevelToSeverityNumber(level);
|
||||
const attributes = convertToAttributes(properties, objectRenderer, exceptionMode);
|
||||
|
||||
attributes['category'] = [...category];
|
||||
|
||||
logger.emit({
|
||||
severityNumber,
|
||||
severityText: level,
|
||||
body:
|
||||
typeof options.messageType === 'function'
|
||||
? convertMessageToCustomBodyFormat(
|
||||
message,
|
||||
objectRenderer,
|
||||
exceptionMode,
|
||||
options.messageType
|
||||
)
|
||||
: options.messageType === 'array'
|
||||
? convertMessageToArray(message, objectRenderer, exceptionMode)
|
||||
: convertMessageToString(message, objectRenderer, exceptionMode),
|
||||
attributes,
|
||||
timestamp: new Date(timestamp)
|
||||
} satisfies OTLogRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* An OpenTelemetry sink with async disposal and initialization tracking.
|
||||
*/
|
||||
export interface OpenTelemetrySink extends Sink, AsyncDisposable {
|
||||
/**
|
||||
* A promise that resolves when the sink's lazy initialization completes.
|
||||
* For sinks created with an explicit `loggerProvider`, this resolves
|
||||
* immediately. For sinks using automatic exporter creation, this resolves
|
||||
* once the OpenTelemetry logger provider is fully initialized.
|
||||
*/
|
||||
readonly ready: Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sink that forwards log records to OpenTelemetry.
|
||||
*
|
||||
* When a custom `loggerProvider` is provided, it is used directly.
|
||||
* Otherwise, the sink will lazily initialize a logger provider on the first
|
||||
* log record, using OTLP over HTTP/JSON.
|
||||
*
|
||||
* @param options Options for creating the sink.
|
||||
* @returns The sink.
|
||||
*/
|
||||
export function getOpenTelemetrySink(options: OpenTelemetrySinkOptions = {}): OpenTelemetrySink {
|
||||
if (options.diagnostics) {
|
||||
diag.setLogger(new DiagLoggerAdaptor(), DiagLogLevel.DEBUG);
|
||||
}
|
||||
|
||||
if (options.loggerProvider != null) {
|
||||
const loggerProvider = options.loggerProvider;
|
||||
const logger = loggerProvider.getLogger('fastgpt');
|
||||
const shutdown = loggerProvider.shutdown?.bind(loggerProvider);
|
||||
const sink: OpenTelemetrySink = Object.assign(
|
||||
(record: LogRecord) => {
|
||||
const { category } = record;
|
||||
if (category[0] === 'logtape' && category[1] === 'meta' && category[2] === 'otel') {
|
||||
return;
|
||||
}
|
||||
emitLogRecord(logger, record, options);
|
||||
},
|
||||
{
|
||||
ready: Promise.resolve(),
|
||||
async [Symbol.asyncDispose](): Promise<void> {
|
||||
if (shutdown != null) await shutdown();
|
||||
}
|
||||
}
|
||||
);
|
||||
return sink;
|
||||
}
|
||||
|
||||
let loggerProvider: ILoggerProvider | null = null;
|
||||
let logger: OTLogger | null = null;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
let initError: Error | null = null;
|
||||
let pendingRecords: LogRecord[] = [];
|
||||
|
||||
const sink: OpenTelemetrySink = Object.assign(
|
||||
(record: LogRecord) => {
|
||||
const { category } = record;
|
||||
if (category[0] === 'logtape' && category[1] === 'meta' && category[2] === 'otel') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger != null) {
|
||||
emitLogRecord(logger, record, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (initError != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRecords.push(record);
|
||||
|
||||
if (initPromise == null) {
|
||||
initPromise = initializeLoggerProvider(options)
|
||||
.then((provider) => {
|
||||
loggerProvider = provider;
|
||||
logger = provider.getLogger('fastgpt');
|
||||
for (const pendingRecord of pendingRecords) {
|
||||
emitLogRecord(logger, pendingRecord, options);
|
||||
}
|
||||
pendingRecords = [];
|
||||
})
|
||||
.catch((error) => {
|
||||
initError = error as Error;
|
||||
pendingRecords = [];
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to initialize OpenTelemetry logger:', error);
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
get ready(): Promise<void> {
|
||||
return initPromise ?? Promise.resolve();
|
||||
},
|
||||
async [Symbol.asyncDispose](): Promise<void> {
|
||||
if (initPromise != null) {
|
||||
try {
|
||||
await initPromise;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (loggerProvider?.shutdown != null) {
|
||||
await loggerProvider.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return sink;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JavaScript value to an OpenTelemetry AnyValue.
|
||||
* This function recursively handles nested objects and arrays while preserving
|
||||
* their structure according to the OpenTelemetry specification.
|
||||
*
|
||||
* @param value The value to convert
|
||||
* @param objectRenderer How to render objects that can't be converted directly
|
||||
* @param exceptionMode How to handle Error objects
|
||||
* @returns An AnyValue or null if the value should be skipped
|
||||
*/
|
||||
function convertValueToAnyValue(
|
||||
value: unknown,
|
||||
objectRenderer: ObjectRenderer,
|
||||
exceptionMode: ExceptionAttributeMode
|
||||
): AnyValue | null {
|
||||
if (value == null) return null;
|
||||
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
let primitiveType: string | null = null;
|
||||
let isHomogeneous = true;
|
||||
|
||||
for (const item of value) {
|
||||
if (item == null) continue;
|
||||
const itemType = typeof item;
|
||||
if (itemType !== 'string' && itemType !== 'number' && itemType !== 'boolean') {
|
||||
isHomogeneous = false;
|
||||
break;
|
||||
}
|
||||
if (primitiveType === null) {
|
||||
primitiveType = itemType;
|
||||
} else if (primitiveType !== itemType) {
|
||||
isHomogeneous = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHomogeneous && primitiveType !== null) {
|
||||
return value as AnyValue;
|
||||
}
|
||||
|
||||
const converted: AnyValue[] = [];
|
||||
for (const item of value) {
|
||||
const convertedItem = convertValueToAnyValue(item, objectRenderer, exceptionMode);
|
||||
if (convertedItem !== null) {
|
||||
converted.push(convertedItem);
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
if (value instanceof Error) {
|
||||
const errorObj = serializeValue(value) as Record<string, unknown>;
|
||||
const converted: Record<string, AnyValue> = {};
|
||||
for (const [key, val] of Object.entries(errorObj)) {
|
||||
const convertedVal = convertValueToAnyValue(val, objectRenderer, exceptionMode);
|
||||
if (convertedVal !== null) {
|
||||
converted[key] = convertedVal;
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
const isPlainObject = proto === Object.prototype || proto === null;
|
||||
|
||||
if (isPlainObject) {
|
||||
const converted: Record<string, AnyValue> = {};
|
||||
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
||||
const convertedVal = convertValueToAnyValue(val, objectRenderer, exceptionMode);
|
||||
if (convertedVal !== null) {
|
||||
converted[key] = convertedVal;
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
if (objectRenderer === 'inspect') {
|
||||
return nodeInspect(value);
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function convertToAttributes(
|
||||
properties: Record<string, unknown>,
|
||||
objectRenderer: ObjectRenderer,
|
||||
exceptionMode: ExceptionAttributeMode
|
||||
): Record<string, AnyValue> {
|
||||
const attributes: Record<string, AnyValue> = {};
|
||||
for (const [name, value] of Object.entries(properties)) {
|
||||
if (value == null) continue;
|
||||
|
||||
if (value instanceof Error && exceptionMode === 'semconv') {
|
||||
attributes['exception.type'] = value.name;
|
||||
attributes['exception.message'] = value.message;
|
||||
if (typeof value.stack === 'string') {
|
||||
attributes['exception.stacktrace'] = value.stack;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const convertedValue = convertValueToAnyValue(value, objectRenderer, exceptionMode);
|
||||
if (convertedValue !== null) {
|
||||
attributes[name] = convertedValue;
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function serializeValue(value: unknown): unknown {
|
||||
if (value instanceof Error) {
|
||||
const serialized: Record<string, unknown> = {
|
||||
name: value.name,
|
||||
message: value.message
|
||||
};
|
||||
|
||||
if (typeof value.stack === 'string') {
|
||||
serialized.stack = value.stack;
|
||||
}
|
||||
|
||||
const cause = (value as { cause?: unknown }).cause;
|
||||
if (cause !== undefined) {
|
||||
serialized.cause = serializeValue(cause);
|
||||
}
|
||||
|
||||
if (typeof AggregateError !== 'undefined' && value instanceof AggregateError) {
|
||||
serialized.errors = value.errors.map(serializeValue);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!(key in serialized)) {
|
||||
serialized[key] = serializeValue((value as unknown as Record<string, unknown>)[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return serialized;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(serializeValue);
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const serialized: Record<string, unknown> = {};
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
serialized[key] = serializeValue(val);
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function convertToString(
|
||||
value: unknown,
|
||||
objectRenderer: ObjectRenderer,
|
||||
exceptionMode: ExceptionAttributeMode
|
||||
): string | null | undefined {
|
||||
if (value === null || value === undefined || typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (objectRenderer === 'inspect') return nodeInspect(value);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value.toString();
|
||||
} else if (value instanceof Date) return value.toISOString();
|
||||
else if (value instanceof Error && (exceptionMode === 'raw' || exceptionMode === 'semconv')) {
|
||||
return JSON.stringify(serializeValue(value));
|
||||
} else return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function convertMessageToArray(
|
||||
message: readonly unknown[],
|
||||
objectRenderer: ObjectRenderer,
|
||||
exceptionMode: ExceptionAttributeMode
|
||||
): AnyValue {
|
||||
const body: (string | null | undefined)[] = [];
|
||||
for (let i = 0; i < message.length; i += 2) {
|
||||
const msg = message[i] as string;
|
||||
body.push(msg);
|
||||
if (message.length <= i + 1) break;
|
||||
const val = message[i + 1];
|
||||
body.push(convertToString(val, objectRenderer, exceptionMode));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function convertMessageToString(
|
||||
message: readonly unknown[],
|
||||
objectRenderer: ObjectRenderer,
|
||||
exceptionMode: ExceptionAttributeMode
|
||||
): AnyValue {
|
||||
let body = '';
|
||||
for (let i = 0; i < message.length; i += 2) {
|
||||
const msg = message[i] as string;
|
||||
body += msg;
|
||||
if (message.length <= i + 1) break;
|
||||
const val = message[i + 1];
|
||||
const extra = convertToString(val, objectRenderer, exceptionMode);
|
||||
body += extra ?? JSON.stringify(extra);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function convertMessageToCustomBodyFormat(
|
||||
message: readonly unknown[],
|
||||
objectRenderer: ObjectRenderer,
|
||||
exceptionMode: ExceptionAttributeMode,
|
||||
bodyFormatter: BodyFormatter
|
||||
): AnyValue {
|
||||
const body = message.map((msg) => convertToString(msg, objectRenderer, exceptionMode));
|
||||
return bodyFormatter(body);
|
||||
}
|
||||
|
||||
class DiagLoggerAdaptor implements DiagLogger {
|
||||
logger: Logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = getLogger(['logtape', 'meta', 'otel']);
|
||||
}
|
||||
|
||||
#escape(msg: string): string {
|
||||
return msg.replaceAll('{', '{{').replaceAll('}', '}}');
|
||||
}
|
||||
|
||||
error(msg: string, ...values: unknown[]): void {
|
||||
this.logger.error(`${this.#escape(msg)}: {values}`, { values });
|
||||
}
|
||||
|
||||
warn(msg: string, ...values: unknown[]): void {
|
||||
this.logger.warn(`${this.#escape(msg)}: {values}`, { values });
|
||||
}
|
||||
|
||||
info(msg: string, ...values: unknown[]): void {
|
||||
this.logger.info(`${this.#escape(msg)}: {values}`, { values });
|
||||
}
|
||||
|
||||
debug(msg: string, ...values: unknown[]): void {
|
||||
this.logger.debug(`${this.#escape(msg)}: {values}`, { values });
|
||||
}
|
||||
|
||||
verbose(msg: string, ...values: unknown[]): void {
|
||||
this.logger.debug(`${this.#escape(msg)}: {values}`, { values });
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { Config, LogLevel, LogRecord } from '@logtape/logtape';
|
||||
import { getConsoleSink, withFilter } from '@logtape/logtape';
|
||||
import { getPrettyFormatter } from '@logtape/pretty';
|
||||
import { getOpenTelemetrySink } from './otel';
|
||||
import dayjs from 'dayjs';
|
||||
import { mapLevelToSeverityNumber, sensitiveProperties } from './helpers';
|
||||
|
||||
type SinkId = 'console' | 'jsonl' | 'otel';
|
||||
type FilterId = string;
|
||||
type LogTapeConfig<S extends string = SinkId, F extends string = FilterId> = Config<S, F>;
|
||||
type SinkConfig = LogTapeConfig<string>['sinks'];
|
||||
|
||||
type CreateSinksOptions = {
|
||||
enableConsole: boolean;
|
||||
enableOtel: boolean;
|
||||
otelServiceName: string;
|
||||
otelUrl?: string;
|
||||
consoleLevel?: LogLevel;
|
||||
otelLevel?: LogLevel;
|
||||
};
|
||||
|
||||
type CreateSinksResult = {
|
||||
sinks: SinkConfig;
|
||||
composedSinks: SinkId[];
|
||||
};
|
||||
|
||||
export async function createSinks(options: CreateSinksOptions): Promise<CreateSinksResult> {
|
||||
const {
|
||||
enableConsole,
|
||||
enableOtel,
|
||||
otelServiceName,
|
||||
otelUrl,
|
||||
consoleLevel = 'trace',
|
||||
otelLevel = 'info'
|
||||
} = options;
|
||||
|
||||
const sinkConfig = {
|
||||
bufferSize: 8192,
|
||||
flushInterval: 5000,
|
||||
nonBlocking: true,
|
||||
lazy: true
|
||||
} as const;
|
||||
|
||||
const sinks: SinkConfig = {};
|
||||
const composedSinks: SinkId[] = [];
|
||||
|
||||
const levelFilter = (record: LogRecord, level: LogLevel) => {
|
||||
return mapLevelToSeverityNumber(record.level) >= mapLevelToSeverityNumber(level);
|
||||
};
|
||||
|
||||
if (enableConsole) {
|
||||
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: () => dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
// Full depth for nested objects (e.g. Zod errors) in console output
|
||||
inspectOptions: { depth: 5 }
|
||||
})
|
||||
}),
|
||||
(record) => levelFilter(record, consoleLevel)
|
||||
);
|
||||
composedSinks.push('console');
|
||||
console.log('✓ Logtape console sink enabled');
|
||||
}
|
||||
|
||||
if (enableOtel) {
|
||||
if (!otelUrl) {
|
||||
throw new Error('LOG_OTEL_URL is required when LOG_ENABLE_OTEL is true');
|
||||
}
|
||||
|
||||
sinks.otel = withFilter(
|
||||
getOpenTelemetrySink({
|
||||
serviceName: otelServiceName,
|
||||
otlpExporterConfig: {
|
||||
url: otelUrl
|
||||
}
|
||||
}),
|
||||
(record) => {
|
||||
const lvlCd = levelFilter(record, otelLevel);
|
||||
const spCd = sensitiveProperties.some((sp) => sp in record.properties);
|
||||
|
||||
return lvlCd && !spCd;
|
||||
}
|
||||
);
|
||||
|
||||
composedSinks.push('otel');
|
||||
console.log(`✓ Logtape OpenTelemetry URL: ${otelUrl}`);
|
||||
console.log(`✓ Logtape OpenTelemetry service name: ${otelServiceName}`);
|
||||
console.log('✓ Logtape OpenTelemetry enabled');
|
||||
}
|
||||
|
||||
return { sinks, composedSinks };
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { isIP } from 'net';
|
||||
import * as dns from 'node:dns/promises';
|
||||
import { SERVICE_LOCAL_HOST } from './tools';
|
||||
import { isDevEnv } from '@fastgpt/global/common/system/constants';
|
||||
import { isIP, isIPv6 } from 'net';
|
||||
import dns from 'dns/promises';
|
||||
|
||||
const isDevEnv = process.env.NODE_ENV === 'development';
|
||||
const SERVICE_LOCAL_PORT = `${process.env.PORT || 3000}`;
|
||||
const SERVICE_LOCAL_HOST =
|
||||
process.env.HOSTNAME && isIPv6(process.env.HOSTNAME)
|
||||
? `[${process.env.HOSTNAME}]:${SERVICE_LOCAL_PORT}`
|
||||
: `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`;
|
||||
|
||||
export const isInternalAddress = async (url: string): Promise<boolean> => {
|
||||
if (isDevEnv) return false;
|
||||
|
||||
const isInternalIPv6 = (ip: string): boolean => {
|
||||
// 移除 IPv6 地址中的方括号(如果有)
|
||||
const cleanIp = ip.replace(/^\[|\]$/g, '');
|
||||
|
||||
Reference in New Issue
Block a user