diff --git a/pkg/apiserver/diagnose/diagnose.go b/pkg/apiserver/diagnose/diagnose.go index 130623be0a..57cab1fd14 100644 --- a/pkg/apiserver/diagnose/diagnose.go +++ b/pkg/apiserver/diagnose/diagnose.go @@ -15,10 +15,16 @@ package diagnose import ( "encoding/json" + "fmt" + "io/ioutil" "net/http" + "net/url" + "os" + "sync" "time" "github.com/gin-gonic/gin" + "github.com/goccy/go-graphviz" "github.com/pingcap/log" "go.uber.org/zap" @@ -35,6 +41,8 @@ const ( timeLayout = "2006-01-02 15:04:05" ) +var graphvizMutex sync.Mutex + type Service struct { // FIXME: Use fx.In config *config.Config @@ -71,13 +79,109 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint.GET("/reports/:id/status", auth.MWAuthRequired(), s.reportStatusHandler) + endpoint.POST("/metrics_relation/generate", auth.MWAuthRequired(), s.metricsRelationHandler) + endpoint.GET("/metrics_relation/view", s.metricsRelationViewHandler) } -type GenerateReportRequest struct { - StartTime int64 `json:"start_time"` - EndTime int64 `json:"end_time"` - CompareStartTime int64 `json:"compare_start_time"` - CompareEndTime int64 `json:"compare_end_time"` +func (s *Service) generateMetricsRelation(startTime, endTime time.Time, graphType string) (string, error) { + params := url.Values{} + params.Add("start", startTime.Format(time.RFC3339)) + params.Add("end", endTime.Format(time.RFC3339)) + params.Add("type", graphType) + encodedParams := params.Encode() + + data, err := s.tidbClient.SendGetRequest("/metrics/profile?" + encodedParams) + if err != nil { + return "", err + } + + file, err := ioutil.TempFile("", "metrics*.svg") + if err != nil { + return "", fmt.Errorf("failed to create temp file: %v", err) + } + _ = file.Close() + + g := graphviz.New() + // FIXME: should share a global mutex for profiling. + graphvizMutex.Lock() + defer graphvizMutex.Unlock() + graph, err := graphviz.ParseBytes(data) + if err != nil { + _ = os.Remove(file.Name()) + return "", fmt.Errorf("failed to parse DOT file: %v", err) + } + + if err := g.RenderFilename(graph, graphviz.SVG, file.Name()); err != nil { + _ = os.Remove(file.Name()) + return "", fmt.Errorf("failed to render SVG: %v", err) + } + + return file.Name(), nil +} + +type GenerateMetricsRelationRequest struct { + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Type string `json:"type"` +} + +// @Id diagnoseGenerateMetricsRelationship +// @Summary Generate metrics relationship graph. +// @Param request body GenerateMetricsRelationRequest true "Request body" +// @Router /diagnose/metrics_relation/generate [post] +// @Success 200 {string} string +// @Security JwtAuth +// @Failure 401 {object} utils.APIError "Unauthorized failure" +func (s *Service) metricsRelationHandler(c *gin.Context) { + var req GenerateMetricsRelationRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.MakeInvalidRequestErrorFromError(c, err) + return + } + + startTime := time.Unix(req.StartTime, 0) + endTime := time.Unix(req.EndTime, 0) + + path, err := s.generateMetricsRelation(startTime, endTime, req.Type) + if err != nil { + _ = c.Error(err) + return + } + + token, err := utils.NewJWTString("diagnose/metrics", path) + if err != nil { + _ = c.Error(err) + return + } + + c.JSON(http.StatusOK, token) +} + +// @Summary View metrics relationship graph. +// @Produce image/svg +// @Param token query string true "token" +// @Failure 400 {object} utils.APIError +// @Failure 401 {object} utils.APIError "Unauthorized failure" +// @Failure 500 {object} utils.APIError +// @Router /diagnose/metrics_relation/view [get] +func (s *Service) metricsRelationViewHandler(c *gin.Context) { + token := c.Query("token") + path, err := utils.ParseJWTString("diagnose/metrics", token) + if err != nil { + utils.MakeInvalidRequestErrorFromError(c, err) + return + } + + data, err := ioutil.ReadFile(path) + if err != nil { + _ = c.Error(err) + return + } + + // Do not remove it? Otherwise the link will just expire.. + // _ = os.Remove(path) + + c.Data(http.StatusOK, "image/svg+xml", data) } // @Summary SQL diagnosis reports history @@ -95,6 +199,13 @@ func (s *Service) reportsHandler(c *gin.Context) { c.JSON(http.StatusOK, reports) } +type GenerateReportRequest struct { + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + CompareStartTime int64 `json:"compare_start_time"` + CompareEndTime int64 `json:"compare_end_time"` +} + // @Summary SQL diagnosis report // @Description Generate sql diagnosis report // @Param request body GenerateReportRequest true "Request body" diff --git a/release-version b/release-version index 970227a28d..1bf7f6b34e 100644 --- a/release-version +++ b/release-version @@ -1,3 +1,3 @@ # This file specifies the TiDB Dashboard internal version, which will be printed in `--version` # and UI. In release branch, changing this file will result in publishing a new version and tag. -2020.09.08.1 +2020.09.21.1 diff --git a/ui/lib/apps/Diagnose/pages/DiagnoseGenerator.tsx b/ui/lib/apps/Diagnose/pages/DiagnoseGenerator.tsx index e558a1ef14..d7ec8ffd38 100644 --- a/ui/lib/apps/Diagnose/pages/DiagnoseGenerator.tsx +++ b/ui/lib/apps/Diagnose/pages/DiagnoseGenerator.tsx @@ -1,12 +1,20 @@ -import { Button, Form, Input, InputNumber, message, Select, Switch } from 'antd' +import { + Button, + Form, + Input, + InputNumber, + message, + Modal, + Select, + Switch, +} from 'antd' import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane' -import React from 'react' +import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { getValueFormat } from '@baurine/grafana-value-formats' - import client from '@lib/client' -import { Card } from '@lib/components' +import { Card, Pre } from '@lib/components' import { DatePicker } from '@lib/components' import DiagnoseHistory from '../components/DiagnoseHistory' @@ -47,11 +55,62 @@ export default function DiagnoseGenerator() { const { t } = useTranslation() const navigate = useNavigate() const handleFinish = useFinishHandler(navigate) + const [form] = Form.useForm() + const [isGenerateRelationPosting, setGenerateRelationPosting] = useState( + false + ) + + const handleMetricsRelation = useCallback(async () => { + try { + await form.validateFields() + } catch (e) { + return + } + + const fieldsValue = form.getFieldsValue() + + const start_time = fieldsValue['rangeBegin'].unix() + let range_duration = fieldsValue['rangeDuration'] + if (fieldsValue['rangeDuration'] === 0) { + range_duration = fieldsValue['rangeDurationCustom'] + } + const end_time = start_time + range_duration * 60 + + try { + setGenerateRelationPosting(true) + + const resp = await client + .getInstance() + .diagnoseGenerateMetricsRelationship({ + start_time, + end_time, + type: 'sum', + }) + Modal.success({ + title: t('diagnose.metrics_relation.success.title'), + okText: t('diagnose.metrics_relation.success.button'), + okButtonProps: { + target: '_blank', + href: + `${client.getBasePath()}/diagnose/metrics_relation/view?token=` + + encodeURIComponent(resp.data), + }, + }) + } catch (e) { + Modal.error({ + title: 'Error', + content:
{e?.response?.data?.message ?? e.message}
, + }) + } + + setGenerateRelationPosting(false) + }, [t, form]) return (
+ + +
diff --git a/ui/lib/apps/Diagnose/translations/en.yaml b/ui/lib/apps/Diagnose/translations/en.yaml index 6c0ce9d94a..bfdec8f070 100644 --- a/ui/lib/apps/Diagnose/translations/en.yaml +++ b/ui/lib/apps/Diagnose/translations/en.yaml @@ -7,6 +7,7 @@ diagnose: is_compare: Compare by Baseline compare_range_begin: Baseline Range Start Time submit: Start + metrics_relation: Generate Metrics Relation list_table: id: Report ID diagnose_create_time: Diagnose At @@ -26,3 +27,7 @@ diagnose: progress: Progress time_duration: custom: Custom + metrics_relation: + success: + title: Generate metrics relation graph successfully + button: View Graph diff --git a/ui/lib/apps/Diagnose/translations/zh.yaml b/ui/lib/apps/Diagnose/translations/zh.yaml index 83b2c1bcba..def6bdae2d 100644 --- a/ui/lib/apps/Diagnose/translations/zh.yaml +++ b/ui/lib/apps/Diagnose/translations/zh.yaml @@ -7,6 +7,7 @@ diagnose: is_compare: 与基线区间对比 compare_range_begin: 基线区间起始时间 submit: 开始 + metrics_relation: 生成监控关系图 list_table: id: 报告 ID diagnose_create_time: 诊断时间 @@ -26,3 +27,7 @@ diagnose: progress: 生成进度 time_duration: custom: 自定义 + metrics_relation: + success: + title: 监控耗时关系图生成成功 + button: 查看关系图