mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-07 02:50:31 +01:00
Compare commits
34 Commits
host-lists
...
fix/alert-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d1c30a98 | ||
|
|
d7846338ce | ||
|
|
5dac1ad20a | ||
|
|
8d704c331c | ||
|
|
f8e47496fa | ||
|
|
6fef9d9676 | ||
|
|
190767fd0a | ||
|
|
1e78786cae | ||
|
|
6448fb17e7 | ||
|
|
f2e33d7ca9 | ||
|
|
6c7167a224 | ||
|
|
00421235b0 | ||
|
|
0e2b67059b | ||
|
|
910c44cefc | ||
|
|
8bad036423 | ||
|
|
a21830132f | ||
|
|
9419f56e95 | ||
|
|
347868c18b | ||
|
|
17e20e7f41 | ||
|
|
2b0da82f94 | ||
|
|
911362cecf | ||
|
|
481f9620d3 | ||
|
|
e5be431f18 | ||
|
|
503ed45a99 | ||
|
|
28818fbaac | ||
|
|
c0e40614bf | ||
|
|
2d732ae4a9 | ||
|
|
8466e31e02 | ||
|
|
efdaf7ee43 | ||
|
|
0dec94a5c6 | ||
|
|
204728ff60 | ||
|
|
e51f4d986d | ||
|
|
337a941d0d | ||
|
|
fc4b55cb34 |
13
Makefile
13
Makefile
@@ -79,7 +79,7 @@ build-query-service-static:
|
||||
@if [ $(DEV_BUILD) != "" ]; then \
|
||||
cd $(QUERY_SERVICE_DIRECTORY) && \
|
||||
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
||||
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
|
||||
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
|
||||
else \
|
||||
cd $(QUERY_SERVICE_DIRECTORY) && \
|
||||
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
||||
@@ -188,13 +188,4 @@ check-no-ee-references:
|
||||
fi
|
||||
|
||||
test:
|
||||
go test ./pkg/query-service/app/metrics/...
|
||||
go test ./pkg/query-service/cache/...
|
||||
go test ./pkg/query-service/app/...
|
||||
go test ./pkg/query-service/app/querier/...
|
||||
go test ./pkg/query-service/converter/...
|
||||
go test ./pkg/query-service/formatter/...
|
||||
go test ./pkg/query-service/tests/integration/...
|
||||
go test ./pkg/query-service/rules/...
|
||||
go test ./pkg/query-service/collectorsimulator/...
|
||||
go test ./pkg/query-service/postprocess/...
|
||||
go test ./pkg/query-service/...
|
||||
|
||||
@@ -34,7 +34,7 @@ x-db-depend: &db-depend
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
otel-collector-migrator:
|
||||
otel-collector-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
# clickhouse-2:
|
||||
# condition: service_healthy
|
||||
@@ -212,11 +212,13 @@ services:
|
||||
volumes:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
otel-collector-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
|
||||
container_name: otel-migrator
|
||||
container_name: otel-migrator-sync
|
||||
command:
|
||||
- "sync"
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
- "--up="
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
@@ -225,6 +227,22 @@ services:
|
||||
# clickhouse-3:
|
||||
# condition: service_healthy
|
||||
|
||||
otel-collector-migrator-async:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
|
||||
container_name: otel-migrator-async
|
||||
command:
|
||||
- "async"
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
- "--up="
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
otel-collector-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
# clickhouse-2:
|
||||
# condition: service_healthy
|
||||
# clickhouse-3:
|
||||
# condition: service_healthy
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.12}
|
||||
@@ -262,7 +280,7 @@ services:
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
otel-collector-migrator:
|
||||
otel-collector-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
query-service:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -9,7 +9,15 @@ import (
|
||||
|
||||
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
if !strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+gateway.AllowedPrefix) {
|
||||
validPath := false
|
||||
for _, allowedPrefix := range gateway.AllowedPrefix {
|
||||
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
|
||||
validPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validPath {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
var (
|
||||
RoutePrefix string = "/api/gateway"
|
||||
AllowedPrefix string = "/v1/workspaces/me"
|
||||
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me"}
|
||||
)
|
||||
|
||||
type proxy struct {
|
||||
|
||||
@@ -373,7 +373,7 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AnomalyDetection,
|
||||
Active: false,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
|
||||
BIN
frontend/public/Images/signoz-hero-image.webp
Normal file
BIN
frontend/public/Images/signoz-hero-image.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
1
frontend/public/css/uPlot.min.css
vendored
Normal file
1
frontend/public/css/uPlot.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
|
||||
BIN
frontend/public/fonts/FiraCode-VariableFont_wght.ttf
Normal file
BIN
frontend/public/fonts/FiraCode-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/Inter-VariableFont_opsz,wght.ttf
Normal file
BIN
frontend/public/fonts/Inter-VariableFont_opsz,wght.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/SpaceMono-Regular.ttf
Normal file
BIN
frontend/public/fonts/SpaceMono-Regular.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/WorkSans-VariableFont_wght.ttf
Normal file
BIN
frontend/public/fonts/WorkSans-VariableFont_wght.ttf
Normal file
Binary file not shown.
18
frontend/src/api/preferences/getAllOrgPreferences.ts
Normal file
18
frontend/src/api/preferences/getAllOrgPreferences.ts
Normal 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;
|
||||
18
frontend/src/api/preferences/getAllUserPreference.ts
Normal file
18
frontend/src/api/preferences/getAllUserPreference.ts
Normal 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;
|
||||
20
frontend/src/api/preferences/getOrgPreference.ts
Normal file
20
frontend/src/api/preferences/getOrgPreference.ts
Normal 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;
|
||||
22
frontend/src/api/preferences/getUserPreference.ts
Normal file
22
frontend/src/api/preferences/getUserPreference.ts
Normal 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;
|
||||
25
frontend/src/api/preferences/updateOrgPreference.ts
Normal file
25
frontend/src/api/preferences/updateOrgPreference.ts
Normal 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;
|
||||
25
frontend/src/api/preferences/updateUserPreference.ts
Normal file
25
frontend/src/api/preferences/updateUserPreference.ts
Normal 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;
|
||||
@@ -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`;
|
||||
|
||||
@@ -129,6 +129,7 @@ function LogDetail({
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
maskStyle={{ background: 'none' }}
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
||||
|
||||
@@ -195,21 +195,20 @@ function ListLogView({
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
$isActiveLog={isHighlighted}
|
||||
$isActiveLog={
|
||||
isHighlighted ||
|
||||
activeLog?.id === logData.id ||
|
||||
activeContextLog?.id === logData.id
|
||||
}
|
||||
$isDarkMode={isDarkMode}
|
||||
$logType={logType}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleDetailedView}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
<div className="log-line">
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
isActive={
|
||||
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||
<div>
|
||||
<LogContainer fontSize={fontSize}>
|
||||
<LogGeneralField
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card, Typography } from 'antd';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground } from 'utils/logs';
|
||||
|
||||
interface LogTextProps {
|
||||
linesPerRow?: number;
|
||||
@@ -15,6 +15,7 @@ interface LogContainerProps {
|
||||
export const Container = styled(Card)<{
|
||||
$isActiveLog: boolean;
|
||||
$isDarkMode: boolean;
|
||||
$logType: string;
|
||||
fontSize: FontSize;
|
||||
}>`
|
||||
width: 100% !important;
|
||||
@@ -41,13 +42,8 @@ export const Container = styled(Card)<{
|
||||
? `padding:0.3rem 0.6rem;`
|
||||
: ``}
|
||||
|
||||
${({ $isActiveLog, $isDarkMode }): string =>
|
||||
$isActiveLog
|
||||
? `background-color: ${
|
||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
||||
} !important`
|
||||
: ''}
|
||||
}
|
||||
${({ $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
getActiveLogBackground($isActiveLog, $isDarkMode, $logType)}
|
||||
`;
|
||||
|
||||
export const Text = styled(Typography.Text)`
|
||||
|
||||
@@ -41,10 +41,4 @@
|
||||
background-color: var(--bg-sakura-500);
|
||||
}
|
||||
}
|
||||
|
||||
&.isActive {
|
||||
.line {
|
||||
background-color: var(--bg-robin-400, #7190f9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,6 @@ describe('LogStateIndicator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders correctly when isActive is true', () => {
|
||||
const { container } = render(
|
||||
<LogStateIndicator type="INFO" isActive fontSize={FontSize.MEDIUM} />,
|
||||
);
|
||||
const indicator = container.firstChild as HTMLElement;
|
||||
expect(indicator.classList.contains('isActive')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correctly with different types', () => {
|
||||
const { container: containerInfo } = render(
|
||||
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
|
||||
|
||||
@@ -44,22 +44,16 @@ export const LogType = {
|
||||
|
||||
function LogStateIndicator({
|
||||
type,
|
||||
isActive,
|
||||
fontSize,
|
||||
}: {
|
||||
type: string;
|
||||
fontSize: FontSize;
|
||||
isActive?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
|
||||
<div className="log-state-indicator">
|
||||
<div className={cx('line', type, fontSize)}> </div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LogStateIndicator.defaultProps = {
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export default LogStateIndicator;
|
||||
|
||||
@@ -162,20 +162,15 @@ function RawLogView({
|
||||
$isDarkMode={isDarkMode}
|
||||
$isReadOnly={isReadOnly}
|
||||
$isHightlightedLog={isHighlighted}
|
||||
$isActiveLog={isActiveLog}
|
||||
$isActiveLog={
|
||||
activeLog?.id === data.id || activeContextLog?.id === data.id || isActiveLog
|
||||
}
|
||||
$logType={logType}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
isActive={
|
||||
activeLog?.id === data.id ||
|
||||
activeContextLog?.id === data.id ||
|
||||
isActiveLog
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||
|
||||
<RawLogContent
|
||||
$isReadOnly={isReadOnly}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isHightlightedLog: boolean;
|
||||
$logType: string;
|
||||
fontSize: FontSize;
|
||||
}>`
|
||||
position: relative;
|
||||
@@ -34,11 +35,12 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
: `margin: 2px 0;`}
|
||||
}
|
||||
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
${({ $isActiveLog, $logType }): string =>
|
||||
getActiveLogBackground($isActiveLog, true, $logType)}
|
||||
|
||||
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
|
||||
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
$isActiveLog
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
|
||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||
|
||||
${({ $isHightlightedLog, $isDarkMode }): string =>
|
||||
|
||||
@@ -35,8 +35,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
linesPerRow,
|
||||
fontSize,
|
||||
appendTo = 'center',
|
||||
activeContextLog,
|
||||
activeLog,
|
||||
isListViewPanel,
|
||||
} = props;
|
||||
|
||||
@@ -90,9 +88,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
<div className="table-timestamp">
|
||||
<LogStateIndicator
|
||||
type={getLogIndicatorTypeForTable(item)}
|
||||
isActive={
|
||||
activeLog?.id === item.id || activeContextLog?.id === item.id
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||
@@ -130,16 +125,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
},
|
||||
...(appendTo === 'end' ? fieldColumns : []),
|
||||
];
|
||||
}, [
|
||||
fields,
|
||||
isListViewPanel,
|
||||
appendTo,
|
||||
isDarkMode,
|
||||
linesPerRow,
|
||||
activeLog?.id,
|
||||
activeContextLog?.id,
|
||||
fontSize,
|
||||
]);
|
||||
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
|
||||
|
||||
return { columns, dataSource: flattenLogData };
|
||||
};
|
||||
|
||||
@@ -3,10 +3,6 @@ import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
|
||||
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||
{
|
||||
value: QueryFunctionsTypes.ANOMALY,
|
||||
label: 'Anomaly',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.CUTOFF_MIN,
|
||||
label: 'Cut Off Min',
|
||||
|
||||
@@ -63,6 +63,16 @@
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
.anomaly-alert-evaluation-view-series-list-item-color {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
|
||||
display: inline-flex;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -108,3 +118,63 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uplot-tooltip {
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: #ddd;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
max-height: 500px;
|
||||
width: 280px;
|
||||
overflow-y: auto;
|
||||
display: none; /* Hide tooltip by default */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.uplot-tooltip-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.uplot-tooltip-series {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px 0px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uplot-tooltip-series-name {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.uplot-tooltip-band {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.uplot-tooltip-marker {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import { LineChart } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import tooltipPlugin from './tooltipPlugin';
|
||||
|
||||
function UplotChart({
|
||||
data,
|
||||
options,
|
||||
@@ -72,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);
|
||||
@@ -149,10 +151,38 @@ function AnomalyAlertEvaluationView({
|
||||
const options = {
|
||||
width: dimensions.width,
|
||||
height: dimensions.height - 36,
|
||||
plugins: [bandsPlugin],
|
||||
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
|
||||
focus: {
|
||||
alpha: 0.3,
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
live: false,
|
||||
isolate: true,
|
||||
},
|
||||
cursor: {
|
||||
lock: false,
|
||||
focus: {
|
||||
prox: 1e6,
|
||||
bias: 1,
|
||||
},
|
||||
points: {
|
||||
size: (
|
||||
u: { series: { [x: string]: { points: { size: number } } } },
|
||||
seriesIdx: string | number,
|
||||
): number => u.series[seriesIdx].points.size * 3,
|
||||
width: (u: any, seriesIdx: any, size: number): number => size / 4,
|
||||
stroke: (
|
||||
u: {
|
||||
series: {
|
||||
[x: string]: { points: { stroke: (arg0: any, arg1: any) => any } };
|
||||
};
|
||||
},
|
||||
seriesIdx: string | number,
|
||||
): string => `${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
|
||||
fill: (): string => '#fff',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
label: 'Time',
|
||||
@@ -165,6 +195,7 @@ function AnomalyAlertEvaluationView({
|
||||
width: 2,
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
},
|
||||
{
|
||||
label: `Predicted Value`,
|
||||
@@ -173,18 +204,29 @@ function AnomalyAlertEvaluationView({
|
||||
dash: [2, 2],
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
},
|
||||
{
|
||||
label: `Upper Band`,
|
||||
stroke: 'transparent',
|
||||
show: false,
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
show: false,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Lower Band`,
|
||||
stroke: 'transparent',
|
||||
show: false,
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
show: false,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
: allSeries.map((seriesKey) => ({
|
||||
@@ -193,11 +235,13 @@ function AnomalyAlertEvaluationView({
|
||||
width: 2,
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
}))),
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
time: true,
|
||||
spanGaps: true,
|
||||
},
|
||||
y: {
|
||||
...getYAxisScaleForAnomalyDetection({
|
||||
@@ -211,9 +255,6 @@ function AnomalyAlertEvaluationView({
|
||||
grid: {
|
||||
show: true,
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
},
|
||||
axes: getAxes(isDarkMode, yAxisUnit),
|
||||
};
|
||||
|
||||
@@ -287,17 +328,24 @@ function AnomalyAlertEvaluationView({
|
||||
)}
|
||||
|
||||
{filteredSeriesKeys.map((seriesKey) => (
|
||||
<Checkbox
|
||||
className="anomaly-alert-evaluation-view-series-list-item"
|
||||
key={seriesKey}
|
||||
type="checkbox"
|
||||
name="series"
|
||||
value={seriesKey}
|
||||
checked={selectedSeries === seriesKey}
|
||||
onChange={(): void => handleSeriesChange(seriesKey)}
|
||||
>
|
||||
{seriesKey}
|
||||
</Checkbox>
|
||||
<div key={seriesKey}>
|
||||
<Checkbox
|
||||
className="anomaly-alert-evaluation-view-series-list-item"
|
||||
key={seriesKey}
|
||||
type="checkbox"
|
||||
name="series"
|
||||
value={seriesKey}
|
||||
checked={selectedSeries === seriesKey}
|
||||
onChange={(): void => handleSeriesChange(seriesKey)}
|
||||
>
|
||||
<div
|
||||
className="anomaly-alert-evaluation-view-series-list-item-color"
|
||||
style={{ backgroundColor: seriesData[seriesKey].color }}
|
||||
/>
|
||||
|
||||
{seriesKey}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredSeriesKeys.length === 0 && (
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
|
||||
const tooltipPlugin = (
|
||||
isDarkMode: boolean,
|
||||
): { hooks: { init: (u: any) => void } } => {
|
||||
let tooltip: HTMLDivElement;
|
||||
const tooltipLeftOffset = 10;
|
||||
const tooltipTopOffset = 10;
|
||||
let isMouseOverPlot = false;
|
||||
|
||||
function formatValue(value: string | number | Date): string | number | Date {
|
||||
if (typeof value === 'string' && !Number.isNaN(parseFloat(value))) {
|
||||
return parseFloat(value).toFixed(3);
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(3);
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
if (value == null) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function updateTooltip(u: any, left: number, top: number): void {
|
||||
const idx = u.posToIdx(left);
|
||||
const xVal = u.data[0][idx];
|
||||
|
||||
if (xVal == null) {
|
||||
tooltip.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const xDate = new Date(xVal * 1000);
|
||||
const formattedXDate = formatValue(xDate);
|
||||
|
||||
let tooltipContent = `<div class="uplot-tooltip-title">Time: ${formattedXDate}</div>`;
|
||||
|
||||
let mainValue;
|
||||
let upperBand;
|
||||
let lowerBand;
|
||||
|
||||
let color = null;
|
||||
|
||||
// Loop through all series (excluding the x-axis series)
|
||||
for (let i = 1; i < u.series.length; i++) {
|
||||
const series = u.series[i];
|
||||
|
||||
const yVal = u.data[i][idx];
|
||||
const formattedYVal = formatValue(yVal);
|
||||
|
||||
color = generateColor(
|
||||
series.label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
|
||||
// Create the round marker for the series
|
||||
const marker = `<span class="uplot-tooltip-marker" style="background-color: ${color};"></span>`;
|
||||
|
||||
if (series.label.toLowerCase().includes('upper band')) {
|
||||
upperBand = formattedYVal;
|
||||
} else if (series.label.toLowerCase().includes('lower band')) {
|
||||
lowerBand = formattedYVal;
|
||||
} else if (series.label.toLowerCase().includes('main series')) {
|
||||
mainValue = formattedYVal;
|
||||
} else {
|
||||
tooltipContent += `
|
||||
<div class="uplot-tooltip-series">
|
||||
${marker}
|
||||
<span class="uplot-tooltip-series-name">${series.label}:</span>
|
||||
<span class="uplot-tooltip-series-value">${formattedYVal}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add main value, upper band, and lower band to the tooltip
|
||||
if (mainValue !== undefined) {
|
||||
const marker = `<span class="uplot-tooltip-marker"></span>`;
|
||||
tooltipContent += `
|
||||
<div class="uplot-tooltip-series">
|
||||
${marker}
|
||||
<span class="uplot-tooltip-series-name">Main Series:</span>
|
||||
<span class="uplot-tooltip-series-value">${mainValue}</span>
|
||||
</div>`;
|
||||
}
|
||||
if (upperBand !== undefined) {
|
||||
const marker = `<span class="uplot-tooltip-marker"></span>`;
|
||||
tooltipContent += `
|
||||
<div class="uplot-tooltip-series">
|
||||
${marker}
|
||||
<span class="uplot-tooltip-series-name">Upper Band:</span>
|
||||
<span class="uplot-tooltip-series-value">${upperBand}</span>
|
||||
</div>`;
|
||||
}
|
||||
if (lowerBand !== undefined) {
|
||||
const marker = `<span class="uplot-tooltip-marker"></span>`;
|
||||
tooltipContent += `
|
||||
<div class="uplot-tooltip-series">
|
||||
${marker}
|
||||
<span class="uplot-tooltip-series-name">Lower Band:</span>
|
||||
<span class="uplot-tooltip-series-value">${lowerBand}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
tooltip.innerHTML = tooltipContent;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.left = `${left + tooltipLeftOffset}px`;
|
||||
tooltip.style.top = `${top + tooltipTopOffset}px`;
|
||||
}
|
||||
|
||||
function init(u: any): void {
|
||||
tooltip = document.createElement('div');
|
||||
tooltip.className = 'uplot-tooltip';
|
||||
tooltip.style.display = 'none';
|
||||
u.over.appendChild(tooltip);
|
||||
|
||||
// Add event listeners
|
||||
u.over.addEventListener('mouseenter', () => {
|
||||
isMouseOverPlot = true;
|
||||
});
|
||||
|
||||
u.over.addEventListener('mouseleave', () => {
|
||||
isMouseOverPlot = false;
|
||||
tooltip.style.display = 'none';
|
||||
});
|
||||
|
||||
u.over.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
if (isMouseOverPlot) {
|
||||
const rect = u.over.getBoundingClientRect();
|
||||
const left = e.clientX - rect.left;
|
||||
const top = e.clientY - rect.top;
|
||||
updateTooltip(u, left, top);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default tooltipPlugin;
|
||||
@@ -211,6 +211,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}
|
||||
}, [licenseData, isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
// after logging out hide the trial expiry banner
|
||||
if (!isLoggedIn) {
|
||||
setShowTrialExpiryBanner(false);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const handleUpgrade = (): void => {
|
||||
if (role === 'ADMIN') {
|
||||
history.push(ROUTES.BILLING);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
AlertDef,
|
||||
@@ -58,6 +59,49 @@ export const alertDefaults: AlertDef = {
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
||||
export const anamolyAlertDefaults: AlertDef = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
version: ENTITY_VERSION_V4,
|
||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
...initialQueryBuilderFormValuesMap.metrics,
|
||||
functions: [
|
||||
{
|
||||
name: 'anomaly',
|
||||
args: [],
|
||||
namedArgs: { z_score_threshold: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
promQueries: { A: initialQueryPromQLData },
|
||||
chQueries: {
|
||||
A: {
|
||||
name: 'A',
|
||||
query: ``,
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
unit: undefined,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
matchType: defaultMatchType,
|
||||
algorithm: defaultAlgorithm,
|
||||
seasonality: defaultSeasonality,
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
||||
export const logAlertDefaults: AlertDef = {
|
||||
alertType: AlertTypes.LOGS_BASED_ALERT,
|
||||
condition: {
|
||||
@@ -149,7 +193,7 @@ export const exceptionAlertDefaults: AlertDef = {
|
||||
};
|
||||
|
||||
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: alertDefaults,
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: anamolyAlertDefaults,
|
||||
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
|
||||
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
|
||||
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AlertDef } from 'types/api/alerts/def';
|
||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||
import {
|
||||
alertDefaults,
|
||||
anamolyAlertDefaults,
|
||||
exceptionAlertDefaults,
|
||||
logAlertDefaults,
|
||||
traceAlertDefaults,
|
||||
@@ -24,8 +25,12 @@ function CreateRules(): JSX.Element {
|
||||
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const alertTypeFromURL = queryParams.get(QueryParams.ruleType);
|
||||
const version = queryParams.get('version');
|
||||
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
|
||||
const alertTypeFromParams =
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
? AlertTypes.ANOMALY_BASED_ALERT
|
||||
: queryParams.get(QueryParams.alertType);
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
function getAlertTypeFromDataSource(): AlertTypes | null {
|
||||
@@ -58,7 +63,7 @@ function CreateRules(): JSX.Element {
|
||||
break;
|
||||
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||
setInitValues({
|
||||
...alertDefaults,
|
||||
...anamolyAlertDefaults,
|
||||
version: version || ENTITY_VERSION_V4,
|
||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
});
|
||||
@@ -78,7 +83,10 @@ function CreateRules(): JSX.Element {
|
||||
: typ,
|
||||
);
|
||||
|
||||
if (typ === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
if (
|
||||
typ === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
) {
|
||||
queryParams.set(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
padding: 10px 10px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: rgba(22, 24, 29, 0.6);
|
||||
@@ -33,6 +33,7 @@
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-slate-500);
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
PanelBottomClose,
|
||||
Plus,
|
||||
X,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
CSSProperties,
|
||||
@@ -515,7 +514,11 @@ function ExplorerOptions({
|
||||
|
||||
return (
|
||||
<div className="explorer-options-container">
|
||||
{isQueryUpdated && !isExplorerOptionHidden && (
|
||||
{
|
||||
// if a viewName is selected and the explorer options are not hidden then
|
||||
// always show the clear option
|
||||
}
|
||||
{!isExplorerOptionHidden && viewName && (
|
||||
<div
|
||||
className={cx(
|
||||
isEditDeleteSupported ? '' : 'hide-update',
|
||||
@@ -529,18 +532,25 @@ function ExplorerOptions({
|
||||
icon={<X size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider
|
||||
type="vertical"
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
/>
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
{
|
||||
// only show the update view option when the query is updated
|
||||
}
|
||||
{isQueryUpdated && (
|
||||
<>
|
||||
<Divider
|
||||
type="vertical"
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
/>
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isExplorerOptionHidden && (
|
||||
@@ -564,10 +574,7 @@ function ExplorerOptions({
|
||||
}}
|
||||
dropdownStyle={dropdownStyle}
|
||||
className="views-dropdown"
|
||||
allowClear={{
|
||||
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
||||
}}
|
||||
onClear={handleClearSelect}
|
||||
allowClear={false}
|
||||
ref={ref}
|
||||
>
|
||||
{viewsData?.data?.data?.map((view) => {
|
||||
@@ -662,8 +669,8 @@ function ExplorerOptions({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ExplorerOptionsHideArea
|
||||
viewName={viewName}
|
||||
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||
sourcepage={sourcepage}
|
||||
@@ -672,7 +679,6 @@ function ExplorerOptions({
|
||||
onUpdateQueryHandler={onUpdateQueryHandler}
|
||||
isEditDeleteSupported={isEditDeleteSupported}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
className="save-view-modal"
|
||||
title={<span className="title">Save this view</span>}
|
||||
@@ -705,7 +711,6 @@ function ExplorerOptions({
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
footer={null}
|
||||
onOk={onCancel(false)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { setExplorerToolBarVisibility } from './utils';
|
||||
|
||||
interface DroppableAreaProps {
|
||||
viewName: string;
|
||||
isQueryUpdated: boolean;
|
||||
isExplorerOptionHidden?: boolean;
|
||||
sourcepage: DataSource;
|
||||
@@ -20,6 +21,7 @@ interface DroppableAreaProps {
|
||||
}
|
||||
|
||||
function ExplorerOptionsHideArea({
|
||||
viewName,
|
||||
isQueryUpdated,
|
||||
isExplorerOptionHidden,
|
||||
sourcepage,
|
||||
@@ -39,7 +41,7 @@ function ExplorerOptionsHideArea({
|
||||
<div className="explorer-option-droppable-container">
|
||||
{isExplorerOptionHidden && (
|
||||
<>
|
||||
{isQueryUpdated && (
|
||||
{viewName && (
|
||||
<div className="explorer-actions-btn">
|
||||
<Tooltip title="Clear this view">
|
||||
<Button
|
||||
@@ -49,7 +51,7 @@ function ExplorerOptionsHideArea({
|
||||
icon={<X size={14} color={Color.BG_INK_500} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
{isEditDeleteSupported && (
|
||||
{isEditDeleteSupported && isQueryUpdated && (
|
||||
<Tooltip title="Update this View">
|
||||
<Button
|
||||
onClick={onUpdateQueryHandler}
|
||||
|
||||
@@ -160,6 +160,15 @@ function RuleOptions({
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeDeviation = (value: number): void => {
|
||||
const target = value || alertDef.condition.target || 3;
|
||||
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: { ...alertDef.condition, target: Number(target) },
|
||||
});
|
||||
};
|
||||
|
||||
const renderEvalWindows = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
@@ -203,6 +212,28 @@ function RuleOptions({
|
||||
</InlineSelect>
|
||||
);
|
||||
|
||||
const renderDeviationOpts = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue={3}
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.condition.target}
|
||||
onChange={(value: number | unknown): void => {
|
||||
if (typeof value === 'number') {
|
||||
onChangeDeviation(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Option value={1}>1</Select.Option>
|
||||
<Select.Option value={2}>2</Select.Option>
|
||||
<Select.Option value={3}>3</Select.Option>
|
||||
<Select.Option value={4}>4</Select.Option>
|
||||
<Select.Option value={5}>5</Select.Option>
|
||||
<Select.Option value={6}>6</Select.Option>
|
||||
<Select.Option value={7}>7</Select.Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
|
||||
const renderSeasonality = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
@@ -237,39 +268,6 @@ function RuleOptions({
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const renderAnomalyRuleOpts = (
|
||||
onChange: InputNumberProps['onChange'],
|
||||
): JSX.Element => (
|
||||
<Form.Item>
|
||||
<Typography.Text className="rule-definition">
|
||||
{t('text_condition1_anomaly')}
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={queryOptions}
|
||||
placeholder={t('selected_query_placeholder')}
|
||||
value={alertDef.condition.selectedQueryName}
|
||||
onChange={onChangeSelectedQueryName}
|
||||
/>
|
||||
{t('text_condition3')} {renderEvalWindows()}
|
||||
<Typography.Text>is</Typography.Text>
|
||||
<InputNumber
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
<Typography.Text>deviations</Typography.Text>
|
||||
{renderCompareOps()}
|
||||
<Typography.Text>the predicted data</Typography.Text>
|
||||
{renderMatchOpts()}
|
||||
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
|
||||
seasonality
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const renderPromRuleOptions = (): JSX.Element => (
|
||||
<Form.Item>
|
||||
<Typography.Text>
|
||||
@@ -320,6 +318,32 @@ function RuleOptions({
|
||||
});
|
||||
};
|
||||
|
||||
const renderAnomalyRuleOpts = (): JSX.Element => (
|
||||
<Form.Item>
|
||||
<Typography.Text className="rule-definition">
|
||||
{t('text_condition1_anomaly')}
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={queryOptions}
|
||||
placeholder={t('selected_query_placeholder')}
|
||||
value={alertDef.condition.selectedQueryName}
|
||||
onChange={onChangeSelectedQueryName}
|
||||
/>
|
||||
{t('text_condition3')} {renderEvalWindows()}
|
||||
<Typography.Text>is</Typography.Text>
|
||||
{renderDeviationOpts()}
|
||||
<Typography.Text>deviations</Typography.Text>
|
||||
{renderCompareOps()}
|
||||
<Typography.Text>the predicted data</Typography.Text>
|
||||
{renderMatchOpts()}
|
||||
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
|
||||
seasonality
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const renderFrequency = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
@@ -354,7 +378,7 @@ function RuleOptions({
|
||||
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
|
||||
{queryCategory !== EQueryType.PROM &&
|
||||
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||
<>{renderAnomalyRuleOpts(onChange)}</>
|
||||
<>{renderAnomalyRuleOpts()}</>
|
||||
)}
|
||||
|
||||
{queryCategory !== EQueryType.PROM &&
|
||||
|
||||
@@ -39,6 +39,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
@@ -72,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,
|
||||
@@ -88,6 +102,8 @@ function FormAlertRules({
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
||||
// In case of alert the panel types should always be "Graph" only
|
||||
const panelType = PANEL_TYPES.TIME_SERIES;
|
||||
@@ -120,9 +136,7 @@ function FormAlertRules({
|
||||
|
||||
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||
|
||||
const [detectionMethod, setDetectionMethod] = useState<string>(
|
||||
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
);
|
||||
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
||||
@@ -154,11 +168,24 @@ function FormAlertRules({
|
||||
|
||||
useShareBuilderUrl(sq);
|
||||
|
||||
const handleDetectionMethodChange = (value: string): void => {
|
||||
setAlertDef((def) => ({
|
||||
...def,
|
||||
ruleType: value,
|
||||
}));
|
||||
|
||||
logEvent(`Alert: Detection method changed`, {
|
||||
detectionMethod: value,
|
||||
});
|
||||
|
||||
setDetectionMethod(value);
|
||||
};
|
||||
|
||||
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
|
||||
const anomalyFunction = {
|
||||
name: 'anomaly',
|
||||
args: [],
|
||||
namedArgs: { z_score_threshold: 9 },
|
||||
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
|
||||
};
|
||||
const functions = data.functions || [];
|
||||
|
||||
@@ -166,6 +193,19 @@ function FormAlertRules({
|
||||
// Add anomaly if not already present
|
||||
if (!functions.some((func) => func.name === 'anomaly')) {
|
||||
functions.push(anomalyFunction);
|
||||
} else {
|
||||
const anomalyFuncIndex = functions.findIndex(
|
||||
(func) => func.name === 'anomaly',
|
||||
);
|
||||
|
||||
if (anomalyFuncIndex !== -1) {
|
||||
const anomalyFunc = {
|
||||
...functions[anomalyFuncIndex],
|
||||
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
|
||||
};
|
||||
functions.splice(anomalyFuncIndex, 1);
|
||||
functions.push(anomalyFunc);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove anomaly if present
|
||||
@@ -178,6 +218,20 @@ function FormAlertRules({
|
||||
return functions;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const ruleType =
|
||||
detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
? AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
: AlertDetectionTypes.THRESHOLD_ALERT;
|
||||
|
||||
queryParams.set(QueryParams.ruleType, ruleType);
|
||||
|
||||
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
||||
|
||||
history.replace(generatedUrl);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [detectionMethod]);
|
||||
|
||||
const updateFunctionsBasedOnAlertType = (): void => {
|
||||
for (let index = 0; index < currentQuery.builder.queryData.length; index++) {
|
||||
const queryData = currentQuery.builder.queryData[index];
|
||||
@@ -191,7 +245,11 @@ function FormAlertRules({
|
||||
useEffect(() => {
|
||||
updateFunctionsBasedOnAlertType();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [detectionMethod, alertDef, currentQuery.builder.queryData.length]);
|
||||
}, [
|
||||
detectionMethod,
|
||||
alertDef.condition.target,
|
||||
currentQuery.builder.queryData.length,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const broadcastToSpecificChannels =
|
||||
@@ -215,7 +273,8 @@ function FormAlertRules({
|
||||
});
|
||||
|
||||
setDetectionMethod(ruleType);
|
||||
}, [initialValue, isNewRule, alertTypeFromURL]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialValue, isNewRule]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set selectedQueryName based on the length of queryOptions
|
||||
@@ -335,7 +394,11 @@ function FormAlertRules({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) {
|
||||
if (
|
||||
alertDef.ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT &&
|
||||
alertDef.condition?.target !== 0 &&
|
||||
!alertDef.condition?.target
|
||||
) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('target_missing'),
|
||||
@@ -493,6 +556,7 @@ function FormAlertRules({
|
||||
queryType: currentQuery.queryType,
|
||||
alertId: postableAlert?.id,
|
||||
alertName: postableAlert?.alert,
|
||||
ruleType: postableAlert?.ruleType,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
@@ -577,6 +641,7 @@ function FormAlertRules({
|
||||
queryType: currentQuery.queryType,
|
||||
status: statusResponse.status,
|
||||
statusMessage: statusResponse.message,
|
||||
ruleType: postableAlert.ruleType,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
||||
@@ -650,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', {
|
||||
@@ -672,15 +760,6 @@ function FormAlertRules({
|
||||
},
|
||||
];
|
||||
|
||||
const handleDetectionMethodChange = (value: any): void => {
|
||||
setAlertDef((def) => ({
|
||||
...def,
|
||||
ruleType: value,
|
||||
}));
|
||||
|
||||
setDetectionMethod(value);
|
||||
};
|
||||
|
||||
const isAnomalyDetectionEnabled =
|
||||
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||
|
||||
@@ -709,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>
|
||||
@@ -745,7 +828,7 @@ function FormAlertRules({
|
||||
<Tabs2
|
||||
key={detectionMethod}
|
||||
tabs={tabs}
|
||||
initialSelectedTab={detectionMethod}
|
||||
initialSelectedTab={detectionMethod || ''}
|
||||
onSelectTab={handleDetectionMethodChange}
|
||||
/>
|
||||
|
||||
|
||||
@@ -97,13 +97,19 @@ function GridTableComponent({
|
||||
|
||||
const newColumnData = columns.map((e) => ({
|
||||
...e,
|
||||
render: (text: string): ReactNode => {
|
||||
const isNumber = !Number.isNaN(Number(text));
|
||||
render: (text: string, ...rest: any): ReactNode => {
|
||||
let textForThreshold = text;
|
||||
if (columnUnits && columnUnits?.[e.title as string]) {
|
||||
textForThreshold = rest[0][`${e.title}_without_unit`];
|
||||
}
|
||||
const isNumber = !Number.isNaN(Number(textForThreshold));
|
||||
|
||||
if (thresholds && isNumber) {
|
||||
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
||||
thresholds,
|
||||
e.title as string,
|
||||
Number(text),
|
||||
Number(textForThreshold),
|
||||
columnUnits?.[e.title as string],
|
||||
);
|
||||
|
||||
const idx = thresholds.findIndex(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
||||
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
||||
@@ -30,10 +31,39 @@ function evaluateCondition(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates whether a given value meets a specified threshold condition.
|
||||
* It first converts the value to the appropriate unit if a threshold unit is provided,
|
||||
* and then checks the condition using the specified operator.
|
||||
*
|
||||
* @param value - The value to be evaluated.
|
||||
* @param thresholdValue - The threshold value to compare against.
|
||||
* @param thresholdOperator - The operator used for comparison (e.g., '>', '<', '==').
|
||||
* @param thresholdUnit - The unit to which the value should be converted.
|
||||
* @param columnUnit - The current unit of the value.
|
||||
* @returns A boolean indicating whether the value meets the threshold condition.
|
||||
*/
|
||||
function evaluateThresholdWithConvertedValue(
|
||||
value: number,
|
||||
thresholdValue: number,
|
||||
thresholdOperator?: string,
|
||||
thresholdUnit?: string,
|
||||
columnUnit?: string,
|
||||
): boolean {
|
||||
const convertedValue = convertUnit(value, columnUnit, thresholdUnit);
|
||||
|
||||
if (convertedValue) {
|
||||
return evaluateCondition(thresholdOperator, convertedValue, thresholdValue);
|
||||
}
|
||||
|
||||
return evaluateCondition(thresholdOperator, value, thresholdValue);
|
||||
}
|
||||
|
||||
export function findMatchingThreshold(
|
||||
thresholds: ThresholdProps[],
|
||||
label: string,
|
||||
value: number,
|
||||
columnUnit?: string,
|
||||
): {
|
||||
threshold: ThresholdProps;
|
||||
hasMultipleMatches: boolean;
|
||||
@@ -45,10 +75,12 @@ export function findMatchingThreshold(
|
||||
if (
|
||||
threshold.thresholdValue !== undefined &&
|
||||
threshold.thresholdTableOptions === label &&
|
||||
evaluateCondition(
|
||||
threshold.thresholdOperator,
|
||||
evaluateThresholdWithConvertedValue(
|
||||
value,
|
||||
threshold.thresholdValue,
|
||||
threshold?.thresholdValue,
|
||||
threshold.thresholdOperator,
|
||||
threshold.thresholdUnit,
|
||||
columnUnit,
|
||||
)
|
||||
) {
|
||||
matchingThresholds.push(threshold);
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ function ImportJSON({
|
||||
|
||||
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
||||
|
||||
// Add validation for uuid
|
||||
if (dashboardData.uuid !== undefined && dashboardData.uuid.trim() === '') {
|
||||
// silently remove uuid if it is empty
|
||||
delete dashboardData.uuid;
|
||||
}
|
||||
|
||||
if (dashboardData?.layout) {
|
||||
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
|
||||
} else {
|
||||
@@ -123,11 +129,14 @@ function ImportJSON({
|
||||
});
|
||||
}
|
||||
setDashboardCreating(false);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setDashboardCreating(false);
|
||||
setIsFeatureAlert(false);
|
||||
|
||||
setIsCreateDashboardError(true);
|
||||
notifications.error({
|
||||
message: error instanceof Error ? error.message : t('error_loading_json'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,3 +2,25 @@
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-row-backdrop {
|
||||
&.INFO {
|
||||
background-color: var(--bg-robin-500) 10;
|
||||
}
|
||||
&.WARNING,
|
||||
&.WARN {
|
||||
background-color: var(--bg-amber-500) 10;
|
||||
}
|
||||
&.ERROR {
|
||||
background-color: var(--bg-cherry-500) 10;
|
||||
}
|
||||
&.TRACE {
|
||||
background-color: var(--bg-forest-400) 10;
|
||||
}
|
||||
&.DEBUG {
|
||||
background-color: var(--bg-aqua-500) 10;
|
||||
}
|
||||
&.FATAL {
|
||||
background-color: var(--bg-sakura-500) 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { useTableView } from 'components/Logs/TableView/useTableView';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
@@ -21,6 +22,11 @@ import { TableHeaderCellStyled, TableRowStyled } from './styles';
|
||||
import TableRow from './TableRow';
|
||||
import { InfinityTableProps } from './types';
|
||||
|
||||
interface CustomTableRowProps {
|
||||
activeContextLogId: string;
|
||||
activeLogId: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
|
||||
children,
|
||||
@@ -31,10 +37,17 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const logType = getLogIndicatorType(props.item);
|
||||
|
||||
return (
|
||||
<TableRowStyled
|
||||
$isDarkMode={isDarkMode}
|
||||
$isActiveLog={isHighlighted}
|
||||
$isActiveLog={
|
||||
isHighlighted ||
|
||||
(context as CustomTableRowProps).activeContextLogId === props.item.id ||
|
||||
(context as CustomTableRowProps).activeLogId === props.item.id
|
||||
}
|
||||
$logType={logType}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
@@ -66,8 +79,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
...tableViewProps,
|
||||
onClickExpand: onSetActiveLog,
|
||||
onOpenLogsContext: handleSetActiveContextLog,
|
||||
activeLog,
|
||||
activeContextLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onDragColumns } = useDragColumns<
|
||||
@@ -153,7 +164,14 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
// TODO: fix it in the future
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
TableRow: CustomTableRow,
|
||||
TableRow: (props): any =>
|
||||
CustomTableRow({
|
||||
...props,
|
||||
context: {
|
||||
activeContextLogId: activeContextLog?.id,
|
||||
activeLogId: activeLog?.id,
|
||||
},
|
||||
} as any),
|
||||
}}
|
||||
itemContent={itemContent}
|
||||
fixedHeaderContent={tableHeader}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
@@ -37,13 +36,12 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
|
||||
export const TableRowStyled = styled.tr<{
|
||||
$isActiveLog: boolean;
|
||||
$isDarkMode: boolean;
|
||||
$logType: string;
|
||||
}>`
|
||||
td {
|
||||
${({ $isActiveLog, $isDarkMode }): string =>
|
||||
${({ $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
$isActiveLog
|
||||
? `background-color: ${
|
||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
||||
} !important`
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
|
||||
: ''};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -47,7 +45,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
|
||||
import {
|
||||
Dashboard,
|
||||
DashboardData,
|
||||
IDashboardVariable,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
@@ -63,6 +65,30 @@ interface DashboardDescriptionProps {
|
||||
handle: FullScreenHandle;
|
||||
}
|
||||
|
||||
function sanitizeDashboardData(
|
||||
selectedData: DashboardData,
|
||||
): Omit<DashboardData, 'uuid'> {
|
||||
if (!selectedData?.variables) {
|
||||
const { uuid, ...rest } = selectedData;
|
||||
return rest;
|
||||
}
|
||||
|
||||
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const { selectedValue, ...rest } = value;
|
||||
acc[key] = rest;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, IDashboardVariable>,
|
||||
);
|
||||
|
||||
const { uuid, ...restData } = selectedData;
|
||||
return {
|
||||
...restData,
|
||||
variables: updatedVariables,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const { handle } = props;
|
||||
@@ -328,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}
|
||||
@@ -407,7 +421,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
type="text"
|
||||
icon={<FileJson size={14} />}
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(selectedData, selectedData.title);
|
||||
downloadObjectAsJson(
|
||||
sanitizeDashboardData(selectedData),
|
||||
selectedData.title,
|
||||
);
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
@@ -417,7 +434,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
type="text"
|
||||
icon={<ClipboardCopy size={14} />}
|
||||
onClick={(): void => {
|
||||
setCopy(JSON.stringify(selectedData, null, 2));
|
||||
setCopy(
|
||||
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
|
||||
);
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -297,6 +297,16 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.invalid-unit {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Giest Mono';
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.48px;
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-card-container:hover {
|
||||
|
||||
@@ -3,17 +3,18 @@ import './Threshold.styles.scss';
|
||||
|
||||
import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { unitOptions } from 'container/NewWidget/utils';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Check, Pencil, Trash2, X } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||
|
||||
import {
|
||||
operatorOptions,
|
||||
panelTypeVsDragAndDrop,
|
||||
showAsOptions,
|
||||
unitOptions,
|
||||
} from '../constants';
|
||||
import { convertUnit } from '../dataFormatCategories';
|
||||
import ColorSelector from './ColorSelector';
|
||||
import CustomColor from './CustomColor';
|
||||
import ShowCaseValue from './ShowCaseValue';
|
||||
@@ -40,6 +41,7 @@ function Threshold({
|
||||
thresholdLabel = '',
|
||||
tableOptions,
|
||||
thresholdTableOptions = '',
|
||||
columnUnits,
|
||||
}: ThresholdProps): JSX.Element {
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
|
||||
const [operator, setOperator] = useState<string | number>(
|
||||
@@ -192,6 +194,13 @@ function Threshold({
|
||||
|
||||
const allowDragAndDrop = panelTypeVsDragAndDrop[selectedGraph];
|
||||
|
||||
const isInvalidUnitComparison = useMemo(
|
||||
() =>
|
||||
unit !== 'none' &&
|
||||
convertUnit(value, unit, columnUnits?.[tableSelectedOption]) === null,
|
||||
[unit, value, columnUnits, tableSelectedOption],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={allowDragAndDrop ? ref : null}
|
||||
@@ -303,7 +312,7 @@ function Threshold({
|
||||
{isEditMode ? (
|
||||
<Select
|
||||
defaultValue={unit}
|
||||
options={unitOptions}
|
||||
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
|
||||
onChange={handleUnitChange}
|
||||
showSearch
|
||||
className="unit-selection"
|
||||
@@ -339,6 +348,12 @@ function Threshold({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isInvalidUnitComparison && (
|
||||
<Typography.Text className="invalid-unit">
|
||||
Threshold unit ({unit}) is not valid in comparison with the column unit (
|
||||
{columnUnits?.[tableSelectedOption] || 'none'})
|
||||
</Typography.Text>
|
||||
)}
|
||||
{isEditMode && (
|
||||
<div className="threshold-action-button">
|
||||
<Button
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
import './ThresholdSelector.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { Events } from 'constants/events';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Antenna, Plus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import Threshold from './Threshold';
|
||||
@@ -21,22 +19,23 @@ function ThresholdSelector({
|
||||
setThresholds,
|
||||
yAxisUnit,
|
||||
selectedGraph,
|
||||
columnUnits,
|
||||
}: ThresholdSelectorProps): JSX.Element {
|
||||
const [tableOptions, setTableOptions] = useState<
|
||||
Array<{ value: string; label: string }>
|
||||
>([]);
|
||||
useEffect(() => {
|
||||
eventEmitter.on(
|
||||
Events.TABLE_COLUMNS_DATA,
|
||||
(data: { columns: ColumnsType<RowData>; dataSource: RowData[] }) => {
|
||||
const newTableOptions = data.columns.map((e) => ({
|
||||
value: e.title as string,
|
||||
label: e.title as string,
|
||||
}));
|
||||
setTableOptions([...newTableOptions]);
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
function getAggregateColumnsNamesAndLabels(): string[] {
|
||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const queries = currentQuery.builder.queryData.map((q) => q.queryName);
|
||||
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
|
||||
return [...queries, ...formulas];
|
||||
}
|
||||
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
|
||||
return currentQuery.clickhouse_sql.map((q) => q.name);
|
||||
}
|
||||
return currentQuery.promql.map((q) => q.name);
|
||||
}
|
||||
|
||||
const aggregationQueries = getAggregateColumnsNamesAndLabels();
|
||||
|
||||
const moveThreshold = useCallback(
|
||||
(dragIndex: number, hoverIndex: number) => {
|
||||
@@ -66,7 +65,7 @@ function ThresholdSelector({
|
||||
moveThreshold,
|
||||
keyIndex: thresholds.length,
|
||||
selectedGraph,
|
||||
thresholdTableOptions: tableOptions[0]?.value || '',
|
||||
thresholdTableOptions: aggregationQueries[0] || '',
|
||||
},
|
||||
...thresholds,
|
||||
]);
|
||||
@@ -105,8 +104,12 @@ function ThresholdSelector({
|
||||
moveThreshold={moveThreshold}
|
||||
selectedGraph={selectedGraph}
|
||||
thresholdLabel={threshold.thresholdLabel}
|
||||
tableOptions={tableOptions}
|
||||
tableOptions={aggregationQueries.map((query) => ({
|
||||
value: query,
|
||||
label: query,
|
||||
}))}
|
||||
thresholdTableOptions={threshold.thresholdTableOptions}
|
||||
columnUnits={columnUnits}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
||||
|
||||
export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
|
||||
|
||||
@@ -19,6 +20,7 @@ export type ThresholdProps = {
|
||||
moveThreshold: (dragIndex: number, hoverIndex: number) => void;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
tableOptions?: Array<{ value: string; label: string }>;
|
||||
columnUnits?: ColumnUnit;
|
||||
};
|
||||
|
||||
export type ShowCaseValueProps = {
|
||||
@@ -36,4 +38,5 @@ export type ThresholdSelectorProps = {
|
||||
thresholds: ThresholdProps[];
|
||||
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
columnUnits: ColumnUnit;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
|
||||
|
||||
import { getCategorySelectOptionByName } from './alertFomatCategories';
|
||||
|
||||
export const operatorOptions: DefaultOptionType[] = [
|
||||
{ value: '>', label: '>' },
|
||||
@@ -11,11 +8,6 @@ export const operatorOptions: DefaultOptionType[] = [
|
||||
{ value: '<=', label: '<=' },
|
||||
];
|
||||
|
||||
export const unitOptions = categoryToSupport.map((category) => ({
|
||||
label: category,
|
||||
options: getCategorySelectOptionByName(category),
|
||||
}));
|
||||
|
||||
export const showAsOptions: DefaultOptionType[] = [
|
||||
{ value: 'Text', label: 'Text' },
|
||||
{ value: 'Background', label: 'Background' },
|
||||
|
||||
@@ -438,3 +438,168 @@ export const dataTypeCategories: DataTypeCategories = [
|
||||
export const flattenedCategories = flattenDeep(
|
||||
dataTypeCategories.map((category) => category.formats),
|
||||
);
|
||||
|
||||
type ConversionFactors = {
|
||||
[key: string]: {
|
||||
[key: string]: number | null;
|
||||
};
|
||||
};
|
||||
|
||||
// Object containing conversion factors for various categories and formats
|
||||
const conversionFactors: ConversionFactors = {
|
||||
[CategoryNames.Time]: {
|
||||
[TimeFormats.Hertz]: 1,
|
||||
[TimeFormats.Nanoseconds]: 1e-9,
|
||||
[TimeFormats.Microseconds]: 1e-6,
|
||||
[TimeFormats.Milliseconds]: 1e-3,
|
||||
[TimeFormats.Seconds]: 1,
|
||||
[TimeFormats.Minutes]: 60,
|
||||
[TimeFormats.Hours]: 3600,
|
||||
[TimeFormats.Days]: 86400,
|
||||
[TimeFormats.DurationMs]: 1e-3,
|
||||
[TimeFormats.DurationS]: 1,
|
||||
[TimeFormats.DurationHms]: null, // Requires special handling
|
||||
[TimeFormats.DurationDhms]: null, // Requires special handling
|
||||
[TimeFormats.Timeticks]: null, // Requires special handling
|
||||
[TimeFormats.ClockMs]: 1e-3,
|
||||
[TimeFormats.ClockS]: 1,
|
||||
},
|
||||
[CategoryNames.Throughput]: {
|
||||
[ThroughputFormats.CountsPerSec]: 1,
|
||||
[ThroughputFormats.OpsPerSec]: 1,
|
||||
[ThroughputFormats.RequestsPerSec]: 1,
|
||||
[ThroughputFormats.ReadsPerSec]: 1,
|
||||
[ThroughputFormats.WritesPerSec]: 1,
|
||||
[ThroughputFormats.IOOpsPerSec]: 1,
|
||||
[ThroughputFormats.CountsPerMin]: 1 / 60,
|
||||
[ThroughputFormats.OpsPerMin]: 1 / 60,
|
||||
[ThroughputFormats.ReadsPerMin]: 1 / 60,
|
||||
[ThroughputFormats.WritesPerMin]: 1 / 60,
|
||||
},
|
||||
[CategoryNames.Data]: {
|
||||
[DataFormats.BytesIEC]: 1,
|
||||
[DataFormats.BytesSI]: 1,
|
||||
[DataFormats.BitsIEC]: 0.125,
|
||||
[DataFormats.BitsSI]: 0.125,
|
||||
[DataFormats.KibiBytes]: 1024,
|
||||
[DataFormats.KiloBytes]: 1000,
|
||||
[DataFormats.MebiBytes]: 1048576,
|
||||
[DataFormats.MegaBytes]: 1000000,
|
||||
[DataFormats.GibiBytes]: 1073741824,
|
||||
[DataFormats.GigaBytes]: 1000000000,
|
||||
[DataFormats.TebiBytes]: 1099511627776,
|
||||
[DataFormats.TeraBytes]: 1000000000000,
|
||||
[DataFormats.PebiBytes]: 1125899906842624,
|
||||
[DataFormats.PetaBytes]: 1000000000000000,
|
||||
},
|
||||
[CategoryNames.DataRate]: {
|
||||
[DataRateFormats.PacketsPerSec]: null, // Cannot convert directly to other data rates
|
||||
[DataRateFormats.BytesPerSecIEC]: 1,
|
||||
[DataRateFormats.BytesPerSecSI]: 1,
|
||||
[DataRateFormats.BitsPerSecIEC]: 0.125,
|
||||
[DataRateFormats.BitsPerSecSI]: 0.125,
|
||||
[DataRateFormats.KibiBytesPerSec]: 1024,
|
||||
[DataRateFormats.KibiBitsPerSec]: 128,
|
||||
[DataRateFormats.KiloBytesPerSec]: 1000,
|
||||
[DataRateFormats.KiloBitsPerSec]: 125,
|
||||
[DataRateFormats.MebiBytesPerSec]: 1048576,
|
||||
[DataRateFormats.MebiBitsPerSec]: 131072,
|
||||
[DataRateFormats.MegaBytesPerSec]: 1000000,
|
||||
[DataRateFormats.MegaBitsPerSec]: 125000,
|
||||
[DataRateFormats.GibiBytesPerSec]: 1073741824,
|
||||
[DataRateFormats.GibiBitsPerSec]: 134217728,
|
||||
[DataRateFormats.GigaBytesPerSec]: 1000000000,
|
||||
[DataRateFormats.GigaBitsPerSec]: 125000000,
|
||||
[DataRateFormats.TebiBytesPerSec]: 1099511627776,
|
||||
[DataRateFormats.TebiBitsPerSec]: 137438953472,
|
||||
[DataRateFormats.TeraBytesPerSec]: 1000000000000,
|
||||
[DataRateFormats.TeraBitsPerSec]: 125000000000,
|
||||
[DataRateFormats.PebiBytesPerSec]: 1125899906842624,
|
||||
[DataRateFormats.PebiBitsPerSec]: 140737488355328,
|
||||
[DataRateFormats.PetaBytesPerSec]: 1000000000000000,
|
||||
[DataRateFormats.PetaBitsPerSec]: 125000000000000,
|
||||
},
|
||||
[CategoryNames.Miscellaneous]: {
|
||||
[MiscellaneousFormats.None]: null,
|
||||
[MiscellaneousFormats.String]: null,
|
||||
[MiscellaneousFormats.Short]: null,
|
||||
[MiscellaneousFormats.Percent]: 1,
|
||||
[MiscellaneousFormats.PercentUnit]: 100,
|
||||
[MiscellaneousFormats.Humidity]: 1,
|
||||
[MiscellaneousFormats.Decibel]: null,
|
||||
[MiscellaneousFormats.Hexadecimal0x]: null,
|
||||
[MiscellaneousFormats.Hexadecimal]: null,
|
||||
[MiscellaneousFormats.ScientificNotation]: null,
|
||||
[MiscellaneousFormats.LocaleFormat]: null,
|
||||
[MiscellaneousFormats.Pixels]: null,
|
||||
},
|
||||
[CategoryNames.Boolean]: {
|
||||
[BooleanFormats.TRUE_FALSE]: null, // Not convertible
|
||||
[BooleanFormats.YES_NO]: null, // Not convertible
|
||||
[BooleanFormats.ON_OFF]: null, // Not convertible
|
||||
},
|
||||
};
|
||||
|
||||
// Function to get the conversion factor between two units in a specific category
|
||||
function getConversionFactor(
|
||||
fromUnit: string,
|
||||
toUnit: string,
|
||||
category: CategoryNames,
|
||||
): number | null {
|
||||
// Retrieves the conversion factors for the specified category
|
||||
const categoryFactors = conversionFactors[category];
|
||||
if (!categoryFactors) {
|
||||
return null; // Returns null if the category does not exist
|
||||
}
|
||||
const fromFactor = categoryFactors[fromUnit];
|
||||
const toFactor = categoryFactors[toUnit];
|
||||
if (
|
||||
fromFactor === undefined ||
|
||||
toFactor === undefined ||
|
||||
fromFactor === null ||
|
||||
toFactor === null
|
||||
) {
|
||||
return null; // Returns null if either unit does not exist or is not convertible
|
||||
}
|
||||
return fromFactor / toFactor; // Returns the conversion factor ratio
|
||||
}
|
||||
|
||||
// Function to convert a value from one unit to another
|
||||
export function convertUnit(
|
||||
value: number,
|
||||
fromUnitId?: string,
|
||||
toUnitId?: string,
|
||||
): number | null {
|
||||
let fromUnit: string | undefined;
|
||||
let toUnit: string | undefined;
|
||||
|
||||
// Finds the category that contains the specified units and extracts fromUnit and toUnit using array methods
|
||||
const category = dataTypeCategories.find((category) =>
|
||||
category.formats.some((format) => {
|
||||
if (format.id === fromUnitId) fromUnit = format.id;
|
||||
if (format.id === toUnitId) toUnit = format.id;
|
||||
return fromUnit && toUnit; // Break out early if both units are found
|
||||
}),
|
||||
);
|
||||
|
||||
if (!category || !fromUnit || !toUnit) return null; // Return null if category or units are not found
|
||||
|
||||
// Gets the conversion factor for the specified units
|
||||
const conversionFactor = getConversionFactor(
|
||||
fromUnit,
|
||||
toUnit,
|
||||
category.name as any,
|
||||
);
|
||||
if (conversionFactor === null) return null; // Return null if conversion is not possible
|
||||
|
||||
return value * conversionFactor;
|
||||
}
|
||||
|
||||
// Function to get the category name for a given unit ID
|
||||
export const getCategoryName = (unitId: string): CategoryNames | null => {
|
||||
// Finds the category that contains the specified unit ID
|
||||
const foundCategory = dataTypeCategories.find((category) =>
|
||||
category.formats.some((format) => format.id === unitId),
|
||||
);
|
||||
return foundCategory ? (foundCategory.name as CategoryNames) : null;
|
||||
};
|
||||
|
||||
@@ -311,6 +311,7 @@ function RightContainer({
|
||||
setThresholds={setThresholds}
|
||||
yAxisUnit={yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
columnUnits={columnUnits}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -8,12 +9,19 @@ import {
|
||||
listViewInitialTraceQuery,
|
||||
PANEL_TYPES_INITIAL_QUERY,
|
||||
} from 'container/NewDashboard/ComponentsSlider/constants';
|
||||
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
|
||||
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
|
||||
import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
dataTypeCategories,
|
||||
getCategoryName,
|
||||
} from './RightContainer/dataFormatCategories';
|
||||
import { CategoryNames } from './RightContainer/types';
|
||||
|
||||
export const getIsQueryModified = (
|
||||
currentQuery: Query,
|
||||
stagedQuery: Query | null,
|
||||
@@ -529,3 +537,41 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
|
||||
EQueryType.PROM,
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a list of category select options based on the provided category name.
|
||||
* If the category is found, it maps the formats to an array of objects containing
|
||||
* the label and value for each format.
|
||||
*/
|
||||
export const getCategorySelectOptionByName = (
|
||||
name?: CategoryNames | string,
|
||||
): DefaultOptionType[] =>
|
||||
dataTypeCategories
|
||||
.find((category) => category.name === name)
|
||||
?.formats.map((format) => ({
|
||||
label: format.name,
|
||||
value: format.id,
|
||||
})) || [];
|
||||
|
||||
/**
|
||||
* Generates unit options based on the provided column unit.
|
||||
* It first retrieves the category name associated with the column unit.
|
||||
* If the category is empty, it maps all supported categories to their respective
|
||||
* select options. If a valid category is found, it filters the supported categories
|
||||
* to return only the options for the matched category.
|
||||
*/
|
||||
export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
|
||||
const category = getCategoryName(columnUnit);
|
||||
if (isEmpty(category)) {
|
||||
return categoryToSupport.map((category) => ({
|
||||
label: category,
|
||||
options: getCategorySelectOptionByName(category),
|
||||
}));
|
||||
}
|
||||
return categoryToSupport
|
||||
.filter((supportedCategory) => supportedCategory === category)
|
||||
.map((filteredCategory) => ({
|
||||
label: filteredCategory,
|
||||
options: getCategorySelectOptionByName(filteredCategory),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||
|
||||
interface FunctionProps {
|
||||
query: IBuilderQuery;
|
||||
@@ -57,6 +57,13 @@ export default function Function({
|
||||
? logsQueryFunctionOptions
|
||||
: metricQueryFunctionOptions;
|
||||
|
||||
const disableRemoveFunction = funcData.name === QueryFunctionsTypes.ANOMALY;
|
||||
|
||||
if (funcData.name === QueryFunctionsTypes.ANOMALY) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex className="query-function">
|
||||
<Select
|
||||
@@ -92,6 +99,7 @@ export default function Function({
|
||||
|
||||
<Button
|
||||
className="periscope-btn query-function-delete-btn"
|
||||
disabled={disableRemoveFunction}
|
||||
onClick={(): void => {
|
||||
handleDeleteFunction(funcData, index);
|
||||
}}
|
||||
|
||||
@@ -92,6 +92,8 @@ export default function QueryFunctions({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const hasAnomalyFunction = functions.some((func) => func.name === 'anomaly');
|
||||
|
||||
const handleAddNewFunction = (): void => {
|
||||
const defaultFunctionStruct =
|
||||
query.dataSource === DataSource.LOGS
|
||||
@@ -105,9 +107,22 @@ export default function QueryFunctions({
|
||||
},
|
||||
];
|
||||
|
||||
setFunctions(updatedFunctionsArr);
|
||||
const functionsCopy = cloneDeep(updatedFunctionsArr);
|
||||
|
||||
onChange(updatedFunctionsArr);
|
||||
const anomalyFuncIndex = functionsCopy.findIndex(
|
||||
(func) => func.name === 'anomaly',
|
||||
);
|
||||
|
||||
if (anomalyFuncIndex !== -1) {
|
||||
const anomalyFunc = functionsCopy[anomalyFuncIndex];
|
||||
|
||||
functionsCopy.splice(anomalyFuncIndex, 1);
|
||||
functionsCopy.push(anomalyFunc);
|
||||
}
|
||||
|
||||
setFunctions(functionsCopy);
|
||||
|
||||
onChange(functionsCopy);
|
||||
};
|
||||
|
||||
const handleDeleteFunction = (
|
||||
@@ -181,7 +196,9 @@ export default function QueryFunctions({
|
||||
<Tooltip
|
||||
title={
|
||||
functions && functions.length >= 3 ? (
|
||||
'Functions are in early access. You can add a maximum of 3 function as of now.'
|
||||
`Functions are in early access. You can add a maximum of ${
|
||||
hasAnomalyFunction ? 2 : 3
|
||||
} function as of now.`
|
||||
) : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add new function
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import './QueryBuilderSearchV2.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import {
|
||||
ArrowDown,
|
||||
|
||||
@@ -319,7 +319,7 @@ function QueryBuilderSearchV2(
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
|
||||
setSearchValue(`${(parsedValue as BaseAutocompleteData)?.key} `);
|
||||
}
|
||||
} else if (currentState === DropdownState.OPERATOR) {
|
||||
if (isEmpty(value) && currentFilterItem?.key?.key) {
|
||||
@@ -360,7 +360,7 @@ function QueryBuilderSearchV2(
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
|
||||
setSearchValue(`${currentFilterItem?.key?.key} ${value} `);
|
||||
}
|
||||
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
const operatorType =
|
||||
@@ -512,11 +512,6 @@ function QueryBuilderSearchV2(
|
||||
|
||||
// this useEffect takes care of tokenisation based on the search state
|
||||
useEffect(() => {
|
||||
// if we are still fetching the suggestions then return as we won't know the type / data-type etc for the attribute key
|
||||
if (isFetchingSuggestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no search value reset to the default state
|
||||
if (!searchValue) {
|
||||
setCurrentFilterItem(undefined);
|
||||
@@ -766,6 +761,7 @@ function QueryBuilderSearchV2(
|
||||
suggestionsData?.payload?.attributes,
|
||||
]);
|
||||
|
||||
// keep the query in sync with the selected tags in logs explorer page
|
||||
useEffect(() => {
|
||||
const filterTags: IBuilderQuery['filters'] = {
|
||||
op: 'AND',
|
||||
@@ -788,16 +784,14 @@ function QueryBuilderSearchV2(
|
||||
|
||||
if (!isEqual(query.filters, filterTags)) {
|
||||
onChange(filterTags);
|
||||
setTags(
|
||||
filterTags.items.map((tag) => ({
|
||||
...tag,
|
||||
op: getOperatorFromValue(tag.op),
|
||||
})) as ITag[],
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tags]);
|
||||
|
||||
// keep the use effects pure!
|
||||
// if the tags lacks the ID then the above use effect will add it to query
|
||||
// and then the below use effect will take care of adding it to the tags.
|
||||
// keep the tags in sycn with current query.
|
||||
useEffect(() => {
|
||||
// convert the query and tags to same format before comparison
|
||||
if (!isEqual(getInitTags(query), tags)) {
|
||||
|
||||
@@ -30,12 +30,12 @@
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
property="og:image"
|
||||
content="https://signoz.io/img/signoz-hero-image.webp"
|
||||
content="/images/signoz-hero-image.webp"
|
||||
/>
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
name="twitter:image"
|
||||
content="https://signoz.io/img/signoz-hero-image.webp"
|
||||
content="/images/signoz-hero-image.webp"
|
||||
/>
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
@@ -51,26 +51,10 @@
|
||||
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
|
||||
<meta name="robots" content="noindex">
|
||||
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Fira+Code"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/uplot@1.6.26/dist/uPlot.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="/css/uPlot.min.css" />
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&family=Work+Sans:wght@500&family=Space+Mono&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { cloneDeep, isUndefined } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { generateColor } from './generateColor';
|
||||
|
||||
function getXAxisTimestamps(seriesList: QueryData[]): number[] {
|
||||
const timestamps = new Set();
|
||||
|
||||
@@ -95,6 +97,7 @@ export const getUPlotChartData = (
|
||||
|
||||
const processAnomalyDetectionData = (
|
||||
anomalyDetectionData: any,
|
||||
isDarkMode: boolean,
|
||||
): Record<string, { data: number[][]; color: string }> => {
|
||||
if (!anomalyDetectionData) {
|
||||
return {};
|
||||
@@ -126,7 +129,8 @@ const processAnomalyDetectionData = (
|
||||
legend || '',
|
||||
);
|
||||
|
||||
const objKey = `${queryName}-${label}`;
|
||||
const objKey =
|
||||
anomalyDetectionData.length > 1 ? `${queryName}-${label}` : label;
|
||||
|
||||
processedData[objKey] = {
|
||||
data: [
|
||||
@@ -136,7 +140,10 @@ const processAnomalyDetectionData = (
|
||||
upperBoundSeries[index].values.map((v: { value: number }) => v.value),
|
||||
lowerBoundSeries[index].values.map((v: { value: number }) => v.value),
|
||||
],
|
||||
color: colors[index],
|
||||
color: generateColor(
|
||||
objKey,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
),
|
||||
legendLabel: label,
|
||||
};
|
||||
}
|
||||
@@ -146,7 +153,8 @@ const processAnomalyDetectionData = (
|
||||
};
|
||||
|
||||
export const getUplotChartDataForAnomalyDetection = (
|
||||
apiResponse?: MetricRangePayloadProps,
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
isDarkMode: boolean,
|
||||
): Record<
|
||||
string,
|
||||
{
|
||||
@@ -156,6 +164,5 @@ export const getUplotChartDataForAnomalyDetection = (
|
||||
}
|
||||
> => {
|
||||
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
|
||||
|
||||
return processAnomalyDetectionData(anomalyDetectionData);
|
||||
return processAnomalyDetectionData(anomalyDetectionData, isDarkMode);
|
||||
};
|
||||
|
||||
@@ -233,6 +233,47 @@ GetYAxisScale): { auto?: boolean; range?: uPlot.Scale.Range } => {
|
||||
return { auto: false, range: [min, max] };
|
||||
};
|
||||
|
||||
function adjustMinMax(
|
||||
min: number,
|
||||
max: number,
|
||||
): {
|
||||
adjustedMin: number;
|
||||
adjustedMax: number;
|
||||
} {
|
||||
// Ensure min and max are valid
|
||||
if (min === -Infinity && max === Infinity) {
|
||||
return { adjustedMin: -Infinity, adjustedMax: Infinity };
|
||||
}
|
||||
|
||||
const range = max - min;
|
||||
const adjustment = range * 0.1;
|
||||
|
||||
let adjustedMin: number;
|
||||
let adjustedMax: number;
|
||||
|
||||
// Handle the case for -Infinity
|
||||
if (min === -Infinity) {
|
||||
adjustedMin = -Infinity;
|
||||
} else if (min === 0) {
|
||||
adjustedMin = min - adjustment; // Special case for when min is 0
|
||||
} else if (min < 0) {
|
||||
// For negative min, add 10% of the range to bring closer to zero
|
||||
adjustedMin = min - range * 0.1;
|
||||
} else {
|
||||
// For positive min, subtract 10% from min itself
|
||||
adjustedMin = min - min * 0.1;
|
||||
}
|
||||
|
||||
// Handle the case for Infinity
|
||||
if (max === Infinity) {
|
||||
adjustedMax = Infinity;
|
||||
} else {
|
||||
adjustedMax = max * 1.1; // Regular case for finite max
|
||||
}
|
||||
|
||||
return { adjustedMin, adjustedMax };
|
||||
}
|
||||
|
||||
function getMinMax(data: any): { minValue: number; maxValue: number } {
|
||||
// Exclude the first array
|
||||
const arrays = data.slice(1);
|
||||
@@ -244,7 +285,9 @@ function getMinMax(data: any): { minValue: number; maxValue: number } {
|
||||
const minValue = flattened.length ? Math.min(...flattened) : 0;
|
||||
const maxValue = Math.max(...flattened);
|
||||
|
||||
return { minValue, maxValue };
|
||||
const { adjustedMin, adjustedMax } = adjustMinMax(minValue, maxValue);
|
||||
|
||||
return { minValue: adjustedMin, maxValue: adjustedMax };
|
||||
}
|
||||
|
||||
export const getYAxisScaleForAnomalyDetection = ({
|
||||
|
||||
@@ -11,20 +11,19 @@
|
||||
.edit-rules-card {
|
||||
width: 20rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
.content {
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const KAFKA_SETUP_DOC_LINK =
|
||||
'https://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master';
|
||||
'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started';
|
||||
|
||||
export function convertToTitleCase(text: string): string {
|
||||
return text
|
||||
|
||||
@@ -303,7 +303,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
return (
|
||||
loading ||
|
||||
!values.email ||
|
||||
!values.organizationName ||
|
||||
(!precheck.sso && (!values.password || !values.confirmPassword)) ||
|
||||
(!isDetailsDisable && !values.firstName) ||
|
||||
confirmPasswordError ||
|
||||
@@ -354,7 +353,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
<FormContainer.Item noStyle name="organizationName">
|
||||
<Input
|
||||
placeholder={t('placeholder_orgname')}
|
||||
required
|
||||
id="organizationName"
|
||||
disabled={isDetailsDisable}
|
||||
/>
|
||||
|
||||
@@ -274,3 +274,28 @@ notifications - 2050
|
||||
url('../public/fonts/GeistMonoVF.woff2') format('woff');
|
||||
/* Add other formats if needed (e.g., woff2, truetype, opentype, svg) */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../public/fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype');
|
||||
font-weight: 300 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
src: url('../public/fonts/WorkSans-VariableFont_wght.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url('../public/fonts/SpaceMono-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fira Code';
|
||||
src: url('../public/fonts/FiraCode-VariableFont_wght.ttf') format('truetype');
|
||||
font-weight: 300 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
39
frontend/src/types/api/preferences/userOrgPreferences.ts
Normal file
39
frontend/src/types/api/preferences/userOrgPreferences.ts
Normal 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>;
|
||||
}
|
||||
@@ -50,6 +50,7 @@ export type OrderByPayload = {
|
||||
export interface QueryFunctionProps {
|
||||
name: string;
|
||||
args: string[];
|
||||
namedArgs?: Record<string, any>;
|
||||
}
|
||||
|
||||
// Type for query builder
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
|
||||
export const getDefaultLogBackground = (
|
||||
isReadOnly?: boolean,
|
||||
@@ -17,10 +18,28 @@ export const getDefaultLogBackground = (
|
||||
export const getActiveLogBackground = (
|
||||
isActiveLog = true,
|
||||
isDarkMode = true,
|
||||
logType?: string,
|
||||
): string => {
|
||||
if (!isActiveLog) return ``;
|
||||
if (isDarkMode) return `background-color: ${Color.BG_SLATE_200};`;
|
||||
return `background-color: ${Color.BG_VANILLA_300}; color: ${Color.TEXT_SLATE_400}`;
|
||||
if (isDarkMode) {
|
||||
switch (logType) {
|
||||
case LogType.INFO:
|
||||
return `background-color: ${Color.BG_ROBIN_500}10 !important;`;
|
||||
case LogType.WARN:
|
||||
return `background-color: ${Color.BG_AMBER_500}10 !important;`;
|
||||
case LogType.ERROR:
|
||||
return `background-color: ${Color.BG_CHERRY_500}10 !important;`;
|
||||
case LogType.TRACE:
|
||||
return `background-color: ${Color.BG_FOREST_400}10 !important;`;
|
||||
case LogType.DEBUG:
|
||||
return `background-color: ${Color.BG_AQUA_500}10 !important;`;
|
||||
case LogType.FATAL:
|
||||
return `background-color: ${Color.BG_SAKURA_500}10 !important;`;
|
||||
default:
|
||||
return `background-color: ${Color.BG_SLATE_200} !important;`;
|
||||
}
|
||||
}
|
||||
return `background-color: ${Color.BG_VANILLA_300}!important; color: ${Color.TEXT_SLATE_400} !important;`;
|
||||
};
|
||||
|
||||
export const getHightLightedLogBackground = (
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -486,6 +518,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
|
||||
|
||||
// === Authentication APIs ===
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/bulk", am.AdminAccess(aH.inviteUsers)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/{email}", am.AdminAccess(aH.revokeInvite)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.listPendingInvites)).Methods(http.MethodGet)
|
||||
@@ -1976,6 +2009,32 @@ func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
|
||||
aH.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) inviteUsers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := parseInviteUsersRequest(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := auth.InviteUsers(ctx, req)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
// Check the response status and set the appropriate HTTP status code
|
||||
if response.Status == "failure" {
|
||||
w.WriteHeader(http.StatusBadRequest) // 400 Bad Request for failure
|
||||
} else if response.Status == "partial_success" {
|
||||
w.WriteHeader(http.StatusPartialContent) // 206 Partial Content
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK) // 200 OK for success
|
||||
}
|
||||
|
||||
aH.WriteJSON(w, r, response)
|
||||
}
|
||||
|
||||
// getInvite returns the invite object details for the given invite token. We do not need to
|
||||
// protect this API because invite token itself is meant to be private.
|
||||
func (aH *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2489,22 +2548,35 @@ func (aH *APIHandler) RegisterMessagingQueuesRoutes(router *mux.Router, am *Auth
|
||||
// SubRouter for kafka
|
||||
kafkaRouter := router.PathPrefix("/api/v1/messaging-queues/kafka").Subrouter()
|
||||
|
||||
consumerLagRouter := kafkaRouter.PathPrefix("/consumer-lag").Subrouter()
|
||||
consumerLagRouter.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerData)).Methods(http.MethodPost)
|
||||
consumerLagRouter.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerData)).Methods(http.MethodPost)
|
||||
consumerLagRouter.HandleFunc("/network-latency", am.ViewAccess(aH.getNetworkData)).Methods(http.MethodPost)
|
||||
|
||||
onboardingRouter := kafkaRouter.PathPrefix("/onboarding").Subrouter()
|
||||
onboardingRouter.HandleFunc("/producers", am.ViewAccess(aH.onboardProducers)).Methods(http.MethodPost)
|
||||
onboardingRouter.HandleFunc("/consumers", am.ViewAccess(aH.onboardConsumers)).Methods(http.MethodPost)
|
||||
onboardingRouter.HandleFunc("/kafka", am.ViewAccess(aH.onboardKafka)).Methods(http.MethodPost)
|
||||
|
||||
partitionLatency := kafkaRouter.PathPrefix("/partition-latency").Subrouter()
|
||||
partitionLatency.HandleFunc("/overview", am.ViewAccess(aH.getPartitionOverviewLatencyData)).Methods(http.MethodPost)
|
||||
partitionLatency.HandleFunc("/consumer", am.ViewAccess(aH.getConsumerPartitionLatencyData)).Methods(http.MethodPost)
|
||||
|
||||
consumerLagRouter := kafkaRouter.PathPrefix("/consumer-lag").Subrouter()
|
||||
consumerLagRouter.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerData)).Methods(http.MethodPost)
|
||||
consumerLagRouter.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerData)).Methods(http.MethodPost)
|
||||
consumerLagRouter.HandleFunc("/network-latency", am.ViewAccess(aH.getNetworkData)).Methods(http.MethodPost)
|
||||
|
||||
topicThroughput := kafkaRouter.PathPrefix("/topic-throughput").Subrouter()
|
||||
topicThroughput.HandleFunc("/producer", am.ViewAccess(aH.getProducerThroughputOverview)).Methods(http.MethodPost)
|
||||
topicThroughput.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerThroughputDetails)).Methods(http.MethodPost)
|
||||
topicThroughput.HandleFunc("/consumer", am.ViewAccess(aH.getConsumerThroughputOverview)).Methods(http.MethodPost)
|
||||
topicThroughput.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerThroughputDetails)).Methods(http.MethodPost)
|
||||
|
||||
spanEvaluation := kafkaRouter.PathPrefix("/span").Subrouter()
|
||||
spanEvaluation.HandleFunc("/evaluation", am.ViewAccess(aH.getProducerConsumerEval)).Methods(http.MethodPost)
|
||||
|
||||
// for other messaging queues, add SubRouters here
|
||||
}
|
||||
|
||||
// not using md5 hashing as the plain string would work
|
||||
func uniqueIdentifier(clientID, serviceInstanceID, serviceName, separator string) string {
|
||||
return clientID + separator + serviceInstanceID + separator + serviceName
|
||||
func uniqueIdentifier(params []string, separator string) string {
|
||||
return strings.Join(params, separator)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) onboardProducers(
|
||||
@@ -2847,7 +2919,7 @@ func (aH *APIHandler) getNetworkData(
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQRParamsNetwork(messagingQueue, "throughput", attributeCache)
|
||||
queryRangeParams, err := mq.BuildQRParamsWithCache(messagingQueue, "throughput", attributeCache)
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -2874,7 +2946,8 @@ func (aH *APIHandler) getNetworkData(
|
||||
clientID, clientIDOk := series.Labels["client_id"]
|
||||
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
|
||||
params := []string{clientID, serviceInstanceID, serviceName}
|
||||
hashKey := uniqueIdentifier(params, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if clientIDOk && serviceInstanceIDOk && serviceNameOk && !ok {
|
||||
attributeCache.Hash[hashKey] = struct{}{}
|
||||
@@ -2885,7 +2958,7 @@ func (aH *APIHandler) getNetworkData(
|
||||
}
|
||||
}
|
||||
|
||||
queryRangeParams, err = mq.BuildQRParamsNetwork(messagingQueue, "fetch-latency", attributeCache)
|
||||
queryRangeParams, err = mq.BuildQRParamsWithCache(messagingQueue, "fetch-latency", attributeCache)
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -2911,7 +2984,8 @@ func (aH *APIHandler) getNetworkData(
|
||||
clientID, clientIDOk := series.Labels["client_id"]
|
||||
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#")
|
||||
params := []string{clientID, serviceInstanceID, serviceName}
|
||||
hashKey := uniqueIdentifier(params, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if clientIDOk && serviceInstanceIDOk && serviceNameOk && ok {
|
||||
latencySeries = append(latencySeries, series)
|
||||
@@ -3013,6 +3087,362 @@ func (aH *APIHandler) getConsumerData(
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// s1
|
||||
func (aH *APIHandler) getPartitionOverviewLatencyData(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "partition_latency")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
result = postprocess.TransformToTableForClickHouseQueries(result)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// s1
|
||||
func (aH *APIHandler) getConsumerPartitionLatencyData(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "consumer_partition_latency")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
result = postprocess.TransformToTableForClickHouseQueries(result)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// s3 p overview
|
||||
// fetch traces
|
||||
// cache attributes
|
||||
// fetch byte rate metrics
|
||||
func (aH *APIHandler) getProducerThroughputOverview(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
attributeCache := &mq.Clients{
|
||||
Hash: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview", attributeCache)
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
|
||||
for _, res := range result {
|
||||
for _, series := range res.Series {
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
topicName, topicNameOk := series.Labels["topic"]
|
||||
params := []string{serviceName, topicName}
|
||||
hashKey := uniqueIdentifier(params, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if topicNameOk && serviceNameOk && !ok {
|
||||
attributeCache.Hash[hashKey] = struct{}{}
|
||||
attributeCache.TopicName = append(attributeCache.TopicName, topicName)
|
||||
attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryRangeParams, err = mq.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview-latency", attributeCache)
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resultFetchLatency, errQueriesByNameFetchLatency, err := aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQueriesByNameFetchLatency)
|
||||
return
|
||||
}
|
||||
|
||||
latencyColumn := &v3.Result{QueryName: "latency"}
|
||||
var latencySeries []*v3.Series
|
||||
for _, res := range resultFetchLatency {
|
||||
for _, series := range res.Series {
|
||||
topic, topicOk := series.Labels["topic"]
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
params := []string{topic, serviceName}
|
||||
hashKey := uniqueIdentifier(params, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if topicOk && serviceNameOk && ok {
|
||||
latencySeries = append(latencySeries, series)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
latencyColumn.Series = latencySeries
|
||||
result = append(result, latencyColumn)
|
||||
|
||||
resultFetchLatency = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: resultFetchLatency,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// s3 p details
|
||||
func (aH *APIHandler) getProducerThroughputDetails(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "producer-throughput-details")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
result = postprocess.TransformToTableForClickHouseQueries(result)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// s3 c overview
|
||||
func (aH *APIHandler) getConsumerThroughputOverview(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "consumer-throughput-overview")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
result = postprocess.TransformToTableForClickHouseQueries(result)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// s3 c details
|
||||
func (aH *APIHandler) getConsumerThroughputDetails(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "consumer-throughput-details")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
result = postprocess.TransformToTableForClickHouseQueries(result)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// s4
|
||||
// needs logic to parse duration
|
||||
// needs logic to get the percentage
|
||||
// show 10 traces
|
||||
func (aH *APIHandler) getProducerConsumerEval(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
messagingQueue, apiErr := ParseMessagingQueueBody(r)
|
||||
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := mq.BuildQueryRangeParams(messagingQueue, "producer-consumer-eval")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
// ParseMessagingQueueBody parse for messaging queue params
|
||||
func ParseMessagingQueueBody(r *http.Request) (*mq.MessagingQueue, *model.ApiError) {
|
||||
messagingQueue := new(mq.MessagingQueue)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
342
pkg/query-service/app/inframetrics/clusters.go
Normal file
342
pkg/query-service/app/inframetrics/clusters.go
Normal 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
|
||||
}
|
||||
81
pkg/query-service/app/inframetrics/common.go
Normal file
81
pkg/query-service/app/inframetrics/common.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
329
pkg/query-service/app/inframetrics/namespaces.go
Normal file
329
pkg/query-service/app/inframetrics/namespaces.go
Normal 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
|
||||
}
|
||||
349
pkg/query-service/app/inframetrics/nodes.go
Normal file
349
pkg/query-service/app/inframetrics/nodes.go
Normal 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
|
||||
}
|
||||
118
pkg/query-service/app/inframetrics/nodes_query.go
Normal file
118
pkg/query-service/app/inframetrics/nodes_query.go
Normal 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,
|
||||
}
|
||||
387
pkg/query-service/app/inframetrics/pods.go
Normal file
387
pkg/query-service/app/inframetrics/pods.go
Normal 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
|
||||
}
|
||||
196
pkg/query-service/app/inframetrics/pods_query.go
Normal file
196
pkg/query-service/app/inframetrics/pods_query.go
Normal 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,
|
||||
}
|
||||
@@ -28,6 +28,7 @@ Response in query range `table` format
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -519,3 +520,461 @@ Response in query range `table` format
|
||||
]
|
||||
}
|
||||
```
|
||||
### Partition Latency
|
||||
|
||||
```json
|
||||
/api/v1/messaging-queues/kafka/partition-latency/overview
|
||||
```
|
||||
```json
|
||||
{
|
||||
"start": 1728287046000000000,
|
||||
"end": 1728587046000000000
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "topic",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "partition_latency",
|
||||
"queryName": "partition_latency",
|
||||
"isValueColumn": true
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"p99": "2",
|
||||
"partition_latency": 1.18,
|
||||
"topic": "topic1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"p99": "2",
|
||||
"partition_latency": 0.15,
|
||||
"topic": "topic2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"p99": "2",
|
||||
"partition_latency": 0.26,
|
||||
"topic": "topic3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
---------
|
||||
|
||||
```json
|
||||
/api/v1/messaging-queues/kafka/partition-latency/consumer
|
||||
```
|
||||
```json
|
||||
{
|
||||
"start": 1728287046000000000,
|
||||
"end": 1728587046000000000,
|
||||
"variables": {
|
||||
"partition": "2",
|
||||
"topic": "topic1"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "consumer_group",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "service_name",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "throughput",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"consumer_group": "cg1",
|
||||
"error_rate": "0",
|
||||
"p99": "0.11994228000000004",
|
||||
"service_name": "consumer-svc",
|
||||
"throughput": "1.18116"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
---------
|
||||
### Topic throughput
|
||||
|
||||
```json
|
||||
/api/v1/messaging-queues/kafka/topic-throughput/producer
|
||||
```
|
||||
```json
|
||||
{
|
||||
"start": 1728287046000000000,
|
||||
"end": 1728587046000000000
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "topic",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "serviceName",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "throughput",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "8.662880220000002",
|
||||
"serviceName": "producer-svc1",
|
||||
"throughput": "0.41642666666666667",
|
||||
"topic": "topic1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "9.786847500000016",
|
||||
"serviceName": "producer-svc2",
|
||||
"throughput": "0.76473",
|
||||
"topic": "topic1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "14.432925500000021",
|
||||
"serviceName": "producer-svc3",
|
||||
"throughput": "0.08976",
|
||||
"topic": "topic2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "14.32833297000002",
|
||||
"serviceName": "producer-svc2",
|
||||
"throughput": "0.06449333333333333",
|
||||
"topic": "topic2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "13.416533810000036",
|
||||
"serviceName": "producer-svc4",
|
||||
"throughput": "0.14766",
|
||||
"topic": "topic3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "13.366232000000034",
|
||||
"serviceName": "producer-svc3",
|
||||
"throughput": "0.11166666666666666",
|
||||
"topic": "topic3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
---------
|
||||
### Topic throughput
|
||||
|
||||
```json
|
||||
/api/v1/messaging-queues/kafka/topic-throughput/producer-details
|
||||
```
|
||||
```json
|
||||
{
|
||||
"start": 1728287046000000000,
|
||||
"end": 1728587046000000000,
|
||||
"variables": {
|
||||
"partition": "2",
|
||||
"topic": "topic1",
|
||||
"service_name": "producer-svc2"
|
||||
}
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "partition",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "throughput",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "9.165558780000026",
|
||||
"partition": "2",
|
||||
"throughput": "0.76473"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
---------
|
||||
### Topic throughput
|
||||
|
||||
```json
|
||||
/api/v1/messaging-queues/kafka/topic-throughput/consumer
|
||||
```
|
||||
```json
|
||||
{
|
||||
"start": 1728287046000000000,
|
||||
"end": 1728587046000000000
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "topic",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "service_name",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "ingestion_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "byte_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"byte_rate": "17.7174",
|
||||
"error_rate": "0",
|
||||
"ingestion_rate": "1.18116",
|
||||
"p99": "0.12260112000000009",
|
||||
"service_name": "consumer-svc",
|
||||
"topic": "topic1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"byte_rate": "2.1594533333333334",
|
||||
"error_rate": "0",
|
||||
"ingestion_rate": "0.15424666666666667",
|
||||
"p99": "7.4079657800000005",
|
||||
"service_name": "consumer-svc2",
|
||||
"topic": "topic2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"byte_rate": "3.66446",
|
||||
"error_rate": "0",
|
||||
"ingestion_rate": "0.25933",
|
||||
"p99": "6.135769970000011",
|
||||
"service_name": "consumer-svc3",
|
||||
"topic": "topic3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
---------
|
||||
### Topic throughput
|
||||
|
||||
```json
|
||||
/api/v1/messaging-queues/kafka/topic-throughput/consumer-details
|
||||
```
|
||||
```json
|
||||
{
|
||||
"start": 1728287046000000000,
|
||||
"end": 1728587046000000000,
|
||||
"variables": {
|
||||
"topic": "topic1",
|
||||
"service_name": "consumer-svc"
|
||||
}
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "",
|
||||
"result": [
|
||||
{
|
||||
"table": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "partition",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "p99",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "error_rate",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
},
|
||||
{
|
||||
"name": "throughput",
|
||||
"queryName": "",
|
||||
"isValueColumn": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"data": {
|
||||
"error_rate": "0",
|
||||
"p99": "0.11789381000000003",
|
||||
"partition": "2",
|
||||
"throughput": "1.18116"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,6 +5,7 @@ const KafkaQueue = "kafka"
|
||||
type MessagingQueue struct {
|
||||
Start int64 `json:"start"`
|
||||
End int64 `json:"end"`
|
||||
EvalTime int64 `json:"eval_time"`
|
||||
Variables map[string]string `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ type Clients struct {
|
||||
ClientID []string
|
||||
ServiceInstanceID []string
|
||||
ServiceName []string
|
||||
TopicName []string
|
||||
}
|
||||
|
||||
type OnboardingResponse struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ WITH consumer_query AS (
|
||||
serviceName,
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
COUNT(*) AS total_requests,
|
||||
SUM(CASE WHEN statusCode = 2 THEN 1 ELSE 0 END) AS error_count,
|
||||
sumIf(1, statusCode = 2) AS error_count,
|
||||
avg(CASE WHEN has(numberTagMap, 'messaging.message.body.size') THEN numberTagMap['messaging.message.body.size'] ELSE NULL END) AS avg_msg_size
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
@@ -30,7 +30,7 @@ SELECT
|
||||
serviceName AS service_name,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
|
||||
COALESCE(total_requests / %d, 0) AS throughput, -- Convert nanoseconds to seconds
|
||||
COALESCE(total_requests / %d, 0) AS throughput,
|
||||
COALESCE(avg_msg_size, 0) AS avg_msg_size
|
||||
FROM
|
||||
consumer_query
|
||||
@@ -40,6 +40,257 @@ ORDER BY
|
||||
return query
|
||||
}
|
||||
|
||||
// S1 landing
|
||||
func generatePartitionLatencySQL(start, end int64, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
query := fmt.Sprintf(`
|
||||
WITH partition_query AS (
|
||||
SELECT
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
count(*) AS total_requests,
|
||||
stringTagMap['messaging.destination.name'] AS topic,
|
||||
stringTagMap['messaging.destination.partition.id'] AS partition
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
AND timestamp <= '%d'
|
||||
AND kind = 4
|
||||
AND msgSystem = '%s'
|
||||
GROUP BY topic, partition
|
||||
)
|
||||
|
||||
SELECT
|
||||
topic,
|
||||
partition,
|
||||
p99,
|
||||
COALESCE(total_requests / %d, 0) AS throughput
|
||||
FROM
|
||||
partition_query
|
||||
ORDER BY
|
||||
topic;
|
||||
`, start, end, queueType, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
// S1 consumer
|
||||
func generateConsumerPartitionLatencySQL(start, end int64, topic, partition, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
query := fmt.Sprintf(`
|
||||
WITH consumer_pl AS (
|
||||
SELECT
|
||||
stringTagMap['messaging.kafka.consumer.group'] AS consumer_group,
|
||||
serviceName,
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
COUNT(*) AS total_requests,
|
||||
sumIf(1, statusCode = 2) AS error_count
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
AND timestamp <= '%d'
|
||||
AND kind = 5
|
||||
AND msgSystem = '%s'
|
||||
AND stringTagMap['messaging.destination.name'] = '%s'
|
||||
AND stringTagMap['messaging.destination.partition.id'] = '%s'
|
||||
GROUP BY consumer_group, serviceName
|
||||
)
|
||||
|
||||
SELECT
|
||||
consumer_group,
|
||||
serviceName AS service_name,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
|
||||
COALESCE(total_requests / %d, 0) AS throughput
|
||||
FROM
|
||||
consumer_pl
|
||||
ORDER BY
|
||||
consumer_group;
|
||||
`, start, end, queueType, topic, partition, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
// S3, producer overview
|
||||
func generateProducerPartitionThroughputSQL(start, end int64, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
// t, svc, rps, byte*, p99, err
|
||||
query := fmt.Sprintf(`
|
||||
WITH producer_latency AS (
|
||||
SELECT
|
||||
serviceName,
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
stringTagMap['messaging.destination.name'] AS topic,
|
||||
COUNT(*) AS total_requests,
|
||||
sumIf(1, statusCode = 2) AS error_count
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
AND timestamp <= '%d'
|
||||
AND kind = 4
|
||||
AND msgSystem = '%s'
|
||||
GROUP BY topic, serviceName
|
||||
)
|
||||
|
||||
SELECT
|
||||
topic,
|
||||
serviceName AS service_name,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
|
||||
COALESCE(total_requests / %d, 0) AS throughput
|
||||
FROM
|
||||
producer_latency
|
||||
`, start, end, queueType, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
// S3, producer topic/service overview
|
||||
func generateProducerTopicLatencySQL(start, end int64, topic, service, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
query := fmt.Sprintf(`
|
||||
WITH consumer_latency AS (
|
||||
SELECT
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
stringTagMap['messaging.destination.partition.id'] AS partition,
|
||||
COUNT(*) AS total_requests,
|
||||
sumIf(1, statusCode = 2) AS error_count
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
AND timestamp <= '%d'
|
||||
AND kind = 4
|
||||
AND serviceName = '%s'
|
||||
AND msgSystem = '%s'
|
||||
AND stringTagMap['messaging.destination.name'] = '%s'
|
||||
GROUP BY partition
|
||||
)
|
||||
|
||||
SELECT
|
||||
partition,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
|
||||
COALESCE(total_requests / %d, 0) AS throughput
|
||||
FROM
|
||||
consumer_latency
|
||||
`, start, end, service, queueType, topic, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
// S3 consumer overview
|
||||
func generateConsumerLatencySQL(start, end int64, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
query := fmt.Sprintf(`
|
||||
WITH consumer_latency AS (
|
||||
SELECT
|
||||
serviceName,
|
||||
stringTagMap['messaging.destination.name'] AS topic,
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
COUNT(*) AS total_requests,
|
||||
sumIf(1, statusCode = 2) AS error_count,
|
||||
SUM(numberTagMap['messaging.message.body.size']) AS total_bytes
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
AND timestamp <= '%d'
|
||||
AND kind = 5
|
||||
AND msgSystem = '%s'
|
||||
GROUP BY topic, serviceName
|
||||
)
|
||||
|
||||
SELECT
|
||||
topic,
|
||||
serviceName AS service_name,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
|
||||
COALESCE(total_requests / %d, 0) AS ingestion_rate,
|
||||
COALESCE(total_bytes / %d, 0) AS byte_rate
|
||||
FROM
|
||||
consumer_latency
|
||||
ORDER BY
|
||||
topic;
|
||||
`, start, end, queueType, timeRange, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
// S3 consumer topic/service
|
||||
func generateConsumerServiceLatencySQL(start, end int64, topic, service, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
query := fmt.Sprintf(`
|
||||
WITH consumer_latency AS (
|
||||
SELECT
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
stringTagMap['messaging.destination.partition.id'] AS partition,
|
||||
COUNT(*) AS total_requests,
|
||||
sumIf(1, statusCode = 2) AS error_count
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
AND timestamp <= '%d'
|
||||
AND kind = 5
|
||||
AND serviceName = '%s'
|
||||
AND msgSystem = '%s'
|
||||
AND stringTagMap['messaging.destination.name'] = '%s'
|
||||
GROUP BY partition
|
||||
)
|
||||
|
||||
SELECT
|
||||
partition,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_requests, 0) AS error_rate,
|
||||
COALESCE(total_requests / %d, 0) AS throughput
|
||||
FROM
|
||||
consumer_latency
|
||||
`, start, end, service, queueType, topic, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
// s4
|
||||
func generateProducerConsumerEvalSQL(start, end int64, queueType string, evalTime int64) string {
|
||||
query := fmt.Sprintf(`
|
||||
WITH trace_data AS (
|
||||
SELECT
|
||||
p.serviceName AS producer_service,
|
||||
c.serviceName AS consumer_service,
|
||||
p.traceID,
|
||||
p.timestamp AS producer_timestamp,
|
||||
c.timestamp AS consumer_timestamp,
|
||||
p.durationNano AS durationNano,
|
||||
(toUnixTimestamp64Nano(c.timestamp) - toUnixTimestamp64Nano(p.timestamp)) + p.durationNano AS time_difference
|
||||
FROM
|
||||
signoz_traces.distributed_signoz_index_v2 p
|
||||
INNER JOIN
|
||||
signoz_traces.distributed_signoz_index_v2 c
|
||||
ON p.traceID = c.traceID
|
||||
AND c.parentSpanID = p.spanID
|
||||
WHERE
|
||||
p.kind = 4
|
||||
AND c.kind = 5
|
||||
AND toUnixTimestamp64Nano(p.timestamp) BETWEEN '%d' AND '%d'
|
||||
AND toUnixTimestamp64Nano(c.timestamp) BETWEEN '%d' AND '%d'
|
||||
AND c.msgSystem = '%s'
|
||||
AND p.msgSystem = '%s'
|
||||
)
|
||||
|
||||
SELECT
|
||||
producer_service,
|
||||
consumer_service,
|
||||
COUNT(*) AS total_spans,
|
||||
SUM(time_difference > '%d') AS breached_spans,
|
||||
((breached_spans) * 100.0) / total_spans AS breach_percentage,
|
||||
arraySlice(
|
||||
arrayMap(x -> x.1,
|
||||
arraySort(
|
||||
x -> -x.2,
|
||||
groupArrayIf((traceID, time_difference), time_difference > '%d')
|
||||
)
|
||||
),
|
||||
1, 10
|
||||
) AS top_traceIDs
|
||||
FROM trace_data
|
||||
GROUP BY
|
||||
producer_service,
|
||||
consumer_service
|
||||
`, start, end, start, end, queueType, queueType, evalTime, evalTime)
|
||||
return query
|
||||
}
|
||||
|
||||
func generateProducerSQL(start, end int64, topic, partition, queueType string) string {
|
||||
timeRange := (end - start) / 1000000000
|
||||
query := fmt.Sprintf(`
|
||||
@@ -48,7 +299,7 @@ WITH producer_query AS (
|
||||
serviceName,
|
||||
quantile(0.99)(durationNano) / 1000000 AS p99,
|
||||
count(*) AS total_count,
|
||||
SUM(CASE WHEN statusCode = 2 THEN 1 ELSE 0 END) AS error_count
|
||||
sumIf(1, statusCode = 2) AS error_count
|
||||
FROM signoz_traces.distributed_signoz_index_v2
|
||||
WHERE
|
||||
timestamp >= '%d'
|
||||
@@ -64,12 +315,11 @@ SELECT
|
||||
serviceName AS service_name,
|
||||
p99,
|
||||
COALESCE((error_count * 100.0) / total_count, 0) AS error_percentage,
|
||||
COALESCE(total_count / %d, 0) AS throughput -- Convert nanoseconds to seconds
|
||||
COALESCE(total_count / %d, 0) AS throughput
|
||||
FROM
|
||||
producer_query
|
||||
ORDER BY
|
||||
serviceName;
|
||||
|
||||
`, start, end, queueType, topic, partition, timeRange)
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ package kafka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
@@ -12,6 +11,9 @@ var defaultStepInterval int64 = 60
|
||||
|
||||
func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string) (*v3.QueryRangeParamsV3, error) {
|
||||
|
||||
if constants.KafkaSpanEval == "false" && queryContext == "producer-consumer-eval" {
|
||||
return nil, fmt.Errorf("span evaluation feature is disabled and is experimental")
|
||||
}
|
||||
// ToDo: propagate this through APIs when there are different handlers
|
||||
queueType := KafkaQueue
|
||||
|
||||
@@ -37,14 +39,6 @@ func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string)
|
||||
return queryRangeParams, nil
|
||||
}
|
||||
|
||||
func PrepareClikhouseQueries(messagingQueue *MessagingQueue, queryContext string) (*v3.ClickHouseQuery, error) {
|
||||
queueType := KafkaQueue
|
||||
|
||||
chq, err := BuildClickHouseQuery(messagingQueue, queueType, queryContext)
|
||||
|
||||
return chq, err
|
||||
}
|
||||
|
||||
func buildClickHouseQueryNetwork(messagingQueue *MessagingQueue, queueType string) (*v3.ClickHouseQuery, error) {
|
||||
start := messagingQueue.Start
|
||||
end := messagingQueue.End
|
||||
@@ -65,12 +59,60 @@ func buildClickHouseQueryNetwork(messagingQueue *MessagingQueue, queueType strin
|
||||
}, nil
|
||||
}
|
||||
|
||||
func formatstring(str []string) string {
|
||||
joined := strings.Join(str, ", ")
|
||||
if len(joined) <= 2 {
|
||||
return ""
|
||||
func buildBuilderQueriesProducerBytes(unixMilliStart, unixMilliEnd int64, attributeCache *Clients) (map[string]*v3.BuilderQuery, error) {
|
||||
bq := make(map[string]*v3.BuilderQuery)
|
||||
queryName := fmt.Sprintf("latency")
|
||||
|
||||
chq := &v3.BuilderQuery{
|
||||
QueryName: queryName,
|
||||
StepInterval: common.MinAllowedStepInterval(unixMilliStart, unixMilliEnd),
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "kafka_producer_byte_rate",
|
||||
},
|
||||
AggregateOperator: v3.AggregateOperatorAvg,
|
||||
Temporality: v3.Unspecified,
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationAvg,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_name",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorIn,
|
||||
Value: attributeCache.ServiceName,
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "topic",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorIn,
|
||||
Value: attributeCache.TopicName,
|
||||
},
|
||||
},
|
||||
},
|
||||
Expression: queryName,
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
GroupBy: []v3.AttributeKey{{
|
||||
Key: "service_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
{
|
||||
Key: "topic",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
},
|
||||
}
|
||||
return joined[1 : len(joined)-1]
|
||||
bq[queryName] = chq
|
||||
return bq, nil
|
||||
}
|
||||
|
||||
func buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd int64, attributeCache *Clients) (map[string]*v3.BuilderQuery, error) {
|
||||
@@ -143,7 +185,7 @@ func buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd int64, attributeCac
|
||||
return bq, nil
|
||||
}
|
||||
|
||||
func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, attributeCache *Clients) (*v3.QueryRangeParamsV3, error) {
|
||||
func BuildQRParamsWithCache(messagingQueue *MessagingQueue, queryContext string, attributeCache *Clients) (*v3.QueryRangeParamsV3, error) {
|
||||
|
||||
queueType := KafkaQueue
|
||||
|
||||
@@ -151,6 +193,7 @@ func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, a
|
||||
unixMilliEnd := messagingQueue.End / 1000000
|
||||
|
||||
var cq *v3.CompositeQuery
|
||||
var err error
|
||||
|
||||
if queryContext == "throughput" {
|
||||
chq, err := buildClickHouseQueryNetwork(messagingQueue, queueType)
|
||||
@@ -171,6 +214,24 @@ func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, a
|
||||
BuilderQueries: bhq,
|
||||
PanelType: v3.PanelTypeTable,
|
||||
}
|
||||
} else if queryContext == "producer-throughput-overview" {
|
||||
start := messagingQueue.Start
|
||||
end := messagingQueue.End
|
||||
query := generateProducerPartitionThroughputSQL(start, end, queueType)
|
||||
|
||||
cq, err = buildCompositeQuery(&v3.ClickHouseQuery{
|
||||
Query: query,
|
||||
}, queryContext)
|
||||
} else if queryContext == "producer-throughput-overview-latency" {
|
||||
bhq, err := buildBuilderQueriesProducerBytes(unixMilliStart, unixMilliEnd, attributeCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cq = &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: bhq,
|
||||
PanelType: v3.PanelTypeTable,
|
||||
}
|
||||
}
|
||||
|
||||
queryRangeParams := &v3.QueryRangeParamsV3{
|
||||
@@ -182,7 +243,7 @@ func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, a
|
||||
FormatForWeb: true,
|
||||
}
|
||||
|
||||
return queryRangeParams, nil
|
||||
return queryRangeParams, err
|
||||
}
|
||||
|
||||
func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, queryContext string) (*v3.ClickHouseQuery, error) {
|
||||
@@ -190,16 +251,22 @@ func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
|
||||
end := messagingQueue.End
|
||||
|
||||
var topic, partition string
|
||||
|
||||
if queryContext == "producer" || queryContext == "consumer" {
|
||||
if queryContext == "producer" ||
|
||||
queryContext == "consumer" ||
|
||||
queryContext == "consumer_partition_latency" ||
|
||||
queryContext == "producer-topic-throughput" ||
|
||||
queryContext == "producer-throughput-details" ||
|
||||
queryContext == "consumer-throughput-details" {
|
||||
var ok bool
|
||||
topic, ok = messagingQueue.Variables["topic"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type for Topic")
|
||||
}
|
||||
partition, ok = messagingQueue.Variables["partition"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type for Partition")
|
||||
if queryContext != "consumer-throughput-details" {
|
||||
partition, ok = messagingQueue.Variables["partition"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type for Partition")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +279,26 @@ func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
|
||||
return nil, fmt.Errorf("invalid type for consumer group")
|
||||
}
|
||||
query = generateConsumerSQL(start, end, topic, partition, consumerGroup, queueType)
|
||||
} else if queryContext == "producer-topic-throughput" {
|
||||
query = generatePartitionLatencySQL(start, end, queueType)
|
||||
} else if queryContext == "consumer_partition_latency" {
|
||||
query = generateConsumerPartitionLatencySQL(start, end, topic, partition, queueType)
|
||||
} else if queryContext == "producer-throughput-details" {
|
||||
svcName, ok := messagingQueue.Variables["service_name"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type for service")
|
||||
}
|
||||
query = generateProducerTopicLatencySQL(start, end, topic, svcName, queueType)
|
||||
} else if queryContext == "consumer-throughput-overview" {
|
||||
query = generateConsumerLatencySQL(start, end, queueType)
|
||||
} else if queryContext == "consumer-throughput-details" {
|
||||
svcName, ok := messagingQueue.Variables["service_name"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type for service")
|
||||
}
|
||||
query = generateConsumerServiceLatencySQL(start, end, topic, svcName, queueType)
|
||||
} else if queryContext == "producer-consumer-eval" {
|
||||
query = generateProducerConsumerEvalSQL(start, end, queueType, messagingQueue.EvalTime)
|
||||
} else if queryContext == "onboard_producers" {
|
||||
query = onboardProducersSQL(start, end, queueType)
|
||||
} else if queryContext == "onboard_consumers" {
|
||||
@@ -219,13 +306,21 @@ func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
|
||||
} else if queryContext == "onboard_kafka" {
|
||||
query = onboardKafkaSQL(start, end)
|
||||
}
|
||||
|
||||
return &v3.ClickHouseQuery{
|
||||
Query: query,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildCompositeQuery(chq *v3.ClickHouseQuery, queryContext string) (*v3.CompositeQuery, error) {
|
||||
|
||||
if queryContext == "producer-consumer-eva" {
|
||||
return &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeClickHouseSQL,
|
||||
ClickHouseQueries: map[string]*v3.ClickHouseQuery{queryContext: chq},
|
||||
PanelType: v3.PanelTypeList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeClickHouseSQL,
|
||||
ClickHouseQueries: map[string]*v3.ClickHouseQuery{queryContext: chq},
|
||||
|
||||
@@ -496,7 +496,7 @@ func IsOrderByTs(orderBy []v3.OrderBy) bool {
|
||||
// PrepareLogsQuery prepares the query for logs
|
||||
// start and end are in epoch millisecond
|
||||
// step is in seconds
|
||||
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.LogQBOptions) (string, error) {
|
||||
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.QBOptions) (string, error) {
|
||||
|
||||
// adjust the start and end time to the step interval
|
||||
// NOTE: Disabling this as it's creating confusion between charts and actual data
|
||||
|
||||
@@ -1201,7 +1201,7 @@ var testPrepLogsQueryData = []struct {
|
||||
TableName string
|
||||
AggregateOperator v3.AggregateOperator
|
||||
ExpectedQuery string
|
||||
Options v3.LogQBOptions
|
||||
Options v3.QBOptions
|
||||
}{
|
||||
{
|
||||
Name: "Test TS with limit- first",
|
||||
@@ -1223,7 +1223,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT `method` from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') group by `method` order by value DESC) LIMIT 10",
|
||||
Options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
Options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
},
|
||||
{
|
||||
Name: "Test TS with limit- first - with order by value",
|
||||
@@ -1246,7 +1246,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT `method` from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') group by `method` order by value ASC) LIMIT 10",
|
||||
Options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
Options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
},
|
||||
{
|
||||
Name: "Test TS with limit- first - with order by attribute",
|
||||
@@ -1269,7 +1269,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT `method` from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') group by `method` order by `method` ASC) LIMIT 10",
|
||||
Options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
Options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
},
|
||||
{
|
||||
Name: "Test TS with limit- second",
|
||||
@@ -1291,7 +1291,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') AND (`method`) GLOBAL IN (#LIMIT_PLACEHOLDER) group by `method`,ts order by value DESC",
|
||||
Options: v3.LogQBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
|
||||
Options: v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
|
||||
},
|
||||
{
|
||||
Name: "Test TS with limit- second - with order by",
|
||||
@@ -1314,7 +1314,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND has(attributes_string_key, 'method') AND has(attributes_string_key, 'name') AND (`method`) GLOBAL IN (#LIMIT_PLACEHOLDER) group by `method`,ts order by `method` ASC",
|
||||
Options: v3.LogQBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
|
||||
Options: v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
|
||||
},
|
||||
// Live tail
|
||||
{
|
||||
@@ -1334,7 +1334,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND ",
|
||||
Options: v3.LogQBOptions{IsLivetailQuery: true},
|
||||
Options: v3.QBOptions{IsLivetailQuery: true},
|
||||
},
|
||||
{
|
||||
Name: "Live Tail Query with contains",
|
||||
@@ -1353,7 +1353,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] ILIKE '%GET%' AND ",
|
||||
Options: v3.LogQBOptions{IsLivetailQuery: true},
|
||||
Options: v3.QBOptions{IsLivetailQuery: true},
|
||||
},
|
||||
{
|
||||
Name: "Live Tail Query W/O filter",
|
||||
@@ -1369,7 +1369,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where ",
|
||||
Options: v3.LogQBOptions{IsLivetailQuery: true},
|
||||
Options: v3.QBOptions{IsLivetailQuery: true},
|
||||
},
|
||||
{
|
||||
Name: "Table query w/o limit",
|
||||
@@ -1385,7 +1385,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC",
|
||||
Options: v3.LogQBOptions{},
|
||||
Options: v3.QBOptions{},
|
||||
},
|
||||
{
|
||||
Name: "Table query with limit",
|
||||
@@ -1402,7 +1402,7 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10",
|
||||
Options: v3.LogQBOptions{},
|
||||
Options: v3.QBOptions{},
|
||||
},
|
||||
{
|
||||
Name: "Ignore offset if order by is timestamp in list queries",
|
||||
@@ -1488,7 +1488,7 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
TableName string
|
||||
AggregateOperator v3.AggregateOperator
|
||||
ExpectedQuery string
|
||||
Options v3.LogQBOptions
|
||||
Options v3.QBOptions
|
||||
}{
|
||||
{
|
||||
Name: "Test limit less than pageSize - order by ts",
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
logsV3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/resource"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||
@@ -33,6 +34,7 @@ const (
|
||||
BODY = "body"
|
||||
DISTRIBUTED_LOGS_V2 = "distributed_logs_v2"
|
||||
DISTRIBUTED_LOGS_V2_RESOURCE = "distributed_logs_v2_resource"
|
||||
DB_NAME = "signoz_logs"
|
||||
NANOSECOND = 1000000000
|
||||
)
|
||||
|
||||
@@ -372,7 +374,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build
|
||||
}
|
||||
|
||||
// build the where clause for resource table
|
||||
resourceSubQuery, err := buildResourceSubQuery(bucketStart, bucketEnd, mq.Filters, mq.GroupBy, mq.AggregateAttribute, false)
|
||||
resourceSubQuery, err := resource.BuildResourceSubQuery(DB_NAME, DISTRIBUTED_LOGS_V2_RESOURCE, bucketStart, bucketEnd, mq.Filters, mq.GroupBy, mq.AggregateAttribute, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -463,7 +465,7 @@ func buildLogsLiveTailQuery(mq *v3.BuilderQuery) (string, error) {
|
||||
}
|
||||
|
||||
// no values for bucket start and end
|
||||
resourceSubQuery, err := buildResourceSubQuery(0, 0, mq.Filters, mq.GroupBy, mq.AggregateAttribute, true)
|
||||
resourceSubQuery, err := resource.BuildResourceSubQuery(DB_NAME, DISTRIBUTED_LOGS_V2_RESOURCE, 0, 0, mq.Filters, mq.GroupBy, mq.AggregateAttribute, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -491,7 +493,7 @@ func buildLogsLiveTailQuery(mq *v3.BuilderQuery) (string, error) {
|
||||
}
|
||||
|
||||
// PrepareLogsQuery prepares the query for logs
|
||||
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.LogQBOptions) (string, error) {
|
||||
func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options v3.QBOptions) (string, error) {
|
||||
|
||||
// adjust the start and end time to the step interval
|
||||
// NOTE: Disabling this as it's creating confusion between charts and actual data
|
||||
|
||||
@@ -806,7 +806,7 @@ func TestPrepareLogsQuery(t *testing.T) {
|
||||
queryType v3.QueryType
|
||||
panelType v3.PanelType
|
||||
mq *v3.BuilderQuery
|
||||
options v3.LogQBOptions
|
||||
options v3.QBOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -875,7 +875,7 @@ func TestPrepareLogsQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
GroupBy: []v3.AttributeKey{{Key: "user", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
|
||||
},
|
||||
options: v3.LogQBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
options: v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, PreferRPM: true},
|
||||
},
|
||||
want: "SELECT `user` from (SELECT attributes_string['user'] as `user`, toFloat64(count(distinct(attributes_string['name']))) as value from signoz_logs.distributed_logs_v2 " +
|
||||
"where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND (ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458) AND attributes_string['method'] = 'GET' " +
|
||||
@@ -904,7 +904,7 @@ func TestPrepareLogsQuery(t *testing.T) {
|
||||
GroupBy: []v3.AttributeKey{{Key: "user", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
|
||||
Limit: 2,
|
||||
},
|
||||
options: v3.LogQBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
|
||||
options: v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit},
|
||||
},
|
||||
want: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string['user'] as `user`, toFloat64(count(distinct(attributes_string['name']))) as value " +
|
||||
"from signoz_logs.distributed_logs_v2 where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND (ts_bucket_start >= 1680064560 AND ts_bucket_start <= 1680066458) AND " +
|
||||
@@ -929,7 +929,7 @@ func TestPrepareLogsQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
options: v3.LogQBOptions{IsLivetailQuery: true},
|
||||
options: v3.QBOptions{IsLivetailQuery: true},
|
||||
},
|
||||
want: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body, attributes_string, attributes_number, attributes_bool, resources_string " +
|
||||
"from signoz_logs.distributed_logs_v2 where attributes_string['method'] = 'GET' AND mapContains(attributes_string, 'method') AND ",
|
||||
@@ -952,7 +952,7 @@ func TestPrepareLogsQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
options: v3.LogQBOptions{IsLivetailQuery: true},
|
||||
options: v3.QBOptions{IsLivetailQuery: true},
|
||||
},
|
||||
want: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body, attributes_string, attributes_number, attributes_bool, resources_string from " +
|
||||
"signoz_logs.distributed_logs_v2 where attributes_string['method'] = 'GET' AND mapContains(attributes_string, 'method') AND " +
|
||||
@@ -972,7 +972,7 @@ func TestPrepareLogsQuery(t *testing.T) {
|
||||
Expression: "A",
|
||||
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
|
||||
},
|
||||
options: v3.LogQBOptions{IsLivetailQuery: true},
|
||||
options: v3.QBOptions{IsLivetailQuery: true},
|
||||
},
|
||||
want: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body, attributes_string, attributes_number, attributes_bool, resources_string " +
|
||||
"from signoz_logs.distributed_logs_v2 where ",
|
||||
|
||||
@@ -114,12 +114,14 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
|
||||
|
||||
samplesTableFilter := fmt.Sprintf("metric_name = %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key), start, end)
|
||||
|
||||
tableName := helpers.WhichSamplesTableToUse(start, end, mq)
|
||||
|
||||
// Select the aggregate value for interval
|
||||
queryTmpl :=
|
||||
"SELECT fingerprint, %s" +
|
||||
" toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL %d SECOND) as ts," +
|
||||
" %s as per_series_value" +
|
||||
" FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_V4_TABLENAME +
|
||||
" FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + tableName +
|
||||
" INNER JOIN" +
|
||||
" (%s) as filtered_time_series" +
|
||||
" USING fingerprint" +
|
||||
@@ -130,37 +132,30 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
|
||||
selectLabelsAny := helpers.SelectLabelsAny(mq.GroupBy)
|
||||
selectLabels := helpers.SelectLabels(mq.GroupBy)
|
||||
|
||||
op := helpers.AggregationColumnForSamplesTable(start, end, mq)
|
||||
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAvg:
|
||||
op := "avg(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationSum:
|
||||
op := "sum(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationMin:
|
||||
op := "min(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationMax:
|
||||
op := "max(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationCount:
|
||||
op := "count(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationCountDistinct:
|
||||
op := "count(distinct(value))"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationAnyLast:
|
||||
op := "anyLast(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationRate:
|
||||
op := "max(value)"
|
||||
innerSubQuery := fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
rateQueryTmpl :=
|
||||
"SELECT %s ts, " + rateWithoutNegative +
|
||||
" as per_series_value FROM (%s) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)"
|
||||
subQuery = fmt.Sprintf(rateQueryTmpl, selectLabels, innerSubQuery)
|
||||
case v3.TimeAggregationIncrease:
|
||||
op := "max(value)"
|
||||
innerSubQuery := fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
rateQueryTmpl :=
|
||||
"SELECT %s ts, " + increaseWithoutNegative +
|
||||
|
||||
@@ -26,12 +26,14 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
|
||||
|
||||
samplesTableFilter := fmt.Sprintf("metric_name = %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key), start, end)
|
||||
|
||||
tableName := helpers.WhichSamplesTableToUse(start, end, mq)
|
||||
|
||||
// Select the aggregate value for interval
|
||||
queryTmpl :=
|
||||
"SELECT fingerprint, %s" +
|
||||
" toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL %d SECOND) as ts," +
|
||||
" %s as per_series_value" +
|
||||
" FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_V4_TABLENAME +
|
||||
" FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + tableName +
|
||||
" INNER JOIN" +
|
||||
" (%s) as filtered_time_series" +
|
||||
" USING fingerprint" +
|
||||
@@ -41,33 +43,27 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
|
||||
|
||||
selectLabelsAny := helpers.SelectLabelsAny(mq.GroupBy)
|
||||
|
||||
op := helpers.AggregationColumnForSamplesTable(start, end, mq)
|
||||
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAvg:
|
||||
op := "avg(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationSum:
|
||||
op := "sum(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationMin:
|
||||
op := "min(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationMax:
|
||||
op := "max(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationCount:
|
||||
op := "count(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationCountDistinct:
|
||||
op := "count(distinct(value))"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationAnyLast:
|
||||
op := "anyLast(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationRate:
|
||||
op := fmt.Sprintf("sum(value)/%d", step)
|
||||
op := fmt.Sprintf("%s/%d", op, step)
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
case v3.TimeAggregationIncrease:
|
||||
op := "sum(value)"
|
||||
subQuery = fmt.Sprintf(queryTmpl, selectLabelsAny, step, op, timeSeriesSubQuery)
|
||||
}
|
||||
return subQuery, nil
|
||||
@@ -89,10 +85,8 @@ func prepareQueryOptimized(start, end, step int64, mq *v3.BuilderQuery) (string,
|
||||
|
||||
samplesTableFilter := fmt.Sprintf("metric_name = %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key), start, end)
|
||||
|
||||
var tableName string = constants.SIGNOZ_SAMPLES_V4_TABLENAME
|
||||
if mq.AggregateAttribute.Type == v3.AttributeKeyType(v3.MetricTypeExponentialHistogram) {
|
||||
tableName = "distributed_exp_hist"
|
||||
}
|
||||
tableName := helpers.WhichSamplesTableToUse(start, end, mq)
|
||||
|
||||
// Select the aggregate value for interval
|
||||
queryTmpl :=
|
||||
"SELECT %s" +
|
||||
@@ -108,16 +102,16 @@ func prepareQueryOptimized(start, end, step int64, mq *v3.BuilderQuery) (string,
|
||||
|
||||
switch mq.SpaceAggregation {
|
||||
case v3.SpaceAggregationSum:
|
||||
op := "sum(value)"
|
||||
op := helpers.AggregationColumnForSamplesTable(start, end, mq)
|
||||
if mq.TimeAggregation == v3.TimeAggregationRate {
|
||||
op = "sum(value)/" + fmt.Sprintf("%d", step)
|
||||
op = fmt.Sprintf("%s/%d", op, step)
|
||||
}
|
||||
query = fmt.Sprintf(queryTmpl, selectLabels, step, op, timeSeriesSubQuery, groupBy, orderBy)
|
||||
case v3.SpaceAggregationMin:
|
||||
op := "min(value)"
|
||||
op := helpers.AggregationColumnForSamplesTable(start, end, mq)
|
||||
query = fmt.Sprintf(queryTmpl, selectLabels, step, op, timeSeriesSubQuery, groupBy, orderBy)
|
||||
case v3.SpaceAggregationMax:
|
||||
op := "max(value)"
|
||||
op := helpers.AggregationColumnForSamplesTable(start, end, mq)
|
||||
query = fmt.Sprintf(queryTmpl, selectLabels, step, op, timeSeriesSubQuery, groupBy, orderBy)
|
||||
case v3.SpaceAggregationPercentile50,
|
||||
v3.SpaceAggregationPercentile75,
|
||||
|
||||
@@ -13,31 +13,248 @@ import (
|
||||
var (
|
||||
sixHoursInMilliseconds = time.Hour.Milliseconds() * 6
|
||||
oneDayInMilliseconds = time.Hour.Milliseconds() * 24
|
||||
oneWeekInMilliseconds = oneDayInMilliseconds * 7
|
||||
)
|
||||
|
||||
// start and end are in milliseconds
|
||||
func which(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 we need to use the `time_series_v4_1day` 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
|
||||
// else we need to use the `time_series_v4_1week` table
|
||||
var tableName string
|
||||
if end-start <= sixHoursInMilliseconds {
|
||||
if end-start < sixHoursInMilliseconds {
|
||||
// adjust the start time to nearest 1 hour
|
||||
start = start - (start % (time.Hour.Milliseconds() * 1))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_LOCAL_TABLENAME
|
||||
} else if end-start <= oneDayInMilliseconds {
|
||||
} else if end-start < oneDayInMilliseconds {
|
||||
// adjust the start time to nearest 6 hours
|
||||
start = start - (start % (time.Hour.Milliseconds() * 6))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_6HRS_LOCAL_TABLENAME
|
||||
} else {
|
||||
} else if end-start < oneWeekInMilliseconds {
|
||||
// adjust the start time to nearest 1 day
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
|
||||
} else {
|
||||
if constants.UseMetricsPreAggregation() {
|
||||
// adjust the start time to nearest 1 week
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
|
||||
} else {
|
||||
// continue to use the 1 day table
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
|
||||
}
|
||||
}
|
||||
|
||||
return start, end, tableName
|
||||
}
|
||||
|
||||
// start and end are in milliseconds
|
||||
// we have three tables for samples
|
||||
// 1. distributed_samples_v4
|
||||
// 2. distributed_samples_v4_agg_5m - for queries with time range above or equal to 1 day and less than 1 week
|
||||
// 3. distributed_samples_v4_agg_30m - for queries with time range above or equal to 1 week
|
||||
// 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
|
||||
}
|
||||
|
||||
// continue to use the old table if pre-aggregation is disabled
|
||||
if !constants.UseMetricsPreAggregation() {
|
||||
return constants.SIGNOZ_SAMPLES_V4_TABLENAME
|
||||
}
|
||||
|
||||
// if the time aggregation is count_distinct, we need to use the distributed_samples_v4 table
|
||||
// because the aggregated tables don't support count_distinct
|
||||
if mq.TimeAggregation == v3.TimeAggregationCountDistinct {
|
||||
return constants.SIGNOZ_SAMPLES_V4_TABLENAME
|
||||
}
|
||||
|
||||
if end-start < oneDayInMilliseconds {
|
||||
// if we are dealing with delta metrics and interval is greater than 5 minutes, we can use the 5m aggregated table
|
||||
// why would interval be greater than 5 minutes?
|
||||
// we allow people to configure the step interval so we can make use of this
|
||||
if mq.Temporality == v3.Delta && mq.TimeAggregation == v3.TimeAggregationIncrease && mq.StepInterval >= 300 && mq.StepInterval < 1800 {
|
||||
return constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME
|
||||
} else if mq.Temporality == v3.Delta && mq.TimeAggregation == v3.TimeAggregationIncrease && mq.StepInterval >= 1800 {
|
||||
// if we are dealing with delta metrics and interval is greater than 30 minutes, we can use the 30m aggregated table
|
||||
return constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
|
||||
}
|
||||
return constants.SIGNOZ_SAMPLES_V4_TABLENAME
|
||||
} else if end-start < oneWeekInMilliseconds {
|
||||
return constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME
|
||||
} else {
|
||||
return constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME
|
||||
}
|
||||
}
|
||||
|
||||
func AggregationColumnForSamplesTable(start, end int64, mq *v3.BuilderQuery) string {
|
||||
tableName := WhichSamplesTableToUse(start, end, mq)
|
||||
var aggregationColumn string
|
||||
switch mq.Temporality {
|
||||
case v3.Delta:
|
||||
// for delta metrics, we only support `RATE`/`INCREASE` both of which are sum
|
||||
// although it doesn't make sense to use anyLast, avg, min, max, count on delta metrics,
|
||||
// we are keeping it here to make sure that query will not be invalid
|
||||
switch tableName {
|
||||
case constants.SIGNOZ_SAMPLES_V4_TABLENAME:
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAnyLast:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
case v3.TimeAggregationSum:
|
||||
aggregationColumn = "sum(value)"
|
||||
case v3.TimeAggregationAvg:
|
||||
aggregationColumn = "avg(value)"
|
||||
case v3.TimeAggregationMin:
|
||||
aggregationColumn = "min(value)"
|
||||
case v3.TimeAggregationMax:
|
||||
aggregationColumn = "max(value)"
|
||||
case v3.TimeAggregationCount:
|
||||
aggregationColumn = "count(value)"
|
||||
case v3.TimeAggregationCountDistinct:
|
||||
aggregationColumn = "countDistinct(value)"
|
||||
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
|
||||
aggregationColumn = "sum(value)"
|
||||
}
|
||||
case constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME, constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME:
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAnyLast:
|
||||
aggregationColumn = "anyLast(last)"
|
||||
case v3.TimeAggregationSum:
|
||||
aggregationColumn = "sum(sum)"
|
||||
case v3.TimeAggregationAvg:
|
||||
aggregationColumn = "sum(sum) / sum(count)"
|
||||
case v3.TimeAggregationMin:
|
||||
aggregationColumn = "min(min)"
|
||||
case v3.TimeAggregationMax:
|
||||
aggregationColumn = "max(max)"
|
||||
case v3.TimeAggregationCount:
|
||||
aggregationColumn = "sum(count)"
|
||||
// count_distinct is not supported in aggregated tables
|
||||
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
|
||||
aggregationColumn = "sum(sum)"
|
||||
}
|
||||
}
|
||||
case v3.Cumulative:
|
||||
// for cumulative metrics, we only support `RATE`/`INCREASE`. The max value in window is
|
||||
// used to calculate the sum which is then divided by the window size to get the rate
|
||||
switch tableName {
|
||||
case constants.SIGNOZ_SAMPLES_V4_TABLENAME:
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAnyLast:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
case v3.TimeAggregationSum:
|
||||
aggregationColumn = "sum(value)"
|
||||
case v3.TimeAggregationAvg:
|
||||
aggregationColumn = "avg(value)"
|
||||
case v3.TimeAggregationMin:
|
||||
aggregationColumn = "min(value)"
|
||||
case v3.TimeAggregationMax:
|
||||
aggregationColumn = "max(value)"
|
||||
case v3.TimeAggregationCount:
|
||||
aggregationColumn = "count(value)"
|
||||
case v3.TimeAggregationCountDistinct:
|
||||
aggregationColumn = "countDistinct(value)"
|
||||
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
|
||||
aggregationColumn = "max(value)"
|
||||
}
|
||||
case constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME, constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME:
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAnyLast:
|
||||
aggregationColumn = "anyLast(last)"
|
||||
case v3.TimeAggregationSum:
|
||||
aggregationColumn = "sum(sum)"
|
||||
case v3.TimeAggregationAvg:
|
||||
aggregationColumn = "sum(sum) / sum(count)"
|
||||
case v3.TimeAggregationMin:
|
||||
aggregationColumn = "min(min)"
|
||||
case v3.TimeAggregationMax:
|
||||
aggregationColumn = "max(max)"
|
||||
case v3.TimeAggregationCount:
|
||||
aggregationColumn = "sum(count)"
|
||||
// count_distinct is not supported in aggregated tables
|
||||
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
|
||||
aggregationColumn = "max(max)"
|
||||
}
|
||||
}
|
||||
case v3.Unspecified:
|
||||
switch tableName {
|
||||
case constants.SIGNOZ_SAMPLES_V4_TABLENAME:
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAnyLast:
|
||||
aggregationColumn = "anyLast(value)"
|
||||
case v3.TimeAggregationSum:
|
||||
aggregationColumn = "sum(value)"
|
||||
case v3.TimeAggregationAvg:
|
||||
aggregationColumn = "avg(value)"
|
||||
case v3.TimeAggregationMin:
|
||||
aggregationColumn = "min(value)"
|
||||
case v3.TimeAggregationMax:
|
||||
aggregationColumn = "max(value)"
|
||||
case v3.TimeAggregationCount:
|
||||
aggregationColumn = "count(value)"
|
||||
case v3.TimeAggregationCountDistinct:
|
||||
aggregationColumn = "countDistinct(value)"
|
||||
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // ideally, this should never happen
|
||||
aggregationColumn = "sum(value)"
|
||||
}
|
||||
case constants.SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME, constants.SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME:
|
||||
switch mq.TimeAggregation {
|
||||
case v3.TimeAggregationAnyLast:
|
||||
aggregationColumn = "anyLast(last)"
|
||||
case v3.TimeAggregationSum:
|
||||
aggregationColumn = "sum(sum)"
|
||||
case v3.TimeAggregationAvg:
|
||||
aggregationColumn = "sum(sum) / sum(count)"
|
||||
case v3.TimeAggregationMin:
|
||||
aggregationColumn = "min(min)"
|
||||
case v3.TimeAggregationMax:
|
||||
aggregationColumn = "max(max)"
|
||||
case v3.TimeAggregationCount:
|
||||
aggregationColumn = "sum(count)"
|
||||
// count_distinct is not supported in aggregated tables
|
||||
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // ideally, this should never happen
|
||||
aggregationColumn = "sum(sum)"
|
||||
}
|
||||
}
|
||||
}
|
||||
return aggregationColumn
|
||||
}
|
||||
|
||||
// PrepareTimeseriesFilterQuery builds the sub-query to be used for filtering timeseries based on the search criteria
|
||||
func PrepareTimeseriesFilterQuery(start, end int64, mq *v3.BuilderQuery) (string, error) {
|
||||
var conditions []string
|
||||
@@ -47,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 := which(start, end)
|
||||
start, end, tableName := whichTSTableToUse(start, end, mq)
|
||||
|
||||
conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", start, end))
|
||||
|
||||
@@ -127,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 := which(start, end)
|
||||
start, end, tableName := whichTSTableToUse(start, end, mq)
|
||||
|
||||
conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", start, end))
|
||||
|
||||
|
||||
402
pkg/query-service/app/metrics/v4/query_builder_pre_agg_test.go
Normal file
402
pkg/query-service/app/metrics/v4/query_builder_pre_agg_test.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package v4
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metricsV3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
func TestPrepareMetricQueryCumulativeRatePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
expectedQueryContains string
|
||||
}{
|
||||
{
|
||||
name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_calls_total",
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_name",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorContains,
|
||||
Value: "frontend",
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{{
|
||||
Key: "service_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
}},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, ts, sum(per_series_value) as value FROM (SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(max) as per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_calls_total' AND temporality = 'Cumulative' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991920000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative, multiple group by",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_calls_total",
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "service_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
{
|
||||
Key: "endpoint",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, endpoint, ts, sum(per_series_value) as value FROM (SELECT service_name, endpoint, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(endpoint) as endpoint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(max) as per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'endpoint') as endpoint, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_calls_total' AND temporality = 'Cumulative' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991920000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, endpoint, ts ORDER BY service_name ASC, endpoint ASC, ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryDeltaRatePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
expectedQueryContains string
|
||||
}{
|
||||
{
|
||||
name: "test time aggregation = rate, space aggregation = sum, temporality = delta, no group by",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_calls_total",
|
||||
},
|
||||
Temporality: v3.Delta,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
},
|
||||
expectedQueryContains: "SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(sum)/60 as value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_calls_total' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test time aggregation = rate, space aggregation = sum, temporality = delta, group by service_name",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_calls_total",
|
||||
},
|
||||
Temporality: v3.Delta,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{{
|
||||
Key: "service_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
}},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(sum)/60 as value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_calls_total' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryCumulativeQuantilePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
expectedQueryContains string
|
||||
}{
|
||||
{
|
||||
name: "test temporality = cumulative, quantile = 0.99",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_latency_bucket",
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_name",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorContains,
|
||||
Value: "frontend",
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{{
|
||||
Key: "service_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
}},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, ts, sum(per_series_value) as value FROM (SELECT service_name, le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(max) as per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Cumulative' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test temporality = cumulative, quantile = 0.99 without group by",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_latency_bucket",
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_name",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorContains,
|
||||
Value: "frontend",
|
||||
},
|
||||
},
|
||||
},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, ts, sum(per_series_value) as value FROM (SELECT le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(max) as per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Cumulative' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryDeltaQuantilePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
expectedQueryContains string
|
||||
}{
|
||||
{
|
||||
name: "test temporality = delta, quantile = 0.99 group by service_name",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_latency_bucket",
|
||||
},
|
||||
Temporality: v3.Delta,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_name",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorContains,
|
||||
Value: "frontend",
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{{
|
||||
Key: "service_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
}},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(sum)/60 as value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test temporality = delta, quantile = 0.99 no group by",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "signoz_latency_bucket",
|
||||
},
|
||||
Temporality: v3.Delta,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "service_name",
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
},
|
||||
Operator: v3.FilterOperatorContains,
|
||||
Value: "frontend",
|
||||
},
|
||||
},
|
||||
},
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(sum)/60 as value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryGaugePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
expectedQueryContains string
|
||||
}{
|
||||
{
|
||||
name: "test gauge query with no group by",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_usage",
|
||||
},
|
||||
Temporality: v3.Unspecified,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
Expression: "A",
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: false,
|
||||
},
|
||||
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(sum) / sum(count) as per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'system_cpu_usage' AND temporality = 'Unspecified' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'system_cpu_usage' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test gauge query with group by host_name",
|
||||
builderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_usage",
|
||||
},
|
||||
Temporality: v3.Unspecified,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
}},
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
},
|
||||
expectedQueryContains: "SELECT host_name, ts, sum(per_series_value) as value FROM (SELECT fingerprint, any(host_name) as host_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(sum) / sum(count) as per_series_value FROM signoz_metrics.distributed_samples_v4_agg_5m INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'host_name') as host_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'system_cpu_usage' AND temporality = 'Unspecified' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'system_cpu_usage' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY host_name, ts ORDER BY host_name ASC, ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,7 @@ func TestPrepareTimeseriesFilterQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryCumulativeRate(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -242,6 +243,7 @@ func TestPrepareMetricQueryCumulativeRate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryDeltaRate(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -266,7 +268,7 @@ func TestPrepareMetricQueryDeltaRate(t *testing.T) {
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
},
|
||||
expectedQueryContains: "SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'signoz_calls_total' AND temporality = 'Delta' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY ts ORDER BY ts ASC",
|
||||
expectedQueryContains: "SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_calls_total' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test time aggregation = rate, space aggregation = sum, temporality = delta, group by service_name",
|
||||
@@ -292,12 +294,14 @@ func TestPrepareMetricQueryDeltaRate(t *testing.T) {
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'signoz_calls_total' AND temporality = 'Delta' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_calls_total' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_calls_total' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
@@ -306,6 +310,7 @@ func TestPrepareMetricQueryDeltaRate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -344,7 +349,7 @@ func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, ts, sum(per_series_value) as value FROM (SELECT service_name, le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Cumulative' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, ts, sum(per_series_value) as value FROM (SELECT service_name, le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Cumulative' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test temporality = cumulative, quantile = 0.99 without group by",
|
||||
@@ -374,12 +379,14 @@ func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, ts, sum(per_series_value) as value FROM (SELECT le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Cumulative' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
|
||||
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, ts, sum(per_series_value) as value FROM (SELECT le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Cumulative' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
@@ -388,6 +395,7 @@ func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -426,7 +434,7 @@ func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test temporality = delta, quantile = 0.99 no group by",
|
||||
@@ -456,12 +464,14 @@ func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
|
||||
Disabled: false,
|
||||
SpaceAggregation: v3.SpaceAggregationPercentile99,
|
||||
},
|
||||
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
|
||||
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
@@ -470,6 +480,7 @@ func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryGauge(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -494,7 +505,7 @@ func TestPrepareMetricQueryGauge(t *testing.T) {
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: false,
|
||||
},
|
||||
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'system_cpu_usage' AND temporality = 'Unspecified' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'system_cpu_usage' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
|
||||
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'system_cpu_usage' AND temporality = 'Unspecified' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'system_cpu_usage' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
|
||||
},
|
||||
{
|
||||
name: "test gauge query with group by host_name",
|
||||
@@ -520,12 +531,14 @@ func TestPrepareMetricQueryGauge(t *testing.T) {
|
||||
Expression: "A",
|
||||
Disabled: false,
|
||||
},
|
||||
expectedQueryContains: "SELECT host_name, ts, sum(per_series_value) as value FROM (SELECT fingerprint, any(host_name) as host_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'host_name') as host_name, fingerprint FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name = 'system_cpu_usage' AND temporality = 'Unspecified' AND unix_milli >= 1650974400000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'system_cpu_usage' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY host_name, ts ORDER BY host_name ASC, ts ASC",
|
||||
expectedQueryContains: "SELECT host_name, ts, sum(per_series_value) as value FROM (SELECT fingerprint, any(host_name) as host_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'host_name') as host_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name = 'system_cpu_usage' AND temporality = 'Unspecified' AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name = 'system_cpu_usage' AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY host_name, ts ORDER BY host_name ASC, ts ASC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// 1650991982000 - April 26, 2022 10:23:02 PM
|
||||
// 1651078382000 - April 27, 2022 10:23:02 PM
|
||||
query, err := PrepareMetricQuery(1650991982000, 1651078382000, v3.QueryTypeBuilder, v3.PanelTypeGraph, testCase.builderQuery, metricsV3.Options{})
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, query, testCase.expectedQueryContains)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
@@ -724,6 +725,42 @@ func parseInviteRequest(r *http.Request) (*model.InviteRequest, error) {
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func isValidRole(role string) bool {
|
||||
switch role {
|
||||
case constants.AdminGroup, constants.EditorGroup, constants.ViewerGroup:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseInviteUsersRequest(r *http.Request) (*model.BulkInviteRequest, error) {
|
||||
var req model.BulkInviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate that the request contains users
|
||||
if len(req.Users) == 0 {
|
||||
return nil, fmt.Errorf("no users provided for invitation")
|
||||
}
|
||||
|
||||
// Trim spaces and validate each user
|
||||
for i := range req.Users {
|
||||
req.Users[i].Email = strings.TrimSpace(req.Users[i].Email)
|
||||
if req.Users[i].Email == "" {
|
||||
return nil, fmt.Errorf("email is required for each user")
|
||||
}
|
||||
if req.Users[i].FrontendBaseUrl == "" {
|
||||
return nil, fmt.Errorf("frontendBaseUrl is required for each user")
|
||||
}
|
||||
if !isValidRole(req.Users[i].Role) {
|
||||
return nil, fmt.Errorf("invalid role for user: %s", req.Users[i].Email)
|
||||
}
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseSetApdexScoreRequest(r *http.Request) (*model.ApdexSettings, error) {
|
||||
var req model.ApdexSettings
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -1094,25 +1131,18 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
|
||||
query.StepInterval = minStep
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if query.DataSource == v3.DataSourceMetrics && baseconstants.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
|
||||
start, end := queryRangeParams.Start, queryRangeParams.End
|
||||
if end-start >= 24*time.Hour.Milliseconds() && end-start < 7*24*time.Hour.Milliseconds() {
|
||||
query.StepInterval = int64(math.Round(float64(query.StepInterval)/300)) * 300
|
||||
} else if end-start >= 7*24*time.Hour.Milliseconds() {
|
||||
query.StepInterval = int64(math.Round(float64(query.StepInterval)/1800)) * 1800
|
||||
}
|
||||
}
|
||||
query.ShiftBy = timeShiftBy
|
||||
|
||||
query.SetShiftByFromFunc()
|
||||
|
||||
if query.Filters == nil || len(query.Filters.Items) == 0 {
|
||||
continue
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1416,12 +1471,6 @@ func TestParseQueryRangeParamsStepIntervalAdjustment(t *testing.T) {
|
||||
end: time.Now().UnixMilli(),
|
||||
step: 1, // gets updated
|
||||
},
|
||||
{
|
||||
desc: "1 week and 1 minute step",
|
||||
start: time.Now().Add(-7 * 24 * time.Hour).UnixMilli(),
|
||||
end: time.Now().UnixMilli(),
|
||||
step: 60, // gets updated
|
||||
},
|
||||
{
|
||||
desc: "1 day and 1 hour step",
|
||||
start: time.Now().Add(-24 * time.Hour).UnixMilli(),
|
||||
@@ -1446,12 +1495,6 @@ func TestParseQueryRangeParamsStepIntervalAdjustment(t *testing.T) {
|
||||
end: time.Now().UnixMilli(),
|
||||
step: 300, // no update
|
||||
},
|
||||
{
|
||||
desc: "1 week and 10 minutes step",
|
||||
start: time.Now().Add(-7 * 24 * time.Hour).UnixMilli(),
|
||||
end: time.Now().UnixMilli(),
|
||||
step: 600, // get updated
|
||||
},
|
||||
{
|
||||
desc: "1 week and 45 minutes step",
|
||||
start: time.Now().Add(-7 * 24 * time.Hour).UnixMilli(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user