diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index c1aa885b6a..fd42658745 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -2,6 +2,6 @@
# Owners are automatically requested for review for PRs that changes code
# that they own.
* @ankitnayan
-/frontend/ @palashgdev @pranshuchittora
+/frontend/ @palashgdev
/deploy/ @prashant-shahi
**/query-service/ @srikanthccv
diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml
index c655d69df2..6de51f4733 100644
--- a/.github/workflows/staging-deployment.yaml
+++ b/.github/workflows/staging-deployment.yaml
@@ -11,21 +11,23 @@ jobs:
environment: staging
steps:
- name: Executing remote ssh commands using ssh key
- uses: appleboy/ssh-action@v0.1.6
+ uses: appleboy/ssh-action@v0.1.8
env:
GITHUB_BRANCH: develop
GITHUB_SHA: ${{ github.sha }}
with:
host: ${{ secrets.HOST_DNS }}
username: ${{ secrets.USERNAME }}
- key: ${{ secrets.EC2_SSH_KEY }}
+ key: ${{ secrets.SSH_KEY }}
envs: GITHUB_BRANCH,GITHUB_SHA
command_timeout: 60m
script: |
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
+ export OTELCOL_TAG="main"
docker system prune --force
+ docker pull signoz/signoz-otel-collector:main
cd ~/signoz
git status
git add .
diff --git a/.github/workflows/testing-deployment.yaml b/.github/workflows/testing-deployment.yaml
index d122291d42..d65a4e8bbc 100644
--- a/.github/workflows/testing-deployment.yaml
+++ b/.github/workflows/testing-deployment.yaml
@@ -11,14 +11,14 @@ jobs:
if: ${{ github.event.label.name == 'testing-deploy' }}
steps:
- name: Executing remote ssh commands using ssh key
- uses: appleboy/ssh-action@v0.1.6
+ uses: appleboy/ssh-action@v0.1.8
env:
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
with:
host: ${{ secrets.HOST_DNS }}
username: ${{ secrets.USERNAME }}
- key: ${{ secrets.EC2_SSH_KEY }}
+ key: ${{ secrets.SSH_KEY }}
envs: GITHUB_BRANCH,GITHUB_SHA
command_timeout: 60m
script: |
diff --git a/README.de-de.md b/README.de-de.md
index 55dd7f4c22..6587756b9b 100644
--- a/README.de-de.md
+++ b/README.de-de.md
@@ -85,9 +85,9 @@ Hier findest du die vollständige Liste von unterstützten Programmiersprachen -
### Bereitstellung mit Docker
-Bitte folge den [hier](https://signoz.io/docs/deployment/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
+Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
-Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/deployment/troubleshooting) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
+Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
 
diff --git a/README.md b/README.md
index 70779f3de5..4920ca9a26 100644
--- a/README.md
+++ b/README.md
@@ -130,9 +130,9 @@ You can find the complete list of languages here - https://opentelemetry.io/docs
### Deploy using Docker
-Please follow the steps listed [here](https://signoz.io/docs/deployment/docker/) to install using docker
+Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
-The [troubleshooting instructions](https://signoz.io/docs/deployment/troubleshooting) may be helpful if you face any issues.
+The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
 
diff --git a/README.pt-br.md b/README.pt-br.md
index ce168b4101..c817e8afb9 100644
--- a/README.pt-br.md
+++ b/README.pt-br.md
@@ -84,9 +84,9 @@ Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry
### Implantar usando Docker
-Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/docker/) para instalar usando o Docker.
+Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o Docker.
-Esse [guia para solução de problemas](https://signoz.io/docs/deployment/troubleshooting) pode ser útil se você enfrentar quaisquer problemas.
+Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.
 
diff --git a/README.zh-cn.md b/README.zh-cn.md
index 3658eeb520..aaa89551bf 100644
--- a/README.zh-cn.md
+++ b/README.zh-cn.md
@@ -80,9 +80,9 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo
### 使用Docker部署
-请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装
+请按照[这里](https://signoz.io/docs/install/docker/)列出的步骤使用Docker来安装
-如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。
+如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助。
 
diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml
index a0c2ba8105..42dbd5afc6 100644
--- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml
+++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml
@@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
- image: signoz/query-service:0.17.0
+ image: signoz/query-service:0.18.1
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
- image: signoz/frontend:0.17.0
+ image: signoz/frontend:0.18.1
deploy:
restart_policy:
condition: on-failure
@@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
- image: signoz/signoz-otel-collector:0.66.6
+ image: signoz/signoz-otel-collector:0.66.7
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -208,7 +208,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
- image: signoz/signoz-otel-collector:0.66.6
+ image: signoz/signoz-otel-collector:0.66.7
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml
index e7c9360879..80bd4a9890 100644
--- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml
+++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml
@@ -41,7 +41,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: otel-collector
- image: signoz/signoz-otel-collector:0.66.6
+ image: signoz/signoz-otel-collector:0.66.7
command: ["--config=/etc/otel-collector-config.yaml"]
# user: root # required for reading docker container logs
volumes:
@@ -67,7 +67,7 @@ services:
otel-collector-metrics:
container_name: otel-collector-metrics
- image: signoz/signoz-otel-collector:0.66.6
+ image: signoz/signoz-otel-collector:0.66.7
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml
index 4906ba4c10..9a967cc800 100644
--- a/deploy/docker/clickhouse-setup/docker-compose.yaml
+++ b/deploy/docker/clickhouse-setup/docker-compose.yaml
@@ -153,7 +153,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
- image: signoz/query-service:${DOCKER_TAG:-0.17.0}
+ image: signoz/query-service:${DOCKER_TAG:-0.18.1}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
- image: signoz/frontend:${DOCKER_TAG:-0.17.0}
+ image: signoz/frontend:${DOCKER_TAG:-0.18.1}
container_name: frontend
restart: on-failure
depends_on:
@@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
- image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.6}
+ image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7}
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -219,7 +219,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
- image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.6}
+ image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7}
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
diff --git a/deploy/install.sh b/deploy/install.sh
index e8a14a5821..e908dd8952 100755
--- a/deploy/install.sh
+++ b/deploy/install.sh
@@ -125,7 +125,7 @@ check_ports_occupied() {
echo "+++++++++++ ERROR ++++++++++++++++++++++"
echo "SigNoz requires ports 3301 & 4317 to be open. Please shut down any other service(s) that may be running on these ports."
- echo "You can run SigNoz on another port following this guide https://signoz.io/docs/deployment/docker#troubleshooting"
+ echo "You can run SigNoz on another port following this guide https://signoz.io/docs/install/troubleshooting/"
echo "++++++++++++++++++++++++++++++++++++++++"
echo ""
exit 1
@@ -249,7 +249,7 @@ bye() { # Prints a friendly good bye message and exits the script.
echo ""
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
- # echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker#troubleshooting"
+ echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
echo "++++++++++++++++++++++++++++++++++++++++"
@@ -500,7 +500,7 @@ if [[ $status_code -ne 200 ]]; then
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
- echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker/#troubleshooting-of-common-issues"
+ echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us on SigNoz for support https://signoz.io/slack"
echo "++++++++++++++++++++++++++++++++++++++++"
diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts
index b3b8061422..c9441402d9 100644
--- a/frontend/jest.setup.ts
+++ b/frontend/jest.setup.ts
@@ -1,5 +1,20 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable object-shorthand */
+/* eslint-disable func-names */
+
/**
* Adds custom matchers from the react testing library to all tests
*/
import '@testing-library/jest-dom';
import 'jest-styled-components';
+
+// Mock window.matchMedia
+window.matchMedia =
+ window.matchMedia ||
+ function (): any {
+ return {
+ matches: false,
+ addListener: function () {},
+ removeListener: function () {},
+ };
+ };
diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx
index 832e557e49..edfe843882 100644
--- a/frontend/src/AppRoutes/index.tsx
+++ b/frontend/src/AppRoutes/index.tsx
@@ -4,6 +4,7 @@ import Spinner from 'components/Spinner';
import AppLayout from 'container/AppLayout';
import { useThemeConfig } from 'hooks/useDarkMode';
import { NotificationProvider } from 'hooks/useNotifications';
+import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import React, { Suspense } from 'react';
@@ -17,30 +18,32 @@ function App(): JSX.Element {
return (
-
-
+
+
-
-
- }>
-
- {routes.map(({ path, component, exact }) => (
-
- ))}
+
+
+
+ }>
+
+ {routes.map(({ path, component, exact }) => (
+
+ ))}
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
);
}
diff --git a/frontend/src/api/apiV1.ts b/frontend/src/api/apiV1.ts
index 5145443b2a..2e7df02395 100644
--- a/frontend/src/api/apiV1.ts
+++ b/frontend/src/api/apiV1.ts
@@ -1,6 +1,7 @@
const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/';
+export const apiV3 = '/api/v3/';
export const apiAlertManager = '/api/alertmanager';
export default apiV1;
diff --git a/frontend/src/api/errors/getAll.ts b/frontend/src/api/errors/getAll.ts
index 7014e52a56..8d6793ee87 100644
--- a/frontend/src/api/errors/getAll.ts
+++ b/frontend/src/api/errors/getAll.ts
@@ -1,7 +1,6 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
-import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getAll';
@@ -9,11 +8,17 @@ const getAll = async (
props: Props,
): Promise | ErrorResponse> => {
try {
- const response = await axios.get(
- `/listErrors?${createQueryParams({
- ...props,
- })}`,
- );
+ const response = await axios.post(`/listErrors`, {
+ start: `${props.start}`,
+ end: `${props.end}`,
+ order: props.order,
+ orderParam: props.orderParam,
+ limit: props.limit,
+ offset: props.offset,
+ exceptionType: props.exceptionType,
+ serviceName: props.serviceName,
+ tags: props.tags,
+ });
return {
statusCode: 200,
diff --git a/frontend/src/api/errors/getErrorCounts.ts b/frontend/src/api/errors/getErrorCounts.ts
index 4992a6d391..977eeb226f 100644
--- a/frontend/src/api/errors/getErrorCounts.ts
+++ b/frontend/src/api/errors/getErrorCounts.ts
@@ -1,7 +1,6 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
-import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
@@ -9,11 +8,13 @@ const getErrorCounts = async (
props: Props,
): Promise | ErrorResponse> => {
try {
- const response = await axios.get(
- `/countErrors?${createQueryParams({
- ...props,
- })}`,
- );
+ const response = await axios.post(`/countErrors`, {
+ start: `${props.start}`,
+ end: `${props.end}`,
+ exceptionType: props.exceptionType,
+ serviceName: props.serviceName,
+ tags: props.tags,
+ });
return {
statusCode: 200,
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
index 2a48730f04..584e3b4868 100644
--- a/frontend/src/api/index.ts
+++ b/frontend/src/api/index.ts
@@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
-import apiV1, { apiAlertManager, apiV2 } from './apiV1';
+import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
import { Logout } from './utils';
const interceptorsResponse = (
@@ -109,6 +109,17 @@ ApiV2Instance.interceptors.response.use(
);
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
+// axios V3
+export const ApiV3Instance = axios.create({
+ baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
+});
+ApiV3Instance.interceptors.response.use(
+ interceptorsResponse,
+ interceptorRejected,
+);
+ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
+//
+
AxiosAlertManagerInstance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
diff --git a/frontend/src/api/queryBuilder/getAggregateAttribute.ts b/frontend/src/api/queryBuilder/getAggregateAttribute.ts
new file mode 100644
index 0000000000..15e221d975
--- /dev/null
+++ b/frontend/src/api/queryBuilder/getAggregateAttribute.ts
@@ -0,0 +1,33 @@
+import { ApiV3Instance } from 'api';
+import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
+import { AxiosError, AxiosResponse } from 'axios';
+// ** Helpers
+import { ErrorResponse, SuccessResponse } from 'types/api';
+// ** Types
+import { IGetAggregateAttributePayload } from 'types/api/queryBuilder/getAggregatorAttribute';
+import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
+
+export const getAggregateAttribute = async ({
+ aggregateOperator,
+ searchText,
+ dataSource,
+}: IGetAggregateAttributePayload): Promise<
+ SuccessResponse | ErrorResponse
+> => {
+ try {
+ const response: AxiosResponse<{
+ data: IQueryAutocompleteResponse;
+ }> = await ApiV3Instance.get(
+ `autocomplete/aggregate_attributes?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&searchText=${searchText}`,
+ );
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.statusText,
+ payload: response.data.data,
+ };
+ } catch (e) {
+ return ErrorResponseHandler(e as AxiosError);
+ }
+};
diff --git a/frontend/src/api/queryBuilder/getAttributeKeys.ts b/frontend/src/api/queryBuilder/getAttributeKeys.ts
new file mode 100644
index 0000000000..439c20f132
--- /dev/null
+++ b/frontend/src/api/queryBuilder/getAttributeKeys.ts
@@ -0,0 +1,34 @@
+import { ApiV3Instance } from 'api';
+import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
+import { AxiosError, AxiosResponse } from 'axios';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+// ** Types
+import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
+import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
+
+export const getAggregateKeys = async ({
+ aggregateOperator,
+ searchText,
+ dataSource,
+ aggregateAttribute,
+ tagType,
+}: IGetAttributeKeysPayload): Promise<
+ SuccessResponse | ErrorResponse
+> => {
+ try {
+ const response: AxiosResponse<{
+ data: IQueryAutocompleteResponse;
+ }> = await ApiV3Instance.get(
+ `autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&tagType=${tagType}&searchText=${searchText}`,
+ );
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.statusText,
+ payload: response.data.data,
+ };
+ } catch (e) {
+ return ErrorResponseHandler(e as AxiosError);
+ }
+};
diff --git a/frontend/src/api/queryBuilder/getAttributesKeysValues.ts b/frontend/src/api/queryBuilder/getAttributesKeysValues.ts
new file mode 100644
index 0000000000..f5b938a345
--- /dev/null
+++ b/frontend/src/api/queryBuilder/getAttributesKeysValues.ts
@@ -0,0 +1,63 @@
+import { ApiV3Instance } from 'api';
+import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
+import { AxiosError } from 'axios';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+
+export type TagKeyValueProps = {
+ dataSource: string;
+ aggregateOperator?: string;
+ aggregateAttribute?: string;
+ searchText?: string;
+ attributeKey?: string;
+};
+
+export interface AttributeKeyOptions {
+ key: string;
+ type: string;
+ dataType: 'string' | 'boolean' | 'number';
+ isColumn: boolean;
+}
+
+export const getAttributesKeys = async (
+ props: TagKeyValueProps,
+): Promise | ErrorResponse> => {
+ try {
+ const response = await ApiV3Instance.get(
+ `/autocomplete/attribute_keys?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}`,
+ );
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.data.status,
+ payload: response.data.data.attributeKeys,
+ };
+ } catch (error) {
+ return ErrorResponseHandler(error as AxiosError);
+ }
+};
+
+export interface TagValuePayloadProps {
+ boolAttributeValues: null | string[];
+ numberAttributeValues: null | string[];
+ stringAttributeValues: null | string[];
+}
+
+export const getAttributesValues = async (
+ props: TagKeyValueProps,
+): Promise | ErrorResponse> => {
+ try {
+ const response = await ApiV3Instance.get(
+ `/autocomplete/attribute_values?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}&attributeKey=${props.attributeKey}`,
+ );
+
+ return {
+ statusCode: 200,
+ error: null,
+ message: response.data.status,
+ payload: response.data.data,
+ };
+ } catch (error) {
+ return ErrorResponseHandler(error as AxiosError);
+ }
+};
diff --git a/frontend/src/api/trace/getSpans.ts b/frontend/src/api/trace/getSpans.ts
index 8b56caa46d..261b2652c6 100644
--- a/frontend/src/api/trace/getSpans.ts
+++ b/frontend/src/api/trace/getSpans.ts
@@ -10,7 +10,7 @@ const getSpans = async (
): Promise | ErrorResponse> => {
try {
const updatedSelectedTags = props.selectedTags.map((e) => ({
- Key: e.Key,
+ Key: `${e.Key}.(string)`,
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
diff --git a/frontend/src/api/trace/getSpansAggregate.ts b/frontend/src/api/trace/getSpansAggregate.ts
index cfa1f7e31f..7f245605fc 100644
--- a/frontend/src/api/trace/getSpansAggregate.ts
+++ b/frontend/src/api/trace/getSpansAggregate.ts
@@ -28,7 +28,7 @@ const getSpanAggregate = async (
});
const updatedSelectedTags = props.selectedTags.map((e) => ({
- Key: e.Key,
+ Key: `${e.Key}.(string)`,
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
diff --git a/frontend/src/components/Logs/TableView/styles.ts b/frontend/src/components/Logs/TableView/styles.ts
index cd361b9c5e..951404afec 100644
--- a/frontend/src/components/Logs/TableView/styles.ts
+++ b/frontend/src/components/Logs/TableView/styles.ts
@@ -16,6 +16,4 @@ export const TableBodyContent = styled.div`
font-size: 0.875rem;
line-height: 2rem;
-
- cursor: pointer;
`;
diff --git a/frontend/src/components/TextToolTip/index.tsx b/frontend/src/components/TextToolTip/index.tsx
index 7e6c04c6f1..64a9cd053a 100644
--- a/frontend/src/components/TextToolTip/index.tsx
+++ b/frontend/src/components/TextToolTip/index.tsx
@@ -1,23 +1,40 @@
-/* eslint-disable react/no-unstable-nested-components */
+import { grey } from '@ant-design/colors';
import { QuestionCircleFilled } from '@ant-design/icons';
import { Tooltip } from 'antd';
-import React from 'react';
+import { themeColors } from 'constants/theme';
+import { useIsDarkMode } from 'hooks/useDarkMode';
+import React, { useMemo } from 'react';
+
+import { style } from './styles';
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
+ const isDarkMode = useIsDarkMode();
+
+ const overlay = useMemo(
+ () => (
+
+ {`${text} `}
+ {url && (
+
+ here
+
+ )}
+
+ ),
+ [text, url],
+ );
+
+ const iconStyle = useMemo(
+ () => ({
+ ...style,
+ color: isDarkMode ? themeColors.whiteCream : grey[0],
+ }),
+ [isDarkMode],
+ );
+
return (
- (
-
- {`${text} `}
- {url && (
-
- here
-
- )}
-
- )}
- >
-
+
+
);
}
diff --git a/frontend/src/components/TextToolTip/styles.ts b/frontend/src/components/TextToolTip/styles.ts
new file mode 100644
index 0000000000..bb2532182d
--- /dev/null
+++ b/frontend/src/components/TextToolTip/styles.ts
@@ -0,0 +1 @@
+export const style = { fontSize: '1.3125rem' };
diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts
index 29221e9f9c..35c1e2c2ca 100644
--- a/frontend/src/constants/query.ts
+++ b/frontend/src/constants/query.ts
@@ -1,5 +1,4 @@
-// eslint-disable-next-line @typescript-eslint/naming-convention
-export enum METRICS_PAGE_QUERY_PARAM {
+export enum QueryParams {
interval = 'interval',
startTime = 'startTime',
endTime = 'endTime',
@@ -12,4 +11,5 @@ export enum METRICS_PAGE_QUERY_PARAM {
selectedTags = 'selectedTags',
aggregationOption = 'aggregationOption',
entity = 'entity',
+ resourceAttributes = 'resourceAttribute',
}
diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts
new file mode 100644
index 0000000000..5c81778742
--- /dev/null
+++ b/frontend/src/constants/queryBuilder.ts
@@ -0,0 +1,177 @@
+// ** Helpers
+import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
+import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import {
+ Having,
+ IBuilderFormula,
+ IBuilderQueryForm,
+} from 'types/api/queryBuilder/queryBuilderData';
+import {
+ BoolOperators,
+ DataSource,
+ LogsAggregatorOperator,
+ MetricAggregateOperator,
+ NumberOperators,
+ StringOperators,
+ TracesAggregatorOperator,
+} from 'types/common/queryBuilder';
+
+export const MAX_FORMULAS = 20;
+export const MAX_QUERIES = 26;
+
+export const formulasNames: string[] = Array.from(
+ Array(MAX_FORMULAS),
+ (_, i) => `F${i + 1}`,
+);
+const alpha: number[] = Array.from(Array(MAX_QUERIES), (_, i) => i + 65);
+export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
+
+export enum QueryBuilderKeys {
+ GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
+ GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
+}
+
+export const mapOfOperators: Record = {
+ metrics: Object.values(MetricAggregateOperator),
+ logs: Object.values(LogsAggregatorOperator),
+ traces: Object.values(TracesAggregatorOperator),
+};
+
+export const mapOfFilters: Record = {
+ // eslint-disable-next-line sonarjs/no-duplicate-string
+ metrics: ['Aggregation interval', 'Having'],
+ logs: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
+ traces: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
+};
+
+export const initialHavingValues: Having = {
+ columnName: '',
+ op: '',
+ value: [],
+};
+
+export const initialAggregateAttribute: IBuilderQueryForm['aggregateAttribute'] = {
+ dataType: null,
+ key: '',
+ isColumn: null,
+ type: null,
+};
+
+export const initialQueryBuilderFormValues: IBuilderQueryForm = {
+ dataSource: DataSource.METRICS,
+ queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
+ aggregateOperator: Object.values(MetricAggregateOperator)[0],
+ aggregateAttribute: initialAggregateAttribute,
+ tagFilters: { items: [], op: 'AND' },
+ expression: '',
+ disabled: false,
+ having: [],
+ stepInterval: 30,
+ limit: 10,
+ orderBy: [],
+ groupBy: [],
+ legend: '',
+ reduceTo: '',
+};
+
+export const initialFormulaBuilderFormValues: IBuilderFormula = {
+ label: createNewBuilderItemName({
+ existNames: [],
+ sourceNames: formulasNames,
+ }),
+ expression: '',
+ disabled: false,
+ legend: '',
+};
+
+export const operatorsByTypes: Record = {
+ string: Object.values(StringOperators),
+ number: Object.values(NumberOperators),
+ bool: Object.values(BoolOperators),
+};
+
+export type IQueryBuilderState = 'search';
+
+export const QUERY_BUILDER_SEARCH_VALUES = {
+ MULTIPLY: 'MULTIPLY_VALUE',
+ SINGLE: 'SINGLE_VALUE',
+ NON: 'NON_VALUE',
+ NOT_VALID: 'NOT_VALID',
+};
+
+export const OPERATORS = {
+ IN: 'IN',
+ NIN: 'NOT_IN',
+ LIKE: 'LIKE',
+ NLIKE: 'NOT_LIKE',
+ EQUALS: '=',
+ NOT_EQUALS: '!=',
+ EXISTS: 'EXISTS',
+ NOT_EXISTS: 'NOT_EXISTS',
+ CONTAINS: 'CONTAINS',
+ NOT_CONTAINS: 'NOT_CONTAINS',
+ GTE: '>=',
+ GT: '>',
+ LTE: '<=',
+ LT: '<',
+};
+
+export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
+ string: [
+ OPERATORS.EQUALS,
+ OPERATORS.NOT_EQUALS,
+ OPERATORS.IN,
+ OPERATORS.NIN,
+ OPERATORS.LIKE,
+ OPERATORS.NLIKE,
+ OPERATORS.CONTAINS,
+ OPERATORS.NOT_CONTAINS,
+ OPERATORS.EXISTS,
+ OPERATORS.NOT_EXISTS,
+ ],
+ number: [
+ OPERATORS.EQUALS,
+ OPERATORS.NOT_EQUALS,
+ OPERATORS.IN,
+ OPERATORS.NIN,
+ OPERATORS.EXISTS,
+ OPERATORS.NOT_EXISTS,
+ OPERATORS.GTE,
+ OPERATORS.GT,
+ OPERATORS.LTE,
+ OPERATORS.LT,
+ ],
+ boolean: [
+ OPERATORS.EQUALS,
+ OPERATORS.NOT_EQUALS,
+ OPERATORS.EXISTS,
+ OPERATORS.NOT_EXISTS,
+ ],
+ universal: [
+ OPERATORS.EQUALS,
+ OPERATORS.NOT_EQUALS,
+ OPERATORS.IN,
+ OPERATORS.NIN,
+ OPERATORS.EXISTS,
+ OPERATORS.NOT_EXISTS,
+ OPERATORS.LIKE,
+ OPERATORS.NLIKE,
+ OPERATORS.GTE,
+ OPERATORS.GT,
+ OPERATORS.LTE,
+ OPERATORS.LT,
+ OPERATORS.CONTAINS,
+ OPERATORS.NOT_CONTAINS,
+ ],
+};
+
+export const HAVING_OPERATORS: string[] = [
+ OPERATORS.EQUALS,
+ OPERATORS.NOT_EQUALS,
+ OPERATORS.IN,
+ OPERATORS.NIN,
+ OPERATORS.GTE,
+ OPERATORS.GT,
+ OPERATORS.LTE,
+ OPERATORS.LT,
+];
diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx
index 64d83e70ec..c3b0580f44 100644
--- a/frontend/src/container/AllError/index.tsx
+++ b/frontend/src/container/AllError/index.tsx
@@ -18,6 +18,8 @@ import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
+import useResourceAttribute from 'hooks/useResourceAttribute';
+import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
@@ -93,9 +95,11 @@ function AllErrors(): JSX.Element {
],
);
+ const { queries } = useResourceAttribute();
+
const [{ isLoading, data }, errorCountResponse] = useQueries([
{
- queryKey: ['getAllErrors', updatedPath, maxTime, minTime],
+ queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
queryFn: (): Promise | ErrorResponse> =>
getAll({
end: maxTime,
@@ -106,6 +110,7 @@ function AllErrors(): JSX.Element {
orderParam: getUpdatedParams,
exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName,
+ tags: convertRawQueriesToTraceSelectedTags(queries),
}),
enabled: !loading,
},
@@ -116,6 +121,7 @@ function AllErrors(): JSX.Element {
minTime,
getUpdatedExceptionType,
getUpdatedServiceName,
+ queries,
],
queryFn: (): Promise> =>
getErrorCounts({
@@ -123,6 +129,7 @@ function AllErrors(): JSX.Element {
start: minTime,
exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName,
+ tags: convertRawQueriesToTraceSelectedTags(queries),
}),
enabled: !loading,
},
diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx
index a8d5c777c9..05ba17b8f6 100644
--- a/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx
+++ b/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx
@@ -1,23 +1,15 @@
import { Button, Row } from 'antd';
import React from 'react';
-import { QueryFields } from './utils';
-
interface SearchFieldsActionBarProps {
- fieldsQuery: QueryFields[][];
- applyUpdate: () => void;
- clearFilters: () => void;
+ applyUpdate: VoidFunction;
+ clearFilters: VoidFunction;
}
export function SearchFieldsActionBar({
- fieldsQuery,
applyUpdate,
clearFilters,
}: SearchFieldsActionBarProps): JSX.Element | null {
- if (fieldsQuery.length === 0) {
- return null;
- }
-
return (
void;
onDropDownToggleHandler: (value: boolean) => VoidFunction;
+ updateQueryString: (value: string) => void;
}
function SearchFields({
- updateParsedQuery,
onDropDownToggleHandler,
+ updateQueryString,
}: SearchFieldsProps): JSX.Element {
const {
searchFilter: { parsedQuery },
@@ -90,15 +91,15 @@ function SearchFields({
}
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
- updateParsedQuery(flatParsedQuery);
+ updateQueryString(reverseParser(flatParsedQuery));
onDropDownToggleHandler(false)();
- }, [onDropDownToggleHandler, fieldsQuery, updateParsedQuery, notifications]);
+ }, [fieldsQuery, notifications, onDropDownToggleHandler, updateQueryString]);
const clearFilters = useCallback((): void => {
keyPrefixRef.current = hashCode(JSON.stringify([]));
- updateParsedQuery([]);
- onDropDownToggleHandler(false)();
- }, [onDropDownToggleHandler, updateParsedQuery]);
+ setFieldsQuery([]);
+ updateQueryString('');
+ }, [updateQueryString]);
return (
<>
@@ -113,7 +114,6 @@ function SearchFields({
>
diff --git a/frontend/src/container/LogsSearchFilter/index.tsx b/frontend/src/container/LogsSearchFilter/index.tsx
index 75f95769f1..efe7a39534 100644
--- a/frontend/src/container/LogsSearchFilter/index.tsx
+++ b/frontend/src/container/LogsSearchFilter/index.tsx
@@ -36,11 +36,7 @@ function SearchFilter({
getLogsAggregate,
getLogsFields,
}: SearchFilterProps): JSX.Element {
- const {
- updateParsedQuery,
- updateQueryString,
- queryString,
- } = useSearchParser();
+ const { updateQueryString, queryString } = useSearchParser();
const [searchText, setSearchText] = useState(queryString);
const [showDropDown, setShowDropDown] = useState(false);
const searchRef = useRef(null);
@@ -187,8 +183,8 @@ function SearchFilter({
content={
}
diff --git a/frontend/src/container/LogsSearchFilter/useSearchParser.ts b/frontend/src/container/LogsSearchFilter/useSearchParser.ts
index 11b501400f..a3dab8a75c 100644
--- a/frontend/src/container/LogsSearchFilter/useSearchParser.ts
+++ b/frontend/src/container/LogsSearchFilter/useSearchParser.ts
@@ -1,8 +1,7 @@
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
-import { parseQuery, reverseParser } from 'lib/logql';
-import { ILogQLParsedQueryItem } from 'lib/logql/types';
+import { parseQuery } from 'lib/logql';
import isEqual from 'lodash-es/isEqual';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -21,7 +20,6 @@ import { getGlobalTime } from './utils';
export function useSearchParser(): {
queryString: string;
parsedQuery: unknown;
- updateParsedQuery: (arg0: ILogQLParsedQueryItem[]) => void;
updateQueryString: (arg0: string) => void;
} {
const dispatch = useDispatch>();
@@ -64,7 +62,7 @@ export function useSearchParser(): {
},
// need to hide this warning as we don't want to update the query string on every change
// eslint-disable-next-line react-hooks/exhaustive-deps
- [dispatch, parsedQuery],
+ [dispatch, parsedQuery, selectedTime],
);
useEffect(() => {
@@ -75,32 +73,9 @@ export function useSearchParser(): {
}
}, [queryString, updateQueryString, parsedFilters]);
- const updateParsedQuery = useCallback(
- (updatedParsedPayload: ILogQLParsedQueryItem[]) => {
- dispatch({
- type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
- payload: updatedParsedPayload,
- });
- const reversedParsedQuery = reverseParser(updatedParsedPayload);
- if (
- !isEqual(queryString, reversedParsedQuery) ||
- (queryString === '' && reversedParsedQuery === '')
- ) {
- dispatch({
- type: SET_SEARCH_QUERY_STRING,
- payload: {
- searchQueryString: reversedParsedQuery,
- },
- });
- }
- },
- [dispatch, queryString],
- );
-
return {
queryString,
parsedQuery,
- updateParsedQuery,
updateQueryString,
};
}
diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx
deleted file mode 100644
index 09c5d27471..0000000000
--- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { convertMetricKeyToTrace } from 'lib/resourceAttributes';
-import React from 'react';
-
-import { QueryChipContainer, QueryChipItem } from './styles';
-import { IResourceAttributeQuery } from './types';
-
-interface IQueryChipProps {
- queryData: IResourceAttributeQuery;
- onClose: (id: string) => void;
- disabled: boolean;
-}
-
-export default function QueryChip({
- queryData,
- onClose,
- disabled,
-}: IQueryChipProps): JSX.Element {
- return (
-
- {convertMetricKeyToTrace(queryData.tagKey)}
- {queryData.operator}
- {
- if (!disabled) onClose(queryData.id);
- }}
- >
- {queryData.tagValue.join(', ')}
-
-
- );
-}
diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts
deleted file mode 100644
index 3b9078f76b..0000000000
--- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { createMachine } from 'xstate';
-
-export const ResourceAttributesFilterMachine =
- /** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
- createMachine({
- tsTypes: {} as import('./ResourceAttributesFilter.Machine.typegen').Typegen0,
- initial: 'Idle',
- states: {
- TagKey: {
- on: {
- NEXT: {
- actions: 'onSelectOperator',
- target: 'Operator',
- },
- onBlur: {
- actions: 'onBlurPurge',
- target: 'Idle',
- },
- RESET: {
- target: 'Idle',
- },
- },
- },
- Operator: {
- on: {
- NEXT: {
- actions: 'onSelectTagValue',
- target: 'TagValue',
- },
- onBlur: {
- actions: 'onBlurPurge',
- target: 'Idle',
- },
- RESET: {
- target: 'Idle',
- },
- },
- },
- TagValue: {
- on: {
- onBlur: {
- actions: ['onValidateQuery', 'onBlurPurge'],
- target: 'Idle',
- },
- RESET: {
- target: 'Idle',
- },
- },
- },
- Idle: {
- on: {
- NEXT: {
- actions: 'onSelectTagKey',
- description: 'Select Category',
- target: 'TagKey',
- },
- },
- },
- },
- id: 'Dashboard Search And Filter',
- });
diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx
deleted file mode 100644
index b8bed255f7..0000000000
--- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-import { CloseCircleFilled } from '@ant-design/icons';
-import { useMachine } from '@xstate/react';
-import { Button, Select, Spin } from 'antd';
-import ROUTES from 'constants/routes';
-import history from 'lib/history';
-import { convertMetricKeyToTrace } from 'lib/resourceAttributes';
-import { map } from 'lodash-es';
-import React, { useEffect, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { ResetInitialData } from 'store/actions/metrics/resetInitialData';
-import { SetResourceAttributeQueries } from 'store/actions/metrics/setResourceAttributeQueries';
-import { AppState } from 'store/reducers';
-import MetricReducer from 'types/reducer/metrics';
-import { v4 as uuid } from 'uuid';
-
-import QueryChip from './QueryChip';
-import { ResourceAttributesFilterMachine } from './ResourceAttributesFilter.Machine';
-import { QueryChipItem, SearchContainer } from './styles';
-import { IOption, IResourceAttributeQuery } from './types';
-import { createQuery, GetTagKeys, GetTagValues, OperatorSchema } from './utils';
-
-function ResourceAttributesFilter(): JSX.Element | null {
- const dispatch = useDispatch();
- const [disabled, setDisabled] = useState(
- !(history.location.pathname === ROUTES.APPLICATION),
- );
-
- useEffect(() => {
- const unListen = history.listen(({ pathname }) => {
- setDisabled(!(pathname === ROUTES.APPLICATION));
- });
- return (): void => {
- if (!history.location.pathname.startsWith(`${ROUTES.APPLICATION}/`)) {
- dispatch(ResetInitialData());
- }
- unListen();
- };
- }, [dispatch]);
-
- const { resourceAttributeQueries } = useSelector(
- (state) => state.metrics,
- );
- const [loading, setLoading] = useState(true);
- const [selectedValues, setSelectedValues] = useState([]);
- const [staging, setStaging] = useState([]);
- const [queries, setQueries] = useState([]);
- const [optionsData, setOptionsData] = useState<{
- mode: undefined | 'tags' | 'multiple';
- options: IOption[];
- }>({
- mode: undefined,
- options: [],
- });
-
- const dispatchQueries = (updatedQueries: IResourceAttributeQuery[]): void => {
- dispatch(SetResourceAttributeQueries(updatedQueries));
- };
- const handleLoading = (isLoading: boolean): void => {
- setLoading(isLoading);
- if (isLoading) {
- setOptionsData({ mode: undefined, options: [] });
- }
- };
- const [state, send] = useMachine(ResourceAttributesFilterMachine, {
- actions: {
- onSelectTagKey: () => {
- handleLoading(true);
- GetTagKeys()
- .then((tagKeys) => setOptionsData({ options: tagKeys, mode: undefined }))
- .finally(() => {
- handleLoading(false);
- });
- },
- onSelectOperator: () => {
- setOptionsData({ options: OperatorSchema, mode: undefined });
- },
- onSelectTagValue: () => {
- handleLoading(true);
-
- GetTagValues(staging[0])
- .then((tagValuesOptions) =>
- setOptionsData({ options: tagValuesOptions, mode: 'multiple' }),
- )
- .finally(() => {
- handleLoading(false);
- });
- },
- onBlurPurge: () => {
- setSelectedValues([]);
- setStaging([]);
- },
- onValidateQuery: (): void => {
- if (staging.length < 2 || selectedValues.length === 0) {
- return;
- }
-
- const generatedQuery = createQuery([...staging, selectedValues]);
- if (generatedQuery) {
- dispatchQueries([...queries, generatedQuery]);
- }
- },
- },
- });
-
- useEffect(() => {
- setQueries(resourceAttributeQueries);
- }, [resourceAttributeQueries]);
-
- const handleFocus = (): void => {
- if (state.value === 'Idle') {
- send('NEXT');
- }
- };
-
- const handleBlur = (): void => {
- send('onBlur');
- };
- const handleChange = (value: never): void => {
- if (!optionsData.mode) {
- setStaging((prevStaging) => [...prevStaging, value]);
- setSelectedValues([]);
- send('NEXT');
- return;
- }
-
- setSelectedValues([...value]);
- };
-
- const handleClose = (id: string): void => {
- dispatchQueries(queries.filter((queryData) => queryData.id !== id));
- };
-
- const handleClearAll = (): void => {
- send('RESET');
- dispatchQueries([]);
- setStaging([]);
- setSelectedValues([]);
- };
- const disabledAndEmpty = !!(
- !queries.length &&
- !staging.length &&
- !selectedValues.length &&
- disabled
- );
- const disabledOrEmpty = !!(
- queries.length ||
- staging.length ||
- selectedValues.length ||
- disabled
- );
-
- if (disabledAndEmpty) {
- return null;
- }
-
- return (
-
-
- {map(
- queries,
- (query): JSX.Element => (
-
- ),
- )}
- {map(staging, (item, idx) => (
-
- {idx === 0 ? convertMetricKeyToTrace(item) : item}
-
- ))}
-
- {!disabled && (
-
- Loading...{' '}
-
- ) : (
-
- No resource attributes available to filter. Please refer docs to send
- attributes.
-
- )
- }
- />
- )}
-
- {(queries.length || staging.length || selectedValues.length) && !disabled ? (
- } type="text" />
- ) : null}
-
- );
-}
-
-export default ResourceAttributesFilter;
diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/types.ts b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/types.ts
deleted file mode 100644
index 928b486707..0000000000
--- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export interface IOption {
- label: string;
- value: string;
-}
-
-export interface IResourceAttributeQuery {
- id: string;
- tagKey: string;
- operator: string;
- tagValue: string[];
-}
diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/utils.ts b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/utils.ts
deleted file mode 100644
index aa16f160ac..0000000000
--- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/utils.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import {
- getResourceAttributesTagKeys,
- getResourceAttributesTagValues,
-} from 'api/metrics/getResourceAttributes';
-import { OperatorConversions } from 'constants/resourceAttributes';
-import { convertMetricKeyToTrace } from 'lib/resourceAttributes';
-import { v4 as uuid } from 'uuid';
-
-import { IOption, IResourceAttributeQuery } from './types';
-
-export const OperatorSchema: IOption[] = OperatorConversions.map(
- (operator) => ({
- label: operator.label,
- value: operator.label,
- }),
-);
-
-export const GetTagKeys = async (): Promise => {
- // if (TagKeysCache) {
- // return new Promise((resolve) => {
- // resolve(TagKeysCache);
- // });
- // }
- const { payload } = await getResourceAttributesTagKeys({
- metricName: 'signoz_calls_total',
- match: 'resource_',
- });
- if (!payload || !payload?.data) {
- return [];
- }
- return payload.data.map((tagKey: string) => ({
- label: convertMetricKeyToTrace(tagKey),
- value: tagKey,
- }));
-};
-
-export const GetTagValues = async (tagKey: string): Promise => {
- const { payload } = await getResourceAttributesTagValues({
- tagKey,
- metricName: 'signoz_calls_total',
- });
-
- if (!payload || !payload?.data) {
- return [];
- }
- return payload.data.map((tagValue: string) => ({
- label: tagValue,
- value: tagValue,
- }));
-};
-
-export const createQuery = (
- selectedItems: Array = [],
-): IResourceAttributeQuery | null => {
- if (selectedItems.length === 3) {
- return {
- id: uuid().slice(0, 8),
- tagKey: selectedItems[0] as string,
- operator: selectedItems[1] as string,
- tagValue: selectedItems[2] as string[],
- };
- }
- return null;
-};
diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx
index 37b77ad81c..f5ce2a0188 100644
--- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx
+++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx
@@ -4,21 +4,20 @@ import {
databaseCallsAvgDuration,
databaseCallsRPS,
} from 'container/MetricsApplication/MetricsPageQueries/DBCallQueries';
+import useResourceAttribute from 'hooks/useResourceAttribute';
import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
-} from 'lib/resourceAttributes';
+} from 'hooks/useResourceAttribute/utils';
import React, { useMemo, useState } from 'react';
-import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
-import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
-import MetricReducer from 'types/reducer/metrics';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { Button } from './styles';
import {
dbSystemTags,
+ handleNonInQueryRange,
onGraphClickHandler,
onViewTracePopupClick,
} from './util';
@@ -26,22 +25,22 @@ import {
function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const [selectedTimeStamp, setSelectedTimeStamp] = useState(0);
- const { resourceAttributeQueries } = useSelector(
- (state) => state.metrics,
- );
+ const { queries } = useResourceAttribute();
+
const tagFilterItems = useMemo(
- () => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
- [resourceAttributeQueries],
+ () =>
+ handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
+ [queries],
);
+
const selectedTraceTags: string = useMemo(
() =>
JSON.stringify(
- convertRawQueriesToTraceSelectedTags(resourceAttributeQueries).concat(
- ...dbSystemTags,
- ) || [],
+ convertRawQueriesToTraceSelectedTags(queries).concat(...dbSystemTags) || [],
),
- [resourceAttributeQueries],
+ [queries],
);
+
const legend = '{{db_system}}';
const databaseCallsRPSWidget = useMemo(
diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx
index f6d2d31fbb..c615179d65 100644
--- a/frontend/src/container/MetricsApplication/Tabs/External.tsx
+++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx
@@ -6,33 +6,34 @@ import {
externalCallErrorPercent,
externalCallRpsByAddress,
} from 'container/MetricsApplication/MetricsPageQueries/ExternalQueries';
+import useResourceAttribute from 'hooks/useResourceAttribute';
import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
-} from 'lib/resourceAttributes';
+} from 'hooks/useResourceAttribute/utils';
import React, { useMemo, useState } from 'react';
-import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
-import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
-import MetricReducer from 'types/reducer/metrics';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { legend } from './constant';
import { Button } from './styles';
-import { onGraphClickHandler, onViewTracePopupClick } from './util';
+import {
+ handleNonInQueryRange,
+ onGraphClickHandler,
+ onViewTracePopupClick,
+} from './util';
function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
const [selectedTimeStamp, setSelectedTimeStamp] = useState(0);
const { servicename } = useParams<{ servicename?: string }>();
- const { resourceAttributeQueries } = useSelector(
- (state) => state.metrics,
- );
+ const { queries } = useResourceAttribute();
const tagFilterItems = useMemo(
- () => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
- [resourceAttributeQueries],
+ () =>
+ handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
+ [queries],
);
const externalCallErrorWidget = useMemo(
@@ -51,11 +52,8 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
);
const selectedTraceTags = useMemo(
- () =>
- JSON.stringify(
- convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [],
- ),
- [resourceAttributeQueries],
+ () => JSON.stringify(convertRawQueriesToTraceSelectedTags(queries) || []),
+ [queries],
);
const externalCallDurationWidget = useMemo(
diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx
index de15029408..64cb0ed49a 100644
--- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx
+++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx
@@ -1,18 +1,21 @@
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
import Graph from 'components/Graph';
-import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
+import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
-import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
-import { colors } from 'lib/getRandomColor';
-import history from 'lib/history';
+import { routeConfig } from 'container/SideNav/config';
+import { getQueryString } from 'container/SideNav/helper';
+import useResourceAttribute from 'hooks/useResourceAttribute';
import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
-} from 'lib/resourceAttributes';
+} from 'hooks/useResourceAttribute/utils';
+import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
+import { colors } from 'lib/getRandomColor';
+import history from 'lib/history';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { useParams } from 'react-router-dom';
+import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -25,11 +28,16 @@ import {
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
import TopOperationsTable from '../TopOperationsTable';
import { Button } from './styles';
-import { onGraphClickHandler, onViewTracePopupClick } from './util';
+import {
+ handleNonInQueryRange,
+ onGraphClickHandler,
+ onViewTracePopupClick,
+} from './util';
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const [selectedTimeStamp, setSelectedTimeStamp] = useState(0);
+ const { search } = useLocation();
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
@@ -54,20 +62,21 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
[handleSetTimeStamp],
);
- const {
- topOperations,
- serviceOverview,
- resourceAttributeQueries,
- topLevelOperations,
- } = useSelector((state) => state.metrics);
+ const { topOperations, serviceOverview, topLevelOperations } = useSelector<
+ AppState,
+ MetricReducer
+ >((state) => state.metrics);
+
+ const { queries } = useResourceAttribute();
const selectedTraceTags: string = JSON.stringify(
- convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [],
+ convertRawQueriesToTraceSelectedTags(queries) || [],
);
const tagFilterItems = useMemo(
- () => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
- [resourceAttributeQueries],
+ () =>
+ handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
+ [queries],
);
const operationPerSecWidget = useMemo(
@@ -116,14 +125,19 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
const currentTime = timestamp;
const tPlusOne = timestamp + 60 * 1000;
- const urlParams = new URLSearchParams();
- urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
- urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
+ const urlParams = new URLSearchParams(search);
+ urlParams.set(QueryParams.startTime, currentTime.toString());
+ urlParams.set(QueryParams.endTime, tPlusOne.toString());
+
+ const avialableParams = routeConfig[ROUTES.TRACE];
+ const queryString = getQueryString(avialableParams, urlParams);
history.replace(
`${
ROUTES.TRACE
- }?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1`,
+ }?selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1&${queryString.join(
+ '',
+ )}`,
);
};
diff --git a/frontend/src/container/MetricsApplication/Tabs/util.ts b/frontend/src/container/MetricsApplication/Tabs/util.ts
index a8b53d6f62..185df530e1 100644
--- a/frontend/src/container/MetricsApplication/Tabs/util.ts
+++ b/frontend/src/container/MetricsApplication/Tabs/util.ts
@@ -1,7 +1,10 @@
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
-import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
+import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
+import { routeConfig } from 'container/SideNav/config';
+import { getQueryString } from 'container/SideNav/helper';
import history from 'lib/history';
+import { IQueryBuilderTagFilterItems } from 'types/api/dashboard/getAll';
import { Tags } from 'types/reducer/trace';
export const dbSystemTags: Tags[] = [
@@ -30,16 +33,18 @@ export function onViewTracePopupClick({
const currentTime = timestamp;
const tPlusOne = timestamp + 60 * 1000;
- const urlParams = new URLSearchParams();
- urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
- urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
+ const urlParams = new URLSearchParams(window.location.search);
+ urlParams.set(QueryParams.startTime, currentTime.toString());
+ urlParams.set(QueryParams.endTime, tPlusOne.toString());
+ const avialableParams = routeConfig[ROUTES.TRACE];
+ const queryString = getQueryString(avialableParams, urlParams);
history.replace(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1${
isExternalCall ? '&spanKind=3' : ''
- }`,
+ }&${queryString.join('&')}`,
);
};
}
@@ -60,7 +65,7 @@ export function onGraphClickHandler(
const points = chart.getElementsAtEventForMode(
event.native,
'nearest',
- { intersect: true },
+ { intersect: false },
true,
);
const id = `${from}_button`;
@@ -84,3 +89,16 @@ export function onGraphClickHandler(
}
};
}
+
+export const handleNonInQueryRange = (
+ tags: IQueryBuilderTagFilterItems[],
+): IQueryBuilderTagFilterItems[] =>
+ tags.map((tag) => {
+ if (tag.op === 'Not IN') {
+ return {
+ ...tag,
+ op: 'NIN',
+ };
+ }
+ return tag;
+ });
diff --git a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx
index 0a23480ee2..d47bc30c94 100644
--- a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx
+++ b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx
@@ -1,27 +1,25 @@
import { Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
-import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
+import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
+import useResourceAttribute from 'hooks/useResourceAttribute';
+import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import history from 'lib/history';
-import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
import React from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
-import MetricReducer from 'types/reducer/metrics';
function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
const { minTime, maxTime } = useSelector(
(state) => state.globalTime,
);
- const { resourceAttributeQueries } = useSelector(
- (state) => state.metrics,
- );
+ const { queries } = useResourceAttribute();
const selectedTraceTags: string = JSON.stringify(
- convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [],
+ convertRawQueriesToTraceSelectedTags(queries) || [],
);
const { data } = props;
@@ -31,14 +29,8 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
const handleOnClick = (operation: string): void => {
const urlParams = new URLSearchParams();
const { servicename } = params;
- urlParams.set(
- METRICS_PAGE_QUERY_PARAM.startTime,
- (minTime / 1000000).toString(),
- );
- urlParams.set(
- METRICS_PAGE_QUERY_PARAM.endTime,
- (maxTime / 1000000).toString(),
- );
+ urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
+ urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
history.push(
`${
diff --git a/frontend/src/container/MetricsApplication/index.tsx b/frontend/src/container/MetricsApplication/index.tsx
index af76ca95ae..0e189fb1bd 100644
--- a/frontend/src/container/MetricsApplication/index.tsx
+++ b/frontend/src/container/MetricsApplication/index.tsx
@@ -1,11 +1,11 @@
import RouteTab from 'components/RouteTab';
import ROUTES from 'constants/routes';
+import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import React, { memo } from 'react';
import { generatePath, useParams } from 'react-router-dom';
import { useLocation } from 'react-use';
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
-import ResourceAttributesFilter from './ResourceAttributesFilter';
import DBCall from './Tabs/DBCall';
import External from './Tabs/External';
import Overview from './Tabs/Overview';
diff --git a/frontend/src/container/MetricsTable/Metrics.test.tsx b/frontend/src/container/MetricsTable/Metrics.test.tsx
new file mode 100644
index 0000000000..b81a9533b7
--- /dev/null
+++ b/frontend/src/container/MetricsTable/Metrics.test.tsx
@@ -0,0 +1,70 @@
+import { render, RenderResult, screen, waitFor } from '@testing-library/react';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+import {
+ combineReducers,
+ legacy_createStore as createStore,
+ Store,
+} from 'redux';
+
+import { InitialValue } from '../../store/reducers/metric';
+import Metrics from './index';
+
+const rootReducer = combineReducers({
+ metrics: (state = InitialValue) => state,
+});
+
+const mockStore = createStore(rootReducer);
+
+const renderWithReduxAndRouter = (mockStore: Store) => (
+ component: React.ReactElement,
+): RenderResult =>
+ render(
+
+ {component}
+ ,
+ );
+
+describe('Metrics Component', () => {
+ it('renders without errors', async () => {
+ renderWithReduxAndRouter(mockStore)( );
+
+ await waitFor(() => {
+ expect(screen.getByText(/application/i)).toBeInTheDocument();
+ expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument();
+ expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument();
+ expect(screen.getByText(/operations per second/i)).toBeInTheDocument();
+ });
+ });
+
+ it('renders loading when required conditions are met', async () => {
+ const customStore = createStore(rootReducer, {
+ metrics: {
+ services: [],
+ loading: true,
+ error: false,
+ },
+ });
+
+ const { container } = renderWithReduxAndRouter(customStore)( );
+
+ const spinner = container.querySelector('.ant-spin-nested-loading');
+
+ expect(spinner).toBeInTheDocument();
+ });
+
+ it('renders no data when required conditions are met', async () => {
+ const customStore = createStore(rootReducer, {
+ metrics: {
+ services: [],
+ loading: false,
+ error: false,
+ },
+ });
+
+ renderWithReduxAndRouter(customStore)( );
+
+ expect(screen.getByText('No data')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/container/MetricsTable/index.tsx b/frontend/src/container/MetricsTable/index.tsx
index c815769f54..6e9b74f88a 100644
--- a/frontend/src/container/MetricsTable/index.tsx
+++ b/frontend/src/container/MetricsTable/index.tsx
@@ -11,6 +11,8 @@ import localStorageSet from 'api/browser/localstorage/set';
import { ResizeTable } from 'components/ResizeTable';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import ROUTES from 'constants/routes';
+import { routeConfig } from 'container/SideNav/config';
+import { getQueryString } from 'container/SideNav/helper';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
@@ -88,11 +90,17 @@ function Metrics(): JSX.Element {
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()),
- render: (text: string): JSX.Element => (
-
- {text}
-
- ),
+ render: (metrics: string): JSX.Element => {
+ const urlParams = new URLSearchParams(search);
+ const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
+ const queryString = getQueryString(avialableParams, urlParams);
+
+ return (
+
+ {metrics}
+
+ );
+ },
}),
[filterDropdown, FilterIcon, search],
);
diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx
index 9910252b3f..8841d10d0a 100644
--- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx
+++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx
@@ -129,7 +129,8 @@ function VariableItem({
useEffect(() => {
getOptions();
- }, [getOptions]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
const handleChange = (value: string | string[]): void => {
if (
diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.typegen.ts b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.typegen.ts
index e7f7ee3de7..f2019aaa26 100644
--- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.typegen.ts
+++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.typegen.ts
@@ -2,31 +2,31 @@
export interface Typegen0 {
'@@xstate/typegen': true;
- eventsCausingActions: {
- onSelectOperator: 'NEXT';
- onBlurPurge: 'onBlur';
- onSelectTagValue: 'NEXT';
- onValidateQuery: 'onBlur';
- onSelectTagKey: 'NEXT';
- };
internalEvents: {
'xstate.init': { type: 'xstate.init' };
};
invokeSrcNameMap: {};
missingImplementations: {
actions:
- | 'onSelectOperator'
| 'onBlurPurge'
+ | 'onSelectOperator'
+ | 'onSelectTagKey'
| 'onSelectTagValue'
- | 'onValidateQuery'
- | 'onSelectTagKey';
- services: never;
- guards: never;
+ | 'onValidateQuery';
delays: never;
+ guards: never;
+ services: never;
+ };
+ eventsCausingActions: {
+ onBlurPurge: 'onBlur';
+ onSelectOperator: 'NEXT';
+ onSelectTagKey: 'NEXT';
+ onSelectTagValue: 'NEXT';
+ onValidateQuery: 'onBlur';
};
- eventsCausingServices: {};
- eventsCausingGuards: {};
eventsCausingDelays: {};
- matchesStates: 'TagKey' | 'Operator' | 'TagValue' | 'Idle';
+ eventsCausingGuards: {};
+ eventsCausingServices: {};
+ matchesStates: 'Idle' | 'Operator' | 'TagKey' | 'TagValue';
tags: never;
}
diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx
index 6657f030b1..8914457154 100644
--- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx
+++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx
@@ -1,10 +1,9 @@
-/* eslint-disable */
-//@ts-nocheck
-
+/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, Tabs } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
+import { QueryBuilder } from 'container/QueryBuilder';
import { cloneDeep, isEqual } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
@@ -31,6 +30,7 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
import QueryBuilderQueryContainer from './QueryBuilder/queryBuilder';
import TabHeader from './TabHeader';
+import { IHandleUpdatedQuery } from './types';
import { getQueryKey } from './utils/getQueryKey';
import { showUnstagedStashConfirmBox } from './utils/userSettings';
@@ -54,9 +54,7 @@ function QuerySection({
const { search } = useLocation();
const { widgets } = selectedDashboards.data;
- const urlQuery = useMemo(() => {
- return new URLSearchParams(search);
- }, [search]);
+ const urlQuery = useMemo(() => new URLSearchParams(search), [search]);
const getWidget = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
@@ -169,6 +167,9 @@ function QuerySection({
}
selectedGraph={selectedGraph}
/>
+
+ // TODO: uncomment for testing new QueryBuilder
+ //
),
},
{
diff --git a/frontend/src/container/NewWidget/styles.ts b/frontend/src/container/NewWidget/styles.ts
index 386f044a0b..c9941ad07c 100644
--- a/frontend/src/container/NewWidget/styles.ts
+++ b/frontend/src/container/NewWidget/styles.ts
@@ -11,12 +11,14 @@ export const Container = styled.div`
export const RightContainerWrapper = styled(Col)`
&&& {
min-width: 200px;
+ margin-bottom: 1rem;
}
`;
export const LeftContainerWrapper = styled(Col)`
&&& {
margin-right: 1rem;
+ margin-bottom: 1rem;
max-width: 70%;
}
`;
diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts
index eb945aa6fb..01bc5726e5 100644
--- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts
+++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts
@@ -1,9 +1,14 @@
-import {
- IBuilderFormula,
- IBuilderQuery,
-} from 'types/api/queryBuilder/queryBuilderData';
+import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
+import { DataSource } from 'types/common/queryBuilder';
+
+export type QueryBuilderConfig =
+ | {
+ queryVariant: 'static';
+ initialDataSource: DataSource;
+ }
+ | { queryVariant: 'dropdown' };
export type QueryBuilderProps = {
- queryData: IBuilderQuery[];
- queryFormula: IBuilderFormula[];
+ config?: QueryBuilderConfig;
+ panelType?: ITEMS;
};
diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx
index c732a27d76..2bb16824c7 100644
--- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx
+++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx
@@ -1,23 +1,93 @@
+import { PlusOutlined } from '@ant-design/icons';
+import { Button, Col, Row } from 'antd';
+import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
// ** Hooks
import { useQueryBuilder } from 'hooks/useQueryBuilder';
-import React from 'react';
+// ** Constants
+import React, { memo, useEffect, useMemo } from 'react';
+// ** Components
+import { Formula, Query } from './components';
// ** Types
import { QueryBuilderProps } from './QueryBuilder.interfaces';
+// ** Styles
-// TODO: temporary eslint disable while variable isn't used
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export function QueryBuilder(props: QueryBuilderProps): JSX.Element {
- // TODO: temporary doesn't use
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { queryBuilderData } = useQueryBuilder();
+export const QueryBuilder = memo(function QueryBuilder({
+ config,
+ panelType,
+}: QueryBuilderProps): JSX.Element {
+ const {
+ queryBuilderData,
+ setupInitialDataSource,
+ addNewQuery,
+ addNewFormula,
+ } = useQueryBuilder();
- // Here we can use Form from antd library and fill context data or edit
- // Connect form with adding or removing items from the list
+ useEffect(() => {
+ if (config && config.queryVariant === 'static') {
+ setupInitialDataSource(config.initialDataSource);
+ }
- // Here will be map of query queryBuilderData.queryData and queryBuilderData.queryFormulas components
- // Each component can be part of antd Form list where we can add or remove items
- // Also need decide to make a copy of queryData for working with form or not and after it set the full new list with formulas or queries to the context
- // With button to add him
- return null
;
-}
+ return (): void => {
+ setupInitialDataSource(null);
+ };
+ }, [config, setupInitialDataSource]);
+
+ const isDisabledQueryButton = useMemo(
+ () => queryBuilderData.queryData.length >= MAX_QUERIES,
+ [queryBuilderData],
+ );
+
+ const isDisabledFormulaButton = useMemo(
+ () => queryBuilderData.queryFormulas.length >= MAX_FORMULAS,
+ [queryBuilderData],
+ );
+
+ return (
+
+
+
+ {queryBuilderData.queryData.map((query, index) => (
+
+ 1}
+ queryVariant={config?.queryVariant || 'dropdown'}
+ query={query}
+ panelType={panelType}
+ />
+
+ ))}
+ {queryBuilderData.queryFormulas.map((formula, index) => (
+
+
+
+ ))}
+
+
+
+
+
+ }
+ onClick={addNewQuery}
+ >
+ Query
+
+
+
+ }
+ >
+ Formula
+
+
+
+
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.interfaces.ts b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.interfaces.ts
new file mode 100644
index 0000000000..41c3e7c25a
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.interfaces.ts
@@ -0,0 +1,6 @@
+import React from 'react';
+
+export type AdditionalFiltersProps = {
+ listOfAdditionalFilter: string[];
+ children: React.ReactNode;
+};
diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.styled.ts b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.styled.ts
new file mode 100644
index 0000000000..8f23dd0e3c
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.styled.ts
@@ -0,0 +1,34 @@
+import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
+import { Col, Typography } from 'antd';
+import styled, { css } from 'styled-components';
+
+const IconCss = css`
+ margin-right: 0.6875rem;
+ transition: all 0.2s ease;
+`;
+
+export const StyledIconOpen = styled(PlusSquareOutlined)`
+ ${IconCss}
+`;
+
+export const StyledIconClose = styled(MinusSquareOutlined)`
+ ${IconCss}
+`;
+
+export const StyledInner = styled(Col)`
+ width: fit-content;
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.875rem;
+ min-height: 1.375rem;
+ cursor: pointer;
+ &:hover {
+ ${StyledIconOpen}, ${StyledIconClose} {
+ opacity: 0.7;
+ }
+ }
+`;
+
+export const StyledLink = styled(Typography.Link)`
+ pointer-events: none;
+`;
diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx
new file mode 100644
index 0000000000..2d398479bf
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx
@@ -0,0 +1,53 @@
+import { Col, Row } from 'antd';
+import React, { Fragment, memo, ReactNode, useState } from 'react';
+
+// ** Types
+import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces';
+// ** Styles
+import {
+ StyledIconClose,
+ StyledIconOpen,
+ StyledInner,
+ StyledLink,
+} from './AdditionalFiltersToggler.styled';
+
+export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
+ children,
+ listOfAdditionalFilter,
+}: AdditionalFiltersProps): JSX.Element {
+ const [isOpenedFilters, setIsOpenedFilters] = useState(false);
+
+ const handleToggleOpenFilters = (): void => {
+ setIsOpenedFilters((prevState) => !prevState);
+ };
+
+ const filtersTexts: ReactNode = listOfAdditionalFilter.map((str, index) => {
+ const isNextLast = index + 1 === listOfAdditionalFilter.length - 1;
+ if (index === listOfAdditionalFilter.length - 1) {
+ return (
+
+ and {str.toUpperCase()}
+
+ );
+ }
+
+ return (
+
+ {str.toUpperCase()}
+ {isNextLast ? ' ' : ', '}
+
+ );
+ });
+
+ return (
+
+
+
+ {isOpenedFilters ? : }
+ {!isOpenedFilters && Add conditions for {filtersTexts} }
+
+
+ {isOpenedFilters && {children}}
+
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/index.ts b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/index.ts
new file mode 100644
index 0000000000..d08059abdf
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/index.ts
@@ -0,0 +1 @@
+export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts
new file mode 100644
index 0000000000..0a34d52c6f
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts
@@ -0,0 +1,6 @@
+import { SelectProps } from 'antd';
+import { DataSource } from 'types/common/queryBuilder';
+
+export type QueryLabelProps = {
+ onChange: (value: DataSource) => void;
+} & Omit;
diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx
new file mode 100644
index 0000000000..2f5387398c
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx
@@ -0,0 +1,35 @@
+import { Select } from 'antd';
+import React, { memo } from 'react';
+import { DataSource } from 'types/common/queryBuilder';
+import { SelectOption } from 'types/common/select';
+// ** Helpers
+import { transformToUpperCase } from 'utils/transformToUpperCase';
+
+// ** Types
+import { QueryLabelProps } from './DataSourceDropdown.interfaces';
+
+const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
+
+export const DataSourceDropdown = memo(function DataSourceDropdown(
+ props: QueryLabelProps,
+): JSX.Element {
+ const { onChange, value, style } = props;
+
+ const dataSourceOptions: SelectOption<
+ DataSource,
+ string
+ >[] = dataSourceMap.map((source) => ({
+ label: transformToUpperCase(source),
+ value: source,
+ }));
+
+ return (
+
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/index.ts b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/index.ts
new file mode 100644
index 0000000000..0dcd4699cb
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/index.ts
@@ -0,0 +1 @@
+export { DataSourceDropdown } from './DataSourceDropdown';
diff --git a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.interfaces.ts b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.interfaces.ts
new file mode 100644
index 0000000000..aa1a7cf67e
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.interfaces.ts
@@ -0,0 +1,6 @@
+import { CSSProperties } from 'react';
+
+export type FilterLabelProps = {
+ label: string;
+ style?: CSSProperties;
+};
diff --git a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.styled.ts b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.styled.ts
new file mode 100644
index 0000000000..85936ce595
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.styled.ts
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+export const StyledLabel = styled.div`
+ padding: 0 0.6875rem;
+ min-height: 2rem;
+ width: 100%;
+ display: inline-flex;
+ white-space: nowrap;
+ align-items: center;
+ border-radius: 0.125rem;
+ border: 0.0625rem solid #434343;
+ background-color: #141414;
+`;
diff --git a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx
new file mode 100644
index 0000000000..48153f4823
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx
@@ -0,0 +1,12 @@
+import React, { memo } from 'react';
+
+// ** Types
+import { FilterLabelProps } from './FilterLabel.interfaces';
+// ** Styles
+import { StyledLabel } from './FilterLabel.styled';
+
+export const FilterLabel = memo(function FilterLabel({
+ label,
+}: FilterLabelProps): JSX.Element {
+ return {label} ;
+});
diff --git a/frontend/src/container/QueryBuilder/components/FilterLabel/index.ts b/frontend/src/container/QueryBuilder/components/FilterLabel/index.ts
new file mode 100644
index 0000000000..d8b9ef25eb
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/FilterLabel/index.ts
@@ -0,0 +1 @@
+export { FilterLabel } from './FilterLabel';
diff --git a/frontend/src/container/QueryBuilder/components/Formula/Formula.interfaces.ts b/frontend/src/container/QueryBuilder/components/Formula/Formula.interfaces.ts
new file mode 100644
index 0000000000..aa0cb1c475
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/Formula/Formula.interfaces.ts
@@ -0,0 +1,3 @@
+import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
+
+export type FormulaProps = { formula: IBuilderFormula; index: number };
diff --git a/frontend/src/container/QueryBuilder/components/Formula/Formula.tsx b/frontend/src/container/QueryBuilder/components/Formula/Formula.tsx
new file mode 100644
index 0000000000..23a263528d
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/Formula/Formula.tsx
@@ -0,0 +1,73 @@
+import { Col, Input } from 'antd';
+// ** Components
+import { ListItemWrapper, ListMarker } from 'container/QueryBuilder/components';
+// ** Hooks
+import { useQueryBuilder } from 'hooks/useQueryBuilder';
+import React, { ChangeEvent, useCallback } from 'react';
+import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
+
+// ** Types
+import { FormulaProps } from './Formula.interfaces';
+
+const { TextArea } = Input;
+
+export function Formula({ index, formula }: FormulaProps): JSX.Element {
+ const { removeEntityByIndex, handleSetFormulaData } = useQueryBuilder();
+
+ const handleDelete = useCallback(() => {
+ removeEntityByIndex('queryFormulas', index);
+ }, [index, removeEntityByIndex]);
+
+ const handleToggleDisableFormula = useCallback((): void => {
+ const newFormula: IBuilderFormula = {
+ ...formula,
+ disabled: !formula.disabled,
+ };
+
+ handleSetFormulaData(index, newFormula);
+ }, [index, formula, handleSetFormulaData]);
+
+ const handleChange = useCallback(
+ (e: ChangeEvent) => {
+ const { name, value } = e.target;
+ const newFormula: IBuilderFormula = {
+ ...formula,
+ [name]: value,
+ };
+
+ handleSetFormulaData(index, newFormula);
+ },
+ [index, formula, handleSetFormulaData],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/container/QueryBuilder/components/Formula/index.ts b/frontend/src/container/QueryBuilder/components/Formula/index.ts
new file mode 100644
index 0000000000..490875c2c7
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/Formula/index.ts
@@ -0,0 +1 @@
+export { Formula } from './Formula';
diff --git a/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.interfaces.ts b/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.interfaces.ts
new file mode 100644
index 0000000000..aeeefe04c1
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.interfaces.ts
@@ -0,0 +1,6 @@
+import React from 'react';
+
+export type ListItemWrapperProps = {
+ onDelete: () => void;
+ children: React.ReactNode;
+};
diff --git a/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.styled.ts b/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.styled.ts
new file mode 100644
index 0000000000..cbe1b79f9c
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.styled.ts
@@ -0,0 +1,26 @@
+import { CloseCircleOutlined } from '@ant-design/icons';
+import { Row } from 'antd';
+import styled from 'styled-components';
+
+export const StyledDeleteEntity = styled(CloseCircleOutlined)`
+ position: absolute;
+ top: 0.5rem;
+ right: 0.9375rem;
+ z-index: 1;
+ cursor: pointer;
+ opacity: 0.45;
+ width: 1.3125rem;
+ height: 1.3125rem;
+ svg {
+ width: 100%;
+ height: 100%;
+ }
+`;
+
+export const StyledRow = styled(Row)`
+ padding-right: 3rem;
+`;
+
+export const StyledFilterRow = styled(Row)`
+ margin-bottom: 0.875rem;
+`;
diff --git a/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.tsx b/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.tsx
new file mode 100644
index 0000000000..1136ec02f2
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/ListItemWrapper/ListItemWrapper.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+// ** Types
+import { ListItemWrapperProps } from './ListItemWrapper.interfaces';
+// ** Styles
+import { StyledDeleteEntity, StyledRow } from './ListItemWrapper.styled';
+
+export function ListItemWrapper({
+ children,
+ onDelete,
+}: ListItemWrapperProps): JSX.Element {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/container/QueryBuilder/components/ListItemWrapper/index.ts b/frontend/src/container/QueryBuilder/components/ListItemWrapper/index.ts
new file mode 100644
index 0000000000..c8d9f7e5da
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/ListItemWrapper/index.ts
@@ -0,0 +1 @@
+export { ListItemWrapper } from './ListItemWrapper';
diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.interfaces.ts b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.interfaces.ts
index 5ef7ac5a9a..bb360ed69f 100644
--- a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.interfaces.ts
+++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.interfaces.ts
@@ -1,8 +1,11 @@
+import { CSSProperties } from 'react';
+
export type ListMarkerProps = {
isDisabled: boolean;
labelName: string;
index: number;
className?: string;
- isAvailableToDisable: boolean;
- toggleDisabled: (index: number) => void;
+ isAvailableToDisable?: boolean;
+ onDisable: (index: number) => void;
+ style?: CSSProperties;
};
diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts
index f876af973b..5c1e5cc396 100644
--- a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts
+++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts
@@ -1,9 +1,13 @@
import { Button } from 'antd';
import styled from 'styled-components';
-export const StyledButton = styled(Button)`
+export const StyledButton = styled(Button)<{ $isAvailableToDisable: boolean }>`
min-width: 2rem;
height: 2.25rem;
- padding: 0.125rem;
+ padding: ${(props): string =>
+ props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'};
border-radius: 0.375rem;
+ margin-right: 0.5rem;
+ pointer-events: ${(props): string =>
+ props.$isAvailableToDisable ? 'default' : 'none'};
`;
diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx
index a1f0437864..86115db7cd 100644
--- a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx
+++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx
@@ -1,25 +1,26 @@
import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons';
import { ButtonProps } from 'antd';
-import React from 'react';
+import React, { memo } from 'react';
// ** Types
import { ListMarkerProps } from './ListMarker.interfaces';
// ** Styles
import { StyledButton } from './ListMarker.styled';
-export function ListMarker({
+export const ListMarker = memo(function ListMarker({
isDisabled,
labelName,
index,
- isAvailableToDisable,
+ isAvailableToDisable = true,
className,
- toggleDisabled,
+ onDisable,
+ style,
}: ListMarkerProps): JSX.Element {
const buttonProps: Partial = isAvailableToDisable
? {
type: isDisabled ? 'default' : 'primary',
icon: isDisabled ? : ,
- onClick: (): void => toggleDisabled(index),
+ onClick: (): void => onDisable(index),
}
: { type: 'primary' };
@@ -29,8 +30,10 @@ export function ListMarker({
icon={buttonProps.icon}
onClick={buttonProps.onClick}
className={className}
+ $isAvailableToDisable={isAvailableToDisable}
+ style={style}
>
{labelName}
);
-}
+});
diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts
new file mode 100644
index 0000000000..5420be12ea
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts
@@ -0,0 +1,10 @@
+import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+
+export type QueryProps = {
+ index: number;
+ isAvailableToDisable: boolean;
+ query: IBuilderQueryForm;
+ queryVariant: 'static' | 'dropdown';
+ panelType?: ITEMS;
+};
diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx
new file mode 100644
index 0000000000..c692ea1042
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx
@@ -0,0 +1,393 @@
+import { Col, Input, Row } from 'antd';
+// ** Constants
+import {
+ initialAggregateAttribute,
+ initialQueryBuilderFormValues,
+ mapOfFilters,
+ mapOfOperators,
+} from 'constants/queryBuilder';
+// ** Components
+import {
+ AdditionalFiltersToggler,
+ DataSourceDropdown,
+ FilterLabel,
+ ListItemWrapper,
+ ListMarker,
+} from 'container/QueryBuilder/components';
+import {
+ AggregatorFilter,
+ GroupByFilter,
+ HavingFilter,
+ OperatorsSelect,
+ ReduceToFilter,
+} from 'container/QueryBuilder/filters';
+import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter';
+import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
+import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter';
+import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
+import { useQueryBuilder } from 'hooks/useQueryBuilder';
+import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
+// ** Hooks
+import React, { memo, useCallback, useMemo } from 'react';
+import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import {
+ Having,
+ IBuilderQueryForm,
+ TagFilter,
+} from 'types/api/queryBuilder/queryBuilderData';
+import { DataSource, StringOperators } from 'types/common/queryBuilder';
+import { transformToUpperCase } from 'utils/transformToUpperCase';
+
+// ** Types
+import { QueryProps } from './Query.interfaces';
+
+export const Query = memo(function Query({
+ index,
+ isAvailableToDisable,
+ queryVariant,
+ query,
+ panelType,
+}: QueryProps): JSX.Element {
+ const {
+ handleSetQueryData,
+ removeEntityByIndex,
+ initialDataSource,
+ } = useQueryBuilder();
+
+ const currentListOfOperators = useMemo(
+ () => mapOfOperators[query.dataSource],
+ [query],
+ );
+ const listOfAdditionalFilters = useMemo(() => mapOfFilters[query.dataSource], [
+ query,
+ ]);
+
+ const handleChangeOperator = useCallback(
+ (value: string): void => {
+ const aggregateDataType: BaseAutocompleteData['dataType'] =
+ query.aggregateAttribute.dataType;
+
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ aggregateOperator: value,
+ having: [],
+ groupBy: [],
+ orderBy: [],
+ limit: null,
+ tagFilters: { items: [], op: 'AND' },
+ };
+
+ if (!aggregateDataType) {
+ handleSetQueryData(index, newQuery);
+ return;
+ }
+
+ switch (aggregateDataType) {
+ case 'string':
+ case 'bool': {
+ const typeOfValue = findDataTypeOfOperator(value);
+
+ handleSetQueryData(index, {
+ ...newQuery,
+ ...(typeOfValue === 'number'
+ ? { aggregateAttribute: initialAggregateAttribute }
+ : {}),
+ });
+
+ break;
+ }
+ case 'float64':
+ case 'int64': {
+ handleSetQueryData(index, newQuery);
+
+ break;
+ }
+
+ default: {
+ handleSetQueryData(index, newQuery);
+ break;
+ }
+ }
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleChangeAggregatorAttribute = useCallback(
+ (value: BaseAutocompleteData): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ aggregateAttribute: value,
+ having: [],
+ };
+
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleChangeDataSource = useCallback(
+ (nextSource: DataSource): void => {
+ let newQuery: IBuilderQueryForm = {
+ ...query,
+ dataSource: nextSource,
+ };
+
+ if (nextSource !== query.dataSource) {
+ const initCopy = {
+ ...(initialQueryBuilderFormValues as Partial),
+ };
+ delete initCopy.queryName;
+
+ newQuery = {
+ ...newQuery,
+ ...initCopy,
+ dataSource: initialDataSource || nextSource,
+ aggregateOperator: mapOfOperators[nextSource][0],
+ };
+ }
+
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, initialDataSource, handleSetQueryData],
+ );
+
+ const handleToggleDisableQuery = useCallback((): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ disabled: !query.disabled,
+ };
+
+ handleSetQueryData(index, newQuery);
+ }, [index, query, handleSetQueryData]);
+
+ const handleChangeGroupByKeys = useCallback(
+ (values: BaseAutocompleteData[]): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ groupBy: values,
+ };
+
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleChangeQueryLegend = useCallback(
+ (e: React.ChangeEvent): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ legend: e.target.value,
+ };
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleChangeReduceTo = useCallback(
+ (value: string): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ reduceTo: value,
+ };
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleChangeHavingFilter = useCallback(
+ (having: Having[]) => {
+ const newQuery: IBuilderQueryForm = { ...query, having };
+
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleDeleteQuery = useCallback(() => {
+ removeEntityByIndex('queryData', index);
+ }, [removeEntityByIndex, index]);
+
+ const isMatricsDataSource = useMemo(
+ () => query.dataSource === DataSource.METRICS,
+ [query.dataSource],
+ );
+
+ const handleChangeOrderByKeys = useCallback(
+ (values: BaseAutocompleteData[]): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ orderBy: values,
+ };
+ handleSetQueryData(index, newQuery);
+ },
+ [handleSetQueryData, index, query],
+ );
+
+ const handleChangeLimit = useCallback(
+ (value: number | null): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ limit: value,
+ };
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleChangeAggregateEvery = useCallback(
+ (value: number): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ stepInterval: value,
+ };
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ const handleChangeTagFilters = useCallback(
+ (value: TagFilter): void => {
+ const newQuery: IBuilderQueryForm = {
+ ...query,
+ tagFilters: value,
+ };
+ handleSetQueryData(index, newQuery);
+ },
+ [index, query, handleSetQueryData],
+ );
+
+ return (
+
+
+
+
+
+ {queryVariant === 'dropdown' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isMatricsDataSource && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {panelType === 'VALUE' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {!isMatricsDataSource && (
+
+
+
+
+
+
+
+
+
+
+ )}
+ {query.aggregateOperator !== StringOperators.NOOP && (
+
+
+
+
+
+
+
+
+
+
+ )}
+ {!isMatricsDataSource && (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/components/Query/index.ts b/frontend/src/container/QueryBuilder/components/Query/index.ts
new file mode 100644
index 0000000000..7a1049f4a5
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/components/Query/index.ts
@@ -0,0 +1 @@
+export { Query } from './Query';
diff --git a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.interfaces.ts b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.interfaces.ts
deleted file mode 100644
index 48b8775b1f..0000000000
--- a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.interfaces.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { SelectProps } from 'antd';
-import { DataSource } from 'types/common/queryBuilder';
-
-type StaticLabel = { variant: 'static'; dataSource: DataSource };
-
-export type DropdownLabel = {
- variant: 'dropdown';
- onChange: (value: DataSource) => void;
-} & Omit;
-
-export type QueryLabelProps = StaticLabel | DropdownLabel;
-
-export function isLabelDropdown(
- label: QueryLabelProps,
-): label is DropdownLabel {
- return label.variant === 'dropdown';
-}
diff --git a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.styled.ts b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.styled.ts
deleted file mode 100644
index 56b4030035..0000000000
--- a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.styled.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Select } from 'antd';
-import styled, { css } from 'styled-components';
-
-// ** Types
-import { DropdownLabel } from './QueryLabel.interfaces';
-
-const LabelStyle = css`
- width: fit-content;
- min-width: 5.75rem;
-`;
-
-export const StyledSingleLabel = styled(Select)`
- pointer-events: none;
- ${LabelStyle}
-`;
-
-export const StyledDropdownLabel = styled(Select)`
- ${LabelStyle}
-`;
diff --git a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.tsx b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.tsx
deleted file mode 100644
index 1bc6fad1a4..0000000000
--- a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Select } from 'antd';
-import React from 'react';
-import { DataSource } from 'types/common/queryBuilder';
-import { SelectOption } from 'types/common/select';
-
-// ** Types
-import { isLabelDropdown, QueryLabelProps } from './QueryLabel.interfaces';
-// ** Styles
-import { StyledSingleLabel } from './QueryLabel.styled';
-
-const { Option } = Select;
-
-const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
-
-export function QueryLabel(props: QueryLabelProps): JSX.Element {
- const isDropdown = isLabelDropdown(props);
-
- if (!isDropdown) {
- const { dataSource } = props;
-
- return (
-
- {dataSource}
-
- );
- }
-
- const { onChange } = props;
-
- const dataSourceOptions: SelectOption<
- DataSource,
- string
- >[] = dataSourceMap.map((source) => ({
- label: source.charAt(0).toUpperCase() + source.slice(1),
- value: source,
- }));
-
- return (
-
- );
-}
diff --git a/frontend/src/container/QueryBuilder/components/QueryLabel/index.ts b/frontend/src/container/QueryBuilder/components/QueryLabel/index.ts
deleted file mode 100644
index 7403bb9ce5..0000000000
--- a/frontend/src/container/QueryBuilder/components/QueryLabel/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { QueryLabel } from './QueryLabel';
diff --git a/frontend/src/container/QueryBuilder/components/index.ts b/frontend/src/container/QueryBuilder/components/index.ts
index a5ced11d5b..8e329f162d 100644
--- a/frontend/src/container/QueryBuilder/components/index.ts
+++ b/frontend/src/container/QueryBuilder/components/index.ts
@@ -1,2 +1,7 @@
+export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
+export { DataSourceDropdown } from './DataSourceDropdown';
+export { FilterLabel } from './FilterLabel';
+export { Formula } from './Formula';
+export { ListItemWrapper } from './ListItemWrapper';
export { ListMarker } from './ListMarker';
-export { QueryLabel } from './QueryLabel';
+export { Query } from './Query';
diff --git a/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx
new file mode 100644
index 0000000000..027fabeec4
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx
@@ -0,0 +1,62 @@
+import { Input } from 'antd';
+import getStep from 'lib/getStep';
+import React, { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { AppState } from 'store/reducers';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+import { GlobalReducer } from 'types/reducer/globalTime';
+
+import { selectStyle } from '../QueryBuilderSearch/config';
+
+function AggregateEveryFilter({
+ onChange,
+ query,
+}: AggregateEveryFilterProps): JSX.Element {
+ const { maxTime, minTime } = useSelector(
+ (state) => state.globalTime,
+ );
+
+ const stepInterval = useMemo(
+ () =>
+ getStep({
+ start: minTime,
+ end: maxTime,
+ inputFormat: 'ns',
+ }),
+ [maxTime, minTime],
+ );
+
+ const handleKeyDown = (event: {
+ keyCode: number;
+ which: number;
+ preventDefault: () => void;
+ }): void => {
+ const keyCode = event.keyCode || event.which;
+ const isBackspace = keyCode === 8;
+ const isNumeric =
+ (keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
+
+ if (!isNumeric && !isBackspace) {
+ event.preventDefault();
+ }
+ };
+
+ return (
+ onChange(Number(event.target.value))}
+ onKeyDown={handleKeyDown}
+ />
+ );
+}
+
+interface AggregateEveryFilterProps {
+ onChange: (values: number) => void;
+ query: IBuilderQueryForm;
+}
+
+export default AggregateEveryFilter;
diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.intefaces.ts b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.intefaces.ts
new file mode 100644
index 0000000000..e8e4f072fb
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.intefaces.ts
@@ -0,0 +1,7 @@
+import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+
+export type AgregatorFilterProps = {
+ onChange: (value: BaseAutocompleteData) => void;
+ query: IBuilderQueryForm;
+};
diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx
new file mode 100644
index 0000000000..49945bf905
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx
@@ -0,0 +1,83 @@
+// ** Components
+import { AutoComplete, Spin } from 'antd';
+// ** Api
+import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
+import { initialAggregateAttribute } from 'constants/queryBuilder';
+import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
+import React, { memo, useMemo, useState } from 'react';
+import { useQuery } from 'react-query';
+import { SelectOption } from 'types/common/select';
+import { transformToUpperCase } from 'utils/transformToUpperCase';
+
+import { selectStyle } from '../QueryBuilderSearch/config';
+// ** Types
+import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
+
+export const AggregatorFilter = memo(function AggregatorFilter({
+ onChange,
+ query,
+}: AgregatorFilterProps): JSX.Element {
+ const [searchText, setSearchText] = useState('');
+
+ const { data, isFetching } = useQuery(
+ [
+ 'GET_AGGREGATE_ATTRIBUTE',
+ searchText,
+ query.aggregateOperator,
+ query.dataSource,
+ ],
+ async () =>
+ getAggregateAttribute({
+ searchText,
+ aggregateOperator: query.aggregateOperator,
+ dataSource: query.dataSource,
+ }),
+ { enabled: !!query.aggregateOperator && !!query.dataSource },
+ );
+
+ const handleSearchAttribute = (searchText: string): void =>
+ setSearchText(searchText);
+
+ const optionsData: SelectOption[] =
+ data?.payload?.attributeKeys?.map((item) => ({
+ label: transformStringWithPrefix({
+ str: item.key,
+ prefix: item.type || '',
+ condition: !item.isColumn,
+ }),
+ value: item.key,
+ })) || [];
+
+ const handleChangeAttribute = (value: string): void => {
+ const currentAttributeObj = data?.payload?.attributeKeys?.find(
+ (item) => item.key === value,
+ ) || { ...initialAggregateAttribute, key: value };
+
+ onChange(currentAttributeObj);
+ };
+
+ const value = useMemo(
+ () =>
+ transformStringWithPrefix({
+ str: query.aggregateAttribute.key,
+ prefix: query.aggregateAttribute.type || '',
+ condition: !query.aggregateAttribute.isColumn,
+ }),
+ [query],
+ );
+
+ return (
+ : null}
+ options={optionsData}
+ value={value}
+ onChange={handleChangeAttribute}
+ />
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/index.ts b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/index.ts
new file mode 100644
index 0000000000..2ec6993746
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/index.ts
@@ -0,0 +1 @@
+export { AggregatorFilter } from './AggregatorFilter';
diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.interfaces.ts b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.interfaces.ts
new file mode 100644
index 0000000000..24b82d855e
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.interfaces.ts
@@ -0,0 +1,15 @@
+import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+
+export type GroupByFilterProps = {
+ query: IBuilderQueryForm;
+ onChange: (values: BaseAutocompleteData[]) => void;
+};
+
+export type GroupByFilterValue = {
+ disabled: boolean | undefined;
+ key: string;
+ label: string;
+ title: string | undefined;
+ value: string;
+};
diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx
new file mode 100644
index 0000000000..f05735f072
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx
@@ -0,0 +1,107 @@
+import { Select, Spin } from 'antd';
+import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
+// ** Constants
+import { QueryBuilderKeys } from 'constants/queryBuilder';
+// ** Components
+// ** Helpers
+import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
+import React, { memo, useMemo, useState } from 'react';
+import { useQuery } from 'react-query';
+import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { MetricAggregateOperator } from 'types/common/queryBuilder';
+import { SelectOption } from 'types/common/select';
+
+import { selectStyle } from '../QueryBuilderSearch/config';
+import {
+ GroupByFilterProps,
+ GroupByFilterValue,
+} from './GroupByFilter.interfaces';
+
+export const GroupByFilter = memo(function GroupByFilter({
+ query,
+ onChange,
+}: GroupByFilterProps): JSX.Element {
+ const [searchText, setSearchText] = useState('');
+
+ const { data, isFetching } = useQuery(
+ [QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
+ async () =>
+ getAggregateKeys({
+ aggregateAttribute: query.aggregateAttribute.key,
+ tagType: query.aggregateAttribute.type,
+ dataSource: query.dataSource,
+ aggregateOperator: query.aggregateOperator,
+ searchText,
+ }),
+ { enabled: !!query.aggregateAttribute.key, keepPreviousData: true },
+ );
+
+ const handleSearchKeys = (searchText: string): void => {
+ setSearchText(searchText);
+ };
+
+ const optionsData: SelectOption[] =
+ data?.payload?.attributeKeys?.map((item) => ({
+ label: transformStringWithPrefix({
+ str: item.key,
+ prefix: item.type || '',
+ condition: !item.isColumn,
+ }),
+ value: item.key,
+ })) || [];
+
+ const handleChange = (values: GroupByFilterValue[]): void => {
+ const groupByValues: BaseAutocompleteData[] = values.map((item) => {
+ const iterationArray = data?.payload?.attributeKeys || query.groupBy;
+ const existGroup = iterationArray.find((group) => group.key === item.value);
+ if (existGroup) {
+ return existGroup;
+ }
+
+ return {
+ isColumn: null,
+ key: item.value,
+ dataType: null,
+ type: null,
+ };
+ });
+
+ onChange(groupByValues);
+ };
+
+ const values: GroupByFilterValue[] = query.groupBy.map((item) => ({
+ label: transformStringWithPrefix({
+ str: item.key,
+ prefix: item.type || '',
+ condition: !item.isColumn,
+ }),
+ key: item.key,
+ value: item.key,
+ disabled: undefined,
+ title: undefined,
+ }));
+
+ const isDisabledSelect = useMemo(
+ () =>
+ !query.aggregateAttribute.key ||
+ query.aggregateOperator === MetricAggregateOperator.NOOP,
+ [query.aggregateAttribute.key, query.aggregateOperator],
+ );
+
+ return (
+ : null}
+ onChange={handleChange}
+ />
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/index.ts b/frontend/src/container/QueryBuilder/filters/GroupByFilter/index.ts
new file mode 100644
index 0000000000..4377822177
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/index.ts
@@ -0,0 +1 @@
+export { GroupByFilter } from './GroupByFilter';
diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.interfaces.ts b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.interfaces.ts
new file mode 100644
index 0000000000..b670f7faad
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.interfaces.ts
@@ -0,0 +1,9 @@
+import {
+ Having,
+ IBuilderQueryForm,
+} from 'types/api/queryBuilder/queryBuilderData';
+
+export type HavingFilterProps = {
+ query: IBuilderQueryForm;
+ onChange: (having: Having[]) => void;
+};
diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx
new file mode 100644
index 0000000000..e00d98203a
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx
@@ -0,0 +1,196 @@
+import { Select } from 'antd';
+// ** Constants
+import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
+// ** Hooks
+import { useTagValidation } from 'hooks/queryBuilder/useTagValidation';
+import {
+ transformFromStringToHaving,
+ transformHavingToStringValue,
+} from 'lib/query/transformQueryBuilderData';
+// ** Helpers
+import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { Having } from 'types/api/queryBuilder/queryBuilderData';
+import { SelectOption } from 'types/common/select';
+
+// ** Types
+import { HavingFilterProps } from './HavingFilter.interfaces';
+
+const { Option } = Select;
+
+export function HavingFilter({
+ query,
+ onChange,
+}: HavingFilterProps): JSX.Element {
+ const { having } = query;
+ const [searchText, setSearchText] = useState('');
+ const [options, setOptions] = useState[]>([]);
+ const [localValues, setLocalValues] = useState([]);
+ const [currentFormValue, setCurrentFormValue] = useState(
+ initialHavingValues,
+ );
+
+ const { isMulti } = useTagValidation(
+ searchText,
+ currentFormValue.op,
+ currentFormValue.value,
+ );
+
+ const aggregatorAttribute = useMemo(
+ () =>
+ transformStringWithPrefix({
+ str: query.aggregateAttribute.key,
+ prefix: query.aggregateAttribute.type || '',
+ condition: !query.aggregateAttribute.isColumn,
+ }),
+ [query],
+ );
+
+ const columnName = useMemo(
+ () => `${query.aggregateOperator.toUpperCase()}(${aggregatorAttribute})`,
+ [query, aggregatorAttribute],
+ );
+
+ const aggregatorOptions: SelectOption[] = useMemo(
+ () => [{ label: columnName, value: columnName }],
+ [columnName],
+ );
+
+ const getHavingObject = useCallback((currentSearch: string): Having => {
+ const textArr = currentSearch.split(' ');
+ const [columnName = '', op = '', ...value] = textArr;
+
+ return { columnName, op, value };
+ }, []);
+
+ const generateOptions = useCallback(
+ (search: string): void => {
+ const [aggregator = '', op = '', ...restValue] = search.split(' ');
+ let newOptions: SelectOption[] = [];
+
+ const isAggregatorExist = columnName
+ .toLowerCase()
+ .includes(search.toLowerCase());
+
+ const isAggregatorChosen = aggregator === columnName;
+
+ if (isAggregatorExist || aggregator === '') {
+ newOptions = aggregatorOptions;
+ }
+
+ if ((isAggregatorChosen && op === '') || op) {
+ const filteredOperators = HAVING_OPERATORS.filter((num) =>
+ num.toLowerCase().includes(op.toLowerCase()),
+ );
+
+ newOptions = filteredOperators.map((opt) => ({
+ label: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
+ value: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
+ }));
+ }
+
+ setOptions(newOptions);
+ },
+ [columnName, aggregatorOptions],
+ );
+
+ const isValidHavingValue = (search: string): boolean => {
+ const values = getHavingObject(search).value.join(' ');
+ if (values) {
+ const numRegexp = /^[^a-zA-Z]*$/;
+
+ return numRegexp.test(values);
+ }
+
+ return true;
+ };
+
+ const handleSearch = (search: string): void => {
+ const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
+
+ const currentSearch = isMulti
+ ? trimmedSearch
+ : trimmedSearch.split(' ').slice(0, 3).join(' ');
+ const isValidSearch = isValidHavingValue(currentSearch);
+
+ if (isValidSearch) {
+ setSearchText(currentSearch);
+ }
+ };
+
+ const resetChanges = (): void => {
+ handleSearch('');
+ setCurrentFormValue(initialHavingValues);
+ setOptions(aggregatorOptions);
+ };
+
+ const handleChange = (values: string[]): void => {
+ const having: Having[] = values.map(transformFromStringToHaving);
+
+ const isSelectable: boolean =
+ currentFormValue.value.length > 0 &&
+ currentFormValue.value.every((value) => !!value);
+
+ if (isSelectable) {
+ onChange(having);
+ resetChanges();
+ }
+ };
+
+ const handleSelect = (currentValue: string): void => {
+ const { columnName, op, value } = getHavingObject(currentValue);
+
+ const isCompletedValue = value.every((item) => !!item);
+
+ const isClearSearch = isCompletedValue && columnName && op;
+
+ handleSearch(isClearSearch ? '' : currentValue);
+ };
+
+ const parseSearchText = useCallback(
+ (text: string) => {
+ const { columnName, op, value } = getHavingObject(text);
+ setCurrentFormValue({ columnName, op, value });
+
+ generateOptions(text);
+ },
+ [generateOptions, getHavingObject],
+ );
+
+ const handleDeselect = (value: string): void => {
+ const result = localValues.filter((item) => item !== value);
+ setLocalValues(result);
+ };
+
+ useEffect(() => {
+ parseSearchText(searchText);
+ }, [searchText, parseSearchText]);
+
+ useEffect(() => {
+ setLocalValues(transformHavingToStringValue(having));
+ }, [having]);
+
+ return (
+
+ {options.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx b/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx
new file mode 100644
index 0000000000..4cba84714a
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx
@@ -0,0 +1,166 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+// Constants
+import {
+ HAVING_OPERATORS,
+ initialQueryBuilderFormValues,
+} from 'constants/queryBuilder';
+import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData';
+import React from 'react';
+// ** Types
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+import { DataSource } from 'types/common/queryBuilder';
+
+// ** Components
+import { HavingFilter } from '../HavingFilter';
+
+const valueWithAttributeAndOperator: IBuilderQueryForm = {
+ ...initialQueryBuilderFormValues,
+ dataSource: DataSource.LOGS,
+ aggregateOperator: 'SUM',
+ aggregateAttribute: {
+ isColumn: false,
+ key: 'bytes',
+ type: 'tag',
+ dataType: 'float64',
+ },
+};
+
+describe('Having filter behaviour', () => {
+ test('Having filter render is rendered', () => {
+ const mockFn = jest.fn();
+ const { unmount } = render(
+ ,
+ );
+
+ const selectId = 'havingSelect';
+
+ const select = screen.getByTestId(selectId);
+
+ expect(select).toBeInTheDocument();
+
+ unmount();
+ });
+
+ test('Having render is disabled initially', () => {
+ const mockFn = jest.fn();
+ const { unmount } = render(
+ ,
+ );
+
+ const input = screen.getByRole('combobox');
+
+ expect(input).toBeDisabled();
+
+ unmount();
+ });
+
+ test('Is having filter is enable', () => {
+ const mockFn = jest.fn();
+ const { unmount } = render(
+ ,
+ );
+
+ const input = screen.getByRole('combobox');
+
+ expect(input).toBeEnabled();
+
+ unmount();
+ });
+
+ test('Autocomplete in the having filter', async () => {
+ const onChange = jest.fn();
+ const user = userEvent.setup();
+
+ const constructedAttribute = 'SUM(tag_bytes)';
+ const optionTestTitle = 'havingOption';
+
+ const { unmount } = render(
+ ,
+ );
+
+ // get input
+ const input = screen.getByRole('combobox');
+
+ // click on the select
+ await user.click(input);
+
+ // show predefined options for operator with attribute SUM(tag_bytes)
+ const option = screen.getByTitle(optionTestTitle);
+
+ expect(option).toBeInTheDocument();
+
+ await user.click(option);
+
+ // autocomplete input
+ expect(input).toHaveValue(constructedAttribute);
+
+ // clear value from input and write from keyboard
+ await user.clear(input);
+
+ await user.type(input, 'bytes');
+
+ // show again predefined options for operator with attribute SUM(tag_bytes)
+ const sameAttributeOption = screen.getByTitle(optionTestTitle);
+
+ expect(sameAttributeOption).toBeInTheDocument();
+
+ await user.click(sameAttributeOption);
+
+ expect(input).toHaveValue(constructedAttribute);
+
+ // show operators after SUM(tag_bytes)
+ const operatorsOptions = screen.getAllByTitle(optionTestTitle);
+
+ expect(operatorsOptions.length).toEqual(HAVING_OPERATORS.length);
+
+ // show operators after SUM(tag_bytes) when type from keyboard
+ await user.clear(input);
+
+ await user.type(input, `${constructedAttribute} !=`);
+
+ // get filtered operators
+ const filteredOperators = screen.getAllByTitle(optionTestTitle);
+
+ expect(filteredOperators.length).toEqual(1);
+
+ // clear and show again all operators
+ await user.clear(input);
+ await user.type(input, constructedAttribute);
+
+ const returnedOptions = screen.getAllByTitle(optionTestTitle);
+
+ expect(returnedOptions.length).toEqual(HAVING_OPERATORS.length);
+
+ // check write value after operator
+ await user.clear(input);
+ await user.type(input, `${constructedAttribute} != 123`);
+
+ expect(input).toHaveValue(`${constructedAttribute} != 123`);
+
+ const optionWithValue = screen.getByTitle(optionTestTitle);
+
+ // onChange after complete writting in the input or autocomplete
+ await user.click(optionWithValue);
+
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith([
+ transformFromStringToHaving(`${constructedAttribute} != 123`),
+ ]);
+
+ // onChange with multiple operator
+ await user.type(input, `${constructedAttribute} IN 123 123`);
+
+ expect(input).toHaveValue(`${constructedAttribute} IN 123 123`);
+
+ const optionWithMultipleValue = screen.getByTitle(optionTestTitle);
+ await user.click(optionWithMultipleValue);
+
+ expect(onChange).toHaveBeenCalledTimes(2);
+ expect(onChange).toHaveBeenCalledWith([
+ transformFromStringToHaving(`${constructedAttribute} IN 123 123`),
+ ]);
+
+ unmount();
+ });
+});
diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/index.ts b/frontend/src/container/QueryBuilder/filters/HavingFilter/index.ts
new file mode 100644
index 0000000000..5149764c01
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/index.ts
@@ -0,0 +1 @@
+export { HavingFilter } from './HavingFilter';
diff --git a/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx
new file mode 100644
index 0000000000..066f22911d
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx
@@ -0,0 +1,28 @@
+import { InputNumber } from 'antd';
+import React from 'react';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+
+import { selectStyle } from '../QueryBuilderSearch/config';
+
+function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
+ const onChangeHandler = (value: number | null): void => {
+ onChange(value);
+ };
+
+ return (
+
+ );
+}
+
+interface LimitFilterProps {
+ onChange: (values: number | null) => void;
+ query: IBuilderQueryForm;
+}
+
+export default LimitFilter;
diff --git a/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.interfaces.ts b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.interfaces.ts
new file mode 100644
index 0000000000..f47e813112
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.interfaces.ts
@@ -0,0 +1,7 @@
+import { SelectProps } from 'antd';
+
+export type OperatorsSelectProps = Omit & {
+ operators: string[];
+ onChange: (value: string) => void;
+ value: string;
+};
diff --git a/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.tsx b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.tsx
new file mode 100644
index 0000000000..718198fee9
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.tsx
@@ -0,0 +1,34 @@
+import { Select } from 'antd';
+import React, { memo } from 'react';
+// ** Types
+import { SelectOption } from 'types/common/select';
+// ** Helpers
+import { transformToUpperCase } from 'utils/transformToUpperCase';
+
+import { selectStyle } from '../QueryBuilderSearch/config';
+import { OperatorsSelectProps } from './OperatorsSelect.interfaces';
+
+export const OperatorsSelect = memo(function OperatorsSelect({
+ operators,
+ value,
+ onChange,
+ ...props
+}: OperatorsSelectProps): JSX.Element {
+ const operatorsOptions: SelectOption[] = operators.map(
+ (operator) => ({
+ label: transformToUpperCase(operator),
+ value: operator,
+ }),
+ );
+
+ return (
+
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/filters/OperatorsSelect/index.ts b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/index.ts
new file mode 100644
index 0000000000..a9e79513e7
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/index.ts
@@ -0,0 +1 @@
+export { OperatorsSelect } from './OperatorsSelect';
diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts
new file mode 100644
index 0000000000..b1b085a8de
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts
@@ -0,0 +1,15 @@
+import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+
+export type OrderByFilterProps = {
+ query: IBuilderQueryForm;
+ onChange: (values: BaseAutocompleteData[]) => void;
+};
+
+export type OrderByFilterValue = {
+ disabled: boolean | undefined;
+ key: string;
+ label: string;
+ title: string | undefined;
+ value: string;
+};
diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx
new file mode 100644
index 0000000000..e441c5b595
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx
@@ -0,0 +1,141 @@
+import { Select, Spin } from 'antd';
+import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
+import { QueryBuilderKeys } from 'constants/queryBuilder';
+import { IOption } from 'hooks/useResourceAttribute/types';
+import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
+import React, { useCallback, useMemo, useState } from 'react';
+import { useQuery } from 'react-query';
+import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+import { MetricAggregateOperator } from 'types/common/queryBuilder';
+
+import { selectStyle } from '../QueryBuilderSearch/config';
+import {
+ OrderByFilterProps,
+ OrderByFilterValue,
+} from './OrderByFilter.interfaces';
+import { getLabelFromValue, mapLabelValuePairs } from './utils';
+
+export function OrderByFilter({
+ query,
+ onChange,
+}: OrderByFilterProps): JSX.Element {
+ const [searchText, setSearchText] = useState('');
+ const [selectedValue, setSelectedValue] = useState([]);
+
+ const { data, isFetching } = useQuery(
+ [QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
+ async () =>
+ getAggregateKeys({
+ aggregateAttribute: query.aggregateAttribute.key,
+ tagType: query.aggregateAttribute.type,
+ dataSource: query.dataSource,
+ aggregateOperator: query.aggregateOperator,
+ searchText,
+ }),
+ { enabled: !!query.aggregateAttribute.key, keepPreviousData: true },
+ );
+
+ const handleSearchKeys = useCallback(
+ (searchText: string): void => setSearchText(searchText),
+ [],
+ );
+
+ const generateOptionsData = (
+ attributeKeys: BaseAutocompleteData[] | undefined,
+ selectedValue: OrderByFilterValue[],
+ query: IBuilderQueryForm,
+ ): IOption[] => {
+ const selectedValueLabels = getLabelFromValue(selectedValue);
+
+ const noAggregationOptions = attributeKeys
+ ? mapLabelValuePairs(attributeKeys)
+ .flat()
+ .filter(
+ (option) => !selectedValueLabels.includes(option.label.split(' ')[0]),
+ )
+ : [];
+
+ const aggregationOptions = mapLabelValuePairs(query.groupBy)
+ .flat()
+ .concat([
+ {
+ label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
+ value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
+ },
+ {
+ label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
+ value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
+ },
+ ])
+ .filter(
+ (option) => !selectedValueLabels.includes(option.label.split(' ')[0]),
+ );
+
+ return query.aggregateOperator === MetricAggregateOperator.NOOP
+ ? noAggregationOptions
+ : aggregationOptions;
+ };
+
+ const optionsData = useMemo(
+ () => generateOptionsData(data?.payload?.attributeKeys, selectedValue, query),
+ [data?.payload?.attributeKeys, query, selectedValue],
+ );
+
+ const handleChange = (values: OrderByFilterValue[]): void => {
+ setSelectedValue(values);
+ const orderByValues: BaseAutocompleteData[] = values?.map((item) => {
+ const iterationArray = data?.payload?.attributeKeys || query.orderBy;
+ const existingOrderValues = iterationArray.find(
+ (group) => group.key === item.value,
+ );
+ if (existingOrderValues) {
+ return existingOrderValues;
+ }
+
+ return {
+ isColumn: null,
+ key: item.value,
+ dataType: null,
+ type: null,
+ };
+ });
+ onChange(orderByValues);
+ };
+
+ const values: OrderByFilterValue[] = query.orderBy.map((item) => ({
+ label: transformStringWithPrefix({
+ str: item.key,
+ prefix: item.type || '',
+ condition: !item.isColumn,
+ }),
+ key: item.key,
+ value: item.key,
+ disabled: undefined,
+ title: undefined,
+ }));
+
+ const isDisabledSelect = useMemo(
+ () =>
+ !query.aggregateAttribute.key ||
+ query.aggregateOperator === MetricAggregateOperator.NOOP,
+ [query.aggregateAttribute.key, query.aggregateOperator],
+ );
+
+ return (
+ : null}
+ onChange={handleChange}
+ />
+ );
+}
diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/index.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/index.ts
new file mode 100644
index 0000000000..ece36ae2f6
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/index.ts
@@ -0,0 +1 @@
+export { OrderByFilter } from './OrderByFilter';
diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts
new file mode 100644
index 0000000000..e5b31df7bd
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts
@@ -0,0 +1,32 @@
+import { IOption } from 'hooks/useResourceAttribute/types';
+import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
+import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+
+import { OrderByFilterValue } from './OrderByFilter.interfaces';
+
+export function mapLabelValuePairs(
+ arr: BaseAutocompleteData[],
+): Array[] {
+ return arr.map((item) => {
+ const label = transformStringWithPrefix({
+ str: item.key,
+ prefix: item.type || '',
+ condition: !item.isColumn,
+ });
+ const value = item.key;
+ return [
+ {
+ label: `${label} asc`,
+ value: `${value} asc`,
+ },
+ {
+ label: `${label} desc`,
+ value: `${value} desc`,
+ },
+ ];
+ });
+}
+
+export function getLabelFromValue(arr: OrderByFilterValue[]): string[] {
+ return arr.map((value) => value.label.split(' ')[0]);
+}
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/config.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/config.ts
new file mode 100644
index 0000000000..652a15cca5
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/config.ts
@@ -0,0 +1 @@
+export const selectStyle = { width: '100%' };
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx
new file mode 100644
index 0000000000..56f7efe942
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx
@@ -0,0 +1,134 @@
+import { Select, Spin, Tag, Tooltip } from 'antd';
+import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
+import React, { useEffect, useMemo } from 'react';
+import {
+ IBuilderQueryForm,
+ TagFilter,
+} from 'types/api/queryBuilder/queryBuilderData';
+import { v4 as uuid } from 'uuid';
+
+import { selectStyle } from './config';
+import { StyledCheckOutlined, TypographyText } from './style';
+import { isInNotInOperator } from './utils';
+
+function QueryBuilderSearch({
+ query,
+ onChange,
+}: QueryBuilderSearchProps): JSX.Element {
+ const {
+ updateTag,
+ handleClearTag,
+ handleKeyDown,
+ handleSearch,
+ handleSelect,
+ tags,
+ options,
+ searchValue,
+ isMulti,
+ isFetching,
+ } = useAutoComplete(query);
+
+ const onTagRender = ({
+ value,
+ closable,
+ onClose,
+ }: CustomTagProps): React.ReactElement => {
+ const isInNin = isInNotInOperator(value);
+
+ const onCloseHandler = (): void => {
+ onClose();
+ handleSearch('');
+ };
+
+ const tagEditHandler = (value: string): void => {
+ updateTag(value);
+ handleSearch(value);
+ };
+
+ return (
+
+
+ tagEditHandler(value)}
+ >
+ {value}
+
+
+
+ );
+ };
+
+ const onChangeHandler = (value: string[]): void => {
+ if (!isMulti) handleSearch(value[value.length - 1]);
+ };
+
+ const onInputKeyDownHandler = (event: React.KeyboardEvent): void => {
+ if (isMulti || event.key === 'Backspace') handleKeyDown(event);
+ };
+
+ const queryTags = useMemo(() => {
+ if (!query.aggregateAttribute.key) return [];
+ return tags;
+ }, [query.aggregateAttribute.key, tags]);
+
+ useEffect(() => {
+ const initialTagFilters: TagFilter = { items: [], op: 'AND' };
+ initialTagFilters.items = tags.map((tag) => {
+ const [tagKey, tagOperator, ...tagValue] = tag.split(' ');
+ return {
+ id: uuid().slice(0, 8),
+ key: tagKey,
+ op: tagOperator,
+ value: tagValue.map((i) => i.replace(',', '')),
+ };
+ });
+ onChange(initialTagFilters);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [tags]);
+
+ return (
+ : null}
+ >
+ {options?.map((option) => (
+
+ {option.value}
+ {option.selected && }
+
+ ))}
+
+ );
+}
+
+interface QueryBuilderSearchProps {
+ query: IBuilderQueryForm;
+ onChange: (value: TagFilter) => void;
+}
+
+export interface CustomTagProps {
+ label: React.ReactNode;
+ value: string;
+ disabled: boolean;
+ onClose: (event?: React.MouseEvent) => void;
+ closable: boolean;
+}
+
+export default QueryBuilderSearch;
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts
new file mode 100644
index 0000000000..de72e4bf82
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts
@@ -0,0 +1,12 @@
+import { CheckOutlined } from '@ant-design/icons';
+import { Typography } from 'antd';
+import styled from 'styled-components';
+
+export const TypographyText = styled(Typography.Text)<{ $isInNin: boolean }>`
+ width: ${({ $isInNin }): string => ($isInNin ? '10rem' : 'auto')};
+ cursor: pointer;
+`;
+
+export const StyledCheckOutlined = styled(CheckOutlined)`
+ float: right;
+`;
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts
new file mode 100644
index 0000000000..7ff23055eb
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts
@@ -0,0 +1,9 @@
+import { OPERATORS } from 'constants/queryBuilder';
+
+export function isInNotInOperator(value: string): boolean {
+ return value?.includes(OPERATORS.IN || OPERATORS.NIN);
+}
+
+export function isExistsNotExistsOperator(value: string): boolean {
+ return value?.includes(OPERATORS.EXISTS || OPERATORS.NOT_EXISTS);
+}
diff --git a/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.interfaces.ts b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.interfaces.ts
new file mode 100644
index 0000000000..8f9ee7b05c
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.interfaces.ts
@@ -0,0 +1,7 @@
+import { SelectProps } from 'antd';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+
+export type ReduceToFilterProps = Omit & {
+ query: IBuilderQueryForm;
+ onChange: (value: string) => void;
+};
diff --git a/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx
new file mode 100644
index 0000000000..1e99bd24c0
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx
@@ -0,0 +1,26 @@
+import { Select } from 'antd';
+import React, { memo } from 'react';
+// ** Types
+import { EReduceOperator } from 'types/common/queryBuilder';
+import { SelectOption } from 'types/common/select';
+
+import { ReduceToFilterProps } from './ReduceToFilter.interfaces';
+
+export const ReduceToFilter = memo(function ReduceToFilter({
+ query,
+ onChange,
+}: ReduceToFilterProps): JSX.Element {
+ const options: SelectOption[] = Object.values(
+ EReduceOperator,
+ ).map((str) => ({ label: str, value: str }));
+
+ return (
+
+ );
+});
diff --git a/frontend/src/container/QueryBuilder/filters/ReduceToFilter/index.ts b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/index.ts
new file mode 100644
index 0000000000..6c7359fdd2
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/index.ts
@@ -0,0 +1 @@
+export { ReduceToFilter } from './ReduceToFilter';
diff --git a/frontend/src/container/QueryBuilder/filters/index.ts b/frontend/src/container/QueryBuilder/filters/index.ts
new file mode 100644
index 0000000000..f9dcba8eaf
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/filters/index.ts
@@ -0,0 +1,5 @@
+export { AggregatorFilter } from './AggregatorFilter';
+export { GroupByFilter } from './GroupByFilter';
+export { HavingFilter } from './HavingFilter';
+export { OperatorsSelect } from './OperatorsSelect';
+export { ReduceToFilter } from './ReduceToFilter';
diff --git a/frontend/src/container/QueryBuilder/type.ts b/frontend/src/container/QueryBuilder/type.ts
new file mode 100644
index 0000000000..85cfe42e84
--- /dev/null
+++ b/frontend/src/container/QueryBuilder/type.ts
@@ -0,0 +1,16 @@
+import { IQueryBuilderState } from 'constants/queryBuilder';
+
+export interface InitialStateI {
+ search: string;
+}
+
+export interface ContextValueI {
+ values: InitialStateI;
+ onChangeHandler: (type: IQueryBuilderState) => (value: string) => void;
+ onSubmitHandler: VoidFunction;
+}
+
+export type Option = {
+ value: string;
+ selected?: boolean;
+};
diff --git a/frontend/src/container/ResourceAttributesFilter/ResourceAttributesFilter.tsx b/frontend/src/container/ResourceAttributesFilter/ResourceAttributesFilter.tsx
new file mode 100644
index 0000000000..3c208b621d
--- /dev/null
+++ b/frontend/src/container/ResourceAttributesFilter/ResourceAttributesFilter.tsx
@@ -0,0 +1,88 @@
+import { CloseCircleFilled } from '@ant-design/icons';
+import { Button, Select, Spin } from 'antd';
+import useResourceAttribute, {
+ isResourceEmpty,
+} from 'hooks/useResourceAttribute';
+import { convertMetricKeyToTrace } from 'hooks/useResourceAttribute/utils';
+import React, { useMemo } from 'react';
+import { v4 as uuid } from 'uuid';
+
+import QueryChip from './components/QueryChip';
+import { QueryChipItem, SearchContainer } from './styles';
+
+function ResourceAttributesFilter({
+ suffixIcon,
+}: ResourceAttributesFilterProps): JSX.Element | null {
+ const {
+ queries,
+ staging,
+ handleClose,
+ handleBlur,
+ handleClearAll,
+ handleFocus,
+ handleChange,
+ selectedQuery,
+ optionsData,
+ loading,
+ } = useResourceAttribute();
+
+ const isEmpty = useMemo(
+ () => isResourceEmpty(queries, staging, selectedQuery),
+ [queries, selectedQuery, staging],
+ );
+
+ return (
+
+
+ {queries.map((query) => (
+
+ ))}
+ {staging.map((query, idx) => (
+
+ {idx === 0 ? convertMetricKeyToTrace(query) : query}
+
+ ))}
+
+
+ Loading...
+
+ ) : (
+
+ No resource attributes available to filter. Please refer docs to send
+ attributes.
+
+ )
+ }
+ />
+
+ {queries.length || staging.length || selectedQuery.length ? (
+ } type="text" />
+ ) : null}
+
+ );
+}
+
+interface ResourceAttributesFilterProps {
+ suffixIcon?: React.ReactNode;
+}
+
+ResourceAttributesFilter.defaultProps = {
+ suffixIcon: undefined,
+};
+
+export default ResourceAttributesFilter;
diff --git a/frontend/src/container/ResourceAttributesFilter/components/QueryChip/QueryChip.tsx b/frontend/src/container/ResourceAttributesFilter/components/QueryChip/QueryChip.tsx
new file mode 100644
index 0000000000..fd912e3ade
--- /dev/null
+++ b/frontend/src/container/ResourceAttributesFilter/components/QueryChip/QueryChip.tsx
@@ -0,0 +1,23 @@
+import { convertMetricKeyToTrace } from 'hooks/useResourceAttribute/utils';
+import React from 'react';
+
+import { QueryChipContainer, QueryChipItem } from '../../styles';
+import { IQueryChipProps } from './types';
+
+function QueryChip({ queryData, onClose }: IQueryChipProps): JSX.Element {
+ const onCloseHandler = (): void => {
+ onClose(queryData.id);
+ };
+
+ return (
+
+ {convertMetricKeyToTrace(queryData.tagKey)}
+ {queryData.operator}
+
+ {queryData.tagValue.join(', ')}
+
+
+ );
+}
+
+export default QueryChip;
diff --git a/frontend/src/container/ResourceAttributesFilter/components/QueryChip/index.ts b/frontend/src/container/ResourceAttributesFilter/components/QueryChip/index.ts
new file mode 100644
index 0000000000..6f3ad1e383
--- /dev/null
+++ b/frontend/src/container/ResourceAttributesFilter/components/QueryChip/index.ts
@@ -0,0 +1,3 @@
+import QueryChip from './QueryChip';
+
+export default QueryChip;
diff --git a/frontend/src/container/ResourceAttributesFilter/components/QueryChip/types.ts b/frontend/src/container/ResourceAttributesFilter/components/QueryChip/types.ts
new file mode 100644
index 0000000000..98652828c4
--- /dev/null
+++ b/frontend/src/container/ResourceAttributesFilter/components/QueryChip/types.ts
@@ -0,0 +1,6 @@
+import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
+
+export interface IQueryChipProps {
+ queryData: IResourceAttribute;
+ onClose: (id: string) => void;
+}
diff --git a/frontend/src/container/ResourceAttributesFilter/index.ts b/frontend/src/container/ResourceAttributesFilter/index.ts
new file mode 100644
index 0000000000..d5f48f2ad9
--- /dev/null
+++ b/frontend/src/container/ResourceAttributesFilter/index.ts
@@ -0,0 +1,3 @@
+import ResourceAttributesFilter from './ResourceAttributesFilter';
+
+export default ResourceAttributesFilter;
diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/styles.ts b/frontend/src/container/ResourceAttributesFilter/styles.ts
similarity index 77%
rename from frontend/src/container/MetricsApplication/ResourceAttributesFilter/styles.ts
rename to frontend/src/container/ResourceAttributesFilter/styles.ts
index 4d6a1b6d77..c1dcd863f2 100644
--- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/styles.ts
+++ b/frontend/src/container/ResourceAttributesFilter/styles.ts
@@ -2,9 +2,7 @@ import { grey } from '@ant-design/colors';
import { Tag } from 'antd';
import styled from 'styled-components';
-export const SearchContainer = styled.div<{
- disabled: boolean;
-}>`
+export const SearchContainer = styled.div`
width: 100%;
display: flex;
align-items: center;
@@ -12,8 +10,8 @@ export const SearchContainer = styled.div<{
padding: 0.2rem;
margin: 1rem 0;
border: 1px solid #ccc5;
- ${({ disabled }): string => (disabled ? `cursor: not-allowed;` : '')}
`;
+
export const QueryChipContainer = styled.span`
display: flex;
align-items: center;
diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts
index 87cd2cff60..f14da541c7 100644
--- a/frontend/src/container/SideNav/config.ts
+++ b/frontend/src/container/SideNav/config.ts
@@ -1 +1,38 @@
+import { QueryParams } from 'constants/query';
+import ROUTES from 'constants/routes';
+
export const styles = { background: '#1f1f1f' };
+
+export const routeConfig: Record = {
+ [ROUTES.SERVICE_METRICS]: [QueryParams.resourceAttributes],
+ [ROUTES.SERVICE_MAP]: [QueryParams.resourceAttributes],
+ [ROUTES.ALL_ERROR]: [QueryParams.resourceAttributes],
+ [ROUTES.ALERTS_NEW]: [QueryParams.resourceAttributes],
+ [ROUTES.ALL_CHANNELS]: [QueryParams.resourceAttributes],
+ [ROUTES.ALL_DASHBOARD]: [QueryParams.resourceAttributes],
+ [ROUTES.APPLICATION]: [QueryParams.resourceAttributes],
+ [ROUTES.CHANNELS_EDIT]: [QueryParams.resourceAttributes],
+ [ROUTES.CHANNELS_NEW]: [QueryParams.resourceAttributes],
+ [ROUTES.DASHBOARD]: [QueryParams.resourceAttributes],
+ [ROUTES.DASHBOARD_WIDGET]: [QueryParams.resourceAttributes],
+ [ROUTES.EDIT_ALERTS]: [QueryParams.resourceAttributes],
+ [ROUTES.ERROR_DETAIL]: [QueryParams.resourceAttributes],
+ [ROUTES.HOME_PAGE]: [QueryParams.resourceAttributes],
+ [ROUTES.INSTRUMENTATION]: [QueryParams.resourceAttributes],
+ [ROUTES.LIST_ALL_ALERT]: [QueryParams.resourceAttributes],
+ [ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes],
+ [ROUTES.LOGIN]: [QueryParams.resourceAttributes],
+ [ROUTES.LOGS]: [QueryParams.resourceAttributes],
+ [ROUTES.MY_SETTINGS]: [QueryParams.resourceAttributes],
+ [ROUTES.NOT_FOUND]: [QueryParams.resourceAttributes],
+ [ROUTES.ORG_SETTINGS]: [QueryParams.resourceAttributes],
+ [ROUTES.PASSWORD_RESET]: [QueryParams.resourceAttributes],
+ [ROUTES.SETTINGS]: [QueryParams.resourceAttributes],
+ [ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
+ [ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
+ [ROUTES.TRACE]: [QueryParams.resourceAttributes],
+ [ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
+ [ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],
+ [ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes],
+ [ROUTES.VERSION]: [QueryParams.resourceAttributes],
+};
diff --git a/frontend/src/container/SideNav/helper.test.ts b/frontend/src/container/SideNav/helper.test.ts
new file mode 100644
index 0000000000..ada551242e
--- /dev/null
+++ b/frontend/src/container/SideNav/helper.test.ts
@@ -0,0 +1,34 @@
+import { getQueryString } from './helper';
+
+describe('getQueryString', () => {
+ it('returns an array of query strings for given available parameters and URLSearchParams', () => {
+ const availableParams = ['param1', 'param2', 'param3'];
+ const params = new URLSearchParams(
+ 'param1=value1¶m2=value2¶m4=value4',
+ );
+
+ const result = getQueryString(availableParams, params);
+
+ expect(result).toEqual(['param1=value1', 'param2=value2', '']);
+ });
+
+ it('returns an array of empty strings if no matching parameters are found', () => {
+ const availableParams = ['param1', 'param2', 'param3'];
+ const params = new URLSearchParams('param4=value4¶m5=value5');
+
+ const result = getQueryString(availableParams, params);
+
+ expect(result).toEqual(['', '', '']);
+ });
+
+ it('returns an empty array if the available parameters list is empty', () => {
+ const availableParams: string[] = [];
+ const params = new URLSearchParams(
+ 'param1=value1¶m2=value2¶m3=value3',
+ );
+
+ const result = getQueryString(availableParams, params);
+
+ expect(result).toEqual([]);
+ });
+});
diff --git a/frontend/src/container/SideNav/helper.ts b/frontend/src/container/SideNav/helper.ts
new file mode 100644
index 0000000000..6160702336
--- /dev/null
+++ b/frontend/src/container/SideNav/helper.ts
@@ -0,0 +1,10 @@
+export const getQueryString = (
+ avialableParams: string[],
+ params: URLSearchParams,
+): string[] =>
+ avialableParams.map((param) => {
+ if (params.has(param)) {
+ return `${param}=${params.get(param)}`;
+ }
+ return '';
+ });
diff --git a/frontend/src/container/SideNav/index.tsx b/frontend/src/container/SideNav/index.tsx
index 24e888797c..d0e7fced95 100644
--- a/frontend/src/container/SideNav/index.tsx
+++ b/frontend/src/container/SideNav/index.tsx
@@ -12,7 +12,8 @@ import { SideBarCollapse } from 'store/actions/app';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
-import { styles } from './config';
+import { routeConfig, styles } from './config';
+import { getQueryString } from './helper';
import menus from './menuItems';
import Slack from './Slack';
import {
@@ -34,7 +35,7 @@ function SideNav(): JSX.Element {
AppReducer
>((state) => state.app);
- const { pathname } = useLocation();
+ const { pathname, search } = useLocation();
const { t } = useTranslation('');
const onCollapse = useCallback(() => {
@@ -47,11 +48,16 @@ function SideNav(): JSX.Element {
const onClickHandler = useCallback(
(to: string) => {
+ const params = new URLSearchParams(search);
+ const avialableParams = routeConfig[to];
+
+ const queryString = getQueryString(avialableParams, params);
+
if (pathname !== to) {
- history.push(to);
+ history.push(`${to}?${queryString.join('&')}`);
}
},
- [pathname],
+ [pathname, search],
);
const onClickSlackHandler = (): void => {
diff --git a/frontend/src/container/SideNav/menuItems.ts b/frontend/src/container/SideNav/menuItems.ts
index 5be9c9b9a1..2724878ffc 100644
--- a/frontend/src/container/SideNav/menuItems.ts
+++ b/frontend/src/container/SideNav/menuItems.ts
@@ -29,12 +29,12 @@ const menus: SidebarMenu[] = [
to: ROUTES.LOGS,
name: 'Logs',
// tags: ['Beta'],
- children: [
- {
- key: ROUTES.LOGS,
- label: 'Search',
- },
- ],
+ // children: [
+ // {
+ // key: ROUTES.LOGS,
+ // label: 'Search',
+ // },
+ // ],
},
{
Icon: DashboardFilled,
diff --git a/frontend/src/container/TopNav/CustomDateTimeModal/CustomDateTimeModal.test.tsx b/frontend/src/container/TopNav/CustomDateTimeModal/CustomDateTimeModal.test.tsx
new file mode 100644
index 0000000000..719de038c3
--- /dev/null
+++ b/frontend/src/container/TopNav/CustomDateTimeModal/CustomDateTimeModal.test.tsx
@@ -0,0 +1,42 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+
+import CustomDateTimeModal from './index';
+
+describe('CustomDateTimeModal', () => {
+ const handleCreate = jest.fn();
+ const handleCancel = jest.fn();
+
+ beforeEach(() => {
+ render(
+ ,
+ );
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the modal with title and buttons', () => {
+ expect(screen.getByText('Chose date and time range')).toBeInTheDocument();
+ expect(screen.getByText('Apply')).toBeInTheDocument();
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
+ });
+
+ it('donot calls onCreate when the Apply button is clicked without selecting dates', () => {
+ fireEvent.click(screen.getByText('Apply'));
+
+ expect(handleCreate).toHaveBeenCalledTimes(0);
+ expect(handleCreate).not.toHaveBeenCalledWith(undefined);
+ });
+
+ it('calls onCancel when Cancel button is clicked', () => {
+ fireEvent.click(screen.getByText('Cancel'));
+
+ expect(handleCancel).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/frontend/src/container/TopNav/CustomDateTimeModal/index.tsx b/frontend/src/container/TopNav/CustomDateTimeModal/index.tsx
index e2e8ad428c..2682815a74 100644
--- a/frontend/src/container/TopNav/CustomDateTimeModal/index.tsx
+++ b/frontend/src/container/TopNav/CustomDateTimeModal/index.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-no-bind */
import { DatePicker, Modal } from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import React, { useState } from 'react';
@@ -12,18 +11,21 @@ function CustomDateTimeModal({
onCreate,
onCancel,
}: CustomDateTimeModalProps): JSX.Element {
- const [customDateTimeRange, setCustomDateTimeRange] = useState();
+ const [selectedDate, setDateTime] = useState();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function handleRangePickerOk(date_time: any): void {
- setCustomDateTimeRange(date_time);
- }
+ const onModalOkHandler = (date_time: any): void => {
+ onCreate(date_time);
+ setDateTime(date_time);
+ };
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function disabledDate(current: any): boolean {
+ const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
- }
+ };
+
+ const onOk = (): void => {
+ if (selectedDate) onCreate(selectedDate);
+ };
return (
onCreate(customDateTimeRange || null)}
+ onOk={onOk}
>
);
diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx b/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx
index 1524d4060a..0de6229661 100644
--- a/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx
+++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx
@@ -178,7 +178,7 @@ function Duration(): JSX.Element {
if (value === undefined) {
return
;
}
- return {`${getMs(value?.toString())}ms`}
;
+ return {`${value?.toString()}ms`}
;
}, []);
return (
diff --git a/frontend/src/container/Trace/Search/index.tsx b/frontend/src/container/Trace/Search/index.tsx
index d9b4741082..099059ac80 100644
--- a/frontend/src/container/Trace/Search/index.tsx
+++ b/frontend/src/container/Trace/Search/index.tsx
@@ -27,7 +27,7 @@ function Search({
const dispatch = useDispatch>();
useEffect(() => {
- if (traces.filterLoading) {
+ if (!traces.filterLoading) {
const initialTags = parseTagsToQuery(traces.selectedTags);
if (!initialTags.isError) {
setValue(initialTags.payload);
diff --git a/frontend/src/hooks/queryBuilder/useAutoComplete.ts b/frontend/src/hooks/queryBuilder/useAutoComplete.ts
new file mode 100644
index 0000000000..4c48106918
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useAutoComplete.ts
@@ -0,0 +1,133 @@
+import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
+import { Option } from 'container/QueryBuilder/type';
+import { useCallback, useState } from 'react';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
+
+import { useFetchKeysAndValues } from './useFetchKeysAndValues';
+import { useOptions } from './useOptions';
+import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
+import { useTag } from './useTag';
+import { useTagValidation } from './useTagValidation';
+
+interface IAutoComplete {
+ updateTag: (value: string) => void;
+ handleSearch: (value: string) => void;
+ handleClearTag: (value: string) => void;
+ handleSelect: (value: string) => void;
+ handleKeyDown: (event: React.KeyboardEvent) => void;
+ options: Option[];
+ tags: string[];
+ searchValue: string;
+ isMulti: boolean;
+ isFetching: boolean;
+}
+
+export const useAutoComplete = (query: IBuilderQueryForm): IAutoComplete => {
+ const [searchValue, setSearchValue] = useState('');
+
+ const handleSearch = (value: string): void => setSearchValue(value);
+
+ const { keys, results, isFetching } = useFetchKeysAndValues(
+ searchValue,
+ query,
+ );
+
+ const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
+
+ const {
+ isValidTag,
+ isExist,
+ isValidOperator,
+ isMulti,
+ isFreeText,
+ } = useTagValidation(searchValue, operator, result);
+
+ const { handleAddTag, handleClearTag, tags, updateTag } = useTag(
+ key,
+ isValidTag,
+ isFreeText,
+ handleSearch,
+ );
+
+ const handleSelect = useCallback(
+ (value: string): void => {
+ if (isMulti) {
+ setSearchValue((prev: string) => {
+ if (prev.includes(value)) {
+ return prev.replace(` ${value}`, '');
+ }
+ return checkStringEndsWithSpace(prev)
+ ? `${prev} ${value}`
+ : `${prev} ${value},`;
+ });
+ }
+ if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) {
+ handleAddTag(value);
+ }
+ if (!isMulti && isExistsNotExistsOperator(value)) {
+ handleAddTag(value);
+ }
+ },
+ [handleAddTag, isMulti, isValidTag],
+ );
+
+ const handleKeyDown = useCallback(
+ (event: React.KeyboardEvent): void => {
+ if (
+ event.key === ' ' &&
+ (searchValue.endsWith(' ') || searchValue.length === 0)
+ ) {
+ event.preventDefault();
+ }
+
+ if (event.key === 'Enter' && searchValue && isValidTag) {
+ if (isMulti || isFreeText) {
+ event.stopPropagation();
+ }
+ event.preventDefault();
+ handleAddTag(searchValue);
+ }
+
+ if (event.key === 'Backspace' && !searchValue) {
+ event.stopPropagation();
+ const last = tags[tags.length - 1];
+ handleClearTag(last);
+ }
+ },
+ [
+ handleAddTag,
+ handleClearTag,
+ isFreeText,
+ isMulti,
+ isValidTag,
+ searchValue,
+ tags,
+ ],
+ );
+
+ const options = useOptions(
+ key,
+ keys,
+ operator,
+ searchValue,
+ isMulti,
+ isValidOperator,
+ isExist,
+ results,
+ result,
+ );
+
+ return {
+ updateTag,
+ handleSearch,
+ handleClearTag,
+ handleSelect,
+ handleKeyDown,
+ options,
+ tags,
+ searchValue,
+ isMulti,
+ isFetching,
+ };
+};
diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts
new file mode 100644
index 0000000000..3fd04181dc
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts
@@ -0,0 +1,106 @@
+import {
+ AttributeKeyOptions,
+ getAttributesKeys,
+ getAttributesValues,
+} from 'api/queryBuilder/getAttributesKeysValues';
+import { useEffect, useRef, useState } from 'react';
+import { useQuery } from 'react-query';
+import { useDebounce } from 'react-use';
+import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
+import { separateSearchValue } from 'utils/separateSearchValue';
+
+type UseFetchKeysAndValuesReturnValues = {
+ keys: AttributeKeyOptions[];
+ results: string[];
+ isFetching: boolean;
+};
+
+/**
+ * Custom hook to fetch attribute keys and values from an API
+ * @param searchValue - the search query value
+ * @param query - an object containing data for the query
+ * @returns an object containing the fetched attribute keys, results, and the status of the fetch
+ */
+
+export const useFetchKeysAndValues = (
+ searchValue: string,
+ query: IBuilderQueryForm,
+): UseFetchKeysAndValuesReturnValues => {
+ const [keys, setKeys] = useState([]);
+ const [results, setResults] = useState([]);
+ const { data, isFetching, status } = useQuery(
+ [
+ 'GET_ATTRIBUTE_KEY',
+ searchValue,
+ query.dataSource,
+ query.aggregateOperator,
+ query.aggregateAttribute.key,
+ ],
+ async () =>
+ getAttributesKeys({
+ searchText: searchValue,
+ dataSource: query.dataSource,
+ aggregateOperator: query.aggregateOperator,
+ aggregateAttribute: query.aggregateAttribute.key,
+ }),
+ { enabled: !!query.aggregateOperator && !!query.dataSource },
+ );
+
+ /**
+ * Fetches the options to be displayed based on the selected value
+ * @param value - the selected value
+ * @param query - an object containing data for the query
+ */
+ const handleFetchOption = async (
+ value: string,
+ query: IBuilderQueryForm,
+ ): Promise => {
+ if (value) {
+ // separate the search value into the attribute key and the operator
+ const [tKey, operator] = separateSearchValue(value);
+ setResults([]);
+ if (tKey && operator) {
+ const { payload } = await getAttributesValues({
+ searchText: searchValue,
+ dataSource: query.dataSource,
+ aggregateOperator: query.aggregateOperator,
+ aggregateAttribute: query.aggregateAttribute.key,
+ attributeKey: tKey,
+ });
+ if (payload) {
+ const values = Object.values(payload).find((el) => !!el);
+ if (values) {
+ setResults(values);
+ } else {
+ setResults([]);
+ }
+ }
+ }
+ }
+ };
+
+ // creates a ref to the fetch function so that it doesn't change on every render
+ const clearFetcher = useRef(handleFetchOption).current;
+
+ // debounces the fetch function to avoid excessive API calls
+ useDebounce(() => clearFetcher(searchValue, query), 500, [
+ clearFetcher,
+ searchValue,
+ query,
+ ]);
+
+ // update the fetched keys when the fetch status changes
+ useEffect(() => {
+ if (status === 'success' && data?.payload) {
+ setKeys(data?.payload);
+ } else {
+ setKeys([]);
+ }
+ }, [data?.payload, status]);
+
+ return {
+ keys,
+ results,
+ isFetching,
+ };
+};
diff --git a/frontend/src/hooks/queryBuilder/useIsValidTag.ts b/frontend/src/hooks/queryBuilder/useIsValidTag.ts
new file mode 100644
index 0000000000..216971b0ac
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useIsValidTag.ts
@@ -0,0 +1,22 @@
+import { useMemo } from 'react';
+
+import { OperatorType } from './useOperatorType';
+
+const validationMapper: Record<
+ OperatorType,
+ (resultLength: number) => boolean
+> = {
+ SINGLE_VALUE: (resultLength: number) => resultLength === 1,
+ MULTIPLY_VALUE: (resultLength: number) => resultLength >= 1,
+ NON_VALUE: (resultLength: number) => resultLength === 0,
+ NOT_VALID: () => false,
+};
+
+export const useIsValidTag = (
+ operatorType: OperatorType,
+ resultLength: number,
+): boolean =>
+ useMemo(() => validationMapper[operatorType]?.(resultLength), [
+ operatorType,
+ resultLength,
+ ]);
diff --git a/frontend/src/hooks/queryBuilder/useOperatorType.ts b/frontend/src/hooks/queryBuilder/useOperatorType.ts
new file mode 100644
index 0000000000..a387708301
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useOperatorType.ts
@@ -0,0 +1,27 @@
+import { OPERATORS } from 'constants/queryBuilder';
+
+export type OperatorType =
+ | 'SINGLE_VALUE'
+ | 'MULTIPLY_VALUE'
+ | 'NON_VALUE'
+ | 'NOT_VALID';
+
+const operatorTypeMapper: Record = {
+ [OPERATORS.IN]: 'MULTIPLY_VALUE',
+ [OPERATORS.NIN]: 'MULTIPLY_VALUE',
+ [OPERATORS.EXISTS]: 'NON_VALUE',
+ [OPERATORS.NOT_EXISTS]: 'NON_VALUE',
+ [OPERATORS.LTE]: 'SINGLE_VALUE',
+ [OPERATORS.LT]: 'SINGLE_VALUE',
+ [OPERATORS.GTE]: 'SINGLE_VALUE',
+ [OPERATORS.GT]: 'SINGLE_VALUE',
+ [OPERATORS.LIKE]: 'SINGLE_VALUE',
+ [OPERATORS.NLIKE]: 'SINGLE_VALUE',
+ [OPERATORS.CONTAINS]: 'SINGLE_VALUE',
+ [OPERATORS.NOT_CONTAINS]: 'SINGLE_VALUE',
+ [OPERATORS.EQUALS]: 'SINGLE_VALUE',
+ [OPERATORS.NOT_EQUALS]: 'SINGLE_VALUE',
+};
+
+export const useOperatorType = (operator: string): OperatorType =>
+ operatorTypeMapper[operator] || 'NOT_VALID';
diff --git a/frontend/src/hooks/queryBuilder/useOperators.ts b/frontend/src/hooks/queryBuilder/useOperators.ts
new file mode 100644
index 0000000000..b56fed1dee
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useOperators.ts
@@ -0,0 +1,20 @@
+import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
+import { QUERY_BUILDER_OPERATORS_BY_TYPES } from 'constants/queryBuilder';
+import { useMemo } from 'react';
+
+type IOperators =
+ | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.universal
+ | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.string
+ | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.boolean
+ | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.number;
+
+export const useOperators = (
+ key: string,
+ keys: AttributeKeyOptions[],
+): IOperators =>
+ useMemo(() => {
+ const currentKey = keys?.find((el) => el.key === key);
+ return currentKey
+ ? QUERY_BUILDER_OPERATORS_BY_TYPES[currentKey.dataType]
+ : QUERY_BUILDER_OPERATORS_BY_TYPES.universal;
+ }, [keys, key]);
diff --git a/frontend/src/hooks/queryBuilder/useOptions.ts b/frontend/src/hooks/queryBuilder/useOptions.ts
new file mode 100644
index 0000000000..537eebafd7
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useOptions.ts
@@ -0,0 +1,78 @@
+import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
+import { Option } from 'container/QueryBuilder/type';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+import { useOperators } from './useOperators';
+
+export const useOptions = (
+ key: string,
+ keys: AttributeKeyOptions[],
+ operator: string,
+ searchValue: string,
+ isMulti: boolean,
+ isValidOperator: boolean,
+ isExist: boolean,
+ results: string[],
+ result: string[],
+): Option[] => {
+ const [options, setOptions] = useState([]);
+ const operators = useOperators(key, keys);
+
+ const updateOptions = useCallback(() => {
+ if (!key) {
+ setOptions(
+ searchValue
+ ? [{ value: searchValue }, ...keys.map((k) => ({ value: k.key }))]
+ : keys?.map((k) => ({ value: k.key })),
+ );
+ } else if (key && !operator) {
+ setOptions(
+ operators?.map((o) => ({
+ value: `${key} ${o}`,
+ label: `${key} ${o.replace('_', ' ')}`,
+ })),
+ );
+ } else if (key && operator) {
+ if (isMulti) {
+ setOptions(results.map((r) => ({ value: `${r}` })));
+ } else if (isExist) {
+ setOptions([]);
+ } else if (isValidOperator) {
+ const hasAllResults = result.every((val) => results.includes(val));
+ const values = results.map((r) => ({
+ value: `${key} ${operator} ${r}`,
+ }));
+ const options = hasAllResults
+ ? values
+ : [{ value: searchValue }, ...values];
+ setOptions(options);
+ }
+ }
+ }, [
+ isExist,
+ isMulti,
+ isValidOperator,
+ key,
+ keys,
+ operator,
+ operators,
+ result,
+ results,
+ searchValue,
+ ]);
+
+ useEffect(() => {
+ updateOptions();
+ }, [updateOptions]);
+
+ return useMemo(
+ () =>
+ options?.map((option) => {
+ if (isMulti) {
+ return { ...option, selected: searchValue.includes(option.value) };
+ }
+ return option;
+ }),
+ [isMulti, options, searchValue],
+ );
+};
diff --git a/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts b/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts
new file mode 100644
index 0000000000..cd1e7cec2f
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts
@@ -0,0 +1,32 @@
+import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
+import { useMemo } from 'react';
+import { getCountOfSpace } from 'utils/getCountOfSpace';
+import { separateSearchValue } from 'utils/separateSearchValue';
+
+type ICurrentKeyAndOperator = [string, string, string[]];
+
+export const useSetCurrentKeyAndOperator = (
+ value: string,
+ keys: AttributeKeyOptions[],
+): ICurrentKeyAndOperator => {
+ const [key, operator, result] = useMemo(() => {
+ let key = '';
+ let operator = '';
+ let result: string[] = [];
+
+ if (value) {
+ const [tKey, tOperator, tResult] = separateSearchValue(value);
+ const isSuggestKey = keys?.some((el) => el.key === tKey);
+
+ if (getCountOfSpace(value) >= 1 || isSuggestKey) {
+ key = tKey || '';
+ operator = tOperator || '';
+ result = tResult.filter((el) => el);
+ }
+ }
+
+ return [key, operator, result];
+ }, [value, keys]);
+
+ return [key, operator, result];
+};
diff --git a/frontend/src/hooks/queryBuilder/useTag.ts b/frontend/src/hooks/queryBuilder/useTag.ts
new file mode 100644
index 0000000000..edd6c036ab
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useTag.ts
@@ -0,0 +1,59 @@
+import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
+import { useCallback, useState } from 'react';
+
+type IUseTag = {
+ handleAddTag: (value: string) => void;
+ handleClearTag: (value: string) => void;
+ tags: string[];
+ updateTag: (value: string) => void;
+};
+
+/**
+ * A custom React hook for handling tags.
+ * @param {string} key - A string value to identify tags.
+ * @param {boolean} isValidTag - A boolean value to indicate whether the tag is valid.
+ * @param {boolean} isFreeText - A boolean value to indicate whether free text is allowed.
+ * @param {function} handleSearch - A callback function to handle search.
+ * @returns {IUseTag} The return object containing handlers and tags.
+ */
+export const useTag = (
+ key: string,
+ isValidTag: boolean,
+ isFreeText: boolean,
+ handleSearch: (value: string) => void,
+): IUseTag => {
+ const [tags, setTags] = useState([]);
+
+ const updateTag = (value: string): void => {
+ const newTags = tags?.filter((item: string) => item !== value);
+ setTags(newTags);
+ };
+
+ /**
+ * Adds a new tag to the tag list.
+ * @param {string} value - The tag value to be added.
+ */
+ const handleAddTag = useCallback(
+ (value: string): void => {
+ if (
+ (value && key && isValidTag) ||
+ isFreeText ||
+ isExistsNotExistsOperator(value)
+ ) {
+ setTags((prevTags) => [...prevTags, value]);
+ handleSearch('');
+ }
+ },
+ [key, isValidTag, isFreeText, handleSearch],
+ );
+
+ /**
+ * Removes a tag from the tag list.
+ * @param {string} value - The tag value to be removed.
+ */
+ const handleClearTag = useCallback((value: string): void => {
+ setTags((prevTags) => prevTags.filter((v) => v !== value));
+ }, []);
+
+ return { handleAddTag, handleClearTag, tags, updateTag };
+};
diff --git a/frontend/src/hooks/queryBuilder/useTagValidation.ts b/frontend/src/hooks/queryBuilder/useTagValidation.ts
new file mode 100644
index 0000000000..fd3932b93a
--- /dev/null
+++ b/frontend/src/hooks/queryBuilder/useTagValidation.ts
@@ -0,0 +1,35 @@
+import { QUERY_BUILDER_SEARCH_VALUES } from 'constants/queryBuilder';
+import { useMemo } from 'react';
+import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
+
+import { useIsValidTag } from './useIsValidTag';
+import { useOperatorType } from './useOperatorType';
+
+type ITagValidation = {
+ isValidTag: boolean;
+ isExist: boolean;
+ isValidOperator: boolean;
+ isMulti: boolean;
+ isFreeText: boolean;
+};
+
+export const useTagValidation = (
+ value: string,
+ operator: string,
+ result: string[],
+): ITagValidation => {
+ const operatorType = useOperatorType(operator);
+ const isValidTag = useIsValidTag(operatorType, result.length);
+
+ const { isExist, isValidOperator, isMulti, isFreeText } = useMemo(() => {
+ const isExist = operatorType === QUERY_BUILDER_SEARCH_VALUES.NON;
+ const isValidOperator =
+ operatorType !== QUERY_BUILDER_SEARCH_VALUES.NOT_VALID;
+ const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
+ const isFreeText = operator === '' && !checkStringEndsWithSpace(value);
+
+ return { isExist, isValidOperator, isMulti, isFreeText };
+ }, [operator, operatorType, value]);
+
+ return { isValidTag, isExist, isValidOperator, isMulti, isFreeText };
+};
diff --git a/frontend/src/hooks/useQueryBuilder.ts b/frontend/src/hooks/useQueryBuilder.ts
index d51fdb8c35..262e20c137 100644
--- a/frontend/src/hooks/useQueryBuilder.ts
+++ b/frontend/src/hooks/useQueryBuilder.ts
@@ -1,8 +1,6 @@
-import {
- QueryBuilderContext,
- QueryBuilderContextType,
-} from 'providers/QueryBuilder';
+import { QueryBuilderContext } from 'providers/QueryBuilder';
import { useContext } from 'react';
+import { QueryBuilderContextType } from 'types/common/queryBuilder';
export function useQueryBuilder(): QueryBuilderContextType {
return useContext(QueryBuilderContext);
diff --git a/frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx b/frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx
new file mode 100644
index 0000000000..11b298074e
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx
@@ -0,0 +1,188 @@
+import { useMachine } from '@xstate/react';
+import ROUTES from 'constants/routes';
+import { encode } from 'js-base64';
+import history from 'lib/history';
+import React, { useCallback, useMemo, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+
+import { whilelistedKeys } from './config';
+import { ResourceContext } from './context';
+import { ResourceAttributesFilterMachine } from './machine';
+import {
+ IResourceAttribute,
+ IResourceAttributeProps,
+ OptionsData,
+} from './types';
+import {
+ createQuery,
+ getResourceAttributeQueriesFromURL,
+ GetTagKeys,
+ GetTagValues,
+ mappingWithRoutesAndKeys,
+ OperatorSchema,
+} from './utils';
+
+function ResourceProvider({ children }: Props): JSX.Element {
+ const { pathname } = useLocation();
+ const [loading, setLoading] = useState(true);
+ const [selectedQuery, setSelectedQueries] = useState([]);
+ const [staging, setStaging] = useState([]);
+ const [queries, setQueries] = useState(
+ getResourceAttributeQueriesFromURL(),
+ );
+
+ const [optionsData, setOptionsData] = useState({
+ mode: undefined,
+ options: [],
+ });
+
+ const handleLoading = (isLoading: boolean): void => {
+ setLoading(isLoading);
+ if (isLoading) {
+ setOptionsData({ mode: undefined, options: [] });
+ }
+ };
+
+ const dispatchQueries = useCallback(
+ (queries: IResourceAttribute[]): void => {
+ history.replace({
+ pathname,
+ search:
+ queries && queries.length
+ ? `?resourceAttribute=${encode(JSON.stringify(queries))}`
+ : '',
+ });
+ setQueries(queries);
+ },
+ [pathname],
+ );
+
+ const [state, send] = useMachine(ResourceAttributesFilterMachine, {
+ actions: {
+ onSelectTagKey: () => {
+ handleLoading(true);
+ GetTagKeys()
+ .then((tagKeys) =>
+ setOptionsData({
+ options: mappingWithRoutesAndKeys(pathname, tagKeys),
+ mode: undefined,
+ }),
+ )
+ .finally(() => {
+ handleLoading(false);
+ });
+ },
+ onSelectOperator: () => {
+ setOptionsData({ options: OperatorSchema, mode: undefined });
+ },
+ onSelectTagValue: () => {
+ handleLoading(true);
+
+ GetTagValues(staging[0])
+ .then((tagValuesOptions) =>
+ setOptionsData({ options: tagValuesOptions, mode: 'multiple' }),
+ )
+ .finally(() => {
+ handleLoading(false);
+ });
+ },
+ onBlurPurge: () => {
+ setSelectedQueries([]);
+ setStaging([]);
+ },
+ onValidateQuery: (): void => {
+ if (staging.length < 2 || selectedQuery.length === 0) {
+ return;
+ }
+
+ const generatedQuery = createQuery([...staging, selectedQuery]);
+ if (generatedQuery) {
+ dispatchQueries([...queries, generatedQuery]);
+ }
+ },
+ },
+ });
+
+ const handleFocus = useCallback((): void => {
+ if (state.value === 'Idle') {
+ send('NEXT');
+ }
+ }, [send, state.value]);
+
+ const handleBlur = useCallback((): void => {
+ send('onBlur');
+ }, [send]);
+
+ const handleChange = useCallback(
+ (value: string): void => {
+ if (!optionsData.mode) {
+ setStaging((prevStaging) => [...prevStaging, value]);
+ setSelectedQueries([]);
+ send('NEXT');
+ return;
+ }
+
+ setSelectedQueries([...value]);
+ },
+ [optionsData.mode, send],
+ );
+
+ const handleClose = useCallback(
+ (id: string): void => {
+ dispatchQueries(queries.filter((queryData) => queryData.id !== id));
+ },
+ [dispatchQueries, queries],
+ );
+
+ const handleClearAll = useCallback(() => {
+ send('RESET');
+ dispatchQueries([]);
+ setStaging([]);
+ setQueries([]);
+ setOptionsData({ mode: undefined, options: [] });
+ }, [dispatchQueries, send]);
+
+ const getVisibleQueries = useMemo(() => {
+ if (pathname === ROUTES.SERVICE_MAP) {
+ return queries.filter((query) => whilelistedKeys.includes(query.tagKey));
+ }
+ return queries;
+ }, [queries, pathname]);
+
+ const value: IResourceAttributeProps = useMemo(
+ () => ({
+ queries: getVisibleQueries,
+ staging,
+ handleClearAll,
+ handleClose,
+ handleBlur,
+ handleFocus,
+ loading,
+ handleChange,
+ selectedQuery,
+ optionsData,
+ }),
+ [
+ handleBlur,
+ handleChange,
+ handleClearAll,
+ handleClose,
+ handleFocus,
+ loading,
+ staging,
+ selectedQuery,
+ optionsData,
+ getVisibleQueries,
+ ],
+ );
+
+ return (
+ {children}
+ );
+}
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export default ResourceProvider;
diff --git a/frontend/src/hooks/useResourceAttribute/config.ts b/frontend/src/hooks/useResourceAttribute/config.ts
new file mode 100644
index 0000000000..f15a8ccf60
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/config.ts
@@ -0,0 +1,5 @@
+export const whilelistedKeys = [
+ 'resource_deployment_environment',
+ 'resource_k8s_cluster_name',
+ 'resource_k8s_cluster_namespace',
+];
diff --git a/frontend/src/hooks/useResourceAttribute/context.ts b/frontend/src/hooks/useResourceAttribute/context.ts
new file mode 100644
index 0000000000..2ee296048a
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/context.ts
@@ -0,0 +1,7 @@
+import { createContext } from 'react';
+
+import { IResourceAttributeProps } from './types';
+
+export const ResourceContext = createContext(
+ {} as IResourceAttributeProps,
+);
diff --git a/frontend/src/hooks/useResourceAttribute/index.ts b/frontend/src/hooks/useResourceAttribute/index.ts
new file mode 100644
index 0000000000..1e70a15391
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/index.ts
@@ -0,0 +1,7 @@
+import ResourceProvider from './ResourceProvider';
+import useResourceAttribute from './useResourceAttribute';
+import { convertMetricKeyToTrace, isResourceEmpty } from './utils';
+
+export default useResourceAttribute;
+
+export { convertMetricKeyToTrace, isResourceEmpty, ResourceProvider };
diff --git a/frontend/src/hooks/useResourceAttribute/machine.ts b/frontend/src/hooks/useResourceAttribute/machine.ts
new file mode 100644
index 0000000000..7d7fa662a1
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/machine.ts
@@ -0,0 +1,61 @@
+import { createMachine } from 'xstate';
+
+export const ResourceAttributesFilterMachine =
+ /** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGrwDaABgC6iUAAccsZu2Y46akAA9EATkUB2bgEYAbBYsBWWwA5HAFkW3F7gDQgRRABaU3duFwsXAGZbWwAmF3co01jTAF80-zRMXAJiMkpqeiY2Th5+IVExfQAhVgBXfCVVJBBNbV19QxMEcys7B2c3T28-AOC4xUduKItrSbiEuNMo6zcMrPRsPEJScnwqWgYiFg4uPgFhcQAlKRIpBRVDdp09A1aevpt7J1cPLx8-kCCCCcUcURmcwWSxWa0cGxA2W2eT2hSOJTOPAA8uouKh2Dh8JJZI8WhotK8uh9EPM4tYZl4IrZHNY1rZrEDgqFwpEoi43HEnMt3NYEUjcrsCgcisdTmVuDi8QSibUGk0nq0Xp13qAerT6VFGRZmayXOzOSDJtNZrT3I44t5bHaLGKthL8vtDsUTqVzor8PjCWJbvdSc8KdrujTFgajSa2RzxpbwZDbfbHc7XTkdh60d65ecKgA1VANMDVOh1RrNcMdN6GYFBayOKw2xZ2h1eZ3+PX2+mxFzWEWmFymBxRLPIyWemUY+XF0v1cshh41zUR+vUhDNuncAdD6wjscWKIW0FTVPt9NdluT92o6Xon2Y7gASQgrHL0jka-JdapuqIPEcTcIoihxHyTh2Pa-JntyETRO4ngig6yTuBkmQgHQOAQHAhjijmD5erKvr4LWlI6sYiDJIo3Aiieh7Gk4UynkmQRRJ44TARYijJC4AJRBOmEESiUrEXOhaXKI5GRluPG0SkI7uIKhr2vaZ7Nq2cxrGByQWKYpiisJbqEWJs7PvK-qBmR67-pReq6aB1g+DEkEcaYcQaS2l7gTCqzrMZ2aiTOT4FuUAglmWMmboB258hCESmNeLgQR4jheVp8y+SlsIBZsQXTnmJEvu+n7RQBVEIEkLh0dYDFjvYjgsRlqY6bxY4GUZGRAA */
+ createMachine({
+ tsTypes: {} as import('./machine.typegen').Typegen0,
+ initial: 'Idle',
+ states: {
+ TagKey: {
+ on: {
+ NEXT: {
+ actions: 'onSelectOperator',
+ target: 'Operator',
+ },
+ onBlur: {
+ actions: 'onBlurPurge',
+ target: 'Idle',
+ },
+ RESET: {
+ target: 'Idle',
+ },
+ },
+ },
+ Operator: {
+ on: {
+ NEXT: {
+ actions: 'onSelectTagValue',
+ target: 'TagValue',
+ },
+ onBlur: {
+ actions: 'onBlurPurge',
+ target: 'Idle',
+ },
+ RESET: {
+ target: 'Idle',
+ },
+ },
+ },
+ TagValue: {
+ on: {
+ onBlur: {
+ actions: ['onValidateQuery', 'onBlurPurge'],
+ target: 'Idle',
+ },
+ RESET: {
+ target: 'Idle',
+ },
+ },
+ },
+ Idle: {
+ on: {
+ NEXT: {
+ actions: 'onSelectTagKey',
+ description: 'Select Category',
+ target: 'TagKey',
+ },
+ },
+ },
+ },
+ id: 'ResourceAttributesFilterMachine',
+ });
diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts b/frontend/src/hooks/useResourceAttribute/machine.typegen.ts
similarity index 85%
rename from frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts
rename to frontend/src/hooks/useResourceAttribute/machine.typegen.ts
index e7f7ee3de7..f2019aaa26 100644
--- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts
+++ b/frontend/src/hooks/useResourceAttribute/machine.typegen.ts
@@ -2,31 +2,31 @@
export interface Typegen0 {
'@@xstate/typegen': true;
- eventsCausingActions: {
- onSelectOperator: 'NEXT';
- onBlurPurge: 'onBlur';
- onSelectTagValue: 'NEXT';
- onValidateQuery: 'onBlur';
- onSelectTagKey: 'NEXT';
- };
internalEvents: {
'xstate.init': { type: 'xstate.init' };
};
invokeSrcNameMap: {};
missingImplementations: {
actions:
- | 'onSelectOperator'
| 'onBlurPurge'
+ | 'onSelectOperator'
+ | 'onSelectTagKey'
| 'onSelectTagValue'
- | 'onValidateQuery'
- | 'onSelectTagKey';
- services: never;
- guards: never;
+ | 'onValidateQuery';
delays: never;
+ guards: never;
+ services: never;
+ };
+ eventsCausingActions: {
+ onBlurPurge: 'onBlur';
+ onSelectOperator: 'NEXT';
+ onSelectTagKey: 'NEXT';
+ onSelectTagValue: 'NEXT';
+ onValidateQuery: 'onBlur';
};
- eventsCausingServices: {};
- eventsCausingGuards: {};
eventsCausingDelays: {};
- matchesStates: 'TagKey' | 'Operator' | 'TagValue' | 'Idle';
+ eventsCausingGuards: {};
+ eventsCausingServices: {};
+ matchesStates: 'Idle' | 'Operator' | 'TagKey' | 'TagValue';
tags: never;
}
diff --git a/frontend/src/hooks/useResourceAttribute/types.ts b/frontend/src/hooks/useResourceAttribute/types.ts
new file mode 100644
index 0000000000..422a0555ba
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/types.ts
@@ -0,0 +1,31 @@
+export interface IResourceAttribute {
+ id: string;
+ tagKey: string;
+ operator: string;
+ tagValue: string[];
+}
+
+export interface IOption {
+ label: string;
+ value: string;
+}
+
+type Modes = 'tags' | 'multiple';
+
+export interface OptionsData {
+ mode?: Modes;
+ options: IOption[];
+}
+
+export interface IResourceAttributeProps {
+ queries: IResourceAttribute[];
+ staging: string[];
+ handleClearAll: VoidFunction;
+ handleClose: (id: string) => void;
+ handleBlur: VoidFunction;
+ handleFocus: VoidFunction;
+ loading: boolean;
+ handleChange: (value: string) => void;
+ selectedQuery: string[];
+ optionsData: OptionsData;
+}
diff --git a/frontend/src/hooks/useResourceAttribute/useResourceAttribute.tsx b/frontend/src/hooks/useResourceAttribute/useResourceAttribute.tsx
new file mode 100644
index 0000000000..4b81be163d
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/useResourceAttribute.tsx
@@ -0,0 +1,9 @@
+import { useContext } from 'react';
+
+import { ResourceContext } from './context';
+import { IResourceAttributeProps } from './types';
+
+const useResourceAttribute = (): IResourceAttributeProps =>
+ useContext(ResourceContext);
+
+export default useResourceAttribute;
diff --git a/frontend/src/hooks/useResourceAttribute/utils.ts b/frontend/src/hooks/useResourceAttribute/utils.ts
new file mode 100644
index 0000000000..c76cf534e3
--- /dev/null
+++ b/frontend/src/hooks/useResourceAttribute/utils.ts
@@ -0,0 +1,154 @@
+import {
+ getResourceAttributesTagKeys,
+ getResourceAttributesTagValues,
+} from 'api/metrics/getResourceAttributes';
+import { OperatorConversions } from 'constants/resourceAttributes';
+import ROUTES from 'constants/routes';
+import {
+ IOption,
+ IResourceAttribute,
+ IResourceAttributeProps,
+} from 'hooks/useResourceAttribute/types';
+import { decode } from 'js-base64';
+import history from 'lib/history';
+import { IQueryBuilderTagFilterItems } from 'types/api/dashboard/getAll';
+import { OperatorValues, Tags } from 'types/reducer/trace';
+import { v4 as uuid } from 'uuid';
+
+import { whilelistedKeys } from './config';
+
+/**
+ * resource_x_y -> x.y
+ */
+export const convertMetricKeyToTrace = (key: string): string => {
+ const splittedKey = key.split('_');
+
+ if (splittedKey.length <= 1) {
+ return '';
+ }
+ return splittedKey.splice(1).join('.');
+};
+
+/**
+ * x.y -> resource_x_y
+ */
+export const convertTraceKeyToMetric = (key: string): string => {
+ const splittedKey = key.split('.');
+ return `resource_${splittedKey.join('_')}`;
+};
+
+export const convertOperatorLabelToMetricOperator = (label: string): string =>
+ OperatorConversions.find((operator) => operator.label === label)
+ ?.metricValue || '';
+
+export const convertOperatorLabelToTraceOperator = (
+ label: string,
+): OperatorValues =>
+ OperatorConversions.find((operator) => operator.label === label)
+ ?.traceValue as OperatorValues;
+
+export const convertRawQueriesToTraceSelectedTags = (
+ queries: IResourceAttribute[],
+ tagType = 'ResourceAttribute',
+): Tags[] =>
+ queries.map((query) => ({
+ Key: convertMetricKeyToTrace(query.tagKey),
+ Operator: convertOperatorLabelToTraceOperator(query.operator),
+ StringValues: query.tagValue,
+ NumberValues: [],
+ BoolValues: [],
+ TagType: tagType,
+ }));
+
+/* Convert resource attributes to tagFilter items for queryBuilder */
+export const resourceAttributesToTagFilterItems = (
+ queries: IResourceAttribute[],
+): IQueryBuilderTagFilterItems[] =>
+ queries.map((res) => ({
+ id: `${res.id}`,
+ key: `${res.tagKey}`,
+ op: `${res.operator}`,
+ value: `${res.tagValue}`.split(','),
+ }));
+
+export const OperatorSchema: IOption[] = OperatorConversions.map(
+ (operator) => ({
+ label: operator.label,
+ value: operator.label,
+ }),
+);
+
+export const GetTagKeys = async (): Promise => {
+ const { payload } = await getResourceAttributesTagKeys({
+ metricName: 'signoz_calls_total',
+ match: 'resource_',
+ });
+ if (!payload || !payload?.data) {
+ return [];
+ }
+ return payload.data.map((tagKey: string) => ({
+ label: convertMetricKeyToTrace(tagKey),
+ value: tagKey,
+ }));
+};
+
+export const GetTagValues = async (tagKey: string): Promise => {
+ const { payload } = await getResourceAttributesTagValues({
+ tagKey,
+ metricName: 'signoz_calls_total',
+ });
+
+ if (!payload || !payload?.data) {
+ return [];
+ }
+ return payload.data.map((tagValue: string) => ({
+ label: tagValue,
+ value: tagValue,
+ }));
+};
+
+export const createQuery = (
+ selectedItems: Array = [],
+): IResourceAttribute | null => {
+ if (selectedItems.length === 3) {
+ return {
+ id: uuid().slice(0, 8),
+ tagKey: selectedItems[0] as string,
+ operator: selectedItems[1] as string,
+ tagValue: selectedItems[2] as string[],
+ };
+ }
+ return null;
+};
+
+export function getResourceAttributeQueriesFromURL(): IResourceAttribute[] {
+ const resourceAttributeQuery = new URLSearchParams(
+ history.location.search,
+ ).get('resourceAttribute');
+
+ try {
+ if (resourceAttributeQuery) {
+ return JSON.parse(decode(resourceAttributeQuery)) as IResourceAttribute[];
+ }
+ } catch (error) {
+ console.error(error);
+ }
+
+ return [];
+}
+
+export const isResourceEmpty = (
+ queries: IResourceAttributeProps['queries'],
+ staging: IResourceAttributeProps['staging'],
+ selectedQuery: IResourceAttributeProps['selectedQuery'],
+): boolean => !!(queries.length || staging.length || selectedQuery.length);
+
+export const mappingWithRoutesAndKeys = (
+ pathname: string,
+ filters: IOption[],
+): IOption[] => {
+ if (ROUTES.SERVICE_MAP === pathname) {
+ return filters.filter((filter) => whilelistedKeys.includes(filter.value));
+ }
+ return filters;
+};
diff --git a/frontend/src/lib/__fixtures__/getRandomColor.ts b/frontend/src/lib/__fixtures__/getRandomColor.ts
new file mode 100644
index 0000000000..2ebbecae15
--- /dev/null
+++ b/frontend/src/lib/__fixtures__/getRandomColor.ts
@@ -0,0 +1,50 @@
+import { themeColors } from 'constants/theme';
+import { SIGNOZ_UI_COLOR_HEX } from 'lib/getRandomColor';
+import { Span } from 'types/api/trace/getTraceItem';
+
+const spans: Span[] = [
+ [
+ 1,
+ 'span1',
+ 'trace1',
+ 'serviceA',
+ 'op1',
+ '100',
+ '200',
+ [SIGNOZ_UI_COLOR_HEX],
+ [themeColors.chartcolors.turquoise],
+ [''],
+ [''],
+ false,
+ ],
+ [
+ 2,
+ 'span2',
+ 'trace2',
+ 'serviceB',
+ 'op2',
+ '200',
+ '300',
+ [SIGNOZ_UI_COLOR_HEX],
+ [themeColors.chartcolors.turquoise],
+ [''],
+ [''],
+ false,
+ ],
+ [
+ 3,
+ 'span3',
+ 'trace3',
+ 'serviceC',
+ 'op3',
+ '300',
+ '400',
+ [],
+ [],
+ [''],
+ [''],
+ false,
+ ],
+];
+
+export default spans;
diff --git a/frontend/src/lib/getRandomColor.test.ts b/frontend/src/lib/getRandomColor.test.ts
new file mode 100644
index 0000000000..906ac95f06
--- /dev/null
+++ b/frontend/src/lib/getRandomColor.test.ts
@@ -0,0 +1,29 @@
+import { themeColors } from 'constants/theme';
+import { Span } from 'types/api/trace/getTraceItem';
+
+import spans from './__fixtures__/getRandomColor';
+import { colors, spanServiceNameToColorMapping } from './getRandomColor';
+
+describe('spanServiceNameToColorMapping', () => {
+ test('should map span services to colors', () => {
+ const expectedServiceToColorMap = {
+ serviceA: themeColors.chartcolors.turquoise,
+ serviceB: themeColors.chartcolors.turquoise,
+ serviceC: colors[2], // 2 is because we have already used 0 and 1 in the above services,
+ };
+
+ const result = spanServiceNameToColorMapping(spans);
+
+ expect(result).toEqual(expectedServiceToColorMap);
+ });
+
+ test('should return an empty object when input is an empty array', () => {
+ const spans: Span[] = [];
+
+ const expectedServiceToColorMap = {};
+
+ const result = spanServiceNameToColorMapping(spans);
+
+ expect(result).toEqual(expectedServiceToColorMap);
+ });
+});
diff --git a/frontend/src/lib/getRandomColor.ts b/frontend/src/lib/getRandomColor.ts
index fc85331af7..6cca527b32 100644
--- a/frontend/src/lib/getRandomColor.ts
+++ b/frontend/src/lib/getRandomColor.ts
@@ -13,17 +13,33 @@ const getRandomColor = (): string => {
return colors[index];
};
+export const SIGNOZ_UI_COLOR_HEX = 'signoz_ui_color_hex';
+
export const spanServiceNameToColorMapping = (
spans: Span[],
): { [key: string]: string } => {
- const serviceNameSet = new Set();
+ const allServiceMap = new Map();
+
spans.forEach((spanItem) => {
- serviceNameSet.add(spanItem[3]);
+ const signozUiColorKeyIndex = spanItem[7].findIndex(
+ (span) => span === SIGNOZ_UI_COLOR_HEX,
+ );
+
+ allServiceMap.set(
+ spanItem[3],
+ signozUiColorKeyIndex === -1
+ ? undefined
+ : spanItem[8][signozUiColorKeyIndex],
+ );
});
+
const serviceToColorMap: { [key: string]: string } = {};
- Array.from(serviceNameSet).forEach((serviceName, idx) => {
- serviceToColorMap[`${serviceName}`] = colors[idx % colors.length];
+
+ Array.from(allServiceMap).forEach(([serviceName, signozColor], idx) => {
+ serviceToColorMap[`${serviceName}`] =
+ signozColor || colors[idx % colors.length];
});
+
return serviceToColorMap;
};
diff --git a/frontend/src/lib/newQueryBuilder/createNewBuilderItemName.ts b/frontend/src/lib/newQueryBuilder/createNewBuilderItemName.ts
new file mode 100644
index 0000000000..0e4dbd0429
--- /dev/null
+++ b/frontend/src/lib/newQueryBuilder/createNewBuilderItemName.ts
@@ -0,0 +1,17 @@
+type CreateNewBuilderItemNameParams = {
+ existNames: string[];
+ sourceNames: string[];
+};
+
+export const createNewBuilderItemName = ({
+ existNames,
+ sourceNames,
+}: CreateNewBuilderItemNameParams): string => {
+ for (let i = 0; i < sourceNames.length; i += 1) {
+ if (!existNames.includes(sourceNames[i])) {
+ return sourceNames[i];
+ }
+ }
+
+ return '';
+};
diff --git a/frontend/src/lib/query/findDataTypeOfOperator.ts b/frontend/src/lib/query/findDataTypeOfOperator.ts
new file mode 100644
index 0000000000..9833c0c23b
--- /dev/null
+++ b/frontend/src/lib/query/findDataTypeOfOperator.ts
@@ -0,0 +1,22 @@
+import { operatorsByTypes } from 'constants/queryBuilder';
+import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
+
+export const findDataTypeOfOperator = (value: string): LocalDataType | null => {
+ const entries = Object.entries(operatorsByTypes) as [
+ LocalDataType,
+ string[],
+ ][];
+
+ for (let i = 0; i < entries.length; i += 1) {
+ for (let j = 0; j < entries[i][1].length; j += 1) {
+ const currentOperator = entries[i][1][j];
+ const type = entries[i][0];
+
+ if (currentOperator === value) {
+ return type;
+ }
+ }
+ }
+
+ return null;
+};
diff --git a/frontend/src/lib/query/transformQueryBuilderData.ts b/frontend/src/lib/query/transformQueryBuilderData.ts
new file mode 100644
index 0000000000..c488c45d86
--- /dev/null
+++ b/frontend/src/lib/query/transformQueryBuilderData.ts
@@ -0,0 +1,22 @@
+import { OPERATORS } from 'constants/queryBuilder';
+import { Having } from 'types/api/queryBuilder/queryBuilderData';
+
+export const transformHavingToStringValue = (having: Having[]): string[] => {
+ const result: string[] = having.map((item) => {
+ const operator = Object.entries(OPERATORS).find(([key]) => key === item.op);
+
+ return `${item.columnName} ${operator ? operator[1] : ''} ${item.value.join(
+ ' ',
+ )}`;
+ });
+
+ return result;
+};
+
+export const transformFromStringToHaving = (havingStr: string): Having => {
+ const [columnName, op, ...value] = havingStr.split(' ');
+
+ const operator = Object.entries(OPERATORS).find(([, value]) => value === op);
+
+ return { columnName, op: operator ? operator[0] : '', value };
+};
diff --git a/frontend/src/lib/query/transformStringWithPrefix.ts b/frontend/src/lib/query/transformStringWithPrefix.ts
new file mode 100644
index 0000000000..83799143ca
--- /dev/null
+++ b/frontend/src/lib/query/transformStringWithPrefix.ts
@@ -0,0 +1,16 @@
+type TransformStringWithPrefixParams = {
+ str: string;
+ prefix: string;
+ condition: boolean;
+};
+
+export const transformStringWithPrefix = ({
+ str,
+ prefix,
+ condition,
+}: TransformStringWithPrefixParams): string => {
+ if (prefix) {
+ return condition ? `${prefix}_${str}` : str;
+ }
+ return str;
+};
diff --git a/frontend/src/lib/resourceAttributes.ts b/frontend/src/lib/resourceAttributes.ts
deleted file mode 100644
index 3c3fdd99e1..0000000000
--- a/frontend/src/lib/resourceAttributes.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { OperatorConversions } from 'constants/resourceAttributes';
-import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types';
-import { IQueryBuilderTagFilterItems } from 'types/api/dashboard/getAll';
-import { OperatorValues, Tags } from 'types/reducer/trace';
-
-/**
- * resource_x_y -> x.y
- */
-export const convertMetricKeyToTrace = (key: string): string => {
- const splittedKey = key.split('_');
-
- if (splittedKey.length <= 1) {
- return '';
- }
- return splittedKey.splice(1).join('.');
-};
-
-/**
- * x.y -> resource_x_y
- */
-export const convertTraceKeyToMetric = (key: string): string => {
- const splittedKey = key.split('.');
- return `resource_${splittedKey.join('_')}`;
-};
-
-export const convertOperatorLabelToMetricOperator = (label: string): string =>
- OperatorConversions.find((operator) => operator.label === label)
- ?.metricValue || '';
-
-export const convertOperatorLabelToTraceOperator = (
- label: string,
-): OperatorValues =>
- OperatorConversions.find((operator) => operator.label === label)
- ?.traceValue as OperatorValues;
-
-export const convertRawQueriesToTraceSelectedTags = (
- queries: IResourceAttributeQuery[],
-): Tags[] =>
- queries.map((query) => ({
- Key: convertMetricKeyToTrace(query.tagKey),
- Operator: convertOperatorLabelToTraceOperator(query.operator),
- StringValues: query.tagValue,
- NumberValues: [],
- BoolValues: [],
- }));
-
-/**
- * Converts Resource Attribute Queries to PromQL query string
- */
-export const resourceAttributesQueryToPromQL = (
- queries: IResourceAttributeQuery[],
-): string => {
- let parsedQueryString = '';
-
- if (Array.isArray(queries))
- queries.forEach((query) => {
- parsedQueryString += `, ${
- query.tagKey
- }${convertOperatorLabelToMetricOperator(
- query.operator,
- )}"${query.tagValue.join('|')}"`;
- });
-
- return parsedQueryString;
-};
-
-/* Convert resource attributes to tagFilter items for queryBuilder */
-export const resourceAttributesToTagFilterItems = (
- queries: IResourceAttributeQuery[],
-): IQueryBuilderTagFilterItems[] =>
- queries.map((res) => ({
- id: `${res.id}`,
- key: `${res.tagKey}`,
- op: `${res.operator}`,
- value: `${res.tagValue}`.split(','),
- }));
diff --git a/frontend/src/modules/Servicemap/Map.tsx b/frontend/src/modules/Servicemap/Map.tsx
new file mode 100644
index 0000000000..42c8a3d61e
--- /dev/null
+++ b/frontend/src/modules/Servicemap/Map.tsx
@@ -0,0 +1,56 @@
+/* eslint-disable */
+//@ts-nocheck
+import { useIsDarkMode } from 'hooks/useDarkMode';
+import React, { memo } from 'react';
+import { ForceGraph2D } from 'react-force-graph';
+
+import { getGraphData, getTooltip, transformLabel } from './utils';
+
+function ServiceMap({ fgRef, serviceMap }: any): JSX.Element {
+ const isDarkMode = useIsDarkMode();
+
+ const { nodes, links } = getGraphData(serviceMap, isDarkMode);
+
+ const graphData = { nodes, links };
+
+ return (
+ d.target}
+ linkDirectionalParticles="value"
+ linkDirectionalParticleSpeed={(d) => d.value}
+ nodeCanvasObject={(node, ctx) => {
+ const label = transformLabel(node.id);
+ const { fontSize } = node;
+ ctx.font = `${fontSize}px Roboto`;
+ const { width } = node;
+
+ ctx.fillStyle = node.color;
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
+ ctx.fill();
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = isDarkMode ? '#ffffff' : '#000000';
+ ctx.fillText(label, node.x, node.y);
+ }}
+ onLinkHover={(node) => {
+ const tooltip = document.querySelector('.graph-tooltip');
+ if (tooltip && node) {
+ tooltip.innerHTML = getTooltip(node);
+ }
+ }}
+ nodePointerAreaPaint={(node, color, ctx) => {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
+ ctx.fill();
+ }}
+ />
+ );
+}
+
+export default memo(ServiceMap);
diff --git a/frontend/src/modules/Servicemap/ServiceMap.tsx b/frontend/src/modules/Servicemap/ServiceMap.tsx
index 9c36aa7e18..21b43c3733 100644
--- a/frontend/src/modules/Servicemap/ServiceMap.tsx
+++ b/frontend/src/modules/Servicemap/ServiceMap.tsx
@@ -3,9 +3,12 @@
import { Card } from 'antd';
import Spinner from 'components/Spinner';
-import { useIsDarkMode } from 'hooks/useDarkMode';
+import TextToolTip from 'components/TextToolTip';
+import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
+import useResourceAttribute from 'hooks/useResourceAttribute';
+import { whilelistedKeys } from 'hooks/useResourceAttribute/config';
+import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
import React, { useEffect, useRef } from 'react';
-import { ForceGraph2D } from 'react-force-graph';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
@@ -13,7 +16,7 @@ import { AppState } from 'store/reducers';
import styled from 'styled-components';
import { GlobalTime } from 'types/actions/globalTime';
-import { getGraphData, getTooltip, getZoomPx, transformLabel } from './utils';
+import Map from './Map';
const Container = styled.div`
.force-graph-container {
@@ -38,7 +41,10 @@ const Container = styled.div`
interface ServiceMapProps extends RouteComponentProps {
serviceMap: ServiceMapStore;
globalTime: GlobalTime;
- getDetailedServiceMapItems: (time: GlobalTime) => void;
+ getDetailedServiceMapItems: (
+ time: GlobalTime,
+ queries: IResourceAttribute[],
+ ) => void;
}
interface graphNode {
id: string;
@@ -60,17 +66,17 @@ export interface graphDataType {
function ServiceMap(props: ServiceMapProps): JSX.Element {
const fgRef = useRef();
- const isDarkMode = useIsDarkMode();
-
const { getDetailedServiceMapItems, globalTime, serviceMap } = props;
+ const { queries } = useResourceAttribute();
+
useEffect(() => {
/*
Call the apis only when the route is loaded.
Check this issue: https://github.com/SigNoz/signoz/issues/110
*/
- getDetailedServiceMapItems(globalTime);
- }, [globalTime, getDetailedServiceMapItems]);
+ getDetailedServiceMapItems(globalTime, queries);
+ }, [globalTime, getDetailedServiceMapItems, queries]);
useEffect(() => {
fgRef.current && fgRef.current.d3Force('charge').strength(-400);
@@ -83,51 +89,26 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
if (!serviceMap.loading && serviceMap.items.length === 0) {
return (
+
No Service Found
);
}
-
- const { nodes, links } = getGraphData(serviceMap, isDarkMode);
- const graphData = { nodes, links };
return (
- d.target}
- linkDirectionalParticles="value"
- linkDirectionalParticleSpeed={(d) => d.value}
- nodeCanvasObject={(node, ctx) => {
- const label = transformLabel(node.id);
- const { fontSize } = node;
- ctx.font = `${fontSize}px Roboto`;
- const { width } = node;
-
- ctx.fillStyle = node.color;
- ctx.beginPath();
- ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
- ctx.fill();
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = isDarkMode ? '#ffffff' : '#000000';
- ctx.fillText(label, node.x, node.y);
- }}
- onLinkHover={(node) => {
- const tooltip = document.querySelector('.graph-tooltip');
- if (tooltip && node) {
- tooltip.innerHTML = getTooltip(node);
- }
- }}
- nodePointerAreaPaint={(node, color, ctx) => {
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
- ctx.fill();
- }}
+
+ }
/>
+
+
);
}
diff --git a/frontend/src/pages/AllErrors/index.tsx b/frontend/src/pages/AllErrors/index.tsx
index 4093a20657..2e7238bebc 100644
--- a/frontend/src/pages/AllErrors/index.tsx
+++ b/frontend/src/pages/AllErrors/index.tsx
@@ -1,6 +1,7 @@
import RouteTab from 'components/RouteTab';
import ROUTES from 'constants/routes';
import AllErrorsContainer from 'container/AllError';
+import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,18 +9,21 @@ function AllErrors(): JSX.Element {
const { t } = useTranslation();
return (
-
+ <>
+
+
+ >
);
}
diff --git a/frontend/src/pages/MetricApplication/index.tsx b/frontend/src/pages/MetricApplication/index.tsx
index 96e12f50ea..e29a2b6056 100644
--- a/frontend/src/pages/MetricApplication/index.tsx
+++ b/frontend/src/pages/MetricApplication/index.tsx
@@ -1,7 +1,8 @@
import { Typography } from 'antd';
import Spinner from 'components/Spinner';
import MetricsApplicationContainer from 'container/MetricsApplication';
-import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
+import useResourceAttribute from 'hooks/useResourceAttribute';
+import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import React, { useEffect, useMemo } from 'react';
import { connect, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
@@ -27,16 +28,11 @@ function MetricsApplication({ getInitialData }: MetricsProps): JSX.Element {
>((state) => state.metrics);
const { servicename } = useParams();
-
- const { resourceAttributeQueries } = useSelector(
- (state) => state.metrics,
- );
+ const { queries } = useResourceAttribute();
const selectedTags = useMemo(
- () =>
- (convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) as Tags[]) ||
- [],
- [resourceAttributeQueries],
+ () => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
+ [queries],
);
useEffect(() => {
diff --git a/frontend/src/pages/Metrics/index.tsx b/frontend/src/pages/Metrics/index.tsx
index 5af71f6bef..f224cec02a 100644
--- a/frontend/src/pages/Metrics/index.tsx
+++ b/frontend/src/pages/Metrics/index.tsx
@@ -3,10 +3,11 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import ReleaseNote from 'components/ReleaseNote';
import Spinner from 'components/Spinner';
import { SKIP_ONBOARDING } from 'constants/onboarding';
-import ResourceAttributesFilter from 'container/MetricsApplication/ResourceAttributesFilter';
import MetricTable from 'container/MetricsTable';
+import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import { useNotifications } from 'hooks/useNotifications';
-import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
+import useResourceAttribute from 'hooks/useResourceAttribute';
+import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import React, { useEffect, useMemo } from 'react';
import { connect, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@@ -25,12 +26,9 @@ function Metrics({ getService }: MetricsProps): JSX.Element {
GlobalReducer
>((state) => state.globalTime);
const location = useLocation();
- const {
- services,
- resourceAttributeQueries,
- error,
- errorMessage,
- } = useSelector((state) => state.metrics);
+ const { services, error, errorMessage } = useSelector(
+ (state) => state.metrics,
+ );
const { notifications } = useNotifications();
useEffect(() => {
@@ -41,12 +39,13 @@ function Metrics({ getService }: MetricsProps): JSX.Element {
}
}, [error, errorMessage, notifications]);
+ const { queries } = useResourceAttribute();
+
const selectedTags = useMemo(
- () =>
- (convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) as Tags[]) ||
- [],
- [resourceAttributeQueries],
+ () => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [],
+ [queries],
);
+
const isSkipped = getLocalStorageKey(SKIP_ONBOARDING) === 'true';
useEffect(() => {
diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx
index d50b5d7eba..6704c5d8e7 100644
--- a/frontend/src/providers/QueryBuilder.tsx
+++ b/frontend/src/providers/QueryBuilder.tsx
@@ -1,3 +1,13 @@
+import {
+ alphabet,
+ formulasNames,
+ initialFormulaBuilderFormValues,
+ initialQueryBuilderFormValues,
+ mapOfOperators,
+ MAX_FORMULAS,
+ MAX_QUERIES,
+} from 'constants/queryBuilder';
+import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import React, {
createContext,
PropsWithChildren,
@@ -9,27 +19,25 @@ import React, {
// TODO: Rename Types on the Reusable type for any source
import {
IBuilderFormula,
- IBuilderQuery,
+ IBuilderQueryForm,
} from 'types/api/queryBuilder/queryBuilderData';
-
-export type QueryBuilderData = {
- queryData: IBuilderQuery[];
- queryFormulas: IBuilderFormula[];
-};
-
-// ** TODO: temporary types for context, fix it during development
-export type QueryBuilderContextType = {
- queryBuilderData: QueryBuilderData;
- resetQueryBuilderData: () => void;
- handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
- handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
-};
+import {
+ DataSource,
+ QueryBuilderContextType,
+ QueryBuilderData,
+} from 'types/common/queryBuilder';
export const QueryBuilderContext = createContext({
queryBuilderData: { queryData: [], queryFormulas: [] },
+ initialDataSource: null,
resetQueryBuilderData: () => {},
handleSetQueryData: () => {},
handleSetFormulaData: () => {},
+ initQueryBuilderData: () => {},
+ setupInitialDataSource: () => {},
+ removeEntityByIndex: () => {},
+ addNewQuery: () => {},
+ addNewFormula: () => {},
});
const initialQueryBuilderData: QueryBuilderData = {
@@ -40,48 +48,185 @@ const initialQueryBuilderData: QueryBuilderData = {
export function QueryBuilderProvider({
children,
}: PropsWithChildren): JSX.Element {
- // ** TODO: get queryId from url for getting data for query builder
- // ** TODO: type the params which will be used for request of the data for query builder
+ // TODO: this is temporary. It will be used when we have fixed dataSource and need create new query with this data source
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [initialDataSource, setInitialDataSource] = useState(
+ null,
+ );
+ // TODO: when initialDataSource will be setuped, on create button initial dataSource will from initialDataSource
const [queryBuilderData, setQueryBuilderData] = useState({
queryData: [],
queryFormulas: [],
});
- // ** TODO: Also in the future need to add AddFormula and AddQuery and remove them.
-
+ // ** Method for resetting query builder data
const resetQueryBuilderData = useCallback((): void => {
setQueryBuilderData(initialQueryBuilderData);
}, []);
- const handleSetQueryData = useCallback(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- (index: number, queryData: IBuilderQuery): void => {},
+ // ** Method for setuping query builder data
+ // ** Before setuping transform data from backend to frontend format
+ const initQueryBuilderData = useCallback(
+ (queryBuilderData: QueryBuilderData): void => {
+ setQueryBuilderData(queryBuilderData);
+ },
[],
);
+
+ const removeEntityByIndex = useCallback(
+ (type: keyof QueryBuilderData, index: number) => {
+ setQueryBuilderData((prevState) => {
+ const currentArray: (IBuilderQueryForm | IBuilderFormula)[] =
+ prevState[type];
+ return {
+ ...prevState,
+ [type]: currentArray.filter((item, i) => index !== i),
+ };
+ });
+ },
+ [],
+ );
+
+ const createNewQuery = useCallback(
+ (queries: IBuilderQueryForm[]): IBuilderQueryForm => {
+ const existNames = queries.map((item) => item.queryName);
+
+ const newQuery: IBuilderQueryForm = {
+ ...initialQueryBuilderFormValues,
+ queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }),
+ ...(initialDataSource
+ ? {
+ dataSource: initialDataSource,
+ aggregateOperator: mapOfOperators[initialDataSource][0],
+ expression: createNewBuilderItemName({
+ existNames,
+ sourceNames: alphabet,
+ }),
+ }
+ : {}),
+ };
+
+ return newQuery;
+ },
+ [initialDataSource],
+ );
+
+ const createNewFormula = useCallback((formulas: IBuilderFormula[]) => {
+ const existNames = formulas.map((item) => item.label);
+
+ const newFormula: IBuilderFormula = {
+ ...initialFormulaBuilderFormValues,
+ label: createNewBuilderItemName({ existNames, sourceNames: formulasNames }),
+ };
+
+ return newFormula;
+ }, []);
+
+ const addNewQuery = useCallback(() => {
+ setQueryBuilderData((prevState) => {
+ if (prevState.queryData.length >= MAX_QUERIES) return prevState;
+
+ const newQuery = createNewQuery(prevState.queryData);
+
+ return { ...prevState, queryData: [...prevState.queryData, newQuery] };
+ });
+ }, [createNewQuery]);
+
+ const addNewFormula = useCallback(() => {
+ setQueryBuilderData((prevState) => {
+ if (prevState.queryFormulas.length >= MAX_FORMULAS) return prevState;
+
+ const newFormula = createNewFormula(prevState.queryFormulas);
+
+ return {
+ ...prevState,
+ queryFormulas: [...prevState.queryFormulas, newFormula],
+ };
+ });
+ }, [createNewFormula]);
+
+ const setupInitialDataSource = useCallback(
+ (newInitialDataSource: DataSource | null) =>
+ setInitialDataSource(newInitialDataSource),
+ [],
+ );
+
+ const updateQueryBuilderData = useCallback(
+ (
+ queries: IBuilderQueryForm[],
+ index: number,
+ newQueryData: IBuilderQueryForm,
+ ) => queries.map((item, idx) => (index === idx ? newQueryData : item)),
+ [],
+ );
+
+ const updateFormulaBuilderData = useCallback(
+ (formulas: IBuilderFormula[], index: number, newFormula: IBuilderFormula) =>
+ formulas.map((item, idx) => (index === idx ? newFormula : item)),
+ [],
+ );
+
+ const handleSetQueryData = useCallback(
+ (index: number, newQueryData: IBuilderQueryForm): void => {
+ setQueryBuilderData((prevState) => {
+ const updatedQueryBuilderData = updateQueryBuilderData(
+ prevState.queryData,
+ index,
+ newQueryData,
+ );
+
+ return {
+ ...prevState,
+ queryData: updatedQueryBuilderData,
+ };
+ });
+ },
+ [updateQueryBuilderData],
+ );
const handleSetFormulaData = useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- (index: number, formulaData: IBuilderFormula): void => {},
- [],
+ (index: number, formulaData: IBuilderFormula): void => {
+ setQueryBuilderData((prevState) => {
+ const updatedFormulasBuilderData = updateFormulaBuilderData(
+ prevState.queryFormulas,
+ index,
+ formulaData,
+ );
+
+ return {
+ ...prevState,
+ queryFormulas: updatedFormulasBuilderData,
+ };
+ });
+ },
+ [updateFormulaBuilderData],
);
- // ** TODO: Discuss with Palash how the state of the queryBuilder and queryFormulas
- // ** TODO: should be filled from url
-
- // ** TODO: put these values and setter to the context value
-
const contextValues: QueryBuilderContextType = useMemo(
() => ({
queryBuilderData,
+ initialDataSource,
resetQueryBuilderData,
handleSetQueryData,
handleSetFormulaData,
+ initQueryBuilderData,
+ setupInitialDataSource,
+ removeEntityByIndex,
+ addNewQuery,
+ addNewFormula,
}),
[
queryBuilderData,
+ initialDataSource,
resetQueryBuilderData,
handleSetQueryData,
handleSetFormulaData,
+ initQueryBuilderData,
+ setupInitialDataSource,
+ removeEntityByIndex,
+ addNewQuery,
+ addNewFormula,
],
);
diff --git a/frontend/src/store/actions/metrics/setResourceAttributeQueries.ts b/frontend/src/store/actions/metrics/setResourceAttributeQueries.ts
deleted file mode 100644
index 14b262d4b6..0000000000
--- a/frontend/src/store/actions/metrics/setResourceAttributeQueries.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types';
-import { decode, encode } from 'js-base64';
-import history from 'lib/history';
-import { resourceAttributesQueryToPromQL } from 'lib/resourceAttributes';
-import { SET_RESOURCE_ATTRIBUTE_QUERIES } from 'types/actions/metrics';
-
-export function GetResourceAttributeQueriesFromURL():
- | IResourceAttributeQuery[]
- | null {
- const resourceAttributeQuery = new URLSearchParams(
- history.location.search,
- ).get('resourceAttribute');
-
- try {
- if (resourceAttributeQuery) {
- return JSON.parse(
- decode(resourceAttributeQuery),
- ) as IResourceAttributeQuery[];
- }
- } catch (error) {
- console.error(error);
- }
-
- return null;
-}
-
-export const SetResourceAttributeQueriesFromURL = (
- queries: IResourceAttributeQuery[],
-): void => {
- history.push({
- pathname: history.location.pathname,
- search:
- queries && queries.length
- ? `?resourceAttribute=${encode(JSON.stringify(queries))}`
- : '',
- });
-};
-export const SetResourceAttributeQueries = (
- queries: IResourceAttributeQuery[],
-): {
- type: typeof SET_RESOURCE_ATTRIBUTE_QUERIES;
- payload: {
- queries: IResourceAttributeQuery[];
- promQLQuery: string;
- };
-} => {
- SetResourceAttributeQueriesFromURL(queries);
- return {
- type: SET_RESOURCE_ATTRIBUTE_QUERIES,
- payload: {
- queries,
- promQLQuery: resourceAttributesQueryToPromQL(queries),
- },
- };
-};
diff --git a/frontend/src/store/actions/serviceMap.ts b/frontend/src/store/actions/serviceMap.ts
index 5274eec75c..b9fa22959d 100644
--- a/frontend/src/store/actions/serviceMap.ts
+++ b/frontend/src/store/actions/serviceMap.ts
@@ -1,4 +1,6 @@
import api from 'api';
+import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
+import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { Dispatch } from 'redux';
import { GlobalTime } from 'types/actions/globalTime';
@@ -30,16 +32,17 @@ export interface ServiceMapLoading {
};
}
-export const getDetailedServiceMapItems = (globalTime: GlobalTime) => async (
- dispatch: Dispatch,
-): Promise => {
+export const getDetailedServiceMapItems = (
+ globalTime: GlobalTime,
+ queries: IResourceAttribute[],
+) => async (dispatch: Dispatch): Promise => {
const start = `${globalTime.minTime}`;
const end = `${globalTime.maxTime}`;
const serviceMapPayload = {
start,
end,
- tags: [],
+ tags: convertRawQueriesToTraceSelectedTags(queries),
};
const [dependencyGraphResponse] = await Promise.all([
api.post(`/dependency_graph`, serviceMapPayload),
diff --git a/frontend/src/store/reducers/metric.ts b/frontend/src/store/reducers/metric.ts
index 2cb316d2c1..103244cc96 100644
--- a/frontend/src/store/reducers/metric.ts
+++ b/frontend/src/store/reducers/metric.ts
@@ -1,5 +1,3 @@
-import { resourceAttributesQueryToPromQL } from 'lib/resourceAttributes';
-import { GetResourceAttributeQueriesFromURL } from 'store/actions/metrics/setResourceAttributeQueries';
import {
GET_INITIAL_APPLICATION_ERROR,
GET_INITIAL_APPLICATION_LOADING,
@@ -9,11 +7,10 @@ import {
GET_SERVICE_LIST_SUCCESS,
MetricsActions,
RESET_INITIAL_APPLICATION_DATA,
- SET_RESOURCE_ATTRIBUTE_QUERIES,
} from 'types/actions/metrics';
import InitialValueTypes from 'types/reducer/metrics';
-const InitialValue: InitialValueTypes = {
+export const InitialValue: InitialValueTypes = {
error: false,
errorMessage: '',
loading: true,
@@ -25,10 +22,6 @@ const InitialValue: InitialValueTypes = {
externalAverageDuration: [],
externalError: [],
serviceOverview: [],
- resourceAttributeQueries: GetResourceAttributeQueriesFromURL() || [],
- resourceAttributePromQLQuery: resourceAttributesQueryToPromQL(
- GetResourceAttributeQueriesFromURL() || [],
- ),
topLevelOperations: [],
};
@@ -110,15 +103,6 @@ const metrics = (
};
}
- case SET_RESOURCE_ATTRIBUTE_QUERIES: {
- const { queries, promQLQuery } = action.payload;
- return {
- ...state,
- resourceAttributeQueries: queries,
- resourceAttributePromQLQuery: promQLQuery,
- };
- }
-
default:
return state;
}
diff --git a/frontend/src/types/actions/metrics.ts b/frontend/src/types/actions/metrics.ts
index bc48f0929f..c350e0e265 100644
--- a/frontend/src/types/actions/metrics.ts
+++ b/frontend/src/types/actions/metrics.ts
@@ -1,8 +1,3 @@
-// import { DBOverView } from 'types/api/metrics/getDBOverview';
-// import { ExternalAverageDuration } from 'types/api/metrics/getExternalAverageDuration';
-// import { ExternalError } from 'types/api/metrics/getExternalError';
-// import { ExternalService } from 'types/api/metrics/getExternalService';
-import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types';
import { ServicesList } from 'types/api/metrics/getService';
import { ServiceOverview } from 'types/api/metrics/getServiceOverview';
import { TopOperations } from 'types/api/metrics/getTopOperations';
@@ -15,7 +10,6 @@ export const GET_INITIAL_APPLICATION_LOADING =
export const GET_INITIAL_APPLICATION_ERROR = 'GET_INITIAL_APPLICATION_ERROR';
export const GET_INTIAL_APPLICATION_DATA = 'GET_INTIAL_APPLICATION_DATA';
export const RESET_INITIAL_APPLICATION_DATA = 'RESET_INITIAL_APPLICATION_DATA';
-export const SET_RESOURCE_ATTRIBUTE_QUERIES = 'SET_RESOURCE_ATTRIBUTE_QUERIES';
export interface GetServiceList {
type: typeof GET_SERVICE_LIST_SUCCESS;
@@ -52,18 +46,9 @@ export interface ResetInitialApplicationData {
type: typeof RESET_INITIAL_APPLICATION_DATA;
}
-export interface SetResourceAttributeQueries {
- type: typeof SET_RESOURCE_ATTRIBUTE_QUERIES;
- payload: {
- queries: IResourceAttributeQuery[];
- promQLQuery: string;
- };
-}
-
export type MetricsActions =
| GetServiceListError
| GetServiceListLoading
| GetServiceList
| GetInitialApplicationData
- | ResetInitialApplicationData
- | SetResourceAttributeQueries;
+ | ResetInitialApplicationData;
diff --git a/frontend/src/types/api/errors/getAll.ts b/frontend/src/types/api/errors/getAll.ts
index 38444cac62..26ad5e943b 100644
--- a/frontend/src/types/api/errors/getAll.ts
+++ b/frontend/src/types/api/errors/getAll.ts
@@ -1,4 +1,5 @@
import { GlobalTime } from 'types/actions/globalTime';
+import { Tags } from 'types/reducer/trace';
export type Order = 'ascending' | 'descending';
export type OrderBy =
@@ -17,6 +18,7 @@ export interface Props {
offset?: number;
exceptionType?: string;
serviceName?: string;
+ tags?: Tags[];
}
export interface Exception {
diff --git a/frontend/src/types/api/errors/getErrorCounts.ts b/frontend/src/types/api/errors/getErrorCounts.ts
index 67526d3093..733b3f4f35 100644
--- a/frontend/src/types/api/errors/getErrorCounts.ts
+++ b/frontend/src/types/api/errors/getErrorCounts.ts
@@ -1,10 +1,12 @@
import { GlobalTime } from 'types/actions/globalTime';
+import { Tags } from 'types/reducer/trace';
export type Props = {
start: GlobalTime['minTime'];
end: GlobalTime['minTime'];
exceptionType: string;
serviceName: string;
+ tags: Tags[];
};
export type PayloadProps = number;
diff --git a/frontend/src/types/api/queryBuilder/getAggregatorAttribute.ts b/frontend/src/types/api/queryBuilder/getAggregatorAttribute.ts
new file mode 100644
index 0000000000..d308e013a3
--- /dev/null
+++ b/frontend/src/types/api/queryBuilder/getAggregatorAttribute.ts
@@ -0,0 +1,7 @@
+import { DataSource } from 'types/common/queryBuilder';
+
+export interface IGetAggregateAttributePayload {
+ aggregateOperator: string;
+ dataSource: DataSource;
+ searchText: string;
+}
diff --git a/frontend/src/types/api/queryBuilder/getAttributeKeys.ts b/frontend/src/types/api/queryBuilder/getAttributeKeys.ts
new file mode 100644
index 0000000000..b3452aef26
--- /dev/null
+++ b/frontend/src/types/api/queryBuilder/getAttributeKeys.ts
@@ -0,0 +1,11 @@
+import { DataSource } from 'types/common/queryBuilder';
+
+import { BaseAutocompleteData } from './queryAutocompleteResponse';
+
+export interface IGetAttributeKeysPayload {
+ aggregateOperator: string;
+ dataSource: DataSource;
+ searchText: string;
+ aggregateAttribute: string;
+ tagType: BaseAutocompleteData['type'];
+}
diff --git a/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts
new file mode 100644
index 0000000000..72b2c3a6f5
--- /dev/null
+++ b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts
@@ -0,0 +1,14 @@
+export type LocalDataType = 'number' | 'string' | 'bool';
+
+export type DataType = 'int64' | 'float64' | 'string' | 'bool';
+
+export interface BaseAutocompleteData {
+ dataType: DataType | null;
+ isColumn: boolean | null;
+ key: string;
+ type: 'tag' | 'resource' | null;
+}
+
+export interface IQueryAutocompleteResponse {
+ attributeKeys: BaseAutocompleteData[];
+}
diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts
index dbfa82ce53..247618f99d 100644
--- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts
+++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts
@@ -1,22 +1,53 @@
-import { EAggregateOperator, EReduceOperator } from 'types/common/dashboard';
+import { DataSource } from 'types/common/queryBuilder';
-import { IQueryBuilderTagFilters } from '../dashboard/getAll';
-
-export interface IBuilderQuery {
- // TODO: add another list of operator depended from data source
- aggregateOperator: EAggregateOperator;
- disabled: boolean;
- label: string;
- legend: string;
- attribute: string;
- groupBy: string[];
- tagFilters: IQueryBuilderTagFilters;
- reduceTo?: EReduceOperator;
-}
+import { BaseAutocompleteData } from './queryAutocompleteResponse';
+// Type for Formula
export interface IBuilderFormula {
expression: string;
disabled: boolean;
label: string;
legend: string;
}
+
+export interface TagFilterItem {
+ id: string;
+ key: string;
+ // TODO: type it in the future
+ op: string;
+ value: string[];
+}
+
+export interface TagFilter {
+ items: TagFilterItem[] | [];
+ // TODO: type it in the future
+ op: string;
+}
+
+export type Having = {
+ columnName: string;
+ op: string;
+ value: string[];
+};
+
+// Type for query builder
+export type IBuilderQuery = {
+ queryName: string;
+ dataSource: DataSource;
+ aggregateOperator: string;
+ aggregateAttribute: string;
+ tagFilters: TagFilter;
+ groupBy: BaseAutocompleteData[];
+ expression: string;
+ disabled: boolean;
+ having: Having[];
+ limit: number | null;
+ stepInterval: number;
+ orderBy: BaseAutocompleteData[];
+ reduceTo: string;
+};
+
+export type IBuilderQueryForm = Omit & {
+ aggregateAttribute: BaseAutocompleteData;
+ legend: string;
+};
diff --git a/frontend/src/types/common/queryBuilder.ts b/frontend/src/types/common/queryBuilder.ts
index f1190b857f..8ebf5a6138 100644
--- a/frontend/src/types/common/queryBuilder.ts
+++ b/frontend/src/types/common/queryBuilder.ts
@@ -1,5 +1,153 @@
+import {
+ IBuilderFormula,
+ IBuilderQueryForm,
+} from 'types/api/queryBuilder/queryBuilderData';
+
export enum DataSource {
METRICS = 'metrics',
TRACES = 'traces',
LOGS = 'logs',
}
+
+export enum StringOperators {
+ NOOP = 'noop',
+ COUNT = 'count',
+ COUNT_DISTINCT = 'count_distinct',
+}
+
+export enum NumberOperators {
+ SUM = 'sum',
+ AVG = 'avg',
+ MAX = 'max',
+ MIN = 'min',
+ P05 = 'p05',
+ P10 = 'p10',
+ P20 = 'p20',
+ P25 = 'p25',
+ P50 = 'p50',
+ P75 = 'p75',
+ P90 = 'p90',
+ P95 = 'p95',
+ P99 = 'p99',
+ RATE = 'rate',
+ SUM_RATE = 'sum_rate',
+ AVG_RATE = 'avg_rate',
+ MAX_RATE = 'max_rate',
+ MIN_RATE = 'min_rate',
+ RATE_SUM = 'rate_sum',
+ RATE_AVG = 'rate_avg',
+ RATE_MIN = 'rate_min',
+ RATE_MAX = 'rate_max',
+ HIST_QUANTILE_50 = 'hist_quantile_50',
+ HIST_QUANTILE_75 = 'hist_quantile_75',
+ HIST_QUANTILE_90 = 'hist_quantile_90',
+ HIST_QUANTILE_95 = 'hist_quantile_95',
+ HIST_QUANTILE_99 = 'hist_quantile_99',
+}
+
+// TODO: add boolean operators from backend
+export enum BoolOperators {
+ NOOP = 'noop',
+ COUNT = 'count',
+ COUNT_DISTINCT = 'count_distinct',
+}
+
+export enum MetricAggregateOperator {
+ NOOP = 'noop',
+ COUNT = 'count',
+ COUNT_DISTINCT = 'count_distinct',
+ SUM = 'sum',
+ AVG = 'avg',
+ MAX = 'max',
+ MIN = 'min',
+ P05 = 'p05',
+ P10 = 'p10',
+ P20 = 'p20',
+ P25 = 'p25',
+ P50 = 'p50',
+ P75 = 'p75',
+ P90 = 'p90',
+ P95 = 'p95',
+ P99 = 'p99',
+ RATE = 'rate',
+ SUM_RATE = 'sum_rate',
+ AVG_RATE = 'avg_rate',
+ MAX_RATE = 'max_rate',
+ MIN_RATE = 'min_rate',
+ RATE_SUM = 'rate_sum',
+ RATE_AVG = 'rate_avg',
+ RATE_MIN = 'rate_min',
+ RATE_MAX = 'rate_max',
+ HIST_QUANTILE_50 = 'hist_quantile_50',
+ HIST_QUANTILE_75 = 'hist_quantile_75',
+ HIST_QUANTILE_90 = 'hist_quantile_90',
+ HIST_QUANTILE_95 = 'hist_quantile_95',
+ HIST_QUANTILE_99 = 'hist_quantile_99',
+}
+
+export enum TracesAggregatorOperator {
+ NOOP = 'noop',
+ COUNT = 'count',
+ COUNT_DISTINCT = 'count_distinct',
+ SUM = 'sum',
+ AVG = 'avg',
+ MAX = 'max',
+ MIN = 'min',
+ P05 = 'p05',
+ P10 = 'p10',
+ P20 = 'p20',
+ P25 = 'p25',
+ P50 = 'p50',
+ P75 = 'p75',
+ P90 = 'p90',
+ P95 = 'p95',
+ P99 = 'p99',
+ RATE = 'rate',
+}
+
+export enum LogsAggregatorOperator {
+ NOOP = 'noop',
+ COUNT = 'count',
+ COUNT_DISTINCT = 'count_distinct',
+ SUM = 'sum',
+ AVG = 'avg',
+ MAX = 'max',
+ MIN = 'min',
+ P05 = 'p05',
+ P10 = 'p10',
+ P20 = 'p20',
+ P25 = 'p25',
+ P50 = 'p50',
+ P75 = 'p75',
+ P90 = 'p90',
+ P95 = 'p95',
+ P99 = 'p99',
+ RATE = 'rate',
+}
+
+export enum EReduceOperator {
+ LATEST_OF_VALUES_IN_TIMEFRAME = 'Latest of values in timeframe',
+ 'SUM_OF_VALUES_IN_TIMEFRAME' = 'Sum of values in timeframe',
+ 'AVERAGE_OF_VALUES_IN_TIMEFRAME' = 'Average of values in timeframe',
+ 'MAX_OF_VALUES_IN_TIMEFRAME' = 'Max of values in timeframe',
+ 'MIN_OF_VALUES_IN_TIMEFRAME' = 'Min of values in timeframe',
+}
+
+export type QueryBuilderData = {
+ queryData: IBuilderQueryForm[];
+ queryFormulas: IBuilderFormula[];
+};
+
+// ** TODO: temporary types for context, fix it during development
+export type QueryBuilderContextType = {
+ queryBuilderData: QueryBuilderData;
+ initialDataSource: DataSource | null;
+ resetQueryBuilderData: () => void;
+ handleSetQueryData: (index: number, queryData: IBuilderQueryForm) => void;
+ handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
+ initQueryBuilderData: (queryBuilderData: QueryBuilderData) => void;
+ setupInitialDataSource: (newInitialDataSource: DataSource | null) => void;
+ removeEntityByIndex: (type: keyof QueryBuilderData, index: number) => void;
+ addNewQuery: () => void;
+ addNewFormula: () => void;
+};
diff --git a/frontend/src/types/reducer/metrics.ts b/frontend/src/types/reducer/metrics.ts
index 7903b2c21a..5a7554a725 100644
--- a/frontend/src/types/reducer/metrics.ts
+++ b/frontend/src/types/reducer/metrics.ts
@@ -1,4 +1,3 @@
-import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types';
import { DBOverView } from 'types/api/metrics/getDBOverview';
import { ExternalAverageDuration } from 'types/api/metrics/getExternalAverageDuration';
import { ExternalError } from 'types/api/metrics/getExternalError';
@@ -19,8 +18,6 @@ interface MetricReducer {
externalAverageDuration: ExternalAverageDuration[];
externalError: ExternalError[];
serviceOverview: ServiceOverview[];
- resourceAttributeQueries: IResourceAttributeQuery[];
- resourceAttributePromQLQuery: string;
topLevelOperations: string[];
}
diff --git a/frontend/src/utils/checkStringEndsWithSpace.ts b/frontend/src/utils/checkStringEndsWithSpace.ts
new file mode 100644
index 0000000000..99769b151b
--- /dev/null
+++ b/frontend/src/utils/checkStringEndsWithSpace.ts
@@ -0,0 +1,4 @@
+export const checkStringEndsWithSpace = (str: string): boolean => {
+ const endSpace = / $/;
+ return endSpace.test(str);
+};
diff --git a/frontend/src/utils/getCountOfSpace.ts b/frontend/src/utils/getCountOfSpace.ts
new file mode 100644
index 0000000000..168520afd2
--- /dev/null
+++ b/frontend/src/utils/getCountOfSpace.ts
@@ -0,0 +1 @@
+export const getCountOfSpace = (s: string): number => s.split(' ').length - 1;
diff --git a/frontend/src/utils/getSearchParams.ts b/frontend/src/utils/getSearchParams.ts
new file mode 100644
index 0000000000..7de4457f75
--- /dev/null
+++ b/frontend/src/utils/getSearchParams.ts
@@ -0,0 +1,9 @@
+export const getSearchParams = (newParams: {
+ [key: string]: string;
+}): URLSearchParams => {
+ const params = new URLSearchParams();
+ Object.entries(newParams).forEach(([key, value]) => {
+ params.set(key, value);
+ });
+ return params;
+};
diff --git a/frontend/src/utils/separateSearchValue.ts b/frontend/src/utils/separateSearchValue.ts
new file mode 100644
index 0000000000..4499c478a8
--- /dev/null
+++ b/frontend/src/utils/separateSearchValue.ts
@@ -0,0 +1,12 @@
+import { OPERATORS } from 'constants/queryBuilder';
+
+export const separateSearchValue = (
+ value: string,
+): [string, string, string[]] => {
+ const separatedString = value.split(' ');
+ const [key, operator, ...result] = separatedString;
+ if (operator === OPERATORS.IN || operator === OPERATORS.NIN) {
+ return [key, operator, result];
+ }
+ return [key, operator, Array(result.join(' '))];
+};
diff --git a/frontend/src/utils/transformToUpperCase.ts b/frontend/src/utils/transformToUpperCase.ts
new file mode 100644
index 0000000000..95a5f48adb
--- /dev/null
+++ b/frontend/src/utils/transformToUpperCase.ts
@@ -0,0 +1,2 @@
+export const transformToUpperCase = (str: string): string =>
+ str.charAt(0).toUpperCase() + str.slice(1);
diff --git a/pkg/query-service/app/clickhouseReader/options.go b/pkg/query-service/app/clickhouseReader/options.go
index da5a87ecd2..71388ad1ee 100644
--- a/pkg/query-service/app/clickhouseReader/options.go
+++ b/pkg/query-service/app/clickhouseReader/options.go
@@ -26,13 +26,15 @@ const (
defaultDurationTable string = "distributed_durationSort"
defaultUsageExplorerTable string = "distributed_usage_explorer"
defaultSpansTable string = "distributed_signoz_spans"
- defaultDependencyGraphTable string = "distributed_dependency_graph_minutes"
+ defaultDependencyGraphTable string = "distributed_dependency_graph_minutes_v2"
defaultTopLevelOperationsTable string = "distributed_top_level_operations"
+ defaultSpanAttributeTable string = "distributed_span_attributes"
defaultLogsDB string = "signoz_logs"
defaultLogsTable string = "distributed_logs"
defaultLogsLocalTable string = "logs"
defaultLogAttributeKeysTable string = "distributed_logs_attribute_keys"
defaultLogResourceKeysTable string = "distributed_logs_resource_keys"
+ defaultLogTagAttributeTable string = "distributed_tag_attributes"
defaultLiveTailRefreshSeconds int = 10
defaultWriteBatchDelay time.Duration = 5 * time.Second
defaultWriteBatchSize int = 10000
@@ -62,6 +64,7 @@ type namespaceConfig struct {
UsageExplorerTable string
SpansTable string
ErrorTable string
+ SpanAttributeTable string
DependencyGraphTable string
TopLevelOperationsTable string
LogsDB string
@@ -69,6 +72,7 @@ type namespaceConfig struct {
LogsLocalTable string
LogsAttributeKeysTable string
LogsResourceKeysTable string
+ LogsTagAttributeTable string
LiveTailRefreshSeconds int
WriteBatchDelay time.Duration
WriteBatchSize int
@@ -130,6 +134,7 @@ func NewOptions(datasource string, primaryNamespace string, otherNamespaces ...s
DurationTable: defaultDurationTable,
UsageExplorerTable: defaultUsageExplorerTable,
SpansTable: defaultSpansTable,
+ SpanAttributeTable: defaultSpanAttributeTable,
DependencyGraphTable: defaultDependencyGraphTable,
TopLevelOperationsTable: defaultTopLevelOperationsTable,
LogsDB: defaultLogsDB,
@@ -137,6 +142,7 @@ func NewOptions(datasource string, primaryNamespace string, otherNamespaces ...s
LogsLocalTable: defaultLogsLocalTable,
LogsAttributeKeysTable: defaultLogAttributeKeysTable,
LogsResourceKeysTable: defaultLogResourceKeysTable,
+ LogsTagAttributeTable: defaultLogTagAttributeTable,
LiveTailRefreshSeconds: defaultLiveTailRefreshSeconds,
WriteBatchDelay: defaultWriteBatchDelay,
WriteBatchSize: defaultWriteBatchSize,
diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go
index e3e5fc47d7..e2940c6a23 100644
--- a/pkg/query-service/app/clickhouseReader/reader.go
+++ b/pkg/query-service/app/clickhouseReader/reader.go
@@ -3,6 +3,7 @@ package clickhouseReader
import (
"bytes"
"context"
+ "database/sql"
"encoding/json"
"fmt"
@@ -41,6 +42,7 @@ import (
promModel "github.com/prometheus/common/model"
"go.signoz.io/signoz/pkg/query-service/app/logs"
+ "go.signoz.io/signoz/pkg/query-service/app/services"
"go.signoz.io/signoz/pkg/query-service/constants"
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
"go.signoz.io/signoz/pkg/query-service/interfaces"
@@ -93,6 +95,7 @@ type ClickHouseReader struct {
errorTable string
usageExplorerTable string
SpansTable string
+ spanAttributeTable string
dependencyGraphTable string
topLevelOperationsTable string
logsDB string
@@ -100,6 +103,7 @@ type ClickHouseReader struct {
logsLocalTable string
logsAttributeKeys string
logsResourceKeys string
+ logsTagAttributeTable string
queryEngine *promql.Engine
remoteStorage *remote.Storage
fanoutStorage *storage.Storage
@@ -142,6 +146,7 @@ func NewReader(localDB *sqlx.DB, configFile string, featureFlag interfaces.Featu
usageExplorerTable: options.primary.UsageExplorerTable,
durationTable: options.primary.DurationTable,
SpansTable: options.primary.SpansTable,
+ spanAttributeTable: options.primary.SpanAttributeTable,
dependencyGraphTable: options.primary.DependencyGraphTable,
topLevelOperationsTable: options.primary.TopLevelOperationsTable,
logsDB: options.primary.LogsDB,
@@ -149,6 +154,7 @@ func NewReader(localDB *sqlx.DB, configFile string, featureFlag interfaces.Featu
logsLocalTable: options.primary.LogsLocalTable,
logsAttributeKeys: options.primary.LogsAttributeKeysTable,
logsResourceKeys: options.primary.LogsResourceKeysTable,
+ logsTagAttributeTable: options.primary.LogsTagAttributeTable,
liveTailRefreshSeconds: options.primary.LiveTailRefreshSeconds,
promConfigFile: configFile,
featureFlags: featureFlag,
@@ -1996,20 +2002,22 @@ func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams *
sum(total_count)/ @duration AS callRate,
sum(error_count)/sum(total_count) * 100 as errorRate
FROM %s.%s
- WHERE toUInt64(toDateTime(timestamp)) >= @start AND toUInt64(toDateTime(timestamp)) <= @end
- GROUP BY
- src,
- dest`,
+ WHERE toUInt64(toDateTime(timestamp)) >= @start AND toUInt64(toDateTime(timestamp)) <= @end`,
r.TraceDB, r.dependencyGraphTable,
)
+ tags := createTagQueryFromTagQueryParams(queryParams.Tags)
+ filterQuery, filterArgs := services.BuildServiceMapQuery(tags)
+ query += filterQuery + " GROUP BY src, dest;"
+ args = append(args, filterArgs...)
+
zap.S().Debug(query, args)
err := r.db.Select(ctx, &response, query, args...)
if err != nil {
zap.S().Error("Error in processing sql query: ", err)
- return nil, fmt.Errorf("Error in processing sql query")
+ return nil, fmt.Errorf("error in processing sql query %w", err)
}
return &response, nil
@@ -2778,7 +2786,7 @@ func (r *ClickHouseReader) GetErrorFromErrorID(ctx context.Context, queryParams
}
var getErrorWithSpanReponse []model.ErrorWithSpan
- query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID AND errorID = @errorID LIMIT 1", r.TraceDB, r.errorTable)
+ query := fmt.Sprintf("SELECT errorID, exceptionType, exceptionStacktrace, exceptionEscaped, exceptionMessage, timestamp, spanID, traceID, serviceName, groupID FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID AND errorID = @errorID LIMIT 1", r.TraceDB, r.errorTable)
args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))}
err := r.db.Select(ctx, &getErrorWithSpanReponse, query, args...)
@@ -2801,7 +2809,7 @@ func (r *ClickHouseReader) GetErrorFromGroupID(ctx context.Context, queryParams
var getErrorWithSpanReponse []model.ErrorWithSpan
- query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID LIMIT 1", r.TraceDB, r.errorTable)
+ query := fmt.Sprintf("SELECT errorID, exceptionType, exceptionStacktrace, exceptionEscaped, exceptionMessage, timestamp, spanID, traceID, serviceName, groupID FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID LIMIT 1", r.TraceDB, r.errorTable)
args := []interface{}{clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))}
err := r.db.Select(ctx, &getErrorWithSpanReponse, query, args...)
@@ -3382,7 +3390,7 @@ func (r *ClickHouseReader) GetLogFields(ctx context.Context) (*model.GetFieldsRe
func extractSelectedAndInterestingFields(tableStatement string, fieldType string, fields *[]model.LogField, response *model.GetFieldsResponse) {
for _, field := range *fields {
field.Type = fieldType
- if strings.Contains(tableStatement, fmt.Sprintf("INDEX %s_idx", field.Name)) {
+ if isSelectedField(tableStatement, field.Name) {
response.Selected = append(response.Selected, field)
} else {
response.Interesting = append(response.Interesting, field)
@@ -3390,6 +3398,10 @@ func extractSelectedAndInterestingFields(tableStatement string, fieldType string
}
}
+func isSelectedField(tableStatement, field string) bool {
+ return strings.Contains(tableStatement, fmt.Sprintf("INDEX %s_idx", field))
+}
+
func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.UpdateField) *model.ApiError {
// if a field is selected it means that the field needs to be indexed
if field.Selected {
@@ -3703,8 +3715,9 @@ func (r *ClickHouseReader) GetMetricAggregateAttributes(ctx context.Context, req
}
key := v3.AttributeKey{
Key: metricName,
- DataType: v3.AttributeKeyDataTypeNumber,
- Type: v3.AttributeKeyTypeTag,
+ DataType: v3.AttributeKeyDataTypeFloat64,
+ Type: v3.AttributeKeyTypeUnspecified,
+ IsColumn: true,
}
response.AttributeKeys = append(response.AttributeKeys, key)
}
@@ -3740,6 +3753,7 @@ func (r *ClickHouseReader) GetMetricAttributeKeys(ctx context.Context, req *v3.F
Key: attributeKey,
DataType: v3.AttributeKeyDataTypeString, // https://github.com/OpenObservability/OpenMetrics/blob/main/proto/openmetrics_data_model.proto#L64-L72.
Type: v3.AttributeKeyTypeTag,
+ IsColumn: false,
}
response.AttributeKeys = append(response.AttributeKeys, key)
}
@@ -3779,6 +3793,249 @@ func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, req *v3
return &attributeValues, nil
}
+func isColumn(tableStatement, field string) bool {
+ return strings.Contains(tableStatement, fmt.Sprintf("`%s` ", field))
+}
+
+func (r *ClickHouseReader) GetLogAggregateAttributes(ctx context.Context, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error) {
+
+ var query string
+ var err error
+ var rows driver.Rows
+ var response v3.AggregateAttributeResponse
+ var stringAllowed bool
+
+ where := ""
+ switch req.Operator {
+ case
+ v3.AggregateOperatorCountDistinct,
+ v3.AggregateOpeatorCount:
+ where = "tagKey ILIKE $1"
+ stringAllowed = true
+ case
+ v3.AggregateOperatorRateSum,
+ v3.AggregateOperatorRateMax,
+ v3.AggregateOperatorRateAvg,
+ v3.AggregateOperatorRate,
+ v3.AggregateOperatorRateMin,
+ v3.AggregateOperatorP05,
+ v3.AggregateOperatorP10,
+ v3.AggregateOperatorP20,
+ v3.AggregateOperatorP25,
+ v3.AggregateOperatorP50,
+ v3.AggregateOperatorP75,
+ v3.AggregateOperatorP90,
+ v3.AggregateOperatorP95,
+ v3.AggregateOperatorP99,
+ v3.AggregateOperatorAvg,
+ v3.AggregateOperatorSum,
+ v3.AggregateOperatorMin,
+ v3.AggregateOperatorMax:
+ where = "tagKey ILIKE $1 AND (tagDataType='int64' or tagDataType='float64')"
+ stringAllowed = false
+ case
+ v3.AggregateOperatorNoOp:
+ return &v3.AggregateAttributeResponse{}, nil
+ default:
+ return nil, fmt.Errorf("unsupported aggregate operator")
+ }
+
+ query = fmt.Sprintf("SELECT DISTINCT(tagKey), tagType, tagDataType from %s.%s WHERE %s limit $2", r.logsDB, r.logsTagAttributeTable, where)
+ rows, err = r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText), req.Limit)
+ if err != nil {
+ zap.S().Error(err)
+ return nil, fmt.Errorf("error while executing query: %s", err.Error())
+ }
+ defer rows.Close()
+
+ statements := []model.ShowCreateTableStatement{}
+ query = fmt.Sprintf("SHOW CREATE TABLE %s.%s", r.logsDB, r.logsLocalTable)
+ err = r.db.Select(ctx, &statements, query)
+ if err != nil {
+ return nil, fmt.Errorf("error while fetching logs schema: %s", err.Error())
+ }
+
+ var tagKey string
+ var dataType string
+ var attType string
+ for rows.Next() {
+ if err := rows.Scan(&tagKey, &attType, &dataType); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ key := v3.AttributeKey{
+ Key: tagKey,
+ DataType: v3.AttributeKeyDataType(dataType),
+ Type: v3.AttributeKeyType(attType),
+ IsColumn: isColumn(statements[0].Statement, tagKey),
+ }
+ response.AttributeKeys = append(response.AttributeKeys, key)
+ }
+ // add other attributes
+ for _, field := range constants.StaticFieldsLogsV3 {
+ if !stringAllowed && field.DataType == v3.AttributeKeyDataTypeString {
+ continue
+ } else if len(req.SearchText) == 0 || strings.Contains(field.Key, req.SearchText) {
+ field.IsColumn = isColumn(statements[0].Statement, field.Key)
+ response.AttributeKeys = append(response.AttributeKeys, field)
+ }
+ }
+
+ return &response, nil
+}
+
+func (r *ClickHouseReader) GetLogAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
+ var query string
+ var err error
+ var rows driver.Rows
+ var response v3.FilterAttributeKeyResponse
+
+ if len(req.SearchText) != 0 {
+ query = fmt.Sprintf("select distinct tagKey, tagType, tagDataType from %s.%s where tagKey ILIKE $1 limit $2", r.logsDB, r.logsTagAttributeTable)
+ rows, err = r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText), req.Limit)
+ } else {
+ query = fmt.Sprintf("select distinct tagKey, tagType, tagDataType from %s.%s limit $1", r.logsDB, r.logsTagAttributeTable)
+ rows, err = r.db.Query(ctx, query, req.Limit)
+ }
+
+ if err != nil {
+ zap.S().Error(err)
+ return nil, fmt.Errorf("error while executing query: %s", err.Error())
+ }
+ defer rows.Close()
+
+ statements := []model.ShowCreateTableStatement{}
+ query = fmt.Sprintf("SHOW CREATE TABLE %s.%s", r.logsDB, r.logsLocalTable)
+ err = r.db.Select(ctx, &statements, query)
+ if err != nil {
+ return nil, fmt.Errorf("error while fetching logs schema: %s", err.Error())
+ }
+
+ var attributeKey string
+ var attributeDataType string
+ var tagType string
+ for rows.Next() {
+ if err := rows.Scan(&attributeKey, &tagType, &attributeDataType); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+
+ key := v3.AttributeKey{
+ Key: attributeKey,
+ DataType: v3.AttributeKeyDataType(attributeDataType),
+ Type: v3.AttributeKeyType(tagType),
+ IsColumn: isColumn(statements[0].Statement, attributeKey),
+ }
+
+ response.AttributeKeys = append(response.AttributeKeys, key)
+ }
+
+ // add other attributes
+ for _, f := range constants.StaticFieldsLogsV3 {
+ if len(req.SearchText) == 0 || strings.Contains(f.Key, req.SearchText) {
+ f.IsColumn = isColumn(statements[0].Statement, f.Key)
+ response.AttributeKeys = append(response.AttributeKeys, f)
+ }
+ }
+
+ return &response, nil
+}
+
+func (r *ClickHouseReader) GetLogAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
+ var err error
+ var filterValueColumn string
+ var rows driver.Rows
+ var attributeValues v3.FilterAttributeValueResponse
+
+ // if dataType or tagType is not present return empty response
+ if len(req.FilterAttributeKeyDataType) == 0 || len(req.TagType) == 0 || req.FilterAttributeKey == "body" {
+ return &v3.FilterAttributeValueResponse{}, nil
+ }
+
+ // if data type is bool, return true and false
+ if req.FilterAttributeKeyDataType == v3.AttributeKeyDataTypeBool {
+ return &v3.FilterAttributeValueResponse{
+ BoolAttributeValues: []bool{true, false},
+ }, nil
+ }
+
+ query := "select distinct"
+ switch req.FilterAttributeKeyDataType {
+ case v3.AttributeKeyDataTypeInt64:
+ filterValueColumn = "int64TagValue"
+ case v3.AttributeKeyDataTypeFloat64:
+ filterValueColumn = "float64TagValue"
+ case v3.AttributeKeyDataTypeString:
+ filterValueColumn = "stringTagValue"
+ }
+
+ searchText := fmt.Sprintf("%%%s%%", req.SearchText)
+
+ // check if the tagKey is a topLevelColumn
+ if _, ok := constants.LogsTopLevelColumnsV3[req.FilterAttributeKey]; ok {
+ // query the column for the last 48 hours
+ filterValueColumnWhere := req.FilterAttributeKey
+ selectKey := req.FilterAttributeKey
+ if req.FilterAttributeKeyDataType != v3.AttributeKeyDataTypeString {
+ filterValueColumnWhere = fmt.Sprintf("toString(%s)", req.FilterAttributeKey)
+ selectKey = fmt.Sprintf("toInt64(%s)", req.FilterAttributeKey)
+ }
+
+ // prepare the query and run
+ if len(req.SearchText) != 0 {
+ query = fmt.Sprintf("select distinct %s from %s.%s where timestamp >= toInt64(toUnixTimestamp(now() - INTERVAL 48 HOUR)*1000000000) and %s ILIKE $1 limit $2", selectKey, r.logsDB, r.logsTable, filterValueColumnWhere)
+ rows, err = r.db.Query(ctx, query, searchText, req.Limit)
+ } else {
+ query = fmt.Sprintf("select distinct %s from %s.%s where timestamp >= toInt64(toUnixTimestamp(now() - INTERVAL 48 HOUR)*1000000000) limit $1", selectKey, r.logsDB, r.logsTable)
+ rows, err = r.db.Query(ctx, query, req.Limit)
+ }
+ } else if len(req.SearchText) != 0 {
+ filterValueColumnWhere := filterValueColumn
+ if req.FilterAttributeKeyDataType != v3.AttributeKeyDataTypeString {
+ filterValueColumnWhere = fmt.Sprintf("toString(%s)", filterValueColumn)
+ }
+ query = fmt.Sprintf("select distinct %s from %s.%s where tagKey=$1 and %s ILIKE $2 and tagType=$3 limit $4", filterValueColumn, r.logsDB, r.logsTagAttributeTable, filterValueColumnWhere)
+ rows, err = r.db.Query(ctx, query, req.FilterAttributeKey, searchText, req.TagType, req.Limit)
+ } else {
+ query = fmt.Sprintf("select distinct %s from %s.%s where tagKey=$1 and tagType=$2 limit $3", filterValueColumn, r.logsDB, r.logsTagAttributeTable)
+ rows, err = r.db.Query(ctx, query, req.FilterAttributeKey, req.TagType, req.Limit)
+ }
+
+ if err != nil {
+ zap.S().Error(err)
+ return nil, fmt.Errorf("error while executing query: %s", err.Error())
+ }
+ defer rows.Close()
+
+ var strAttributeValue string
+ var float64AttributeValue sql.NullFloat64
+ var int64AttributeValue sql.NullInt64
+ for rows.Next() {
+ switch req.FilterAttributeKeyDataType {
+ case v3.AttributeKeyDataTypeInt64:
+ if err := rows.Scan(&int64AttributeValue); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ if int64AttributeValue.Valid {
+ attributeValues.NumberAttributeValues = append(attributeValues.NumberAttributeValues, int64AttributeValue.Int64)
+ }
+ case v3.AttributeKeyDataTypeFloat64:
+ if err := rows.Scan(&float64AttributeValue); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ if float64AttributeValue.Valid {
+ attributeValues.NumberAttributeValues = append(attributeValues.NumberAttributeValues, float64AttributeValue.Float64)
+ }
+ case v3.AttributeKeyDataTypeString:
+ if err := rows.Scan(&strAttributeValue); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ attributeValues.StringAttributeValues = append(attributeValues.StringAttributeValues, strAttributeValue)
+ }
+ }
+
+ return &attributeValues, nil
+
+}
+
func readRow(vars []interface{}, columnNames []string) ([]string, map[string]string, v3.Point) {
// Each row will have a value and a timestamp, and an optional list of label values
// example: {Timestamp: ..., Value: ...}
@@ -3918,6 +4175,48 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
return readRowsForTimeSeriesResult(rows, vars, columnNames)
}
+// GetListResultV3 runs the query and returns list of rows
+func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) {
+
+ defer utils.Elapsed("GetListResultV3", query)()
+
+ rows, err := r.db.Query(ctx, query)
+
+ if err != nil {
+ zap.S().Errorf("error while reading time series result %v", err)
+ return nil, err
+ }
+ defer rows.Close()
+
+ var (
+ columnTypes = rows.ColumnTypes()
+ columnNames = rows.Columns()
+ vars = make([]interface{}, len(columnTypes))
+ )
+ for i := range columnTypes {
+ vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
+ }
+
+ var rowList []*v3.Row
+
+ for rows.Next() {
+ if err := rows.Scan(vars...); err != nil {
+ return nil, err
+ }
+ row := map[string]interface{}{}
+ var t time.Time
+ for idx, v := range vars {
+ if columnNames[idx] == "timestamp" {
+ t = time.Unix(0, int64(*v.(*uint64)))
+ }
+ row[columnNames[idx]] = v
+ }
+ rowList = append(rowList, &v3.Row{Timestamp: t, Data: row})
+ }
+
+ return rowList, nil
+
+}
func (r *ClickHouseReader) CheckClickHouse(ctx context.Context) error {
rows, err := r.db.Query(ctx, "SELECT 1")
if err != nil {
@@ -3927,3 +4226,164 @@ func (r *ClickHouseReader) CheckClickHouse(ctx context.Context) error {
return nil
}
+
+func (r *ClickHouseReader) GetTraceAggregateAttributes(ctx context.Context, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error) {
+ var query string
+ var err error
+ var rows driver.Rows
+ var response v3.AggregateAttributeResponse
+ where := ""
+ switch req.Operator {
+ case
+ v3.AggregateOperatorCountDistinct,
+ v3.AggregateOpeatorCount:
+ where = "tagKey ILIKE $1"
+ case
+ v3.AggregateOperatorRateSum,
+ v3.AggregateOperatorRateMax,
+ v3.AggregateOperatorRateAvg,
+ v3.AggregateOperatorRate,
+ v3.AggregateOperatorRateMin,
+ v3.AggregateOperatorP05,
+ v3.AggregateOperatorP10,
+ v3.AggregateOperatorP20,
+ v3.AggregateOperatorP25,
+ v3.AggregateOperatorP50,
+ v3.AggregateOperatorP75,
+ v3.AggregateOperatorP90,
+ v3.AggregateOperatorP95,
+ v3.AggregateOperatorP99,
+ v3.AggregateOperatorAvg,
+ v3.AggregateOperatorSum,
+ v3.AggregateOperatorMin,
+ v3.AggregateOperatorMax:
+ where = "tagKey ILIKE $1 AND dataType='float64'"
+ case
+ v3.AggregateOperatorNoOp:
+ return &v3.AggregateAttributeResponse{}, nil
+ default:
+ return nil, fmt.Errorf("unsupported aggregate operator")
+ }
+ query = fmt.Sprintf("SELECT DISTINCT(tagKey), tagType, dataType, isColumn FROM %s.%s WHERE %s", r.TraceDB, r.spanAttributeTable, where)
+ if req.Limit != 0 {
+ query = query + fmt.Sprintf(" LIMIT %d;", req.Limit)
+ }
+ rows, err = r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText))
+
+ if err != nil {
+ zap.S().Error(err)
+ return nil, fmt.Errorf("error while executing query: %s", err.Error())
+ }
+ defer rows.Close()
+
+ var tagKey string
+ var dataType string
+ var tagType string
+ var isColumn bool
+ for rows.Next() {
+ if err := rows.Scan(&tagKey, &tagType, &dataType, &isColumn); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ key := v3.AttributeKey{
+ Key: tagKey,
+ DataType: v3.AttributeKeyDataType(dataType),
+ Type: v3.AttributeKeyType(tagType),
+ IsColumn: isColumn,
+ }
+ response.AttributeKeys = append(response.AttributeKeys, key)
+ }
+ return &response, nil
+}
+
+func (r *ClickHouseReader) GetTraceAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error) {
+
+ var query string
+ var err error
+ var rows driver.Rows
+ var response v3.FilterAttributeKeyResponse
+
+ query = fmt.Sprintf("SELECT DISTINCT(tagKey), tagType, dataType, isColumn FROM %s.%s WHERE tagKey ILIKE $1", r.TraceDB, r.spanAttributeTable)
+
+ if req.Limit != 0 {
+ query = query + fmt.Sprintf(" LIMIT %d;", req.Limit)
+ }
+ rows, err = r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText))
+
+ if err != nil {
+ zap.S().Error(err)
+ return nil, fmt.Errorf("error while executing query: %s", err.Error())
+ }
+ defer rows.Close()
+
+ var tagKey string
+ var dataType string
+ var tagType string
+ var isColumn bool
+ for rows.Next() {
+ if err := rows.Scan(&tagKey, &tagType, &dataType, &isColumn); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ key := v3.AttributeKey{
+ Key: tagKey,
+ DataType: v3.AttributeKeyDataType(dataType),
+ Type: v3.AttributeKeyType(tagType),
+ IsColumn: isColumn,
+ }
+ response.AttributeKeys = append(response.AttributeKeys, key)
+ }
+ return &response, nil
+}
+
+func (r *ClickHouseReader) GetTraceAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error) {
+
+ var query string
+ var err error
+ var rows driver.Rows
+ var attributeValues v3.FilterAttributeValueResponse
+ // if dataType or tagType is not present return empty response
+ if len(req.FilterAttributeKeyDataType) == 0 || len(req.TagType) == 0 || req.FilterAttributeKey == "body" {
+ return &v3.FilterAttributeValueResponse{}, nil
+ }
+ switch req.FilterAttributeKeyDataType {
+ case v3.AttributeKeyDataTypeString:
+ query = fmt.Sprintf("SELECT DISTINCT stringTagValue from %s.%s WHERE tagKey = $1 AND stringTagValue ILIKE $2 AND tagType=$3 limit $4", r.TraceDB, r.spanAttributeTable)
+ rows, err = r.db.Query(ctx, query, req.FilterAttributeKey, fmt.Sprintf("%%%s%%", req.SearchText), req.TagType, req.Limit)
+ if err != nil {
+ zap.S().Error(err)
+ return nil, fmt.Errorf("error while executing query: %s", err.Error())
+ }
+ defer rows.Close()
+
+ var strAttributeValue string
+ for rows.Next() {
+ if err := rows.Scan(&strAttributeValue); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ attributeValues.StringAttributeValues = append(attributeValues.StringAttributeValues, strAttributeValue)
+ }
+ case v3.AttributeKeyDataTypeFloat64, v3.AttributeKeyDataTypeInt64:
+ query = fmt.Sprintf("SELECT DISTINCT float64TagValue from %s.%s where tagKey = $1 AND toString(float64TagValue) ILIKE $2 AND tagType=$3 limit $4", r.TraceDB, r.spanAttributeTable)
+ rows, err = r.db.Query(ctx, query, req.FilterAttributeKey, fmt.Sprintf("%%%s%%", req.SearchText), req.TagType, req.Limit)
+ if err != nil {
+ zap.S().Error(err)
+ return nil, fmt.Errorf("error while executing query: %s", err.Error())
+ }
+ defer rows.Close()
+
+ var numberAttributeValue sql.NullFloat64
+ for rows.Next() {
+ if err := rows.Scan(&numberAttributeValue); err != nil {
+ return nil, fmt.Errorf("error while scanning rows: %s", err.Error())
+ }
+ if numberAttributeValue.Valid {
+ attributeValues.NumberAttributeValues = append(attributeValues.NumberAttributeValues, numberAttributeValue.Float64)
+ }
+ }
+ case v3.AttributeKeyDataTypeBool:
+ attributeValues.BoolAttributeValues = []bool{true, false}
+ default:
+ return nil, fmt.Errorf("invalid data type")
+ }
+
+ return &attributeValues, nil
+}
diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go
index a617201233..3e0728cc09 100644
--- a/pkg/query-service/app/http_handler.go
+++ b/pkg/query-service/app/http_handler.go
@@ -21,6 +21,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
"go.signoz.io/signoz/pkg/query-service/app/explorer"
"go.signoz.io/signoz/pkg/query-service/app/logs"
+ logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
"go.signoz.io/signoz/pkg/query-service/app/metrics"
metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
"go.signoz.io/signoz/pkg/query-service/app/parser"
@@ -106,12 +107,10 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
builderOpts := queryBuilderOptions{
BuildMetricQuery: metricsv3.PrepareMetricQuery,
- BuildTraceQuery: func(start, end, step int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error) {
- return "", errors.New("not implemented")
- },
- BuildLogQuery: func(start, end, step int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error) {
+ BuildTraceQuery: func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error) {
return "", errors.New("not implemented")
},
+ BuildLogQuery: logsv3.PrepareLogsQuery,
}
aH.queryBuilder = NewQueryBuilder(builderOpts)
@@ -255,9 +254,12 @@ func (aH *APIHandler) RegisterMetricsRoutes(router *mux.Router, am *AuthMiddlewa
func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *AuthMiddleware) {
subRouter := router.PathPrefix("/api/v3").Subrouter()
- subRouter.HandleFunc("/autocomplete/aggregate_attributes", am.ViewAccess(aH.autocompleteAggregateAttributes)).Methods(http.MethodGet)
- subRouter.HandleFunc("/autocomplete/attribute_keys", am.ViewAccess(aH.autoCompleteAttributeKeys)).Methods(http.MethodGet)
- subRouter.HandleFunc("/autocomplete/attribute_values", am.ViewAccess(aH.autoCompleteAttributeValues)).Methods(http.MethodGet)
+ subRouter.HandleFunc("/autocomplete/aggregate_attributes", am.ViewAccess(
+ withCacheControl(AutoCompleteCacheControlAge, aH.autocompleteAggregateAttributes))).Methods(http.MethodGet)
+ subRouter.HandleFunc("/autocomplete/attribute_keys", am.ViewAccess(
+ withCacheControl(AutoCompleteCacheControlAge, aH.autoCompleteAttributeKeys))).Methods(http.MethodGet)
+ subRouter.HandleFunc("/autocomplete/attribute_values", am.ViewAccess(
+ withCacheControl(AutoCompleteCacheControlAge, aH.autoCompleteAttributeValues))).Methods(http.MethodGet)
subRouter.HandleFunc("/query_range", am.ViewAccess(aH.QueryRangeV3)).Methods(http.MethodPost)
}
@@ -2366,9 +2368,9 @@ func (aH *APIHandler) autocompleteAggregateAttributes(w http.ResponseWriter, r *
case v3.DataSourceMetrics:
response, err = aH.reader.GetMetricAggregateAttributes(r.Context(), req)
case v3.DataSourceLogs:
- // TODO: implement
+ response, err = aH.reader.GetLogAggregateAttributes(r.Context(), req)
case v3.DataSourceTraces:
- // TODO: implement
+ response, err = aH.reader.GetTraceAggregateAttributes(r.Context(), req)
default:
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
return
@@ -2395,9 +2397,9 @@ func (aH *APIHandler) autoCompleteAttributeKeys(w http.ResponseWriter, r *http.R
case v3.DataSourceMetrics:
response, err = aH.reader.GetMetricAttributeKeys(r.Context(), req)
case v3.DataSourceLogs:
- // TODO: implement
+ response, err = aH.reader.GetLogAttributeKeys(r.Context(), req)
case v3.DataSourceTraces:
- // TODO: implement
+ response, err = aH.reader.GetTraceAttributeKeys(r.Context(), req)
default:
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
return
@@ -2424,9 +2426,9 @@ func (aH *APIHandler) autoCompleteAttributeValues(w http.ResponseWriter, r *http
case v3.DataSourceMetrics:
response, err = aH.reader.GetMetricAttributeValues(r.Context(), req)
case v3.DataSourceLogs:
- // TODO: implement
+ response, err = aH.reader.GetLogAttributeValues(r.Context(), req)
case v3.DataSourceTraces:
- // TODO: implement
+ response, err = aH.reader.GetTraceAttributeValues(r.Context(), req)
default:
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
return
@@ -2440,7 +2442,7 @@ func (aH *APIHandler) autoCompleteAttributeValues(w http.ResponseWriter, r *http
aH.Respond(w, response)
}
-func (aH *APIHandler) execClickHouseQueries(ctx context.Context, queries map[string]string) ([]*v3.Result, error, map[string]string) {
+func (aH *APIHandler) execClickHouseGraphQueries(ctx context.Context, queries map[string]string) ([]*v3.Result, error, map[string]string) {
type channelResult struct {
Series []*v3.Series
Err error
@@ -2489,6 +2491,55 @@ func (aH *APIHandler) execClickHouseQueries(ctx context.Context, queries map[str
return res, nil, nil
}
+func (aH *APIHandler) execClickHouseListQueries(ctx context.Context, queries map[string]string) ([]*v3.Result, error, map[string]string) {
+ type channelResult struct {
+ List []*v3.Row
+ Err error
+ Name string
+ Query string
+ }
+
+ ch := make(chan channelResult, len(queries))
+ var wg sync.WaitGroup
+
+ for name, query := range queries {
+ wg.Add(1)
+ go func(name, query string) {
+ defer wg.Done()
+ rowList, err := aH.reader.GetListResultV3(ctx, query)
+
+ if err != nil {
+ ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
+ return
+ }
+ ch <- channelResult{List: rowList, Name: name, Query: query}
+ }(name, query)
+ }
+
+ wg.Wait()
+ close(ch)
+
+ var errs []error
+ errQuriesByName := make(map[string]string)
+ res := make([]*v3.Result, 0)
+ // read values from the channel
+ for r := range ch {
+ if r.Err != nil {
+ errs = append(errs, r.Err)
+ errQuriesByName[r.Name] = r.Query
+ continue
+ }
+ res = append(res, &v3.Result{
+ QueryName: r.Name,
+ List: r.List,
+ })
+ }
+ if len(errs) != 0 {
+ return nil, fmt.Errorf("encountered multiple errors: %s", multierr.Combine(errs...)), errQuriesByName
+ }
+ return res, nil, nil
+}
+
func (aH *APIHandler) execPromQueries(ctx context.Context, metricsQueryRangeParams *v3.QueryRangeParamsV3) ([]*v3.Result, error, map[string]string) {
type channelResult struct {
Series []*v3.Series
@@ -2569,19 +2620,83 @@ func (aH *APIHandler) execPromQueries(ctx context.Context, metricsQueryRangePara
return res, nil, nil
}
-func (aH *APIHandler) queryRangeV3(queryRangeParams *v3.QueryRangeParamsV3, w http.ResponseWriter, r *http.Request) {
+func (aH *APIHandler) getLogFieldsV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3) (map[string]v3.AttributeKey, error) {
+ data := map[string]v3.AttributeKey{}
+ for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
+ if query.DataSource == v3.DataSourceLogs {
+ fields, apiError := aH.reader.GetLogFields(ctx)
+ if apiError != nil {
+ return nil, apiError.Err
+ }
+
+ // top level fields meta will always be present in the frontend. (can be support for that as enchancement)
+ getType := func(t string) (v3.AttributeKeyType, bool) {
+ if t == "attributes" {
+ return v3.AttributeKeyTypeTag, false
+ } else if t == "resources" {
+ return v3.AttributeKeyTypeResource, false
+ }
+ return "", true
+ }
+
+ for _, selectedField := range fields.Selected {
+ fieldType, pass := getType(selectedField.Type)
+ if pass {
+ continue
+ }
+ data[selectedField.Name] = v3.AttributeKey{
+ Key: selectedField.Name,
+ Type: fieldType,
+ DataType: v3.AttributeKeyDataType(strings.ToLower(selectedField.DataType)),
+ IsColumn: true,
+ }
+ }
+ for _, interestingField := range fields.Interesting {
+ fieldType, pass := getType(interestingField.Type)
+ if pass {
+ continue
+ }
+ data[interestingField.Name] = v3.AttributeKey{
+ Key: interestingField.Name,
+ Type: fieldType,
+ DataType: v3.AttributeKeyDataType(strings.ToLower(interestingField.DataType)),
+ IsColumn: false,
+ }
+ }
+ break
+ }
+ }
+ return data, nil
+}
+
+func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3, w http.ResponseWriter, r *http.Request) {
var result []*v3.Result
var err error
var errQuriesByName map[string]string
+ var queries map[string]string
switch queryRangeParams.CompositeQuery.QueryType {
case v3.QueryTypeBuilder:
- queries, err := aH.queryBuilder.prepareQueries(queryRangeParams)
+ // get the fields if any logs query is present
+ var fields map[string]v3.AttributeKey
+ fields, err = aH.getLogFieldsV3(ctx, queryRangeParams)
+ if err != nil {
+ apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err}
+ RespondError(w, apiErrObj, errQuriesByName)
+ return
+ }
+
+ queries, err = aH.queryBuilder.prepareQueries(queryRangeParams, fields)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
- result, err, errQuriesByName = aH.execClickHouseQueries(r.Context(), queries)
+
+ if queryRangeParams.CompositeQuery.PanelType == v3.PanelTypeList {
+ result, err, errQuriesByName = aH.execClickHouseListQueries(r.Context(), queries)
+ } else {
+ result, err, errQuriesByName = aH.execClickHouseGraphQueries(r.Context(), queries)
+ }
case v3.QueryTypeClickHouseSQL:
queries := make(map[string]string)
for name, query := range queryRangeParams.CompositeQuery.ClickHouseQueries {
@@ -2590,7 +2705,7 @@ func (aH *APIHandler) queryRangeV3(queryRangeParams *v3.QueryRangeParamsV3, w ht
}
queries[name] = query.Query
}
- result, err, errQuriesByName = aH.execClickHouseQueries(r.Context(), queries)
+ result, err, errQuriesByName = aH.execClickHouseGraphQueries(r.Context(), queries)
case v3.QueryTypePromQL:
result, err, errQuriesByName = aH.execPromQueries(r.Context(), queryRangeParams)
default:
@@ -2620,5 +2735,5 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
return
}
- aH.queryRangeV3(queryRangeParams, w, r)
+ aH.queryRangeV3(r.Context(), queryRangeParams, w, r)
}
diff --git a/pkg/query-service/app/http_utils.go b/pkg/query-service/app/http_utils.go
new file mode 100644
index 0000000000..2066044836
--- /dev/null
+++ b/pkg/query-service/app/http_utils.go
@@ -0,0 +1,22 @@
+package app
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+)
+
+var (
+ // AutoCompleteCacheControlAge is the max-age for the cache-control header
+ // for the autocomplete endpoint.
+ // The default export interval for the SDK is 60 seconds, and ~10s at collector, so
+ // one minute should be a safe value.
+ AutoCompleteCacheControlAge = 60 * time.Second
+)
+
+func withCacheControl(maxAge time.Duration, h http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", int(maxAge.Seconds())))
+ h(w, r)
+ }
+}
diff --git a/pkg/query-service/app/logs/parser.go b/pkg/query-service/app/logs/parser.go
index 18760ba972..393c418b32 100644
--- a/pkg/query-service/app/logs/parser.go
+++ b/pkg/query-service/app/logs/parser.go
@@ -173,9 +173,9 @@ func parseLogQuery(query string) ([]string, error) {
searchString := strings.TrimSpace(strings.Split(v, op)[1])
operatorRemovedTokens := strings.Split(operatorRegex.ReplaceAllString(v, " "), " ")
- searchCol := strings.ToLower(operatorRemovedTokens[0])
- if searchCol == AND || searchCol == OR {
- searchCol = strings.ToLower(operatorRemovedTokens[1])
+ searchCol := operatorRemovedTokens[0]
+ if strings.ToLower(searchCol) == AND || strings.ToLower(searchCol) == OR {
+ searchCol = operatorRemovedTokens[1]
}
col := searchCol
if strings.ToLower(searchCol) == "fulltext" {
diff --git a/pkg/query-service/app/logs/parser_test.go b/pkg/query-service/app/logs/parser_test.go
index f11419e30f..843de2725c 100644
--- a/pkg/query-service/app/logs/parser_test.go
+++ b/pkg/query-service/app/logs/parser_test.go
@@ -97,6 +97,11 @@ var correctQueriesTest = []struct {
`id.userid in (100) and id_userid gt 50`,
[]string{`id.userid IN (100) `, `and id_userid > 50 `},
},
+ {
+ `filters with case sensitive key name`,
+ `userIdentifier in ('user') and userIdentifier contains 'user'`,
+ []string{`userIdentifier IN ('user') `, `AND userIdentifier ILIKE '%user%' `},
+ },
}
func TestParseLogQueryCorrect(t *testing.T) {
diff --git a/pkg/query-service/app/logs/v3/query_builder.go b/pkg/query-service/app/logs/v3/query_builder.go
new file mode 100644
index 0000000000..19c4efc5a1
--- /dev/null
+++ b/pkg/query-service/app/logs/v3/query_builder.go
@@ -0,0 +1,411 @@
+package v3
+
+import (
+ "fmt"
+ "math"
+ "strings"
+
+ "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"
+)
+
+var aggregateOperatorToPercentile = map[v3.AggregateOperator]float64{
+ v3.AggregateOperatorP05: 0.05,
+ v3.AggregateOperatorP10: 0.10,
+ v3.AggregateOperatorP20: 0.20,
+ v3.AggregateOperatorP25: 0.25,
+ v3.AggregateOperatorP50: 0.50,
+ v3.AggregateOperatorP75: 0.75,
+ v3.AggregateOperatorP90: 0.90,
+ v3.AggregateOperatorP95: 0.95,
+ v3.AggregateOperatorP99: 0.99,
+}
+
+var aggregateOperatorToSQLFunc = map[v3.AggregateOperator]string{
+ v3.AggregateOperatorAvg: "avg",
+ v3.AggregateOperatorMax: "max",
+ v3.AggregateOperatorMin: "min",
+ v3.AggregateOperatorSum: "sum",
+ v3.AggregateOperatorRateSum: "sum",
+ v3.AggregateOperatorRateAvg: "avg",
+ v3.AggregateOperatorRateMax: "max",
+ v3.AggregateOperatorRateMin: "min",
+}
+
+var logOperators = map[v3.FilterOperator]string{
+ v3.FilterOperatorEqual: "=",
+ v3.FilterOperatorNotEqual: "!=",
+ v3.FilterOperatorLessThan: "<",
+ v3.FilterOperatorLessThanOrEq: "<=",
+ v3.FilterOperatorGreaterThan: ">",
+ v3.FilterOperatorGreaterThanOrEq: ">=",
+ v3.FilterOperatorLike: "ILIKE",
+ v3.FilterOperatorNotLike: "NOT ILIKE",
+ v3.FilterOperatorContains: "ILIKE",
+ v3.FilterOperatorNotContains: "NOT ILIKE",
+ v3.FilterOperatorRegex: "REGEXP",
+ v3.FilterOperatorNotRegex: "NOT REGEXP",
+ v3.FilterOperatorIn: "IN",
+ v3.FilterOperatorNotIn: "NOT IN",
+ v3.FilterOperatorExists: "has(%s_%s_key, '%s')",
+ v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')",
+ // (todo) check contains/not contains/
+}
+
+func encrichFieldWithMetadata(field v3.AttributeKey, fields map[string]v3.AttributeKey) (v3.AttributeKey, error) {
+ if field.Type == "" || field.DataType == "" {
+ // check if the field is present in the fields map
+ if existingField, ok := fields[field.Key]; ok {
+ if existingField.IsColumn {
+ return field, nil
+ }
+ field.Type = existingField.Type
+ field.DataType = existingField.DataType
+ } else {
+ return field, fmt.Errorf("field not found to enrich metadata")
+ }
+ }
+ return field, nil
+}
+
+func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
+ if columnType == v3.AttributeKeyTypeTag {
+ return "attributes"
+ }
+ return "resources"
+}
+
+func getClickhouseLogsColumnDataType(columnDataType v3.AttributeKeyDataType) string {
+ if columnDataType == v3.AttributeKeyDataTypeFloat64 {
+ return "float64"
+ }
+ if columnDataType == v3.AttributeKeyDataTypeInt64 {
+ return "int64"
+ }
+ // for bool also we are returning string as we store bool data as string.
+ return "string"
+}
+
+// getClickhouseColumnName returns the corresponding clickhouse column name for the given attribute/resource key
+func getClickhouseColumnName(key v3.AttributeKey, fields map[string]v3.AttributeKey) (string, error) {
+ clickhouseColumn := key.Key
+ //if the key is present in the topLevelColumn then it will be only searched in those columns,
+ //regardless if it is indexed/present again in resource or column attribute
+ var err error
+ _, isTopLevelCol := constants.LogsTopLevelColumnsV3[key.Key]
+ if !isTopLevelCol && !key.IsColumn {
+ key, err = encrichFieldWithMetadata(key, fields)
+ if err != nil {
+ return "", err
+ }
+ columnType := getClickhouseLogsColumnType(key.Type)
+ columnDataType := getClickhouseLogsColumnDataType(key.DataType)
+ clickhouseColumn = fmt.Sprintf("%s_%s_value[indexOf(%s_%s_key, '%s')]", columnType, columnDataType, columnType, columnDataType, key.Key)
+ }
+ return clickhouseColumn, nil
+}
+
+// getSelectLabels returns the select labels for the query based on groupBy and aggregateOperator
+func getSelectLabels(aggregatorOperator v3.AggregateOperator, groupBy []v3.AttributeKey, fields map[string]v3.AttributeKey) (string, error) {
+ var selectLabels string
+ if aggregatorOperator == v3.AggregateOperatorNoOp {
+ selectLabels = ""
+ } else {
+ for _, tag := range groupBy {
+ columnName, err := getClickhouseColumnName(tag, fields)
+ if err != nil {
+ return "", err
+ }
+ selectLabels += fmt.Sprintf(", %s as %s", columnName, tag.Key)
+ }
+ }
+ return selectLabels, nil
+}
+
+func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey, fields map[string]v3.AttributeKey) (string, error) {
+ var conditions []string
+
+ if fs != nil && len(fs.Items) != 0 {
+ for _, item := range fs.Items {
+ toFormat := item.Value
+ op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
+ if logsOp, ok := logOperators[op]; ok {
+ switch op {
+ case v3.FilterOperatorExists, v3.FilterOperatorNotExists:
+ //(todo): refractor this later
+ key, err := encrichFieldWithMetadata(item.Key, fields)
+ if err != nil {
+ return "", err
+ }
+ columnType := getClickhouseLogsColumnType(key.Type)
+ columnDataType := getClickhouseLogsColumnDataType(key.DataType)
+ conditions = append(conditions, fmt.Sprintf(logsOp, columnType, columnDataType, item.Key.Key))
+ case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
+ // generate the key
+ columnName, err := getClickhouseColumnName(item.Key, fields)
+ if err != nil {
+ return "", err
+ }
+ conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, item.Value))
+ default:
+ // generate the key
+ columnName, err := getClickhouseColumnName(item.Key, fields)
+ if err != nil {
+ return "", err
+ }
+ fmtVal := utils.ClickHouseFormattedValue(toFormat)
+ conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, logsOp, fmtVal))
+ }
+ } else {
+ return "", fmt.Errorf("unsupported operator: %s", op)
+ }
+ }
+ }
+
+ // add group by conditions to filter out log lines which doesn't have the key
+ for _, attr := range groupBy {
+ enrichedAttr, err := encrichFieldWithMetadata(attr, fields)
+ if err != nil {
+ return "", err
+ }
+ if !enrichedAttr.IsColumn {
+ columnType := getClickhouseLogsColumnType(enrichedAttr.Type)
+ columnDataType := getClickhouseLogsColumnDataType(enrichedAttr.DataType)
+ conditions = append(conditions, fmt.Sprintf("indexOf(%s_%s_key, '%s') > 0", columnType, columnDataType, enrichedAttr.Key))
+ }
+ }
+
+ queryString := strings.Join(conditions, " AND ")
+
+ if len(queryString) > 0 {
+ queryString = " AND " + queryString
+ }
+ return queryString, nil
+}
+
+// getZerosForEpochNano returns the number of zeros to be appended to the epoch time for converting it to nanoseconds
+func getZerosForEpochNano(epoch int64) int64 {
+ count := 0
+ if epoch == 0 {
+ count = 1
+ } else {
+ for epoch != 0 {
+ epoch /= 10
+ count++
+ }
+ }
+ return int64(math.Pow(10, float64(19-count)))
+}
+
+func buildLogsQuery(start, end, step int64, mq *v3.BuilderQuery, fields map[string]v3.AttributeKey) (string, error) {
+
+ filterSubQuery, err := buildLogsTimeSeriesFilterQuery(mq.Filters, mq.GroupBy, fields)
+ if err != nil {
+ return "", err
+ }
+
+ // timerange will be sent in epoch millisecond
+ timeFilter := fmt.Sprintf("(timestamp >= %d AND timestamp <= %d)", start*getZerosForEpochNano(start), end*getZerosForEpochNano(end))
+
+ selectLabels, err := getSelectLabels(mq.AggregateOperator, mq.GroupBy, fields)
+ if err != nil {
+ return "", err
+ }
+
+ having := having(mq.Having)
+ if having != "" {
+ having = " having " + having
+ }
+
+ queryTmpl :=
+ "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL %d SECOND) AS ts" + selectLabels +
+ ", %s as value " +
+ "from signoz_logs.distributed_logs " +
+ "where " + timeFilter + "%s " +
+ "group by %s%s " +
+ "order by %sts"
+
+ groupBy := groupByAttributeKeyTags(mq.GroupBy...)
+ orderBy := orderByAttributeKeyTags(mq.OrderBy, mq.GroupBy)
+
+ aggregationKey := ""
+ if mq.AggregateAttribute.Key != "" {
+ aggregationKey, err = getClickhouseColumnName(mq.AggregateAttribute, fields)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ switch mq.AggregateOperator {
+ case v3.AggregateOperatorRate:
+ op := fmt.Sprintf("count(%s)/%d", aggregationKey, step)
+ query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy)
+ return query, nil
+ case
+ v3.AggregateOperatorRateSum,
+ v3.AggregateOperatorRateMax,
+ v3.AggregateOperatorRateAvg,
+ v3.AggregateOperatorRateMin:
+ op := fmt.Sprintf("%s(%s)/%d", aggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey, step)
+ query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy)
+ return query, nil
+ case
+ v3.AggregateOperatorP05,
+ v3.AggregateOperatorP10,
+ v3.AggregateOperatorP20,
+ v3.AggregateOperatorP25,
+ v3.AggregateOperatorP50,
+ v3.AggregateOperatorP75,
+ v3.AggregateOperatorP90,
+ v3.AggregateOperatorP95,
+ v3.AggregateOperatorP99:
+ op := fmt.Sprintf("quantile(%v)(%s)", aggregateOperatorToPercentile[mq.AggregateOperator], aggregationKey)
+ query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy)
+ return query, nil
+ case v3.AggregateOperatorAvg, v3.AggregateOperatorSum, v3.AggregateOperatorMin, v3.AggregateOperatorMax:
+ op := fmt.Sprintf("%s(%s)", aggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey)
+ query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy)
+ return query, nil
+ case v3.AggregateOpeatorCount:
+ if mq.AggregateAttribute.Key != "" {
+ field, err := encrichFieldWithMetadata(mq.AggregateAttribute, fields)
+ if err != nil {
+ return "", err
+ }
+ columnType := getClickhouseLogsColumnType(field.Type)
+ columnDataType := getClickhouseLogsColumnDataType(field.DataType)
+ filterSubQuery = fmt.Sprintf("%s AND has(%s_%s_key, '%s')", filterSubQuery, columnType, columnDataType, mq.AggregateAttribute.Key)
+ // check having
+ }
+
+ op := "toFloat64(count(*))"
+ query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy)
+ return query, nil
+ case v3.AggregateOperatorCountDistinct:
+ op := fmt.Sprintf("toFloat64(count(distinct(%s)))", aggregationKey)
+ query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy)
+ return query, nil
+ case v3.AggregateOperatorNoOp:
+ queryTmpl := constants.LogsSQLSelect + "from signoz_logs.distributed_logs where %s %s"
+ query := fmt.Sprintf(queryTmpl, timeFilter, filterSubQuery)
+ return query, nil
+ default:
+ return "", fmt.Errorf("unsupported aggregate operator")
+ }
+}
+
+// groupBy returns a string of comma separated tags for group by clause
+// `ts` is always added to the group by clause
+func groupBy(tags ...string) string {
+ tags = append(tags, "ts")
+ return strings.Join(tags, ",")
+}
+
+func groupByAttributeKeyTags(tags ...v3.AttributeKey) string {
+ groupTags := []string{}
+ for _, tag := range tags {
+ groupTags = append(groupTags, tag.Key)
+ }
+ return groupBy(groupTags...)
+}
+
+// orderBy returns a string of comma separated tags for order by clause
+// if the order is not specified, it defaults to ASC
+func orderBy(items []v3.OrderBy, tags []string) string {
+ var orderBy []string
+ for _, tag := range tags {
+ found := false
+ for _, item := range items {
+ if item.ColumnName == tag {
+ found = true
+ orderBy = append(orderBy, fmt.Sprintf("%s %s", item.ColumnName, item.Order))
+ break
+ }
+ }
+ if !found {
+ orderBy = append(orderBy, fmt.Sprintf("%s ASC", tag))
+ }
+ }
+
+ // users might want to order by value of aggreagation
+ for _, item := range items {
+ if item.ColumnName == constants.SigNozOrderByValue {
+ orderBy = append(orderBy, fmt.Sprintf("value %s", item.Order))
+ }
+ }
+ return strings.Join(orderBy, ",")
+}
+
+func orderByAttributeKeyTags(items []v3.OrderBy, tags []v3.AttributeKey) string {
+ var groupTags []string
+ for _, tag := range tags {
+ groupTags = append(groupTags, tag.Key)
+ }
+ str := orderBy(items, groupTags)
+ if len(str) > 0 {
+ str = str + ","
+ }
+ return str
+}
+
+func having(items []v3.Having) string {
+ // aggregate something and filter on that aggregate
+ var having []string
+ for _, item := range items {
+ having = append(having, fmt.Sprintf("value %s %s", item.Operator, utils.ClickHouseFormattedValue(item.Value)))
+ }
+ return strings.Join(having, " AND ")
+}
+
+func reduceQuery(query string, reduceTo v3.ReduceToOperator, aggregateOperator v3.AggregateOperator) (string, error) {
+ // the timestamp picked is not relevant here since the final value used is show the single
+ // chart with just the query value.
+ switch reduceTo {
+ case v3.ReduceToOperatorLast:
+ query = fmt.Sprintf("SELECT anyLast(value) as value, any(ts) as ts FROM (%s)", query)
+ case v3.ReduceToOperatorSum:
+ query = fmt.Sprintf("SELECT sum(value) as value, any(ts) as ts FROM (%s)", query)
+ case v3.ReduceToOperatorAvg:
+ query = fmt.Sprintf("SELECT avg(value) as value, any(ts) as ts FROM (%s)", query)
+ case v3.ReduceToOperatorMax:
+ query = fmt.Sprintf("SELECT max(value) as value, any(ts) as ts FROM (%s)", query)
+ case v3.ReduceToOperatorMin:
+ query = fmt.Sprintf("SELECT min(value) as value, any(ts) as ts FROM (%s)", query)
+ default:
+ return "", fmt.Errorf("unsupported reduce operator")
+ }
+ return query, nil
+}
+
+func addLimitToQuery(query string, limit uint64, panelType v3.PanelType) string {
+ if limit == 0 {
+ limit = 100
+ }
+ if panelType == v3.PanelTypeList {
+ return fmt.Sprintf("%s LIMIT %d", query, limit)
+ }
+ return query
+}
+
+func addOffsetToQuery(query string, offset uint64) string {
+ return fmt.Sprintf("%s OFFSET %d", query, offset)
+}
+
+func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, fields map[string]v3.AttributeKey) (string, error) {
+ query, err := buildLogsQuery(start, end, mq.StepInterval, mq, fields)
+ if err != nil {
+ return "", err
+ }
+ if panelType == v3.PanelTypeValue {
+ query, err = reduceQuery(query, mq.ReduceTo, mq.AggregateOperator)
+ }
+
+ query = addLimitToQuery(query, mq.Limit, panelType)
+
+ if mq.Offset != 0 {
+ query = addOffsetToQuery(query, mq.Offset)
+ }
+
+ return query, err
+}
diff --git a/pkg/query-service/app/logs/v3/query_builder_test.go b/pkg/query-service/app/logs/v3/query_builder_test.go
new file mode 100644
index 0000000000..caff3867f3
--- /dev/null
+++ b/pkg/query-service/app/logs/v3/query_builder_test.go
@@ -0,0 +1,723 @@
+package v3
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+ "go.signoz.io/signoz/pkg/query-service/constants"
+ v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
+)
+
+var testGetClickhouseColumnNameData = []struct {
+ Name string
+ AttributeKey v3.AttributeKey
+ ExpectedColumnName string
+}{
+ {
+ Name: "attribute",
+ AttributeKey: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ ExpectedColumnName: "attributes_string_value[indexOf(attributes_string_key, 'user_name')]",
+ },
+ {
+ Name: "resource",
+ AttributeKey: v3.AttributeKey{Key: "servicename", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource},
+ ExpectedColumnName: "resources_string_value[indexOf(resources_string_key, 'servicename')]",
+ },
+ {
+ Name: "selected field",
+ AttributeKey: v3.AttributeKey{Key: "servicename", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true},
+ ExpectedColumnName: "servicename",
+ },
+ {
+ Name: "top level column",
+ AttributeKey: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ ExpectedColumnName: "trace_id",
+ },
+ {
+ Name: "top level column with isColumn ignored",
+ AttributeKey: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: false},
+ ExpectedColumnName: "trace_id",
+ },
+}
+
+func TestGetClickhouseColumnName(t *testing.T) {
+ for _, tt := range testGetClickhouseColumnNameData {
+ Convey("testGetClickhouseColumnNameData", t, func() {
+ columnName, err := getClickhouseColumnName(tt.AttributeKey, map[string]v3.AttributeKey{})
+ So(err, ShouldBeNil)
+ So(columnName, ShouldEqual, tt.ExpectedColumnName)
+ })
+ }
+}
+
+var testGetSelectLabelsData = []struct {
+ Name string
+ AggregateOperator v3.AggregateOperator
+ GroupByTags []v3.AttributeKey
+ SelectLabels string
+}{
+ {
+ Name: "select fields for groupBy attribute",
+ AggregateOperator: v3.AggregateOpeatorCount,
+ GroupByTags: []v3.AttributeKey{{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ SelectLabels: ", attributes_string_value[indexOf(attributes_string_key, 'user_name')] as user_name",
+ },
+ {
+ Name: "select fields for groupBy resource",
+ AggregateOperator: v3.AggregateOpeatorCount,
+ GroupByTags: []v3.AttributeKey{{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}},
+ SelectLabels: ", resources_string_value[indexOf(resources_string_key, 'user_name')] as user_name",
+ },
+ {
+ Name: "select fields for groupBy attribute and resource",
+ AggregateOperator: v3.AggregateOpeatorCount,
+ GroupByTags: []v3.AttributeKey{
+ {Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource},
+ {Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ },
+ SelectLabels: ", resources_string_value[indexOf(resources_string_key, 'user_name')] as user_name, attributes_string_value[indexOf(attributes_string_key, 'host')] as host",
+ },
+ {
+ Name: "select fields for groupBy materialized columns",
+ AggregateOperator: v3.AggregateOpeatorCount,
+ GroupByTags: []v3.AttributeKey{{Key: "host", IsColumn: true}},
+ SelectLabels: ", host as host",
+ },
+}
+
+func TestGetSelectLabels(t *testing.T) {
+ for _, tt := range testGetSelectLabelsData {
+ Convey("testGetSelectLabelsData", t, func() {
+ selectLabels, err := getSelectLabels(tt.AggregateOperator, tt.GroupByTags, map[string]v3.AttributeKey{})
+ So(err, ShouldBeNil)
+ So(selectLabels, ShouldEqual, tt.SelectLabels)
+ })
+ }
+}
+
+var timeSeriesFilterQueryData = []struct {
+ Name string
+ FilterSet *v3.FilterSet
+ GroupBy []v3.AttributeKey
+ ExpectedFilter string
+}{
+ {
+ Name: "Test attribute and resource attribute",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "john", Operator: "="},
+ {Key: v3.AttributeKey{Key: "k8s_namespace", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "my_service", Operator: "!="},
+ }},
+ ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'user_name')] = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
+ },
+ {
+ Name: "Test materialized column",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "john", Operator: "="},
+ {Key: v3.AttributeKey{Key: "k8s_namespace", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "my_service", Operator: "!="},
+ }},
+ ExpectedFilter: " AND user_name = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
+ },
+ {
+ Name: "Test like",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.%", Operator: "like"},
+ }},
+ ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'host')] ILIKE '102.%'",
+ },
+ {
+ Name: "Test IN",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "bytes", DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag}, Value: []interface{}{1, 2, 3, 4}, Operator: "in"},
+ }},
+ ExpectedFilter: " AND attributes_float64_value[indexOf(attributes_float64_key, 'bytes')] IN [1,2,3,4]",
+ },
+ {
+ Name: "Test DataType int64",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "bytes", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag}, Value: 10, Operator: ">"},
+ }},
+ ExpectedFilter: " AND attributes_int64_value[indexOf(attributes_int64_key, 'bytes')] > 10",
+ },
+ {
+ Name: "Test NOT IN",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: []interface{}{"john", "bunny"}, Operator: "nin"},
+ }},
+ ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'name')] NOT IN ['john','bunny']",
+ },
+ {
+ Name: "Test exists",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "bytes", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "", Operator: "exists"},
+ }},
+ ExpectedFilter: " AND has(attributes_string_key, 'bytes')",
+ },
+ {
+ Name: "Test not exists",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "bytes", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "", Operator: "nexists"},
+ }},
+ ExpectedFilter: " AND not has(attributes_string_key, 'bytes')",
+ },
+ {
+ Name: "Test contains",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.", Operator: "contains"},
+ }},
+ ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'host')] ILIKE '%102.%'",
+ },
+ {
+ Name: "Test not contains",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.", Operator: "ncontains"},
+ }},
+ ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'host')] NOT ILIKE '%102.%'",
+ },
+ {
+ Name: "Test groupBy",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.", Operator: "ncontains"},
+ }},
+ GroupBy: []v3.AttributeKey{{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'host')] NOT ILIKE '%102.%' AND indexOf(attributes_string_key, 'host') > 0",
+ },
+ {
+ Name: "Test groupBy isColumn",
+ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.", Operator: "ncontains"},
+ }},
+ GroupBy: []v3.AttributeKey{{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}},
+ ExpectedFilter: " AND attributes_string_value[indexOf(attributes_string_key, 'host')] NOT ILIKE '%102.%'",
+ },
+}
+
+func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) {
+ for _, tt := range timeSeriesFilterQueryData {
+ Convey("TestBuildLogsTimeSeriesFilterQuery", t, func() {
+ query, err := buildLogsTimeSeriesFilterQuery(tt.FilterSet, tt.GroupBy, map[string]v3.AttributeKey{})
+ So(err, ShouldBeNil)
+ So(query, ShouldEqual, tt.ExpectedFilter)
+ })
+ }
+}
+
+var testBuildLogsQueryData = []struct {
+ Name string
+ Start int64
+ End int64
+ Step int64
+ BuilderQuery *v3.BuilderQuery
+ GroupByTags []v3.AttributeKey
+ TableName string
+ AggregateOperator v3.AggregateOperator
+ ExpectedQuery string
+}{
+ {
+ Name: "Test aggregate count on select field",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateOperator: v3.AggregateOpeatorCount,
+ Expression: "A",
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts order by ts",
+ },
+ {
+ Name: "Test aggregate count on a attribute",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ AggregateOperator: v3.AggregateOpeatorCount,
+ Expression: "A",
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND has(attributes_string_key, 'user_name') group by ts order by ts",
+ },
+ {
+ Name: "Test aggregate count on a with filter",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ AggregateOperator: v3.AggregateOpeatorCount,
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "bytes", DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag}, Value: 100, Operator: ">"},
+ }},
+ Expression: "A",
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND attributes_float64_value[indexOf(attributes_float64_key, 'bytes')] > 100 AND has(attributes_string_key, 'user_name') group by ts order by ts",
+ },
+ {
+ Name: "Test aggregate count distinct and order by value",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "name", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorCountDistinct,
+ Expression: "A",
+ OrderBy: []v3.OrderBy{{ColumnName: "#SIGNOZ_VALUE", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(name))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts order by value ASC,ts",
+ },
+ {
+ Name: "Test aggregate count distinct on non selected field",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ AggregateOperator: v3.AggregateOperatorCountDistinct,
+ Expression: "A",
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts order by ts",
+ },
+ {
+ Name: "Test aggregate count distinct with filter and groupBy",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "name", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorCountDistinct,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
+ {Key: v3.AttributeKey{Key: "x", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "abc", Operator: "!="},
+ },
+ },
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}, {ColumnName: "ts", Order: "ASC"}},
+ },
+ 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(name))) as value from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND resources_string_value[indexOf(resources_string_key, 'x')] != 'abc' " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate count with multiple filter,groupBy and orderBy",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "name", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorCountDistinct,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
+ {Key: v3.AttributeKey{Key: "x", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "abc", Operator: "!="},
+ },
+ },
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, {Key: "x", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}, {ColumnName: "x", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
+ " attributes_string_value[indexOf(attributes_string_key, 'method')] as method, " +
+ "resources_string_value[indexOf(resources_string_key, 'x')] as x, " +
+ "toFloat64(count(distinct(name))) as value from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND resources_string_value[indexOf(resources_string_key, 'x')] != 'abc' " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "AND indexOf(resources_string_key, 'x') > 0 " +
+ "group by method,x,ts " +
+ "order by method ASC,x ASC,ts",
+ },
+ {
+ Name: "Test aggregate avg",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag},
+ AggregateOperator: v3.AggregateOperatorAvg,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
+ },
+ },
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}, {ColumnName: "x", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
+ " attributes_string_value[indexOf(attributes_string_key, 'method')] as method, " +
+ "avg(attributes_float64_value[indexOf(attributes_float64_key, 'bytes')]) as value " +
+ "from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate sum",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorSum,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
+ },
+ },
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
+ " attributes_string_value[indexOf(attributes_string_key, 'method')] as method, " +
+ "sum(bytes) as value " +
+ "from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate min",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorMin,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
+ },
+ },
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
+ " attributes_string_value[indexOf(attributes_string_key, 'method')] as method, " +
+ "min(bytes) as value " +
+ "from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate max",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorMax,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
+ },
+ },
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
+ " attributes_string_value[indexOf(attributes_string_key, 'method')] as method, " +
+ "max(bytes) as value " +
+ "from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate PXX",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorP05,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
+ " attributes_string_value[indexOf(attributes_string_key, 'method')] as method, " +
+ "quantile(0.05)(bytes) as value " +
+ "from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate RateSum",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", IsColumn: true},
+ AggregateOperator: v3.AggregateOperatorRateSum,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method" +
+ ", sum(bytes)/60 as value from signoz_logs.distributed_logs " +
+ "where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate rate",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeFloat64},
+ AggregateOperator: v3.AggregateOperatorRate,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method" +
+ ", count(attributes_float64_value[indexOf(attributes_float64_key, 'bytes')])/60 as value " +
+ "from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test aggregate RateSum without materialized column",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "bytes", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeFloat64},
+ AggregateOperator: v3.AggregateOperatorRateSum,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
+ GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, " +
+ "attributes_string_value[indexOf(attributes_string_key, 'method')] as method, " +
+ "sum(attributes_float64_value[indexOf(attributes_float64_key, 'bytes')])/60 as value " +
+ "from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
+ "AND indexOf(attributes_string_key, 'method') > 0 " +
+ "group by method,ts " +
+ "order by method ASC,ts",
+ },
+ {
+ Name: "Test Noop",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ SelectColumns: []v3.AttributeKey{},
+ QueryName: "A",
+ AggregateOperator: v3.AggregateOperatorNoOp,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
+ // GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}},
+ // OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}},
+ },
+ 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((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
+ "from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) ",
+ },
+ {
+ Name: "Test aggregate with having clause",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ AggregateOperator: v3.AggregateOperatorCountDistinct,
+ Expression: "A",
+ Having: []v3.Having{
+ {
+ ColumnName: "name",
+ Operator: ">",
+ Value: 10,
+ },
+ },
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) group by ts having value > 10 order by ts",
+ },
+ {
+ Name: "Test aggregate with having clause and filters",
+ Start: 1680066360726210000,
+ End: 1680066458000000000,
+ Step: 60,
+ BuilderQuery: &v3.BuilderQuery{
+ QueryName: "A",
+ AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
+ AggregateOperator: v3.AggregateOperatorCountDistinct,
+ Expression: "A",
+ Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
+ {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
+ },
+ },
+ Having: []v3.Having{
+ {
+ ColumnName: "name",
+ Operator: ">",
+ Value: 10,
+ },
+ },
+ },
+ TableName: "logs",
+ ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' group by ts having value > 10 order by ts",
+ },
+}
+
+func TestBuildLogsQuery(t *testing.T) {
+ for _, tt := range testBuildLogsQueryData {
+ Convey("TestBuildLogsQuery", t, func() {
+ query, err := buildLogsQuery(tt.Start, tt.End, tt.Step, tt.BuilderQuery, map[string]v3.AttributeKey{})
+ So(err, ShouldBeNil)
+ So(query, ShouldEqual, tt.ExpectedQuery)
+
+ })
+ }
+}
+
+var testGetZerosForEpochNanoData = []struct {
+ Name string
+ Epoch int64
+ Multiplier int64
+ Result int64
+}{
+ {
+ Name: "Test 1",
+ Epoch: 1680712080000,
+ Multiplier: 1000000,
+ Result: 1680712080000000000,
+ },
+ {
+ Name: "Test 1",
+ Epoch: 1680712080000000000,
+ Multiplier: 1,
+ Result: 1680712080000000000,
+ },
+}
+
+func TestGetZerosForEpochNano(t *testing.T) {
+ for _, tt := range testGetZerosForEpochNanoData {
+ Convey("testGetZerosForEpochNanoData", t, func() {
+ multiplier := getZerosForEpochNano(tt.Epoch)
+ So(multiplier, ShouldEqual, tt.Multiplier)
+ So(tt.Epoch*multiplier, ShouldEqual, tt.Result)
+ })
+ }
+}
+
+var testOrderBy = []struct {
+ Name string
+ Items []v3.OrderBy
+ Tags []string
+ Result string
+}{
+ {
+ Name: "Test 1",
+ Items: []v3.OrderBy{
+ {
+ ColumnName: "name",
+ Order: "asc",
+ },
+ {
+ ColumnName: constants.SigNozOrderByValue,
+ Order: "desc",
+ },
+ },
+ Tags: []string{"name"},
+ Result: "name asc,value desc",
+ },
+ {
+ Name: "Test 2",
+ Items: []v3.OrderBy{
+ {
+ ColumnName: "name",
+ Order: "asc",
+ },
+ {
+ ColumnName: "bytes",
+ Order: "asc",
+ },
+ },
+ Tags: []string{"name", "bytes"},
+ Result: "name asc,bytes asc",
+ },
+ {
+ Name: "Test 3",
+ Items: []v3.OrderBy{
+ {
+ ColumnName: "name",
+ Order: "asc",
+ },
+ {
+ ColumnName: constants.SigNozOrderByValue,
+ Order: "asc",
+ },
+ {
+ ColumnName: "bytes",
+ Order: "asc",
+ },
+ },
+ Tags: []string{"name", "bytes"},
+ Result: "name asc,bytes asc,value asc",
+ },
+}
+
+func TestOrderBy(t *testing.T) {
+ for _, tt := range testOrderBy {
+ Convey("testOrderBy", t, func() {
+ res := orderBy(tt.Items, tt.Tags)
+ So(res, ShouldEqual, tt.Result)
+
+ // So(multiplier, ShouldEqual, tt.Multiplier)
+ // So(tt.Epoch*multiplier, ShouldEqual, tt.Result)
+ })
+ }
+}
diff --git a/pkg/query-service/app/metrics/v3/query_builder.go b/pkg/query-service/app/metrics/v3/query_builder.go
index b26da0bc1f..1d4eabeac4 100644
--- a/pkg/query-service/app/metrics/v3/query_builder.go
+++ b/pkg/query-service/app/metrics/v3/query_builder.go
@@ -10,7 +10,7 @@ import (
)
var aggregateOperatorToPercentile = map[v3.AggregateOperator]float64{
- v3.AggregateOperatorP05: 0.5,
+ v3.AggregateOperatorP05: 0.05,
v3.AggregateOperatorP10: 0.10,
v3.AggregateOperatorP20: 0.20,
v3.AggregateOperatorP25: 0.25,
@@ -49,9 +49,10 @@ func buildMetricsTimeSeriesFilterQuery(fs *v3.FilterSet, groupTags []v3.Attribut
if fs != nil && len(fs.Items) != 0 {
for _, item := range fs.Items {
toFormat := item.Value
- op := strings.ToLower(strings.TrimSpace(item.Operator))
+ op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
// if the received value is an array for like/match op, just take the first value
- if op == "like" || op == "match" || op == "nlike" || op == "nmatch" {
+ // or should we throw an error?
+ if op == v3.FilterOperatorLike || op == v3.FilterOperatorRegex || op == v3.FilterOperatorNotLike || op == v3.FilterOperatorNotRegex {
x, ok := item.Value.([]interface{})
if ok {
if len(x) == 0 {
@@ -62,37 +63,37 @@ func buildMetricsTimeSeriesFilterQuery(fs *v3.FilterSet, groupTags []v3.Attribut
}
fmtVal := utils.ClickHouseFormattedValue(toFormat)
switch op {
- case "eq":
+ case v3.FilterOperatorEqual:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') = %s", item.Key.Key, fmtVal))
- case "neq":
+ case v3.FilterOperatorNotEqual:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') != %s", item.Key.Key, fmtVal))
- case "in":
+ case v3.FilterOperatorIn:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') IN %s", item.Key.Key, fmtVal))
- case "nin":
+ case v3.FilterOperatorNotIn:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') NOT IN %s", item.Key.Key, fmtVal))
- case "like":
+ case v3.FilterOperatorLike:
conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
- case "nlike":
+ case v3.FilterOperatorNotLike:
conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
- case "match":
+ case v3.FilterOperatorRegex:
conditions = append(conditions, fmt.Sprintf("match(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
- case "nmatch":
+ case v3.FilterOperatorNotRegex:
conditions = append(conditions, fmt.Sprintf("not match(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
- case "gt":
+ case v3.FilterOperatorGreaterThan:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') > %s", item.Key.Key, fmtVal))
- case "gte":
+ case v3.FilterOperatorGreaterThanOrEq:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') >= %s", item.Key.Key, fmtVal))
- case "lt":
+ case v3.FilterOperatorLessThan:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') < %s", item.Key.Key, fmtVal))
- case "lte":
+ case v3.FilterOperatorLessThanOrEq:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') <= %s", item.Key.Key, fmtVal))
- case "contains":
+ case v3.FilterOperatorContains:
conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
- case "ncontains":
+ case v3.FilterOperatorNotContains:
conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
- case "exists":
+ case v3.FilterOperatorExists:
conditions = append(conditions, fmt.Sprintf("has(JSONExtractKeys(labels), %s)", item.Key.Key))
- case "nexists":
+ case v3.FilterOperatorNotExists:
conditions = append(conditions, fmt.Sprintf("not has(JSONExtractKeys(labels), %s)", item.Key.Key))
default:
return "", fmt.Errorf("unsupported operation")
@@ -117,7 +118,36 @@ func buildMetricsTimeSeriesFilterQuery(fs *v3.FilterSet, groupTags []v3.Attribut
func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName string) (string, error) {
- filterSubQuery, err := buildMetricsTimeSeriesFilterQuery(mq.Filters, mq.GroupBy, mq.AggregateAttribute.Key, mq.AggregateOperator)
+ metricQueryGroupBy := mq.GroupBy
+
+ // if the aggregate operator is a histogram quantile, and user has not forgotten
+ // the le tag in the group by then add the le tag to the group by
+ if mq.AggregateOperator == v3.AggregateOperatorHistQuant50 ||
+ mq.AggregateOperator == v3.AggregateOperatorHistQuant75 ||
+ mq.AggregateOperator == v3.AggregateOperatorHistQuant90 ||
+ mq.AggregateOperator == v3.AggregateOperatorHistQuant95 ||
+ mq.AggregateOperator == v3.AggregateOperatorHistQuant99 {
+ found := false
+ for _, tag := range mq.GroupBy {
+ if tag.Key == "le" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ metricQueryGroupBy = append(
+ metricQueryGroupBy,
+ v3.AttributeKey{
+ Key: "le",
+ DataType: v3.AttributeKeyDataTypeString,
+ Type: v3.AttributeKeyTypeTag,
+ IsColumn: false,
+ },
+ )
+ }
+ }
+
+ filterSubQuery, err := buildMetricsTimeSeriesFilterQuery(mq.Filters, metricQueryGroupBy, mq.AggregateAttribute.Key, mq.AggregateOperator)
if err != nil {
return "", err
}
@@ -151,18 +181,22 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str
groupTagsWithoutLe := groupSelect(tagsWithoutLe...)
orderWithoutLe := orderBy(mq.OrderBy, tagsWithoutLe)
- groupBy := groupByAttributeKeyTags(mq.GroupBy...)
- groupTags := groupSelectAttributeKeyTags(mq.GroupBy...)
- orderBy := orderByAttributeKeyTags(mq.OrderBy, mq.GroupBy)
+ groupBy := groupByAttributeKeyTags(metricQueryGroupBy...)
+ groupTags := groupSelectAttributeKeyTags(metricQueryGroupBy...)
+ orderBy := orderByAttributeKeyTags(mq.OrderBy, metricQueryGroupBy)
if len(orderBy) != 0 {
orderBy += ","
}
+ if len(orderWithoutLe) != 0 {
+ orderWithoutLe += ","
+ }
switch mq.AggregateOperator {
case v3.AggregateOperatorRate:
// Calculate rate of change of metric for each unique time series
groupBy = "fingerprint, ts"
+ orderBy = "fingerprint, "
groupTags = "fingerprint,"
op := "max(value)" // max value should be the closest value for point in time
subQuery := fmt.Sprintf(
@@ -351,8 +385,8 @@ func reduceQuery(query string, reduceTo v3.ReduceToOperator, aggregateOperator v
return query, nil
}
-func PrepareMetricQuery(start, end, step int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery) (string, error) {
- query, err := buildMetricQuery(start, end, step, mq, constants.SIGNOZ_TIMESERIES_TABLENAME)
+func PrepareMetricQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery) (string, error) {
+ query, err := buildMetricQuery(start, end, mq.StepInterval, mq, constants.SIGNOZ_TIMESERIES_TABLENAME)
if err != nil {
return "", err
}
diff --git a/pkg/query-service/app/metrics/v3/query_builder_test.go b/pkg/query-service/app/metrics/v3/query_builder_test.go
index 12bb2dca87..bcb9cfc69e 100644
--- a/pkg/query-service/app/metrics/v3/query_builder_test.go
+++ b/pkg/query-service/app/metrics/v3/query_builder_test.go
@@ -26,7 +26,7 @@ func TestBuildQuery(t *testing.T) {
PanelType: v3.PanelTypeGraph,
},
}
- query, err := PrepareMetricQuery(q.Start, q.End, q.Step, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"])
+ query, err := PrepareMetricQuery(q.Start, q.End, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"])
require.NoError(t, err)
require.Contains(t, query, "WHERE metric_name = 'name'")
})
@@ -44,8 +44,8 @@ func TestBuildQueryWithFilters(t *testing.T) {
QueryName: "A",
AggregateAttribute: v3.AttributeKey{Key: "name"},
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
- {Key: v3.AttributeKey{Key: "a"}, Value: "b", Operator: "neq"},
- {Key: v3.AttributeKey{Key: "code"}, Value: "ERROR_*", Operator: "nmatch"},
+ {Key: v3.AttributeKey{Key: "a"}, Value: "b", Operator: v3.FilterOperatorNotEqual},
+ {Key: v3.AttributeKey{Key: "code"}, Value: "ERROR_*", Operator: v3.FilterOperatorNotRegex},
}},
AggregateOperator: v3.AggregateOperatorRateMax,
Expression: "A",
@@ -53,7 +53,7 @@ func TestBuildQueryWithFilters(t *testing.T) {
},
},
}
- query, err := PrepareMetricQuery(q.Start, q.End, q.Step, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"])
+ query, err := PrepareMetricQuery(q.Start, q.End, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"])
require.NoError(t, err)
require.Contains(t, query, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'a') != 'b'")
@@ -74,7 +74,7 @@ func TestBuildQueryWithMultipleQueries(t *testing.T) {
QueryName: "A",
AggregateAttribute: v3.AttributeKey{Key: "name"},
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
- {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: "in"},
+ {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: v3.FilterOperatorIn},
}},
AggregateOperator: v3.AggregateOperatorRateAvg,
Expression: "A",
@@ -89,7 +89,7 @@ func TestBuildQueryWithMultipleQueries(t *testing.T) {
},
}
- query, err := PrepareMetricQuery(q.Start, q.End, q.Step, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"])
+ query, err := PrepareMetricQuery(q.Start, q.End, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"])
require.NoError(t, err)
require.Contains(t, query, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']")
diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go
index 5c4393ae7a..024ae85e6b 100644
--- a/pkg/query-service/app/parser.go
+++ b/pkg/query-service/app/parser.go
@@ -833,6 +833,8 @@ func parseFilterAttributeKeyRequest(r *http.Request) (*v3.FilterAttributeKeyRequ
dataSource := v3.DataSource(r.URL.Query().Get("dataSource"))
aggregateOperator := v3.AggregateOperator(r.URL.Query().Get("aggregateOperator"))
aggregateAttribute := r.URL.Query().Get("aggregateAttribute")
+ tagType := v3.TagType(r.URL.Query().Get("tagType"))
+
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
limit = 50
@@ -846,10 +848,15 @@ func parseFilterAttributeKeyRequest(r *http.Request) (*v3.FilterAttributeKeyRequ
return nil, err
}
+ if err := tagType.Validate(); err != nil && tagType != v3.TagType("") {
+ return nil, err
+ }
+
req = v3.FilterAttributeKeyRequest{
DataSource: dataSource,
AggregateOperator: aggregateOperator,
AggregateAttribute: aggregateAttribute,
+ TagType: tagType,
Limit: limit,
SearchText: r.URL.Query().Get("searchText"),
}
@@ -862,7 +869,9 @@ func parseFilterAttributeValueRequest(r *http.Request) (*v3.FilterAttributeValue
dataSource := v3.DataSource(r.URL.Query().Get("dataSource"))
aggregateOperator := v3.AggregateOperator(r.URL.Query().Get("aggregateOperator"))
+ filterAttributeKeyDataType := v3.AttributeKeyDataType(r.URL.Query().Get("filterAttributeKeyDataType")) // can be empty
aggregateAttribute := r.URL.Query().Get("aggregateAttribute")
+ tagType := v3.TagType(r.URL.Query().Get("tagType")) // can be empty
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
@@ -878,12 +887,14 @@ func parseFilterAttributeValueRequest(r *http.Request) (*v3.FilterAttributeValue
}
req = v3.FilterAttributeValueRequest{
- DataSource: dataSource,
- AggregateOperator: aggregateOperator,
- AggregateAttribute: aggregateAttribute,
- Limit: limit,
- SearchText: r.URL.Query().Get("searchText"),
- FilterAttributeKey: r.URL.Query().Get("attributeKey"),
+ DataSource: dataSource,
+ AggregateOperator: aggregateOperator,
+ AggregateAttribute: aggregateAttribute,
+ TagType: tagType,
+ Limit: limit,
+ SearchText: r.URL.Query().Get("searchText"),
+ FilterAttributeKey: r.URL.Query().Get("attributeKey"),
+ FilterAttributeKeyDataType: filterAttributeKeyDataType,
}
return &req, nil
}
diff --git a/pkg/query-service/app/query_builder.go b/pkg/query-service/app/query_builder.go
index 4ebddbf5ed..59627ed4af 100644
--- a/pkg/query-service/app/query_builder.go
+++ b/pkg/query-service/app/query_builder.go
@@ -35,9 +35,9 @@ var SupportedFunctions = []string{
var evalFuncs = map[string]govaluate.ExpressionFunction{}
-type prepareTracesQueryFunc func(start, end, step int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error)
-type prepareLogsQueryFunc func(start, end, step int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error)
-type prepareMetricQueryFunc func(start, end, step int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error)
+type prepareTracesQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error)
+type prepareLogsQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery, fields map[string]v3.AttributeKey) (string, error)
+type prepareMetricQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error)
type queryBuilder struct {
options queryBuilderOptions
@@ -127,7 +127,7 @@ func expressionToQuery(qp *v3.QueryRangeParamsV3, varToQuery map[string]string,
return formulaQuery, nil
}
-func (qb *queryBuilder) prepareQueries(params *v3.QueryRangeParamsV3) (map[string]string, error) {
+func (qb *queryBuilder) prepareQueries(params *v3.QueryRangeParamsV3, args ...interface{}) (map[string]string, error) {
queries := make(map[string]string)
compositeQuery := params.CompositeQuery
@@ -139,19 +139,23 @@ func (qb *queryBuilder) prepareQueries(params *v3.QueryRangeParamsV3) (map[strin
if query.Expression == queryName {
switch query.DataSource {
case v3.DataSourceTraces:
- queryString, err := qb.options.BuildTraceQuery(params.Start, params.End, params.Step, compositeQuery.QueryType, compositeQuery.PanelType, query)
+ queryString, err := qb.options.BuildTraceQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query)
if err != nil {
return nil, err
}
queries[queryName] = queryString
case v3.DataSourceLogs:
- queryString, err := qb.options.BuildLogQuery(params.Start, params.End, params.Step, compositeQuery.QueryType, compositeQuery.PanelType, query)
+ fields := map[string]v3.AttributeKey{}
+ if len(args) == 1 {
+ fields = args[0].(map[string]v3.AttributeKey)
+ }
+ queryString, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, fields)
if err != nil {
return nil, err
}
queries[queryName] = queryString
case v3.DataSourceMetrics:
- queryString, err := qb.options.BuildMetricQuery(params.Start, params.End, params.Step, compositeQuery.QueryType, compositeQuery.PanelType, query)
+ queryString, err := qb.options.BuildMetricQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query)
if err != nil {
return nil, err
}
diff --git a/pkg/query-service/app/query_builder_test.go b/pkg/query-service/app/query_builder_test.go
index 1e9a282212..e72448a738 100644
--- a/pkg/query-service/app/query_builder_test.go
+++ b/pkg/query-service/app/query_builder_test.go
@@ -22,7 +22,7 @@ func TestBuildQueryWithMultipleQueriesAndFormula(t *testing.T) {
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{Key: "name"},
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
- {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: "in"},
+ {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: v3.FilterOperatorIn},
}},
AggregateOperator: v3.AggregateOperatorRateMax,
Expression: "A",
@@ -68,7 +68,7 @@ func TestBuildQueryWithIncorrectQueryRef(t *testing.T) {
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{Key: "name"},
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
- {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: "in"},
+ {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: v3.FilterOperatorIn},
}},
AggregateOperator: v3.AggregateOperatorRateMax,
Expression: "A",
@@ -105,7 +105,7 @@ func TestBuildQueryWithThreeOrMoreQueriesRefAndFormula(t *testing.T) {
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{Key: "name"},
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
- {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: "in"},
+ {Key: v3.AttributeKey{Key: "in"}, Value: []interface{}{"a", "b", "c"}, Operator: v3.FilterOperatorIn},
}},
AggregateOperator: v3.AggregateOperatorRateMax,
Expression: "A",
diff --git a/pkg/query-service/app/services/map.go b/pkg/query-service/app/services/map.go
new file mode 100644
index 0000000000..ff0a7c2b3c
--- /dev/null
+++ b/pkg/query-service/app/services/map.go
@@ -0,0 +1,63 @@
+package services
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/ClickHouse/clickhouse-go/v2"
+ "go.signoz.io/signoz/pkg/query-service/model"
+)
+
+var (
+ columns = map[string]struct{}{
+ "deployment_environment": {},
+ "k8s_cluster_name": {},
+ "k8s_namespace_name": {},
+ }
+)
+
+func BuildServiceMapQuery(tags []model.TagQuery) (string, []interface{}) {
+ var filterQuery string
+ var namedArgs []interface{}
+ for _, tag := range tags {
+ key := strings.ReplaceAll(tag.GetKey(), ".", "_")
+ operator := tag.GetOperator()
+ value := tag.GetValues()
+
+ if _, ok := columns[key]; !ok {
+ continue
+ }
+
+ switch operator {
+ case model.InOperator:
+ filterQuery += fmt.Sprintf(" AND %s IN @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, value))
+ case model.NotInOperator:
+ filterQuery += fmt.Sprintf(" AND %s NOT IN @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, value))
+ case model.EqualOperator:
+ filterQuery += fmt.Sprintf(" AND %s = @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, value))
+ case model.NotEqualOperator:
+ filterQuery += fmt.Sprintf(" AND %s != @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, value))
+ case model.ContainsOperator:
+ filterQuery += fmt.Sprintf(" AND %s LIKE @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%%%s%%", value)))
+ case model.NotContainsOperator:
+ filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%%%s%%", value)))
+ case model.StartsWithOperator:
+ filterQuery += fmt.Sprintf(" AND %s LIKE @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%s%%", value)))
+ case model.NotStartsWithOperator:
+ filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", key, key)
+ namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%s%%", value)))
+ case model.ExistsOperator:
+ filterQuery += fmt.Sprintf(" AND %s IS NOT NULL", key)
+ case model.NotExistsOperator:
+ filterQuery += fmt.Sprintf(" AND %s IS NULL", key)
+ }
+ }
+ return filterQuery, namedArgs
+}
diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go
index ce03e364e1..d0ded00bbe 100644
--- a/pkg/query-service/constants/constants.go
+++ b/pkg/query-service/constants/constants.go
@@ -5,6 +5,7 @@ import (
"strconv"
"go.signoz.io/signoz/pkg/query-service/model"
+ v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
)
const (
@@ -221,3 +222,49 @@ var ReservedColumnTargetAliases = map[string]struct{}{
// logsPPLPfx is a short constant for logsPipelinePrefix
const LogsPPLPfx = "logstransform/pipeline_"
+
+// The datatype present here doesn't represent the actual datatype of column in the logs table.
+var StaticFieldsLogsV3 = []v3.AttributeKey{
+ {
+ Key: "trace_id",
+ DataType: v3.AttributeKeyDataTypeString,
+ Type: v3.AttributeKeyTypeTag,
+ },
+ {
+ Key: "span_id",
+ DataType: v3.AttributeKeyDataTypeString,
+ Type: v3.AttributeKeyTypeTag,
+ },
+ {
+ Key: "trace_flags",
+ DataType: v3.AttributeKeyDataTypeInt64,
+ Type: v3.AttributeKeyTypeTag,
+ },
+ {
+ Key: "severity_text",
+ DataType: v3.AttributeKeyDataTypeString,
+ Type: v3.AttributeKeyTypeTag,
+ },
+ {
+ Key: "severity_number",
+ DataType: v3.AttributeKeyDataTypeInt64,
+ Type: v3.AttributeKeyTypeTag,
+ },
+ {
+ Key: "body",
+ DataType: v3.AttributeKeyDataTypeString,
+ Type: v3.AttributeKeyTypeTag,
+ },
+}
+
+var LogsTopLevelColumnsV3 = map[string]struct{}{
+ "trace_id": {},
+ "span_id": {},
+ "trace_flags": {},
+ "severity_text": {},
+ "severity_number": {},
+ "timestamp": {},
+ "id": {},
+}
+
+const SigNozOrderByValue = "#SIGNOZ_VALUE"
diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go
index 089003fc8c..1393e28406 100644
--- a/pkg/query-service/interfaces/interface.go
+++ b/pkg/query-service/interfaces/interface.go
@@ -35,6 +35,9 @@ type Reader interface {
// clickhouse only.
GetDisks(ctx context.Context) (*[]model.DiskItem, *model.ApiError)
GetSpanFilters(ctx context.Context, query *model.SpanFilterParams) (*model.SpanFiltersResponse, *model.ApiError)
+ GetTraceAggregateAttributes(ctx context.Context, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error)
+ GetTraceAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error)
+ GetTraceAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error)
GetTagFilters(ctx context.Context, query *model.TagFilterParams) (*model.TagFilters, *model.ApiError)
GetTagValues(ctx context.Context, query *model.TagFilterParams) (*model.TagValues, *model.ApiError)
GetFilteredSpans(ctx context.Context, query *model.GetFilteredSpansParams) (*model.GetFilterSpansResponse, *model.ApiError)
@@ -60,7 +63,10 @@ type Reader interface {
GetMetricAggregateAttributes(ctx context.Context, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error)
GetMetricAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error)
GetMetricAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error)
+
+ // QB V3 metrics/traces/logs
GetTimeSeriesResultV3(ctx context.Context, query string) ([]*v3.Series, error)
+ GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error)
GetTotalSpans(ctx context.Context) (uint64, error)
GetSpansInLastHeartBeatInterval(ctx context.Context) (uint64, error)
@@ -75,6 +81,9 @@ type Reader interface {
GetLogs(ctx context.Context, params *model.LogsFilterParams) (*[]model.GetLogsResponse, *model.ApiError)
TailLogs(ctx context.Context, client *model.LogsTailClient)
AggregateLogs(ctx context.Context, params *model.LogsAggregateParams) (*model.GetLogsAggregatesResponse, *model.ApiError)
+ GetLogAttributeKeys(ctx context.Context, req *v3.FilterAttributeKeyRequest) (*v3.FilterAttributeKeyResponse, error)
+ GetLogAttributeValues(ctx context.Context, req *v3.FilterAttributeValueRequest) (*v3.FilterAttributeValueResponse, error)
+ GetLogAggregateAttributes(ctx context.Context, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error)
// Connection needed for rules, not ideal but required
GetConn() clickhouse.Conn
diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go
index 8a55fb02a8..7c62275dc5 100644
--- a/pkg/query-service/model/v3/v3.go
+++ b/pkg/query-service/model/v3/v3.go
@@ -182,11 +182,19 @@ type AggregateAttributeRequest struct {
type TagType string
const (
- TagTypeColumn TagType = "column"
TagTypeTag TagType = "tag"
TagTypeResource TagType = "resource"
)
+func (q TagType) Validate() error {
+ switch q {
+ case TagTypeTag, TagTypeResource:
+ return nil
+ default:
+ return fmt.Errorf("invalid tag type: %s", q)
+ }
+}
+
// FilterAttributeKeyRequest is a request to fetch possible attribute keys
// for a selected aggregate operator and aggregate attribute and search text.
type FilterAttributeKeyRequest struct {
@@ -201,11 +209,22 @@ type FilterAttributeKeyRequest struct {
type AttributeKeyDataType string
const (
- AttributeKeyDataTypeString AttributeKeyDataType = "string"
- AttributeKeyDataTypeNumber AttributeKeyDataType = "number"
- AttributeKeyDataTypeBool AttributeKeyDataType = "bool"
+ AttributeKeyDataTypeUnspecified AttributeKeyDataType = ""
+ AttributeKeyDataTypeString AttributeKeyDataType = "string"
+ AttributeKeyDataTypeInt64 AttributeKeyDataType = "int64"
+ AttributeKeyDataTypeFloat64 AttributeKeyDataType = "float64"
+ AttributeKeyDataTypeBool AttributeKeyDataType = "bool"
)
+func (q AttributeKeyDataType) Validate() error {
+ switch q {
+ case AttributeKeyDataTypeString, AttributeKeyDataTypeInt64, AttributeKeyDataTypeFloat64, AttributeKeyDataTypeBool:
+ return nil
+ default:
+ return fmt.Errorf("invalid tag data type: %s", q)
+ }
+}
+
// FilterAttributeValueRequest is a request to fetch possible attribute values
// for a selected aggregate operator, aggregate attribute, filter attribute key
// and search text.
@@ -231,8 +250,9 @@ type FilterAttributeKeyResponse struct {
type AttributeKeyType string
const (
- AttributeKeyTypeTag AttributeKeyType = "tag"
- AttributeKeyTypeResource AttributeKeyType = "resource"
+ AttributeKeyTypeUnspecified AttributeKeyType = ""
+ AttributeKeyTypeTag AttributeKeyType = "tag"
+ AttributeKeyTypeResource AttributeKeyType = "resource"
)
type AttributeKey struct {
@@ -244,7 +264,7 @@ type AttributeKey struct {
func (a AttributeKey) Validate() error {
switch a.DataType {
- case AttributeKeyDataTypeBool, AttributeKeyDataTypeNumber, AttributeKeyDataTypeString:
+ case AttributeKeyDataTypeBool, AttributeKeyDataTypeInt64, AttributeKeyDataTypeFloat64, AttributeKeyDataTypeString, AttributeKeyDataTypeUnspecified:
break
default:
return fmt.Errorf("invalid attribute dataType: %s", a.DataType)
@@ -369,6 +389,7 @@ func (c *CompositeQuery) Validate() error {
type BuilderQuery struct {
QueryName string `json:"queryName"`
+ StepInterval int64 `json:"stepInterval"`
DataSource DataSource `json:"dataSource"`
AggregateOperator AggregateOperator `json:"aggregateOperator"`
AggregateAttribute AttributeKey `json:"aggregateAttribute,omitempty"`
@@ -418,6 +439,12 @@ func (b *BuilderQuery) Validate() error {
return fmt.Errorf("group by is invalid %w", err)
}
}
+
+ if b.DataSource == DataSourceMetrics && len(b.GroupBy) > 0 {
+ if b.AggregateOperator == AggregateOperatorNoOp || b.AggregateOperator == AggregateOperatorRate {
+ return fmt.Errorf("group by requires aggregate operator other than noop or rate")
+ }
+ }
}
if b.SelectColumns != nil {
@@ -454,10 +481,33 @@ func (f *FilterSet) Validate() error {
return nil
}
+type FilterOperator string
+
+const (
+ FilterOperatorEqual FilterOperator = "="
+ FilterOperatorNotEqual FilterOperator = "!="
+ FilterOperatorGreaterThan FilterOperator = ">"
+ FilterOperatorGreaterThanOrEq FilterOperator = ">="
+ FilterOperatorLessThan FilterOperator = "<"
+ FilterOperatorLessThanOrEq FilterOperator = "<="
+ FilterOperatorIn FilterOperator = "in"
+ FilterOperatorNotIn FilterOperator = "nin"
+ FilterOperatorContains FilterOperator = "contains"
+ FilterOperatorNotContains FilterOperator = "ncontains"
+ FilterOperatorRegex FilterOperator = "regex"
+ FilterOperatorNotRegex FilterOperator = "nregex"
+ // (I)LIKE is faster than REGEX and supports index
+ FilterOperatorLike FilterOperator = "like"
+ FilterOperatorNotLike FilterOperator = "nlike"
+
+ FilterOperatorExists FilterOperator = "exists"
+ FilterOperatorNotExists FilterOperator = "nexists"
+)
+
type FilterItem struct {
- Key AttributeKey `json:"key"`
- Value interface{} `json:"value"`
- Operator string `json:"op"`
+ Key AttributeKey `json:"key"`
+ Value interface{} `json:"value"`
+ Operator FilterOperator `json:"op"`
}
type OrderBy struct {
@@ -467,7 +517,7 @@ type OrderBy struct {
type Having struct {
ColumnName string `json:"columnName"`
- Operator string `json:"operator"`
+ Operator string `json:"op"`
Value interface{} `json:"value"`
}
@@ -488,8 +538,8 @@ type Series struct {
}
type Row struct {
- Timestamp time.Time `json:"timestamp"`
- Data map[string]string `json:"data"`
+ Timestamp time.Time `json:"timestamp"`
+ Data map[string]interface{} `json:"data"`
}
type Point struct {
diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml
index c0e3963ed2..9ae72cad2c 100644
--- a/pkg/query-service/tests/test-deploy/docker-compose.yaml
+++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml
@@ -169,7 +169,7 @@ services:
<<: *clickhouse-depends
otel-collector:
- image: signoz/signoz-otel-collector:0.66.6
+ image: signoz/signoz-otel-collector:0.66.7
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -195,7 +195,7 @@ services:
<<: *clickhouse-depends
otel-collector-metrics:
- image: signoz/signoz-otel-collector:0.66.6
+ image: signoz/signoz-otel-collector:0.66.7
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
diff --git a/sample-apps/hotrod/hotrod-template.yaml b/sample-apps/hotrod/hotrod-template.yaml
index 6fdd6dd9ae..f2d432ca4d 100644
--- a/sample-apps/hotrod/hotrod-template.yaml
+++ b/sample-apps/hotrod/hotrod-template.yaml
@@ -56,13 +56,6 @@ spec:
name: hotrod
ports:
- containerPort: 8080
- resources:
- requests:
- cpu: 100m
- memory: 100Mi
- limits:
- cpu: 200m
- memory: 200Mi
restartPolicy: Always
---
apiVersion: v1
@@ -126,13 +119,6 @@ spec:
name: comm-plus-1
- containerPort: 8089
name: web-ui
- resources:
- requests:
- cpu: 100m
- memory: 100Mi
- limits:
- cpu: 200m
- memory: 200Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
@@ -203,13 +189,6 @@ spec:
volumeMounts:
- mountPath: /locust
name: locust-scripts
- resources:
- requests:
- cpu: 100m
- memory: 100Mi
- limits:
- cpu: 200m
- memory: 200Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
diff --git a/sample-apps/hotrod/hotrod.yaml b/sample-apps/hotrod/hotrod.yaml
index dfad0132c7..63d7fc88de 100644
--- a/sample-apps/hotrod/hotrod.yaml
+++ b/sample-apps/hotrod/hotrod.yaml
@@ -56,13 +56,6 @@ spec:
name: hotrod
ports:
- containerPort: 8080
- resources:
- requests:
- cpu: 100m
- memory: 100Mi
- limits:
- cpu: 200m
- memory: 200Mi
restartPolicy: Always
---
apiVersion: v1
@@ -126,13 +119,6 @@ spec:
name: comm-plus-1
- containerPort: 8089
name: web-ui
- resources:
- requests:
- cpu: 100m
- memory: 100Mi
- limits:
- cpu: 200m
- memory: 200Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
@@ -203,13 +189,6 @@ spec:
volumeMounts:
- mountPath: /locust
name: locust-scripts
- resources:
- requests:
- cpu: 100m
- memory: 100Mi
- limits:
- cpu: 200m
- memory: 200Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst