Compare commits

...

1 Commits

Author SHA1 Message Date
nikhilmantri0902
76e9074ca7 chore: initial logical changes 2026-02-15 19:04:44 +05:30
10 changed files with 304 additions and 121 deletions

View File

@@ -50,6 +50,8 @@ export interface HostListResponse {
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
endTimeBeforeRetention?: boolean;
noRecordsInSelectedTimeRangeAndFilters?: boolean;
};
}

View File

@@ -3,15 +3,39 @@ import { Typography } from 'antd';
export default function HostsEmptyOrIncorrectMetrics({
noData,
incorrectData,
endTimeBeforeRetention,
noRecordsInSelectedTimeRangeAndFilters,
}: {
noData: boolean;
incorrectData: boolean;
endTimeBeforeRetention?: boolean;
noRecordsInSelectedTimeRangeAndFilters?: boolean;
}): JSX.Element {
return (
<div className="hosts-empty-state-container">
<div className="hosts-empty-state-container-content">
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
{noRecordsInSelectedTimeRangeAndFilters && (
<div className="no-hosts-message">
<Typography.Text className="no-hosts-message-text">
No host metrics in the selected time range and filters.
</Typography.Text>
</div>
)}
{endTimeBeforeRetention && (
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">
End time before retention
</Typography.Title>
<Typography.Text className="no-hosts-message-text">
Your requested end time is earlier than the earliest time of retention of
host metrics, please adjust your end time.
</Typography.Text>
</div>
)}
{noData && (
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import {
Skeleton,
@@ -20,6 +20,90 @@ import {
HostsListTableProps,
} from './utils';
function getEmptyOrLoadingView(viewState: {
isError: boolean;
errorMessage: string;
showHostsEmptyState: boolean;
sentAnyHostMetricsData: boolean;
isSendingIncorrectK8SAgentMetrics: boolean;
showEndTimeBeforeRetentionMessage: boolean;
showNoRecordsInSelectedTimeRangeMessage: boolean;
showNoFilteredHostsMessage: boolean;
showTableLoadingState: boolean;
}): React.ReactNode {
const { isError, errorMessage } = viewState;
if (isError) {
return <Typography>{errorMessage}</Typography>;
}
if (viewState.showHostsEmptyState) {
return (
<HostsEmptyOrIncorrectMetrics
noData={!viewState.sentAnyHostMetricsData}
incorrectData={viewState.isSendingIncorrectK8SAgentMetrics}
/>
);
}
if (viewState.showEndTimeBeforeRetentionMessage) {
return (
<HostsEmptyOrIncorrectMetrics
noData={false}
incorrectData={false}
endTimeBeforeRetention
/>
);
}
if (viewState.showNoRecordsInSelectedTimeRangeMessage) {
return (
<HostsEmptyOrIncorrectMetrics
noData={false}
incorrectData={false}
noRecordsInSelectedTimeRangeAndFilters
/>
);
}
if (viewState.showNoFilteredHostsMessage) {
return (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
);
}
if (viewState.showTableLoadingState) {
return (
<div className="hosts-list-loading-state">
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
</div>
);
}
return null;
}
export default function HostsListTable({
isLoading,
isFetching,
@@ -46,6 +130,16 @@ export default function HostsListTable({
[data],
);
const endTimeBeforeRetention = useMemo(
() => data?.payload?.data?.endTimeBeforeRetention || false,
[data],
);
const noRecordsInSelectedTimeRangeAndFilters = useMemo(
() => data?.payload?.data?.noRecordsInSelectedTimeRangeAndFilters || false,
[data],
);
const formattedHostMetricsData = useMemo(
() => formatDataForTable(hostMetricsData),
[hostMetricsData],
@@ -97,63 +191,36 @@ export default function HostsListTable({
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
!filters.items.length;
const showEndTimeBeforeRetentionMessage =
!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
endTimeBeforeRetention &&
!filters.items.length;
const showNoRecordsInSelectedTimeRangeMessage =
!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
noRecordsInSelectedTimeRangeAndFilters;
const showTableLoadingState =
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
if (isError) {
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
}
const emptyOrLoadingView = getEmptyOrLoadingView({
isError,
errorMessage: data?.error || 'Something went wrong',
showHostsEmptyState,
sentAnyHostMetricsData,
isSendingIncorrectK8SAgentMetrics,
showEndTimeBeforeRetentionMessage,
showNoRecordsInSelectedTimeRangeMessage,
showNoFilteredHostsMessage,
showTableLoadingState,
});
if (showHostsEmptyState) {
return (
<HostsEmptyOrIncorrectMetrics
noData={!sentAnyHostMetricsData}
incorrectData={isSendingIncorrectK8SAgentMetrics}
/>
);
}
if (showNoFilteredHostsMessage) {
return (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
);
}
if (showTableLoadingState) {
return (
<div className="hosts-list-loading-state">
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
</div>
);
if (emptyOrLoadingView) {
return <>{emptyOrLoadingView}</>;
}
return (

View File

@@ -1,12 +1,15 @@
/* eslint-disable react/jsx-props-no-spreading */
import { render, screen } from '@testing-library/react';
import { HostData, HostListResponse } from 'api/infraMonitoring/getHostLists';
import { SuccessResponse } from 'types/api';
import HostsListTable from '../HostsListTable';
import { HostsListTableProps } from '../utils';
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
describe('HostsListTable', () => {
const mockHost = {
const createMockHost = (): HostData =>
({
hostName: 'test-host-1',
active: true,
cpu: 0.75,
@@ -14,20 +17,45 @@ describe('HostsListTable', () => {
wait: 0.03,
load15: 1.5,
os: 'linux',
};
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
} as HostData);
const mockTableData = {
const createMockTableData = (
overrides: Partial<HostListResponse['data']> = {},
): SuccessResponse<HostListResponse> => {
const mockHost = createMockHost();
return {
statusCode: 200,
message: 'Success',
error: null,
payload: {
status: 'success',
data: {
hosts: [mockHost],
type: 'list',
records: [mockHost],
groups: null,
total: 1,
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
...overrides,
},
},
};
};
describe('HostsListTable', () => {
const mockHost = createMockHost();
const mockTableData = createMockTableData();
const mockOnHostClick = jest.fn();
const mockSetCurrentPage = jest.fn();
const mockSetOrderBy = jest.fn();
const mockSetPageSize = jest.fn();
const mockProps = {
const mockProps: HostsListTableProps = {
isLoading: false,
isError: false,
isFetching: false,
@@ -43,7 +71,7 @@ describe('HostsListTable', () => {
pageSize: 10,
setOrderBy: mockSetOrderBy,
setPageSize: mockSetPageSize,
} as any;
};
it('renders loading state if isLoading is true and tableData is empty', () => {
const { container } = render(
@@ -51,7 +79,7 @@ describe('HostsListTable', () => {
{...mockProps}
isLoading
hostMetricsData={[]}
tableData={{ payload: { data: { hosts: [] } } }}
tableData={createMockTableData({ records: [] })}
/>,
);
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
@@ -63,7 +91,7 @@ describe('HostsListTable', () => {
{...mockProps}
isFetching
hostMetricsData={[]}
tableData={{ payload: { data: { hosts: [] } } }}
tableData={createMockTableData({ records: [] })}
/>,
);
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
@@ -79,11 +107,10 @@ describe('HostsListTable', () => {
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={{
payload: {
data: { hosts: [] },
},
}}
tableData={createMockTableData({
records: [],
noRecordsInSelectedTimeRangeAndFilters: true,
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
@@ -94,58 +121,77 @@ describe('HostsListTable', () => {
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={{
...mockTableData,
payload: {
...mockTableData.payload,
data: {
...mockTableData.payload.data,
sentAnyHostMetricsData: false,
hosts: [],
},
},
}}
tableData={createMockTableData({
sentAnyHostMetricsData: false,
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
});
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
it('renders empty state if isSendingK8SAgentMetrics is true', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={{
...mockTableData,
payload: {
...mockTableData.payload,
data: {
...mockTableData.payload.data,
isSendingIncorrectK8SAgentMetrics: true,
hosts: [],
},
},
}}
tableData={createMockTableData({
isSendingK8SAgentMetrics: true,
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
});
it('renders end time before retention message when endTimeBeforeRetention is true', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={createMockTableData({
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
endTimeBeforeRetention: true,
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
expect(
screen.getByText(
/Your requested end time is earlier than the earliest time of retention/,
),
).toBeInTheDocument();
});
it('renders no records message when noRecordsInSelectedTimeRangeAndFilters is true', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={createMockTableData({
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
noRecordsInSelectedTimeRangeAndFilters: true,
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
expect(
screen.getByText(/No host metrics in the selected time range and filters/),
).toBeInTheDocument();
});
it('renders table data', () => {
const { container } = render(
<HostsListTable
{...mockProps}
tableData={{
...mockTableData,
payload: {
...mockTableData.payload,
data: {
...mockTableData.payload.data,
isSendingIncorrectK8SAgentMetrics: false,
sentAnyHostMetricsData: true,
},
},
}}
tableData={createMockTableData({
isSendingK8SAgentMetrics: false,
sentAnyHostMetricsData: true,
})}
/>,
);
expect(container.querySelector('.hosts-list-table')).toBeTruthy();

View File

@@ -4308,6 +4308,34 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
}
// GetHostMetricsExistenceAndEarliestTime returns (count, minFirstReportedUnixMilli, error) for the given host metric names
// from distributed_metadata. When count is 0, minFirstReportedUnixMilli is 0.
func (r *ClickHouseReader) GetHostMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) (uint64, uint64, error) {
if len(metricNames) == 0 {
return 0, 0, nil
}
quotedNames := make([]string, len(metricNames))
for i, m := range metricNames {
quotedNames[i] = utils.ClickHouseFormattedValue(m)
}
namesStr := strings.Join(quotedNames, ", ")
query := fmt.Sprintf(
`SELECT count(*) AS cnt, min(first_reported_unix_milli) AS min_first_reported
FROM %s.%s
WHERE metric_name IN (%s)`,
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_METADATA_TABLENAME, namesStr)
var count, minFirstReported uint64
err := r.db.QueryRow(ctx, query).Scan(&count, &minFirstReported)
if err != nil {
zap.L().Error("error getting host metrics existence and earliest time", zap.Error(err))
return 0, 0, err
}
return count, minFirstReported, nil
}
func getPersonalisedError(err error) error {
if err == nil {
return nil

View File

@@ -10,6 +10,7 @@ import (
)
var dotMetricMap = map[string]string{
"system_filesystem_usage": "system.filesystem.usage",
"system_cpu_time": "system.cpu.time",
"system_memory_usage": "system.memory.usage",
"system_cpu_load_average_15m": "system.cpu.load_average.15m",

View File

@@ -67,10 +67,11 @@ var (
GetDotMetrics("os_type"),
}
metricNamesForHosts = map[string]string{
"cpu": GetDotMetrics("system_cpu_time"),
"memory": GetDotMetrics("system_memory_usage"),
"load15": GetDotMetrics("system_cpu_load_average_15m"),
"wait": GetDotMetrics("system_cpu_time"),
"filesystem": GetDotMetrics("system_filesystem_usage"),
"cpu": GetDotMetrics("system_cpu_time"),
"memory": GetDotMetrics("system_memory_usage"),
"load15": GetDotMetrics("system_cpu_load_average_15m"),
"wait": GetDotMetrics("system_cpu_time"),
}
)
@@ -316,24 +317,15 @@ func (h *HostsRepo) getTopHostGroups(ctx context.Context, orgID valuer.UUID, req
return topHostGroups, allHostGroups, nil
}
func (h *HostsRepo) DidSendHostMetricsData(ctx context.Context, req model.HostListRequest) (bool, error) {
// GetHostMetricsExistenceAndEarliestTime returns (count, minFirstReportedUnixMilli, error) for host metrics
// in distributed_metadata. Uses metricNamesForHosts plus system.filesystem.usage.
func (h *HostsRepo) GetHostMetricsExistenceAndEarliestTime(ctx context.Context, req model.HostListRequest) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForHosts {
names = append(names, metricName)
}
namesStr := "'" + strings.Join(names, "','") + "'"
query := fmt.Sprintf("SELECT count() FROM %s.%s WHERE metric_name IN (%s)",
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_v4_1DAY_TABLENAME, namesStr)
count, err := h.reader.GetCountOfThings(ctx, query)
if err != nil {
return false, err
}
return count > 0, nil
return h.reader.GetHostMetricsExistenceAndEarliestTime(ctx, names)
}
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) ([]string, []string, error) {
@@ -412,8 +404,22 @@ func (h *HostsRepo) GetHostList(ctx context.Context, orgID valuer.UUID, req mode
resp.ClusterNames = clusterNames
resp.NodeNames = nodeNames
}
if sentAnyHostMetricsData, err := h.DidSendHostMetricsData(ctx, req); err == nil {
resp.SentAnyHostMetricsData = sentAnyHostMetricsData
// check if any host metrics exist and get earliest retention time
if count, minFirstReportedUnixMilli, err := h.GetHostMetricsExistenceAndEarliestTime(ctx, req); err == nil {
if count == 0 {
resp.SentAnyHostMetricsData = false
resp.Records = []model.HostListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyHostMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.HostListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
@@ -529,6 +535,9 @@ func (h *HostsRepo) GetHostList(ctx context.Context, orgID valuer.UUID, req mode
}
resp.Total = len(allHostGroups)
resp.Records = records
if len(records) == 0 {
resp.NoRecordsInSelectedTimeRangeAndFilters = true
}
resp.SortBy(req.OrderBy)
return resp, nil

View File

@@ -125,6 +125,8 @@ const (
SIGNOZ_TIMESERIES_v4_6HRS_TABLENAME = "distributed_time_series_v4_6hrs"
SIGNOZ_ATTRIBUTES_METADATA_TABLENAME = "distributed_attributes_metadata"
SIGNOZ_ATTRIBUTES_METADATA_LOCAL_TABLENAME = "attributes_metadata"
SIGNOZ_METADATA_TABLENAME = "distributed_metadata"
SIGNOZ_METADATA_LOCAL_TABLENAME = "metadata"
)
// alert related constants

View File

@@ -100,6 +100,8 @@ type Reader interface {
GetCountOfThings(ctx context.Context, query string) (uint64, error)
GetHostMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) (uint64, uint64, error)
//trace
GetTraceFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError)
UpdateTraceField(ctx context.Context, field *model.UpdateField) *model.ApiError

View File

@@ -44,6 +44,8 @@ type HostListResponse struct {
IsSendingK8SAgentMetrics bool `json:"isSendingK8SAgentMetrics"`
ClusterNames []string `json:"clusterNames"`
NodeNames []string `json:"nodeNames"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
NoRecordsInSelectedTimeRangeAndFilters bool `json:"noRecordsInSelectedTimeRangeAndFilters"`
}
func (r *HostListResponse) SortBy(orderBy *v3.OrderBy) {