Commit 22348594 by Ryo Committed by GitHub

perf: add process memory metrics (#6656)

* perf: reduce trace span and metrics

* perf: add process memory metrics

* fix: translations
parent 6e6b026d
import { configureMetricsFromEnv, disposeMetrics, getMeter } from '@fastgpt-sdk/otel/metrics';
import {
configureMetricsFromEnv,
disposeMetrics as disposeOtelMetrics,
getMeter
} from '@fastgpt-sdk/otel/metrics';
import { env } from '../../env';
import { startRuntimeMetrics, stopRuntimeMetrics } from './runtime';
export async function configureMetrics() {
await configureMetricsFromEnv({
......@@ -7,6 +12,13 @@ export async function configureMetrics() {
defaultServiceName: 'fastgpt-client',
defaultMeterName: 'fastgpt-client'
});
startRuntimeMetrics();
}
export async function disposeMetrics() {
stopRuntimeMetrics();
await disposeOtelMetrics();
}
export { disposeMetrics, getMeter };
export { getMeter };
import type {
BatchObservableCallback,
Meter,
Observable,
ObservableGauge
} from '@opentelemetry/api';
import { getMeter } from '@fastgpt-sdk/otel/metrics';
type RuntimeMetricAttributes = Record<string, never>;
type RuntimeObservableSet = {
meter: Meter;
processMemoryRss: ObservableGauge<RuntimeMetricAttributes>;
processMemoryHeapUsed: ObservableGauge<RuntimeMetricAttributes>;
processMemoryHeapTotal: ObservableGauge<RuntimeMetricAttributes>;
processMemoryExternal: ObservableGauge<RuntimeMetricAttributes>;
processMemoryArrayBuffers: ObservableGauge<RuntimeMetricAttributes>;
processUptime: ObservableGauge<RuntimeMetricAttributes>;
};
const prefix = 'fastgpt.runtime.process';
let runtimeMetricsRegistered = false;
let runtimeMeter: Meter | undefined;
let runtimeObservables: Observable<RuntimeMetricAttributes>[] = [];
let runtimeMetricsCallback: BatchObservableCallback<RuntimeMetricAttributes> | undefined;
function createRuntimeObservables(): RuntimeObservableSet {
const meter = getMeter('fastgpt.runtime');
return {
meter,
processMemoryRss: meter.createObservableGauge(`${prefix}.memory.rss`, {
description: 'Resident set size memory used by the current process',
unit: 'By'
}),
processMemoryHeapUsed: meter.createObservableGauge(`${prefix}.memory.heap_used`, {
description: 'V8 heap memory currently used by the current process',
unit: 'By'
}),
processMemoryHeapTotal: meter.createObservableGauge(`${prefix}.memory.heap_total`, {
description: 'Total V8 heap memory allocated for the current process',
unit: 'By'
}),
processMemoryExternal: meter.createObservableGauge(`${prefix}.memory.external`, {
description: 'Memory used by C++ objects bound to JavaScript objects',
unit: 'By'
}),
processMemoryArrayBuffers: meter.createObservableGauge(`${prefix}.memory.array_buffers`, {
description: 'Memory allocated for ArrayBuffer and SharedArrayBuffer instances',
unit: 'By'
}),
processUptime: meter.createObservableGauge(`${prefix}.uptime`, {
description: 'Process uptime',
unit: 's'
})
};
}
export function startRuntimeMetrics() {
if (runtimeMetricsRegistered) return;
const observables = createRuntimeObservables();
runtimeMeter = observables.meter;
runtimeObservables = [
observables.processMemoryRss,
observables.processMemoryHeapUsed,
observables.processMemoryHeapTotal,
observables.processMemoryExternal,
observables.processMemoryArrayBuffers,
observables.processUptime
];
runtimeMetricsCallback = (result) => {
const memoryUsage = process.memoryUsage();
result.observe(observables.processMemoryRss, memoryUsage.rss);
result.observe(observables.processMemoryHeapUsed, memoryUsage.heapUsed);
result.observe(observables.processMemoryHeapTotal, memoryUsage.heapTotal);
result.observe(observables.processMemoryExternal, memoryUsage.external);
result.observe(observables.processMemoryArrayBuffers, memoryUsage.arrayBuffers);
result.observe(observables.processUptime, process.uptime());
};
runtimeMeter.addBatchObservableCallback(runtimeMetricsCallback, runtimeObservables);
runtimeMetricsRegistered = true;
}
export function stopRuntimeMetrics() {
if (!runtimeMetricsRegistered || !runtimeMetricsCallback || !runtimeMeter) return;
runtimeMeter.removeBatchObservableCallback(runtimeMetricsCallback, runtimeObservables);
runtimeMetricsRegistered = false;
runtimeMeter = undefined;
runtimeObservables = [];
runtimeMetricsCallback = undefined;
}
......@@ -13,6 +13,37 @@ export type NextApiHandler<T = any> = (
res: NextApiResponse<T>
) => unknown | Promise<unknown>;
function isIdLikeRouteSegment(segment: string) {
return (
/^\d{4,}$/.test(segment) ||
/^[0-9a-f]{24}$/i.test(segment) ||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(segment) ||
/^[A-Za-z0-9_-]{16,}$/.test(segment)
);
}
function normalizeRouteSegment(segment: string) {
return isIdLikeRouteSegment(segment) ? ':id' : segment;
}
function parseHeaderNumber(value: string | string[] | undefined) {
const normalized = Array.isArray(value) ? value[0] : value;
if (!normalized) return undefined;
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : undefined;
}
function getRequestRoute(url: string) {
const [route = '/'] = url.split('?');
if (!route || route === '/') return '/';
return route
.split('/')
.map((segment) => normalizeRouteSegment(segment))
.join('/');
}
export const NextEntry = ({
beforeCallback = []
}: {
......@@ -28,23 +59,22 @@ export const NextEntry = ({
const responseLogger = getLogger(LogCategories.HTTP.RESPONSE);
const url = req.url || '';
const route = getRequestRoute(url);
const method = req.method?.toUpperCase() || '';
const ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
const userAgent = req.headers['user-agent'];
const contentLength = req.headers['content-length'];
const requestBodySize = parseHeaderNumber(contentLength);
return withContext({ requestId }, async () =>
withActiveSpan(
{
name: `http.request ${method || 'UNKNOWN'} ${url || '/'}`,
name: 'http.request',
tracerName: 'fastgpt.http',
attributes: {
'fastgpt.request.id': requestId,
'http.request.method': method,
'url.full': url,
'client.address': Array.isArray(ip) ? ip.join(',') : ip,
'user_agent.original': userAgent,
'http.request.body.size': contentLength
'http.route': route,
'http.request.body.size': requestBodySize
}
},
async (span) => {
......
......@@ -29,6 +29,19 @@ export type ActiveSpanOptions = {
attributes?: Record<string, unknown>;
};
const DEFAULT_PRODUCTION_TRACING_SAMPLE_RATIO = 0.05;
const DEFAULT_NON_PRODUCTION_TRACING_SAMPLE_RATIO = 1;
function getDefaultTracingSampleRatio() {
if (typeof env.TRACING_OTEL_SAMPLE_RATIO === 'number') {
return env.TRACING_OTEL_SAMPLE_RATIO;
}
return process.env.NODE_ENV === 'production'
? DEFAULT_PRODUCTION_TRACING_SAMPLE_RATIO
: DEFAULT_NON_PRODUCTION_TRACING_SAMPLE_RATIO;
}
function normalizeAttributes(attributes?: Record<string, unknown>) {
if (!attributes) return;
......@@ -51,7 +64,7 @@ export async function configureTracing() {
env,
defaultServiceName: 'fastgpt-client',
defaultTracerName: 'fastgpt-client',
defaultSampleRatio: env.TRACING_OTEL_SAMPLE_RATIO
defaultSampleRatio: getDefaultTracingSampleRatio()
});
}
......
......@@ -16,6 +16,14 @@ type FileTypeSelectorValue = {
customFileExtensionList?: string[];
};
const fileExtensionTypeTranslationMap = new Map<FileExtensionKeyType, string>([
['canSelectFile', 'app:upload_file_extension_type_canSelectFile'],
['canSelectImg', 'app:upload_file_extension_type_canSelectImg'],
['canSelectVideo', 'app:upload_file_extension_type_canSelectVideo'],
['canSelectAudio', 'app:upload_file_extension_type_canSelectAudio'],
['canSelectCustomFileExtension', 'app:upload_file_extension_type_canSelectCustomFileExtension']
]);
export const FileTypeSelectorPanel = ({
value,
onChange
......@@ -190,7 +198,7 @@ export const FileTypeSelectorPanel = ({
onChange={(e) => handleTypeChange(type as FileExtensionKeyType, e.target.checked)}
>
<Box color={'myGray.900'} lineHeight={1}>
{t(`app:upload_file_extension_type_${type}`)}
{t(fileExtensionTypeTranslationMap.get(type as FileExtensionKeyType) || type)}
</Box>
<Box mt={1} fontSize={'xs'} color={'myGray.500'} wordBreak={'break-word'} w="full">
{exts.map((ext) => ext.slice(1)).join('/')}
......
......@@ -466,6 +466,10 @@
"upload_file_extension_type_canSelectCustomFileExtension": "Custom file extension type",
"upload_file_extension_type_canSelectCustomFileExtension_placeholder": "file extension name",
"upload_file_extension_types": "Supported file types",
"upload_file_extension_type_canSelectAudio": "Audio",
"upload_file_extension_type_canSelectFile": "Document",
"upload_file_extension_type_canSelectImg": "Image",
"upload_file_extension_type_canSelectVideo": "Video",
"upload_file_max_amount": "Maximum File Quantity",
"upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation",
"upload_method": "Upload method",
......
......@@ -468,6 +468,10 @@
"upload_file_extension_types": "支持上传的类型",
"upload_file_max_amount": "最大文件数量",
"upload_file_max_amount_tip": "单轮对话中最大上传文件数量",
"upload_file_extension_type_canSelectAudio": "音频",
"upload_file_extension_type_canSelectFile": "文档",
"upload_file_extension_type_canSelectImg": "图片",
"upload_file_extension_type_canSelectVideo": "视频",
"upload_method": "上传方式",
"url_upload": "文件链接",
"use_agent_sandbox": "虚拟机",
......
......@@ -452,6 +452,10 @@
"upload_file_extension_type_canSelectCustomFileExtension": "自定義文件擴展類型",
"upload_file_extension_type_canSelectCustomFileExtension_placeholder": "文件擴展名",
"upload_file_extension_types": "支持上傳的類型",
"upload_file_extension_type_canSelectAudio": "音頻",
"upload_file_extension_type_canSelectFile": "文檔",
"upload_file_extension_type_canSelectImg": "圖片",
"upload_file_extension_type_canSelectVideo": "視頻",
"upload_file_max_amount": "最大檔案數量",
"upload_file_max_amount_tip": "單輪對話中最大上傳檔案數量",
"upload_method": "上傳方式",
......
......@@ -6,6 +6,8 @@ import withRspack from 'next-rspack';
const withBundleAnalyzer = withBundleAnalyzerInit({ enabled: process.env.ANALYZE === 'true' });
const isDev = process.env.NODE_ENV === 'development';
const isWebpack = process.env.WEBPACK === '1';
const isRspack = isDev && !isWebpack;
const nextConfig: NextConfig = {
basePath: process.env.NEXT_PUBLIC_BASE_URL,
......@@ -218,8 +220,5 @@ const nextConfig: NextConfig = {
}
};
const configWithPluginsExceptWithRspack = withBundleAnalyzer(nextConfig);
export default isDev
? withRspack(configWithPluginsExceptWithRspack)
: configWithPluginsExceptWithRspack;
const config = withBundleAnalyzer(nextConfig);
export default isRspack ? withRspack(config) : config;
......@@ -4,6 +4,7 @@
"private": false,
"scripts": {
"dev": "NODE_OPTIONS='--max-old-space-size=8192' npm run build:workers && next dev",
"dev:webpack": "NODE_OPTIONS='--max-old-space-size=8192' npm run build:workers && WEBPACK=1 next dev --webpack",
"build": "npm run build:workers && next build --debug --webpack",
"start": "next start",
"build:workers": "npx tsx scripts/build-workers.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