feat: added healthcheck and attribute checklist component for Kafka (#6371)

* feat: added healthcheck and attribute checklist component for Kafka

* feat: corrected the onboardingapi payload

* feat: added missing configuration button at overview and onboarding flow
This commit is contained in:
SagarRajput-7 2024-11-06 14:23:51 +05:30 committed by GitHub
parent 352296c6cd
commit 7086470ce2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 603 additions and 41 deletions

View File

@ -16,11 +16,13 @@ export interface OnboardingStatusResponse {
const getOnboardingStatus = async (props: {
start: number;
end: number;
endpointService?: string;
}): Promise<SuccessResponse<OnboardingStatusResponse> | ErrorResponse> => {
const { endpointService, ...rest } = props;
try {
const response = await ApiBaseInstance.post(
'/messaging-queues/kafka/onboarding/consumers',
props,
`/messaging-queues/kafka/onboarding/${endpointService || 'consumers'}`,
rest,
);
return {

View File

@ -14,6 +14,7 @@ import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import MessagingQueueHealthCheck from 'pages/MessagingQueues/MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import { getAttributeDataFromOnboardingStatus } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@ -33,6 +34,9 @@ export default function ConnectionStatus(): JSX.Element {
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const {
serviceName,
@ -74,10 +78,14 @@ export default function ConnectionStatus(): JSX.Element {
data: onbData,
error: onbErr,
isFetching: onbFetching,
} = useOnboardingStatus({
} = useOnboardingStatus(
{
enabled: getStartedSource === 'kafka',
refetchInterval: pollInterval,
});
},
getStartedSourceService || '',
'query-key-onboarding-status',
);
const [
shouldRetryOnboardingCall,
@ -326,18 +334,30 @@ export default function ConnectionStatus(): JSX.Element {
<div className="status">
{isQueryServiceLoading && <LoadingOutlined />}
{!isQueryServiceLoading && isReceivingData && (
{!isQueryServiceLoading &&
isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span>
</>
)}
{!isQueryServiceLoading && !isReceivingData && (
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
{!isQueryServiceLoading &&
!isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span>
</>
)}
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
</div>
</div>
<div className="details-info">

View File

@ -9,7 +9,10 @@ import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
import {
ModulesMap,
useCases,
} from 'container/OnboardingContainer/OnboardingContainer';
import {
getDataSources,
getSupportedFrameworks,
@ -49,6 +52,9 @@ export default function DataSource(): JSX.Element {
updateSelectedFramework,
} = useOnboardingContext();
const isKafkaAPM =
getStartedSource === 'kafka' && selectedModule?.id === ModulesMap.APM;
const [supportedDataSources, setSupportedDataSources] = useState<
DataSourceType[]
>([]);
@ -155,14 +161,14 @@ export default function DataSource(): JSX.Element {
className={cx(
'supported-language',
selectedDataSource?.name === dataSource.name ? 'selected' : '',
getStartedSource === 'kafka' &&
isKafkaAPM &&
!messagingQueueKakfaSupportedDataSources.includes(dataSource?.id || '')
? 'disabled'
: '',
)}
key={dataSource.name}
onClick={(): void => {
if (getStartedSource !== 'kafka') {
if (!isKafkaAPM) {
updateSelectedFramework(null);
updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource);

View File

@ -252,7 +252,7 @@ import APM_java_springBoot_docker_recommendedSteps_runApplication from '../Modul
import APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-installOtelCollector.md';
import APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-instrumentApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producer from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-producer.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-producers.md';
// SpringBoot-LinuxAMD64-quickstart
import APM_java_springBoot_linuxAMD64_quickStart_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-instrumentApplication.md';
import APM_java_springBoot_linuxAMD64_quickStart_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-runApplication.md';
@ -1054,7 +1054,7 @@ export const ApmDocFilePaths = {
APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector,
APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producer,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers,
// SpringBoot-LinuxAMD64-recommended
APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector,

View File

@ -8,15 +8,22 @@ type UseOnboardingStatus = (
options?: UseQueryOptions<
SuccessResponse<OnboardingStatusResponse> | ErrorResponse
>,
endpointService?: string,
queryKey?: string,
) => UseQueryResult<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>;
export const useOnboardingStatus: UseOnboardingStatus = (options) =>
export const useOnboardingStatus: UseOnboardingStatus = (
options,
endpointService,
queryKey,
) =>
useQuery<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>({
queryKey: ['onboardingStatus'],
queryKey: [queryKey || `onboardingStatus-${endpointService}`],
queryFn: () =>
getOnboardingStatus({
start: (Date.now() - 15 * 60 * 1000) * 1_000_000,
end: Date.now() * 1_000_000,
endpointService,
}),
...options,
});

View File

@ -0,0 +1,206 @@
import './MessagingQueueHealthCheck.styles.scss';
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
import {
Modal,
Select,
Spin,
Tooltip,
Tree,
TreeDataNode,
Typography,
} from 'antd';
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { Bolt, Check, OctagonAlert, X } from 'lucide-react';
import { ReactNode, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
interface AttributeCheckListProps {
visible: boolean;
onClose: () => void;
onboardingStatusResponses: {
title: string;
data: OnboardingStatusResponse['data'];
errorMsg?: string;
}[];
loading: boolean;
}
export enum AttributesFilters {
ALL = 'all',
SUCCESS = 'success',
ERROR = 'error',
}
function ErrorTitleAndKey({
title,
errorMsg,
isLeaf,
}: {
title: string;
errorMsg?: string;
isLeaf?: boolean;
}): TreeDataNode {
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-error-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
<Tooltip title={errorMsg}>
<div className="attribute-error-warning">
<OctagonAlert size={14} />
Fix
</div>
</Tooltip>
</div>
),
isLeaf,
};
}
function AttributeLabels({ title }: { title: ReactNode }): JSX.Element {
return (
<div className="attribute-label">
<Bolt size={14} />
{title}
</div>
);
}
function treeTitleAndKey({
title,
isLeaf,
}: {
title: string;
isLeaf?: boolean;
}): TreeDataNode {
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-success-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
{isLeaf && (
<div className="success-attribute-icon">
<Tooltip title="Success">
<Check size={14} />
</Tooltip>
</div>
)}
</div>
),
isLeaf,
};
}
function generateTreeDataNodes(
response: OnboardingStatusResponse['data'],
): TreeDataNode[] {
return response
.map((item) => {
if (item.attribute) {
if (item.status === '1') {
return treeTitleAndKey({ title: item.attribute, isLeaf: true });
}
if (item.status === '0') {
return ErrorTitleAndKey({
title: item.attribute,
errorMsg: item.error_message || '',
});
}
}
return null;
})
.filter(Boolean) as TreeDataNode[];
}
function AttributeCheckList({
visible,
onClose,
onboardingStatusResponses,
loading,
}: AttributeCheckListProps): JSX.Element {
const [filter, setFilter] = useState<AttributesFilters>(AttributesFilters.ALL);
const [treeData, setTreeData] = useState<TreeDataNode[]>([]);
const handleFilterChange = (value: AttributesFilters): void => {
setFilter(value);
};
useEffect(() => {
const filteredData = onboardingStatusResponses.map((response) => {
if (response.errorMsg) {
return ErrorTitleAndKey({
title: response.title,
errorMsg: response.errorMsg,
isLeaf: true,
});
}
let filteredData = response.data;
if (filter === AttributesFilters.SUCCESS) {
filteredData = response.data.filter((item) => item.status === '1');
} else if (filter === AttributesFilters.ERROR) {
filteredData = response.data.filter((item) => item.status === '0');
}
return {
...treeTitleAndKey({ title: response.title }),
children: generateTreeDataNodes(filteredData),
};
});
setTreeData(filteredData);
}, [filter, onboardingStatusResponses]);
return (
<Modal
title="Kafka Service Attributes"
open={visible}
onCancel={onClose}
footer={false}
className="mq-health-check-modal"
closeIcon={<X size={14} />}
>
{loading ? (
<div className="loader-container">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</div>
) : (
<div className="modal-content">
<Select
defaultValue={AttributesFilters.ALL}
className="attribute-select"
onChange={handleFilterChange}
options={[
{
value: AttributesFilters.ALL,
label: AttributeLabels({ title: 'Attributes: All' }),
},
{
value: AttributesFilters.SUCCESS,
label: AttributeLabels({ title: 'Attributes: Success' }),
},
{
value: AttributesFilters.ERROR,
label: AttributeLabels({ title: 'Attributes: Error' }),
},
]}
/>
<Tree
showLine
switcherIcon={<CaretDownOutlined />}
treeData={treeData}
height={450}
className="attribute-tree"
/>
</div>
)}
</Modal>
);
}
export default AttributeCheckList;

View File

@ -0,0 +1,165 @@
.mq-health-check-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-close {
margin-top: 4px;
}
.ant-modal-header {
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
margin-bottom: 16px;
padding-bottom: 4px;
.ant-modal-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.52px;
}
}
.modal-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: var(--bg-ink-300);
.attribute-select {
align-items: center;
display: flex;
gap: 8px;
width: 170px;
.ant-select-selector {
display: flex;
height: 28px !important;
padding: 8px;
align-items: center;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
.attribute-tree {
padding: 8px;
}
.tree-text {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
width: 328px;
}
.ant-tree {
.ant-tree-title {
.attribute-error-title {
display: flex;
align-items: center;
color: var(--bg-amber-400);
height: 24px;
.tree-text {
color: var(--bg-amber-400);
}
.attribute-error-warning {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.attribute-success-title {
display: flex;
align-items: center;
height: 24px;
.success-attribute-icon {
width: 44px;
color: var(--bg-vanilla-400);
display: flex;
> svg {
margin-left: auto;
}
}
}
}
.ant-tree-treenode {
width: 100%;
.ant-tree-node-content-wrapper {
width: 100%;
max-width: 380px;
}
}
}
}
.loader-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 8px;
background: var(--bg-ink-300);
height: 156px;
}
}
}
.attribute-label {
display: flex;
align-items: center;
gap: 8px;
}
.config-btn {
display: flex;
align-items: center;
height: 28px;
border-radius: 2px;
border: none;
box-shadow: none;
background: var(--bg-slate-500);
&.missing-config-btn {
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
&:hover {
color: var(--bg-amber-300) !important;
}
}
.config-btn-content {
display: flex;
align-items: center;
margin-right: 8px;
border-right: 1px solid rgba(255, 215, 120, 0.1);
padding-right: 8px;
}
}

View File

@ -0,0 +1,133 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './MessagingQueueHealthCheck.styles.scss';
import { Button } from 'antd';
import cx from 'classnames';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { Bolt, FolderTree } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { MessagingQueueHealthCheckService } from '../MessagingQueuesUtils';
import AttributeCheckList from './AttributeCheckList';
interface MessagingQueueHealthCheckProps {
serviceToInclude: string[];
}
function MessagingQueueHealthCheck({
serviceToInclude,
}: MessagingQueueHealthCheckProps): JSX.Element {
const [loading, setLoading] = useState(false);
const [checkListOpen, setCheckListOpen] = useState(false);
// Consumer Data
const {
data: consumerData,
error: consumerError,
isFetching: consumerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Consumers,
).length,
},
MessagingQueueHealthCheckService.Consumers,
);
// Producer Data
const {
data: producerData,
error: producerError,
isFetching: producerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Producers,
).length,
},
MessagingQueueHealthCheckService.Producers,
);
// Kafka Data
const {
data: kafkaData,
error: kafkaError,
isFetching: kafkaLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Kafka,
).length,
},
MessagingQueueHealthCheckService.Kafka,
);
// combined loading and update state
useEffect(() => {
setLoading(consumerLoading || producerLoading || kafkaLoading);
}, [consumerLoading, producerLoading, kafkaLoading]);
const missingConfiguration = useMemo(() => {
const consumerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Consumers) &&
consumerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const producerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Producers) &&
producerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const kafkaMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Kafka) &&
kafkaData?.payload?.data?.filter((item) => item.status === '0').length) ||
0;
return consumerMissing + producerMissing + kafkaMissing;
}, [consumerData, producerData, kafkaData, serviceToInclude]);
return (
<div>
<Button
onClick={(): void => setCheckListOpen(true)}
loading={loading}
className={cx(
'config-btn',
missingConfiguration ? 'missing-config-btn' : '',
)}
icon={<Bolt size={12} />}
>
<div className="config-btn-content">
{missingConfiguration
? `Missing Configuration (${missingConfiguration})`
: 'Configuration'}
</div>
<FolderTree size={14} />
</Button>
<AttributeCheckList
visible={checkListOpen}
onClose={(): void => setCheckListOpen(false)}
onboardingStatusResponses={[
{
title: 'Consumers',
data: consumerData?.payload?.data || [],
errorMsg: (consumerError || consumerData?.error) as string,
},
{
title: 'Producers',
data: producerData?.payload?.data || [],
errorMsg: (producerError || producerData?.error) as string,
},
{
title: 'Kafka',
data: kafkaData?.payload?.data || [],
errorMsg: (kafkaError || kafkaData?.error) as string,
},
].filter((item) => serviceToInclude.includes(item.title.toLowerCase()))}
loading={loading}
/>
</div>
);
}
export default MessagingQueueHealthCheck;

View File

@ -45,6 +45,11 @@
border-bottom: 1px solid var(--bg-slate-500);
.header-content {
display: flex;
gap: 12px;
align-items: center;
.header-config {
display: flex;
gap: 10px;
@ -65,6 +70,7 @@
}
}
}
}
.messaging-queue-main-graph {
display: flex;

View File

@ -13,8 +13,10 @@ import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import MessagingQueueHealthCheck from './MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
MessagingQueuesViewType,
} from './MessagingQueuesUtils';
import { ComingSoon } from './MQCommon/MQCommon';
@ -70,7 +72,16 @@ function MessagingQueues(): JSX.Element {
{t('breadcrumb')}
</div>
<div className="messaging-header">
<div className="header-content">
<div className="header-config">{t('header')}</div>
<MessagingQueueHealthCheck
serviceToInclude={[
MessagingQueueHealthCheckService.Consumers,
MessagingQueueHealthCheckService.Producers,
MessagingQueueHealthCheckService.Kafka,
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-overview">
@ -87,7 +98,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=consumer`,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
'Configure Consumer',
)
}
@ -106,7 +117,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=producer`,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
'Configure Producer',
)
}
@ -125,7 +136,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=kafka`,
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
'Monitor kafka',
)
}

View File

@ -381,3 +381,9 @@ export const getAttributeDataFromOnboardingStatus = (
attributeDataWithError,
};
};
export enum MessagingQueueHealthCheckService {
Consumers = 'consumers',
Producers = 'producers',
Kafka = 'kafka',
}