From 1fc0461c0f4179bd0d9b0ea8615133683060f7bd Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Fri, 11 Aug 2023 15:17:32 +0530 Subject: [PATCH 01/17] refactor: updated loading state in gridgraphlayout (#3313) Co-authored-by: Palash Gupta --- .../container/GridGraphLayout/Graph/index.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index ee75c509f0..f363e02356 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -101,7 +101,7 @@ function GridCardGraph({ const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget); - if (queryResponse.isRefetching) { + if (queryResponse.isRefetching || queryResponse.isLoading) { return ; } @@ -127,27 +127,23 @@ function GridCardGraph({ ); } - if (queryResponse.status === 'loading' || queryResponse.status === 'idle') { + if (!isEmpty(widget) && prevChartDataSetRef?.labels) { return ( - {!isEmpty(widget) && prevChartDataSetRef?.labels ? ( - - ) : ( - - )} + ); } From 0946bcd5fceb7505c511433a09b216b08291735b Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Fri, 11 Aug 2023 16:58:26 +0530 Subject: [PATCH 02/17] fix: null issue when switching from clickhouse to Query builder (#3318) Co-authored-by: Palash Gupta --- .../container/QueryBuilder/filters/QueryBuilderSearch/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index 7111c1b0e8..efb8be717c 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -125,7 +125,7 @@ function QueryBuilderSearch({ useEffect(() => { const initialTagFilters: TagFilter = { items: [], op: 'AND' }; - const initialSourceKeys = query.filters.items.map( + const initialSourceKeys = query.filters.items?.map( (item) => item.key as BaseAutocompleteData, ); initialTagFilters.items = tags.map((tag) => { From 233589b86753c1ad03e7370774b6214ab984943f Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Fri, 11 Aug 2023 21:54:44 +0530 Subject: [PATCH 03/17] feat: convert livetail to a get endpoint (#3321) --- pkg/query-service/app/http_handler.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 2e5b6aeca5..885058f419 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net/http" "sort" @@ -295,7 +296,7 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *AuthMid subRouter.HandleFunc("/query_range", am.ViewAccess(aH.QueryRangeV3)).Methods(http.MethodPost) // live logs - subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.liveTailLogs)).Methods(http.MethodPost) + subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.liveTailLogs)).Methods(http.MethodGet) } func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) { @@ -3063,6 +3064,10 @@ func applyMetricLimit(results []*v3.Result, queryRangeParams *v3.QueryRangeParam func (aH *APIHandler) liveTailLogs(w http.ResponseWriter, r *http.Request) { + // get the param from url and add it to body + stringReader := strings.NewReader(r.URL.Query().Get("q")) + r.Body = io.NopCloser(stringReader) + queryRangeParams, apiErrorObj := ParseQueryRangeParams(r) if apiErrorObj != nil { zap.S().Errorf(apiErrorObj.Err.Error()) From 03220fcf1161dcd9c7f66a03bffa0d5ec1de914f Mon Sep 17 00:00:00 2001 From: Chandan Jhunjhunwal Date: Sat, 12 Aug 2023 10:10:25 +0530 Subject: [PATCH 04/17] chore: Fixed the typo in ProfilePirctureURL attr (#3307) --- pkg/query-service/app/http_handler.go | 18 +++++++-------- pkg/query-service/auth/auth.go | 32 +++++++++++++-------------- pkg/query-service/dao/sqlite/rbac.go | 8 +++---- pkg/query-service/model/db.go | 16 +++++++------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 885058f419..5267ac49d3 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -1935,18 +1935,18 @@ func (aH *APIHandler) editUser(w http.ResponseWriter, r *http.Request) { if len(update.Name) > 0 { old.Name = update.Name } - if len(update.ProfilePirctureURL) > 0 { - old.ProfilePirctureURL = update.ProfilePirctureURL + if len(update.ProfilePictureURL) > 0 { + old.ProfilePictureURL = update.ProfilePictureURL } _, apiErr = dao.DB().EditUser(ctx, &model.User{ - Id: old.Id, - Name: old.Name, - OrgId: old.OrgId, - Email: old.Email, - Password: old.Password, - CreatedAt: old.CreatedAt, - ProfilePirctureURL: old.ProfilePirctureURL, + Id: old.Id, + Name: old.Name, + OrgId: old.OrgId, + Email: old.Email, + Password: old.Password, + CreatedAt: old.CreatedAt, + ProfilePictureURL: old.ProfilePictureURL, }) if apiErr != nil { RespondError(w, apiErr, nil) diff --git a/pkg/query-service/auth/auth.go b/pkg/query-service/auth/auth.go index b7fc34e1ed..d2488a1399 100644 --- a/pkg/query-service/auth/auth.go +++ b/pkg/query-service/auth/auth.go @@ -250,14 +250,14 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User, } user := &model.User{ - Id: uuid.NewString(), - Name: req.Name, - Email: req.Email, - Password: hash, - CreatedAt: time.Now().Unix(), - ProfilePirctureURL: "", // Currently unused - GroupId: group.Id, - OrgId: org.Id, + Id: uuid.NewString(), + Name: req.Name, + Email: req.Email, + Password: hash, + CreatedAt: time.Now().Unix(), + ProfilePictureURL: "", // Currently unused + GroupId: group.Id, + OrgId: org.Id, } return dao.DB().CreateUser(ctx, user, true) @@ -328,14 +328,14 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b } user := &model.User{ - Id: uuid.NewString(), - Name: req.Name, - Email: req.Email, - Password: hash, - CreatedAt: time.Now().Unix(), - ProfilePirctureURL: "", // Currently unused - GroupId: group.Id, - OrgId: invite.OrgId, + Id: uuid.NewString(), + Name: req.Name, + Email: req.Email, + Password: hash, + CreatedAt: time.Now().Unix(), + ProfilePictureURL: "", // Currently unused + GroupId: group.Id, + OrgId: invite.OrgId, } // TODO(Ahsan): Ideally create user and delete invitation should happen in a txn. diff --git a/pkg/query-service/dao/sqlite/rbac.go b/pkg/query-service/dao/sqlite/rbac.go index 64ff7ed8ae..bc39904ee6 100644 --- a/pkg/query-service/dao/sqlite/rbac.go +++ b/pkg/query-service/dao/sqlite/rbac.go @@ -185,7 +185,7 @@ func (mds *ModelDaoSqlite) CreateUser(ctx context.Context, `INSERT INTO users (id, name, email, password, created_at, profile_picture_url, group_id, org_id) VALUES (?, ?, ?, ?, ?, ?, ?,?);`, user.Id, user.Name, user.Email, user.Password, user.CreatedAt, - user.ProfilePirctureURL, user.GroupId, user.OrgId, + user.ProfilePictureURL, user.GroupId, user.OrgId, ) if err != nil { @@ -275,13 +275,13 @@ func (mds *ModelDaoSqlite) GetUser(ctx context.Context, u.group_id, g.name as role, o.name as organization, - COALESCE((select uf.flags - from user_flags uf + COALESCE((select uf.flags + from user_flags uf where u.id = uf.user_id), '') as flags from users u, groups g, organizations o where g.id=u.group_id and - o.id = u.org_id and + o.id = u.org_id and u.id=?;` if err := mds.db.Select(&users, query, id); err != nil { diff --git a/pkg/query-service/model/db.go b/pkg/query-service/model/db.go index 9ae7270232..9dcefd7ee0 100644 --- a/pkg/query-service/model/db.go +++ b/pkg/query-service/model/db.go @@ -26,14 +26,14 @@ type InvitationObject struct { } type User struct { - Id string `json:"id" db:"id"` - Name string `json:"name" db:"name"` - Email string `json:"email" db:"email"` - Password string `json:"password,omitempty" db:"password"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - ProfilePirctureURL string `json:"profilePictureURL" db:"profile_picture_url"` - OrgId string `json:"orgId,omitempty" db:"org_id"` - GroupId string `json:"groupId,omitempty" db:"group_id"` + Id string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Email string `json:"email" db:"email"` + Password string `json:"password,omitempty" db:"password"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + ProfilePictureURL string `json:"profilePictureURL" db:"profile_picture_url"` + OrgId string `json:"orgId,omitempty" db:"org_id"` + GroupId string `json:"groupId,omitempty" db:"group_id"` } type ApdexSettings struct { From ec7c99dd26566b5b161a1dedab104e23485dff36 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Sun, 13 Aug 2023 21:26:55 +0530 Subject: [PATCH 05/17] fix: alerts work when "equals to" or "not equals to" used with "all the times" or "at least once" (#3244) --- pkg/query-service/rules/resultTypes.go | 5 ++-- pkg/query-service/rules/thresholdRule.go | 33 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/pkg/query-service/rules/resultTypes.go b/pkg/query-service/rules/resultTypes.go index e7e67bc7bd..de2095eea9 100644 --- a/pkg/query-service/rules/resultTypes.go +++ b/pkg/query-service/rules/resultTypes.go @@ -34,8 +34,9 @@ func (s Sample) MarshalJSON() ([]byte, error) { } type Point struct { - T int64 - V float64 + T int64 + V float64 + Vs []float64 } func (p Point) String() string { diff --git a/pkg/query-service/rules/thresholdRule.go b/pkg/query-service/rules/thresholdRule.go index f6e79a1643..fbd65d9948 100644 --- a/pkg/query-service/rules/thresholdRule.go +++ b/pkg/query-service/rules/thresholdRule.go @@ -550,7 +550,40 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer } } + + if s, ok := resultMap[labelHash]; ok { + s.Point.Vs = append(s.Point.Vs, s.Point.V) + } } + + for _, s := range resultMap { + if r.matchType() == AllTheTimes && r.compareOp() == ValueIsEq { + for _, v := range s.Point.Vs { + if v != r.targetVal() { // if any of the values is not equal to target, alert shouldn't be sent + s.Point.V = v + } + } + } else if r.matchType() == AllTheTimes && r.compareOp() == ValueIsNotEq { + for _, v := range s.Point.Vs { + if v == r.targetVal() { // if any of the values is equal to target, alert shouldn't be sent + s.Point.V = v + } + } + } else if r.matchType() == AtleastOnce && r.compareOp() == ValueIsEq { + for _, v := range s.Point.Vs { + if v == r.targetVal() { // if any of the values is equal to target, alert should be sent + s.Point.V = v + } + } + } else if r.matchType() == AtleastOnce && r.compareOp() == ValueIsNotEq { + for _, v := range s.Point.Vs { + if v != r.targetVal() { // if any of the values is not equal to target, alert should be sent + s.Point.V = v + } + } + } + } + zap.S().Debugf("ruleid:", r.ID(), "\t resultmap(potential alerts):", len(resultMap)) for _, sample := range resultMap { From 17c61a61ec2d222b82d71dd695b3a736ebc90fa6 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Mon, 14 Aug 2023 17:08:57 +0530 Subject: [PATCH 06/17] chore: format for livetail changed (#3332) --- pkg/query-service/app/http_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 5267ac49d3..06d57603ec 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -3128,7 +3128,7 @@ func (aH *APIHandler) liveTailLogs(w http.ResponseWriter, r *http.Request) { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.Encode(log) - fmt.Fprintf(w, "event: log\ndata: %v\n\n", buf.String()) + fmt.Fprintf(w, "data: %v\n\n", buf.String()) flusher.Flush() case <-client.Done: zap.S().Debug("done!") From c37d6c3785d6006ab6ed94c1a439dae6c07abd8b Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Tue, 15 Aug 2023 06:37:26 +0530 Subject: [PATCH 07/17] feat: added the support of y axis unit and threshold unit in the alerts page (#3268) --- frontend/public/locales/en-GB/alerts.json | 223 +++--- frontend/public/locales/en/alerts.json | 223 +++--- .../src/container/CreateAlertRule/defaults.ts | 4 + .../FormAlertRules/ChartPreview/config.ts | 98 +++ .../FormAlertRules/ChartPreview/index.tsx | 25 +- .../FormAlertRules/ChartPreview/utils.test.ts | 105 +++ .../FormAlertRules/ChartPreview/utils.ts | 54 ++ .../container/FormAlertRules/RuleOptions.tsx | 105 ++- .../src/container/FormAlertRules/index.tsx | 26 +- .../src/container/FormAlertRules/styles.ts | 4 + .../RightContainer/alertFomatCategories.ts | 121 +++ .../RightContainer/dataFormatCategories.ts | 695 ++++++++++-------- .../NewWidget/RightContainer/types.ts | 364 +++++++++ .../BuilderUnitsFilter/BuilderUnits.tsx | 48 ++ .../filters/BuilderUnitsFilter/config.ts | 10 + .../filters/BuilderUnitsFilter/index.ts | 1 + .../filters/BuilderUnitsFilter/styles.ts | 11 + .../filters/BuilderUnitsFilter/types.ts | 3 + .../filters/BuilderUnitsFilter/utils.ts | 6 + .../container/QueryBuilder/filters/index.ts | 1 + .../mapQueryDataFromApi.ts | 1 + frontend/src/providers/QueryBuilder.tsx | 16 + .../actions/dashboard/getQueryResults.ts | 1 + .../src/types/api/alerts/compositeQuery.ts | 2 + frontend/src/types/api/alerts/def.ts | 1 + .../api/queryBuilder/queryBuilderData.ts | 2 + frontend/src/types/common/queryBuilder.ts | 2 + 27 files changed, 1568 insertions(+), 584 deletions(-) create mode 100644 frontend/src/container/FormAlertRules/ChartPreview/config.ts create mode 100644 frontend/src/container/FormAlertRules/ChartPreview/utils.test.ts create mode 100644 frontend/src/container/FormAlertRules/ChartPreview/utils.ts create mode 100644 frontend/src/container/NewWidget/RightContainer/alertFomatCategories.ts create mode 100644 frontend/src/container/NewWidget/RightContainer/types.ts create mode 100644 frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/BuilderUnits.tsx create mode 100644 frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/config.ts create mode 100644 frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/index.ts create mode 100644 frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/styles.ts create mode 100644 frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/types.ts create mode 100644 frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/utils.ts diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index b5c769a021..816b71e563 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -1,112 +1,113 @@ { - "target_missing": "Please enter a threshold to proceed", - "rule_test_fired": "Test notification sent successfully", - "no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.", - "button_testrule": "Test Notification", - "label_channel_select": "Notification Channels", - "placeholder_channel_select": "select one or more channels", - "channel_select_tooltip": "Leave empty to send this alert on all the configured channels", - "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", - "preview_chart_threshold_label": "Threshold", - "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", - "button_yes": "Yes", - "button_no": "No", - "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", - "remove_label_success": "Labels cleared", - "alert_form_step1": "Step 1 - Define the metric", - "alert_form_step2": "Step 2 - Define Alert Conditions", - "alert_form_step3": "Step 3 - Alert Configuration", - "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", - "confirm_save_title": "Save Changes", - "confirm_save_content_part1": "Your alert built with", - "confirm_save_content_part2": "query will be saved. Press OK to confirm.", - "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", - "rule_created": "Rule created successfully", - "rule_edited": "Rule edited successfully", - "expression_missing": "expression is missing in {{where}}", - "metricname_missing": "metric name is missing in {{where}}", - "condition_required": "at least one metric condition is required", - "alertname_required": "alert name is required", - "promql_required": "promql expression is required when query format is set to PromQL", - "chquery_required": "query is required when query format is set to ClickHouse", - "button_savechanges": "Save Rule", - "button_createrule": "Create Rule", - "button_returntorules": "Return to rules", - "button_cancelchanges": "Cancel", - "button_discard": "Discard", - "text_condition1": "Send a notification when the metric is", - "text_condition2": "the threshold", - "text_condition3": "during the last", - "option_5min": "5 mins", - "option_10min": "10 mins", - "option_15min": "15 mins", - "option_60min": "60 mins", - "option_4hours": "4 hours", - "option_24hours": "24 hours", - "field_threshold": "Alert Threshold", - "option_allthetimes": "all the times", - "option_atleastonce": "at least once", - "option_onaverage": "on average", - "option_intotal": "in total", - "option_above": "above", - "option_below": "below", - "option_equal": "is equal to", - "option_notequal": "not equal to", - "button_query": "Query", - "button_formula": "Formula", - "tab_qb": "Query Builder", - "tab_promql": "PromQL", - "tab_chquery": "ClickHouse Query", - "title_confirm": "Confirm", - "button_ok": "Yes", - "button_cancel": "No", - "field_promql_expr": "PromQL Expression", - "field_alert_name": "Alert Name", - "field_alert_desc": "Alert Description", - "field_labels": "Labels", - "field_severity": "Severity", - "option_critical": "Critical", - "option_error": "Error", - "option_warning": "Warning", - "option_info": "Info", - "user_guide_headline": "Steps to create an Alert", - "user_guide_qb_step1": "Step 1 - Define the metric", - "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", - "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", - "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", - "user_guide_qb_step1d": "Create a formula based on Queries if needed", - "user_guide_qb_step2": "Step 2 - Define Alert Conditions", - "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", - "user_guide_qb_step2b": "Enter the Alert threshold", - "user_guide_qb_step3": "Step 3 -Alert Configuration", - "user_guide_qb_step3a": "Set alert severity, name and descriptions", - "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", - "user_guide_pql_step1": "Step 1 - Define the metric", - "user_guide_pql_step1a": "Write a PromQL query for the metric", - "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", - "user_guide_pql_step2": "Step 2 - Define Alert Conditions", - "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", - "user_guide_pql_step2b": "Enter the Alert threshold", - "user_guide_pql_step3": "Step 3 -Alert Configuration", - "user_guide_pql_step3a": "Set alert severity, name and descriptions", - "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", - "user_guide_ch_step1": "Step 1 - Define the metric", - "user_guide_ch_step1a": "Write a Clickhouse query for alert evaluation. Follow <0>this tutorial to learn about query format and supported vars.", - "user_guide_ch_step1b": "Format the legends based on labels you want to highlight in the preview chart", - "user_guide_ch_step2": "Step 2 - Define Alert Conditions", - "user_guide_ch_step2a": "Select the threshold type and whether you want to alert above/below a value", - "user_guide_ch_step2b": "Enter the Alert threshold", - "user_guide_ch_step3": "Step 3 -Alert Configuration", - "user_guide_ch_step3a": "Set alert severity, name and descriptions", - "user_guide_ch_step3b": "Add tags to the alert in the Label field if needed", - "user_tooltip_more_help": "More details on how to create alerts", - "choose_alert_type": "Choose a type for the alert:", - "metric_based_alert": "Metric based Alert", - "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", - "log_based_alert": "Log-based Alert", - "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", - "traces_based_alert": "Trace-based Alert", - "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", - "exceptions_based_alert": "Exceptions-based Alert", - "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data." -} \ No newline at end of file + "target_missing": "Please enter a threshold to proceed", + "rule_test_fired": "Test notification sent successfully", + "no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.", + "button_testrule": "Test Notification", + "label_channel_select": "Notification Channels", + "placeholder_channel_select": "select one or more channels", + "channel_select_tooltip": "Leave empty to send this alert on all the configured channels", + "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", + "preview_chart_threshold_label": "Threshold", + "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", + "button_yes": "Yes", + "button_no": "No", + "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", + "remove_label_success": "Labels cleared", + "alert_form_step1": "Step 1 - Define the metric", + "alert_form_step2": "Step 2 - Define Alert Conditions", + "alert_form_step3": "Step 3 - Alert Configuration", + "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", + "confirm_save_title": "Save Changes", + "confirm_save_content_part1": "Your alert built with", + "confirm_save_content_part2": "query will be saved. Press OK to confirm.", + "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", + "rule_created": "Rule created successfully", + "rule_edited": "Rule edited successfully", + "expression_missing": "expression is missing in {{where}}", + "metricname_missing": "metric name is missing in {{where}}", + "condition_required": "at least one metric condition is required", + "alertname_required": "alert name is required", + "promql_required": "promql expression is required when query format is set to PromQL", + "chquery_required": "query is required when query format is set to ClickHouse", + "button_savechanges": "Save Rule", + "button_createrule": "Create Rule", + "button_returntorules": "Return to rules", + "button_cancelchanges": "Cancel", + "button_discard": "Discard", + "text_condition1": "Send a notification when the metric is", + "text_condition2": "the threshold", + "text_condition3": "during the last", + "option_5min": "5 mins", + "option_10min": "10 mins", + "option_15min": "15 mins", + "option_60min": "60 mins", + "option_4hours": "4 hours", + "option_24hours": "24 hours", + "field_threshold": "Alert Threshold", + "option_allthetimes": "all the times", + "option_atleastonce": "at least once", + "option_onaverage": "on average", + "option_intotal": "in total", + "option_above": "above", + "option_below": "below", + "option_equal": "is equal to", + "option_notequal": "not equal to", + "button_query": "Query", + "button_formula": "Formula", + "tab_qb": "Query Builder", + "tab_promql": "PromQL", + "tab_chquery": "ClickHouse Query", + "title_confirm": "Confirm", + "button_ok": "Yes", + "button_cancel": "No", + "field_promql_expr": "PromQL Expression", + "field_alert_name": "Alert Name", + "field_alert_desc": "Alert Description", + "field_labels": "Labels", + "field_severity": "Severity", + "option_critical": "Critical", + "option_error": "Error", + "option_warning": "Warning", + "option_info": "Info", + "user_guide_headline": "Steps to create an Alert", + "user_guide_qb_step1": "Step 1 - Define the metric", + "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", + "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", + "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", + "user_guide_qb_step1d": "Create a formula based on Queries if needed", + "user_guide_qb_step2": "Step 2 - Define Alert Conditions", + "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", + "user_guide_qb_step2b": "Enter the Alert threshold", + "user_guide_qb_step3": "Step 3 -Alert Configuration", + "user_guide_qb_step3a": "Set alert severity, name and descriptions", + "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", + "user_guide_pql_step1": "Step 1 - Define the metric", + "user_guide_pql_step1a": "Write a PromQL query for the metric", + "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", + "user_guide_pql_step2": "Step 2 - Define Alert Conditions", + "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", + "user_guide_pql_step2b": "Enter the Alert threshold", + "user_guide_pql_step3": "Step 3 -Alert Configuration", + "user_guide_pql_step3a": "Set alert severity, name and descriptions", + "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", + "user_guide_ch_step1": "Step 1 - Define the metric", + "user_guide_ch_step1a": "Write a Clickhouse query for alert evaluation. Follow <0>this tutorial to learn about query format and supported vars.", + "user_guide_ch_step1b": "Format the legends based on labels you want to highlight in the preview chart", + "user_guide_ch_step2": "Step 2 - Define Alert Conditions", + "user_guide_ch_step2a": "Select the threshold type and whether you want to alert above/below a value", + "user_guide_ch_step2b": "Enter the Alert threshold", + "user_guide_ch_step3": "Step 3 -Alert Configuration", + "user_guide_ch_step3a": "Set alert severity, name and descriptions", + "user_guide_ch_step3b": "Add tags to the alert in the Label field if needed", + "user_tooltip_more_help": "More details on how to create alerts", + "choose_alert_type": "Choose a type for the alert:", + "metric_based_alert": "Metric based Alert", + "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", + "log_based_alert": "Log-based Alert", + "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", + "traces_based_alert": "Trace-based Alert", + "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", + "exceptions_based_alert": "Exceptions-based Alert", + "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", + "field_unit": "Threshold unit" +} diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index b5c769a021..816b71e563 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -1,112 +1,113 @@ { - "target_missing": "Please enter a threshold to proceed", - "rule_test_fired": "Test notification sent successfully", - "no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.", - "button_testrule": "Test Notification", - "label_channel_select": "Notification Channels", - "placeholder_channel_select": "select one or more channels", - "channel_select_tooltip": "Leave empty to send this alert on all the configured channels", - "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", - "preview_chart_threshold_label": "Threshold", - "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", - "button_yes": "Yes", - "button_no": "No", - "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", - "remove_label_success": "Labels cleared", - "alert_form_step1": "Step 1 - Define the metric", - "alert_form_step2": "Step 2 - Define Alert Conditions", - "alert_form_step3": "Step 3 - Alert Configuration", - "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", - "confirm_save_title": "Save Changes", - "confirm_save_content_part1": "Your alert built with", - "confirm_save_content_part2": "query will be saved. Press OK to confirm.", - "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", - "rule_created": "Rule created successfully", - "rule_edited": "Rule edited successfully", - "expression_missing": "expression is missing in {{where}}", - "metricname_missing": "metric name is missing in {{where}}", - "condition_required": "at least one metric condition is required", - "alertname_required": "alert name is required", - "promql_required": "promql expression is required when query format is set to PromQL", - "chquery_required": "query is required when query format is set to ClickHouse", - "button_savechanges": "Save Rule", - "button_createrule": "Create Rule", - "button_returntorules": "Return to rules", - "button_cancelchanges": "Cancel", - "button_discard": "Discard", - "text_condition1": "Send a notification when the metric is", - "text_condition2": "the threshold", - "text_condition3": "during the last", - "option_5min": "5 mins", - "option_10min": "10 mins", - "option_15min": "15 mins", - "option_60min": "60 mins", - "option_4hours": "4 hours", - "option_24hours": "24 hours", - "field_threshold": "Alert Threshold", - "option_allthetimes": "all the times", - "option_atleastonce": "at least once", - "option_onaverage": "on average", - "option_intotal": "in total", - "option_above": "above", - "option_below": "below", - "option_equal": "is equal to", - "option_notequal": "not equal to", - "button_query": "Query", - "button_formula": "Formula", - "tab_qb": "Query Builder", - "tab_promql": "PromQL", - "tab_chquery": "ClickHouse Query", - "title_confirm": "Confirm", - "button_ok": "Yes", - "button_cancel": "No", - "field_promql_expr": "PromQL Expression", - "field_alert_name": "Alert Name", - "field_alert_desc": "Alert Description", - "field_labels": "Labels", - "field_severity": "Severity", - "option_critical": "Critical", - "option_error": "Error", - "option_warning": "Warning", - "option_info": "Info", - "user_guide_headline": "Steps to create an Alert", - "user_guide_qb_step1": "Step 1 - Define the metric", - "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", - "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", - "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", - "user_guide_qb_step1d": "Create a formula based on Queries if needed", - "user_guide_qb_step2": "Step 2 - Define Alert Conditions", - "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", - "user_guide_qb_step2b": "Enter the Alert threshold", - "user_guide_qb_step3": "Step 3 -Alert Configuration", - "user_guide_qb_step3a": "Set alert severity, name and descriptions", - "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", - "user_guide_pql_step1": "Step 1 - Define the metric", - "user_guide_pql_step1a": "Write a PromQL query for the metric", - "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", - "user_guide_pql_step2": "Step 2 - Define Alert Conditions", - "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", - "user_guide_pql_step2b": "Enter the Alert threshold", - "user_guide_pql_step3": "Step 3 -Alert Configuration", - "user_guide_pql_step3a": "Set alert severity, name and descriptions", - "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", - "user_guide_ch_step1": "Step 1 - Define the metric", - "user_guide_ch_step1a": "Write a Clickhouse query for alert evaluation. Follow <0>this tutorial to learn about query format and supported vars.", - "user_guide_ch_step1b": "Format the legends based on labels you want to highlight in the preview chart", - "user_guide_ch_step2": "Step 2 - Define Alert Conditions", - "user_guide_ch_step2a": "Select the threshold type and whether you want to alert above/below a value", - "user_guide_ch_step2b": "Enter the Alert threshold", - "user_guide_ch_step3": "Step 3 -Alert Configuration", - "user_guide_ch_step3a": "Set alert severity, name and descriptions", - "user_guide_ch_step3b": "Add tags to the alert in the Label field if needed", - "user_tooltip_more_help": "More details on how to create alerts", - "choose_alert_type": "Choose a type for the alert:", - "metric_based_alert": "Metric based Alert", - "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", - "log_based_alert": "Log-based Alert", - "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", - "traces_based_alert": "Trace-based Alert", - "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", - "exceptions_based_alert": "Exceptions-based Alert", - "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data." -} \ No newline at end of file + "target_missing": "Please enter a threshold to proceed", + "rule_test_fired": "Test notification sent successfully", + "no_alerts_found": "No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.", + "button_testrule": "Test Notification", + "label_channel_select": "Notification Channels", + "placeholder_channel_select": "select one or more channels", + "channel_select_tooltip": "Leave empty to send this alert on all the configured channels", + "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", + "preview_chart_threshold_label": "Threshold", + "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", + "button_yes": "Yes", + "button_no": "No", + "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", + "remove_label_success": "Labels cleared", + "alert_form_step1": "Step 1 - Define the metric", + "alert_form_step2": "Step 2 - Define Alert Conditions", + "alert_form_step3": "Step 3 - Alert Configuration", + "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", + "confirm_save_title": "Save Changes", + "confirm_save_content_part1": "Your alert built with", + "confirm_save_content_part2": "query will be saved. Press OK to confirm.", + "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", + "rule_created": "Rule created successfully", + "rule_edited": "Rule edited successfully", + "expression_missing": "expression is missing in {{where}}", + "metricname_missing": "metric name is missing in {{where}}", + "condition_required": "at least one metric condition is required", + "alertname_required": "alert name is required", + "promql_required": "promql expression is required when query format is set to PromQL", + "chquery_required": "query is required when query format is set to ClickHouse", + "button_savechanges": "Save Rule", + "button_createrule": "Create Rule", + "button_returntorules": "Return to rules", + "button_cancelchanges": "Cancel", + "button_discard": "Discard", + "text_condition1": "Send a notification when the metric is", + "text_condition2": "the threshold", + "text_condition3": "during the last", + "option_5min": "5 mins", + "option_10min": "10 mins", + "option_15min": "15 mins", + "option_60min": "60 mins", + "option_4hours": "4 hours", + "option_24hours": "24 hours", + "field_threshold": "Alert Threshold", + "option_allthetimes": "all the times", + "option_atleastonce": "at least once", + "option_onaverage": "on average", + "option_intotal": "in total", + "option_above": "above", + "option_below": "below", + "option_equal": "is equal to", + "option_notequal": "not equal to", + "button_query": "Query", + "button_formula": "Formula", + "tab_qb": "Query Builder", + "tab_promql": "PromQL", + "tab_chquery": "ClickHouse Query", + "title_confirm": "Confirm", + "button_ok": "Yes", + "button_cancel": "No", + "field_promql_expr": "PromQL Expression", + "field_alert_name": "Alert Name", + "field_alert_desc": "Alert Description", + "field_labels": "Labels", + "field_severity": "Severity", + "option_critical": "Critical", + "option_error": "Error", + "option_warning": "Warning", + "option_info": "Info", + "user_guide_headline": "Steps to create an Alert", + "user_guide_qb_step1": "Step 1 - Define the metric", + "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", + "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", + "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", + "user_guide_qb_step1d": "Create a formula based on Queries if needed", + "user_guide_qb_step2": "Step 2 - Define Alert Conditions", + "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", + "user_guide_qb_step2b": "Enter the Alert threshold", + "user_guide_qb_step3": "Step 3 -Alert Configuration", + "user_guide_qb_step3a": "Set alert severity, name and descriptions", + "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", + "user_guide_pql_step1": "Step 1 - Define the metric", + "user_guide_pql_step1a": "Write a PromQL query for the metric", + "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", + "user_guide_pql_step2": "Step 2 - Define Alert Conditions", + "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", + "user_guide_pql_step2b": "Enter the Alert threshold", + "user_guide_pql_step3": "Step 3 -Alert Configuration", + "user_guide_pql_step3a": "Set alert severity, name and descriptions", + "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", + "user_guide_ch_step1": "Step 1 - Define the metric", + "user_guide_ch_step1a": "Write a Clickhouse query for alert evaluation. Follow <0>this tutorial to learn about query format and supported vars.", + "user_guide_ch_step1b": "Format the legends based on labels you want to highlight in the preview chart", + "user_guide_ch_step2": "Step 2 - Define Alert Conditions", + "user_guide_ch_step2a": "Select the threshold type and whether you want to alert above/below a value", + "user_guide_ch_step2b": "Enter the Alert threshold", + "user_guide_ch_step3": "Step 3 -Alert Configuration", + "user_guide_ch_step3a": "Set alert severity, name and descriptions", + "user_guide_ch_step3b": "Add tags to the alert in the Label field if needed", + "user_tooltip_more_help": "More details on how to create alerts", + "choose_alert_type": "Choose a type for the alert:", + "metric_based_alert": "Metric based Alert", + "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", + "log_based_alert": "Log-based Alert", + "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", + "traces_based_alert": "Trace-based Alert", + "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", + "exceptions_based_alert": "Exceptions-based Alert", + "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", + "field_unit": "Threshold unit" +} diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index adc7c9154d..2ac2f3a7b8 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -40,6 +40,7 @@ export const alertDefaults: AlertDef = { }, queryType: EQueryType.QUERY_BUILDER, panelType: PANEL_TYPES.TIME_SERIES, + unit: undefined, }, op: defaultCompareOp, matchType: defaultMatchType, @@ -69,6 +70,7 @@ export const logAlertDefaults: AlertDef = { }, queryType: EQueryType.QUERY_BUILDER, panelType: PANEL_TYPES.TIME_SERIES, + unit: undefined, }, op: defaultCompareOp, matchType: '4', @@ -99,6 +101,7 @@ export const traceAlertDefaults: AlertDef = { }, queryType: EQueryType.QUERY_BUILDER, panelType: PANEL_TYPES.TIME_SERIES, + unit: undefined, }, op: defaultCompareOp, matchType: '4', @@ -129,6 +132,7 @@ export const exceptionAlertDefaults: AlertDef = { }, queryType: EQueryType.QUERY_BUILDER, panelType: PANEL_TYPES.TIME_SERIES, + unit: undefined, }, op: defaultCompareOp, matchType: '4', diff --git a/frontend/src/container/FormAlertRules/ChartPreview/config.ts b/frontend/src/container/FormAlertRules/ChartPreview/config.ts new file mode 100644 index 0000000000..94001bbd8c --- /dev/null +++ b/frontend/src/container/FormAlertRules/ChartPreview/config.ts @@ -0,0 +1,98 @@ +import { + DataFormats, + DataRateFormats, + MiscellaneousFormats, + ThroughputFormats, + TimeFormats, +} from 'container/NewWidget/RightContainer/types'; + +export const dataFormatConfig: Record = { + [DataFormats.BytesIEC]: 1, + [DataFormats.BytesSI]: 1, + [DataFormats.BitsIEC]: 1 / 8, + [DataFormats.BitsSI]: 1 / 8, + [DataFormats.KibiBytes]: 1024, + [DataFormats.KiloBytes]: 1000, + [DataFormats.MebiBytes]: 1024 ** 2, + [DataFormats.MegaBytes]: 1000 ** 2, + [DataFormats.GibiBytes]: 1024 ** 3, + [DataFormats.GigaBytes]: 1000 ** 3, + [DataFormats.TebiBytes]: 1024 ** 4, + [DataFormats.TeraBytes]: 1000 ** 4, + [DataFormats.PebiBytes]: 1024 ** 5, + [DataFormats.PetaBytes]: 1000 ** 5, +}; + +export const throughputConfig: Record = { + [ThroughputFormats.CountsPerSec]: 1, + [ThroughputFormats.OpsPerSec]: 1, + [ThroughputFormats.RequestsPerSec]: 1, + [ThroughputFormats.ReadsPerSec]: 1, + [ThroughputFormats.WritesPerSec]: 1, + [ThroughputFormats.IOOpsPerSec]: 1, + [ThroughputFormats.CountsPerMin]: 1 / 60, + [ThroughputFormats.OpsPerMin]: 1 / 60, + [ThroughputFormats.ReadsPerMin]: 1 / 60, + [ThroughputFormats.WritesPerMin]: 1 / 60, +}; + +export const timeUnitsConfig: Record = { + [TimeFormats.Hertz]: 1, + [TimeFormats.Nanoseconds]: 1e-9, + [TimeFormats.Microseconds]: 1e-6, + [TimeFormats.Milliseconds]: 1e-3, + [TimeFormats.Seconds]: 1, + [TimeFormats.Minutes]: 60, + [TimeFormats.Hours]: 3600, + [TimeFormats.Days]: 86400, + [TimeFormats.DurationMs]: 1e-3, + [TimeFormats.DurationS]: 1, + [TimeFormats.DurationHms]: 3600, + [TimeFormats.DurationDhms]: 86400, + [TimeFormats.Timeticks]: 1e-3, + [TimeFormats.ClockMs]: 1e-3, + [TimeFormats.ClockS]: 1, +}; + +export const dataRateUnitsConfig: Record = { + [DataRateFormats.PacketsPerSec]: 1, + [DataRateFormats.BytesPerSecIEC]: dataFormatConfig[DataFormats.BytesIEC], + [DataRateFormats.BytesPerSecSI]: dataFormatConfig[DataFormats.BytesSI], + [DataRateFormats.BitsPerSecIEC]: dataFormatConfig[DataFormats.BitsIEC], + [DataRateFormats.BitsPerSecSI]: dataFormatConfig[DataFormats.BitsSI], + [DataRateFormats.KibiBytesPerSec]: dataFormatConfig[DataFormats.KibiBytes], + [DataRateFormats.KibiBitsPerSec]: dataFormatConfig[DataFormats.KibiBytes] * 8, + [DataRateFormats.KiloBytesPerSec]: dataFormatConfig[DataFormats.KiloBytes], + [DataRateFormats.KiloBitsPerSec]: dataFormatConfig[DataFormats.KiloBytes] * 8, + [DataRateFormats.MebiBytesPerSec]: dataFormatConfig[DataFormats.MebiBytes], + [DataRateFormats.MebiBitsPerSec]: dataFormatConfig[DataFormats.MebiBytes] * 8, + [DataRateFormats.MegaBytesPerSec]: dataFormatConfig[DataFormats.MegaBytes], + [DataRateFormats.MegaBitsPerSec]: dataFormatConfig[DataFormats.MegaBytes] * 8, + [DataRateFormats.GibiBytesPerSec]: dataFormatConfig[DataFormats.GibiBytes], + [DataRateFormats.GibiBitsPerSec]: dataFormatConfig[DataFormats.GibiBytes] * 8, + [DataRateFormats.GigaBytesPerSec]: dataFormatConfig[DataFormats.GigaBytes], + [DataRateFormats.GigaBitsPerSec]: dataFormatConfig[DataFormats.GigaBytes] * 8, + [DataRateFormats.TebiBytesPerSec]: dataFormatConfig[DataFormats.TebiBytes], + [DataRateFormats.TebiBitsPerSec]: dataFormatConfig[DataFormats.TebiBytes] * 8, + [DataRateFormats.TeraBytesPerSec]: dataFormatConfig[DataFormats.TeraBytes], + [DataRateFormats.TeraBitsPerSec]: dataFormatConfig[DataFormats.TeraBytes] * 8, + [DataRateFormats.PebiBytesPerSec]: dataFormatConfig[DataFormats.PebiBytes], + [DataRateFormats.PebiBitsPerSec]: dataFormatConfig[DataFormats.PebiBytes] * 8, + [DataRateFormats.PetaBytesPerSec]: dataFormatConfig[DataFormats.PetaBytes], + [DataRateFormats.PetaBitsPerSec]: dataFormatConfig[DataFormats.PetaBytes] * 8, +}; + +export const miscUnitsConfig: Record = { + [MiscellaneousFormats.None]: 1, + [MiscellaneousFormats.String]: 1, + [MiscellaneousFormats.Short]: 1, + [MiscellaneousFormats.Percent]: 0.01, + [MiscellaneousFormats.PercentUnit]: 1, + [MiscellaneousFormats.Humidity]: 1, + [MiscellaneousFormats.Decibel]: 1, + [MiscellaneousFormats.Hexadecimal0x]: 1, + [MiscellaneousFormats.Hexadecimal]: 1, + [MiscellaneousFormats.ScientificNotation]: 1, + [MiscellaneousFormats.LocaleFormat]: 1, + [MiscellaneousFormats.Pixels]: 1, +}; diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index 33bcfa3c37..f6bf35cbd7 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -9,10 +9,12 @@ import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import getChartData from 'lib/getChartData'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { AlertDef } from 'types/api/alerts/def'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { ChartContainer, FailedMessageContainer } from './styles'; +import { covertIntoDataFormats } from './utils'; export interface ChartPreviewProps { name: string; @@ -21,7 +23,7 @@ export interface ChartPreviewProps { selectedTime?: timePreferenceType; selectedInterval?: Time; headline?: JSX.Element; - threshold?: number | undefined; + alertDef?: AlertDef; userQueryKey?: string; } @@ -32,18 +34,28 @@ function ChartPreview({ selectedTime = 'GLOBAL_TIME', selectedInterval = '5min', headline, - threshold, userQueryKey, + alertDef, }: ChartPreviewProps): JSX.Element | null { const { t } = useTranslation('alerts'); + const threshold = alertDef?.condition.target || 0; + + const thresholdValue = covertIntoDataFormats({ + value: threshold, + sourceUnit: alertDef?.condition.targetUnit, + targetUnit: query?.unit, + }); + const staticLine: StaticLineProps | undefined = threshold !== undefined ? { - yMin: threshold, - yMax: threshold, + yMin: thresholdValue, + yMax: thresholdValue, borderColor: '#f14', borderWidth: 1, - lineText: `${t('preview_chart_threshold_label')} (y=${threshold})`, + lineText: `${t('preview_chart_threshold_label')} (y=${thresholdValue} ${ + query?.unit || '' + })`, textColor: '#f14', } : undefined; @@ -121,6 +133,7 @@ function ChartPreview({ staticLine={staticLine} panelData={queryResponse.data?.payload.data.newResult.data.result || []} query={query || initialQueriesMap.metrics} + yAxisUnit={query?.unit} /> )} @@ -132,8 +145,8 @@ ChartPreview.defaultProps = { selectedTime: 'GLOBAL_TIME', selectedInterval: '5min', headline: undefined, - threshold: undefined, userQueryKey: '', + alertDef: undefined, }; export default ChartPreview; diff --git a/frontend/src/container/FormAlertRules/ChartPreview/utils.test.ts b/frontend/src/container/FormAlertRules/ChartPreview/utils.test.ts new file mode 100644 index 0000000000..e81b57b933 --- /dev/null +++ b/frontend/src/container/FormAlertRules/ChartPreview/utils.test.ts @@ -0,0 +1,105 @@ +import { DataFormats } from 'container/NewWidget/RightContainer/types'; + +import { covertIntoDataFormats } from './utils'; + +describe('Convert One Unit to another unit', () => { + it('should convert from BitsIEC to BytesIEC', () => { + const result = covertIntoDataFormats({ + value: 8, + sourceUnit: DataFormats.BitsIEC, + targetUnit: DataFormats.BytesIEC, + }); + expect(result).toBe(1); + }); + + // for KibiBytes to MebiBytes conversion + it('should convert from KibiBytes to MebiBytes', () => { + const result = covertIntoDataFormats({ + value: 1024, + sourceUnit: DataFormats.KibiBytes, + targetUnit: DataFormats.MebiBytes, + }); + expect(result).toBe(1); + }); + + // for MegaBytes to GigaBytes conversion (SI units) + it('should convert from MegaBytes to GigaBytes (SI)', () => { + const result = covertIntoDataFormats({ + value: 1000, + sourceUnit: DataFormats.MegaBytes, + targetUnit: DataFormats.GigaBytes, + }); + expect(result).toBe(1); + }); + + // for identity conversion + it('should handle identity conversion', () => { + const result = covertIntoDataFormats({ + value: 100, + sourceUnit: DataFormats.KibiBytes, + targetUnit: DataFormats.KibiBytes, + }); + expect(result).toBe(100); + }); + + // BytesIEC to BitsIEC conversion + it('should convert from BytesIEC to BitsIEC', () => { + const result = covertIntoDataFormats({ + value: 1, + sourceUnit: DataFormats.BytesIEC, + targetUnit: DataFormats.BitsIEC, + }); + expect(result).toBe(8); + }); + + // for GibiBytes to TebiBytes conversion + it('should convert from GibiBytes to TebiBytes', () => { + const result = covertIntoDataFormats({ + value: 1024, + sourceUnit: DataFormats.GibiBytes, + targetUnit: DataFormats.TebiBytes, + }); + expect(result).toBe(1); + }); + + // for GigaBytes to TeraBytes conversion (SI units) + it('should convert from GigaBytes to TeraBytes (SI)', () => { + const result = covertIntoDataFormats({ + value: 1000, + sourceUnit: DataFormats.GigaBytes, + targetUnit: DataFormats.TeraBytes, + }); + expect(result).toBe(1); + }); + + // for GigaBytes to GibiBytes conversion (cross conversion) + it('should convert from GigaBytes to GibiBytes', () => { + const result = covertIntoDataFormats({ + value: 1, + sourceUnit: DataFormats.GigaBytes, + targetUnit: DataFormats.GibiBytes, + }); + // 1 GB = 0.93132257461548 GiB approximately + expect(result).toBeCloseTo(0.93132257461548); + }); + + // for a large number conversion + it('should handle large number conversion from PebiBytes to BitsIEC', () => { + const result = covertIntoDataFormats({ + value: 1, + sourceUnit: DataFormats.PebiBytes, + targetUnit: DataFormats.BitsIEC, + }); + expect(result).toBe(1 * 1024 ** 5 * 8); // 1 PebiByte = 2^50 Bytes = 2^53 Bits + }); + + // Negative value conversion + it('should handle negative values', () => { + const result = covertIntoDataFormats({ + value: -1, + sourceUnit: DataFormats.KibiBytes, + targetUnit: DataFormats.BytesIEC, + }); + expect(result).toBe(-1024); + }); +}); diff --git a/frontend/src/container/FormAlertRules/ChartPreview/utils.ts b/frontend/src/container/FormAlertRules/ChartPreview/utils.ts new file mode 100644 index 0000000000..0dbcc21603 --- /dev/null +++ b/frontend/src/container/FormAlertRules/ChartPreview/utils.ts @@ -0,0 +1,54 @@ +import { + BooleanFormats, + DataFormats, + DataRateFormats, + MiscellaneousFormats, + ThroughputFormats, + TimeFormats, +} from 'container/NewWidget/RightContainer/types'; + +import { + dataFormatConfig, + dataRateUnitsConfig, + miscUnitsConfig, + throughputConfig, + timeUnitsConfig, +} from './config'; + +export function covertIntoDataFormats({ + value, + sourceUnit, + targetUnit, +}: IUnit): number { + if (Object.values(BooleanFormats).includes(sourceUnit as BooleanFormats)) { + return 1; + } + + const sourceMultiplier = + dataFormatConfig[sourceUnit as DataFormats] || + timeUnitsConfig[sourceUnit as TimeFormats] || + dataRateUnitsConfig[sourceUnit as DataRateFormats] || + miscUnitsConfig[sourceUnit as MiscellaneousFormats] || + throughputConfig[sourceUnit as ThroughputFormats]; + + const targetDivider = + dataFormatConfig[targetUnit as DataFormats] || + timeUnitsConfig[targetUnit as TimeFormats] || + dataRateUnitsConfig[targetUnit as DataRateFormats] || + miscUnitsConfig[targetUnit as MiscellaneousFormats] || + throughputConfig[sourceUnit as ThroughputFormats]; + + const intermediateValue = value * sourceMultiplier; + + const roundedValue = Math.round(intermediateValue * 1000000) / 1000000; + + const result = roundedValue / targetDivider; + + return Number.isNaN(result) ? 0 : result; +} + +interface IUnit { + value: number; + sourceUnit?: string; + targetUnit?: string; +} diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index 22238a941b..8ece1562fe 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -1,4 +1,17 @@ -import { Form, InputNumber, InputNumberProps, Select, Typography } from 'antd'; +import { + Form, + InputNumber, + InputNumberProps, + Select, + SelectProps, + Space, + Typography, +} from 'antd'; +import { + getCategoryByOptionId, + getCategorySelectOptionByName, +} from 'container/NewWidget/RightContainer/alertFomatCategories'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useTranslation } from 'react-i18next'; import { AlertDef, @@ -10,9 +23,6 @@ import { EQueryType } from 'types/common/dashboard'; import { FormContainer, InlineSelect, StepHeading } from './styles'; -const { Option } = Select; -const FormItem = Form.Item; - function RuleOptions({ alertDef, setAlertDef, @@ -20,6 +30,7 @@ function RuleOptions({ }: RuleOptionsProps): JSX.Element { // init namespace for translations const { t } = useTranslation('alerts'); + const { currentQuery } = useQueryBuilder(); const handleMatchOptChange = (value: string | unknown): void => { const m = (value as string) || alertDef.condition?.matchType; @@ -49,10 +60,10 @@ function RuleOptions({ }); }} > - - - - + {t('option_above')} + {t('option_below')} + {t('option_equal')} + {t('option_notequal')} ); @@ -63,10 +74,10 @@ function RuleOptions({ value={alertDef.condition?.matchType} onChange={(value: string | unknown): void => handleMatchOptChange(value)} > - - - - + {t('option_atleastonce')} + {t('option_allthetimes')} + {t('option_onaverage')} + {t('option_intotal')} ); @@ -77,7 +88,7 @@ function RuleOptions({ value={alertDef.condition?.matchType} onChange={(value: string | unknown): void => handleMatchOptChange(value)} > - + {t('option_atleastonce')} ); @@ -94,31 +105,30 @@ function RuleOptions({ }); }} > - {' '} - - - - - - + {t('option_5min')} + {t('option_10min')} + {t('option_15min')} + {t('option_60min')} + {t('option_4hours')} + {t('option_24hours')} ); const renderThresholdRuleOpts = (): JSX.Element => ( - + {t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '} {renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()} - + ); const renderPromRuleOptions = (): JSX.Element => ( - + {t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '} {renderPromMatchOpts()} - + ); const onChange: InputNumberProps['onChange'] = (value): void => { @@ -133,6 +143,22 @@ function RuleOptions({ }); }; + const onChangeAlertUnit: SelectProps['onChange'] = (value) => { + setAlertDef({ + ...alertDef, + condition: { + ...alertDef.condition, + targetUnit: value as string, + }, + }); + }; + + const selectedCategory = getCategoryByOptionId(currentQuery?.unit || ''); + + const categorySelectOptions = getCategorySelectOptionByName( + selectedCategory?.name, + ); + return ( <> {t('alert_form_step2')} @@ -140,14 +166,29 @@ function RuleOptions({ {queryCategory === EQueryType.PROM ? renderPromRuleOptions() : renderThresholdRuleOpts()} - - - + + + + e.currentTarget.blur()} + /> + + + + + + ); +} + +export { BuilderUnitsFilter }; diff --git a/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/config.ts b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/config.ts new file mode 100644 index 0000000000..244591c2c2 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/config.ts @@ -0,0 +1,10 @@ +import { CategoryNames } from 'container/NewWidget/RightContainer/types'; + +export const categoryToSupport = [ + CategoryNames.Data, + CategoryNames.DataRate, + CategoryNames.Time, + CategoryNames.Throughput, + CategoryNames.Miscellaneous, + CategoryNames.Boolean, +]; diff --git a/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/index.ts b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/index.ts new file mode 100644 index 0000000000..977a301ca7 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/index.ts @@ -0,0 +1 @@ +export { BuilderUnitsFilter } from './BuilderUnits'; diff --git a/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/styles.ts b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/styles.ts new file mode 100644 index 0000000000..ddfba28930 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/styles.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export const selectStyles: React.CSSProperties = { + minWidth: '10rem', +}; + +export const DefaultLabel = styled.label` + display: inline-block; + font-size: 1rem; + line-height: 2rem; +`; diff --git a/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/types.ts b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/types.ts new file mode 100644 index 0000000000..693dab7be6 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/types.ts @@ -0,0 +1,3 @@ +export interface IBuilderUnitsFilterProps { + onChange?: (value: string) => void; +} diff --git a/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/utils.ts b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/utils.ts new file mode 100644 index 0000000000..a3a2947608 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/BuilderUnitsFilter/utils.ts @@ -0,0 +1,6 @@ +import { DefaultOptionType } from 'antd/es/select'; + +export const filterOption = ( + inputValue: string, + option: DefaultOptionType['options'][number], +): boolean => option.label.toLowerCase().includes(inputValue.toLowerCase()); diff --git a/frontend/src/container/QueryBuilder/filters/index.ts b/frontend/src/container/QueryBuilder/filters/index.ts index d15a242fde..5a7292e5b1 100644 --- a/frontend/src/container/QueryBuilder/filters/index.ts +++ b/frontend/src/container/QueryBuilder/filters/index.ts @@ -1,4 +1,5 @@ export { AggregatorFilter } from './AggregatorFilter'; +export { BuilderUnitsFilter } from './BuilderUnitsFilter'; export { GroupByFilter } from './GroupByFilter'; export { HavingFilter } from './HavingFilter'; export { OperatorsSelect } from './OperatorsSelect'; diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts index f30bfc13b7..733513b33b 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts @@ -33,5 +33,6 @@ export const mapQueryDataFromApi = ( clickhouse_sql: clickhouseSql, queryType: compositeQuery.queryType, id: uuid(), + unit: compositeQuery.unit, }; }; diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 4de8de7d4a..7c1f1d9792 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -71,6 +71,7 @@ export const QueryBuilderContext = createContext({ updateAllQueriesOperators: () => initialQueriesMap.metrics, updateQueriesData: () => initialQueriesMap.metrics, initQueryBuilderData: () => {}, + handleOnUnitsChange: () => {}, }); export function QueryBuilderProvider({ @@ -176,6 +177,7 @@ export function QueryBuilderProvider({ queryData: setupedQueryData, }, id: query.id, + unit: query.unit, }; const nextQuery: Query = { @@ -474,6 +476,7 @@ export function QueryBuilderProvider({ promql, clickhouse_sql: clickhouseSql, id: uuid(), + unit: query.unit || initialQueryState.unit, }; urlQuery.set( @@ -513,6 +516,7 @@ export function QueryBuilderProvider({ promql: currentQuery.promql, id: currentQuery.id, queryType, + unit: currentQuery.unit, }, maxTime, minTime, @@ -550,6 +554,16 @@ export function QueryBuilderProvider({ stagedQuery, ]); + const handleOnUnitsChange = useCallback( + (unit: string) => { + setCurrentQuery((prevState) => ({ + ...prevState, + unit, + })); + }, + [setCurrentQuery], + ); + const query: Query = useMemo( () => ({ ...currentQuery, @@ -585,6 +599,7 @@ export function QueryBuilderProvider({ updateAllQueriesOperators, updateQueriesData, initQueryBuilderData, + handleOnUnitsChange, }), [ query, @@ -607,6 +622,7 @@ export function QueryBuilderProvider({ updateAllQueriesOperators, updateQueriesData, initQueryBuilderData, + handleOnUnitsChange, ], ); diff --git a/frontend/src/store/actions/dashboard/getQueryResults.ts b/frontend/src/store/actions/dashboard/getQueryResults.ts index 1487b3f7c8..a132fc8e78 100644 --- a/frontend/src/store/actions/dashboard/getQueryResults.ts +++ b/frontend/src/store/actions/dashboard/getQueryResults.ts @@ -34,6 +34,7 @@ export async function GetMetricQueryRange({ compositeQuery: { queryType: query.queryType, panelType: graphType, + unit: query?.unit, }, }; diff --git a/frontend/src/types/api/alerts/compositeQuery.ts b/frontend/src/types/api/alerts/compositeQuery.ts index 1f25718367..f2856fbb3d 100644 --- a/frontend/src/types/api/alerts/compositeQuery.ts +++ b/frontend/src/types/api/alerts/compositeQuery.ts @@ -3,6 +3,7 @@ import { BuilderClickHouseResource, BuilderPromQLResource, BuilderQueryDataResourse, + Query, } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; @@ -12,4 +13,5 @@ export interface ICompositeMetricQuery { chQueries: BuilderClickHouseResource; queryType: EQueryType; panelType: PANEL_TYPES; + unit: Query['unit']; } diff --git a/frontend/src/types/api/alerts/def.ts b/frontend/src/types/api/alerts/def.ts index 8d5440e7d9..704a05765e 100644 --- a/frontend/src/types/api/alerts/def.ts +++ b/frontend/src/types/api/alerts/def.ts @@ -28,6 +28,7 @@ export interface RuleCondition { op?: string | undefined; target?: number | undefined; matchType?: string | undefined; + targetUnit?: string | undefined; } export interface Labels { diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index b16aac3b3c..926785e072 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -1,3 +1,4 @@ +import { Format } from 'container/NewWidget/RightContainer/types'; import { EQueryType } from 'types/common/dashboard'; import { DataSource, @@ -82,6 +83,7 @@ export interface Query { builder: QueryBuilderData; clickhouse_sql: IClickHouseQuery[]; id: string; + unit?: Format['id']; } export type QueryState = Omit; diff --git a/frontend/src/types/common/queryBuilder.ts b/frontend/src/types/common/queryBuilder.ts index 5e7cf57b02..3d74f30fdc 100644 --- a/frontend/src/types/common/queryBuilder.ts +++ b/frontend/src/types/common/queryBuilder.ts @@ -1,4 +1,5 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; +import { Format } from 'container/NewWidget/RightContainer/types'; import { IBuilderFormula, IBuilderQuery, @@ -187,6 +188,7 @@ export type QueryBuilderContextType = { ) => void; handleRunQuery: () => void; resetStagedQuery: () => void; + handleOnUnitsChange: (units: Format['id']) => void; updateAllQueriesOperators: ( queryData: Query, panelType: PANEL_TYPES, From 2bf534b56f6ca85c75ec6f85dd9e37a4210a89f0 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Tue, 15 Aug 2023 21:19:05 +0530 Subject: [PATCH 08/17] feat: add ms teams channels (#2689) * feat: api machinery to support enterprise plan channels * feat: backend for handling ms teams * feat: frontend for ms teams * fix: fixed some minor issues wiht ms teams * fix: resolved issue with feature gate * chore: add missing span metrics * chore: some minor changes are updated * feat: added the oss flag is updated --------- Co-authored-by: Vishal Sharma Co-authored-by: Srikanth Chekuri Co-authored-by: Palash Gupta --- ee/query-service/model/plans.go | 84 +++++++++++++++ frontend/src/api/channels/createMsTeams.ts | 34 ++++++ frontend/src/api/channels/editMsTeams.ts | 34 ++++++ frontend/src/api/channels/testMsTeams.ts | 34 ++++++ .../src/components/Upgrade/UpgradePrompt.tsx | 31 ++++++ frontend/src/constants/features.ts | 7 ++ .../container/CreateAlertChannels/config.ts | 14 ++- .../container/CreateAlertChannels/index.tsx | 100 +++++++++++++----- .../src/container/EditAlertChannels/index.tsx | 74 +++++++++++-- .../FormAlertChannels/Settings/MsTeams.tsx | 57 ++++++++++ .../src/container/FormAlertChannels/index.tsx | 57 +++++++--- .../src/hooks/useFeatureFlag/utils.test.ts | 13 +++ frontend/src/hooks/useFeatureFlag/utils.ts | 4 + frontend/src/pages/ChannelsEdit/index.tsx | 17 ++- .../src/types/api/channels/createMsTeams.ts | 8 ++ .../src/types/api/channels/editMsTeams.ts | 10 ++ .../app/clickhouseReader/reader.go | 26 +++-- pkg/query-service/app/http_handler.go | 1 - .../integrations/alertManager/model.go | 1 + pkg/query-service/model/featureSet.go | 85 +++++++++++++++ 20 files changed, 631 insertions(+), 60 deletions(-) create mode 100644 frontend/src/api/channels/createMsTeams.ts create mode 100644 frontend/src/api/channels/editMsTeams.ts create mode 100644 frontend/src/api/channels/testMsTeams.ts create mode 100644 frontend/src/components/Upgrade/UpgradePrompt.tsx create mode 100644 frontend/src/container/FormAlertChannels/Settings/MsTeams.tsx create mode 100644 frontend/src/hooks/useFeatureFlag/utils.test.ts create mode 100644 frontend/src/hooks/useFeatureFlag/utils.ts create mode 100644 frontend/src/types/api/channels/createMsTeams.ts create mode 100644 frontend/src/types/api/channels/editMsTeams.ts diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go index 2be68f1ea7..9f56f50655 100644 --- a/ee/query-service/model/plans.go +++ b/ee/query-service/model/plans.go @@ -60,6 +60,34 @@ var BasicPlan = basemodel.FeatureSet{ UsageLimit: 5, Route: "", }, + basemodel.Feature{ + Name: basemodel.AlertChannelSlack, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelWebhook, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelPagerduty, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelMsTeams, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, basemodel.Feature{ Name: basemodel.UseSpanMetrics, Active: false, @@ -112,6 +140,34 @@ var ProPlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: basemodel.AlertChannelSlack, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelWebhook, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelPagerduty, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelMsTeams, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, basemodel.Feature{ Name: basemodel.UseSpanMetrics, Active: false, @@ -164,6 +220,34 @@ var EnterprisePlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: basemodel.AlertChannelSlack, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelWebhook, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelPagerduty, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.AlertChannelMsTeams, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, basemodel.Feature{ Name: basemodel.UseSpanMetrics, Active: false, diff --git a/frontend/src/api/channels/createMsTeams.ts b/frontend/src/api/channels/createMsTeams.ts new file mode 100644 index 0000000000..9e06e275a0 --- /dev/null +++ b/frontend/src/api/channels/createMsTeams.ts @@ -0,0 +1,34 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createMsTeams'; + +const create = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/channels', { + name: props.name, + msteams_configs: [ + { + send_resolved: true, + webhook_url: props.webhook_url, + title: props.title, + text: props.text, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default create; diff --git a/frontend/src/api/channels/editMsTeams.ts b/frontend/src/api/channels/editMsTeams.ts new file mode 100644 index 0000000000..ee6bd309c1 --- /dev/null +++ b/frontend/src/api/channels/editMsTeams.ts @@ -0,0 +1,34 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/editMsTeams'; + +const editMsTeams = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.put(`/channels/${props.id}`, { + name: props.name, + msteams_configs: [ + { + send_resolved: true, + webhook_url: props.webhook_url, + title: props.title, + text: props.text, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default editMsTeams; diff --git a/frontend/src/api/channels/testMsTeams.ts b/frontend/src/api/channels/testMsTeams.ts new file mode 100644 index 0000000000..3b4fc21b23 --- /dev/null +++ b/frontend/src/api/channels/testMsTeams.ts @@ -0,0 +1,34 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createMsTeams'; + +const testMsTeams = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/testChannel', { + name: props.name, + msteams_configs: [ + { + send_resolved: true, + webhook_url: props.webhook_url, + title: props.title, + text: props.text, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default testMsTeams; diff --git a/frontend/src/components/Upgrade/UpgradePrompt.tsx b/frontend/src/components/Upgrade/UpgradePrompt.tsx new file mode 100644 index 0000000000..9281530056 --- /dev/null +++ b/frontend/src/components/Upgrade/UpgradePrompt.tsx @@ -0,0 +1,31 @@ +import { Alert, Space } from 'antd'; +import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app'; + +type UpgradePromptProps = { + title?: string; +}; + +function UpgradePrompt({ title }: UpgradePromptProps): JSX.Element { + return ( + + + This feature is available for paid plans only.{' '} + + Click here + {' '} + to Upgrade + + } + type="warning" + />{' '} + + ); +} + +UpgradePrompt.defaultProps = { + title: 'Upgrade to a Paid Plan', +}; +export default UpgradePrompt; diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index c5f4a1e7b8..0d444ff3f0 100644 --- a/frontend/src/constants/features.ts +++ b/frontend/src/constants/features.ts @@ -1,6 +1,12 @@ // keep this consistent with backend constants.go export enum FeatureKeys { SSO = 'SSO', + ENTERPRISE_PLAN = 'ENTERPRISE_PLAN', + BASIC_PLAN = 'BASIC_PLAN', + ALERT_CHANNEL_SLACK = 'ALERT_CHANNEL_SLACK', + ALERT_CHANNEL_WEBHOOK = 'ALERT_CHANNEL_WEBHOOK', + ALERT_CHANNEL_PAGERDUTY = 'ALERT_CHANNEL_PAGERDUTY', + ALERT_CHANNEL_MSTEAMS = 'ALERT_CHANNEL_MSTEAMS', DurationSort = 'DurationSort', TimestampSort = 'TimestampSort', SMART_TRACE_DETAIL = 'SMART_TRACE_DETAIL', @@ -9,4 +15,5 @@ export enum FeatureKeys { QUERY_BUILDER_ALERTS = 'QUERY_BUILDER_ALERTS', DISABLE_UPSELL = 'DISABLE_UPSELL', USE_SPAN_METRICS = 'USE_SPAN_METRICS', + OSS = 'OSS', } diff --git a/frontend/src/container/CreateAlertChannels/config.ts b/frontend/src/container/CreateAlertChannels/config.ts index 6c89764637..634dc95c41 100644 --- a/frontend/src/container/CreateAlertChannels/config.ts +++ b/frontend/src/container/CreateAlertChannels/config.ts @@ -63,10 +63,16 @@ export const ValidatePagerChannel = (p: PagerChannel): string => { return ''; }; -export type ChannelType = 'slack' | 'email' | 'webhook' | 'pagerduty'; +export type ChannelType = + | 'slack' + | 'email' + | 'webhook' + | 'pagerduty' + | 'msteams'; export const SlackType: ChannelType = 'slack'; export const WebhookType: ChannelType = 'webhook'; export const PagerType: ChannelType = 'pagerduty'; +export const MsTeamsType: ChannelType = 'msteams'; // LabelFilterStatement will be used for preparing filter conditions / matchers export interface LabelFilterStatement { @@ -81,3 +87,9 @@ export interface LabelFilterStatement { // filter value value: string; } + +export interface MsTeamsChannel extends Channel { + webhook_url?: string; + title?: string; + text?: string; +} diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx index 1261f53567..70fe3fb7c6 100644 --- a/frontend/src/container/CreateAlertChannels/index.tsx +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -1,7 +1,9 @@ import { Form } from 'antd'; +import createMsTeamsApi from 'api/channels/createMsTeams'; import createPagerApi from 'api/channels/createPager'; import createSlackApi from 'api/channels/createSlack'; import createWebhookApi from 'api/channels/createWebhook'; +import testMsTeamsApi from 'api/channels/testMsTeams'; import testPagerApi from 'api/channels/testPager'; import testSlackApi from 'api/channels/testSlack'; import testWebhookApi from 'api/channels/testWebhook'; @@ -14,6 +16,8 @@ import { useTranslation } from 'react-i18next'; import { ChannelType, + MsTeamsChannel, + MsTeamsType, PagerChannel, PagerType, SlackChannel, @@ -33,7 +37,7 @@ function CreateAlertChannels({ const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< - Partial + Partial >({ text: `{{ range .Alerts -}} *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} @@ -102,9 +106,7 @@ function CreateAlertChannels({ message: 'Success', description: t('channel_creation_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -165,9 +167,7 @@ function CreateAlertChannels({ message: 'Success', description: t('channel_creation_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -222,9 +222,7 @@ function CreateAlertChannels({ message: 'Success', description: t('channel_creation_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -241,26 +239,71 @@ function CreateAlertChannels({ setSavingState(false); }, [t, notifications, preparePagerRequest]); + const prepareMsTeamsRequest = useCallback( + () => ({ + webhook_url: selectedConfig?.webhook_url || '', + name: selectedConfig?.name || '', + send_resolved: true, + text: selectedConfig?.text || '', + title: selectedConfig?.title || '', + }), + [selectedConfig], + ); + + const onMsTeamsHandler = useCallback(async () => { + setSavingState(true); + + try { + const response = await createMsTeamsApi(prepareMsTeamsRequest()); + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_creation_done'), + }); + history.replace(ROUTES.ALL_CHANNELS); + } else { + notifications.error({ + message: 'Error', + description: response.error || t('channel_creation_failed'), + }); + } + } catch (error) { + notifications.error({ + message: 'Error', + description: t('channel_creation_failed'), + }); + } + setSavingState(false); + }, [prepareMsTeamsRequest, t, notifications]); + const onSaveHandler = useCallback( async (value: ChannelType) => { - switch (value) { - case SlackType: - onSlackHandler(); - break; - case WebhookType: - onWebhookHandler(); - break; - case PagerType: - onPagerHandler(); - break; - default: - notifications.error({ - message: 'Error', - description: t('selected_channel_invalid'), - }); + const functionMapper = { + [SlackType]: onSlackHandler, + [WebhookType]: onWebhookHandler, + [PagerType]: onPagerHandler, + [MsTeamsType]: onMsTeamsHandler, + }; + const functionToCall = functionMapper[value]; + + if (functionToCall) { + functionToCall(); + } else { + notifications.error({ + message: 'Error', + description: t('selected_channel_invalid'), + }); } }, - [onSlackHandler, t, onPagerHandler, onWebhookHandler, notifications], + [ + onSlackHandler, + onWebhookHandler, + onPagerHandler, + onMsTeamsHandler, + notifications, + t, + ], ); const performChannelTest = useCallback( @@ -282,6 +325,10 @@ function CreateAlertChannels({ request = preparePagerRequest(); if (request) response = await testPagerApi(request); break; + case MsTeamsType: + request = prepareMsTeamsRequest(); + response = await testMsTeamsApi(request); + break; default: notifications.error({ message: 'Error', @@ -315,6 +362,7 @@ function CreateAlertChannels({ t, preparePagerRequest, prepareSlackRequest, + prepareMsTeamsRequest, notifications, ], ); diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx index 42ebd543c6..d6cba08381 100644 --- a/frontend/src/container/EditAlertChannels/index.tsx +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -1,13 +1,17 @@ import { Form } from 'antd'; +import editMsTeamsApi from 'api/channels/editMsTeams'; import editPagerApi from 'api/channels/editPager'; import editSlackApi from 'api/channels/editSlack'; import editWebhookApi from 'api/channels/editWebhook'; +import testMsTeamsApi from 'api/channels/testMsTeams'; import testPagerApi from 'api/channels/testPager'; import testSlackApi from 'api/channels/testSlack'; import testWebhookApi from 'api/channels/testWebhook'; import ROUTES from 'constants/routes'; import { ChannelType, + MsTeamsChannel, + MsTeamsType, PagerChannel, PagerType, SlackChannel, @@ -31,7 +35,7 @@ function EditAlertChannels({ const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< - Partial + Partial >({ ...initialValue, }); @@ -81,9 +85,7 @@ function EditAlertChannels({ description: t('channel_edit_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -136,9 +138,7 @@ function EditAlertChannels({ description: t('channel_edit_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { showError(response.error || t('channel_edit_failed')); } @@ -183,9 +183,7 @@ function EditAlertChannels({ description: t('channel_edit_done'), }); - setTimeout(() => { - history.replace(ROUTES.SETTINGS); - }, 2000); + history.replace(ROUTES.ALL_CHANNELS); } else { notifications.error({ message: 'Error', @@ -195,6 +193,48 @@ function EditAlertChannels({ setSavingState(false); }, [preparePagerRequest, notifications, selectedConfig, t]); + const prepareMsTeamsRequest = useCallback( + () => ({ + webhook_url: selectedConfig?.webhook_url || '', + name: selectedConfig?.name || '', + send_resolved: true, + text: selectedConfig?.text || '', + title: selectedConfig?.title || '', + id, + }), + [id, selectedConfig], + ); + + const onMsTeamsEditHandler = useCallback(async () => { + setSavingState(true); + + if (selectedConfig?.webhook_url === '') { + notifications.error({ + message: 'Error', + description: t('webhook_url_required'), + }); + setSavingState(false); + return; + } + + const response = await editMsTeamsApi(prepareMsTeamsRequest()); + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_edit_done'), + }); + + history.replace(ROUTES.ALL_CHANNELS); + } else { + notifications.error({ + message: 'Error', + description: response.error || t('channel_edit_failed'), + }); + } + setSavingState(false); + }, [prepareMsTeamsRequest, t, notifications, selectedConfig]); + const onSaveHandler = useCallback( (value: ChannelType) => { if (value === SlackType) { @@ -203,9 +243,16 @@ function EditAlertChannels({ onWebhookEditHandler(); } else if (value === PagerType) { onPagerEditHandler(); + } else if (value === MsTeamsType) { + onMsTeamsEditHandler(); } }, - [onSlackEditHandler, onWebhookEditHandler, onPagerEditHandler], + [ + onSlackEditHandler, + onWebhookEditHandler, + onPagerEditHandler, + onMsTeamsEditHandler, + ], ); const performChannelTest = useCallback( @@ -227,6 +274,10 @@ function EditAlertChannels({ request = preparePagerRequest(); if (request) response = await testPagerApi(request); break; + case MsTeamsType: + request = prepareMsTeamsRequest(); + if (request) response = await testMsTeamsApi(request); + break; default: notifications.error({ message: 'Error', @@ -260,6 +311,7 @@ function EditAlertChannels({ prepareWebhookRequest, preparePagerRequest, prepareSlackRequest, + prepareMsTeamsRequest, notifications, ], ); diff --git a/frontend/src/container/FormAlertChannels/Settings/MsTeams.tsx b/frontend/src/container/FormAlertChannels/Settings/MsTeams.tsx new file mode 100644 index 0000000000..48751f4acc --- /dev/null +++ b/frontend/src/container/FormAlertChannels/Settings/MsTeams.tsx @@ -0,0 +1,57 @@ +import { Form, Input } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { MsTeamsChannel } from '../../CreateAlertChannels/config'; + +function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element { + const { t } = useTranslation('channels'); + + return ( + <> + + { + setSelectedConfig((value) => ({ + ...value, + webhook_url: event.target.value, + })); + }} + /> + + + + + setSelectedConfig((value) => ({ + ...value, + title: event.target.value, + })) + } + /> + + + + + setSelectedConfig((value) => ({ + ...value, + text: event.target.value, + })) + } + placeholder={t('placeholder_slack_description')} + /> + + + ); +} + +interface MsTeamsProps { + setSelectedConfig: React.Dispatch< + React.SetStateAction> + >; +} + +export default MsTeams; diff --git a/frontend/src/container/FormAlertChannels/index.tsx b/frontend/src/container/FormAlertChannels/index.tsx index 14f6d32413..1aa4de4748 100644 --- a/frontend/src/container/FormAlertChannels/index.tsx +++ b/frontend/src/container/FormAlertChannels/index.tsx @@ -1,8 +1,11 @@ import { Form, FormInstance, Input, Select, Typography } from 'antd'; import { Store } from 'antd/lib/form/interface'; +import UpgradePrompt from 'components/Upgrade/UpgradePrompt'; +import { FeatureKeys } from 'constants/features'; import ROUTES from 'constants/routes'; import { ChannelType, + MsTeamsType, PagerChannel, PagerType, SlackChannel, @@ -10,18 +13,18 @@ import { WebhookChannel, WebhookType, } from 'container/CreateAlertChannels/config'; +import useFeatureFlags from 'hooks/useFeatureFlag'; +import { isFeatureKeys } from 'hooks/useFeatureFlag/utils'; import history from 'lib/history'; import { Dispatch, ReactElement, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; +import MsTeamsSettings from './Settings/MsTeams'; import PagerSettings from './Settings/Pager'; import SlackSettings from './Settings/Slack'; import WebhookSettings from './Settings/Webhook'; import { Button } from './styles'; -const { Option } = Select; -const { Title } = Typography; - function FormAlertChannels({ formInstance, type, @@ -36,8 +39,27 @@ function FormAlertChannels({ editing = false, }: FormAlertChannelsProps): JSX.Element { const { t } = useTranslation('channels'); + const isUserOnEEPlan = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN); + + const feature = `ALERT_CHANNEL_${type.toUpperCase()}`; + + const hasFeature = useFeatureFlags( + isFeatureKeys(feature) ? feature : FeatureKeys.ALERT_CHANNEL_SLACK, + ); + + const isOssFeature = useFeatureFlags(FeatureKeys.OSS); const renderSettings = (): ReactElement | null => { + if ( + // for ee plan + !isOssFeature?.active && + (!hasFeature || !hasFeature.active) && + type === 'msteams' + ) { + // channel type is not available for users plan + return ; + } + switch (type) { case SlackType: return ; @@ -45,14 +67,16 @@ function FormAlertChannels({ return ; case PagerType: return ; - + case MsTeamsType: + return ; default: return null; } }; + return ( <> - {title} + {title}
@@ -69,15 +93,22 @@ function FormAlertChannels({ @@ -85,7 +116,7 @@ function FormAlertChannels({ + + + + + + - + + diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx new file mode 100644 index 0000000000..b20613bae7 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx @@ -0,0 +1,102 @@ +import { Space, Typography } from 'antd'; +import TextToolTip from 'components/TextToolTip'; +import { + apDexToolTipText, + apDexToolTipUrl, + apDexToolTipUrlText, +} from 'constants/apDex'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import Graph from 'container/GridGraphLayout/Graph'; +import DisplayThreshold from 'container/GridGraphLayout/WidgetHeader/DisplayThreshold'; +import { GraphTitle } from 'container/MetricsApplication/constant'; +import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; +import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; +import { ReactNode, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { EQueryType } from 'types/common/dashboard'; +import { v4 as uuid } from 'uuid'; + +import { IServiceName } from '../../types'; +import { ApDexMetricsProps } from './types'; + +function ApDexMetrics({ + delta, + metricsBuckets, + thresholdValue, + onDragSelect, + tagFilterItems, + topLevelOperationsRoute, + handleGraphClick, +}: ApDexMetricsProps): JSX.Element { + const { servicename } = useParams(); + + const apDexMetricsWidget = useMemo( + () => + getWidgetQueryBuilder({ + query: { + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: apDexMetricsQueryBuilderQueries({ + servicename, + tagFilterItems, + topLevelOperationsRoute, + threashold: thresholdValue || 0, + delta: delta || false, + metricsBuckets: metricsBuckets || [], + }), + clickhouse_sql: [], + id: uuid(), + }, + title: ( + + {GraphTitle.APDEX} + + + ), + panelTypes: PANEL_TYPES.TIME_SERIES, + }), + [ + delta, + metricsBuckets, + servicename, + tagFilterItems, + thresholdValue, + topLevelOperationsRoute, + ], + ); + + const threshold: ReactNode = useMemo(() => { + if (thresholdValue) return ; + return null; + }, [thresholdValue]); + + const isQueryEnabled = + topLevelOperationsRoute.length > 0 && + metricsBuckets && + metricsBuckets?.length > 0 && + delta !== undefined; + + return ( + + ); +} + +ApDexMetrics.defaultProps = { + delta: undefined, + le: undefined, +}; + +export default ApDexMetrics; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx new file mode 100644 index 0000000000..0e1486b852 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx @@ -0,0 +1,36 @@ +import Spinner from 'components/Spinner'; +import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta'; +import useErrorNotification from 'hooks/useErrorNotification'; + +import ApDexMetrics from './ApDexMetrics'; +import { metricMeta } from './constants'; +import { ApDexDataSwitcherProps } from './types'; + +function ApDexMetricsApplication({ + handleGraphClick, + onDragSelect, + tagFilterItems, + topLevelOperationsRoute, + thresholdValue, +}: ApDexDataSwitcherProps): JSX.Element { + const { data, isLoading, error } = useGetMetricMeta(metricMeta); + useErrorNotification(error); + + if (isLoading) { + return ; + } + + return ( + + ); +} + +export default ApDexMetricsApplication; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx new file mode 100644 index 0000000000..bf6297785f --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx @@ -0,0 +1,62 @@ +// This component is not been used in the application as we support only metrics for ApDex as of now. +// This component is been kept for future reference. +import { PANEL_TYPES } from 'constants/queryBuilder'; +import Graph from 'container/GridGraphLayout/Graph'; +import { GraphTitle } from 'container/MetricsApplication/constant'; +import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; +import { apDexTracesQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { EQueryType } from 'types/common/dashboard'; +import { v4 as uuid } from 'uuid'; + +import { IServiceName } from '../../types'; +import { ApDexDataSwitcherProps } from './types'; + +function ApDexTraces({ + handleGraphClick, + onDragSelect, + topLevelOperationsRoute, + tagFilterItems, + thresholdValue, +}: ApDexDataSwitcherProps): JSX.Element { + const { servicename } = useParams(); + + const apDexTracesWidget = useMemo( + () => + getWidgetQueryBuilder({ + query: { + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: apDexTracesQueryBuilderQueries({ + servicename, + tagFilterItems, + topLevelOperationsRoute, + threashold: thresholdValue || 0, + }), + clickhouse_sql: [], + id: uuid(), + }, + title: GraphTitle.APDEX, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), + [servicename, tagFilterItems, thresholdValue, topLevelOperationsRoute], + ); + + const isQueryEnabled = + topLevelOperationsRoute.length > 0 && thresholdValue !== undefined; + + return ( + + ); +} + +export default ApDexTraces; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/constants.ts b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/constants.ts new file mode 100644 index 0000000000..91467b372f --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/constants.ts @@ -0,0 +1 @@ +export const metricMeta = 'signoz_latency_bucket'; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/index.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/index.tsx new file mode 100644 index 0000000000..f69e871bb6 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/index.tsx @@ -0,0 +1,47 @@ +import Spinner from 'components/Spinner'; +import { Card, GraphContainer } from 'container/MetricsApplication/styles'; +import { useGetApDexSettings } from 'hooks/apDex/useGetApDexSettings'; +import useErrorNotification from 'hooks/useErrorNotification'; +import { memo } from 'react'; +import { useParams } from 'react-router-dom'; + +import { IServiceName } from '../../types'; +import ApDexMetricsApplication from './ApDexMetricsApplication'; +import { ApDexApplicationProps } from './types'; + +function ApDexApplication({ + handleGraphClick, + onDragSelect, + topLevelOperationsRoute, + tagFilterItems, +}: ApDexApplicationProps): JSX.Element { + const { servicename } = useParams(); + const { data, isLoading, error, isRefetching } = useGetApDexSettings( + servicename, + ); + useErrorNotification(error); + + if (isLoading || isRefetching) { + return ( + + + + ); + } + + return ( + + + + + + ); +} + +export default memo(ApDexApplication); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts new file mode 100644 index 0000000000..e3046261f0 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts @@ -0,0 +1,19 @@ +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; + +import { ClickHandlerType } from '../../Overview'; + +export interface ApDexApplicationProps { + handleGraphClick: (type: string) => ClickHandlerType; + onDragSelect: (start: number, end: number) => void; + topLevelOperationsRoute: string[]; + tagFilterItems: TagFilterItem[]; +} + +export interface ApDexDataSwitcherProps extends ApDexApplicationProps { + thresholdValue?: number; +} + +export interface ApDexMetricsProps extends ApDexDataSwitcherProps { + delta?: boolean; + metricsBuckets?: number[]; +} diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index c8ea330fb4..52014ac48b 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -1,3 +1,4 @@ +import Spinner from 'components/Spinner'; import { FeatureKeys } from 'constants/features'; import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridGraphLayout/Graph/'; @@ -24,6 +25,7 @@ function ServiceOverview({ selectedTraceTags, selectedTimeStamp, topLevelOperationsRoute, + topLevelOperationsLoading, }: ServiceOverviewProps): JSX.Element { const { servicename } = useParams(); @@ -63,6 +65,14 @@ function ServiceOverview({ const isQueryEnabled = topLevelOperationsRoute.length > 0; + if (topLevelOperationsLoading) { + return ( + + + + ); + } + return ( <> + + ); +} + +export default ApDexApplication; diff --git a/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.test.tsx b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.test.tsx new file mode 100644 index 0000000000..daa95d1d56 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.test.tsx @@ -0,0 +1,62 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; + +import { axiosResponseThresholdData } from './__mock__/axiosResponseMockThresholdData'; +import ApDexSettings from './ApDexSettings'; + +jest.mock('hooks/apDex/useSetApDexSettings', () => ({ + __esModule: true, + useSetApDexSettings: jest.fn().mockReturnValue({ + mutateAsync: jest.fn(), + isLoading: false, + error: null, + }), +})); + +describe('ApDexSettings', () => { + it('should render the component', () => { + render( + , + ); + + expect(screen.getByText('Application Settings')).toBeInTheDocument(); + }); + + it('should render the spinner when the data is loading', () => { + render( + , + ); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + it('should close the popover when the cancel button is clicked', async () => { + const mockHandlePopOverClose = jest.fn(); + render( + , + ); + + const button = screen.getByText('Cancel'); + fireEvent.click(button); + await waitFor(() => { + expect(mockHandlePopOverClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx new file mode 100644 index 0000000000..b0d5cdba02 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx @@ -0,0 +1,119 @@ +import { CloseOutlined } from '@ant-design/icons'; +import { Card, InputNumber } from 'antd'; +import Spinner from 'components/Spinner'; +import TextToolTip from 'components/TextToolTip'; +import { + apDexToolTipText, + apDexToolTipUrl, + apDexToolTipUrlText, +} from 'constants/apDex'; +import { themeColors } from 'constants/theme'; +import { useSetApDexSettings } from 'hooks/apDex/useSetApDexSettings'; +import { useNotifications } from 'hooks/useNotifications'; +import { useState } from 'react'; + +import { APPLICATION_SETTINGS } from '../constants'; +import { + AppDexThresholdContainer, + Button, + SaveAndCancelContainer, + SaveButton, + Typography, +} from '../styles'; +import { onSaveApDexSettings } from '../utils'; +import { ApDexSettingsProps } from './types'; + +function ApDexSettings({ + servicename, + handlePopOverClose, + isLoading, + data, + refetchGetApDexSetting, +}: ApDexSettingsProps): JSX.Element { + const [thresholdValue, setThresholdValue] = useState(() => { + if (data) { + return data.data[0].threshold; + } + return 0; + }); + const { notifications } = useNotifications(); + + const { isLoading: isApDexLoading, mutateAsync } = useSetApDexSettings({ + servicename, + threshold: thresholdValue, + excludeStatusCode: '', + }); + + const handleThreadholdChange = (value: number | null): void => { + if (value !== null) { + setThresholdValue(value); + } + }; + + if (isLoading) { + return ( + + + + ); + } + + return ( + } + actions={[ + + + + Save + + , + ]} + > + + + Apdex threshold (in seconds){' '} + + + + + {/* TODO: Add this feature later when backend is ready to support it. */} + {/* + + Exclude following error codes from error rate calculation + + + */} + + ); +} + +ApDexSettings.defaultProps = { + isLoading: undefined, + data: undefined, + refetchGetApDexSetting: undefined, +}; + +export default ApDexSettings; diff --git a/frontend/src/pages/MetricsApplication/ApDex/__mock__/axiosResponseMockThresholdData.ts b/frontend/src/pages/MetricsApplication/ApDex/__mock__/axiosResponseMockThresholdData.ts new file mode 100644 index 0000000000..c0028a91c2 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/__mock__/axiosResponseMockThresholdData.ts @@ -0,0 +1,9 @@ +import { AxiosResponse } from 'axios'; + +export const axiosResponseThresholdData = { + data: [ + { + threshold: 0.5, + }, + ], +} as AxiosResponse; diff --git a/frontend/src/pages/MetricsApplication/ApDex/__mock__/thresholdMockData.ts b/frontend/src/pages/MetricsApplication/ApDex/__mock__/thresholdMockData.ts new file mode 100644 index 0000000000..ac681612a1 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/__mock__/thresholdMockData.ts @@ -0,0 +1,7 @@ +export const thresholdMockData = { + data: [ + { + threshold: 0.5, + }, + ], +}; diff --git a/frontend/src/pages/MetricsApplication/ApDex/types.ts b/frontend/src/pages/MetricsApplication/ApDex/types.ts new file mode 100644 index 0000000000..37a931886c --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/types.ts @@ -0,0 +1,10 @@ +import { AxiosResponse } from 'axios'; +import { ApDexPayloadAndSettingsProps } from 'types/api/metrics/getApDex'; + +export interface ApDexSettingsProps { + servicename: string; + handlePopOverClose: () => void; + isLoading?: boolean; + data?: AxiosResponse; + refetchGetApDexSetting?: () => void; +} diff --git a/frontend/src/pages/MetricsApplication/constants.ts b/frontend/src/pages/MetricsApplication/constants.ts new file mode 100644 index 0000000000..de7f19143e --- /dev/null +++ b/frontend/src/pages/MetricsApplication/constants.ts @@ -0,0 +1 @@ +export const APPLICATION_SETTINGS = 'Application Settings'; diff --git a/frontend/src/pages/MetricsApplication/index.tsx b/frontend/src/pages/MetricsApplication/index.tsx index afd5fc4881..94cbd5d99e 100644 --- a/frontend/src/pages/MetricsApplication/index.tsx +++ b/frontend/src/pages/MetricsApplication/index.tsx @@ -8,6 +8,7 @@ import history from 'lib/history'; import { useMemo } from 'react'; import { generatePath, useParams } from 'react-router-dom'; +import ApDexApplication from './ApDex/ApDexApplication'; import { MetricsApplicationTab, TAB_KEY_VS_LABEL } from './types'; import useMetricsApplicationTabKey from './useMetricsApplicationTabKey'; @@ -49,6 +50,7 @@ function MetricsApplication(): JSX.Element { return ( <> + ); diff --git a/frontend/src/pages/MetricsApplication/styles.ts b/frontend/src/pages/MetricsApplication/styles.ts new file mode 100644 index 0000000000..8d3bb451bc --- /dev/null +++ b/frontend/src/pages/MetricsApplication/styles.ts @@ -0,0 +1,44 @@ +import { + Button as ButtonComponent, + Typography as TypographyComponent, +} from 'antd'; +import { themeColors } from 'constants/theme'; +import styled from 'styled-components'; + +export const Button = styled(ButtonComponent)` + &&& { + width: min-content; + align-self: flex-end; + } +`; + +export const AppDexThresholdContainer = styled.div` + display: flex; + align-items: center; +`; + +export const Typography = styled(TypographyComponent)` + &&& { + width: 24rem; + margin: 0.5rem 0; + color: ${themeColors.white}; + } +`; + +export const SaveAndCancelContainer = styled.div` + display: flex; + justify-content: flex-end; + margin-right: 1rem; +`; + +export const SaveButton = styled(ButtonComponent)` + &&& { + margin: 0 0.5rem; + display: flex; + align-items: center; + } +`; + +export const ExcludeErrorCodeContainer = styled.div` + margin: 1rem 0; +`; diff --git a/frontend/src/pages/MetricsApplication/types.ts b/frontend/src/pages/MetricsApplication/types.ts index 0bd7166eaa..b5f6d49731 100644 --- a/frontend/src/pages/MetricsApplication/types.ts +++ b/frontend/src/pages/MetricsApplication/types.ts @@ -1,3 +1,10 @@ +import { NotificationInstance } from 'antd/es/notification/interface'; +import { UseMutateAsyncFunction } from 'react-query'; +import { + ApDexPayloadAndSettingsProps, + SetApDexPayloadProps, +} from 'types/api/metrics/getApDex'; + export enum MetricsApplicationTab { OVER_METRICS = 'OVER_METRICS', DB_CALL_METRICS = 'DB_CALL_METRICS', @@ -9,3 +16,16 @@ export const TAB_KEY_VS_LABEL = { [MetricsApplicationTab.DB_CALL_METRICS]: 'DB Call Metrics', [MetricsApplicationTab.EXTERNAL_METRICS]: 'External Metrics', }; + +export interface OnSaveApDexSettingsProps { + thresholdValue: number; + servicename: string; + notifications: NotificationInstance; + refetchGetApDexSetting?: VoidFunction; + mutateAsync: UseMutateAsyncFunction< + SetApDexPayloadProps, + Error, + ApDexPayloadAndSettingsProps + >; + handlePopOverClose: VoidFunction; +} diff --git a/frontend/src/pages/MetricsApplication/utils.ts b/frontend/src/pages/MetricsApplication/utils.ts index ec47b3ca62..5d785e629a 100644 --- a/frontend/src/pages/MetricsApplication/utils.ts +++ b/frontend/src/pages/MetricsApplication/utils.ts @@ -1,5 +1,8 @@ +import axios from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; + import { TAB_KEYS_VS_METRICS_APPLICATION_KEY } from './config'; -import { MetricsApplicationTab } from './types'; +import { MetricsApplicationTab, OnSaveApDexSettingsProps } from './types'; export const isMetricsApplicationTab = ( tab: string, @@ -15,3 +18,29 @@ export const getMetricsApplicationKey = ( return MetricsApplicationTab.OVER_METRICS; }; + +export const onSaveApDexSettings = ({ + thresholdValue, + refetchGetApDexSetting, + mutateAsync, + notifications, + handlePopOverClose, + servicename, +}: OnSaveApDexSettingsProps) => async (): Promise => { + if (!refetchGetApDexSetting) return; + + try { + await mutateAsync({ + servicename, + threshold: thresholdValue, + excludeStatusCode: '', + }); + await refetchGetApDexSetting(); + } catch (err) { + notifications.error({ + message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG, + }); + } finally { + handlePopOverClose(); + } +}; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 69a3dc6401..886de6ec07 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -1,5 +1,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; +import { ReactNode } from 'react'; import { Layout } from 'react-grid-layout'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; @@ -58,7 +59,7 @@ export interface IBaseWidget { isStacked: boolean; id: string; panelTypes: PANEL_TYPES; - title: string; + title: ReactNode; description: string; opacity: string; nullZeroValues: string; diff --git a/frontend/src/types/api/metrics/getApDex.ts b/frontend/src/types/api/metrics/getApDex.ts new file mode 100644 index 0000000000..051f3994ff --- /dev/null +++ b/frontend/src/types/api/metrics/getApDex.ts @@ -0,0 +1,14 @@ +export interface ApDexPayloadAndSettingsProps { + servicename: string; + threshold: number; + excludeStatusCode: string; +} + +export interface SetApDexPayloadProps { + data: string; +} + +export interface MetricMetaProps { + delta: boolean; + le: number[]; +} diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index 926785e072..caf2d0554d 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -21,7 +21,7 @@ export interface TagFilterItem { id: string; key?: BaseAutocompleteData; op: string; - value: string[] | string; + value: string[] | string | number | boolean; } export interface TagFilter { From 7a4156a3b76afb630057156ba881120796a7db8d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 16 Aug 2023 14:22:40 +0530 Subject: [PATCH 12/17] feat(alerts/query-service): measurement unit support for alerts (#2673) --- go.mod | 2 +- go.sum | 3 +- pkg/query-service/converter/bool.go | 4 + pkg/query-service/converter/converter.go | 137 ++++++++++++++++++ pkg/query-service/converter/data.go | 4 + pkg/query-service/converter/data_rate.go | 4 + .../converter/percent_converter.go | 4 + pkg/query-service/converter/throughput.go | 4 + pkg/query-service/converter/time.go | 4 + pkg/query-service/formatter/bool.go | 4 + pkg/query-service/formatter/data.go | 4 + pkg/query-service/formatter/data_rate.go | 4 + pkg/query-service/formatter/formatter.go | 2 + pkg/query-service/formatter/none.go | 4 + pkg/query-service/formatter/percent.go | 4 + pkg/query-service/formatter/throughput.go | 4 + pkg/query-service/formatter/time.go | 4 + pkg/query-service/model/v3/v3.go | 1 + pkg/query-service/rules/alerting.go | 1 + pkg/query-service/rules/apiParams.go | 2 +- pkg/query-service/rules/promRule.go | 18 ++- pkg/query-service/rules/templates.go | 6 +- pkg/query-service/rules/thresholdRule.go | 32 +++- 23 files changed, 242 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 9a91513386..b5bc300e0b 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230523034029-2b7ff773052c github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230517094211-cd3f3f0aea85 github.com/coreos/go-oidc/v3 v3.4.0 - github.com/dustin/go-humanize v1.0.0 + github.com/dustin/go-humanize v1.0.1 github.com/go-co-op/gocron v1.30.1 github.com/go-kit/log v0.2.1 github.com/go-redis/redis/v8 v8.11.5 diff --git a/go.sum b/go.sum index 923ab20a20..23dbda27c2 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,9 @@ github.com/docker/docker v20.10.22+incompatible h1:6jX4yB+NtcbldT90k7vBSaWJDB3i+ github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y= github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= diff --git a/pkg/query-service/converter/bool.go b/pkg/query-service/converter/bool.go index 2183c3b2a4..74d6bb78a6 100644 --- a/pkg/query-service/converter/bool.go +++ b/pkg/query-service/converter/bool.go @@ -7,6 +7,10 @@ func NewBoolConverter() Converter { return &boolConverter{} } +func (*boolConverter) Name() string { + return "bool" +} + func (c *boolConverter) Convert(v Value, to Unit) Value { // There is no conversion to be done for bool return Value{ diff --git a/pkg/query-service/converter/converter.go b/pkg/query-service/converter/converter.go index 82f87b8805..44a92b8f50 100644 --- a/pkg/query-service/converter/converter.go +++ b/pkg/query-service/converter/converter.go @@ -11,12 +11,20 @@ type Value struct { // Converter converts values from one unit to another type Converter interface { + // Convert converts the given value to the given unit Convert(v Value, to Unit) Value + + // Name returns the name of the converter + Name() string } // noneConverter is a converter that does not convert type noneConverter struct{} +func (*noneConverter) Name() string { + return "none" +} + func (c *noneConverter) Convert(v Value, to Unit) Value { return v } @@ -51,3 +59,132 @@ func FromUnit(u Unit) Converter { return NoneConverter } } + +func UnitToName(u string) string { + switch u { + case "ns": + return " ns" + case "us": + return " us" + case "ms": + return " ms" + case "s": + return " s" + case "m": + return " minutes" + case "h": + return " hours" + case "d": + return " days" + case "bytes": + return " bytes" + case "decbytes": + return " bytes" + case "bits": + return " bits" + case "decbits": + return " bits" + case "kbytes": + return " KiB" + case "decKbytes": + return " kB" + case "mbytes": + return " MiB" + case "decMbytes": + return " MB" + case "gbytes": + return " GiB" + case "decGbytes": + return " GB" + case "tbytes": + return " TiB" + case "decTbytes": + return " TB" + case "pbytes": + return " PiB" + case "decPbytes": + return " PB" + case "binBps": + return " bytes/sec(IEC)" + case "Bps": + return " bytes/sec(SI)" + case "binbps": + return " bits/sec(IEC)" + case "bps": + return " bits/sec(SI)" + case "KiBs": + return " KiB/sec" + case "Kibits": + return " Kibit/sec" + case "KBs": + return " kB/sec" + case "Kbits": + return " kbit/sec" + case "MiBs": + return " MiB/sec" + case "Mibits": + return " Mibit/sec" + case "MBs": + return " MB/sec" + case "Mbits": + return " Mbit/sec" + case "GiBs": + return " GiB/sec" + case "Gibits": + return " Gibit/sec" + case "GBs": + return " GB/sec" + case "Gbits": + return " Gbit/sec" + case "TiBs": + return " TiB/sec" + case "Tibits": + return " Tibit/sec" + case "TBs": + return " TB/sec" + case "Tbits": + return " Tbit/sec" + case "PiBs": + return " PiB/sec" + case "Pibits": + return " Pibit/sec" + case "PBs": + return " PB/sec" + case "Pbits": + return " Pbit/sec" + case "percent": + return " %" + case "percentunit": + return " %" + case "bool": + return "" + case "bool_yes_no": + return "" + case "bool_true_false": + return "" + case "bool_1_0": + return "" + case "cps": + return " counts/sec (cps)" + case "ops": + return " ops/sec (ops)" + case "reqps": + return " requests/sec (rps)" + case "rps": + return " reads/sec (rps)" + case "wps": + return " writes/sec (wps)" + case "iops": + return " I/O ops/sec (iops)" + case "cpm": + return " counts/min (cpm)" + case "opm": + return " ops/min (opm)" + case "rpm": + return " reads/min (rpm)" + case "wpm": + return " writes/min (wpm)" + default: + return u + } +} diff --git a/pkg/query-service/converter/data.go b/pkg/query-service/converter/data.go index f7252a2bc3..1dfb183fb1 100644 --- a/pkg/query-service/converter/data.go +++ b/pkg/query-service/converter/data.go @@ -54,6 +54,10 @@ func NewDataConverter() Converter { return &dataConverter{} } +func (*dataConverter) Name() string { + return "data" +} + func FromDataUnit(u Unit) float64 { switch u { case "bytes": // base 2 diff --git a/pkg/query-service/converter/data_rate.go b/pkg/query-service/converter/data_rate.go index 3951341733..7b5fcebdca 100644 --- a/pkg/query-service/converter/data_rate.go +++ b/pkg/query-service/converter/data_rate.go @@ -50,6 +50,10 @@ func NewDataRateConverter() Converter { return &dataRateConverter{} } +func (*dataRateConverter) Name() string { + return "data_rate" +} + func FromDataRateUnit(u Unit) float64 { // See https://github.com/SigNoz/signoz/blob/5a81f5f90b34845f5b4b3bdd46acf29d04bf3987/frontend/src/container/NewWidget/RightContainer/dataFormatCategories.ts#L62-L85 switch u { diff --git a/pkg/query-service/converter/percent_converter.go b/pkg/query-service/converter/percent_converter.go index 6597da93aa..166b89b540 100644 --- a/pkg/query-service/converter/percent_converter.go +++ b/pkg/query-service/converter/percent_converter.go @@ -7,6 +7,10 @@ func NewPercentConverter() Converter { return &percentConverter{} } +func (*percentConverter) Name() string { + return "percent" +} + func FromPercentUnit(u Unit) float64 { switch u { case "percent": diff --git a/pkg/query-service/converter/throughput.go b/pkg/query-service/converter/throughput.go index 7c38257b81..9fdb4703db 100644 --- a/pkg/query-service/converter/throughput.go +++ b/pkg/query-service/converter/throughput.go @@ -8,6 +8,10 @@ func NewThroughputConverter() Converter { return &throughputConverter{} } +func (*throughputConverter) Name() string { + return "throughput" +} + func (c *throughputConverter) Convert(v Value, to Unit) Value { // There is no conversion to be done for throughput return Value{ diff --git a/pkg/query-service/converter/time.go b/pkg/query-service/converter/time.go index c4f1f03c8a..8dd458cb6d 100644 --- a/pkg/query-service/converter/time.go +++ b/pkg/query-service/converter/time.go @@ -23,6 +23,10 @@ func NewDurationConverter() Converter { return &durationConverter{} } +func (*durationConverter) Name() string { + return "duration" +} + func FromTimeUnit(u Unit) Duration { switch u { case "ns": diff --git a/pkg/query-service/formatter/bool.go b/pkg/query-service/formatter/bool.go index 7080a2bb04..74952cde0e 100644 --- a/pkg/query-service/formatter/bool.go +++ b/pkg/query-service/formatter/bool.go @@ -8,6 +8,10 @@ func NewBoolFormatter() Formatter { return &boolFormatter{} } +func (*boolFormatter) Name() string { + return "bool" +} + func toBool(value float64) string { if value == 0 { return "false" diff --git a/pkg/query-service/formatter/data.go b/pkg/query-service/formatter/data.go index ecf7e6e64d..7ec9e30124 100644 --- a/pkg/query-service/formatter/data.go +++ b/pkg/query-service/formatter/data.go @@ -14,6 +14,10 @@ func NewDataFormatter() Formatter { return &dataFormatter{} } +func (*dataFormatter) Name() string { + return "data" +} + func (f *dataFormatter) Format(value float64, unit string) string { switch unit { case "bytes": diff --git a/pkg/query-service/formatter/data_rate.go b/pkg/query-service/formatter/data_rate.go index ef1053fc93..4027bf3b84 100644 --- a/pkg/query-service/formatter/data_rate.go +++ b/pkg/query-service/formatter/data_rate.go @@ -14,6 +14,10 @@ func NewDataRateFormatter() Formatter { return &dataRateFormatter{} } +func (*dataRateFormatter) Name() string { + return "data_rate" +} + func (f *dataRateFormatter) Format(value float64, unit string) string { switch unit { case "binBps": diff --git a/pkg/query-service/formatter/formatter.go b/pkg/query-service/formatter/formatter.go index 5c700b53c4..bfb0627b93 100644 --- a/pkg/query-service/formatter/formatter.go +++ b/pkg/query-service/formatter/formatter.go @@ -2,6 +2,8 @@ package formatter type Formatter interface { Format(value float64, unit string) string + + Name() string } var ( diff --git a/pkg/query-service/formatter/none.go b/pkg/query-service/formatter/none.go index 8f2fbab6df..208df98821 100644 --- a/pkg/query-service/formatter/none.go +++ b/pkg/query-service/formatter/none.go @@ -8,6 +8,10 @@ func NewNoneFormatter() Formatter { return &noneFormatter{} } +func (*noneFormatter) Name() string { + return "none" +} + func (f *noneFormatter) Format(value float64, unit string) string { return fmt.Sprintf("%v", value) } diff --git a/pkg/query-service/formatter/percent.go b/pkg/query-service/formatter/percent.go index 699ff4894b..9740737a2e 100644 --- a/pkg/query-service/formatter/percent.go +++ b/pkg/query-service/formatter/percent.go @@ -8,6 +8,10 @@ func NewPercentFormatter() Formatter { return &percentFormatter{} } +func (*percentFormatter) Name() string { + return "percent" +} + func toPercent(value float64, decimals DecimalCount) string { return toFixed(value, decimals) + "%" } diff --git a/pkg/query-service/formatter/throughput.go b/pkg/query-service/formatter/throughput.go index 72f8c1a74a..6bad95e019 100644 --- a/pkg/query-service/formatter/throughput.go +++ b/pkg/query-service/formatter/throughput.go @@ -9,6 +9,10 @@ func NewThroughputFormatter() Formatter { return &throughputFormatter{} } +func (*throughputFormatter) Name() string { + return "throughput" +} + func simpleCountUnit(value float64, decimals *int, symbol string) string { units := []string{"", "K", "M", "B", "T"} scaler := scaledUnits(1000, units, 0) diff --git a/pkg/query-service/formatter/time.go b/pkg/query-service/formatter/time.go index 3d9b04d1b7..8c3d860f06 100644 --- a/pkg/query-service/formatter/time.go +++ b/pkg/query-service/formatter/time.go @@ -12,6 +12,10 @@ func NewDurationFormatter() Formatter { return &durationFormatter{} } +func (*durationFormatter) Name() string { + return "duration" +} + func (f *durationFormatter) Format(value float64, unit string) string { switch unit { case "ns": diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 1fb4cf8e60..0cf4e2ed68 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -373,6 +373,7 @@ type CompositeQuery struct { PromQueries map[string]*PromQuery `json:"promQueries,omitempty"` PanelType PanelType `json:"panelType"` QueryType QueryType `json:"queryType"` + Unit string `json:"unit,omitempty"` } func (c *CompositeQuery) Validate() error { diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index 55c2fdfafc..60cb4c9384 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -143,6 +143,7 @@ type RuleCondition struct { CompareOp CompareOp `yaml:"op,omitempty" json:"op,omitempty"` Target *float64 `yaml:"target,omitempty" json:"target,omitempty"` MatchType `json:"matchType,omitempty"` + TargetUnit string `json:"targetUnit,omitempty"` } func (rc *RuleCondition) IsValid() bool { diff --git a/pkg/query-service/rules/apiParams.go b/pkg/query-service/rules/apiParams.go index 649ba6373d..f33af06e81 100644 --- a/pkg/query-service/rules/apiParams.go +++ b/pkg/query-service/rules/apiParams.go @@ -198,7 +198,7 @@ func testTemplateParsing(rl *PostableRule) (errs []error) { } // Trying to parse templates. - tmplData := AlertTemplateData(make(map[string]string), 0, 0) + tmplData := AlertTemplateData(make(map[string]string), "0", "0") defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" parseTest := func(text string) error { tmpl := NewTemplateExpander( diff --git a/pkg/query-service/rules/promRule.go b/pkg/query-service/rules/promRule.go index 76afef01b4..ef7177472e 100644 --- a/pkg/query-service/rules/promRule.go +++ b/pkg/query-service/rules/promRule.go @@ -3,6 +3,7 @@ package rules import ( "context" "fmt" + "strconv" "sync" "time" @@ -12,6 +13,8 @@ import ( plabels "github.com/prometheus/prometheus/model/labels" pql "github.com/prometheus/prometheus/promql" + "go.signoz.io/signoz/pkg/query-service/converter" + "go.signoz.io/signoz/pkg/query-service/formatter" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" qslabels "go.signoz.io/signoz/pkg/query-service/utils/labels" "go.signoz.io/signoz/pkg/query-service/utils/times" @@ -255,6 +258,13 @@ func (r *PromRule) ActiveAlerts() []*Alert { return res } +func (r *PromRule) Unit() string { + if r.ruleCondition != nil && r.ruleCondition.CompositeQuery != nil { + return r.ruleCondition.CompositeQuery.Unit + } + return "" +} + // ForEachActiveAlert runs the given function on each alert. // This should be used when you want to use the actual alerts from the ThresholdRule // and not on its copy. @@ -296,7 +306,9 @@ func (r *PromRule) getPqlQuery() (string, error) { return query, fmt.Errorf("a promquery needs to be set for this rule to function") } if r.ruleCondition.Target != nil && r.ruleCondition.CompareOp != CompareOpNone { - query = fmt.Sprintf("%s %s %f", query, ResolveCompareOp(r.ruleCondition.CompareOp), *r.ruleCondition.Target) + unitConverter := converter.FromUnit(converter.Unit(r.ruleCondition.TargetUnit)) + value := unitConverter.Convert(converter.Value{F: *r.ruleCondition.Target, U: converter.Unit(r.ruleCondition.TargetUnit)}, converter.Unit(r.Unit())) + query = fmt.Sprintf("%s %s %f", query, ResolveCompareOp(r.ruleCondition.CompareOp), value.F) return query, nil } else { return query, nil @@ -310,6 +322,8 @@ func (r *PromRule) getPqlQuery() (string, error) { func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (interface{}, error) { + valueFormatter := formatter.FromUnit(r.Unit()) + q, err := r.getPqlQuery() if err != nil { return nil, err @@ -335,7 +349,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( l[lbl.Name] = lbl.Value } - tmplData := AlertTemplateData(l, smpl.V, r.targetVal()) + tmplData := AlertTemplateData(l, valueFormatter.Format(smpl.V, r.Unit()), strconv.FormatFloat(r.targetVal(), 'f', 2, 64)+converter.UnitToName(r.ruleCondition.TargetUnit)) // Inject some convenience variables that are easier to remember for users // who are not used to Go's templating system. defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" diff --git a/pkg/query-service/rules/templates.go b/pkg/query-service/rules/templates.go index 473a5cfa87..9cc49f787d 100644 --- a/pkg/query-service/rules/templates.go +++ b/pkg/query-service/rules/templates.go @@ -201,11 +201,11 @@ func NewTemplateExpander( } // AlertTemplateData returns the interface to be used in expanding the template. -func AlertTemplateData(labels map[string]string, value float64, threshold float64) interface{} { +func AlertTemplateData(labels map[string]string, value string, threshold string) interface{} { return struct { Labels map[string]string - Value float64 - Threshold float64 + Value string + Threshold string }{ Labels: labels, Value: value, diff --git a/pkg/query-service/rules/thresholdRule.go b/pkg/query-service/rules/thresholdRule.go index fbd65d9948..507e6fa289 100644 --- a/pkg/query-service/rules/thresholdRule.go +++ b/pkg/query-service/rules/thresholdRule.go @@ -7,6 +7,7 @@ import ( "math" "reflect" "sort" + "strconv" "sync" "text/template" "time" @@ -14,6 +15,7 @@ import ( "go.uber.org/zap" "github.com/ClickHouse/clickhouse-go/v2" + "go.signoz.io/signoz/pkg/query-service/converter" "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" "go.signoz.io/signoz/pkg/query-service/constants" @@ -26,6 +28,7 @@ import ( logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3" metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3" tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" + "go.signoz.io/signoz/pkg/query-service/formatter" yaml "gopkg.in/yaml.v2" ) @@ -324,6 +327,14 @@ func (r *ThresholdRule) SendAlerts(ctx context.Context, ts time.Time, resendDela }) notifyFunc(ctx, "", alerts...) } + +func (r *ThresholdRule) Unit() string { + if r.ruleCondition != nil && r.ruleCondition.CompositeQuery != nil { + return r.ruleCondition.CompositeQuery.Unit + } + return "" +} + func (r *ThresholdRule) CheckCondition(v float64) bool { if math.IsNaN(v) { @@ -336,16 +347,20 @@ func (r *ThresholdRule) CheckCondition(v float64) bool { return false } - zap.S().Debugf("target:", v, *r.ruleCondition.Target) + unitConverter := converter.FromUnit(converter.Unit(r.ruleCondition.TargetUnit)) + + value := unitConverter.Convert(converter.Value{F: *r.ruleCondition.Target, U: converter.Unit(r.ruleCondition.TargetUnit)}, converter.Unit(r.Unit())) + + zap.S().Debugf("Checking condition for rule: %s, Converter=%s, Value=%f, Target=%f, CompareOp=%s", r.Name(), unitConverter.Name(), v, value.F, r.ruleCondition.CompareOp) switch r.ruleCondition.CompareOp { case ValueIsEq: - return v == *r.ruleCondition.Target + return v == value.F case ValueIsNotEq: - return v != *r.ruleCondition.Target + return v != value.F case ValueIsBelow: - return v < *r.ruleCondition.Target + return v < value.F case ValueIsAbove: - return v > *r.ruleCondition.Target + return v > value.F default: return false } @@ -716,6 +731,7 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time, ch c func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (interface{}, error) { + valueFormatter := formatter.FromUnit(r.Unit()) res, err := r.buildAndRunQuery(ctx, ts, queriers.Ch) if err != nil { @@ -737,7 +753,11 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie l[lbl.Name] = lbl.Value } - tmplData := AlertTemplateData(l, smpl.V, r.targetVal()) + value := valueFormatter.Format(smpl.V, r.Unit()) + threshold := strconv.FormatFloat(r.targetVal(), 'f', 2, 64) + converter.UnitToName(r.ruleCondition.TargetUnit) + zap.S().Debugf("Alert template data for rule %s: Formatter=%s, Value=%s, Threshold=%s", r.Name(), valueFormatter.Name(), value, threshold) + + tmplData := AlertTemplateData(l, value, threshold) // Inject some convenience variables that are easier to remember for users // who are not used to Go's templating system. defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" From 22d4c53a431cec65c23dbeb184905d6b0f2ddfcc Mon Sep 17 00:00:00 2001 From: Shivanshu Raj Shrivastava Date: Wed, 16 Aug 2023 14:41:51 +0530 Subject: [PATCH 13/17] bump nginx base image for cve fix (#3340) --- frontend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cf9b792cf1..db4974a3b5 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -24,7 +24,7 @@ COPY . . RUN yarn build -FROM nginx:1.18-alpine +FROM nginx:1.24.0-alpine COPY conf/default.conf /etc/nginx/conf.d/default.conf From ded2c981671d6521919a9b57699805cb746190b9 Mon Sep 17 00:00:00 2001 From: Nico <588438+rybnico@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:20:56 +0200 Subject: [PATCH 14/17] fix: Adjust query-service Dockerfiles to make /root/query-service executable for non-root users (#3338) --- ee/query-service/Dockerfile | 3 +++ pkg/query-service/Dockerfile | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ee/query-service/Dockerfile b/ee/query-service/Dockerfile index cfdee0cb6d..4c9d135b4f 100644 --- a/ee/query-service/Dockerfile +++ b/ee/query-service/Dockerfile @@ -39,6 +39,9 @@ COPY --from=builder /go/src/github.com/signoz/signoz/ee/query-service/bin/query- # copy prometheus YAML config COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml +# Make query-service executable for non-root users +RUN chmod 755 /root /root/query-service + # run the binary ENTRYPOINT ["./query-service"] diff --git a/pkg/query-service/Dockerfile b/pkg/query-service/Dockerfile index 2d05e02133..de9f708405 100644 --- a/pkg/query-service/Dockerfile +++ b/pkg/query-service/Dockerfile @@ -44,6 +44,9 @@ COPY --from=builder /go/src/github.com/signoz/signoz/pkg/query-service/bin/query # copy prometheus YAML config COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml +# Make query-service executable for non-root users +RUN chmod 755 /root /root/query-service + # run the binary ENTRYPOINT ["./query-service"] From 8ef4c0bcddc419e466a9a428aef2e966b988a505 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 16 Aug 2023 16:10:59 +0530 Subject: [PATCH 15/17] fix: use `ts` as column to make formula work (#3351) --- pkg/query-service/app/metrics/v3/query_builder.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/query-service/app/metrics/v3/query_builder.go b/pkg/query-service/app/metrics/v3/query_builder.go index 5ee4c24128..ea3888649b 100644 --- a/pkg/query-service/app/metrics/v3/query_builder.go +++ b/pkg/query-service/app/metrics/v3/query_builder.go @@ -403,15 +403,15 @@ func reduceQuery(query string, reduceTo v3.ReduceToOperator, aggregateOperator v // chart with just the query value. For the quer switch reduceTo { case v3.ReduceToOperatorLast: - query = fmt.Sprintf("SELECT anyLastIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s", selectLabels, query, groupBy) + query = fmt.Sprintf("SELECT *, timestamp AS ts FROM (SELECT anyLastIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s)", selectLabels, query, groupBy) case v3.ReduceToOperatorSum: - query = fmt.Sprintf("SELECT sumIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s", selectLabels, query, groupBy) + query = fmt.Sprintf("SELECT *, timestamp AS ts FROM (SELECT sumIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s)", selectLabels, query, groupBy) case v3.ReduceToOperatorAvg: - query = fmt.Sprintf("SELECT avgIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s", selectLabels, query, groupBy) + query = fmt.Sprintf("SELECT *, timestamp AS ts FROM (SELECT avgIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s)", selectLabels, query, groupBy) case v3.ReduceToOperatorMax: - query = fmt.Sprintf("SELECT maxIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s", selectLabels, query, groupBy) + query = fmt.Sprintf("SELECT *, timestamp AS ts FROM (SELECT maxIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s)", selectLabels, query, groupBy) case v3.ReduceToOperatorMin: - query = fmt.Sprintf("SELECT minIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s", selectLabels, query, groupBy) + query = fmt.Sprintf("SELECT *, timestamp AS ts FROM (SELECT minIf(value, toUnixTimestamp(ts) != 0) as value, anyIf(ts, toUnixTimestamp(ts) != 0) AS timestamp %s FROM (%s) %s)", selectLabels, query, groupBy) default: return "", fmt.Errorf("unsupported reduce operator") } From 9a6fcb6b1df5d08393e2a7212105f0c81c341a21 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Wed, 16 Aug 2023 16:52:30 +0300 Subject: [PATCH 16/17] fix: log details action not it (#3350) * fix: log details action not it * fix: getting keys when query param redirect * fix: getting operator * fix: data type for not existing autocomplete value --------- Co-authored-by: Palash Gupta --- frontend/src/hooks/logs/useActiveLog.ts | 105 +++++++++++------- .../chooseAutocompleteFromCustomValue.ts | 5 +- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/frontend/src/hooks/logs/useActiveLog.ts b/frontend/src/hooks/logs/useActiveLog.ts index 004d7e1d92..fae1b6f063 100644 --- a/frontend/src/hooks/logs/useActiveLog.ts +++ b/frontend/src/hooks/logs/useActiveLog.ts @@ -1,20 +1,20 @@ -import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder'; +import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; +import { QueryBuilderKeys } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; +import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useNotifications } from 'hooks/useNotifications'; import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString'; import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue'; import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useQueryClient } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; -import { SuccessResponse } from 'types/api'; import { ILog } from 'types/api/logs/log'; -import { - BaseAutocompleteData, - IQueryAutocompleteResponse, -} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { ILogsReducer } from 'types/reducer/logs'; import { v4 as uuid } from 'uuid'; @@ -31,6 +31,9 @@ export const useActiveLog = (): UseActiveLog => { const { pathname } = useLocation(); const history = useHistory(); const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); + const { notifications } = useNotifications(); + + const { t } = useTranslation('common'); const isLogsPage = useMemo(() => pathname === ROUTES.LOGS, [pathname]); @@ -60,48 +63,64 @@ export const useActiveLog = (): UseActiveLog => { const onClearActiveLog = useCallback((): void => setActiveLog(null), []); const onAddToQueryExplorer = useCallback( - (fieldKey: string, fieldValue: string, operator: string): void => { - const keysAutocomplete: BaseAutocompleteData[] = - queryClient.getQueryData>( - [QueryBuilderKeys.GET_AGGREGATE_KEYS], - { exact: false }, - )?.payload.attributeKeys || []; + async ( + fieldKey: string, + fieldValue: string, + operator: string, + ): Promise => { + try { + const keysAutocompleteResponse = await queryClient.fetchQuery( + [QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey], + async () => + getAggregateKeys({ + searchText: fieldKey, + aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateAttribute: + currentQuery.builder.queryData[0].aggregateAttribute.key, + }), + ); - const existAutocompleteKey = chooseAutocompleteFromCustomValue( - keysAutocomplete, - fieldKey, - ); + const keysAutocomplete: BaseAutocompleteData[] = + keysAutocompleteResponse.payload?.attributeKeys || []; - const currentOperator = - Object.keys(OPERATORS).find((op) => op === operator) || ''; + const existAutocompleteKey = chooseAutocompleteFromCustomValue( + keysAutocomplete, + fieldKey, + ); - const nextQuery: Query = { - ...currentQuery, - builder: { - ...currentQuery.builder, - queryData: currentQuery.builder.queryData.map((item) => ({ - ...item, - filters: { - ...item.filters, - items: [ - ...item.filters.items.filter( - (item) => item.key?.id !== existAutocompleteKey.id, - ), - { - id: uuid(), - key: existAutocompleteKey, - op: currentOperator, - value: fieldValue, - }, - ], - }, - })), - }, - }; + const currentOperator = getOperatorValue(operator); - redirectWithQueryBuilderData(nextQuery); + const nextQuery: Query = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: currentQuery.builder.queryData.map((item) => ({ + ...item, + filters: { + ...item.filters, + items: [ + ...item.filters.items.filter( + (item) => item.key?.id !== existAutocompleteKey.id, + ), + { + id: uuid(), + key: existAutocompleteKey, + op: currentOperator, + value: fieldValue, + }, + ], + }, + })), + }, + }; + + redirectWithQueryBuilderData(nextQuery); + } catch { + notifications.error({ message: t('something_went_wrong') }); + } }, - [currentQuery, queryClient, redirectWithQueryBuilderData], + [currentQuery, notifications, queryClient, redirectWithQueryBuilderData, t], ); const onAddToQueryLogs = useCallback( diff --git a/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts index ccd69df45f..3b1bad0abe 100644 --- a/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts +++ b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts @@ -9,8 +9,9 @@ export const chooseAutocompleteFromCustomValue = ( (sourceAutoComplete) => value === sourceAutoComplete.key, ); - if (!firstBaseAutoCompleteValue) - return { ...initialAutocompleteData, key: value }; + if (!firstBaseAutoCompleteValue) { + return { ...initialAutocompleteData, key: value, dataType: 'string' }; + } return firstBaseAutoCompleteValue; }; From 7554bce11c2b9c3e9cddd19472c77971e33c6710 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 16 Aug 2023 23:45:05 +0530 Subject: [PATCH 17/17] chore: pin versions to 0.26.0 --- deploy/docker-swarm/clickhouse-setup/docker-compose.yaml | 4 ++-- deploy/docker/clickhouse-setup/docker-compose.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index e65e7c041b..fdb88a0195 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -146,7 +146,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.25.5 + image: signoz/query-service:0.26.0 command: [ "-config=/root/config/prometheus.yml" ] # ports: # - "6060:6060" # pprof port @@ -182,7 +182,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:0.25.5 + image: signoz/frontend:0.26.0 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 60b01274c3..15926cfc70 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -163,7 +163,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.25.5} + image: signoz/query-service:${DOCKER_TAG:-0.26.0} container_name: signoz-query-service command: [ "-config=/root/config/prometheus.yml" ] # ports: @@ -198,7 +198,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.25.5} + image: signoz/frontend:${DOCKER_TAG:-0.26.0} container_name: signoz-frontend restart: on-failure depends_on: