Compare commits

...

16 Commits

Author SHA1 Message Date
SagarRajput-7
7759e67eea Merge branch 'SIG-1912' into kafka-2_0-testing-pr 2024-10-29 21:55:24 +05:30
SagarRajput-7
2aa1878c58 feat: added kafka - scenario - 4 2024-10-28 08:12:59 +05:30
SagarRajput-7
b8f2ee6865 feat: added table row clicks func 2024-10-28 07:35:18 +05:30
SagarRajput-7
e775ef60f3 feat: added overview and details table for scenario-3 2024-10-28 07:35:18 +05:30
SagarRajput-7
8913ef9f0f feat: added generic logic for mq detail tables and consumed for sc-1,2 2024-10-28 07:35:18 +05:30
SagarRajput-7
132eb30c7e feat: added generic table component for scenario 1,3,4 2024-10-28 07:35:18 +05:30
SagarRajput-7
d9a830ca34 feat: added onboarding detail for consumer setup 2024-10-24 18:26:22 +05:30
SagarRajput-7
4433ccb996 feat: added missing configuration button at overview and onboarding flow 2024-10-24 18:12:58 +05:30
SagarRajput-7
adf8efb80f feat: corrected the onboardingapi payload 2024-10-24 13:07:10 +05:30
SagarRajput-7
695c9b37bc feat: added healthcheck and attribute checklist component for Kafka 2024-10-24 09:54:29 +05:30
SagarRajput-7
ca3aa9cf27 feat: changed start and end time to nanosecond for api payload 2024-10-23 12:16:43 +05:30
SagarRajput-7
70e6211a77 feat: refactoring and url query changes 2024-10-23 11:36:21 +05:30
SagarRajput-7
cc43c2ca2c feat: added onboarding status api util for attribute data 2024-10-23 11:36:21 +05:30
SagarRajput-7
99b36e3ee1 feat: added onboarding status api with useQueury functions and updated endpoints 2024-10-23 11:36:21 +05:30
SagarRajput-7
1bcf8887f5 fix: polled onboarding status api 2024-10-23 11:36:21 +05:30
SagarRajput-7
938372155c fix: added onboarding setup for producer/consumer for Messaging queues 2024-10-23 11:36:21 +05:30
29 changed files with 1704 additions and 188 deletions

View File

@@ -0,0 +1,39 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface OnboardingStatusResponse {
status: string;
data: {
attribute?: string;
error_message?: string;
status?: string;
}[];
}
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/${endpointService || 'consumers'}`,
rest,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};
export default getOnboardingStatus;

View File

@@ -37,4 +37,7 @@ export enum QueryParams {
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType',
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
configDetail = 'configDetail',
}

View File

@@ -0,0 +1,32 @@
&nbsp;
Once you are done intrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java consumer applications: Ensure your consumer apps are compiled and ready to run.
**Run Consumer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=consumer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.instrumentation.kafka.producer-propagation.enabled=true \
-Dotel.instrumentation.kafka.experimental-span-attributes=true \
-Dotel.instrumentation.kafka.metric-reporter.enabled=true \
-jar /path/to/your/consumer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@@ -0,0 +1,29 @@
&nbsp;
Once you are done intrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java producer applications: Ensure your producer apps are compiled and ready to run.
**Run Producer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=producer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar /path/to/your/producer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@@ -6,11 +6,16 @@ import {
LoadingOutlined,
} from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
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';
import { AppState } from 'store/reducers';
@@ -27,6 +32,12 @@ export default function ConnectionStatus(): JSX.Element {
GlobalReducer
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const {
serviceName,
selectedDataSource,
@@ -57,8 +68,68 @@ export default function ConnectionStatus(): JSX.Element {
maxTime,
selectedTime,
selectedTags,
options: {
enabled: getStartedSource !== 'kafka',
},
});
const [pollInterval, setPollInterval] = useState<number | false>(10000);
const {
data: onbData,
error: onbErr,
isFetching: onbFetching,
} = useOnboardingStatus(
{
enabled: getStartedSource === 'kafka',
refetchInterval: pollInterval,
},
getStartedSourceService || '',
'query-key-onboarding-status',
);
const [
shouldRetryOnboardingCall,
setShouldRetryOnboardingCall,
] = useState<boolean>(false);
useEffect(() => {
if (getStartedSource === 'kafka') {
if (onbData?.statusCode !== 200) {
setShouldRetryOnboardingCall(true);
} else if (onbData?.payload?.status === 'success') {
const attributeData = getAttributeDataFromOnboardingStatus(
onbData?.payload,
);
if (attributeData.overallStatus === 'success') {
setLoading(false);
setIsReceivingData(true);
setPollInterval(false);
} else {
setShouldRetryOnboardingCall(true);
}
}
}
}, [
shouldRetryOnboardingCall,
onbData,
onbErr,
onbFetching,
getStartedSource,
]);
useEffect(() => {
if (retryCount < 0 && getStartedSource === 'kafka') {
setPollInterval(false);
setLoading(false);
}
}, [retryCount, getStartedSource]);
useEffect(() => {
if (getStartedSource === 'kafka' && !onbFetching) {
setRetryCount((prevCount) => prevCount - 1);
}
}, [getStartedSource, onbData, onbFetching]);
const renderDocsReference = (): JSX.Element => {
switch (selectedDataSource?.name) {
case 'java':
@@ -192,25 +263,27 @@ export default function ConnectionStatus(): JSX.Element {
useEffect(() => {
let pollingTimer: string | number | NodeJS.Timer | undefined;
if (loading) {
pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters
const updatedMinTime = (Date.now() - 15 * 60 * 1000) * 1000000;
const updatedMaxTime = Date.now() * 1000000;
if (getStartedSource !== 'kafka') {
if (loading) {
pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters
const updatedMinTime = (Date.now() - 15 * 60 * 1000) * 1000000;
const updatedMaxTime = Date.now() * 1000000;
const payload = {
maxTime: updatedMaxTime,
minTime: updatedMinTime,
selectedTime,
};
const payload = {
maxTime: updatedMaxTime,
minTime: updatedMinTime,
selectedTime,
};
dispatch({
type: UPDATE_TIME_INTERVAL,
payload,
});
}, pollingInterval); // Same interval as pollingInterval
} else if (!loading && pollingTimer) {
clearInterval(pollingTimer);
dispatch({
type: UPDATE_TIME_INTERVAL,
payload,
});
}, pollingInterval); // Same interval as pollingInterval
} else if (!loading && pollingTimer) {
clearInterval(pollingTimer);
}
}
// Clean up the interval when the component unmounts
@@ -221,15 +294,24 @@ export default function ConnectionStatus(): JSX.Element {
}, [refetch, selectedTags, selectedTime, loading]);
useEffect(() => {
verifyApplicationData(data);
if (getStartedSource !== 'kafka') {
verifyApplicationData(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isServiceLoading, data, error, isError]);
useEffect(() => {
refetch();
if (getStartedSource !== 'kafka') {
refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isQueryServiceLoading = useMemo(
() => isServiceLoading || loading || onbFetching,
[isServiceLoading, loading, onbFetching],
);
return (
<div className="connection-status-container">
<div className="full-docs-link">{renderDocsReference()}</div>
@@ -250,30 +332,42 @@ export default function ConnectionStatus(): JSX.Element {
<div className="label"> Status </div>
<div className="status">
{(loading || isServiceLoading) && <LoadingOutlined />}
{!(loading || isServiceLoading) && isReceivingData && (
<>
<CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span>
</>
)}
{!(loading || isServiceLoading) && !isReceivingData && (
<>
<CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span>
</>
)}
{isQueryServiceLoading && <LoadingOutlined />}
{!isQueryServiceLoading &&
isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span>
</>
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
{!isQueryServiceLoading &&
!isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span>
</>
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
</div>
</div>
<div className="details-info">
<div className="label"> Details </div>
<div className="details">
{(loading || isServiceLoading) && <div> Waiting for Update </div>}
{!(loading || isServiceLoading) && isReceivingData && (
{isQueryServiceLoading && <div> Waiting for Update </div>}
{!isQueryServiceLoading && isReceivingData && (
<div> Received data from the application successfully. </div>
)}
{!(loading || isServiceLoading) && !isReceivingData && (
{!isQueryServiceLoading && !isReceivingData && (
<div> Could not detect the install </div>
)}
</div>

View File

@@ -74,4 +74,11 @@ div[class*='-setup-instructions-container'] {
.dataSourceName {
color: var(--bg-slate-500);
}
}
}
.supported-languages-container {
.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}

View File

@@ -6,15 +6,21 @@ import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
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,
hasFrameworks,
messagingQueueKakfaSupportedDataSources,
} from 'container/OnboardingContainer/utils/dataSourceUtils';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -33,6 +39,8 @@ export default function DataSource(): JSX.Element {
const { t } = useTranslation(['common']);
const history = useHistory();
const getStartedSource = useUrlQuery().get(QueryParams.getStartedSource);
const {
serviceName,
selectedModule,
@@ -44,6 +52,9 @@ export default function DataSource(): JSX.Element {
updateSelectedFramework,
} = useOnboardingContext();
const isKafkaAPM =
getStartedSource === 'kafka' && selectedModule?.id === ModulesMap.APM;
const [supportedDataSources, setSupportedDataSources] = useState<
DataSourceType[]
>([]);
@@ -150,13 +161,19 @@ export default function DataSource(): JSX.Element {
className={cx(
'supported-language',
selectedDataSource?.name === dataSource.name ? 'selected' : '',
isKafkaAPM &&
!messagingQueueKakfaSupportedDataSources.includes(dataSource?.id || '')
? 'disabled'
: '',
)}
key={dataSource.name}
onClick={(): void => {
updateSelectedFramework(null);
updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null });
if (!isKafkaAPM) {
updateSelectedFramework(null);
updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null });
}
}}
>
<div>

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { QueryParams } from 'constants/query';
import { ApmDocFilePaths } from 'container/OnboardingContainer/constants/apmDocFilePaths';
import { AwsMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/awsMonitoringDocFilePaths';
import { AzureMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/azureMonitoringDocFilePaths';
@@ -10,6 +11,7 @@ import {
useOnboardingContext,
} from 'container/OnboardingContainer/context/OnboardingContext';
import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer';
import useUrlQuery from 'hooks/useUrlQuery';
import { useEffect, useState } from 'react';
export interface IngestionInfoProps {
@@ -31,6 +33,12 @@ export default function MarkdownStep(): JSX.Element {
const [markdownContent, setMarkdownContent] = useState('');
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const { step } = activeStep;
const getFilePath = (): any => {
@@ -54,6 +62,12 @@ export default function MarkdownStep(): JSX.Element {
path += `_${step?.id}`;
if (
getStartedSource === 'kafka' &&
path === 'APM_java_springBoot_kubernetes_recommendedSteps_runApplication' // todo: Sagar - Make this generic logic in followup PRs
) {
path += `_${getStartedSourceService}`;
}
return path;
};

View File

@@ -252,6 +252,8 @@ 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_consumers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-consumers.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';
@@ -1053,6 +1055,8 @@ 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_producers,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_consumers,
// SpringBoot-LinuxAMD64-recommended
APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector,

View File

@@ -399,3 +399,5 @@ export const moduleRouteMap = {
[ModulesMap.AwsMonitoring]: ROUTES.GET_STARTED_AWS_MONITORING,
[ModulesMap.AzureMonitoring]: ROUTES.GET_STARTED_AZURE_MONITORING,
};
export const messagingQueueKakfaSupportedDataSources = ['java'];

View File

@@ -0,0 +1,29 @@
import getOnboardingStatus, {
OnboardingStatusResponse,
} from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseOnboardingStatus = (
options?: UseQueryOptions<
SuccessResponse<OnboardingStatusResponse> | ErrorResponse
>,
endpointService?: string,
queryKey?: string,
) => UseQueryResult<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>;
export const useOnboardingStatus: UseOnboardingStatus = (
options,
endpointService,
queryKey,
) =>
useQuery<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>({
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

@@ -5,17 +5,28 @@ import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { ListMinus } from 'lucide-react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { MessagingQueuesViewType } from '../MessagingQueuesUtils';
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
import {
MessagingQueuesViewType,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview';
import MessagingQueuesDetails from '../MQDetails/MQDetails';
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
import MessagingQueuesGraph from '../MQGraph/MQGraph';
function MQDetailPage(): JSX.Element {
const history = useHistory();
const [selectedView, setSelectedView] = useState<string>(
MessagingQueuesViewType.consumerLag.value,
);
const [
producerLatencyOption,
setproducerLatencyOption,
] = useState<ProducerLatencyOptions>(ProducerLatencyOptions.Producers);
useEffect(() => {
logEvent('Messaging Queues: Detail page visited', {});
@@ -39,37 +50,23 @@ function MQDetailPage(): JSX.Element {
className="messaging-queue-options"
defaultValue={MessagingQueuesViewType.consumerLag.value}
popupClassName="messaging-queue-options-popup"
onChange={(value): void => setSelectedView(value)}
options={[
{
label: MessagingQueuesViewType.consumerLag.label,
value: MessagingQueuesViewType.consumerLag.value,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.partitionLatency.label}
/>
),
label: MessagingQueuesViewType.partitionLatency.label,
value: MessagingQueuesViewType.partitionLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.producerLatency.label}
/>
),
label: MessagingQueuesViewType.producerLatency.label,
value: MessagingQueuesViewType.producerLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.consumerLatency.label}
/>
),
value: MessagingQueuesViewType.consumerLatency.value,
disabled: true,
label: MessagingQueuesViewType.dropRate.label,
value: MessagingQueuesViewType.dropRate.value,
},
]}
/>
@@ -78,10 +75,23 @@ function MQDetailPage(): JSX.Element {
</div>
<div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions />
<MessagingQueuesGraph />
{selectedView === MessagingQueuesViewType.consumerLag.value ? (
<MessagingQueuesGraph />
) : (
<MessagingQueueOverview
selectedView={selectedView}
option={producerLatencyOption}
setOption={setproducerLatencyOption}
/>
)}
</div>
<div className="messaging-queue-details">
<MessagingQueuesDetails />
{selectedView !== MessagingQueuesViewType.dropRate.value && (
<MessagingQueuesDetails
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
)}
</div>
</div>
);

View File

@@ -4,3 +4,42 @@
flex-direction: column;
gap: 24px;
}
.mq-overview-container {
display: flex;
padding: 24px;
flex-direction: column;
align-items: start;
gap: 16px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-500);
.mq-overview-title {
color: var(--bg-vanilla-200);
font-family: Inter;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 28px;
}
.mq-details-options {
letter-spacing: -0.06px;
cursor: pointer;
.ant-radio-button-wrapper {
border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400);
}
.ant-radio-button-wrapper-checked {
background: var(--bg-slate-400);
color: var(--bg-vanilla-100);
}
.ant-radio-button-wrapper::before {
width: 0px;
}
}
}

View File

@@ -1,65 +1,316 @@
import './MQDetails.style.scss';
import { Radio } from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
ProducerLatencyOptions,
SelectedTimelineQuery,
} from '../MessagingQueuesUtils';
import { ComingSoon } from '../MQCommon/MQCommon';
import {
getConsumerLagDetails,
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './MQTables/getConsumerLagDetails';
import { getPartitionLatencyDetails } from './MQTables/getPartitionLatencyDetails';
import { getTopicThroughputDetails } from './MQTables/getTopicThroughputDetails';
import MessagingQueuesTable from './MQTables/MQTables';
const MQServiceDetailTypePerView = (
producerLatencyOption: ProducerLatencyOptions,
): {
[x: string]: MessagingQueueServiceDetailType[];
} => ({
[MessagingQueuesViewType.consumerLag.value]: [
MessagingQueueServiceDetailType.ConsumerDetails,
MessagingQueueServiceDetailType.ProducerDetails,
MessagingQueueServiceDetailType.NetworkLatency,
MessagingQueueServiceDetailType.PartitionHostMetrics,
],
[MessagingQueuesViewType.partitionLatency.value]: [
MessagingQueueServiceDetailType.ConsumerDetails,
MessagingQueueServiceDetailType.ProducerDetails,
],
[MessagingQueuesViewType.producerLatency.value]: [
producerLatencyOption === ProducerLatencyOptions.Consumers
? MessagingQueueServiceDetailType.ConsumerDetails
: MessagingQueueServiceDetailType.ProducerDetails,
],
});
interface MessagingQueuesOptionsProps {
currentTab: MessagingQueueServiceDetailType;
setCurrentTab: Dispatch<SetStateAction<MessagingQueueServiceDetailType>>;
selectedView: string;
producerLatencyOption: ProducerLatencyOptions;
}
function MessagingQueuesOptions({
currentTab,
setCurrentTab,
}: {
currentTab: ConsumerLagDetailType;
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>;
}): JSX.Element {
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab);
selectedView,
producerLatencyOption,
}: MessagingQueuesOptionsProps): JSX.Element {
const [option, setOption] = useState<MessagingQueueServiceDetailType>(
currentTab,
);
useEffect(() => {
setOption(currentTab);
}, [currentTab]);
const handleChange = (value: MessagingQueueServiceDetailType): void => {
setOption(value);
setCurrentTab(value);
};
const renderRadioButtons = (): JSX.Element[] => {
const detailTypes =
MQServiceDetailTypePerView(producerLatencyOption)[selectedView] || [];
return detailTypes.map((detailType) => (
<Radio.Button
key={detailType}
value={detailType}
disabled={
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
}
className={
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
? 'disabled-option'
: ''
}
>
{ConsumerLagDetailTitle[detailType]}
{detailType === MessagingQueueServiceDetailType.PartitionHostMetrics && (
<ComingSoon />
)}
</Radio.Button>
));
};
return (
<Radio.Group
onChange={(value): void => {
setOption(value.target.value);
setCurrentTab(value.target.value);
}}
onChange={(e): void => handleChange(e.target.value)}
value={option}
className="mq-details-options"
>
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.ProducerDetails}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.NetworkLatency}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]}
</Radio.Button>
<Radio.Button
value={ConsumerLagDetailType.PartitionHostMetrics}
disabled
className="disabled-option"
>
{ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]}
<ComingSoon />
</Radio.Button>
{renderRadioButtons()}
</Radio.Group>
);
}
function MessagingQueuesDetails(): JSX.Element {
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>(
ConsumerLagDetailType.ConsumerDetails,
interface MetaDataAndAPI {
tableApiPayload: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
}
interface MetaDataAndAPIPerView {
detailType: MessagingQueueServiceDetailType;
selectedTimelineQuery: SelectedTimelineQuery;
configDetails?: {
[key: string]: string;
};
minTime: number;
maxTime: number;
}
export const getMetaDataAndAPIPerView = (
metaDataProps: MetaDataAndAPIPerView,
): Record<string, MetaDataAndAPI> => {
const {
detailType,
minTime,
maxTime,
selectedTimelineQuery,
configDetails,
} = metaDataProps;
return {
[MessagingQueuesViewType.consumerLag.value]: {
tableApiPayload: {
start: (selectedTimelineQuery?.start || 0) * 1e9,
end: (selectedTimelineQuery?.end || 0) * 1e9,
variables: {
partition: selectedTimelineQuery?.partition,
topic: selectedTimelineQuery?.topic,
consumer_group: selectedTimelineQuery?.group,
},
detailType,
},
tableApi: getConsumerLagDetails,
},
[MessagingQueuesViewType.partitionLatency.value]: {
tableApiPayload: {
start: minTime,
end: maxTime,
variables: {
partition: configDetails?.partition,
topic: configDetails?.topic,
consumer_group: configDetails?.group,
},
detailType,
},
tableApi: getPartitionLatencyDetails,
},
[MessagingQueuesViewType.producerLatency.value]: {
tableApiPayload: {
start: minTime,
end: maxTime,
variables: {
partition: configDetails?.partition,
topic: configDetails?.topic,
service_name: configDetails?.service_name,
},
detailType,
},
tableApi: getTopicThroughputDetails,
},
};
};
const checkValidityOfDetailConfigs = (
selectedTimelineQuery: SelectedTimelineQuery,
selectedView: string,
currentTab: MessagingQueueServiceDetailType,
configDetails?: {
[key: string]: string;
},
// eslint-disable-next-line sonarjs/cognitive-complexity
): boolean => {
if (selectedView === MessagingQueuesViewType.consumerLag.value) {
return !(
isEmpty(selectedTimelineQuery) ||
(!selectedTimelineQuery?.group &&
!selectedTimelineQuery?.topic &&
!selectedTimelineQuery?.partition)
);
}
if (selectedView === MessagingQueuesViewType.partitionLatency.value) {
if (isEmpty(configDetails)) {
return false;
}
if (currentTab === MessagingQueueServiceDetailType.ConsumerDetails) {
return Boolean(configDetails?.topic && configDetails?.partition);
}
return Boolean(
configDetails?.group && configDetails?.topic && configDetails?.partition,
);
}
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
if (isEmpty(configDetails)) {
return false;
}
if (currentTab === MessagingQueueServiceDetailType.ProducerDetails) {
return Boolean(
configDetails?.topic &&
configDetails?.partition &&
configDetails?.service_name,
);
}
return Boolean(configDetails?.topic && configDetails?.service_name);
}
return selectedView === MessagingQueuesViewType.dropRate.value;
};
function MessagingQueuesDetails({
selectedView,
producerLatencyOption,
}: {
selectedView: string;
producerLatencyOption: ProducerLatencyOptions;
}): JSX.Element {
const [currentTab, setCurrentTab] = useState<MessagingQueueServiceDetailType>(
MessagingQueueServiceDetailType.ConsumerDetails,
);
useEffect(() => {
if (
producerLatencyOption &&
selectedView === MessagingQueuesViewType.producerLatency.value
) {
setCurrentTab(
producerLatencyOption === ProducerLatencyOptions.Consumers
? MessagingQueueServiceDetailType.ConsumerDetails
: MessagingQueueServiceDetailType.ProducerDetails,
);
}
}, [selectedView, producerLatencyOption]);
const urlQuery = useUrlQuery();
const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
);
const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery],
);
const configDetails = decodeURIComponent(
urlQuery.get(QueryParams.configDetail) || '',
);
const configDetailQueryData: {
[key: string]: string;
} = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [
configDetails,
]);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const serviceConfigDetails = useMemo(
() =>
getMetaDataAndAPIPerView({
detailType: currentTab,
minTime,
maxTime,
selectedTimelineQuery: timelineQueryData,
configDetails: configDetailQueryData,
}),
[configDetailQueryData, currentTab, maxTime, minTime, timelineQueryData],
);
return (
<div className="mq-details">
<MessagingQueuesOptions
currentTab={currentTab}
setCurrentTab={setCurrentTab}
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
<MessagingQueuesTable
currentTab={currentTab}
selectedView={selectedView}
tableApi={serviceConfigDetails[selectedView]?.tableApi}
validConfigPresent={checkValidityOfDetailConfigs(
timelineQueryData,
selectedView,
currentTab,
configDetailQueryData,
)}
tableApiPayload={serviceConfigDetails[selectedView]?.tableApiPayload}
/>
<MessagingQueuesTable currentTab={currentTab} />
</div>
);
}

View File

@@ -1,4 +1,7 @@
.mq-tables-container {
width: 100%;
height: 100%;
.mq-table-title {
display: flex;
align-items: center;
@@ -31,9 +34,6 @@
.ant-table-tbody {
.ant-table-cell {
max-width: 250px;
background-color: var(--bg-ink-400);
border-bottom: none;
}
}
@@ -63,6 +63,21 @@
}
}
.mq-table {
&.mq-overview-row-clickable {
.ant-table-row {
background-color: var(--bg-ink-400);
&:hover {
cursor: pointer;
background-color: var(--bg-slate-400) !important;
color: var(--bg-vanilla-400);
transition: background-color 0.3s ease, color 0.3s ease;
}
}
}
}
.lightMode {
.mq-tables-container {
.mq-table-title {

View File

@@ -1,9 +1,10 @@
/* eslint-disable react/require-default-props */
import './MQTables.styles.scss';
import { Skeleton, Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import axios from 'axios';
import { isNumber } from 'chart.js/helpers';
import cx from 'classnames';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
@@ -13,18 +14,20 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
convertToTitleCase,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
RowData,
SelectedTimelineQuery,
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
ConsumerLagPayload,
getConsumerLagDetails,
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
@@ -33,7 +36,6 @@ export function getColumns(
data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>,
): RowData[] {
console.log(data);
if (data?.result?.length === 0) {
return [];
}
@@ -105,10 +107,25 @@ const showPaginationItem = (total: number, range: number[]): JSX.Element => (
</>
);
// eslint-disable-next-line sonarjs/cognitive-complexity
function MessagingQueuesTable({
currentTab,
selectedView,
tableApiPayload,
tableApi,
validConfigPresent = false,
type = 'Detail',
}: {
currentTab: ConsumerLagDetailType;
currentTab?: MessagingQueueServiceDetailType;
selectedView: string;
tableApiPayload?: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
validConfigPresent?: boolean;
type?: 'Detail' | 'Overview';
}): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
@@ -118,11 +135,22 @@ function MessagingQueuesTable({
const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
);
const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery],
);
const configDetails = decodeURIComponent(
urlQuery.get(QueryParams.configDetail) || '',
);
const configDetailQueryData: {
[key: string]: string;
} = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [
configDetails,
]);
const paginationConfig = useMemo(
() =>
tableData?.length > 20 && {
@@ -134,90 +162,104 @@ function MessagingQueuesTable({
[tableData],
);
const props: ConsumerLagPayload = useMemo(
() => ({
start: (timelineQueryData?.start || 0) * 1e9,
end: (timelineQueryData?.end || 0) * 1e9,
variables: {
partition: timelineQueryData?.partition,
topic: timelineQueryData?.topic,
consumer_group: timelineQueryData?.group,
},
detailType: currentTab,
}),
[currentTab, timelineQueryData],
);
const handleConsumerDetailsOnError = (error: Error): void => {
notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
});
};
const { mutate: getConsumerDetails, isLoading } = useMutation(
getConsumerLagDetails,
{
onSuccess: (data) => {
if (data.payload) {
setColumns(getColumns(data?.payload, history));
setTableData(getTableData(data?.payload));
}
},
onError: handleConsumerDetailsOnError,
const { mutate: getViewDetails, isLoading } = useMutation(tableApi, {
onSuccess: (data) => {
if (data.payload) {
setColumns(getColumns(data?.payload, history));
setTableData(getTableData(data?.payload));
}
},
onError: handleConsumerDetailsOnError,
});
useEffect(
() => {
if (validConfigPresent && tableApiPayload) {
getViewDetails(tableApiPayload);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentTab, selectedView, tableApiPayload],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => getConsumerDetails(props), [currentTab, props]);
const [selectedRowKey, setSelectedRowKey] = useState<React.Key>();
const [, setSelectedRows] = useState<any>();
const location = useLocation();
const isLogEventCalled = useRef<boolean>(false);
const onRowClick = (record: { [key: string]: string }): void => {
const selectedKey = record.key;
const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean => {
const isEmptyDetail =
isEmpty(timelineQueryData) ||
(!timelineQueryData?.group &&
!timelineQueryData?.topic &&
!timelineQueryData?.partition);
if (`${selectedKey}_${selectedView}` === selectedRowKey) {
setSelectedRowKey(undefined);
setSelectedRows({});
setConfigDetail(urlQuery, location, history, {});
} else {
setSelectedRowKey(`${selectedKey}_${selectedView}`);
setSelectedRows(record);
if (!isEmptyDetail && !isLogEventCalled.current) {
logEvent('Messaging Queues: More details viewed', {
'tab-option': ConsumerLagDetailTitle[currentTab],
variables: {
group: timelineQueryData?.group,
topic: timelineQueryData?.topic,
partition: timelineQueryData?.partition,
},
});
isLogEventCalled.current = true;
if (!isEmpty(record)) {
setConfigDetail(urlQuery, location, history, record);
}
}
return isEmptyDetail;
};
const subtitle =
selectedView === MessagingQueuesViewType.consumerLag.value
? `${timelineQueryData?.group || ''} ${timelineQueryData?.topic || ''} ${
timelineQueryData?.partition || ''
}`
: `${configDetailQueryData?.service_name || ''} ${
configDetailQueryData?.topic || ''
} ${configDetailQueryData?.partition || ''}`;
return (
<div className="mq-tables-container">
{isEmptyDetails(timelineQueryData) ? (
{!validConfigPresent ? (
<div className="no-data-style">
<Typography.Text>
Click on a co-ordinate above to see the details
{selectedView === MessagingQueuesViewType.consumerLag.value
? 'Click on a co-ordinate above to see the details'
: 'Click on a row above to see the details'}
</Typography.Text>
<Skeleton />
</div>
) : (
<>
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${
timelineQueryData?.topic || ''
} ${timelineQueryData?.partition || ''}`}</div>
</div>
{currentTab && (
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{subtitle}</div>
</div>
)}
<Table
className="mq-table"
className={cx(
'mq-table',
type !== 'Detail' ? 'mq-overview-row-clickable' : '',
)}
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
loading={isLoading}
onRow={(record): any =>
type !== 'Detail'
? {
onClick: (): void => onRowClick(record),
}
: {}
}
rowClassName={(record): any =>
`${record.key}_${selectedView}` === selectedRowKey
? 'ant-table-row-selected'
: ''
}
/>
</>
)}

View File

@@ -2,18 +2,20 @@ import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ConsumerLagDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface ConsumerLagPayload {
export interface MessagingQueueServicePayload {
start?: number | string;
end?: number | string;
variables: {
variables?: {
partition?: string;
topic?: string;
consumer_group?: string;
service_name?: string;
};
detailType: ConsumerLagDetailType;
detailType?: MessagingQueueServiceDetailType | 'producer' | 'consumer';
evalTime?: number;
}
export interface MessagingQueuesPayloadProps {
@@ -36,7 +38,7 @@ export interface MessagingQueuesPayloadProps {
}
export const getConsumerLagDetails = async (
props: ConsumerLagPayload,
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {

View File

@@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getKafkaSpanEval = async (
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { start, end, evalTime } = props;
try {
const response = await axios.post(`messaging-queues/kafka/span/evaluation`, {
start,
end,
eval_time: evalTime,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};

View File

@@ -0,0 +1,39 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getPartitionLatencyDetails = async (
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...rest } = props;
let endpoint = '';
if (detailType === MessagingQueueServiceDetailType.ConsumerDetails) {
endpoint = `/messaging-queues/kafka/partition-latency/consumer`;
} else {
endpoint = `/messaging-queues/kafka/consumer-lag/producer-details`;
}
try {
const response = await axios.post(endpoint, {
...rest,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};

View File

@@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getPartitionLatencyOverview = async (
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
try {
const response = await axios.post(
`/messaging-queues/kafka/partition-latency/overview`,
{
...props,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getTopicThroughputDetails = async (
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...rest } = props;
const endpoint = `/messaging-queues/kafka/topic-throughput/${detailType}`;
try {
const response = await axios.post(endpoint, {
...rest,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};

View File

@@ -0,0 +1,37 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getTopicThroughputOverview = async (
props: Omit<MessagingQueueServicePayload, 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, start, end } = props;
console.log(detailType);
try {
const response = await axios.post(
`messaging-queues/kafka/topic-throughput/${detailType}`,
{
start,
end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};

View File

@@ -0,0 +1,109 @@
import './MQDetails.style.scss';
import { Radio } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
MessagingQueuesViewType,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import { MessagingQueueServicePayload } from './MQTables/getConsumerLagDetails';
import { getKafkaSpanEval } from './MQTables/getKafkaSpanEval';
import { getPartitionLatencyOverview } from './MQTables/getPartitionLatencyOverview';
import { getTopicThroughputOverview } from './MQTables/getTopicThroughputOverview';
import MessagingQueuesTable from './MQTables/MQTables';
type SelectedViewType = keyof typeof MessagingQueuesViewType;
function PartitionLatencyTabs({
option,
setOption,
}: {
option: ProducerLatencyOptions;
setOption: Dispatch<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
return (
<Radio.Group
onChange={(e): void => setOption(e.target.value)}
value={option}
className="mq-details-options"
>
<Radio.Button
value={ProducerLatencyOptions.Producers}
key={ProducerLatencyOptions.Producers}
>
{ProducerLatencyOptions.Producers}
</Radio.Button>
<Radio.Button
value={ProducerLatencyOptions.Consumers}
key={ProducerLatencyOptions.Consumers}
>
{ProducerLatencyOptions.Consumers}
</Radio.Button>
</Radio.Group>
);
}
const getTableApi = (selectedView: string): any => {
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
return getTopicThroughputOverview;
}
if (selectedView === MessagingQueuesViewType.dropRate.value) {
return getKafkaSpanEval;
}
return getPartitionLatencyOverview;
};
function MessagingQueueOverview({
selectedView,
option,
setOption,
}: {
selectedView: string;
option: ProducerLatencyOptions;
setOption: Dispatch<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const tableApiPayload: MessagingQueueServicePayload = {
variables: {},
start: minTime,
end: maxTime,
detailType:
// eslint-disable-next-line no-nested-ternary
selectedView === MessagingQueuesViewType.producerLatency.value
? option === ProducerLatencyOptions.Producers
? 'producer'
: 'consumer'
: undefined,
evalTime:
selectedView === MessagingQueuesViewType.dropRate.value
? 2363404
: undefined,
};
return (
<div className="mq-overview-container">
{selectedView === MessagingQueuesViewType.producerLatency.value ? (
<PartitionLatencyTabs option={option} setOption={setOption} />
) : (
<div className="mq-overview-title">
{MessagingQueuesViewType[selectedView as SelectedViewType].label}
</div>
)}
<MessagingQueuesTable
selectedView={selectedView}
tableApiPayload={tableApiPayload}
tableApi={getTableApi(selectedView)}
validConfigPresent
type="Overview"
/>
</div>
);
}
export default MessagingQueueOverview;

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,22 +45,28 @@
border-bottom: 1px solid var(--bg-slate-500);
.header-config {
.header-content {
display: flex;
gap: 10px;
gap: 12px;
align-items: center;
.messaging-queue-options {
.ant-select-selector {
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
.header-config {
display: flex;
gap: 10px;
align-items: center;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
.messaging-queue-options {
.ant-select-selector {
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
}
@@ -106,6 +112,8 @@
.mq-details-options {
letter-spacing: -0.06px;
cursor: pointer;
.ant-radio-button-wrapper {
border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400);

View File

@@ -4,6 +4,7 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react';
@@ -12,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';
@@ -69,7 +72,16 @@ function MessagingQueues(): JSX.Element {
{t('breadcrumb')}
</div>
<div className="messaging-header">
<div className="header-config">{t('header')}</div>
<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">
@@ -86,7 +98,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
'Configure Consumer',
)
}
@@ -105,7 +117,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
'Configure Producer',
)
}
@@ -124,7 +136,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING,
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
'Monitor kafka',
)
}
@@ -175,7 +187,7 @@ function MessagingQueues(): JSX.Element {
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.consumerLatency.label}</p>
<p>{MessagingQueuesViewType.dropRate.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>

View File

@@ -1,3 +1,4 @@
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
@@ -24,14 +25,17 @@ export type RowData = {
[key: string]: string | number;
};
export enum ConsumerLagDetailType {
export enum MessagingQueueServiceDetailType {
ConsumerDetails = 'consumer-details',
ProducerDetails = 'producer-details',
NetworkLatency = 'network-latency',
PartitionHostMetrics = 'partition-host-metric',
}
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = {
export const ConsumerLagDetailTitle: Record<
MessagingQueueServiceDetailType,
string
> = {
'consumer-details': 'Consumer Groups Details',
'producer-details': 'Producer Details',
'network-latency': 'Network Latency',
@@ -218,8 +222,81 @@ export const MessagingQueuesViewType = {
label: 'Producer Latency view',
value: 'producerLatency',
},
consumerLatency: {
label: 'Consumer latency view',
value: 'consumerLatency',
dropRate: {
label: 'Drop Rate',
value: 'dropRate',
},
};
interface OnboardingStatusAttributeData {
overallStatus: string;
allAvailableAttributes: string[];
attributeDataWithError: { attributeName: string; errorMsg: string }[];
}
export const getAttributeDataFromOnboardingStatus = (
onboardingStatus?: OnboardingStatusResponse | null,
): OnboardingStatusAttributeData => {
const allAvailableAttributes: string[] = [];
const attributeDataWithError: {
attributeName: string;
errorMsg: string;
}[] = [];
if (onboardingStatus?.data && !isEmpty(onboardingStatus?.data)) {
onboardingStatus.data.forEach((status) => {
if (status.attribute) {
allAvailableAttributes.push(status.attribute);
if (status.status === '0') {
attributeDataWithError.push({
attributeName: status.attribute,
errorMsg: status.error_message || '',
});
}
}
});
}
return {
overallStatus: attributeDataWithError.length ? 'error' : 'success',
allAvailableAttributes,
attributeDataWithError,
};
};
export enum MessagingQueueHealthCheckService {
Consumers = 'consumers',
Producers = 'producers',
Kafka = 'kafka',
}
export function setConfigDetail(
urlQuery: URLSearchParams,
location: Location<unknown>,
history: History<unknown>,
paramsToSet?: {
[key: string]: string;
},
): void {
// remove "key" and its value from the paramsToSet object
const { key, ...restParamsToSet } = paramsToSet || {};
if (!isEmpty(restParamsToSet)) {
const configDetail = {
...restParamsToSet,
};
urlQuery.set(
QueryParams.configDetail,
encodeURIComponent(JSON.stringify(configDetail)),
);
} else {
urlQuery.delete(QueryParams.configDetail);
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
export enum ProducerLatencyOptions {
Producers = 'Producers',
Consumers = 'Consumers',
}