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:
Archer
2026-03-16 17:09:25 +08:00
committed by GitHub
parent 21b3f8549a
commit aaa7d17ef1
258 changed files with 6844 additions and 6162 deletions
+1
View File
@@ -27,6 +27,7 @@ export enum QueueNames {
datasetSync = 'datasetSync',
evaluation = 'evaluation',
s3FileDelete = 's3FileDelete',
collectionUpdate = 'collectionUpdate',
// Delete Queue
datasetDelete = 'datasetDelete',
+2 -1
View File
@@ -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'],
+8 -79
View File
@@ -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 };
-22
View File
@@ -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 -1
View File
@@ -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';
-77
View File
@@ -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;
}
-645
View File
@@ -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 });
}
}
-108
View File
@@ -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 };
}
+9 -6
View File
@@ -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, '');