diff --git a/package.json b/package.json index ce5db96bb7..2c9bc3cdcb 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react": "^18.2.0", "react-apexcharts": "^1.4.0", "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", "react-router-dom": "^6.8.1", "react-scripts": "5.0.1", "recoil": "^0.7.6", diff --git a/src/common/Axios.ts b/src/common/Axios.ts index 777d4612fb..91f7d0dde9 100644 --- a/src/common/Axios.ts +++ b/src/common/Axios.ts @@ -5,7 +5,8 @@ import axios from 'axios'; export const AxiosConfig = axios.create({ baseURL: process.env.REACT_APP_LAMBDA_URL, - timeout: 3000, + // timeout in milliseconds; increased from 3000ms due to large number of commit data requests + timeout: 4000, headers: { 'Content-Type': 'application/json', }, diff --git a/src/common/Constant.ts b/src/common/Constant.ts index 71c472bfbc..a719fa75a1 100644 --- a/src/common/Constant.ts +++ b/src/common/Constant.ts @@ -3,7 +3,6 @@ export const USE_CASE: string[] = ['statsd', 'logs', 'disk']; export const REPORTED_METRICS: string[] = [ - 'cpu_usage', 'procstat_cpu_usage', 'procstat_memory_rss', 'procstat_memory_swap', @@ -18,8 +17,7 @@ export const TRANSACTION_PER_MINUTE: number[] = [100, 1000, 5000]; export const OWNER_REPOSITORY: string = 'aws'; export const SERVICE_NAME: string = 'AmazonCloudWatchAgent'; export const CONVERT_REPORTED_METRICS_NAME: { [metric_name: string]: string } = { - cpu_usage: 'CPU Usage', - procstat_cpu_usage: 'Procstat CPU Usage', + procstat_cpu_usage: 'CPU Usage', procstat_memory_rss: 'Memory Resource', procstat_memory_swap: 'Memory Swap', procstat_memory_vms: 'Virtual Memory', diff --git a/src/containers/PerformanceReport/data.d.ts b/src/containers/PerformanceReport/data.d.ts index 28340e41ce..34e41e0a33 100644 --- a/src/containers/PerformanceReport/data.d.ts +++ b/src/containers/PerformanceReport/data.d.ts @@ -43,11 +43,12 @@ export interface PerformanceMetricReport { }; UseCase: { S: string }; + Service: { S: string }; + UniqueID: { S: string }; } // PerformanceMetric shows all collected metrics when running performance metrics export interface PerformanceMetric { - cpu_usage?: { M: PerformanceMetricStatistic }; procstat_cpu_usage?: { M: PerformanceMetricStatistic }; procstat_memory_rss?: { M: PerformanceMetricStatistic }; procstat_memory_swap?: { M: PerformanceMetricStatistic }; @@ -72,6 +73,7 @@ export interface PerformanceMetricStatistic { export interface ServiceLatestVersion { // Release version for the service tag_name: string; + body: string; } export interface ServicePRInformation { @@ -79,6 +81,7 @@ export interface ServicePRInformation { title: string; html_url: string; number: number; + sha: string; } export interface UseCaseData { @@ -87,7 +90,6 @@ export interface UseCaseData { instance_type?: string; data: { [data_rate: string]: { - cpu_usage?: string; procstat_cpu_usage?: string; procstat_memory_rss?: string; procstat_memory_swap?: string; diff --git a/src/containers/PerformanceReport/index.tsx b/src/containers/PerformanceReport/index.tsx index 46df72942f..b185adc04f 100644 --- a/src/containers/PerformanceReport/index.tsx +++ b/src/containers/PerformanceReport/index.tsx @@ -1,19 +1,32 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT -import { CircularProgress, Container, Link, MenuItem, Paper, Select, Table, TableBody, TableCell, TableContainer, TableRow, Typography } from '@mui/material'; +import { Button, CircularProgress, Container, Link, MenuItem, Paper, Select, Table, TableBody, TableCell, TableContainer, TableRow, Typography } from '@mui/material'; import moment from 'moment'; import * as React from 'react'; import { TRANSACTION_PER_MINUTE } from '../../common/Constant'; import { usePageEffect } from '../../core/page'; import { PerformanceTable } from '../../core/table'; -import { UseCaseData } from './data'; -import { GetLatestPerformanceReports, GetServiceLatestVersion } from './service'; +import { ServicePRInformation, UseCaseData } from './data'; +import { createDefaultServicePRInformation, GetLatestPerformanceReports, GetServiceLatestVersion, GetServicePRInformation } from './service'; import { PasswordDialog } from '../../common/Dialog'; +import { SelectChangeEvent } from '@mui/material/Select'; +import ReactMarkdown from 'react-markdown'; + export default function PerformanceReport(props: { password: string; password_is_set: boolean; set_password_state: any }): JSX.Element { usePageEffect({ title: 'Amazon CloudWatch Agent' }); const { password, password_is_set, set_password_state } = props; - const [{ version, commit_date, commit_title, commit_hash, commit_url, use_cases, ami_id, collection_period }] = useStatePerformanceReport(password); + const [{ version, commit_date, commit_title, commit_hash, commit_url, use_cases, ami_id, collection_period, body }] = useStatePerformanceReport(password); const [{ data_type }, setDataTypeState] = useStateDataType(); + const [isHidden, setIsHidden] = React.useState(true); + + const handleDataTypeChange = (event: SelectChangeEvent) => { + setDataTypeState({ data_type: event.target.value }); + }; + + const toggleContent = () => { + setIsHidden(!isHidden); + }; + const selectedUseCaseData: UseCaseData[] = use_cases.filter((useCase: UseCaseData) => useCase?.data_type?.toLowerCase() === data_type.toLowerCase()); return ( @@ -56,7 +69,7 @@ export default function PerformanceReport(props: { password: string; password_is aria-label="a dense table" > - {['Version', 'Architectural', 'Collection Period', 'Testing AMI', 'Commit Hash', 'Commit Name', 'Commit Date', 'Data Type']?.map((name) => ( + {['Version', 'Architectural', 'Collection Period', 'Testing AMI', 'Commit Hash', 'Commit Name', 'Commit Date', 'Data Type', 'Release Notes']?.map((name) => ( {name === 'Version' ? ( @@ -88,24 +100,33 @@ export default function PerformanceReport(props: { password: string; password_is ) : name === 'Commit Date' ? ( {commit_date} - ) : ( - Metric Trace Logs + ) : ( +
+ + {!isHidden && {body}} +
)}
@@ -120,7 +141,7 @@ export default function PerformanceReport(props: { password: string; password_is {data_type} (TPM: {tpm}){' '} - use_case?.data_type === data_type.toLowerCase())} /> +
))} @@ -140,6 +161,7 @@ function useStatePerformanceReport(password: string) { ami_id: undefined as string | undefined, collection_period: undefined as string | undefined, error: undefined as string | undefined, + body: undefined as string | undefined, }); React.useEffect(() => { @@ -155,8 +177,9 @@ function useStatePerformanceReport(password: string) { const use_cases: UseCaseData[] = []; // We only get the latest commit ID; therefore, only use case are different; however, general metadata - // information (e.g Commit_Hash, Commit_Date of the PR) would be the same for all datas. + // information (e.g Commit_Hash, Commit_Date of the PR) would be the same for all data. const commit_hash = performance_reports.at(0)?.CommitHash.S || ''; + const commitHashes = performance_reports.map((report) => report.CommitHash?.S); const commit_date = performance_reports.at(0)?.CommitDate.N; const collection_period = performance_reports.at(0)?.CollectionPeriod.S; const ami_id = performance_reports.at(0)?.InstanceAMI.S; @@ -166,11 +189,10 @@ function useStatePerformanceReport(password: string) { name: pReport?.UseCase.S, data_type: pReport?.DataType.S, instance_type: pReport?.InstanceType.S, - data: TRANSACTION_PER_MINUTE.reduce( + data: Object.keys(pReport?.Results.M).reduce( (accu, tpm) => ({ ...accu, [tpm]: { - cpu_usage: pReport?.Results.M[tpm]?.M?.cpu_usage?.M?.Average?.N, procstat_cpu_usage: pReport?.Results.M[tpm]?.M?.procstat_cpu_usage?.M?.Average?.N, procstat_memory_rss: pReport?.Results.M[tpm]?.M?.procstat_memory_rss?.M?.Average?.N, procstat_memory_swap: pReport?.Results.M[tpm]?.M?.procstat_memory_swap?.M?.Average?.N, @@ -187,7 +209,8 @@ function useStatePerformanceReport(password: string) { ), }); } - // const commit_info = await GetServicePRInformation(password, commit_hash); + const commit_info: ServicePRInformation[] = await GetServicePRInformation(password, commitHashes); + const commit_info_finalized = commit_info.find((value) => value !== undefined) ?? createDefaultServicePRInformation(); setState((prev: any) => ({ ...prev, @@ -195,18 +218,35 @@ function useStatePerformanceReport(password: string) { ami_id: ami_id, collection_period: collection_period, use_cases: use_cases, - // commit_title: `${commit_info?.title} (#${commit_info?.number})`, - // commit_url: commit_info?.html_url, - commit_hash: commit_hash, - commit_title: `PlaceHolder`, - commit_url: `www.github.com/aws/amazon-cloudwatch-agent`, - commit_date: moment.unix(Number(commit_date)).format('dddd, MMMM Do, YYYY h:mm:ss A'), + commit_title: `${commit_info_finalized?.title} (#${commit_info_finalized?.number})`, + commit_url: commit_info_finalized?.html_url, + commit_hash: commit_info_finalized?.sha ?? commit_hash, + commit_date: formatUnixTimestamp(commit_date ?? 0), + body: service_info.body ?? 'Release notes unavailable', })); })(); }, [password, setState]); return [state, setState] as const; } +export const formatUnixTimestamp = (timestamp: string | number, format: string = 'dddd, MMMM Do, YYYY h:mm:ss A'): string => { + try { + // Handle string input + const unixTime = typeof timestamp === 'string' ? Number(timestamp) : timestamp; + + // Validate timestamp + if (!Number.isFinite(unixTime) || unixTime < 0) { + console.log('invalid unix timestamp:'); + return moment.unix(0).format(format); + } + + return moment.unix(unixTime).format(format); + } catch (error) { + console.error('Error formatting unix timestamp:', error); + return moment.unix(0).format(format); + } +}; + function useStateDataType() { const [state, setState] = React.useState({ data_type: 'Metrics' as 'Metrics' | 'Traces' | 'Logs' | string, diff --git a/src/containers/PerformanceReport/service.ts b/src/containers/PerformanceReport/service.ts index b7d450117e..132a874226 100644 --- a/src/containers/PerformanceReport/service.ts +++ b/src/containers/PerformanceReport/service.ts @@ -63,21 +63,38 @@ export async function GetServiceLatestVersion(password: string): Promise { - AxiosConfig.defaults.headers['x-api-key'] = password; - return AxiosConfig.post('/', { - Action: 'Github', - URL: 'GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls', - Params: { - owner: OWNER_REPOSITORY, - repo: process.env.REACT_APP_GITHUB_REPOSITORY, - commit_sha: commit_sha, - }, - }) - .then(function (body: { data: any[] }) { - return Promise.resolve(body.data.at(0)); - }) - .catch(function (error: unknown) { - return Promise.reject(error); +export async function GetServicePRInformation(password: string, commitHashes: string[]): Promise { + try { + AxiosConfig.defaults.headers['x-api-key'] = password; + const prInformation = commitHashes.map(async (commitHash) => { + const result = await AxiosConfig.post('/', { + Action: 'Github', + URL: 'GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls', + Params: { + owner: OWNER_REPOSITORY, + repo: process.env.REACT_APP_GITHUB_REPOSITORY, + commit_sha: commitHash, + }, + }); + if (result.data?.data?.length === undefined) { + console.log('PR Info not found for: ' + commitHash); + return undefined; + } + + return result.data?.data?.at(0); }); + + return Promise.all(prInformation); + } catch (error) { + return Promise.reject(error); + } +} + +export function createDefaultServicePRInformation(): ServicePRInformation { + return { + title: 'PR data unavailable', + html_url: 'https://github.com/aws/amazon-cloudwatch-agent/pulls', + number: 0, + sha: 'default-sha', + }; } diff --git a/src/containers/PerformanceTrend/data.d.ts b/src/containers/PerformanceTrend/data.d.ts index 3fa2836bf0..209862e9cd 100644 --- a/src/containers/PerformanceTrend/data.d.ts +++ b/src/containers/PerformanceTrend/data.d.ts @@ -77,9 +77,13 @@ export interface ServiceCommitInformation { // Release version for the service author: { login: string; - date: string; }; - commit: { message: string }; + commit: { + message: string; + author: { + date: string; + }; + }; sha: string; } diff --git a/src/containers/PerformanceTrend/index.tsx b/src/containers/PerformanceTrend/index.tsx index b5daeda7b5..546857ec3c 100644 --- a/src/containers/PerformanceTrend/index.tsx +++ b/src/containers/PerformanceTrend/index.tsx @@ -4,7 +4,6 @@ import { Box, CircularProgress, Container, MenuItem, Paper, Select, Table, Table import { SelectChangeEvent } from '@mui/material/Select'; import { useTheme } from '@mui/material/styles'; import merge from 'lodash/merge'; -import moment from 'moment'; import * as React from 'react'; import Chart from 'react-apexcharts'; import { CONVERT_REPORTED_METRICS_NAME, REPORTED_METRICS, TRANSACTION_PER_MINUTE, USE_CASE } from '../../common/Constant'; @@ -13,6 +12,7 @@ import { PerformanceTrendData, ServiceCommitInformation, TrendData } from './dat import { GetPerformanceTrendData, GetServiceCommitInformation } from './service'; import { PasswordDialog } from '../../common/Dialog'; import { BasedOptionChart } from './styles'; +import { formatUnixTimestamp } from '../PerformanceReport'; export default function PerformanceTrend(props: { password: string; password_is_set: boolean; set_password_state: any }): JSX.Element { usePageEffect({ title: 'Amazon CloudWatch Agent' }); @@ -188,20 +188,21 @@ export default function PerformanceTrend(props: { password: string; password_is_ const use_case = ctx.opts.series.at(seriesIndex)?.name; const selected_data = series[seriesIndex][dataPointIndex]; const selected_hash = w.globals.categoryLabels[dataPointIndex]; - const selected_hash_information: ServiceCommitInformation | undefined = commits_information - .filter((c: ServiceCommitInformation) => c.sha === selected_hash) - .at(0); - const commit_history = selected_hash_information?.commit.message.replace(/\n\r*\n*/g, '
'); - const commited_by = `Committed by ${selected_hash_information?.author.login} on ${selected_hash_information?.author.date}`; + const selected_hash_information: ServiceCommitInformation | undefined = commits_information.find((c: ServiceCommitInformation) => + c?.sha?.startsWith(selected_hash) + ); + const commit_sha = selected_hash_information?.sha ?? 'No commit available'; + const commit_history = selected_hash_information?.commit.message.replace(/\n\r*\n*/g, '
') ?? 'No commit information available'; + const committed_by = formatCommitInfo(selected_hash_information, { dateFormat: true }); const commit_data = `${use_case}: ${selected_data}`; return ( '
' + - selected_hash_information?.sha + + commit_sha + '
' + commit_history + '
' + - commited_by + + committed_by + '
' + `
` + `
${commit_data}
` + @@ -250,9 +251,9 @@ function useStatePerformanceTrend(password: string) { // With ScanIndexForward being set to true, the trend data are being sorted descending based on the CommitDate. // Therefore, the first data that has commit date is the latest commit. const commit_date = performances.at(0)?.CommitDate.N || ''; - const hash_categories = Array.from(new Set(performances.map((p) => p.CommitHash.S.substring(0, 7)))).reverse(); - // Get all the information for the hash categories in order to get the commiter name, the commit message, and the releveant information - const commits_information = await Promise.all(hash_categories.map((hash) => GetServiceCommitInformation(password, hash))); + const hash_categories = Array.from(new Set(performances.map((p: PerformanceTrendData) => p.CommitHash.S.substring(0, 7)))).reverse(); + // Get all the information for the hash categories in order to get the commiter name, the commit message, and the relevant information + const commits_information: ServiceCommitInformation[] = await Promise.all(hash_categories.map((hash) => GetServiceCommitInformation(password, hash))); /* Generate series of data that has the following format: data_rate: transaction per minute @@ -321,7 +322,7 @@ function useStatePerformanceTrend(password: string) { trend_data: trend_data, hash_categories: hash_categories, commits_information: commits_information, - last_update: moment.unix(Number(commit_date)).format('dddd, MMMM Do, YYYY h:mm:ss A'), + last_update: formatUnixTimestamp(commit_date ?? 0), })); })(); }, [password, setState]); @@ -344,3 +345,33 @@ function useStateSelectedMetrics() { return [state, setState] as const; } + +interface CommitFormatOptions { + dateFormat?: boolean; // Whether to format the date + fallback?: string; + includePrefix?: boolean; // Whether to include "Committed by" prefix +} + +export const formatCommitInfo = (selected_hash_information: ServiceCommitInformation | undefined, options: CommitFormatOptions = {}): string => { + const { dateFormat = false, fallback = 'No commit information available', includePrefix = true } = options; + + try { + if (!selected_hash_information?.author?.login || !selected_hash_information?.commit?.author?.date) { + return fallback; + } + + const { login } = selected_hash_information?.author; + let { date } = selected_hash_information?.commit?.author; + + // Optional date formatting + if (dateFormat) { + date = new Date(date).toLocaleDateString(); + } + + const prefix = includePrefix ? 'Committed by ' : ''; + return `${prefix}${login} on ${date}`; + } catch (error) { + console.error('Error formatting commit info:', error); + return fallback; + } +}; diff --git a/src/containers/PerformanceTrend/service.ts b/src/containers/PerformanceTrend/service.ts index aef23d7418..6c09d7a68e 100644 --- a/src/containers/PerformanceTrend/service.ts +++ b/src/containers/PerformanceTrend/service.ts @@ -47,21 +47,42 @@ async function GetPerformanceTrend(password: string, params: PerformanceTrendDat }); } -export async function GetServiceCommitInformation(password: string, commit_sha: string): Promise { - AxiosConfig.defaults.headers['x-api-key'] = password; - return AxiosConfig.post('/', { - Action: 'Github', - URL: 'GET /repos/{owner}/{repo}/commits/{ref}', - Params: { - owner: OWNER_REPOSITORY, - repo: process.env.REACT_APP_GITHUB_REPOSITORY, - ref: commit_sha, - }, - }) - .then(function (value: { data: any }) { - return Promise.resolve(value?.data); - }) - .catch(function (error: unknown) { - return Promise.reject(error); +export async function GetServiceCommitInformation(password: string, commitSha: string): Promise { + try { + AxiosConfig.defaults.headers['x-api-key'] = password; + const response = await AxiosConfig.post('/', { + Action: 'Github', + URL: 'GET /repos/{owner}/{repo}/commits/{ref}', + Params: { + owner: OWNER_REPOSITORY, + repo: process.env.REACT_APP_GITHUB_REPOSITORY, + ref: commitSha, + }, }); + + // Validate response + if (!response?.data?.data) { + return createDefaultServiceCommitInformation(); + } + + return response.data.data; + } catch (error) { + console.error('Failed to fetch commit information:', error); + throw error; // Re-throw the error for handling by the caller + } +} + +function createDefaultServiceCommitInformation(): ServiceCommitInformation { + return { + author: { + login: 'default-user', + }, + commit: { + message: 'No commit message available', + author: { + date: new Date().toISOString(), + }, + }, + sha: 'default-sha', + }; } diff --git a/src/core/table.tsx b/src/core/table.tsx index 0701207e0e..c3a26ad011 100644 --- a/src/core/table.tsx +++ b/src/core/table.tsx @@ -154,21 +154,35 @@ export function PerformanceTable(props: { use_cases: UseCaseData[]; data_rate: s - {use_cases?.map((use_case) => ( - - {use_case.name} - {use_case.instance_type} - {Number(use_case.data?.[data_rate]?.procstat_cpu_usage).toFixed(2)} - {(Number(use_case.data?.[data_rate]?.procstat_memory_rss) / Number(use_case.data?.[data_rate]?.mem_total)).toFixed(2)} - {(Number(use_case.data?.[data_rate]?.procstat_memory_swap) / Number(use_case.data?.[data_rate]?.mem_total)).toFixed(2)} - {(Number(use_case.data?.[data_rate]?.procstat_memory_data) / Number(use_case.data?.[data_rate]?.mem_total)).toFixed(2)} - {(Number(use_case.data?.[data_rate]?.procstat_memory_vms) / Number(use_case.data?.[data_rate]?.mem_total)).toFixed(2)} - {Number(use_case.data?.[data_rate]?.procstat_write_bytes).toFixed(2)} - {Number(use_case.data?.[data_rate]?.procstat_num_fds).toFixed(2)} - {Number(use_case.data?.[data_rate]?.net_bytes_sent).toFixed(2)} - {Number(use_case.data?.[data_rate]?.net_packets_sent).toFixed(2)} - - ))} + {use_cases + ?.filter((use_case) => use_case.data?.[data_rate]) + .map((use_case: UseCaseData, index) => { + const metrics = use_case.data[data_rate]; + const memTotal = Number(metrics?.mem_total); + + const getFormattedValue = (value: number | undefined, divisor?: number) => { + if (!value) { + return '0.00'; + } + return (divisor ? value / divisor : value).toFixed(2); + }; + + return ( + + {use_case.name} + {use_case.instance_type} + {getFormattedValue(Number(metrics?.procstat_cpu_usage))} + {getFormattedValue(Number(metrics?.procstat_memory_rss), memTotal)} + {getFormattedValue(Number(metrics?.procstat_memory_swap), memTotal)} + {getFormattedValue(Number(metrics?.procstat_memory_data), memTotal)} + {getFormattedValue(Number(metrics?.procstat_memory_vms), memTotal)} + {getFormattedValue(Number(metrics?.procstat_write_bytes))} + {getFormattedValue(Number(metrics?.procstat_num_fds))} + {getFormattedValue(Number(metrics?.net_bytes_sent))} + {getFormattedValue(Number(metrics?.net_packets_sent))} + + ); + })}