diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 35fcaa9a4d..fdb88a0195 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -133,7 +133,7 @@ services: # - ./data/clickhouse-3/:/var/lib/clickhouse/ alertmanager: - image: signoz/alertmanager:0.23.1 + image: signoz/alertmanager:0.23.2 volumes: - ./data/alertmanager:/data command: @@ -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-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index e441faaea7..ae40af5209 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -34,7 +34,7 @@ services: alertmanager: container_name: signoz-alertmanager - image: signoz/alertmanager:0.23.1 + image: signoz/alertmanager:0.23.2 volumes: - ./data/alertmanager:/data depends_on: diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index ed8a503c60..15926cfc70 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -148,7 +148,7 @@ services: # - ./user_scripts:/var/lib/clickhouse/user_scripts/ alertmanager: - image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.1} + image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.2} container_name: signoz-alertmanager volumes: - ./data/alertmanager:/data @@ -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: 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/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/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 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/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/api/metrics/ApDex/apDexSettings.ts b/frontend/src/api/metrics/ApDex/apDexSettings.ts new file mode 100644 index 0000000000..e3d69c9f15 --- /dev/null +++ b/frontend/src/api/metrics/ApDex/apDexSettings.ts @@ -0,0 +1,16 @@ +import axios from 'api'; +import { + ApDexPayloadAndSettingsProps, + SetApDexPayloadProps, +} from 'types/api/metrics/getApDex'; + +export const setApDexSettings = async ({ + servicename, + threshold, + excludeStatusCode, +}: ApDexPayloadAndSettingsProps): Promise => + axios.post('/settings/apdex', { + servicename, + threshold, + excludeStatusCode, + }); diff --git a/frontend/src/api/metrics/ApDex/getApDexSettings.ts b/frontend/src/api/metrics/ApDex/getApDexSettings.ts new file mode 100644 index 0000000000..4dcb96c760 --- /dev/null +++ b/frontend/src/api/metrics/ApDex/getApDexSettings.ts @@ -0,0 +1,8 @@ +import axios from 'api'; +import { AxiosResponse } from 'axios'; +import { ApDexPayloadAndSettingsProps } from 'types/api/metrics/getApDex'; + +export const getApDexSettings = ( + servicename: string, +): Promise> => + axios.get(`/settings/apdex?services=${servicename}`); diff --git a/frontend/src/api/metrics/ApDex/getMetricMeta.ts b/frontend/src/api/metrics/ApDex/getMetricMeta.ts new file mode 100644 index 0000000000..36466e1e69 --- /dev/null +++ b/frontend/src/api/metrics/ApDex/getMetricMeta.ts @@ -0,0 +1,8 @@ +import axios from 'api'; +import { AxiosResponse } from 'axios'; +import { MetricMetaProps } from 'types/api/metrics/getApDex'; + +export const getMetricMeta = ( + metricName: string, +): Promise> => + axios.get(`/metric_meta?metricName=${metricName}`); diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index d1deb08c4e..0065f6b33c 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -3,7 +3,6 @@ import { BarElement, CategoryScale, Chart, - ChartType, Decimation, Filler, Legend, @@ -18,6 +17,7 @@ import { Tooltip, } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; +import { generateGridTitle } from 'container/GridPanelSwitch/utils'; import { useIsDarkMode } from 'hooks/useDarkMode'; import isEqual from 'lodash-es/isEqual'; import { @@ -26,6 +26,7 @@ import { useCallback, useEffect, useImperativeHandle, + useMemo, useRef, } from 'react'; @@ -83,6 +84,7 @@ const Graph = forwardRef( const nearestDatasetIndex = useRef(null); const chartRef = useRef(null); const isDarkMode = useIsDarkMode(); + const gridTitle = useMemo(() => generateGridTitle(title), [title]); const currentTheme = isDarkMode ? 'dark' : 'light'; const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data @@ -119,7 +121,7 @@ const Graph = forwardRef( const options: CustomChartOptions = getGraphOptions( animate, staticLine, - title, + gridTitle, nearestDatasetIndex, yAxisUnit, onDragSelect, @@ -154,7 +156,7 @@ const Graph = forwardRef( }, [ animate, staticLine, - title, + gridTitle, yAxisUnit, onDragSelect, dragSelectColor, diff --git a/frontend/src/components/Graph/types.ts b/frontend/src/components/Graph/types.ts index dcb9607a0c..b005e24c80 100644 --- a/frontend/src/components/Graph/types.ts +++ b/frontend/src/components/Graph/types.ts @@ -7,7 +7,7 @@ import { ChartType, TimeUnit, } from 'chart.js'; -import { ForwardedRef } from 'react'; +import { ForwardedRef, ReactNode } from 'react'; import { dragSelectPluginId, @@ -49,7 +49,7 @@ export interface GraphProps { animate?: boolean; type: ChartType; data: Chart['data']; - title?: string; + title?: ReactNode; isStacked?: boolean; onClickHandler?: GraphOnClickHandler; name: string; diff --git a/frontend/src/components/TextToolTip/index.tsx b/frontend/src/components/TextToolTip/index.tsx index f01a623988..1bcae8454d 100644 --- a/frontend/src/components/TextToolTip/index.tsx +++ b/frontend/src/components/TextToolTip/index.tsx @@ -1,5 +1,8 @@ -import { grey } from '@ant-design/colors'; -import { QuestionCircleFilled } from '@ant-design/icons'; +import { blue, grey } from '@ant-design/colors'; +import { + QuestionCircleFilled, + QuestionCircleOutlined, +} from '@ant-design/icons'; import { Tooltip } from 'antd'; import { themeColors } from 'constants/theme'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -7,7 +10,12 @@ import { useMemo } from 'react'; import { style } from './styles'; -function TextToolTip({ text, url }: TextToolTipProps): JSX.Element { +function TextToolTip({ + text, + url, + useFilledIcon = true, + urlText, +}: TextToolTipProps): JSX.Element { const isDarkMode = useIsDarkMode(); const overlay = useMemo( @@ -16,12 +24,12 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element { {`${text} `} {url && ( - here + {urlText || 'here'} )} ), - [text, url], + [text, url, urlText], ); const iconStyle = useMemo( @@ -32,19 +40,35 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element { [isDarkMode], ); + const iconOutlinedStyle = useMemo( + () => ({ + ...style, + color: isDarkMode ? themeColors.navyBlue : blue[0], + }), + [isDarkMode], + ); + return ( - + {useFilledIcon ? ( + + ) : ( + + )} ); } TextToolTip.defaultProps = { url: '', + urlText: '', + useFilledIcon: true, }; interface TextToolTipProps { url?: string; text: string; + useFilledIcon?: boolean; + urlText?: string; } export default TextToolTip; 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/apDex.ts b/frontend/src/constants/apDex.ts new file mode 100644 index 0000000000..a49b8b9694 --- /dev/null +++ b/frontend/src/constants/apDex.ts @@ -0,0 +1,5 @@ +export const apDexToolTipText = + "Apdex is a way to measure your users' satisfaction with the response time of your web service. It's represented as a score from 0-1."; +export const apDexToolTipUrl = + 'https://signoz.io/docs/userguide/metrics/#apdex?utm_source=product&utm_medium=frontend&utm_campaign=apdex'; +export const apDexToolTipUrlText = 'Learn more about Apdex.'; 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/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/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({ + + ); +} + +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/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/channels/createMsTeams.ts b/frontend/src/types/api/channels/createMsTeams.ts new file mode 100644 index 0000000000..186d0fd542 --- /dev/null +++ b/frontend/src/types/api/channels/createMsTeams.ts @@ -0,0 +1,8 @@ +import { MsTeamsChannel } from 'container/CreateAlertChannels/config'; + +export type Props = MsTeamsChannel; + +export interface PayloadProps { + data: string; + status: string; +} diff --git a/frontend/src/types/api/channels/editMsTeams.ts b/frontend/src/types/api/channels/editMsTeams.ts new file mode 100644 index 0000000000..60962458d1 --- /dev/null +++ b/frontend/src/types/api/channels/editMsTeams.ts @@ -0,0 +1,10 @@ +import { MsTeamsChannel } from 'container/CreateAlertChannels/config'; + +export interface Props extends MsTeamsChannel { + id: string; +} + +export interface PayloadProps { + data: string; + status: string; +} 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 b16aac3b3c..caf2d0554d 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, @@ -20,7 +21,7 @@ export interface TagFilterItem { id: string; key?: BaseAutocompleteData; op: string; - value: string[] | string; + value: string[] | string | number | boolean; } export interface TagFilter { @@ -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, 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/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"] diff --git a/pkg/query-service/README.md b/pkg/query-service/README.md index 8b12d667c1..e7f53076f7 100644 --- a/pkg/query-service/README.md +++ b/pkg/query-service/README.md @@ -13,7 +13,7 @@ https://github.com/SigNoz/signoz/blob/main/CONTRIBUTING.md#to-run-clickhouse-set - Change the alertmanager section in `signoz/deploy/docker/clickhouse-setup/docker-compose.yaml` as follows: ```console alertmanager: - image: signoz/alertmanager:0.23.1 + image: signoz/alertmanager:0.23.2 volumes: - ./data/alertmanager:/data expose: diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 0965f4804f..1224e2838a 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -559,7 +559,9 @@ func getChannelType(receiver *am.Receiver) string { if receiver.WechatConfigs != nil { return "wechat" } - + if receiver.MSTeamsConfigs != nil { + return "msteams" + } return "" } @@ -582,6 +584,13 @@ func (r *ClickHouseReader) EditChannel(receiver *am.Receiver, id string) (*am.Re } channel_type := getChannelType(receiver) + + // check if channel type is supported in the current user plan + if err := r.featureFlags.CheckFeature(fmt.Sprintf("ALERT_CHANNEL_%s", strings.ToUpper(channel_type))); err != nil { + zap.S().Warn("an unsupported feature was blocked", err) + return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("unsupported feature. please upgrade your plan to access this feature")} + } + receiverString, _ := json.Marshal(receiver) { @@ -619,16 +628,21 @@ func (r *ClickHouseReader) EditChannel(receiver *am.Receiver, id string) (*am.Re func (r *ClickHouseReader) CreateChannel(receiver *am.Receiver) (*am.Receiver, *model.ApiError) { + channel_type := getChannelType(receiver) + + // check if channel type is supported in the current user plan + if err := r.featureFlags.CheckFeature(fmt.Sprintf("ALERT_CHANNEL_%s", strings.ToUpper(channel_type))); err != nil { + zap.S().Warn("an unsupported feature was blocked", err) + return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("unsupported feature. please upgrade your plan to access this feature")} + } + + receiverString, _ := json.Marshal(receiver) + tx, err := r.localDB.Begin() if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } - channel_type := getChannelType(receiver) - receiverString, _ := json.Marshal(receiver) - - // todo: check if the channel name already exists, raise an error if so - { stmt, err := tx.Prepare(`INSERT INTO notification_channels (created_at, updated_at, name, type, data) VALUES($1,$2,$3,$4,$5);`) if err != nil { diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 2e5b6aeca5..4910f7118d 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{}) { @@ -1127,7 +1128,6 @@ func (aH *APIHandler) testChannel(w http.ResponseWriter, r *http.Request) { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } - // send alert apiErrorObj := aH.alertManager.TestReceiver(receiver) if apiErrorObj != nil { @@ -1934,18 +1934,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) @@ -3063,6 +3063,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()) @@ -3123,7 +3127,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!") 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") } 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/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/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/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/integrations/alertManager/model.go b/pkg/query-service/integrations/alertManager/model.go index 2bbac4cf9e..5003830bbf 100644 --- a/pkg/query-service/integrations/alertManager/model.go +++ b/pkg/query-service/integrations/alertManager/model.go @@ -21,6 +21,7 @@ type Receiver struct { PushoverConfigs interface{} `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` VictorOpsConfigs interface{} `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` SNSConfigs interface{} `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"` + MSTeamsConfigs interface{} `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"` } type ReceiverResponse struct { 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 { diff --git a/pkg/query-service/model/featureSet.go b/pkg/query-service/model/featureSet.go index d8c1bfc9b3..7653aced50 100644 --- a/pkg/query-service/model/featureSet.go +++ b/pkg/query-service/model/featureSet.go @@ -11,7 +11,92 @@ type Feature struct { const SmartTraceDetail = "SMART_TRACE_DETAIL" const CustomMetricsFunction = "CUSTOM_METRICS_FUNCTION" +const DisableUpsell = "DISABLE_UPSELL" const OSS = "OSS" const QueryBuilderPanels = "QUERY_BUILDER_PANELS" const QueryBuilderAlerts = "QUERY_BUILDER_ALERTS" const UseSpanMetrics = "USE_SPAN_METRICS" +const AlertChannelSlack = "ALERT_CHANNEL_SLACK" +const AlertChannelWebhook = "ALERT_CHANNEL_WEBHOOK" +const AlertChannelPagerduty = "ALERT_CHANNEL_PAGERDUTY" +const AlertChannelMsTeams = "ALERT_CHANNEL_MSTEAMS" + +var BasicPlan = FeatureSet{ + Feature{ + Name: OSS, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: DisableUpsell, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: SmartTraceDetail, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: CustomMetricsFunction, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: QueryBuilderPanels, + Active: true, + Usage: 0, + UsageLimit: 5, + Route: "", + }, + Feature{ + Name: QueryBuilderAlerts, + Active: true, + Usage: 0, + UsageLimit: 5, + Route: "", + }, + Feature{ + Name: UseSpanMetrics, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: AlertChannelSlack, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: AlertChannelWebhook, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: AlertChannelPagerduty, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + Feature{ + Name: AlertChannelMsTeams, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, +} 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/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/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 f6e79a1643..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 } @@ -550,7 +565,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 { @@ -683,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 { @@ -704,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}}" diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index 3baf7d0e46..8cc84f7566 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -138,7 +138,7 @@ services: # - ./data/clickhouse-3/:/var/lib/clickhouse/ alertmanager: - image: signoz/alertmanager:0.23.1 + image: signoz/alertmanager:0.23.2 container_name: signoz-alertmanager volumes: - ./data/alertmanager:/data