Compare commits

...

16 Commits

Author SHA1 Message Date
ahmadshaheer
81d1c30a98 fix: redirect to docs on clicking alert setup guide in create alert page 2024-10-24 20:07:44 +04:30
Vishal Sharma
d7846338ce chore: remove facing issues button (#6256) 2024-10-24 16:53:00 +05:30
Vikrant Gupta
5dac1ad20a fix: explicitly return the empty slice for variables query (#6258) 2024-10-24 15:03:35 +05:30
Yunus M
8d704c331c feat: update the response data type 2024-10-24 14:54:34 +05:30
Yunus M
f8e47496fa feat: add get and update apis for org and user preferences 2024-10-24 14:54:34 +05:30
Srikanth Chekuri
6fef9d9676 chore: fix access for downtime schedules (#6255) 2024-10-24 07:37:03 +00:00
Srikanth Chekuri
190767fd0a chore: add k8s nodes, namespaces, and cluster list (#6230) 2024-10-24 12:17:24 +05:30
Srikanth Chekuri
1e78786cae chore: add k8s pods list (#6229) 2024-10-24 11:33:51 +05:30
Srikanth Chekuri
6448fb17e7 chore: update hosts list to use pre-aggregated data table dynamically (#6227) 2024-10-24 11:19:39 +05:30
Srikanth Chekuri
f2e33d7ca9 chore: enable anomaly detection for ee/paid plan (#6243) 2024-10-24 07:38:31 +05:30
Srikanth Chekuri
6c7167a224 chore: add missing shiftby in alert rule (#6239) 2024-10-24 02:20:32 +05:30
dependabot[bot]
00421235b0 chore(deps): bump micromatch from 4.0.5 to 4.0.8 in /frontend (#5817)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 01:38:31 +05:30
Vishal Sharma
0e2b67059b Fix/bulk invite api error response (#6247) 2024-10-23 23:43:46 +05:30
Yunus M
910c44cefc feat: add org onboarding preference (#6248) 2024-10-23 23:27:25 +05:30
Vishal Sharma
8bad036423 fix: name is optional in bulk invite API (#6246) 2024-10-23 19:49:45 +05:30
Yunus M
a21830132f feat: remove hardcode is darkmode value from get anomaly data func (#6245) 2024-10-23 19:36:02 +05:30
39 changed files with 2770 additions and 427 deletions

View File

@@ -373,7 +373,7 @@ var EnterprisePlan = basemodel.FeatureSet{
},
basemodel.Feature{
Name: basemodel.AnomalyDetection,
Active: false,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",

View File

@@ -0,0 +1,18 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllOrgPreferences = async (): Promise<
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/org/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllOrgPreferences;

View File

@@ -0,0 +1,18 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllUserPreferences = async (): Promise<
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllUserPreferences;

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getOrgPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
const response = await axios.get(`/org/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getOrgPreference;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getUserPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getUserPreference;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateOrgPreferenceProps,
UpdateOrgPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateOrgPreference = async (
preferencePayload: UpdateOrgPreferenceProps,
): Promise<
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(`/org/preferences`, {
preference_value: preferencePayload.value,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateOrgPreference;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateUserPreferenceProps,
UpdateUserPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateUserPreference = async (
preferencePayload: UpdateUserPreferenceProps,
): Promise<
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(`/user/preferences`, {
preference_value: preferencePayload.value,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateUserPreference;

View File

@@ -1,46 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AlertDef } from 'types/api/alerts/def';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
export const chartHelpMessage = (
selectedDashboard: Dashboard | undefined,
graphType: PANEL_TYPES,
): string => `
Hi Team,
I need help in creating this chart. Here are my dashboard details
Name: ${selectedDashboard?.data.title || ''}
Panel type: ${graphType}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardHelpMessage = (
data: DashboardData | undefined,
selectedDashboard: Dashboard | undefined,
): string => `
Hi Team,
I need help with this dashboard. Here are my dashboard details
Name: ${data?.title || ''}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardListMessage = `Hi Team,
I need help with dashboards.
Thanks`;
export const listAlertMessage = `Hi Team,
I need help with managing alerts.
Thanks`;
export const onboardingHelpMessage = (
dataSourceName: string,
moduleId: string,
@@ -55,35 +12,3 @@ Module: ${moduleId}
Thanks
`;
export const alertHelpMessage = (
alertDef: AlertDef,
ruleId: number,
): string => `
Hi Team,
I need help in configuring this alert. Here are my alert rule details
Name: ${alertDef?.alert || ''}
Alert Type: ${alertDef?.alertType || ''}
State: ${(alertDef as any)?.state || ''}
Alert Id: ${ruleId}
Thanks`;
export const integrationsListMessage = `Hi Team,
I need help with Integrations.
Thanks`;
export const integrationDetailMessage = (
selectedIntegration: string,
): string => `
Hi Team,
I need help in configuring this integration.
Integration Id: ${selectedIntegration}
Thanks`;

View File

@@ -74,13 +74,13 @@ function AnomalyAlertEvaluationView({
const dimensions = useResizeObserver(graphRef);
useEffect(() => {
const chartData = getUplotChartDataForAnomalyDetection(data);
const chartData = getUplotChartDataForAnomalyDetection(data, isDarkMode);
setSeriesData(chartData);
setAllSeries(Object.keys(chartData));
setFilteredSeriesKeys(Object.keys(chartData));
}, [data]);
}, [data, isDarkMode]);
useEffect(() => {
const seriesKeys = Object.keys(seriesData);

View File

@@ -73,6 +73,19 @@ export enum AlertDetectionTypes {
ANOMALY_DETECTION_ALERT = 'anomaly_rule',
}
const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
[AlertTypes.METRICS_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.LOGS_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.TRACES_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.EXCEPTIONS_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.ANOMALY_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
};
// eslint-disable-next-line sonarjs/cognitive-complexity
function FormAlertRules({
alertType,
@@ -702,6 +715,29 @@ function FormAlertRules({
const isRuleCreated = !ruleId || ruleId === 0;
function handleRedirection(option: AlertTypes): void {
let url;
if (
option === AlertTypes.METRICS_BASED_ALERT &&
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
) {
url = ALERT_SETUP_GUIDE_URLS[AlertTypes.ANOMALY_BASED_ALERT];
} else {
url = ALERT_SETUP_GUIDE_URLS[option];
}
if (url) {
logEvent('Alert: Check example alert clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
}
}
useEffect(() => {
if (!isRuleCreated) {
logEvent('Alert: Edit page visited', {
@@ -752,7 +788,11 @@ function FormAlertRules({
)}
</div>
<Button className="periscope-btn" icon={<ExternalLink size={14} />}>
<Button
className="periscope-btn"
onClick={(): void => handleRedirection(alertDef.alertType as AlertTypes)}
icon={<ExternalLink size={14} />}
>
Alert Setup Guide
</Button>
</div>

View File

@@ -5,7 +5,6 @@ import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/LaunchChatSupport/util';
import {
DynamicColumnsKey,
TableDataSource,
@@ -397,15 +396,6 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dynamicColumns={dynamicColumns}
onChange={handleChange}
pagination={paginationConfig}
facingIssueBtn={{
attributes: {
screen: 'Alert list page',
},
eventName: 'Alert: Facing Issues in alert',
buttonText: 'Facing issues with alerts?',
message: listAlertMessage,
onHoverText: 'Click here to get help with alerts',
}}
/>
</>
);

View File

@@ -25,8 +25,6 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardListMessage } from 'components/LaunchChatSupport/util';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
@@ -693,16 +691,6 @@ function DashboardsList(): JSX.Element {
<Typography.Text className="subtitle">
Create and manage dashboards for your workspace.
</Typography.Text>
<LaunchChatSupport
attributes={{
screen: 'Dashboard list page',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardListMessage}
buttonText="Need help with dashboards?"
onHoverText="Click here to get help with dashboards"
intercomMessageDisabled
/>
</Flex>
</div>

View File

@@ -12,8 +12,6 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardHelpMessage } from 'components/LaunchChatSupport/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
@@ -356,18 +354,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
{isDashboardLocked && <LockKeyhole size={14} />}
</div>
<div className="right-section">
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: updatedTitle,
screen: 'Dashboard Details',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
buttonText="Need help with this dashboard?"
onHoverText="Click here to get help with dashboard"
intercomMessageDisabled
/>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<Popover
open={isDashboardSettingsOpen}

View File

@@ -4,7 +4,6 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@@ -235,21 +234,6 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,
screen: 'Dashboard widget',
panelType: selectedGraph,
widgetId: query.id,
queryType: currentQuery.queryType,
}}
eventName="Dashboard: Facing Issues in dashboard"
buttonText="Need help with this chart?"
// message={chartHelpMessage(selectedDashboard, graphType)}
onHoverText="Click here to get help with this dashboard widget"
intercomMessageDisabled
/>
<TextToolTip
text="This will temporarily save the current query and graph state. This will persist across tab change"
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"

View File

@@ -153,7 +153,8 @@ const processAnomalyDetectionData = (
};
export const getUplotChartDataForAnomalyDetection = (
apiResponse?: MetricRangePayloadProps,
apiResponse: MetricRangePayloadProps,
isDarkMode: boolean,
): Record<
string,
{
@@ -163,6 +164,5 @@ export const getUplotChartDataForAnomalyDetection = (
}
> => {
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
const isDarkMode = true;
return processAnomalyDetectionData(anomalyDetectionData, isDarkMode);
};

View File

@@ -2,8 +2,6 @@ import './Integrations.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Flex, Input, Typography } from 'antd';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { integrationsListMessage } from 'components/LaunchChatSupport/util';
import { Search } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
@@ -25,13 +23,6 @@ function Header(props: HeaderProps): JSX.Element {
<Typography.Text className="subtitle">
Manage Integrations for this workspace
</Typography.Text>
<LaunchChatSupport
attributes={{ screen: 'Integrations list page' }}
eventName="Integrations: Facing issues in integrations"
buttonText="Facing issues with integrations"
message={integrationsListMessage}
onHoverText="Click here to get help with integrations"
/>
</Flex>
<Input

View File

@@ -5,8 +5,6 @@ import './IntegrationDetailPage.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Skeleton, Typography } from 'antd';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { integrationDetailMessage } from 'components/LaunchChatSupport/util';
import { useGetIntegration } from 'hooks/Integrations/useGetIntegration';
import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus';
import { defaultTo } from 'lodash-es';
@@ -77,18 +75,6 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
>
All Integrations
</Button>
<LaunchChatSupport
attributes={{
screen: 'Integrations detail page',
activeTab: activeDetailTab,
integrationTitle: integrationData?.title || '',
integrationId: selectedIntegration,
}}
eventName="Integrations: Facing issues in integrations"
buttonText="Facing issues with integration"
message={integrationDetailMessage(selectedIntegration)}
onHoverText="Click here to get help with this integration"
/>
</Flex>
{loading ? (

View File

@@ -0,0 +1,39 @@
export interface GetOrgPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetUserPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetAllOrgPreferencesResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetAllUserPreferencesResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface UpdateOrgPreferenceProps {
key: string;
value: unknown;
}
export interface UpdateUserPreferenceProps {
key: string;
value: unknown;
}
export interface UpdateOrgPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface UpdateUserPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}

View File

@@ -6139,7 +6139,7 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@@ -12123,11 +12123,11 @@ micromark@^3.0.0:
uvu "^0.5.0"
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.5"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.2"
braces "^3.0.3"
picomatch "^2.3.1"
microseconds@0.2.0:

View File

@@ -3560,7 +3560,7 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs
}
func (r *ClickHouseReader) QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) {
var result model.DashboardVar
var result = model.DashboardVar{VariableValues: make([]interface{}, 0)}
rows, err := r.db.Query(ctx, query)
zap.L().Info(query)

View File

@@ -112,8 +112,12 @@ type APIHandler struct {
UseLogsNewSchema bool
hostsRepo *inframetrics.HostsRepo
processesRepo *inframetrics.ProcessesRepo
hostsRepo *inframetrics.HostsRepo
processesRepo *inframetrics.ProcessesRepo
podsRepo *inframetrics.PodsRepo
nodesRepo *inframetrics.NodesRepo
namespacesRepo *inframetrics.NamespacesRepo
clustersRepo *inframetrics.ClustersRepo
}
type APIHandlerOpts struct {
@@ -185,6 +189,10 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
hostsRepo := inframetrics.NewHostsRepo(opts.Reader, querierv2)
processesRepo := inframetrics.NewProcessesRepo(opts.Reader, querierv2)
podsRepo := inframetrics.NewPodsRepo(opts.Reader, querierv2)
nodesRepo := inframetrics.NewNodesRepo(opts.Reader, querierv2)
namespacesRepo := inframetrics.NewNamespacesRepo(opts.Reader, querierv2)
clustersRepo := inframetrics.NewClustersRepo(opts.Reader, querierv2)
aH := &APIHandler{
reader: opts.Reader,
@@ -205,6 +213,10 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
UseLogsNewSchema: opts.UseLogsNewSchema,
hostsRepo: hostsRepo,
processesRepo: processesRepo,
podsRepo: podsRepo,
nodesRepo: nodesRepo,
namespacesRepo: namespacesRepo,
clustersRepo: clustersRepo,
}
logsQueryBuilder := logsv3.PrepareLogsQuery
@@ -363,6 +375,26 @@ func (aH *APIHandler) RegisterInfraMetricsRoutes(router *mux.Router, am *AuthMid
processesSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getProcessAttributeKeys)).Methods(http.MethodGet)
processesSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getProcessAttributeValues)).Methods(http.MethodGet)
processesSubRouter.HandleFunc("/list", am.ViewAccess(aH.getProcessList)).Methods(http.MethodPost)
podsSubRouter := router.PathPrefix("/api/v1/pods").Subrouter()
podsSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getPodAttributeKeys)).Methods(http.MethodGet)
podsSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getPodAttributeValues)).Methods(http.MethodGet)
podsSubRouter.HandleFunc("/list", am.ViewAccess(aH.getPodList)).Methods(http.MethodPost)
nodesSubRouter := router.PathPrefix("/api/v1/nodes").Subrouter()
nodesSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getNodeAttributeKeys)).Methods(http.MethodGet)
nodesSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getNodeAttributeValues)).Methods(http.MethodGet)
nodesSubRouter.HandleFunc("/list", am.ViewAccess(aH.getNodeList)).Methods(http.MethodPost)
namespacesSubRouter := router.PathPrefix("/api/v1/namespaces").Subrouter()
namespacesSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getNamespaceAttributeKeys)).Methods(http.MethodGet)
namespacesSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getNamespaceAttributeValues)).Methods(http.MethodGet)
namespacesSubRouter.HandleFunc("/list", am.ViewAccess(aH.getNamespaceList)).Methods(http.MethodPost)
clustersSubRouter := router.PathPrefix("/api/v1/clusters").Subrouter()
clustersSubRouter.HandleFunc("/attribute_keys", am.ViewAccess(aH.getClusterAttributeKeys)).Methods(http.MethodGet)
clustersSubRouter.HandleFunc("/attribute_values", am.ViewAccess(aH.getClusterAttributeValues)).Methods(http.MethodGet)
clustersSubRouter.HandleFunc("/list", am.ViewAccess(aH.getClusterList)).Methods(http.MethodPost)
}
func (aH *APIHandler) RegisterWebSocketPaths(router *mux.Router, am *AuthMiddleware) {
@@ -411,11 +443,11 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
router.HandleFunc("/api/v1/rules/{id}/history/top_contributors", am.ViewAccess(aH.getRuleStateHistoryTopContributors)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/rules/{id}/history/overall_status", am.ViewAccess(aH.getOverallStateTransitions)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/downtime_schedules", am.OpenAccess(aH.listDowntimeSchedules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.OpenAccess(aH.getDowntimeSchedule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules", am.OpenAccess(aH.createDowntimeSchedule)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.OpenAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.OpenAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/downtime_schedules", am.ViewAccess(aH.listDowntimeSchedules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.ViewAccess(aH.getDowntimeSchedule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/downtime_schedules", am.EditAccess(aH.createDowntimeSchedule)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.getDashboards)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.createDashboards)).Methods(http.MethodPost)
@@ -1993,7 +2025,7 @@ func (aH *APIHandler) inviteUsers(w http.ResponseWriter, r *http.Request) {
}
// Check the response status and set the appropriate HTTP status code
if response.Status == "failure" {
w.WriteHeader(http.StatusInternalServerError)
w.WriteHeader(http.StatusBadRequest) // 400 Bad Request for failure
} else if response.Status == "partial_success" {
w.WriteHeader(http.StatusPartialContent) // 206 Partial Content
} else {

View File

@@ -122,3 +122,215 @@ func (aH *APIHandler) getProcessList(w http.ResponseWriter, r *http.Request) {
aH.Respond(w, hostList)
}
func (aH *APIHandler) getPodAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.podsRepo.GetPodAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getPodAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.podsRepo.GetPodAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getPodList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.PodListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
podList, err := aH.podsRepo.GetPodList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, podList)
}
func (aH *APIHandler) getNodeAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.nodesRepo.GetNodeAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getNodeAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.nodesRepo.GetNodeAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getNodeList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.NodeListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
nodeList, err := aH.nodesRepo.GetNodeList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, nodeList)
}
func (aH *APIHandler) getNamespaceAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.namespacesRepo.GetNamespaceAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getNamespaceAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.namespacesRepo.GetNamespaceAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getNamespaceList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.NamespaceListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
namespaceList, err := aH.namespacesRepo.GetNamespaceList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, namespaceList)
}
func (aH *APIHandler) getClusterAttributeKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeKeyRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
keys, err := aH.clustersRepo.GetClusterAttributeKeys(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, keys)
}
func (aH *APIHandler) getClusterAttributeValues(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := parseFilterAttributeValueRequest(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
values, err := aH.clustersRepo.GetClusterAttributeValues(ctx, *req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, values)
}
func (aH *APIHandler) getClusterList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := model.ClusterListRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
clusterList, err := aH.clustersRepo.GetClusterList(ctx, req)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
aH.Respond(w, clusterList)
}

View File

@@ -0,0 +1,342 @@
package inframetrics
import (
"context"
"math"
"sort"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/postprocess"
"golang.org/x/exp/slices"
)
var (
metricToUseForClusters = "k8s_node_cpu_utilization"
clusterAttrsToEnrich = []string{"k8s_cluster_name"}
k8sClusterUIDAttrKey = "k8s_cluster_uid"
queryNamesForClusters = map[string][]string{
"cpu": {"A"},
"cpu_allocatable": {"B"},
"memory": {"C"},
"memory_allocatable": {"D"},
}
clusterQueryNames = []string{"A", "B", "C", "D"}
)
type ClustersRepo struct {
reader interfaces.Reader
querierV2 interfaces.Querier
}
func NewClustersRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *ClustersRepo {
return &ClustersRepo{reader: reader, querierV2: querierV2}
}
func (n *ClustersRepo) GetClusterAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForClusters
if req.Limit == 0 {
req.Limit = 50
}
attributeKeysResponse, err := n.reader.GetMetricAttributeKeys(ctx, &req)
if err != nil {
return nil, err
}
return attributeKeysResponse, nil
}
func (n *ClustersRepo) GetClusterAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForClusters
if req.Limit == 0 {
req.Limit = 50
}
attributeValuesResponse, err := n.reader.GetMetricAttributeValues(ctx, &req)
if err != nil {
return nil, err
}
return attributeValuesResponse, nil
}
func (p *ClustersRepo) getMetadataAttributes(ctx context.Context, req model.ClusterListRequest) (map[string]map[string]string, error) {
clusterAttrs := map[string]map[string]string{}
for _, key := range clusterAttrsToEnrich {
hasKey := false
for _, groupByKey := range req.GroupBy {
if groupByKey.Key == key {
hasKey = true
break
}
}
if !hasKey {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key})
}
}
mq := v3.BuilderQuery{
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricToUseForClusters,
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
query = localQueryToDistributedQuery(query)
attrsListResponse, err := p.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
clusterUID := stringData[k8sClusterUIDAttrKey]
if _, ok := clusterAttrs[clusterUID]; !ok {
clusterAttrs[clusterUID] = map[string]string{}
}
for _, key := range req.GroupBy {
clusterAttrs[clusterUID][key.Key] = stringData[key.Key]
}
}
return clusterAttrs, nil
}
func (p *ClustersRepo) getTopClusterGroups(ctx context.Context, req model.ClusterListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopClusters(req)
queryNames := queryNamesForClusters[req.OrderBy.ColumnName]
topClusterGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topClusterGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, topClusterGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topClusterGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
max := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series)))
paginatedTopClusterGroupsSeries := formattedResponse[0].Series[req.Offset:int(max)]
topClusterGroups := []map[string]string{}
for _, series := range paginatedTopClusterGroupsSeries {
topClusterGroups = append(topClusterGroups, series.Labels)
}
allClusterGroups := []map[string]string{}
for _, series := range formattedResponse[0].Series {
allClusterGroups = append(allClusterGroups, series.Labels)
}
return topClusterGroups, allClusterGroups, nil
}
func (p *ClustersRepo) GetClusterList(ctx context.Context, req model.ClusterListRequest) (model.ClusterListResponse, error) {
resp := model.ClusterListResponse{}
if req.Limit == 0 {
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
if req.GroupBy == nil {
req.GroupBy = []v3.AttributeKey{{Key: k8sClusterUIDAttrKey}}
resp.Type = model.ResponseTypeList
} else {
resp.Type = model.ResponseTypeGroupedList
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := NodesTableListQuery.Clone()
query.Start = req.Start
query.End = req.End
query.Step = step
for _, query := range query.CompositeQuery.BuilderQueries {
query.StepInterval = step
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
query.GroupBy = req.GroupBy
}
clusterAttrs, err := p.getMetadataAttributes(ctx, req)
if err != nil {
return resp, err
}
topClusterGroups, allClusterGroups, err := p.getTopClusterGroups(ctx, req, query)
if err != nil {
return resp, err
}
groupFilters := map[string][]string{}
for _, topClusterGroup := range topClusterGroups {
for k, v := range topClusterGroup {
groupFilters[k] = append(groupFilters[k], v)
}
}
for groupKey, groupValues := range groupFilters {
hasGroupFilter := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, filter := range req.Filters.Items {
if filter.Key.Key == groupKey {
hasGroupFilter = true
break
}
}
}
if !hasGroupFilter {
for _, query := range query.CompositeQuery.BuilderQueries {
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{Key: groupKey},
Value: groupValues,
Operator: v3.FilterOperatorIn,
})
}
}
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, query)
if err != nil {
return resp, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return resp, err
}
records := []model.ClusterListRecord{}
for _, result := range formattedResponse {
for _, row := range result.Table.Rows {
record := model.ClusterListRecord{
CPUUsage: -1,
CPUAllocatable: -1,
MemoryUsage: -1,
MemoryAllocatable: -1,
}
if clusterUID, ok := row.Data[k8sClusterUIDAttrKey].(string); ok {
record.ClusterUID = clusterUID
}
if cpu, ok := row.Data["A"].(float64); ok {
record.CPUUsage = cpu
}
if cpuAllocatable, ok := row.Data["B"].(float64); ok {
record.CPUAllocatable = cpuAllocatable
}
if mem, ok := row.Data["C"].(float64); ok {
record.MemoryUsage = mem
}
if memoryAllocatable, ok := row.Data["D"].(float64); ok {
record.MemoryAllocatable = memoryAllocatable
}
record.Meta = map[string]string{}
if _, ok := clusterAttrs[record.ClusterUID]; ok {
record.Meta = clusterAttrs[record.ClusterUID]
}
for k, v := range row.Data {
if slices.Contains(clusterQueryNames, k) {
continue
}
if labelValue, ok := v.(string); ok {
record.Meta[k] = labelValue
}
}
records = append(records, record)
}
}
resp.Total = len(allClusterGroups)
resp.Records = records
return resp, nil
}

View File

@@ -0,0 +1,81 @@
package inframetrics
import (
"strings"
"time"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/model"
)
// getParamsForTopItems returns the step, time series table name and samples table name
// for the top items query. what are we doing here?
// we want to identify the top hosts/pods/nodes quickly, so we use pre-aggregated data
// for samples and time series tables to speed up the query
// the speed of the query depends on the number of values in group by clause, the higher
// the step interval, the faster the query will be as number of rows to group by is reduced
// here we are using the averaged value of the time series data to get the top items
func getParamsForTopItems(start, end int64) (int64, string, string) {
var step int64
var timeSeriesTableName string
var samplesTableName string
if end-start < time.Hour.Milliseconds() {
// 5 minute aggregation for any query less than 1 hour
step = 5 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME
} else if end-start < time.Hour.Milliseconds()*6 {
// 15 minute aggregation for any query less than 6 hours
step = 15 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_6HRS_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME
} else if end-start < time.Hour.Milliseconds()*24 {
// 1 hour aggregation for any query less than 1 day
step = 60 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
} else if end-start < time.Hour.Milliseconds()*7 {
// 6 hours aggregation for any query less than 1 week
step = 6 * 60 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
} else {
// 12 hours aggregation for any query greater than 1 week
step = 12 * 60 * 60
timeSeriesTableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
samplesTableName = constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
}
return step, timeSeriesTableName, samplesTableName
}
func getParamsForTopHosts(req model.HostListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopPods(req model.PodListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopNodes(req model.NodeListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopNamespaces(req model.NamespaceListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
func getParamsForTopClusters(req model.ClusterListRequest) (int64, string, string) {
return getParamsForTopItems(req.Start, req.End)
}
// TODO(srikanthccv): remove this
// What is happening here?
// The `PrepareTimeseriesFilterQuery` uses the local time series table for sub-query because each fingerprint
// goes to same shard.
// However, in this case, we are interested in the attributes values across all the shards.
// So, we replace the local time series table with the distributed time series table.
// See `PrepareTimeseriesFilterQuery` for more details.
func localQueryToDistributedQuery(query string) string {
return strings.Replace(query, ".time_series_v4", ".distributed_time_series_v4", 1)
}

View File

@@ -2,12 +2,10 @@ package inframetrics
import (
"context"
"fmt"
"math"
"sort"
"strings"
"time"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
@@ -21,23 +19,45 @@ type HostsRepo struct {
querierV2 interfaces.Querier
}
var pointAttrsToIgnore = []string{
"state",
"cpu",
"device",
"direction",
"mode",
"mountpoint",
"type",
"process.cgroup",
"process.command",
"process.command_line",
"process.executable.name",
"process.executable.path",
"process.owner",
"process.parent_pid",
"process.pid",
}
var (
// we don't have a way to get the resource attributes from the current time series table
// but we only want to suggest resource attributes for system metrics,
// this is a list of attributes that we skip from all labels as they are data point attributes
// TODO(srikanthccv): remove this once we have a way to get resource attributes
pointAttrsToIgnore = []string{
"state",
"cpu",
"device",
"direction",
"mode",
"mountpoint",
"type",
"os_type",
"process_cgroup",
"process_command",
"process_command_line",
"process_executable_name",
"process_executable_path",
"process_owner",
"process_parent_pid",
"process_pid",
}
queryNamesForTopHosts = map[string][]string{
"cpu": {"A", "B", "F1"},
"memory": {"C", "D", "F2"},
"wait": {"E", "F", "F3"},
"load15": {"G"},
}
// TODO(srikanthccv): remove hardcoded metric name and support keys from any system metric
metricToUseForHostAttributes = "system_cpu_load_average_15m"
hostNameAttrKey = "host_name"
// TODO(srikanthccv): remove k8s hacky logic from hosts repo after charts users are migrated
k8sNodeNameAttrKey = "k8s_node_name"
agentNameToIgnore = "k8s-infra-otel-agent"
)
func NewHostsRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *HostsRepo {
return &HostsRepo{reader: reader, querierV2: querierV2}
@@ -46,7 +66,7 @@ func NewHostsRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *Hosts
func (h *HostsRepo) GetHostAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
// TODO(srikanthccv): remove hardcoded metric name and support keys from any system metric
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = "system_cpu_load_average_15m"
req.AggregateAttribute = metricToUseForHostAttributes
if req.Limit == 0 {
req.Limit = 50
}
@@ -71,7 +91,7 @@ func (h *HostsRepo) GetHostAttributeKeys(ctx context.Context, req v3.FilterAttri
func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = "system_cpu_load_average_15m"
req.AggregateAttribute = metricToUseForHostAttributes
if req.Limit == 0 {
req.Limit = 50
}
@@ -80,21 +100,21 @@ func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAtt
if err != nil {
return nil, err
}
if req.FilterAttributeKey != "host_name" {
if req.FilterAttributeKey != hostNameAttrKey {
return attributeValuesResponse, nil
}
hostNames := []string{}
for _, attributeValue := range attributeValuesResponse.StringAttributeValues {
if strings.Contains(attributeValue, "k8s-infra-otel-agent") {
if strings.Contains(attributeValue, agentNameToIgnore) {
continue
}
hostNames = append(hostNames, attributeValue)
}
req.FilterAttributeKey = "k8s_node_name"
req.FilterAttributeKey = k8sNodeNameAttrKey
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = "system_cpu_load_average_15m"
req.AggregateAttribute = metricToUseForHostAttributes
if req.Limit == 0 {
req.Limit = 50
}
@@ -104,7 +124,7 @@ func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAtt
return nil, err
}
for _, attributeValue := range attributeValuesResponse.StringAttributeValues {
if strings.Contains(attributeValue, "k8s-infra-otel-agent") {
if strings.Contains(attributeValue, agentNameToIgnore) {
continue
}
hostNames = append(hostNames, attributeValue)
@@ -113,78 +133,6 @@ func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAtt
return &v3.FilterAttributeValueResponse{StringAttributeValues: hostNames}, nil
}
func getGroupKey(record model.HostListRecord, groupBy []v3.AttributeKey) string {
groupKey := ""
for _, key := range groupBy {
groupKey += fmt.Sprintf("%s=%s,", key.Key, record.Meta[key.Key])
}
return groupKey
}
func (h *HostsRepo) getMetadataAttributes(ctx context.Context,
req model.HostListRequest, hostNameAttrKey string) (map[string]map[string]string, error) {
hostAttrs := map[string]map[string]string{}
hasHostName := false
for _, key := range req.GroupBy {
if key.Key == hostNameAttrKey {
hasHostName = true
}
}
if !hasHostName {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: hostNameAttrKey})
}
mq := v3.BuilderQuery{
AggregateAttribute: v3.AttributeKey{
Key: "system_cpu_load_average_15m",
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
// TODO(srikanthccv): remove this
// What is happening here?
// The `PrepareTimeseriesFilterQuery` uses the local time series table for sub-query because each fingerprint
// goes to same shard.
// However, in this case, we are interested in the attributes values across all the shards.
// So, we replace the local time series table with the distributed time series table.
// See `PrepareTimeseriesFilterQuery` for more details.
query = strings.Replace(query, ".time_series_v4", ".distributed_time_series_v4", 1)
attrsListResponse, err := h.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
hostName := stringData[hostNameAttrKey]
if _, ok := hostAttrs[hostName]; !ok {
hostAttrs[hostName] = map[string]string{}
}
for _, key := range req.GroupBy {
hostAttrs[hostName][key.Key] = stringData[key.Key]
}
}
return hostAttrs, nil
}
func (h *HostsRepo) getActiveHosts(ctx context.Context,
req model.HostListRequest, hostNameAttrKey string) (map[string]bool, error) {
activeStatus := map[string]bool{}
@@ -202,7 +150,7 @@ func (h *HostsRepo) getActiveHosts(ctx context.Context,
}
params := v3.QueryRangeParamsV3{
Start: time.Now().Add(-time.Hour).UTC().UnixMilli(),
Start: time.Now().Add(-time.Minute * 10).UTC().UnixMilli(),
End: time.Now().UTC().UnixMilli(),
Step: step,
CompositeQuery: &v3.CompositeQuery{
@@ -212,7 +160,7 @@ func (h *HostsRepo) getActiveHosts(ctx context.Context,
StepInterval: step,
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: "system_cpu_load_average_15m",
Key: metricToUseForHostAttributes,
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
@@ -244,25 +192,103 @@ func (h *HostsRepo) getActiveHosts(ctx context.Context,
return activeStatus, nil
}
// getTopHosts returns the top hosts for the given order by column name
func (h *HostsRepo) getTopHosts(ctx context.Context, req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]string, []string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopHosts(req)
queryNames := queryNamesForTopHosts[req.OrderBy.ColumnName]
topHostsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topHostsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := h.querierV2.QueryRange(ctx, topHostsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topHostsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
paginatedTopHostsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
topHosts := []string{}
for _, series := range paginatedTopHostsSeries {
topHosts = append(topHosts, series.Labels[hostNameAttrKey])
}
allHosts := []string{}
for _, series := range formattedResponse[0].Series {
allHosts = append(allHosts, series.Labels[hostNameAttrKey])
}
return topHosts, allHosts, nil
}
func (h *HostsRepo) getHostsForQuery(ctx context.Context,
req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]model.HostListRecord, error) {
req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]model.HostListRecord, []string, error) {
step := common.MinAllowedStepInterval(req.Start, req.End)
query := q.Clone()
if req.OrderBy != nil {
for _, q := range query.CompositeQuery.BuilderQueries {
q.OrderBy = []v3.OrderBy{*req.OrderBy}
}
}
query.Start = req.Start
query.End = req.End
query.Step = step
topHosts, allHosts, err := h.getTopHosts(ctx, req, q, hostNameAttrKey)
if err != nil {
return nil, nil, err
}
for _, query := range query.CompositeQuery.BuilderQueries {
query.StepInterval = step
// check if the filter has host_name and is either IN or EQUAL operator
// if so, we don't need to add the topHosts filter again
hasHostNameInOrEqual := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, item := range req.Filters.Items {
if item.Key.Key == hostNameAttrKey && (item.Operator == v3.FilterOperatorIn || item.Operator == v3.FilterOperatorEqual) {
hasHostNameInOrEqual = true
}
}
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
@@ -270,29 +296,36 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
// what is happening here?
// if the filter has host_name and we are querying for k8s host metrics,
// we need to replace the host_name with k8s_node_name
if hostNameAttrKey == "k8s_node_name" {
if hostNameAttrKey == k8sNodeNameAttrKey {
for idx, item := range query.Filters.Items {
if item.Key.Key == "host_name" {
query.Filters.Items[idx].Key.Key = "k8s_node_name"
if item.Key.Key == hostNameAttrKey {
query.Filters.Items[idx].Key.Key = k8sNodeNameAttrKey
}
}
}
}
}
hostAttrs, err := h.getMetadataAttributes(ctx, req, hostNameAttrKey)
if err != nil {
return nil, err
if !hasHostNameInOrEqual {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{
Key: hostNameAttrKey,
},
Value: topHosts,
Operator: v3.FilterOperatorIn,
})
}
}
activeHosts, err := h.getActiveHosts(ctx, req, hostNameAttrKey)
if err != nil {
return nil, err
return nil, nil, err
}
queryResponse, _, err := h.querierV2.QueryRange(ctx, query)
if err != nil {
return nil, err
return nil, nil, err
}
type hostTSInfo struct {
@@ -321,7 +354,7 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
formulaResult, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return nil, err
return nil, nil, err
}
for _, result := range formulaResult {
@@ -383,7 +416,6 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
if ok {
record.Load15 = load15
}
record.Meta = hostAttrs[record.HostName]
record.Active = activeHosts[record.HostName]
if hostTSInfoMap[record.HostName] != nil {
record.CPUTimeSeries = hostTSInfoMap[record.HostName].cpuTimeSeries
@@ -394,7 +426,7 @@ func (h *HostsRepo) getHostsForQuery(ctx context.Context,
records = append(records, record)
}
return records, nil
return records, allHosts, nil
}
func dedupRecords(records []model.HostListRecord) []model.HostListRecord {
@@ -414,104 +446,40 @@ func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest)
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
resp := model.HostListResponse{
Type: "list",
}
vmRecords, err := h.getHostsForQuery(ctx, req, &NonK8STableListQuery, "host_name")
vmRecords, vmAllHosts, err := h.getHostsForQuery(ctx, req, &NonK8STableListQuery, hostNameAttrKey)
if err != nil {
return resp, err
}
k8sRecords, err := h.getHostsForQuery(ctx, req, &K8STableListQuery, "k8s_node_name")
k8sRecords, k8sAllHosts, err := h.getHostsForQuery(ctx, req, &K8STableListQuery, k8sNodeNameAttrKey)
if err != nil {
return resp, err
}
uniqueHosts := map[string]bool{}
for _, host := range vmAllHosts {
uniqueHosts[host] = true
}
for _, host := range k8sAllHosts {
uniqueHosts[host] = true
}
records := append(vmRecords, k8sRecords...)
// since we added the fix for incorrect host name, it is possible that both host_name and k8s_node_name
// are present in the response. we need to dedup the results.
records = dedupRecords(records)
resp.Total = len(records)
resp.Total = len(uniqueHosts)
if req.Offset > 0 {
records = records[req.Offset:]
}
if req.Limit > 0 && len(records) > req.Limit {
records = records[:req.Limit]
}
resp.Records = records
if len(req.GroupBy) > 0 {
groups := []model.HostListGroup{}
groupMap := make(map[string][]model.HostListRecord)
for _, record := range records {
groupKey := getGroupKey(record, req.GroupBy)
if _, ok := groupMap[groupKey]; !ok {
groupMap[groupKey] = []model.HostListRecord{record}
} else {
groupMap[groupKey] = append(groupMap[groupKey], record)
}
}
// calculate the group stats, active hosts, etc.
for _, records := range groupMap {
var avgCPU, avgMemory, avgWait, avgLoad15 float64
var validCPU, validMemory, validWait, validLoad15, activeHosts int
for _, record := range records {
if !math.IsNaN(record.CPU) {
avgCPU += record.CPU
validCPU++
}
if !math.IsNaN(record.Memory) {
avgMemory += record.Memory
validMemory++
}
if !math.IsNaN(record.Wait) {
avgWait += record.Wait
validWait++
}
if !math.IsNaN(record.Load15) {
avgLoad15 += record.Load15
validLoad15++
}
if record.Active {
activeHosts++
}
}
avgCPU /= float64(validCPU)
avgMemory /= float64(validMemory)
avgWait /= float64(validWait)
avgLoad15 /= float64(validLoad15)
inactiveHosts := len(records) - activeHosts
// take any record and make it as the group meta
firstRecord := records[0]
var groupValues []string
for _, key := range req.GroupBy {
groupValues = append(groupValues, firstRecord.Meta[key.Key])
}
hostNames := []string{}
for _, record := range records {
hostNames = append(hostNames, record.HostName)
}
groups = append(groups, model.HostListGroup{
GroupValues: groupValues,
Active: activeHosts,
Inactive: inactiveHosts,
GroupCPUAvg: avgCPU,
GroupMemoryAvg: avgMemory,
GroupWaitAvg: avgWait,
GroupLoad15Avg: avgLoad15,
HostNames: hostNames,
})
}
resp.Groups = groups
resp.Type = "grouped_list"
}
return resp, nil
}

View File

@@ -0,0 +1,329 @@
package inframetrics
import (
"context"
"math"
"sort"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/postprocess"
"golang.org/x/exp/slices"
)
var (
metricToUseForNamespaces = "k8s_pod_cpu_utilization"
namespaceAttrsToEnrich = []string{
"k8s_namespace_name",
"k8s_cluster_name",
}
queryNamesForNamespaces = map[string][]string{
"cpu": {"A"},
"memory": {"D"},
}
namespaceQueryNames = []string{"A", "D"}
attributesKeysForNamespaces = []v3.AttributeKey{
{Key: "k8s_namespace_name"},
{Key: "k8s_cluster_name"},
}
k8sNamespaceNameAttrKey = "k8s_namespace_name"
)
type NamespacesRepo struct {
reader interfaces.Reader
querierV2 interfaces.Querier
}
func NewNamespacesRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *NamespacesRepo {
return &NamespacesRepo{reader: reader, querierV2: querierV2}
}
func (p *NamespacesRepo) GetNamespaceAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
return &v3.FilterAttributeKeyResponse{AttributeKeys: attributesKeysForNamespaces}, nil
}
func (p *NamespacesRepo) GetNamespaceAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForNamespaces
if req.Limit == 0 {
req.Limit = 50
}
attributeValuesResponse, err := p.reader.GetMetricAttributeValues(ctx, &req)
if err != nil {
return nil, err
}
return attributeValuesResponse, nil
}
func (p *NamespacesRepo) getMetadataAttributes(ctx context.Context, req model.NamespaceListRequest) (map[string]map[string]string, error) {
namespaceAttrs := map[string]map[string]string{}
for _, key := range namespaceAttrsToEnrich {
hasKey := false
for _, groupByKey := range req.GroupBy {
if groupByKey.Key == key {
hasKey = true
break
}
}
if !hasKey {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key})
}
}
mq := v3.BuilderQuery{
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricToUseForNamespaces,
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
query = localQueryToDistributedQuery(query)
attrsListResponse, err := p.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
namespaceName := stringData[k8sNamespaceNameAttrKey]
if _, ok := namespaceAttrs[namespaceName]; !ok {
namespaceAttrs[namespaceName] = map[string]string{}
}
for _, key := range req.GroupBy {
namespaceAttrs[namespaceName][key.Key] = stringData[key.Key]
}
}
return namespaceAttrs, nil
}
func (p *NamespacesRepo) getTopNamespaceGroups(ctx context.Context, req model.NamespaceListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopNamespaces(req)
queryNames := queryNamesForNamespaces[req.OrderBy.ColumnName]
topNamespaceGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topNamespaceGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, topNamespaceGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topNamespaceGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
paginatedTopNamespaceGroupsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
topNamespaceGroups := []map[string]string{}
for _, series := range paginatedTopNamespaceGroupsSeries {
topNamespaceGroups = append(topNamespaceGroups, series.Labels)
}
allNamespaceGroups := []map[string]string{}
for _, series := range formattedResponse[0].Series {
allNamespaceGroups = append(allNamespaceGroups, series.Labels)
}
return topNamespaceGroups, allNamespaceGroups, nil
}
func (p *NamespacesRepo) GetNamespaceList(ctx context.Context, req model.NamespaceListRequest) (model.NamespaceListResponse, error) {
resp := model.NamespaceListResponse{}
if req.Limit == 0 {
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
if req.GroupBy == nil {
req.GroupBy = []v3.AttributeKey{{Key: k8sNamespaceNameAttrKey}}
resp.Type = model.ResponseTypeList
} else {
resp.Type = model.ResponseTypeGroupedList
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := PodsTableListQuery.Clone()
query.Start = req.Start
query.End = req.End
query.Step = step
for _, q := range query.CompositeQuery.BuilderQueries {
if !slices.Contains(namespaceQueryNames, q.QueryName) {
delete(query.CompositeQuery.BuilderQueries, q.QueryName)
}
q.StepInterval = step
if req.Filters != nil && len(req.Filters.Items) > 0 {
if q.Filters == nil {
q.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
q.Filters.Items = append(q.Filters.Items, req.Filters.Items...)
}
q.GroupBy = req.GroupBy
}
namespaceAttrs, err := p.getMetadataAttributes(ctx, req)
if err != nil {
return resp, err
}
topNamespaceGroups, allNamespaceGroups, err := p.getTopNamespaceGroups(ctx, req, query)
if err != nil {
return resp, err
}
groupFilters := map[string][]string{}
for _, topNamespaceGroup := range topNamespaceGroups {
for k, v := range topNamespaceGroup {
groupFilters[k] = append(groupFilters[k], v)
}
}
for groupKey, groupValues := range groupFilters {
hasGroupFilter := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, filter := range req.Filters.Items {
if filter.Key.Key == groupKey {
hasGroupFilter = true
break
}
}
}
if !hasGroupFilter {
for _, query := range query.CompositeQuery.BuilderQueries {
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{Key: groupKey},
Value: groupValues,
Operator: v3.FilterOperatorIn,
})
}
}
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, query)
if err != nil {
return resp, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return resp, err
}
records := []model.NamespaceListRecord{}
for _, result := range formattedResponse {
for _, row := range result.Table.Rows {
record := model.NamespaceListRecord{
CPUUsage: -1,
MemoryUsage: -1,
}
if name, ok := row.Data[k8sNamespaceNameAttrKey].(string); ok {
record.NamespaceName = name
}
if cpu, ok := row.Data["A"].(float64); ok {
record.CPUUsage = cpu
}
if memory, ok := row.Data["D"].(float64); ok {
record.MemoryUsage = memory
}
record.Meta = map[string]string{}
if _, ok := namespaceAttrs[record.NamespaceName]; ok {
record.Meta = namespaceAttrs[record.NamespaceName]
}
for k, v := range row.Data {
if slices.Contains(namespaceQueryNames, k) {
continue
}
if labelValue, ok := v.(string); ok {
record.Meta[k] = labelValue
}
}
records = append(records, record)
}
}
resp.Total = len(allNamespaceGroups)
resp.Records = records
return resp, nil
}

View File

@@ -0,0 +1,349 @@
package inframetrics
import (
"context"
"math"
"sort"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/postprocess"
"golang.org/x/exp/slices"
)
var (
metricToUseForNodes = "k8s_node_cpu_utilization"
nodeAttrsToEnrich = []string{"k8s_node_name", "k8s_node_uid"}
k8sNodeUIDAttrKey = "k8s_node_uid"
queryNamesForNodes = map[string][]string{
"cpu": {"A"},
"cpu_allocatable": {"B"},
"memory": {"C"},
"memory_allocatable": {"D"},
}
nodeQueryNames = []string{"A", "B", "C", "D"}
metricNamesForNodes = map[string]string{
"cpu": "k8s_node_cpu_utilization",
"cpu_allocatable": "k8s_node_allocatable_cpu",
"memory": "k8s_node_memory_usage",
"memory_allocatable": "k8s_node_allocatable_memory",
}
)
type NodesRepo struct {
reader interfaces.Reader
querierV2 interfaces.Querier
}
func NewNodesRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *NodesRepo {
return &NodesRepo{reader: reader, querierV2: querierV2}
}
func (n *NodesRepo) GetNodeAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForNodes
if req.Limit == 0 {
req.Limit = 50
}
attributeKeysResponse, err := n.reader.GetMetricAttributeKeys(ctx, &req)
if err != nil {
return nil, err
}
return attributeKeysResponse, nil
}
func (n *NodesRepo) GetNodeAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForNodes
if req.Limit == 0 {
req.Limit = 50
}
attributeValuesResponse, err := n.reader.GetMetricAttributeValues(ctx, &req)
if err != nil {
return nil, err
}
return attributeValuesResponse, nil
}
func (p *NodesRepo) getMetadataAttributes(ctx context.Context, req model.NodeListRequest) (map[string]map[string]string, error) {
nodeAttrs := map[string]map[string]string{}
for _, key := range nodeAttrsToEnrich {
hasKey := false
for _, groupByKey := range req.GroupBy {
if groupByKey.Key == key {
hasKey = true
break
}
}
if !hasKey {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key})
}
}
mq := v3.BuilderQuery{
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricToUseForNodes,
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
query = localQueryToDistributedQuery(query)
attrsListResponse, err := p.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
nodeUID := stringData[k8sNodeUIDAttrKey]
if _, ok := nodeAttrs[nodeUID]; !ok {
nodeAttrs[nodeUID] = map[string]string{}
}
for _, key := range req.GroupBy {
nodeAttrs[nodeUID][key.Key] = stringData[key.Key]
}
}
return nodeAttrs, nil
}
func (p *NodesRepo) getTopNodeGroups(ctx context.Context, req model.NodeListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopNodes(req)
queryNames := queryNamesForNodes[req.OrderBy.ColumnName]
topNodeGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topNodeGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, topNodeGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topNodeGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
max := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series)))
paginatedTopNodeGroupsSeries := formattedResponse[0].Series[req.Offset:int(max)]
topNodeGroups := []map[string]string{}
for _, series := range paginatedTopNodeGroupsSeries {
topNodeGroups = append(topNodeGroups, series.Labels)
}
allNodeGroups := []map[string]string{}
for _, series := range formattedResponse[0].Series {
allNodeGroups = append(allNodeGroups, series.Labels)
}
return topNodeGroups, allNodeGroups, nil
}
func (p *NodesRepo) GetNodeList(ctx context.Context, req model.NodeListRequest) (model.NodeListResponse, error) {
resp := model.NodeListResponse{}
if req.Limit == 0 {
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
if req.GroupBy == nil {
req.GroupBy = []v3.AttributeKey{{Key: k8sNodeUIDAttrKey}}
resp.Type = model.ResponseTypeList
} else {
resp.Type = model.ResponseTypeGroupedList
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := NodesTableListQuery.Clone()
query.Start = req.Start
query.End = req.End
query.Step = step
for _, query := range query.CompositeQuery.BuilderQueries {
query.StepInterval = step
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
query.GroupBy = req.GroupBy
}
nodeAttrs, err := p.getMetadataAttributes(ctx, req)
if err != nil {
return resp, err
}
topNodeGroups, allNodeGroups, err := p.getTopNodeGroups(ctx, req, query)
if err != nil {
return resp, err
}
groupFilters := map[string][]string{}
for _, topNodeGroup := range topNodeGroups {
for k, v := range topNodeGroup {
groupFilters[k] = append(groupFilters[k], v)
}
}
for groupKey, groupValues := range groupFilters {
hasGroupFilter := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, filter := range req.Filters.Items {
if filter.Key.Key == groupKey {
hasGroupFilter = true
break
}
}
}
if !hasGroupFilter {
for _, query := range query.CompositeQuery.BuilderQueries {
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{Key: groupKey},
Value: groupValues,
Operator: v3.FilterOperatorIn,
})
}
}
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, query)
if err != nil {
return resp, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return resp, err
}
records := []model.NodeListRecord{}
for _, result := range formattedResponse {
for _, row := range result.Table.Rows {
record := model.NodeListRecord{
NodeCPUUsage: -1,
NodeCPUAllocatable: -1,
NodeMemoryUsage: -1,
NodeMemoryAllocatable: -1,
}
if nodeUID, ok := row.Data[k8sNodeUIDAttrKey].(string); ok {
record.NodeUID = nodeUID
}
if cpu, ok := row.Data["A"].(float64); ok {
record.NodeCPUUsage = cpu
}
if cpuAllocatable, ok := row.Data["B"].(float64); ok {
record.NodeCPUAllocatable = cpuAllocatable
}
if mem, ok := row.Data["C"].(float64); ok {
record.NodeMemoryUsage = mem
}
if memory, ok := row.Data["D"].(float64); ok {
record.NodeMemoryAllocatable = memory
}
record.Meta = map[string]string{}
if _, ok := nodeAttrs[record.NodeUID]; ok {
record.Meta = nodeAttrs[record.NodeUID]
}
for k, v := range row.Data {
if slices.Contains(nodeQueryNames, k) {
continue
}
if labelValue, ok := v.(string); ok {
record.Meta[k] = labelValue
}
}
records = append(records, record)
}
}
resp.Total = len(allNodeGroups)
resp.Records = records
return resp, nil
}

View File

@@ -0,0 +1,118 @@
package inframetrics
import v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
var NodesTableListQuery = v3.QueryRangeParamsV3{
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{
// node cpu utilization
"A": {
QueryName: "A",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForNodes["cpu"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sNodeUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "A",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// node cpu allocatable
"B": {
QueryName: "B",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForNodes["cpu_allocatable"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sNodeUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "B",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAnyLast,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// node memory utilization
"C": {
QueryName: "C",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForNodes["memory"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sNodeUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "C",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// node memory allocatable
"D": {
QueryName: "D",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForNodes["memory_allocatable"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sNodeUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "D",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAnyLast,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
},
PanelType: v3.PanelTypeTable,
QueryType: v3.QueryTypeBuilder,
},
Version: "v4",
FormatForWeb: true,
}

View File

@@ -0,0 +1,387 @@
package inframetrics
import (
"context"
"math"
"sort"
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/postprocess"
"golang.org/x/exp/slices"
)
var (
metricToUseForPods = "k8s_pod_cpu_utilization"
podAttrsToEnrich = []string{
"k8s_pod_uid",
"k8s_pod_name",
"k8s_namespace_name",
"k8s_node_name",
"k8s_deployment_name",
"k8s_statefulset_name",
"k8s_daemonset_name",
"k8s_job_name",
"k8s_cronjob_name",
}
k8sPodUIDAttrKey = "k8s_pod_uid"
queryNamesForPods = map[string][]string{
"cpu": {"A"},
"cpu_request": {"B", "A"},
"cpu_limit": {"C", "A"},
"memory": {"D"},
"memory_request": {"E", "D"},
"memory_limit": {"F", "D"},
"restarts": {"G", "A"},
}
podQueryNames = []string{"A", "B", "C", "D", "E", "F", "G"}
metricNamesForPods = map[string]string{
"cpu": "k8s_pod_cpu_utilization",
"cpu_request": "k8s_pod_cpu_request_utilization",
"cpu_limit": "k8s_pod_cpu_limit_utilization",
"memory": "k8s_pod_memory_usage",
"memory_request": "k8s_pod_memory_request_utilization",
"memory_limit": "k8s_pod_memory_limit_utilization",
"restarts": "k8s_container_restarts",
}
)
type PodsRepo struct {
reader interfaces.Reader
querierV2 interfaces.Querier
}
func NewPodsRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *PodsRepo {
return &PodsRepo{reader: reader, querierV2: querierV2}
}
func (p *PodsRepo) GetPodAttributeKeys(ctx context.Context, req v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
// TODO(srikanthccv): remove hardcoded metric name and support keys from any pod metric
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForPods
if req.Limit == 0 {
req.Limit = 50
}
attributeKeysResponse, err := p.reader.GetMetricAttributeKeys(ctx, &req)
if err != nil {
return nil, err
}
// TODO(srikanthccv): only return resource attributes when we have a way to
// distinguish between resource attributes and other attributes.
filteredKeys := []v3.AttributeKey{}
for _, key := range attributeKeysResponse.AttributeKeys {
if slices.Contains(pointAttrsToIgnore, key.Key) {
continue
}
filteredKeys = append(filteredKeys, key)
}
return &v3.FilterAttributeKeyResponse{AttributeKeys: filteredKeys}, nil
}
func (p *PodsRepo) GetPodAttributeValues(ctx context.Context, req v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
req.DataSource = v3.DataSourceMetrics
req.AggregateAttribute = metricToUseForPods
if req.Limit == 0 {
req.Limit = 50
}
attributeValuesResponse, err := p.reader.GetMetricAttributeValues(ctx, &req)
if err != nil {
return nil, err
}
return attributeValuesResponse, nil
}
func (p *PodsRepo) getMetadataAttributes(ctx context.Context, req model.PodListRequest) (map[string]map[string]string, error) {
podAttrs := map[string]map[string]string{}
for _, key := range podAttrsToEnrich {
hasKey := false
for _, groupByKey := range req.GroupBy {
if groupByKey.Key == key {
hasKey = true
break
}
}
if !hasKey {
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key})
}
}
mq := v3.BuilderQuery{
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricToUseForPods,
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
GroupBy: req.GroupBy,
}
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
if err != nil {
return nil, err
}
query = localQueryToDistributedQuery(query)
attrsListResponse, err := p.reader.GetListResultV3(ctx, query)
if err != nil {
return nil, err
}
for _, row := range attrsListResponse {
stringData := map[string]string{}
for key, value := range row.Data {
if str, ok := value.(string); ok {
stringData[key] = str
} else if strPtr, ok := value.(*string); ok {
stringData[key] = *strPtr
}
}
podName := stringData[k8sPodUIDAttrKey]
if _, ok := podAttrs[podName]; !ok {
podAttrs[podName] = map[string]string{}
}
for _, key := range req.GroupBy {
podAttrs[podName][key.Key] = stringData[key.Key]
}
}
return podAttrs, nil
}
func (p *PodsRepo) getTopPodGroups(ctx context.Context, req model.PodListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
step, timeSeriesTableName, samplesTableName := getParamsForTopPods(req)
queryNames := queryNamesForPods[req.OrderBy.ColumnName]
topPodGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
Start: req.Start,
End: req.End,
Step: step,
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{},
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
},
}
for _, queryName := range queryNames {
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
query.StepInterval = step
query.MetricTableHints = &v3.MetricTableHints{
TimeSeriesTableName: timeSeriesTableName,
SamplesTableName: samplesTableName,
}
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
topPodGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, topPodGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topPodGroupsQueryRangeParams)
if err != nil {
return nil, nil, err
}
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
return nil, nil, nil
}
if req.OrderBy.Order == v3.DirectionDesc {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
})
} else {
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
})
}
paginatedTopPodGroupsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
topPodGroups := []map[string]string{}
for _, series := range paginatedTopPodGroupsSeries {
topPodGroups = append(topPodGroups, series.Labels)
}
allPodGroups := []map[string]string{}
for _, series := range formattedResponse[0].Series {
allPodGroups = append(allPodGroups, series.Labels)
}
return topPodGroups, allPodGroups, nil
}
func (p *PodsRepo) GetPodList(ctx context.Context, req model.PodListRequest) (model.PodListResponse, error) {
resp := model.PodListResponse{}
if req.Limit == 0 {
req.Limit = 10
}
if req.OrderBy == nil {
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
}
if req.GroupBy == nil {
req.GroupBy = []v3.AttributeKey{{Key: k8sPodUIDAttrKey}}
resp.Type = model.ResponseTypeList
} else {
resp.Type = model.ResponseTypeGroupedList
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := PodsTableListQuery.Clone()
query.Start = req.Start
query.End = req.End
query.Step = step
for _, query := range query.CompositeQuery.BuilderQueries {
query.StepInterval = step
if req.Filters != nil && len(req.Filters.Items) > 0 {
if query.Filters == nil {
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
}
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
}
query.GroupBy = req.GroupBy
}
podAttrs, err := p.getMetadataAttributes(ctx, req)
if err != nil {
return resp, err
}
topPodGroups, allPodGroups, err := p.getTopPodGroups(ctx, req, query)
if err != nil {
return resp, err
}
groupFilters := map[string][]string{}
for _, topPodGroup := range topPodGroups {
for k, v := range topPodGroup {
groupFilters[k] = append(groupFilters[k], v)
}
}
for groupKey, groupValues := range groupFilters {
hasGroupFilter := false
if req.Filters != nil && len(req.Filters.Items) > 0 {
for _, filter := range req.Filters.Items {
if filter.Key.Key == groupKey {
hasGroupFilter = true
break
}
}
}
if !hasGroupFilter {
for _, query := range query.CompositeQuery.BuilderQueries {
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
Key: v3.AttributeKey{Key: groupKey},
Value: groupValues,
Operator: v3.FilterOperatorIn,
})
}
}
}
queryResponse, _, err := p.querierV2.QueryRange(ctx, query)
if err != nil {
return resp, err
}
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
if err != nil {
return resp, err
}
records := []model.PodListRecord{}
for _, result := range formattedResponse {
for _, row := range result.Table.Rows {
record := model.PodListRecord{
PodCPU: -1,
PodCPURequest: -1,
PodCPULimit: -1,
PodMemory: -1,
PodMemoryRequest: -1,
PodMemoryLimit: -1,
RestartCount: -1,
}
if podUID, ok := row.Data[k8sPodUIDAttrKey].(string); ok {
record.PodUID = podUID
}
if cpu, ok := row.Data["A"].(float64); ok {
record.PodCPU = cpu
}
if cpuRequest, ok := row.Data["B"].(float64); ok {
record.PodCPURequest = cpuRequest
}
if cpuLimit, ok := row.Data["C"].(float64); ok {
record.PodCPULimit = cpuLimit
}
if memory, ok := row.Data["D"].(float64); ok {
record.PodMemory = memory
}
if memoryRequest, ok := row.Data["E"].(float64); ok {
record.PodMemoryRequest = memoryRequest
}
if memoryLimit, ok := row.Data["F"].(float64); ok {
record.PodMemoryLimit = memoryLimit
}
if restarts, ok := row.Data["G"].(float64); ok {
record.RestartCount = int(restarts)
}
record.Meta = map[string]string{}
if _, ok := podAttrs[record.PodUID]; ok {
record.Meta = podAttrs[record.PodUID]
}
for k, v := range row.Data {
if slices.Contains(podQueryNames, k) {
continue
}
if labelValue, ok := v.(string); ok {
record.Meta[k] = labelValue
}
}
records = append(records, record)
}
}
resp.Total = len(allPodGroups)
resp.Records = records
return resp, nil
}

View File

@@ -0,0 +1,196 @@
package inframetrics
import v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
var PodsTableListQuery = v3.QueryRangeParamsV3{
CompositeQuery: &v3.CompositeQuery{
BuilderQueries: map[string]*v3.BuilderQuery{
// pod cpu utilization
"A": {
QueryName: "A",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForPods["cpu"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sPodUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "A",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// pod cpu request utilization
"B": {
QueryName: "B",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForPods["cpu_request"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sPodUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "B",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// pod cpu limit utilization
"C": {
QueryName: "C",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForPods["cpu_limit"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sPodUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "C",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// pod memory utilization
"D": {
QueryName: "D",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForPods["memory"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sPodUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "D",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// pod memory request utilization
"E": {
QueryName: "E",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForPods["memory_request"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sPodUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "E",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
// pod memory limit utilization
"F": {
QueryName: "F",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForPods["memory_limit"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sPodUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "F",
ReduceTo: v3.ReduceToOperatorAvg,
TimeAggregation: v3.TimeAggregationAvg,
SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false,
},
"G": {
QueryName: "G",
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: metricNamesForPods["restarts"],
DataType: v3.AttributeKeyDataTypeFloat64,
},
Temporality: v3.Unspecified,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{},
},
GroupBy: []v3.AttributeKey{
{
Key: k8sPodUIDAttrKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
Expression: "G",
ReduceTo: v3.ReduceToOperatorSum,
TimeAggregation: v3.TimeAggregationAnyLast,
SpaceAggregation: v3.SpaceAggregationMax,
Functions: []v3.Function{{Name: v3.FunctionNameRunningDiff}},
Disabled: false,
},
},
PanelType: v3.PanelTypeTable,
QueryType: v3.QueryTypeBuilder,
},
Version: "v4",
FormatForWeb: true,
}

View File

@@ -16,8 +16,30 @@ var (
oneWeekInMilliseconds = oneDayInMilliseconds * 7
)
// start and end are in milliseconds
func whichTSTableToUse(start, end int64) (int64, int64, string) {
func whichTSTableToUse(start, end int64, mq *v3.BuilderQuery) (int64, int64, string) {
// if we have a hint for the table, we need to use it
// the hint will be used to override the default table selection logic
if mq.MetricTableHints != nil {
if mq.MetricTableHints.TimeSeriesTableName != "" {
switch mq.MetricTableHints.TimeSeriesTableName {
case constants.SIGNOZ_TIMESERIES_v4_LOCAL_TABLENAME:
// adjust the start time to nearest 1 hour
start = start - (start % (time.Hour.Milliseconds() * 1))
case constants.SIGNOZ_TIMESERIES_v4_6HRS_LOCAL_TABLENAME:
// adjust the start time to nearest 6 hours
start = start - (start % (time.Hour.Milliseconds() * 6))
case constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME:
// adjust the start time to nearest 1 day
start = start - (start % (time.Hour.Milliseconds() * 24))
case constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME:
// adjust the start time to nearest 1 week
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
}
return start, end, mq.MetricTableHints.TimeSeriesTableName
}
}
// If time range is less than 6 hours, we need to use the `time_series_v4` table
// else if time range is less than 1 day and greater than 6 hours, we need to use the `time_series_v4_6hrs` table
// else if time range is less than 1 week and greater than 1 day, we need to use the `time_series_v4_1day` table
@@ -58,6 +80,14 @@ func whichTSTableToUse(start, end int64) (int64, int64, string) {
// if the `timeAggregation` is `count_distinct` we can't use the aggregated tables because they don't support it
func WhichSamplesTableToUse(start, end int64, mq *v3.BuilderQuery) string {
// if we have a hint for the table, we need to use it
// the hint will be used to override the default table selection logic
if mq.MetricTableHints != nil {
if mq.MetricTableHints.SamplesTableName != "" {
return mq.MetricTableHints.SamplesTableName
}
}
// we don't have any aggregated table for sketches (yet)
if mq.AggregateAttribute.Type == v3.AttributeKeyType(v3.MetricTypeExponentialHistogram) {
return constants.SIGNOZ_EXP_HISTOGRAM_TABLENAME
@@ -234,7 +264,7 @@ func PrepareTimeseriesFilterQuery(start, end int64, mq *v3.BuilderQuery) (string
conditions = append(conditions, fmt.Sprintf("metric_name = %s", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key)))
conditions = append(conditions, fmt.Sprintf("temporality = '%s'", mq.Temporality))
start, end, tableName := whichTSTableToUse(start, end)
start, end, tableName := whichTSTableToUse(start, end, mq)
conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", start, end))
@@ -314,7 +344,7 @@ func PrepareTimeseriesFilterQueryV3(start, end int64, mq *v3.BuilderQuery) (stri
conditions = append(conditions, fmt.Sprintf("metric_name = %s", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key)))
conditions = append(conditions, fmt.Sprintf("temporality = '%s'", mq.Temporality))
start, end, tableName := whichTSTableToUse(start, end)
start, end, tableName := whichTSTableToUse(start, end, mq)
conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", start, end))

View File

@@ -750,9 +750,6 @@ func parseInviteUsersRequest(r *http.Request) (*model.BulkInviteRequest, error)
if req.Users[i].Email == "" {
return nil, fmt.Errorf("email is required for each user")
}
if req.Users[i].Name == "" {
return nil, fmt.Errorf("name is required for each user")
}
if req.Users[i].FrontendBaseUrl == "" {
return nil, fmt.Errorf("frontendBaseUrl is required for each user")
}
@@ -1145,25 +1142,7 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
}
}
// Remove the time shift function from the list of functions and set the shift by value
var timeShiftBy int64
if len(query.Functions) > 0 {
for idx := range query.Functions {
function := &query.Functions[idx]
if function.Name == v3.FunctionNameTimeShift {
// move the function to the beginning of the list
// so any other function can use the shifted time
var fns []v3.Function
fns = append(fns, *function)
fns = append(fns, query.Functions[:idx]...)
fns = append(fns, query.Functions[idx+1:]...)
query.Functions = fns
timeShiftBy = int64(function.Args[0].(float64))
break
}
}
}
query.ShiftBy = timeShiftBy
query.SetShiftByFromFunc()
if query.Filters == nil || len(query.Filters.Items) == 0 {
continue

View File

@@ -297,6 +297,8 @@ func TestParseQueryRangeParamsCompositeQuery(t *testing.T) {
compositeQuery v3.CompositeQuery
expectErr bool
errMsg string
hasShiftBy bool
shiftBy int64
}{
{
desc: "no query in request",
@@ -496,6 +498,56 @@ func TestParseQueryRangeParamsCompositeQuery(t *testing.T) {
expectErr: true,
errMsg: "builder query A is invalid: group by is invalid",
},
{
desc: "builder query with shift by",
compositeQuery: v3.CompositeQuery{
PanelType: v3.PanelTypeGraph,
QueryType: v3.QueryTypeBuilder,
BuilderQueries: map[string]*v3.BuilderQuery{
"A": {
QueryName: "A",
DataSource: "logs",
AggregateOperator: "sum",
AggregateAttribute: v3.AttributeKey{Key: "attribute"},
GroupBy: []v3.AttributeKey{{Key: "group_key"}},
Expression: "A",
Functions: []v3.Function{
{
Name: v3.FunctionNameTimeShift,
Args: []interface{}{float64(10)},
},
},
},
},
},
hasShiftBy: true,
shiftBy: 10,
},
{
desc: "builder query with shift by as string",
compositeQuery: v3.CompositeQuery{
PanelType: v3.PanelTypeGraph,
QueryType: v3.QueryTypeBuilder,
BuilderQueries: map[string]*v3.BuilderQuery{
"A": {
QueryName: "A",
DataSource: "logs",
AggregateOperator: "sum",
AggregateAttribute: v3.AttributeKey{Key: "attribute"},
GroupBy: []v3.AttributeKey{{Key: "group_key"}},
Expression: "A",
Functions: []v3.Function{
{
Name: v3.FunctionNameTimeShift,
Args: []interface{}{"3600"},
},
},
},
},
},
hasShiftBy: true,
shiftBy: 3600,
},
}
for _, tc := range reqCases {
@@ -514,13 +566,16 @@ func TestParseQueryRangeParamsCompositeQuery(t *testing.T) {
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body)
_, apiErr := ParseQueryRangeParams(req)
params, apiErr := ParseQueryRangeParams(req)
if tc.expectErr {
require.Error(t, apiErr)
require.Contains(t, apiErr.Error(), tc.errMsg)
} else {
require.Nil(t, apiErr)
}
if tc.hasShiftBy {
require.Equal(t, tc.shiftBy, params.CompositeQuery.BuilderQueries["A"].ShiftBy)
}
})
}
}

View File

@@ -1,37 +1,14 @@
package preferences
var preferenceMap = map[string]Preference{
"DASHBOARDS_LIST_VIEW": {
Key: "DASHBOARDS_LIST_VIEW",
Name: "Dashboards List View",
Description: "",
ValueType: "string",
DefaultValue: "grid",
AllowedValues: []interface{}{"grid", "list"},
IsDiscreteValues: true,
AllowedScopes: []string{"user", "org"},
},
"LOGS_TOOLBAR_COLLAPSED": {
Key: "LOGS_TOOLBAR_COLLAPSED",
Name: "Logs toolbar",
Description: "",
"ORG_ONBOARDING": {
Key: "ORG_ONBOARDING",
Name: "Organisation Onboarding",
Description: "Organisation Onboarding",
ValueType: "boolean",
DefaultValue: false,
AllowedValues: []interface{}{true, false},
IsDiscreteValues: true,
AllowedScopes: []string{"user", "org"},
},
"MAX_DEPTH_ALLOWED": {
Key: "MAX_DEPTH_ALLOWED",
Name: "Max Depth Allowed",
Description: "",
ValueType: "integer",
DefaultValue: 10,
IsDiscreteValues: false,
Range: Range{
Min: 0,
Max: 100,
},
AllowedScopes: []string{"user", "org"},
AllowedScopes: []string{"org"},
},
}

View File

@@ -2,6 +2,15 @@ package model
import v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
type (
ResponseType string
)
const (
ResponseTypeList ResponseType = "list"
ResponseTypeGroupedList ResponseType = "grouped_list"
)
type HostListRequest struct {
Start int64 `json:"start"` // epoch time in ms
End int64 `json:"end"` // epoch time in ms
@@ -80,3 +89,113 @@ type ProcessListGroup struct {
GroupMemoryAvg float64 `json:"groupMemoryAvg"`
ProcessNames []string `json:"processNames"`
}
type PodListRequest struct {
Start int64 `json:"start"` // epoch time in ms
End int64 `json:"end"` // epoch time in ms
Filters *v3.FilterSet `json:"filters"`
GroupBy []v3.AttributeKey `json:"groupBy"`
OrderBy *v3.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
type PodListResponse struct {
Type ResponseType `json:"type"`
Records []PodListRecord `json:"records"`
Total int `json:"total"`
}
type PodListRecord struct {
PodUID string `json:"podUID,omitempty"`
PodCPU float64 `json:"podCPU"`
PodCPURequest float64 `json:"podCPURequest"`
PodCPULimit float64 `json:"podCPULimit"`
PodMemory float64 `json:"podMemory"`
PodMemoryRequest float64 `json:"podMemoryRequest"`
PodMemoryLimit float64 `json:"podMemoryLimit"`
RestartCount int `json:"restartCount"`
Meta map[string]string `json:"meta"`
CountByPhase PodCountByPhase `json:"countByPhase"`
}
type PodCountByPhase struct {
Pending int `json:"pending"`
Running int `json:"running"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Unknown int `json:"unknown"`
}
type NodeListRequest struct {
Start int64 `json:"start"` // epoch time in ms
End int64 `json:"end"` // epoch time in ms
Filters *v3.FilterSet `json:"filters"`
GroupBy []v3.AttributeKey `json:"groupBy"`
OrderBy *v3.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
type NodeListResponse struct {
Type ResponseType `json:"type"`
Records []NodeListRecord `json:"records"`
Total int `json:"total"`
}
type NodeListRecord struct {
NodeUID string `json:"nodeUID,omitempty"`
NodeCPUUsage float64 `json:"nodeCPUUsage"`
NodeCPUAllocatable float64 `json:"nodeCPUAllocatable"`
NodeMemoryUsage float64 `json:"nodeMemoryUsage"`
NodeMemoryAllocatable float64 `json:"nodeMemoryAllocatable"`
Meta map[string]string `json:"meta"`
}
type NamespaceListRequest struct {
Start int64 `json:"start"` // epoch time in ms
End int64 `json:"end"` // epoch time in ms
Filters *v3.FilterSet `json:"filters"`
GroupBy []v3.AttributeKey `json:"groupBy"`
OrderBy *v3.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
type NamespaceListResponse struct {
Type ResponseType `json:"type"`
Records []NamespaceListRecord `json:"records"`
Total int `json:"total"`
}
type NamespaceListRecord struct {
NamespaceName string `json:"namespaceName"`
CPUUsage float64 `json:"cpuUsage"`
MemoryUsage float64 `json:"memoryUsage"`
Meta map[string]string `json:"meta"`
}
type ClusterListRequest struct {
Start int64 `json:"start"` // epoch time in ms
End int64 `json:"end"` // epoch time in ms
Filters *v3.FilterSet `json:"filters"`
GroupBy []v3.AttributeKey `json:"groupBy"`
OrderBy *v3.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
type ClusterListResponse struct {
Type ResponseType `json:"type"`
Records []ClusterListRecord `json:"records"`
Total int `json:"total"`
}
type ClusterListRecord struct {
ClusterUID string `json:"clusterUID"`
CPUUsage float64 `json:"cpuUsage"`
CPUAllocatable float64 `json:"cpuAllocatable"`
MemoryUsage float64 `json:"memoryUsage"`
MemoryAllocatable float64 `json:"memoryAllocatable"`
Meta map[string]string `json:"meta"`
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type DataSource string
@@ -762,6 +763,11 @@ type Function struct {
NamedArgs map[string]interface{} `json:"namedArgs,omitempty"`
}
type MetricTableHints struct {
TimeSeriesTableName string
SamplesTableName string
}
type BuilderQuery struct {
QueryName string `json:"queryName"`
StepInterval int64 `json:"stepInterval"`
@@ -787,6 +793,39 @@ type BuilderQuery struct {
ShiftBy int64
IsAnomaly bool
QueriesUsedInFormula []string
MetricTableHints *MetricTableHints `json:"-"`
}
func (b *BuilderQuery) SetShiftByFromFunc() {
// Remove the time shift function from the list of functions and set the shift by value
var timeShiftBy int64
if len(b.Functions) > 0 {
for idx := range b.Functions {
function := &b.Functions[idx]
if function.Name == FunctionNameTimeShift {
// move the function to the beginning of the list
// so any other function can use the shifted time
var fns []Function
fns = append(fns, *function)
fns = append(fns, b.Functions[:idx]...)
fns = append(fns, b.Functions[idx+1:]...)
b.Functions = fns
if len(function.Args) > 0 {
if shift, ok := function.Args[0].(float64); ok {
timeShiftBy = int64(shift)
} else if shift, ok := function.Args[0].(string); ok {
shiftBy, err := strconv.ParseFloat(shift, 64)
if err != nil {
zap.L().Error("failed to parse time shift by", zap.String("shift", shift), zap.Error(err))
}
timeShiftBy = int64(shiftBy)
}
}
break
}
}
}
b.ShiftBy = timeShiftBy
}
func (b *BuilderQuery) Clone() *BuilderQuery {
@@ -1075,9 +1114,16 @@ func (f *FilterItem) CacheKey() string {
return fmt.Sprintf("key:%s,op:%s,value:%v", f.Key.CacheKey(), f.Operator, f.Value)
}
type Direction string
const (
DirectionAsc Direction = "asc"
DirectionDesc Direction = "desc"
)
type OrderBy struct {
ColumnName string `json:"columnName"`
Order string `json:"order"`
Order Direction `json:"order"`
Key string `json:"-"`
DataType AttributeKeyDataType `json:"-"`
Type AttributeKeyType `json:"-"`

View File

@@ -28,12 +28,12 @@ func PostProcessResult(result []*v3.Result, queryRangeParams *v3.QueryRangeParam
// The function is named applyMetricLimit because it only applies to metrics data source
// In traces and logs, the limit is achieved using subqueries
ApplyMetricLimit(result, queryRangeParams)
// We apply the functions here it's easier to add new functions
ApplyFunctions(result, queryRangeParams)
// Each series in the result produces N number of points, where N is (end - start) / step
// For the panel type table, we need to show one point for each series in the row
// We do that by applying a reduce function to each series
applyReduceTo(result, queryRangeParams)
// We apply the functions here it's easier to add new functions
ApplyFunctions(result, queryRangeParams)
// expressions are executed at query serivce so the value of time.now in the invdividual
// queries will be different so for table panel we are making it same.

View File

@@ -152,6 +152,9 @@ func (r *ThresholdRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3,
if minStep := common.MinAllowedStepInterval(start, end); q.StepInterval < minStep {
q.StepInterval = minStep
}
q.SetShiftByFromFunc()
if q.DataSource == v3.DataSourceMetrics && constants.UseMetricsPreAggregation() {
// if the time range is greater than 1 day, and less than 1 week set the step interval to be multiple of 5 minutes
// if the time range is greater than 1 week, set the step interval to be multiple of 30 mins

View File

@@ -1602,3 +1602,66 @@ func TestThresholdRuleLogsLink(t *testing.T) {
}
}
}
func TestThresholdRuleShiftBy(t *testing.T) {
target := float64(10)
postableRule := PostableRule{
AlertName: "Logs link test",
AlertType: AlertTypeLogs,
RuleType: RuleTypeThreshold,
EvalWindow: Duration(5 * time.Minute),
Frequency: Duration(1 * time.Minute),
RuleCondition: &RuleCondition{
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
BuilderQueries: map[string]*v3.BuilderQuery{
"A": {
QueryName: "A",
StepInterval: 60,
AggregateAttribute: v3.AttributeKey{
Key: "component",
},
AggregateOperator: v3.AggregateOperatorCountDistinct,
DataSource: v3.DataSourceLogs,
Expression: "A",
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{Key: "k8s.container.name", IsColumn: false, Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeString},
Value: "testcontainer",
Operator: v3.FilterOperatorEqual,
},
},
},
Functions: []v3.Function{
{
Name: v3.FunctionNameTimeShift,
Args: []interface{}{float64(10)},
},
},
},
},
},
Target: &target,
CompareOp: ValueAboveOrEq,
},
}
rule, err := NewThresholdRule("69", &postableRule, nil, nil, true)
if err != nil {
assert.NoError(t, err)
}
rule.TemporalityMap = map[string]map[v3.Temporality]bool{
"signoz_calls_total": {
v3.Delta: true,
},
}
params, err := rule.prepareQueryRange(time.Now())
if err != nil {
assert.NoError(t, err)
}
assert.Equal(t, int64(10), params.CompositeQuery.BuilderQueries["A"].ShiftBy)
}