RockteMQ-AI commented on code in PR #3028: URL: https://github.com/apache/rocketmq-dashboard/pull/3028#discussion_r3920243823
########## web/src/utils/clientConnectionDiagnostics.ts: ########## @@ -0,0 +1,607 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ClientConnection } from '../api/connections'; + +export type ClientConnectionHealthStatus = 'healthy' | 'warning' | 'critical'; +export type ClientConnectionIssueSeverity = + Exclude<ClientConnectionHealthStatus, 'healthy'> | 'info'; + +export type ClientConnectionIssueCode = + | 'NO_CONNECTIONS' + | 'PARTIAL_CONNECTION_SCAN' + | 'CLIENT_ID_COLLISION' + | 'EXACT_DUPLICATE_CONNECTION' + | 'MIXED_PROTOCOL_RESOURCE' + | 'MIXED_VERSION_RESOURCE' + | 'SINGLE_CONSUMER_INSTANCE' + | 'ADDRESS_CONCENTRATION' + | 'UNKNOWN_PROTOCOL' + | 'UNKNOWN_LANGUAGE' + | 'UNKNOWN_VERSION' + | 'INVALID_CONNECTION_TIME'; + +export interface ClientConnectionIssue { + id: string; + code: ClientConnectionIssueCode; + severity: ClientConnectionIssueSeverity; + title: string; + description: string; + resource?: string; + clientId?: string; + evidence: string[]; + recommendation: string; +} + +export interface ClientResourceSummary { + id: string; + type: string; + resource: string; + connectionCount: number; + uniqueClientCount: number; + uniqueAddressCount: number; + protocols: string[]; + languages: string[]; + versions: string[]; + partial: boolean; + status: ClientConnectionHealthStatus; + issueCount: number; +} + +export interface ClientConnectionHealthSummary { + totalConnections: number; + uniqueClientCount: number; + uniqueAddressCount: number; + resourceCount: number; + partialConnectionCount: number; + mixedProtocolResourceCount: number; + mixedVersionResourceCount: number; + singleConsumerGroupCount: number; + concentratedAddressCount: number; +} + +export interface ClientConnectionDiagnostics { + status: ClientConnectionHealthStatus; + statusText: string; + statusColor: 'success' | 'warning' | 'error'; + score: number; + summary: ClientConnectionHealthSummary; + resources: ClientResourceSummary[]; + issues: ClientConnectionIssue[]; + recommendations: string[]; +} + +type ConnectionGroup = { + type: string; + resource: string; + connections: ClientConnection[]; +}; + +const STATUS_TEXT: Record<ClientConnectionHealthStatus, string> = { + healthy: '客户端连接健康', + warning: '客户端连接需要关注', + critical: '客户端连接存在高风险', +}; + +const STATUS_COLOR: Record<ClientConnectionHealthStatus, 'success' | 'warning' | 'error'> = { + healthy: 'success', + warning: 'warning', + critical: 'error', +}; + +const KNOWN_PROTOCOLS = new Set(['gRPC', 'Remoting']); +const KNOWN_LANGUAGES = new Set(['Java', 'Go', 'Python', 'Rust', 'Cpp', 'CSharp', 'NodeJS', 'PHP']); + +const normalizeText = (value?: string | null, fallback = 'unknown'): string => { + const trimmed = (value ?? '').trim(); + return trimmed || fallback; +}; + +const uniqueSorted = (values: string[]): string[] => + [...new Set(values.map((value) => normalizeText(value)).filter(Boolean))].sort((a, b) => + a.localeCompare(b), + ); + +const countBy = (values: string[]): Map<string, number> => { + const counts = new Map<string, number>(); + values.forEach((value) => { + const normalized = normalizeText(value); + counts.set(normalized, (counts.get(normalized) ?? 0) + 1); + }); + return counts; +}; + +const issue = ( + code: ClientConnectionIssueCode, + severity: ClientConnectionIssueSeverity, + title: string, + description: string, + recommendation: string, + options: { + resource?: string; + clientId?: string; + evidence?: string[]; + id?: string; + } = {}, +): ClientConnectionIssue => ({ + id: + options.id ?? + [options.resource, options.clientId, code, ...(options.evidence ?? [])] + .filter(Boolean) + .join(':'), + code, + severity, + title, + description, + resource: options.resource, + clientId: options.clientId, + evidence: options.evidence ?? [], + recommendation, +}); + +const connectionIdentity = (connection: ClientConnection): string => + [ + normalizeText(connection.type), + normalizeText(connection.clientId), + normalizeText(connection.groupOrTopic), + normalizeText(connection.address), + ].join('|'); + +const resourceKey = (connection: ClientConnection): string => + `${normalizeText(connection.type)}:${normalizeText(connection.groupOrTopic)}`; + +const groupConnections = (connections: ClientConnection[]): ConnectionGroup[] => { + const groups = new Map<string, ConnectionGroup>(); + + connections.forEach((connection) => { + const key = resourceKey(connection); + const group = groups.get(key); + if (group) { + group.connections.push(connection); + return; Review Comment: **[i18n gap]** The issue `title`, `description`, and `recommendation` strings in this file are hardcoded in Chinese. The UI labels in `translations.ts` are properly bilingual, but the diagnostic *content* will always render in Chinese regardless of the user locale. Suggestion: either (a) add these strings to `translations.ts` and pass the `t()` function into `analyzeClientConnections`, or (b) store i18n keys (e.g. `titleKey: 'diagnostics.clientIdCollision'`) in the issue objects and resolve them at render time in the component. ########## web/src/utils/clientConnectionDiagnostics.ts: ########## @@ -0,0 +1,607 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ClientConnection } from '../api/connections'; + +export type ClientConnectionHealthStatus = 'healthy' | 'warning' | 'critical'; +export type ClientConnectionIssueSeverity = + Exclude<ClientConnectionHealthStatus, 'healthy'> | 'info'; + +export type ClientConnectionIssueCode = + | 'NO_CONNECTIONS' + | 'PARTIAL_CONNECTION_SCAN' + | 'CLIENT_ID_COLLISION' + | 'EXACT_DUPLICATE_CONNECTION' + | 'MIXED_PROTOCOL_RESOURCE' + | 'MIXED_VERSION_RESOURCE' + | 'SINGLE_CONSUMER_INSTANCE' + | 'ADDRESS_CONCENTRATION' + | 'UNKNOWN_PROTOCOL' + | 'UNKNOWN_LANGUAGE' + | 'UNKNOWN_VERSION' + | 'INVALID_CONNECTION_TIME'; + +export interface ClientConnectionIssue { + id: string; + code: ClientConnectionIssueCode; + severity: ClientConnectionIssueSeverity; + title: string; + description: string; + resource?: string; + clientId?: string; + evidence: string[]; + recommendation: string; +} + +export interface ClientResourceSummary { + id: string; + type: string; + resource: string; + connectionCount: number; + uniqueClientCount: number; + uniqueAddressCount: number; + protocols: string[]; + languages: string[]; + versions: string[]; + partial: boolean; + status: ClientConnectionHealthStatus; + issueCount: number; +} + +export interface ClientConnectionHealthSummary { + totalConnections: number; + uniqueClientCount: number; + uniqueAddressCount: number; + resourceCount: number; + partialConnectionCount: number; + mixedProtocolResourceCount: number; + mixedVersionResourceCount: number; + singleConsumerGroupCount: number; + concentratedAddressCount: number; +} + +export interface ClientConnectionDiagnostics { + status: ClientConnectionHealthStatus; + statusText: string; + statusColor: 'success' | 'warning' | 'error'; + score: number; + summary: ClientConnectionHealthSummary; + resources: ClientResourceSummary[]; + issues: ClientConnectionIssue[]; + recommendations: string[]; +} + +type ConnectionGroup = { + type: string; + resource: string; + connections: ClientConnection[]; +}; + +const STATUS_TEXT: Record<ClientConnectionHealthStatus, string> = { Review Comment: **[Minor: KNOWN_LANGUAGES coverage]** The set covers Java/Go/Python/C++/C#/Rust/PHP but misses languages that appear in the RocketMQ ecosystem (e.g. Kotlin, Swift, Dart). Since unknown languages are flagged as `UNKNOWN_LANGUAGE` issues, consider expanding this set or making it configurable to avoid false positives for less common but valid SDK languages. ########## web/src/utils/clientConnectionDiagnostics.ts: ########## @@ -0,0 +1,607 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ClientConnection } from '../api/connections'; + +export type ClientConnectionHealthStatus = 'healthy' | 'warning' | 'critical'; +export type ClientConnectionIssueSeverity = + Exclude<ClientConnectionHealthStatus, 'healthy'> | 'info'; + +export type ClientConnectionIssueCode = + | 'NO_CONNECTIONS' + | 'PARTIAL_CONNECTION_SCAN' + | 'CLIENT_ID_COLLISION' + | 'EXACT_DUPLICATE_CONNECTION' + | 'MIXED_PROTOCOL_RESOURCE' + | 'MIXED_VERSION_RESOURCE' + | 'SINGLE_CONSUMER_INSTANCE' + | 'ADDRESS_CONCENTRATION' + | 'UNKNOWN_PROTOCOL' + | 'UNKNOWN_LANGUAGE' + | 'UNKNOWN_VERSION' + | 'INVALID_CONNECTION_TIME'; + +export interface ClientConnectionIssue { + id: string; + code: ClientConnectionIssueCode; + severity: ClientConnectionIssueSeverity; + title: string; + description: string; + resource?: string; + clientId?: string; + evidence: string[]; + recommendation: string; +} + +export interface ClientResourceSummary { + id: string; + type: string; + resource: string; + connectionCount: number; + uniqueClientCount: number; + uniqueAddressCount: number; + protocols: string[]; + languages: string[]; + versions: string[]; + partial: boolean; + status: ClientConnectionHealthStatus; + issueCount: number; +} + +export interface ClientConnectionHealthSummary { + totalConnections: number; + uniqueClientCount: number; + uniqueAddressCount: number; + resourceCount: number; + partialConnectionCount: number; + mixedProtocolResourceCount: number; + mixedVersionResourceCount: number; + singleConsumerGroupCount: number; + concentratedAddressCount: number; +} + +export interface ClientConnectionDiagnostics { + status: ClientConnectionHealthStatus; + statusText: string; + statusColor: 'success' | 'warning' | 'error'; + score: number; + summary: ClientConnectionHealthSummary; + resources: ClientResourceSummary[]; + issues: ClientConnectionIssue[]; + recommendations: string[]; +} + +type ConnectionGroup = { + type: string; + resource: string; + connections: ClientConnection[]; +}; + +const STATUS_TEXT: Record<ClientConnectionHealthStatus, string> = { + healthy: '客户端连接健康', + warning: '客户端连接需要关注', + critical: '客户端连接存在高风险', +}; + +const STATUS_COLOR: Record<ClientConnectionHealthStatus, 'success' | 'warning' | 'error'> = { + healthy: 'success', + warning: 'warning', + critical: 'error', +}; + +const KNOWN_PROTOCOLS = new Set(['gRPC', 'Remoting']); +const KNOWN_LANGUAGES = new Set(['Java', 'Go', 'Python', 'Rust', 'Cpp', 'CSharp', 'NodeJS', 'PHP']); + +const normalizeText = (value?: string | null, fallback = 'unknown'): string => { + const trimmed = (value ?? '').trim(); + return trimmed || fallback; +}; + +const uniqueSorted = (values: string[]): string[] => + [...new Set(values.map((value) => normalizeText(value)).filter(Boolean))].sort((a, b) => + a.localeCompare(b), + ); + +const countBy = (values: string[]): Map<string, number> => { + const counts = new Map<string, number>(); + values.forEach((value) => { + const normalized = normalizeText(value); + counts.set(normalized, (counts.get(normalized) ?? 0) + 1); + }); + return counts; +}; + +const issue = ( + code: ClientConnectionIssueCode, + severity: ClientConnectionIssueSeverity, + title: string, + description: string, + recommendation: string, + options: { + resource?: string; + clientId?: string; + evidence?: string[]; + id?: string; + } = {}, +): ClientConnectionIssue => ({ + id: + options.id ?? + [options.resource, options.clientId, code, ...(options.evidence ?? [])] + .filter(Boolean) + .join(':'), + code, + severity, + title, + description, + resource: options.resource, + clientId: options.clientId, + evidence: options.evidence ?? [], + recommendation, +}); + +const connectionIdentity = (connection: ClientConnection): string => + [ + normalizeText(connection.type), + normalizeText(connection.clientId), + normalizeText(connection.groupOrTopic), + normalizeText(connection.address), + ].join('|'); + +const resourceKey = (connection: ClientConnection): string => + `${normalizeText(connection.type)}:${normalizeText(connection.groupOrTopic)}`; + +const groupConnections = (connections: ClientConnection[]): ConnectionGroup[] => { + const groups = new Map<string, ConnectionGroup>(); + + connections.forEach((connection) => { + const key = resourceKey(connection); + const group = groups.get(key); + if (group) { + group.connections.push(connection); + return; + } + groups.set(key, { + type: normalizeText(connection.type), + resource: normalizeText(connection.groupOrTopic), + connections: [connection], + }); + }); + + return [...groups.values()].sort( + (left, right) => + left.type.localeCompare(right.type) || left.resource.localeCompare(right.resource), + ); +}; + +const parseTime = (value?: string | null): number | null => { + if (!value) return null; + const normalized = value.includes('T') ? value : value.replace(' ', 'T'); + const timestamp = Date.parse(normalized); + return Number.isFinite(timestamp) ? timestamp : null; +}; + +const resourceSeverity = (issues: ClientConnectionIssue[]): ClientConnectionHealthStatus => { + if (issues.some((item) => item.severity === 'critical')) return 'critical'; + if (issues.some((item) => item.severity === 'warning')) return 'warning'; + return 'healthy'; +}; + +const addInventoryIssues = (connections: ClientConnection[], issues: ClientConnectionIssue[]) => { + if (connections.length === 0) { + issues.push( + issue( + 'NO_CONNECTIONS', + 'critical', + '未发现客户端连接', + '当前 NameServer 查询没有返回任何 Producer 或 Consumer 连接。', + '确认目标 NameServer、Proxy 和 Broker 侧客户端注册链路是否正常。', + ), + ); + return; + } + + const partialCount = connections.filter((connection) => connection.partial).length; + if (partialCount > 0) { + issues.push( + issue( + 'PARTIAL_CONNECTION_SCAN', + 'warning', + '客户端扫描结果不完整', + '部分 Producer 连接来自受限 Topic 扫描,当前列表可能不是完整客户端清单。', + '缩小 Topic 或集群范围后重新查询,并在排障时避免把当前列表视为全集。', + { evidence: [`partial=${partialCount}`] }, + ), + ); + } +}; + +const addClientIdIssues = (connections: ClientConnection[], issues: ClientConnectionIssue[]) => { + const connectionsByClient = new Map<string, ClientConnection[]>(); + const identityCounts = new Map<string, number>(); + + connections.forEach((connection) => { + const clientId = normalizeText(connection.clientId); + const existing = connectionsByClient.get(clientId) ?? []; + existing.push(connection); + connectionsByClient.set(clientId, existing); + + const identity = connectionIdentity(connection); + identityCounts.set(identity, (identityCounts.get(identity) ?? 0) + 1); + }); + + connectionsByClient.forEach((clientConnections, clientId) => { + const addresses = uniqueSorted(clientConnections.map((connection) => connection.address)); + if (addresses.length > 1) { + issues.push( + issue( + 'CLIENT_ID_COLLISION', + 'critical', + 'Client ID 连接到多个地址', + '同一个 Client ID 同时出现在多个远端地址,可能是实例 ID 配置冲突或旧连接未及时清理。', + '检查客户端 instanceName/clientId 配置,确保同一进程实例使用唯一标识。', + { + clientId, + evidence: addresses, + }, + ), + ); + } + }); + + identityCounts.forEach((count, identity) => { + if (count <= 1) return; + const [, clientId, resource, address] = identity.split('|'); + issues.push( + issue( + 'EXACT_DUPLICATE_CONNECTION', + 'info', + '连接记录重复', + '相同客户端、资源和地址出现了重复记录,可能来自采集侧合并或上游返回重复项。', + '刷新连接清单;若重复持续存在,检查客户端连接采集路径是否重复汇总。', + { + clientId, + resource, + evidence: [`address=${address}`, `count=${count}`], + id: `${identity}:EXACT_DUPLICATE_CONNECTION`, Review Comment: **[Duplicate issue code]** The same `MIXED_VERSION_RESOURCE` code is emitted twice for the same resource: once as `warning` when `versions.length > 1`, and again as `info` when `languages.length > 1 && versions.length > 1`. A single resource can produce two issues with the same `code` but different severities, which may confuse downstream consumers (filtering, deduplication, counting). Consider using a distinct code like `MIXED_LANGUAGE_VERSION_RESOURCE` for the second case, or merging them into a single issue with combined evidence. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
