From fc7a94fa66f49b6581dc981c1a416455a7fdb1f3 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Thu, 19 Sep 2024 18:48:37 +0530 Subject: [PATCH 01/12] chore: update dashboard contributing doc and issue template (#6029) * chore: update dashboard contributing doc and issue template * chore: update issue template --- .github/ISSUE_TEMPLATE/request_dashboard.md | 49 +++++++++------------ CONTRIBUTING.md | 13 +++++- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/request_dashboard.md b/.github/ISSUE_TEMPLATE/request_dashboard.md index 7972f40625..ba68449103 100644 --- a/.github/ISSUE_TEMPLATE/request_dashboard.md +++ b/.github/ISSUE_TEMPLATE/request_dashboard.md @@ -1,58 +1,49 @@ --- name: Request Dashboard about: Request a new dashboard for the SigNoz Dashboards repository -title: '' +title: '[Dashboard Request] ' labels: 'dashboard-template' assignees: '' --- -## 📝 Dashboard Request Template + -*Use this template to request a new dashboard for the SigNoz Dashboards repository. Please provide as much detail as possible to help us understand your needs.* +## Dashboard Name ---- - -### 1. Dashboard Name + -Name of the requested dashboard (e.g., MySQL Monitoring Dashboard): +## Expected Dashboard Sections and Panels ---- +(Can be tweaked (add or remove panels/sections) according to available metrics) -### 2. Expected Dashboard Sections and Panels +### Section Name -#### Section Name + -Brief description of the section (e.g., "Resource usage metrics for MySQL database"). +### Panel Name -#### Panel Name + -Panel description (e.g., "Value-type panels displaying current CPU usage, memory usage, etc."). - -- **Example:** + ---- + -### 3. Expected Variables +## Expected Dashboard Variables -List any variables you expect to use in the dashboard (e.g., `deployment.environment`, `hostname`, etc.). + ---- +## Additional Comments or Requirements -### 4. Additional Comments or Requirements + -Any additional details or special requirements for the dashboard? +## References or Screenshots ---- + -### 📋 Notes +## 📋 Notes Please review the [CONTRIBUTING.md](https://github.com/SigNoz/dashboards/blob/main/CONTRIBUTING.md) for guidelines on dashboard structure, naming conventions, and how to submit a pull request. - ---- -Thank you for your request! We will review it and provide feedback or guidance as necessary. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc1c4399d8..632a4e98e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,7 @@ Also, have a look at these [good first issues label](https://github.com/SigNoz/s - [To run ClickHouse setup](#41-to-run-clickhouse-setup-recommended-for-local-development) - [Contribute to SigNoz Helm Chart](#5-contribute-to-signoz-helm-chart-) - [To run helm chart for local development](#51-to-run-helm-chart-for-local-development) +- [Contribute to Dashboards](#6-contribute-to-dashboards-) - [Other Ways to Contribute](#other-ways-to-contribute) # 1. General Instructions 📝 @@ -369,6 +370,17 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod- --- +# 6. Contribute to Dashboards 📈 + +**Need to Update: [https://github.com/SigNoz/dashboards](https://github.com/SigNoz/dashboards)** + +To contribute a new dashboard template for any service, follow the contribution guidelines in the [Dashboard Contributing Guide](https://github.com/SigNoz/dashboards/blob/main/CONTRIBUTING.md). In brief: + +1. Create a dashboard JSON file. +2. Add a README file explaining the dashboard, the metrics ingested, and the configurations needed. +3. Include screenshots of the dashboard in the `assets/` directory. +4. Submit a pull request for review. + ## Other Ways to Contribute There are many other ways to get involved with the community and to participate in this project: @@ -379,7 +391,6 @@ There are many other ways to get involved with the community and to participate - Help answer questions on forums such as Stack Overflow and [SigNoz Community Slack Channel](https://signoz.io/slack). - Tell others about the project on Twitter, your blog, etc. - Again, Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :) Thank You! From 70fb5af19f0c5cad25670f4993436336d88b9a4e Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:18:37 +0530 Subject: [PATCH 02/12] chore: removed empty signoz-core-ui folder (#6030) --- signoz-core-ui | 1 - 1 file changed, 1 deletion(-) delete mode 160000 signoz-core-ui diff --git a/signoz-core-ui b/signoz-core-ui deleted file mode 160000 index f8c925d842..0000000000 --- a/signoz-core-ui +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f8c925d842922f8a30063012a7bfb688a3bf0f36 From 2f7d208eb56d36f4ab02082a74931d194cecde5b Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 19 Sep 2024 19:23:12 +0530 Subject: [PATCH 03/12] fix: added time range key for query and local storage handling (#6009) * fix: added time range key for query and local storage handling * chore: fix jest test cases * fix: send single element array for only variable option as well * fix: intermediate stale data should not be shown --- .../GridCardLayout/GridCard/index.tsx | 21 ++++++++++ .../VariableItem.test.tsx | 8 +--- .../VariableItem.tsx | 40 +++++++++++++++++-- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index a618f807a5..66ce70fb86 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -11,6 +11,7 @@ import { isEqual } from 'lodash-es'; import isEmpty from 'lodash-es/isEmpty'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { memo, useEffect, useRef, useState } from 'react'; +import { useQueryClient } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; @@ -48,6 +49,7 @@ function GridCardGraph({ AppState, GlobalReducer >((state) => state.globalTime); + const queryClient = useQueryClient(); const handleBackNavigation = (): void => { const searchParams = new URLSearchParams(window.location.search); @@ -136,6 +138,25 @@ function GridCardGraph({ }; }); + // TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition + // this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx + useEffect(() => { + if (variablesToGetUpdated.length > 0) { + queryClient.cancelQueries([ + maxTime, + minTime, + globalSelectedInterval, + variables, + widget?.query, + widget?.panelTypes, + widget.timePreferance, + widget.fillSpans, + requestData, + ]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [variablesToGetUpdated]); + useEffect(() => { if (!isEqual(updatedQuery, requestData.query)) { setRequestData((prev) => ({ diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx index 0c8fbd51ae..1cb89d6b95 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx @@ -1,14 +1,8 @@ import '@testing-library/jest-dom/extend-expect'; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import React, { useEffect } from 'react'; +import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; import VariableItem from './VariableItem'; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index baa8228b3c..e14162d0ce 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -25,8 +26,11 @@ import { debounce, isArray, isString } from 'lodash-es'; import map from 'lodash-es/map'; import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { VariableResponseProps } from 'types/api/dashboard/variables/query'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { popupContainer } from 'utils/selectPopupContainer'; import { variablePropsToPayloadVariables } from '../utils'; @@ -80,6 +84,23 @@ function VariableItem({ [], ); + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + useEffect(() => { + if (variableData.allSelected && variableData.type === 'QUERY') { + setVariablesToGetUpdated((prev) => { + const variablesQueue = [...prev.filter((v) => v !== variableData.name)]; + if (variableData.name) { + variablesQueue.push(variableData.name); + } + return variablesQueue; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [minTime, maxTime]); + const [errorMessage, setErrorMessage] = useState(null); const getDependentVariables = (queryValue: string): string[] => { @@ -111,7 +132,14 @@ function VariableItem({ const variableKey = dependentVariablesStr.replace(/\s/g, ''); - return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey]; + // added this time dependency for variables query as API respects the passed time range now + return [ + REACT_QUERY_KEY.DASHBOARD_BY_ID, + variableName, + variableKey, + `${minTime}`, + `${maxTime}`, + ]; }; // eslint-disable-next-line sonarjs/cognitive-complexity @@ -151,10 +179,14 @@ function VariableItem({ valueNotInList = true; } } + // variablesData.allSelected is added for the case where on change of options we need to update the + // local storage if ( variableData.type === 'QUERY' && variableData.name && - (variablesToGetUpdated.includes(variableData.name) || valueNotInList) + (variablesToGetUpdated.includes(variableData.name) || + valueNotInList || + variableData.allSelected) ) { let value = variableData.selectedValue; let allSelected = false; @@ -338,8 +370,8 @@ function VariableItem({ (Array.isArray(selectValue) && selectValue?.includes(option.toString())); if (isChecked) { - if (mode === ToggleTagValue.Only) { - handleChange(option.toString()); + if (mode === ToggleTagValue.Only && variableData.multiSelect) { + handleChange([option.toString()]); } else if (!variableData.multiSelect) { handleChange(option.toString()); } else { From 8eb2cf144e3e66e758621c772396a95e4e9b8220 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Thu, 19 Sep 2024 21:20:57 +0530 Subject: [PATCH 04/12] fix: issues with like and ilike fixed in v4 qb (#6018) --- .../app/logs/v4/query_builder.go | 16 ++++++----- .../app/logs/v4/query_builder_test.go | 4 +-- .../app/logs/v4/resource_query_builder.go | 17 +++++++++--- .../logs/v4/resource_query_builder_test.go | 27 ++++++++++++++++--- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/pkg/query-service/app/logs/v4/query_builder.go b/pkg/query-service/app/logs/v4/query_builder.go index 47fda73c2a..e906c605a1 100644 --- a/pkg/query-service/app/logs/v4/query_builder.go +++ b/pkg/query-service/app/logs/v4/query_builder.go @@ -146,6 +146,7 @@ func buildAttributeFilter(item v3.FilterItem) (string, error) { return fmt.Sprintf(logsOp, keyName, fmtVal), nil case v3.FilterOperatorContains, v3.FilterOperatorNotContains: + // we also want to treat %, _ as literals for contains val := utils.QuoteEscapedStringForContains(fmt.Sprintf("%s", item.Value)) // for body the contains is case insensitive if keyName == BODY { @@ -153,14 +154,15 @@ func buildAttributeFilter(item v3.FilterItem) (string, error) { } else { return fmt.Sprintf("%s %s '%%%s%%'", keyName, logsOp, val), nil } - default: - // for use lower for like and ilike - if op == v3.FilterOperatorLike || op == v3.FilterOperatorNotLike { - if keyName == BODY { - keyName = fmt.Sprintf("lower(%s)", keyName) - fmtVal = fmt.Sprintf("lower(%s)", fmtVal) - } + case v3.FilterOperatorLike, v3.FilterOperatorNotLike: + // for body use lower for like and ilike + val := utils.QuoteEscapedString(fmt.Sprintf("%s", item.Value)) + if keyName == BODY { + return fmt.Sprintf("lower(%s) %s lower('%s')", keyName, logsOp, val), nil + } else { + return fmt.Sprintf("%s %s '%s'", keyName, logsOp, val), nil } + default: return fmt.Sprintf("%s %s %s", keyName, logsOp, fmtVal), nil } } else { diff --git a/pkg/query-service/app/logs/v4/query_builder_test.go b/pkg/query-service/app/logs/v4/query_builder_test.go index 1b24a6aac6..34ea7e1f6f 100644 --- a/pkg/query-service/app/logs/v4/query_builder_test.go +++ b/pkg/query-service/app/logs/v4/query_builder_test.go @@ -277,10 +277,10 @@ func Test_buildAttributeFilter(t *testing.T) { Type: v3.AttributeKeyTypeResource, }, Operator: v3.FilterOperatorLike, - Value: "test", + Value: "test%", }, }, - want: "resources_string['service.name'] LIKE 'test'", + want: "resources_string['service.name'] LIKE 'test%'", }, { name: "build attribute filter like-body", diff --git a/pkg/query-service/app/logs/v4/resource_query_builder.go b/pkg/query-service/app/logs/v4/resource_query_builder.go index 12d6c1a36a..2a56549b43 100644 --- a/pkg/query-service/app/logs/v4/resource_query_builder.go +++ b/pkg/query-service/app/logs/v4/resource_query_builder.go @@ -23,8 +23,13 @@ func buildResourceFilter(logsOp string, key string, op v3.FilterOperator, value return fmt.Sprintf(logsOp, searchKey, chFmtVal) case v3.FilterOperatorContains, v3.FilterOperatorNotContains: // this is required as clickhouseFormattedValue add's quotes to the string + // we also want to treat %, _ as literals for contains escapedStringValue := utils.QuoteEscapedStringForContains(fmt.Sprintf("%s", value)) return fmt.Sprintf("%s %s '%%%s%%'", searchKey, logsOp, escapedStringValue) + case v3.FilterOperatorLike, v3.FilterOperatorNotLike: + // this is required as clickhouseFormattedValue add's quotes to the string + escapedStringValue := utils.QuoteEscapedString(fmt.Sprintf("%s", value)) + return fmt.Sprintf("%s %s '%s'", searchKey, logsOp, escapedStringValue) default: return fmt.Sprintf("%s %s %s", searchKey, logsOp, chFmtVal) } @@ -74,13 +79,19 @@ func buildIndexFilterForInOperator(key string, op v3.FilterOperator, value inter // example:= x like '%john%' = labels like '%x%john%' func buildResourceIndexFilter(key string, op v3.FilterOperator, value interface{}) string { // not using clickhouseFormattedValue as we don't wan't the quotes - formattedValueEscaped := utils.QuoteEscapedStringForContains(fmt.Sprintf("%s", value)) + strVal := fmt.Sprintf("%s", value) + formattedValueEscapedForContains := utils.QuoteEscapedStringForContains(strVal) + formattedValueEscaped := utils.QuoteEscapedString(strVal) // add index filters switch op { - case v3.FilterOperatorContains, v3.FilterOperatorEqual, v3.FilterOperatorLike: + case v3.FilterOperatorContains: + return fmt.Sprintf("labels like '%%%s%%%s%%'", key, formattedValueEscapedForContains) + case v3.FilterOperatorNotContains: + return fmt.Sprintf("labels not like '%%%s%%%s%%'", key, formattedValueEscapedForContains) + case v3.FilterOperatorLike, v3.FilterOperatorEqual: return fmt.Sprintf("labels like '%%%s%%%s%%'", key, formattedValueEscaped) - case v3.FilterOperatorNotContains, v3.FilterOperatorNotEqual, v3.FilterOperatorNotLike: + case v3.FilterOperatorNotLike, v3.FilterOperatorNotEqual: return fmt.Sprintf("labels not like '%%%s%%%s%%'", key, formattedValueEscaped) case v3.FilterOperatorNotRegex: return fmt.Sprintf("labels not like '%%%s%%'", key) diff --git a/pkg/query-service/app/logs/v4/resource_query_builder_test.go b/pkg/query-service/app/logs/v4/resource_query_builder_test.go index 130fd9e98c..e315f739a3 100644 --- a/pkg/query-service/app/logs/v4/resource_query_builder_test.go +++ b/pkg/query-service/app/logs/v4/resource_query_builder_test.go @@ -61,9 +61,9 @@ func Test_buildResourceFilter(t *testing.T) { logsOp: "=", key: "service.name", op: v3.FilterOperatorEqual, - value: "Application", + value: "Application%", }, - want: `simpleJSONExtractString(labels, 'service.name') = 'Application'`, + want: `simpleJSONExtractString(labels, 'service.name') = 'Application%'`, }, { name: "test value with quotes", @@ -75,6 +75,16 @@ func Test_buildResourceFilter(t *testing.T) { }, want: `simpleJSONExtractString(labels, 'service.name') = 'Application\'s'`, }, + { + name: "test like", + args: args{ + logsOp: "LIKE", + key: "service.name", + op: v3.FilterOperatorLike, + value: "Application%_", + }, + want: `simpleJSONExtractString(labels, 'service.name') LIKE 'Application%_'`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -119,9 +129,9 @@ func Test_buildIndexFilterForInOperator(t *testing.T) { args: args{ key: "service.name", op: v3.FilterOperatorIn, - value: "application", + value: "application%", }, - want: `(labels like '%"service.name":"application"%')`, + want: `(labels like '%"service.name":"application\%"%')`, }, { name: "test nin string", @@ -180,6 +190,15 @@ func Test_buildResourceIndexFilter(t *testing.T) { }, want: `labels not like '%service.name%application\%\_test%'`, }, + { + name: "test like with % and _", + args: args{ + key: "service.name", + op: v3.FilterOperatorLike, + value: "application%_test", + }, + want: `labels like '%service.name%application%_test%'`, + }, { name: "test not regex", args: args{ From e203276678e11b0ef0cd5af4a9dd86e2db137b14 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 19 Sep 2024 23:02:32 +0530 Subject: [PATCH 05/12] chore: improve colors for the log line indicators (#6032) --- .../LogStateIndicator/LogStateIndicator.styles.scss | 11 +++-------- frontend/src/container/LogsExplorerChart/utils.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss index 61870abc71..2260bf5aa3 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss @@ -22,26 +22,21 @@ } &.INFO { - background-color: var(--bg-slate-400); + background-color: var(--bg-robin-500); } - &.WARNING, &.WARN { background-color: var(--bg-amber-500); } - &.ERROR { background-color: var(--bg-cherry-500); } - &.TRACE { - background-color: var(--bg-robin-300); + background-color: var(--bg-forest-400); } - &.DEBUG { - background-color: var(--bg-forest-500); + background-color: var(--bg-aqua-500); } - &.FATAL { background-color: var(--bg-sakura-500); } diff --git a/frontend/src/container/LogsExplorerChart/utils.ts b/frontend/src/container/LogsExplorerChart/utils.ts index d052a01585..40253750da 100644 --- a/frontend/src/container/LogsExplorerChart/utils.ts +++ b/frontend/src/container/LogsExplorerChart/utils.ts @@ -9,15 +9,18 @@ export function getColorsForSeverityLabels( const lowerCaseLabel = label.toLowerCase(); if (lowerCaseLabel.includes(`{severity_text="trace"}`)) { - return Color.BG_ROBIN_300; + return Color.BG_FOREST_400; } if (lowerCaseLabel.includes(`{severity_text="debug"}`)) { - return Color.BG_FOREST_500; + return Color.BG_AQUA_500; } - if (lowerCaseLabel.includes(`{severity_text="info"}`)) { - return Color.BG_SLATE_400; + if ( + lowerCaseLabel.includes(`{severity_text="info"}`) || + lowerCaseLabel.includes(`{severity_text=""}`) + ) { + return Color.BG_ROBIN_500; } if (lowerCaseLabel.includes(`{severity_text="warn"}`)) { From 4edc6dbeae834162a64ddce2f575f4d61244f913 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 19 Sep 2024 23:21:31 +0530 Subject: [PATCH 06/12] feat: add last option to alert condition match type (#5929) --- frontend/public/locales/en-GB/alerts.json | 1 + frontend/public/locales/en-GB/rules.json | 1 + frontend/public/locales/en/alerts.json | 1 + frontend/public/locales/en/rules.json | 1 + .../container/FormAlertRules/RuleOptions.tsx | 1 + pkg/query-service/rules/alerting.go | 1 + pkg/query-service/rules/base_rule.go | 21 ++++ .../rules/threshold_rule_test.go | 105 ++++++++++++++++++ 8 files changed, 132 insertions(+) diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index a43d04ab59..86f21c8c78 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -53,6 +53,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/public/locales/en-GB/rules.json b/frontend/public/locales/en-GB/rules.json index 9d55a0ba0f..9ac3641c7a 100644 --- a/frontend/public/locales/en-GB/rules.json +++ b/frontend/public/locales/en-GB/rules.json @@ -40,6 +40,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index e7ed6232ad..02d20a2977 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -53,6 +53,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/public/locales/en/rules.json b/frontend/public/locales/en/rules.json index 9d55a0ba0f..9ac3641c7a 100644 --- a/frontend/public/locales/en/rules.json +++ b/frontend/public/locales/en/rules.json @@ -40,6 +40,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index 2ef6bba4c0..da265f34cc 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -103,6 +103,7 @@ function RuleOptions({ {t('option_allthetimes')} {t('option_onaverage')} {t('option_intotal')} + {t('option_last')} ); diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index cb5205f99e..77c9fbe219 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -112,6 +112,7 @@ const ( AllTheTimes MatchType = "2" OnAverage MatchType = "3" InTotal MatchType = "4" + Last MatchType = "5" ) type RuleCondition struct { diff --git a/pkg/query-service/rules/base_rule.go b/pkg/query-service/rules/base_rule.go index a108938b1d..b82aab91b5 100644 --- a/pkg/query-service/rules/base_rule.go +++ b/pkg/query-service/rules/base_rule.go @@ -483,6 +483,27 @@ func (r *BaseRule) shouldAlert(series v3.Series) (Sample, bool) { shouldAlert = true } } + case Last: + // If the last sample matches the condition, the rule is firing. + shouldAlert = false + alertSmpl = Sample{Point: Point{V: series.Points[len(series.Points)-1].Value}, Metric: lblsNormalized, MetricOrig: lbls} + if r.compareOp() == ValueIsAbove { + if series.Points[len(series.Points)-1].Value > r.targetVal() { + shouldAlert = true + } + } else if r.compareOp() == ValueIsBelow { + if series.Points[len(series.Points)-1].Value < r.targetVal() { + shouldAlert = true + } + } else if r.compareOp() == ValueIsEq { + if series.Points[len(series.Points)-1].Value == r.targetVal() { + shouldAlert = true + } + } else if r.compareOp() == ValueIsNotEq { + if series.Points[len(series.Points)-1].Value != r.targetVal() { + shouldAlert = true + } + } } return alertSmpl, shouldAlert } diff --git a/pkg/query-service/rules/threshold_rule_test.go b/pkg/query-service/rules/threshold_rule_test.go index 65d020d25f..8f9554db52 100644 --- a/pkg/query-service/rules/threshold_rule_test.go +++ b/pkg/query-service/rules/threshold_rule_test.go @@ -677,6 +677,111 @@ func TestThresholdRuleShouldAlert(t *testing.T) { matchType: "4", // InTotal target: 20.0, }, + // Test cases for Last + // greater than last + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: true, + compareOp: "1", // Greater Than + matchType: "5", // Last + target: 5.0, + expectedAlertSample: v3.Point{Value: 10.0}, + }, + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: false, + compareOp: "1", // Greater Than + matchType: "5", // Last + target: 20.0, + }, + // less than last + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: true, + compareOp: "2", // Less Than + matchType: "5", // Last + target: 15.0, + expectedAlertSample: v3.Point{Value: 10.0}, + }, + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: false, + compareOp: "2", // Less Than + matchType: "5", // Last + target: 5.0, + }, + // equals last + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: true, + compareOp: "3", // Equals + matchType: "5", // Last + target: 10.0, + expectedAlertSample: v3.Point{Value: 10.0}, + }, + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: false, + compareOp: "3", // Equals + matchType: "5", // Last + target: 5.0, + }, + // not equals last + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: true, + compareOp: "4", // Not Equals + matchType: "5", // Last + target: 5.0, + expectedAlertSample: v3.Point{Value: 10.0}, + }, + { + values: v3.Series{ + Points: []v3.Point{ + {Value: 10.0}, + {Value: 10.0}, + }, + }, + expectAlert: false, + compareOp: "4", // Not Equals + matchType: "5", // Last + target: 10.0, + }, } fm := featureManager.StartManager() From 54d5666b92020ea5a4afebe8ef2832e620e4d8e7 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:39:10 +0530 Subject: [PATCH 07/12] fix: fixed dashboard header and list title alignment (#6035) * fix: fixed dashboard header and list title alignment * fix: fixed dashboard header and list title alignment * fix: fixed existing styles --- .../ListOfDashboard/DashboardList.styles.scss | 10 +++++++-- .../ListOfDashboard/DashboardsList.tsx | 22 ++++++++++--------- .../Description.styles.scss | 6 ++++- .../DashboardDescription/index.tsx | 7 ++---- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss index cf9ec283d2..6a5a148180 100644 --- a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -64,9 +64,9 @@ .dashboard-icon { display: inline-block; - margin-top: 4px; - margin-right: 4px; line-height: 20px; + height: 14px; + width: 14px; } .dot { @@ -75,6 +75,12 @@ border-radius: 50%; } + .title-link { + display: flex; + align-items: center; + gap: 8px; + } + .title { color: var(--bg-vanilla-100); font-size: var(--font-size-sm); diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index c131119a6c..9908374a1b 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -459,17 +459,19 @@ function DashboardsList(): JSX.Element { placement="left" overlayClassName="title-toolip" > - - - dashboard-image + + dashboard-image + {dashboard.name} - - + + diff --git a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss index d82c2da7b6..0f4b2dcc95 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss @@ -130,12 +130,16 @@ .left-section { display: flex; - flex-wrap: wrap; align-items: center; gap: 8px; width: 45%; + .dashboard-img { + height: 16px; + width: 16px; + } + .dashboard-title { color: #fff; font-family: Inter; diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 31b5e4c247..ea59dc4bcf 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -306,16 +306,13 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
+ dashboard-img 30 ? title : ''}> - dashboard-img{' '} + {' '} {title} From ced72f86a4b5684bf3988cec1f2efd3fcaaead06 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Fri, 20 Sep 2024 13:27:18 +0530 Subject: [PATCH 08/12] doc: add info on request dashboard to contributing md (#6040) --- CONTRIBUTING.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 632a4e98e8..613b225353 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ Also, have a look at these [good first issues label](https://github.com/SigNoz/s ## 1.1 For Creating Issue(s) Before making any significant changes and before filing a new issue, please check [existing open](https://github.com/SigNoz/signoz/issues?q=is%3Aopen+is%3Aissue), or [recently closed](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. -**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy) +**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Request Dashboard](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy) #### Details like these are incredibly useful: @@ -57,7 +57,7 @@ Before making any significant changes and before filing a new issue, please chec Discussing your proposed changes ahead of time will make the contribution process smooth for everyone 🙌. - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -98,13 +98,14 @@ GitHub provides additional document on [forking a repository](https://help.githu stability and quality of the component. -You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [SLACK](https://signoz.io/slack). +You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [slack community](https://signoz.io/slack). ### Pointers: - If you find any **bugs** → please create an [**issue.**](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) - If you find anything **missing** in documentation → you can create an issue with the label **`documentation`**. - If you want to build any **new feature** → please create an [issue with the label **`enhancement`**.](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) - If you want to **discuss** something about the product, start a new [**discussion**.](https://github.com/SigNoz/signoz/discussions) +- If you want to request a new **dashboard template** → please create an issue [here](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+).
@@ -118,7 +119,7 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be - Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :) - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -128,14 +129,13 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be - [**Frontend**](#3-develop-frontend-) (Written in Typescript, React) - [**Backend**](#4-contribute-to-backend-query-service-) (Query Service, written in Go) +- [**Dashboard Templates**](#6-contribute-to-dashboards-) (JSON dashboard templates built with SigNoz) Depending upon your area of expertise & interest, you can choose one or more to contribute. Below are detailed instructions to contribute in each area. -**Please note:** If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻 +**Please note:** If you want to work on an issue, please add a brief description of your solution on the issue before starting work on it. -⚠️ If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted. - - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -189,7 +189,7 @@ Also, have a look at [Frontend README.md](https://github.com/SigNoz/signoz/blob/ ### Important Notes: The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh) - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)** ## 3.2 Contribute to Frontend without installing SigNoz backend @@ -210,7 +210,7 @@ Please ping us in the [`#contributing`](https://signoz-community.slack.com/archi **Frontend should now be accessible at** [`http://localhost:3301/services`](http://localhost:3301/services) - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -310,7 +310,7 @@ Click the button below. A workspace with all required environments will be creat > To use it on your forked repo, edit the 'Open in Gitpod' button URL to `https://gitpod.io/#https://github.com//signoz` --> - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -366,7 +366,7 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod- | HOTROD_NAMESPACE=sample-application bash ``` - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)** --- From cb1cd3555b3b63bdb441512dacdebf2599db67d7 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:36:35 +0530 Subject: [PATCH 09/12] feat: added global search on table panel (#5893) * feat: added global search on table panel * feat: added global search on table panel * feat: added global search conditionally and with new design * feat: removed state from datasource * feat: added global search in full view * feat: added lightMode styles * feat: added test cases for querytable and widgetHeader - global search --- .../FullView/WidgetFullView.styles.scss | 19 + .../GridCard/FullView/index.tsx | 25 +- .../GridCard/WidgetGraphComponent.tsx | 4 + .../WidgetHeader/WidgetHeader.styles.scss | 23 +- .../GridCardLayout/WidgetHeader/index.tsx | 124 ++- .../src/container/GridTableComponent/types.ts | 1 + .../container/PanelWrapper/PanelWrapper.tsx | 2 + .../PanelWrapper/TablePanelWrapper.tsx | 2 + .../PanelWrapper/panelWrapper.types.ts | 1 + .../QueryTable/QueryTable.intefaces.ts | 1 + .../src/container/QueryTable/QueryTable.tsx | 31 +- .../QueryTable/__test__/QueryTable.test.tsx | 73 ++ .../container/QueryTable/__test__/mocks.ts | 797 ++++++++++++++++++ 13 files changed, 1056 insertions(+), 47 deletions(-) create mode 100644 frontend/src/container/QueryTable/__test__/QueryTable.test.tsx create mode 100644 frontend/src/container/QueryTable/__test__/mocks.ts diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss index 29d578f096..78c4459929 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss @@ -15,6 +15,13 @@ box-sizing: border-box; margin: 16px 0; border-radius: 3px; + + .global-search { + .ant-input-group-addon { + border: none; + background-color: var(--bg-ink-300); + } + } } .height-widget { @@ -55,3 +62,15 @@ } } } + +.lightMode { + .full-view-container { + .graph-container { + .global-search { + .ant-input-group-addon { + background-color: var(--bg-vanilla-200); + } + } + } + } +} diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 974a35a39c..d682af12a8 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -1,7 +1,11 @@ import './WidgetFullView.styles.scss'; -import { LoadingOutlined, SyncOutlined } from '@ant-design/icons'; -import { Button, Spin } from 'antd'; +import { + LoadingOutlined, + SearchOutlined, + SyncOutlined, +} from '@ant-design/icons'; +import { Button, Input, Spin } from 'antd'; import cx from 'classnames'; import { ToggleGraphProps } from 'components/Graph/types'; import Spinner from 'components/Spinner'; @@ -172,6 +176,10 @@ function FullView({ const isListView = widget.panelTypes === PANEL_TYPES.LIST; + const isTablePanel = widget.panelTypes === PANEL_TYPES.TABLE; + + const [searchTerm, setSearchTerm] = useState(''); + if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) { return ; } @@ -216,6 +224,18 @@ function FullView({ }} isGraphLegendToggleAvailable={canModifyChart} > + {isTablePanel && ( + } + className="global-search" + placeholder="Search..." + allowClear + key={widget.id} + onChange={(e): void => { + setSearchTerm(e.target.value || ''); + }} + /> + )}
diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index b76c7c9f73..4d5c7fa94c 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -234,6 +234,8 @@ function WidgetGraphComponent({ }); }; + const [searchTerm, setSearchTerm] = useState(''); + const loadingState = (queryResponse.isLoading || queryResponse.status === 'idle') && widget.panelTypes !== PANEL_TYPES.LIST; @@ -317,6 +319,7 @@ function WidgetGraphComponent({ isWarning={isWarning} isFetchingResponse={isFetchingResponse} tableProcessedDataRef={tableProcessedDataRef} + setSearchTerm={setSearchTerm} /> {queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && ( @@ -337,6 +340,7 @@ function WidgetGraphComponent({ onDragSelect={onDragSelect} tableProcessedDataRef={tableProcessedDataRef} customTooltipElement={customTooltipElement} + searchTerm={searchTerm} /> )} diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss index 2fcb3e8e6f..11659e9a3e 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss +++ b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss @@ -2,7 +2,7 @@ display: flex; justify-content: space-between; align-items: center; - height: 30px; + height: 36px; width: 100%; padding: 0.5rem; box-sizing: border-box; @@ -10,6 +10,14 @@ font-weight: 600; cursor: move; + + .ant-input-group-addon { + border: none; + background-color: var(--bg-ink-500); + } + .search-header-icons { + cursor: pointer; + } } .widget-header-title { @@ -19,6 +27,7 @@ .widget-header-actions { display: flex; align-items: center; + gap: 8px; } .widget-header-more-options { visibility: hidden; @@ -30,6 +39,10 @@ padding: 8px; } +.widget-header-more-options-visible { + visibility: visible; +} + .widget-header-hover { visibility: visible; } @@ -37,3 +50,11 @@ .widget-api-actions { padding-right: 0.25rem; } + +.lightMode { + .widget-header-container { + .ant-input-group-addon { + background-color: inherit; + } + } +} diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 7daa4e553d..d4aa6a4c09 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -9,9 +9,10 @@ import { ExclamationCircleOutlined, FullscreenOutlined, MoreOutlined, + SearchOutlined, WarningOutlined, } from '@ant-design/icons'; -import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; +import { Dropdown, Input, MenuProps, Tooltip, Typography } from 'antd'; import Spinner from 'components/Spinner'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; @@ -20,8 +21,9 @@ import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { isEmpty } from 'lodash-es'; +import { X } from 'lucide-react'; import { unparse } from 'papaparse'; -import { ReactNode, useCallback, useMemo } from 'react'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -51,6 +53,7 @@ interface IWidgetHeaderProps { isWarning: boolean; isFetchingResponse: boolean; tableProcessedDataRef: React.MutableRefObject; + setSearchTerm: React.Dispatch>; } function WidgetHeader({ @@ -67,6 +70,7 @@ function WidgetHeader({ isWarning, isFetchingResponse, tableProcessedDataRef, + setSearchTerm, }: IWidgetHeaderProps): JSX.Element | null { const onEditHandler = useCallback((): void => { const widgetId = widget.id; @@ -187,6 +191,10 @@ function WidgetHeader({ const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]); + const [showGlobalSearch, setShowGlobalSearch] = useState(false); + + const globalSearchAvailable = widget.panelTypes === PANEL_TYPES.TABLE; + const menu = useMemo( () => ({ items: updatedMenuList, @@ -201,46 +209,80 @@ function WidgetHeader({ return (
- - {title} - -
-
{threshold}
- {isFetchingResponse && !queryResponse.isError && ( - - )} - {queryResponse.isError && ( - } + placeholder="Search..." + bordered={false} + data-testid="widget-header-search-input" + autoFocus + addonAfter={ + { + e.stopPropagation(); + e.preventDefault(); + setShowGlobalSearch(false); + }} + className="search-header-icons" + /> + } + key={widget.id} + onChange={(e): void => { + setSearchTerm(e.target.value || ''); + }} + /> + ) : ( + <> + - - - )} - - {isWarning && ( - - - - )} - - - -
+ {title} + +
+
{threshold}
+ {isFetchingResponse && !queryResponse.isError && ( + + )} + {queryResponse.isError && ( + + + + )} + + {isWarning && ( + + + + )} + {globalSearchAvailable && ( + setShowGlobalSearch(true)} + data-testid="widget-header-search" + /> + )} + + + +
+ + )}
); } diff --git a/frontend/src/container/GridTableComponent/types.ts b/frontend/src/container/GridTableComponent/types.ts index 6088f9dcb8..883e280b38 100644 --- a/frontend/src/container/GridTableComponent/types.ts +++ b/frontend/src/container/GridTableComponent/types.ts @@ -14,6 +14,7 @@ export type GridTableComponentProps = { columnUnits?: ColumnUnit; tableProcessedDataRef?: React.MutableRefObject; sticky?: TableProps['sticky']; + searchTerm?: string; } & Pick & Omit, 'columns' | 'dataSource'>; diff --git a/frontend/src/container/PanelWrapper/PanelWrapper.tsx b/frontend/src/container/PanelWrapper/PanelWrapper.tsx index ed105b3948..2f5b35485e 100644 --- a/frontend/src/container/PanelWrapper/PanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/PanelWrapper.tsx @@ -16,6 +16,7 @@ function PanelWrapper({ selectedGraph, tableProcessedDataRef, customTooltipElement, + searchTerm, }: PanelWrapperProps): JSX.Element { const Component = PanelTypeVsPanelWrapper[ selectedGraph || widget.panelTypes @@ -39,6 +40,7 @@ function PanelWrapper({ selectedGraph={selectedGraph} tableProcessedDataRef={tableProcessedDataRef} customTooltipElement={customTooltipElement} + searchTerm={searchTerm} /> ); } diff --git a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx index 0eab4143a2..c5222e8d53 100644 --- a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx @@ -8,6 +8,7 @@ function TablePanelWrapper({ widget, queryResponse, tableProcessedDataRef, + searchTerm, }: PanelWrapperProps): JSX.Element { const panelData = (queryResponse.data?.payload?.data?.result?.[0] as any)?.table || []; @@ -20,6 +21,7 @@ function TablePanelWrapper({ columnUnits={widget.columnUnits} tableProcessedDataRef={tableProcessedDataRef} sticky={widget.panelTypes === PANEL_TYPES.TABLE} + searchTerm={searchTerm} // eslint-disable-next-line react/jsx-props-no-spreading {...GRID_TABLE_CONFIG} /> diff --git a/frontend/src/container/PanelWrapper/panelWrapper.types.ts b/frontend/src/container/PanelWrapper/panelWrapper.types.ts index 7d5e3122e8..4778ffdb97 100644 --- a/frontend/src/container/PanelWrapper/panelWrapper.types.ts +++ b/frontend/src/container/PanelWrapper/panelWrapper.types.ts @@ -23,6 +23,7 @@ export type PanelWrapperProps = { onDragSelect: (start: number, end: number) => void; selectedGraph?: PANEL_TYPES; tableProcessedDataRef?: React.MutableRefObject; + searchTerm?: string; customTooltipElement?: HTMLDivElement; }; diff --git a/frontend/src/container/QueryTable/QueryTable.intefaces.ts b/frontend/src/container/QueryTable/QueryTable.intefaces.ts index 7576d796ec..254e4885e7 100644 --- a/frontend/src/container/QueryTable/QueryTable.intefaces.ts +++ b/frontend/src/container/QueryTable/QueryTable.intefaces.ts @@ -19,4 +19,5 @@ export type QueryTableProps = Omit< columns?: ColumnsType; dataSource?: RowData[]; sticky?: TableProps['sticky']; + searchTerm?: string; }; diff --git a/frontend/src/container/QueryTable/QueryTable.tsx b/frontend/src/container/QueryTable/QueryTable.tsx index 1786e5d4e3..e438070173 100644 --- a/frontend/src/container/QueryTable/QueryTable.tsx +++ b/frontend/src/container/QueryTable/QueryTable.tsx @@ -3,8 +3,11 @@ import './QueryTable.styles.scss'; import { ResizeTable } from 'components/ResizeTable'; import Download from 'container/Download/Download'; import { IServiceName } from 'container/MetricsApplication/Tabs/types'; -import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery'; -import { useMemo } from 'react'; +import { + createTableColumnsFromQuery, + RowData, +} from 'lib/query/createTableColumnsFromQuery'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { QueryTableProps } from './QueryTable.intefaces'; @@ -20,6 +23,7 @@ export function QueryTable({ columns, dataSource, sticky, + searchTerm, ...props }: QueryTableProps): JSX.Element { const { isDownloadEnabled = false, fileName = '' } = downloadOption || {}; @@ -55,6 +59,27 @@ export function QueryTable({ hideOnSinglePage: true, }; + const [filterTable, setFilterTable] = useState(null); + + const onTableSearch = useCallback( + (value?: string): void => { + const filterTable = newDataSource.filter((o) => + Object.keys(o).some((k) => + String(o[k]) + .toLowerCase() + .includes(value?.toLowerCase() || ''), + ), + ); + + setFilterTable(filterTable); + }, + [newDataSource], + ); + + useEffect(() => { + onTableSearch(searchTerm); + }, [newDataSource, onTableSearch, searchTerm]); + return (
{isDownloadEnabled && ( @@ -69,7 +94,7 @@ export function QueryTable({ ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string } => ({ + pathname: ``, + }), +})); + +// Mock useDashabord hook +jest.mock('providers/Dashboard/Dashboard', () => ({ + useDashboard: (): any => ({ + selectedDashboard: { + data: { + variables: [], + }, + }, + }), +})); + +describe('QueryTable -', () => { + it('should render correctly with all the data rows', () => { + const { container } = render(); + const tableRows = container.querySelectorAll('tr.ant-table-row'); + expect(tableRows.length).toBe(QueryTableProps.queryTableData.rows.length); + }); + + it('should render correctly with searchTerm', () => { + const { container } = render( + , + ); + const tableRows = container.querySelectorAll('tr.ant-table-row'); + expect(tableRows.length).toBe(3); + }); +}); + +const setSearchTerm = jest.fn(); +describe('WidgetHeader -', () => { + it('global search option should be working', () => { + const { getByText, getByTestId } = render( + , + ); + expect(getByText('Table - Panel')).toBeInTheDocument(); + const searchWidget = getByTestId('widget-header-search'); + expect(searchWidget).toBeInTheDocument(); + // click and open the search input + fireEvent.click(searchWidget); + // check if input is opened + const searchInput = getByTestId('widget-header-search-input'); + expect(searchInput).toBeInTheDocument(); + + // enter search term + fireEvent.change(searchInput, { target: { value: 'frontend' } }); + // check if search term is set + expect(setSearchTerm).toHaveBeenCalledWith('frontend'); + expect(searchInput).toHaveValue('frontend'); + }); + + it('global search should not be present for non-table panel', () => { + const { queryByTestId } = render( + , + ); + expect(queryByTestId('widget-header-search')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/container/QueryTable/__test__/mocks.ts b/frontend/src/container/QueryTable/__test__/mocks.ts new file mode 100644 index 0000000000..abdb7bcfe3 --- /dev/null +++ b/frontend/src/container/QueryTable/__test__/mocks.ts @@ -0,0 +1,797 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +export const QueryTableProps: any = { + props: { + loading: false, + size: 'small', + }, + queryTableData: { + columns: [ + { + name: 'resource_host_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'service_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'operation', + queryName: '', + isValueColumn: false, + }, + { + name: 'A', + queryName: 'A', + isValueColumn: true, + }, + ], + rows: [ + { + data: { + A: 11.5, + operation: 'GetDriver', + resource_host_name: 'test-hs-name', + service_name: 'redis', + }, + }, + { + data: { + A: 10.13, + operation: 'HTTP GET', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + }, + { + data: { + A: 9.21, + operation: 'HTTP GET /route', + resource_host_name: 'test-hs-name', + service_name: 'route', + }, + }, + { + data: { + A: 9.21, + operation: 'HTTP GET: /route', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.92, + operation: 'HTTP GET: /customer', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.92, + operation: 'SQL SELECT', + resource_host_name: 'test-hs-name', + service_name: 'mysql', + }, + }, + { + data: { + A: 0.92, + operation: 'HTTP GET /customer', + resource_host_name: 'test-hs-name', + service_name: 'customer', + }, + }, + ], + }, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_calls_total--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'signoz_calls_total', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: 'metrics', + disabled: false, + expression: 'A', + filters: { + items: [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: 'string', + id: 'resource_host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'resource_host_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'service_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'service_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'operation--string--tag--false', + isColumn: false, + isJSON: false, + key: 'operation', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: 'builder', + }, + columns: [ + { + dataIndex: 'resource_host_name', + title: 'resource_host_name', + width: 145, + }, + { + dataIndex: 'service_name', + title: 'service_name', + width: 145, + }, + { + dataIndex: 'operation', + title: 'operation', + width: 145, + }, + { + dataIndex: 'A', + title: 'A', + width: 145, + }, + ], + dataSource: [ + { + A: 11.5, + operation: 'GetDriver', + resource_host_name: 'test-hs-name', + service_name: 'redis', + }, + { + A: 10.13, + operation: 'HTTP GET', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + { + A: 9.21, + operation: 'HTTP GET /route', + resource_host_name: 'test-hs-name', + service_name: 'route', + }, + { + A: 9.21, + operation: 'HTTP GET: /route', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + { + A: 0.92, + operation: 'HTTP GET: /customer', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + { + A: 0.92, + operation: 'SQL SELECT', + resource_host_name: 'test-hs-name', + service_name: 'mysql', + }, + { + A: 0.92, + operation: 'HTTP GET /customer', + resource_host_name: 'test-hs-name', + service_name: 'customer', + }, + ], + sticky: true, + searchTerm: '', +}; + +export const WidgetHeaderProps: any = { + title: 'Table - Panel', + widget: { + bucketCount: 30, + bucketWidth: 0, + columnUnits: {}, + description: '', + fillSpans: false, + id: 'add65f0d-7662-4024-af51-da567759235d', + isStacked: false, + mergeAllActiveQueries: false, + nullZeroValues: 'zero', + opacity: '1', + panelTypes: 'table', + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_calls_total--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'signoz_calls_total', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: 'metrics', + disabled: false, + expression: 'A', + filters: { + items: [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: 'string', + id: 'resource_host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'resource_host_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'service_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'service_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'operation--string--tag--false', + isColumn: false, + isJSON: false, + key: 'operation', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: 'builder', + }, + selectedLogFields: [ + { + dataType: 'string', + name: 'body', + type: '', + }, + { + dataType: 'string', + name: 'timestamp', + type: '', + }, + ], + selectedTracesFields: [ + { + dataType: 'string', + id: 'serviceName--string--tag--true', + isColumn: true, + isJSON: false, + key: 'serviceName', + type: 'tag', + }, + { + dataType: 'string', + id: 'name--string--tag--true', + isColumn: true, + isJSON: false, + key: 'name', + type: 'tag', + }, + { + dataType: 'float64', + id: 'durationNano--float64--tag--true', + isColumn: true, + isJSON: false, + key: 'durationNano', + type: 'tag', + }, + { + dataType: 'string', + id: 'httpMethod--string--tag--true', + isColumn: true, + isJSON: false, + key: 'httpMethod', + type: 'tag', + }, + { + dataType: 'string', + id: 'responseStatusCode--string--tag--true', + isColumn: true, + isJSON: false, + key: 'responseStatusCode', + type: 'tag', + }, + ], + softMax: 0, + softMin: 0, + stackedBarChart: false, + thresholds: [], + timePreferance: 'GLOBAL_TIME', + title: 'Table - Panel', + yAxisUnit: 'none', + }, + parentHover: false, + queryResponse: { + status: 'success', + isLoading: false, + isSuccess: true, + isError: false, + isIdle: false, + data: { + statusCode: 200, + error: null, + message: 'success', + payload: { + status: 'success', + data: { + resultType: '', + result: [ + { + table: { + columns: [ + { + name: 'resource_host_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'service_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'operation', + queryName: '', + isValueColumn: false, + }, + { + name: 'A', + queryName: 'A', + isValueColumn: true, + }, + ], + rows: [ + { + data: { + A: 11.67, + operation: 'GetDriver', + resource_host_name: '4f6ec470feea', + service_name: 'redis', + }, + }, + { + data: { + A: 10.26, + operation: 'HTTP GET', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 9.33, + operation: 'HTTP GET: /route', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 9.33, + operation: 'HTTP GET /route', + resource_host_name: '4f6ec470feea', + service_name: 'route', + }, + }, + { + data: { + A: 0.93, + operation: 'FindDriverIDs', + resource_host_name: '4f6ec470feea', + service_name: 'redis', + }, + }, + { + data: { + A: 0.93, + operation: 'HTTP GET: /customer', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.93, + operation: '/driver.DriverService/FindNearest', + resource_host_name: '4f6ec470feea', + service_name: 'driver', + }, + }, + { + data: { + A: 0.93, + operation: '/driver.DriverService/FindNearest', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.93, + operation: 'SQL SELECT', + resource_host_name: '4f6ec470feea', + service_name: 'mysql', + }, + }, + { + data: { + A: 0.93, + operation: 'HTTP GET /customer', + resource_host_name: '4f6ec470feea', + service_name: 'customer', + }, + }, + { + data: { + A: 0.93, + operation: 'HTTP GET /dispatch', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.21, + operation: 'check_request limit', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'authenticate_check_cache', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'authenticate_check_db', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'authenticate', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'check cart in cache', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.2, + operation: 'get_cart', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.2, + operation: 'check cart in db', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.2, + operation: 'home', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + ], + }, + }, + ], + }, + }, + params: { + start: 1726669030000, + end: 1726670830000, + step: 60, + variables: {}, + formatForWeb: true, + compositeQuery: { + queryType: 'builder', + panelType: 'table', + fillGaps: false, + builderQueries: { + A: { + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_calls_total--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'signoz_calls_total', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: 'metrics', + disabled: false, + expression: 'A', + filters: { + items: [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: 'string', + id: 'resource_host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'resource_host_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'service_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'service_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'operation--string--tag--false', + isColumn: false, + isJSON: false, + key: 'operation', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + }, + }, + }, + }, + dataUpdatedAt: 1726670830710, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + errorUpdateCount: 0, + isFetched: true, + isFetchedAfterMount: true, + isFetching: false, + isRefetching: false, + isLoadingError: false, + isPlaceholderData: false, + isPreviousData: false, + isRefetchError: false, + isStale: true, + }, + headerMenuList: ['view', 'clone', 'delete', 'edit'], + isWarning: false, + isFetchingResponse: false, + tableProcessedDataRef: { + current: [ + { + resource_host_name: '4f6ec470feea', + service_name: 'redis', + operation: 'GetDriver', + A: 11.67, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET', + A: 10.26, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET: /route', + A: 9.33, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'route', + operation: 'HTTP GET /route', + A: 9.33, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'redis', + operation: 'FindDriverIDs', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET: /customer', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'driver', + operation: '/driver.DriverService/FindNearest', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: '/driver.DriverService/FindNearest', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'mysql', + operation: 'SQL SELECT', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'customer', + operation: 'HTTP GET /customer', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET /dispatch', + A: 0.93, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'check_request limit', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'authenticate_check_cache', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'authenticate_check_db', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'authenticate', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'check cart in cache', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'get_cart', + A: 0.2, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'check cart in db', + A: 0.2, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'home', + A: 0.2, + }, + ], + }, +}; From f6d3f95768385f43694c5ea1c02ac701c4e79711 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Fri, 20 Sep 2024 18:02:33 +0530 Subject: [PATCH 10/12] fix: tlemetry for dashboard/alerts/views using contains on attributes (#6034) * fix: tlemetry for dashboard/alerts/views using contains on attributes * fix: update how telemetry is collected for logs * fix: revert constands * fix: check assertion for operator --- pkg/query-service/app/dashboards/model.go | 61 +++++++++++++++++++---- pkg/query-service/app/explorer/db.go | 13 +++++ pkg/query-service/model/response.go | 4 ++ pkg/query-service/rules/db.go | 28 +++++++++-- pkg/query-service/telemetry/telemetry.go | 3 ++ 5 files changed, 94 insertions(+), 15 deletions(-) diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index 989d266b51..21d39fcbe4 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "regexp" + "slices" "strings" "time" @@ -453,7 +454,6 @@ func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) { totalDashboardsWithPanelAndName := 0 var dashboardNames []string count := 0 - logChQueriesCount := 0 for _, dashboard := range dashboardsData { if isDashboardWithPanelAndName(dashboard.Data) { totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1 @@ -466,19 +466,18 @@ func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) { dashboardsInfo.LogsBasedPanels += dashboardInfo.LogsBasedPanels dashboardsInfo.TracesBasedPanels += dashboardInfo.TracesBasedPanels dashboardsInfo.MetricBasedPanels += dashboardsInfo.MetricBasedPanels + dashboardsInfo.LogsPanelsWithAttrContainsOp += dashboardInfo.LogsPanelsWithAttrContainsOp + dashboardsInfo.DashboardsWithLogsChQuery += dashboardInfo.DashboardsWithLogsChQuery if isDashboardWithTSV2(dashboard.Data) { count = count + 1 } - if isDashboardWithLogsClickhouseQuery(dashboard.Data) { - logChQueriesCount = logChQueriesCount + 1 - } + // check if dashboard is a has a log operator with contains } dashboardsInfo.DashboardNames = dashboardNames dashboardsInfo.TotalDashboards = len(dashboardsData) dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName dashboardsInfo.QueriesWithTSV2 = count - dashboardsInfo.DashboardsWithLogsChQuery = logChQueriesCount return &dashboardsInfo, nil } @@ -495,8 +494,8 @@ func isDashboardWithLogsClickhouseQuery(data map[string]interface{}) bool { if err != nil { return false } - result := strings.Contains(string(jsonData), "signoz_logs.distributed_logs ") || - strings.Contains(string(jsonData), "signoz_logs.logs ") + result := strings.Contains(string(jsonData), "signoz_logs.distributed_logs") || + strings.Contains(string(jsonData), "signoz_logs.logs") return result } @@ -532,11 +531,38 @@ func extractDashboardName(data map[string]interface{}) string { return "" } -func countPanelsInDashboard(data map[string]interface{}) model.DashboardsInfo { - var logsPanelCount, tracesPanelCount, metricsPanelCount int +func checkLogPanelAttrContains(data map[string]interface{}) int { + var logsPanelsWithAttrContains int + filters, ok := data["filters"].(map[string]interface{}) + if ok && filters["items"] != nil { + items, ok := filters["items"].([]interface{}) + if ok { + for _, item := range items { + itemMap, ok := item.(map[string]interface{}) + if ok { + opStr, ok := itemMap["op"].(string) + if ok { + if slices.Contains([]string{"contains", "ncontains", "like", "nlike"}, opStr) { + // check if it's not body + key, ok := itemMap["key"].(map[string]string) + if ok && key["key"] != "body" { + logsPanelsWithAttrContains++ + } + } + } + } + } + } + } + return logsPanelsWithAttrContains +} + +func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsInfo { + var logsPanelCount, tracesPanelCount, metricsPanelCount, logsPanelsWithAttrContains int + var logChQuery bool // totalPanels := 0 - if data != nil && data["widgets"] != nil { - widgets, ok := data["widgets"] + if inputData != nil && inputData["widgets"] != nil { + widgets, ok := inputData["widgets"] if ok { data, ok := widgets.([]interface{}) if ok { @@ -559,20 +585,33 @@ func countPanelsInDashboard(data map[string]interface{}) model.DashboardsInfo { metricsPanelCount++ } else if data["dataSource"] == "logs" { logsPanelCount++ + logsPanelsWithAttrContains += checkLogPanelAttrContains(data) } } } } } + } else if ok && query["queryType"] == "clickhouse_sql" && query["clickhouse_sql"] != nil { + if isDashboardWithLogsClickhouseQuery(inputData) { + logChQuery = true + } } } } } } } + + logChQueryCount := 0 + if logChQuery { + logChQueryCount = 1 + } return model.DashboardsInfo{ LogsBasedPanels: logsPanelCount, TracesBasedPanels: tracesPanelCount, MetricBasedPanels: metricsPanelCount, + + DashboardsWithLogsChQuery: logChQueryCount, + LogsPanelsWithAttrContainsOp: logsPanelsWithAttrContains, } } diff --git a/pkg/query-service/app/explorer/db.go b/pkg/query-service/app/explorer/db.go index 140b0b48d8..c53345e65b 100644 --- a/pkg/query-service/app/explorer/db.go +++ b/pkg/query-service/app/explorer/db.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "strings" "time" @@ -247,6 +248,18 @@ func GetSavedViewsInfo(ctx context.Context) (*model.SavedViewsInfo, error) { savedViewsInfo.TracesSavedViews += 1 } else if view.SourcePage == "logs" { savedViewsInfo.LogsSavedViews += 1 + + for _, query := range view.CompositeQuery.BuilderQueries { + if query.Filters != nil { + for _, item := range query.Filters.Items { + if slices.Contains([]string{"contains", "ncontains", "like", "nlike"}, string(item.Operator)) { + if item.Key.Key != "body" { + savedViewsInfo.LogsSavedViewWithContainsOp += 1 + } + } + } + } + } } } return &savedViewsInfo, nil diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index 03e538879c..3a720aed5e 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -632,12 +632,15 @@ type AlertsInfo struct { AlertNames []string `json:"alertNames"` AlertsWithTSV2 int `json:"alertsWithTSv2"` AlertsWithLogsChQuery int `json:"alertsWithLogsChQuery"` + AlertsWithLogsContainsOp int `json:"alertsWithLogsContainsOp"` } type SavedViewsInfo struct { TotalSavedViews int `json:"totalSavedViews"` TracesSavedViews int `json:"tracesSavedViews"` LogsSavedViews int `json:"logsSavedViews"` + + LogsSavedViewWithContainsOp int `json:"logsSavedViewWithContainsOp"` } type DashboardsInfo struct { @@ -649,6 +652,7 @@ type DashboardsInfo struct { DashboardNames []string `json:"dashboardNames"` QueriesWithTSV2 int `json:"queriesWithTSV2"` DashboardsWithLogsChQuery int `json:"dashboardsWithLogsChQuery"` + LogsPanelsWithAttrContainsOp int `json:"logsPanelsWithAttrContainsOp"` } type TagTelemetryData struct { diff --git a/pkg/query-service/rules/db.go b/pkg/query-service/rules/db.go index e6f8d6301c..697ea63f92 100644 --- a/pkg/query-service/rules/db.go +++ b/pkg/query-service/rules/db.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "strconv" "strings" "time" @@ -551,10 +552,6 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { if strings.Contains(alert, "time_series_v2") { alertsInfo.AlertsWithTSV2 = alertsInfo.AlertsWithTSV2 + 1 } - if strings.Contains(alert, "signoz_logs.distributed_logs") || - strings.Contains(alert, "signoz_logs.logs") { - alertsInfo.AlertsWithLogsChQuery = alertsInfo.AlertsWithLogsChQuery + 1 - } err = json.Unmarshal([]byte(alert), &rule) if err != nil { zap.L().Error("invalid rule data", zap.Error(err)) @@ -563,6 +560,29 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { alertNames = append(alertNames, rule.AlertName) if rule.AlertType == AlertTypeLogs { alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1 + + if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil { + if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL { + if strings.Contains(alert, "signoz_logs.distributed_logs") || + strings.Contains(alert, "signoz_logs.logs") { + alertsInfo.AlertsWithLogsChQuery = alertsInfo.AlertsWithLogsChQuery + 1 + } + } + } + + for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries { + if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder { + if query.Filters != nil { + for _, item := range query.Filters.Items { + if slices.Contains([]string{"contains", "ncontains", "like", "nlike"}, string(item.Operator)) { + if item.Key.Key != "body" { + alertsInfo.AlertsWithLogsContainsOp += 1 + } + } + } + } + } + } } else if rule.AlertType == AlertTypeMetric { alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1 if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil { diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index be6ad4719c..62ff020281 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -333,6 +333,7 @@ func createTelemetry() { "dashboardNames": dashboardsInfo.DashboardNames, "alertNames": alertsInfo.AlertNames, "logsBasedPanels": dashboardsInfo.LogsBasedPanels, + "logsPanelsWithAttrContains": dashboardsInfo.LogsPanelsWithAttrContainsOp, "metricBasedPanels": dashboardsInfo.MetricBasedPanels, "tracesBasedPanels": dashboardsInfo.TracesBasedPanels, "dashboardsWithTSV2": dashboardsInfo.QueriesWithTSV2, @@ -346,6 +347,7 @@ func createTelemetry() { "totalSavedViews": savedViewsInfo.TotalSavedViews, "logsSavedViews": savedViewsInfo.LogsSavedViews, "tracesSavedViews": savedViewsInfo.TracesSavedViews, + "logSavedViewsWithContainsOp": savedViewsInfo.LogsSavedViewWithContainsOp, "slackChannels": alertsInfo.SlackChannels, "webHookChannels": alertsInfo.WebHookChannels, "pagerDutyChannels": alertsInfo.PagerDutyChannels, @@ -357,6 +359,7 @@ func createTelemetry() { "metricsPrometheusQueries": alertsInfo.MetricsPrometheusQueries, "spanMetricsPrometheusQueries": alertsInfo.SpanMetricsPrometheusQueries, "alertsWithLogsChQuery": alertsInfo.AlertsWithLogsChQuery, + "alertsWithLogsContainsOp": alertsInfo.AlertsWithLogsContainsOp, } // send event only if there are dashboards or alerts or channels if (dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 || alertsInfo.TotalChannels > 0 || savedViewsInfo.TotalSavedViews > 0) && apiErr == nil { From 0218f701b2611a62e4a92f3d0cecf22fb4fca4b6 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 20 Sep 2024 18:12:16 +0530 Subject: [PATCH 11/12] fix: alerts links are broken when there is a space in value (#6043) * fix: space between values being converted as + sign in alerts generated links * fix: added inline comment * fix: added inline comment --- .../src/hooks/queryBuilder/useGetCompositeQueryParam.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts index 442531a15b..efef00022a 100644 --- a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts +++ b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts @@ -13,7 +13,11 @@ export const useGetCompositeQueryParam = (): Query | null => { try { if (!compositeQuery) return null; - parsedCompositeQuery = JSON.parse(decodeURIComponent(compositeQuery)); + // MDN reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url + // MDN reference to support + characters using encoding - https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs add later + parsedCompositeQuery = JSON.parse( + decodeURIComponent(compositeQuery.replace(/\+/g, ' ')), + ); } catch (e) { parsedCompositeQuery = null; } From 4aabfe7cf5d87e339a5ecdbda164bf15b9c35df2 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 20 Sep 2024 18:13:55 +0530 Subject: [PATCH 12/12] fix: invalidate the cache for the alerts rules post update call (#6046) --- frontend/src/container/FormAlertRules/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index f53a6b2cfe..2947b2a0b3 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -370,7 +370,10 @@ function FormAlertRules({ }); // invalidate rule in cache - ruleCache.invalidateQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]); + ruleCache.invalidateQueries([ + REACT_QUERY_KEY.ALERT_RULE_DETAILS, + `${ruleId}`, + ]); // eslint-disable-next-line sonarjs/no-identical-functions setTimeout(() => {