package service import ( "fmt" "math" "net/url" "sort" "strconv" "strings" "sync" "time" "go-admin/app/observe/models" "go-admin/app/observe/models/query" "go-admin/app/observe/service/dto" cDto "go-admin/common/dto" "go-admin/common/prometheus" "go-admin/config" "go-admin/utils" "github.com/pkg/errors" "golang.org/x/text/cases" "golang.org/x/text/language" "gorm.io/gorm" ) type UrlMapping struct { // service.Service utils.OtService } // GetPage 获取UrlMapping列表 func (e *UrlMapping) GetPage(req *dto.UrlMappingGetPageReq, resp *[]dto.UrlMappingListResp, count *int64) error { req.CheckFilling(time.Minute * 5) urlMappings := []models.UrlMapping{} level := 1 if req.Level != "" { var err error if level, err = strconv.Atoi(req.Level); err != nil { level = 1 } } appAlias := "" e.Orm.Model(&models.App{}).Where("id", req.AppId).Pluck("alias", &appAlias) if appAlias == "" { return errors.New(fmt.Sprintf("未查询到应用别名: %s", req.AppId)) } req.Level = strconv.Itoa(level) db := e.Orm.Model(&models.UrlMapping{}). Where("app_id", req.AppId). Where("level=? OR (leveltoDateTime(?) and Timestamp=400, 1, 0)) as ErrorNum", "quantile(0.5)(Duration) as P50Duration", "quantile(0.90)(Duration) as P90Duration", "quantile(0.99)(Duration) as P99Duration", } row := struct { Total int64 MaxDuration int64 ErrorNum int64 AvgDuration float64 P50Duration float64 P90Duration float64 P99Duration float64 }{} err := db.Select(fields).Find(&row).Error if err != nil { return } if row.Total == 0 { item.UrlMappingBaseStats = dto.UrlMappingBaseStats{} item.DurationStats = dto.DurationStats{ Time: []string{}, P50: []float64{}, P90: []float64{}, P99: []float64{}, } return } mins := time.Unix(req.EndTime, 0).Sub(time.Unix(req.StartTime, 0)).Minutes() timeField := "formatDateTime(toStartOfMinute(Timestamp), '%F %H:%i', 'PRC') as StartTime" if req.EndTime-req.StartTime > 60*60 { timeField = "formatDateTime(toStartOfFiveMinutes(Timestamp), '%F %H:%i', 'PRC') as StartTime" } fields = []string{ timeField, "quantile(0.5)(Duration) as P50Duration", "quantile(0.90)(Duration) as P90Duration", "quantile(0.99)(Duration) as P99Duration", } quantiles := []struct { StartTime string P50Duration float64 P90Duration float64 P99Duration float64 }{} if err := db.Select(fields).Group(timeField).Order(fmt.Sprintf("%s asc", timeField)).Find(&quantiles).Error; err != nil { return } item.Total = row.Total item.ErrorNum = row.ErrorNum item.ErrorRate = float64(row.ErrorNum) / float64(row.Total) item.Rpm = math.Round(float64(row.Total)/mins*100) / 100 item.Max = math.Round(float64(row.MaxDuration)/1e6*100) / 100 item.Avg = math.Round(float64(row.AvgDuration)/1e6*100) / 100 item.P50 = math.Round(float64(row.P50Duration)/1e6*100) / 100 item.P90 = math.Round(float64(row.P90Duration)/1e6*100) / 100 item.P99 = math.Round(float64(row.P99Duration)/1e6*100) / 100 item.DurationStats = dto.DurationStats{ Time: make([]string, len(quantiles)), P50: make([]float64, len(quantiles)), P90: make([]float64, len(quantiles)), P99: make([]float64, len(quantiles)), } for i, quantile := range quantiles { item.DurationStats.Time[i] = quantile.StartTime item.DurationStats.P50[i] = math.Round(quantile.P50Duration/1e6*100) / 100 item.DurationStats.P90[i] = math.Round(quantile.P90Duration/1e6*100) / 100 item.DurationStats.P99[i] = math.Round(quantile.P99Duration/1e6*100) / 100 } } func (s *UrlMapping) urlMappingStats(wg *sync.WaitGroup, req *dto.UrlMappingGetPageReq, item *dto.UrlMappingListResp) { defer wg.Done() wg2 := sync.WaitGroup{} metric := "observe_server_duration_milliseconds" labels := map[string]string{ "url_level": item.Level, "url_is_perfect_match": item.IsPerfectMatch, "url_method": item.Method, "url_prefix": item.Url, "service_name": item.ServiceName, "app_alias": item.AppAlias, } ts := time.Unix(req.EndTime, 0) mins := time.Unix(req.EndTime, 0).Sub(time.Unix(req.StartTime, 0)).Minutes() hist := prometheus.NewHistogram(&wg2, metric, labels, ts, int64(mins)) var total int64 var avg, max, p50, p90, p99, errRate float64 times := []string{} p50s, p90s, p99s := []float64{}, []float64{}, []float64{} wg2.Add(10) go hist.Total(&total) go hist.Avg(&avg) go hist.ErrorRate(&errRate) go hist.Quantile(0.5, &p50) go hist.Quantile(0.9, &p90) go hist.Quantile(0.99, &p99) go hist.Quantile(0.999, &max) // 如果使用1, 会读到桶边界 go hist.QuantileMinutes(0.5, &[]string{}, &p50s) go hist.QuantileMinutes(0.9, &[]string{}, &p90s) go hist.QuantileMinutes(0.99, ×, &p99s) wg2.Wait() item.Max = math.Round(max*100) / 100 item.Avg = math.Round(avg*100) / 100 item.Total = total item.ErrorRate = math.Round(errRate*100) / 100 item.ErrorNum = int64(float64(total) * errRate) item.Rpm = math.Round(float64(total)/mins*100) / 100 item.P50 = math.Round(p50*100) / 100 item.P90 = math.Round(p90*100) / 100 item.P99 = math.Round(p99*100) / 100 item.DurationStats = dto.DurationStats{ P50: p50s, P90: p90s, P99: p99s, Time: times, } } func (e *UrlMapping) GetFavors(req *dto.UrlMappingGetFavorsReq, resp *[]dto.UrlMappingListResp) error { urlMappings := []models.UrlMapping{} err := e.Orm.Model(&models.UrlMapping{}).Where("app_id=? AND favor=?", req.AppId, 1).Limit(5).Find(&urlMappings).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil } return errors.Wrap(err, "获取收藏的业务映射失败") } return e.genUrlMappingListResp(urlMappings, req, resp) } func (e *UrlMapping) genUrlMappingListResp( urlMappings []models.UrlMapping, a any, resp *[]dto.UrlMappingListResp) error { *resp = make([]dto.UrlMappingListResp, len(urlMappings)) kinds := make([]string, len(urlMappings)) for i, um := range urlMappings { kinds[i] = um.Url (*resp)[i] = dto.UrlMappingListResp{ Id: int(um.ID), AppId: strconv.Itoa(int(um.AppId)), Name: um.Name, Method: um.Method, Url: um.Url, Type: strconv.Itoa(int(um.Type)), Module: um.Module, Summary: um.Summary, Favor: strconv.Itoa(int(um.Favor)), ServiceName: um.ServiceName, } } var err error req := new(dto.UrlMappingGetFavorsReq) switch r := a.(type) { case *dto.UrlMappingGetFavorsReq: req = r case *dto.UrlMappingGetPageReq: req = &dto.UrlMappingGetFavorsReq{ AppId: r.AppId, TimeRange: models.TimeRange{ StartTime: r.StartTime, EndTime: r.EndTime, }, } default: return errors.Errorf("参数req非法") } baseStats := []dto.UrlMappingBaseStats{} baseStatsMap := map[string]dto.UrlMappingBaseStats{} err = e.ChOrm.Debug().Model(&models.TracesURL{}). Select([]string{ "Kind", "COUNT() AS Total", "SUM(IF(StatusCode>=400, 1, 0)) AS ErrorNum", "round(MAX(Duration)/1e6, 2) AS Max", "round(MIN(Duration)/1e6, 2) AS Min", "round(AVG(Duration)/1e6, 2) AS Avg", }).Where("AppId", req.AppId).Where("Kind IN ?", kinds). Where("Timestamp>=toDateTime(?) AND Timestamp=? AND TimeUnix= n { if i < len(explicitBounds) { qv[j] = explicitBounds[i-1] + (explicitBounds[i]-explicitBounds[i-1])*(n-prevSum)/(sum-prevSum) } else { qv[j] = explicitBounds[len(explicitBounds)-1] + (max-explicitBounds[len(explicitBounds)-1])*(n-prevSum)/(sum-prevSum) } } } } return } // Get 获取UrlMapping对象 func (e *UrlMapping) Get(c *dto.UrlMappingGetReq, result *dto.UrlMappingGetResp) error { var data models.UrlMapping model := new(models.UrlMapping) err := e.Orm.Model(&data). First(model, c.GetId()).Error if err != nil { e.Log.Errorf("db error:%s", err) return err } *result = dto.UrlMappingGetResp{ // Id: model.Id, AppId: strconv.Itoa(int(model.AppId)), Name: model.Name, Url: model.Url, Type: strconv.Itoa(int(model.Type)), Module: model.Module, Summary: model.Summary, Favor: strconv.Itoa(int(model.Favor)), } return nil } // Insert 创建UrlMapping对象 func (e *UrlMapping) Insert(c *dto.UrlMappingInsertReq) error { var err error var data models.UrlMapping cnt := int64(0) err = e.Orm.Model(&models.UrlMapping{}).Where("app_id", c.AppId).Where("url", c.Url).Count(&cnt).Error if err != nil { e.Log.Errorf("查询app_id:%d下url:%s的个数失败:%s", c.AppId, c.Url, err) return err } if cnt > 0 { e.Log.Errorf("该app_id:%s下已经存在相同的url:%s", c.AppId, c.Url) return fmt.Errorf("该app_id:%s下已经存在相同的url:%s", c.AppId, c.Url) } c.Generate(&data) err = e.Orm.Create(&data).Error if err != nil { e.Log.Errorf("创建url-mapping失败:%s", err) return err } return nil } // Update 修改UrlMapping对象 func (e *UrlMapping) Update(c *dto.UrlMappingUpdateReq) error { var err error var data = models.UrlMapping{} err = e.Orm.First(&data, c.GetId()).Error if err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("更新目标不存在, id: %d", c.GetId()) } return err } c.Generate(&data) // 去掉以下逻辑,因为app_id+url不再唯一 // var cnt int64 // err = e.Orm.Model(&models.UrlMapping{}). // Where("id != ? AND app_id = ? AND url = ?", c.GetId(), c.AppId, c.Url).Count(&cnt).Error // if err != nil { // e.Log.Errorf("查询app_id:%d下url:%s的个数失败:%s", c.AppId, c.Url, err) // return err // } // if cnt > 0 { // e.Log.Errorf("该app_id:%s下已经存在相同的url:%s", c.AppId, c.Url) // return fmt.Errorf("该app_id:%s下已经存在相同的url:%s", c.AppId, c.Url) // } db := e.Orm.Save(&data) if err = db.Error; err != nil { e.Log.Errorf("UrlMappingService Save error:%s \r\n", err) return err } return nil } // Favor 收藏/取消收藏 func (e *UrlMapping) Favor(c *dto.UrlMappingFavorReq) error { var data = models.UrlMapping{} if err := e.Orm.First(&data, c.Id).Error; err != nil { return errors.Wrap(err, "获取urlmapping数据失败") } // 不需要再有5个的限制了 // if c.Favor == "1" { // var count int64 // err = e.Orm.Model(&models.UrlMapping{}).Where("app_id", data.AppId).Where("favor", 1).Count(&count).Error // if err != nil { // return errors.Wrap(err, "获取收藏数量失败") // } // if count >= 5 { // return errors.New("最多只能收藏5个业务映射") // } // } if err := e.Orm.Model(&data).Update("favor", c.Favor).Error; err != nil { msg := "收藏失败" if c.Favor == "0" { msg = "取消" + msg } return errors.Wrap(err, msg) } return nil } // Remove 删除UrlMapping func (e *UrlMapping) Remove(d *dto.UrlMappingDeleteReq) error { var data models.UrlMapping db := e.Orm.Model(&data).Delete(&data, d.GetIds()) if err := db.Error; err != nil { e.Log.Errorf("Service RemoveUrlMapping error:%s \r\n", err) return err } return nil } // GetDetail 获取指定业务映射指定区间内的全部请求 func (e *UrlMapping) GetDetail(c *dto.UrlMappingGetDetailReq, result *[]dto.UrlMappingGetDetailResp, count *int64) (err error) { // var data models.UrlMapping // err = e.Orm.Model(&models.UrlMapping{}).First(&data, c.Id).Count(count).Error // if err != nil { // msg := "获取业务映射失败" // e.Log.Error(fmt.Errorf("%s, id: %d, err: %s", msg, c.Id, err)) // return fmt.Errorf(msg) // } appAlias := "" if err := e.Orm.Model(&models.App{}).Where("id", c.AppId).Pluck("alias", &appAlias).Error; err != nil { return errors.Wrap(err, "获取app别名失败") } traceUrls := []models.TracesURL{} if c.Route == "" { c.Route = c.Kind // 兼容旧逻辑 } db := e.ChOrm.Debug().Model(&models.TracesURL{}). Scopes(cDto.Paginate(c.GetPageSize(), c.GetPageIndex())). Where("AppAlias", appAlias).Where("Route", c.Route) if c.Method != "" { db.Where("Method", c.Method) } if c.ServiceName != "" { db.Where("ServiceName", c.ServiceName) } if c.OnlyError == "1" || c.OnlyException { // only error 后面删除 db.Where("StatusCode>=?", 400) } if c.MinDuration > 0 { db.Where("Duration>=?", c.MinDuration*float64(time.Millisecond)) } if c.MaxDuration > 0 { db.Where("Duration<=?", c.MaxDuration*float64(time.Millisecond)) } if c.StartTime == c.EndTime && c.StartTime > 0 { // 对于start_time == end_time的情况,取前后一分钟的数据 db.Where("Timestamp>=toDateTime(?) AND Timestamp<=toDateTime(?)", c.StartTime-60, c.EndTime+60) } else { c.CheckFilling(time.Hour * 48) // 最大取2天,加快速度 db.Where("Timestamp>=toDateTime(?) AND Timestamp<=toDateTime(?)", c.StartTime, c.EndTime) } c.SortInfo.Field = cases.Title(language.Und).String(c.SortInfo.Field) db.Order(c.SortInfo.OrderBy([]string{"Timestamp", "Duration"}, "Duration", "DESC")) err = db.Find(&traceUrls).Limit(-1).Offset(-1).Count(count).Error if err != nil && err != gorm.ErrRecordNotFound { return errors.Wrap(err, "获取业务映射详情失败") } *result = make([]dto.UrlMappingGetDetailResp, len(traceUrls)) // spanIds := make([]string, len(*result)) for i, item := range traceUrls { // spanIds[i] = item.SpanID item.Duration /= 1e6 (*result)[i] = dto.UrlMappingGetDetailResp{ Datetime: item.Timestamp.Local().Format(time.DateTime), Timestamp: item.Timestamp.Unix(), Route: item.Route, Target: item.Target, URL: item.URL, Flavor: item.Flavor, Host: item.Host, Method: item.Method, StatusCode: item.StatusCode, Message: item.Message, TraceID: item.TraceID, SpanID: item.SpanID, ServiceName: item.ServiceName, Duration: item.Duration, } } // spanInfos := []models.Trace{} // err = e.ChOrm.Table(models.TableNameTrace).Where("SpanId IN ?", spanIds).Find(&spanInfos).Error // if err != nil { // msg := "获取业务映射对应的span信息失败" // e.Log.Errorf("%s: %s", msg, err) // return fmt.Errorf(msg) // } // spanInfoMap := make(map[string]models.Trace, len(spanInfos)) // for _, span := range spanInfos { // spanInfoMap[span.SpanID] = span // } // for i, item := range *result { // if spanInfo, ok := spanInfoMap[item.SpanID]; ok { // otel_traces 表中的数据不全,有的spanId找不到 // (*result)[i].SpanDetail = &spanInfo // } // } return } // Sankey 桑基图 func (e *UrlMapping) Sankey(c *dto.UrlMappingSankeyReq, result *dto.UrlMappingSankeyResp) (err error) { links1 := []dto.SankeyLink{} c.CheckFilling(time.Minute * 5) var alias string if err := e.Orm.Model(&models.App{}).Where("id", c.AppId).Pluck("alias", &alias).Error; err != nil { return errors.Wrap(err, "appid查找失败") } var model models.TracesURL scope := func(db *gorm.DB) *gorm.DB { db.Where("AppAlias", alias) db.Where("Timestamp >= ? AND Timestamp < ?", c.StartTime, c.EndTime) return db } if c.Percentile == 0 { // 默认百分位数 90% c.Percentile = 0.9 } // 获取指定分位数对应的duration var percentile float64 = 0 fieldsTmpl := "quantile(%.2f)(Duration) AS Q" err = e.ChOrm.Model(&models.TracesURL{}).Select(fmt.Sprintf(fieldsTmpl, c.Percentile)).Scopes(scope).Pluck("Q", &percentile).Error if err != nil { return errors.Wrap(err, "获取时延分位数失败") } pcond := func(db *gorm.DB) *gorm.DB { db.Where("Duration>?", percentile) return db } if err = e.ChOrm.Debug().Model(&models.TracesURL{}). // anyLast取最后遇到的值,结果可能不确定,在AppName频繁变化的情况下,可能会取到不同的值 Select("anyLast(AppName) AS Source, CONCAT('Name:', Name) AS Target, COUNT() AS Value"). Scopes(scope, pcond). Group("Target"). Find(&links1).Error; err != nil { return errors.Wrap(err, "获取links数据失败1") } links2 := []dto.SankeyLink{} if err = e.ChOrm.Model(&model). Select("CONCAT('Name:', Name) AS Source, CONCAT('Route:', Route) AS Target, COUNT() AS Value"). Scopes(scope, pcond). Group("Source, Target"). Find(&links2).Error; err != nil { return errors.Wrap(err, "获取links数据失败2") } // links3 := []dto.SankeyLink{} // t1 := e.ChOrm.Model(&model). // Select("CONCAT('Route:', Route) AS Source, CONCAT('Target:', Target) AS Target, COUNT() AS Value"). // Scopes(scope).Group("Source, Target") // t2 := e.ChOrm.Table("(?) AS t1", t1). // Select("Source, Target, Value, ROW_NUMBER() OVER(PARTITION BY Source ORDER BY Value DESC) AS RowNum") // if err = e.ChOrm.Table("(?) AS t2", t2). // Select("Source, Target, Value"). // Where("RowNum<=?", 10). // 暂定最多只取前10条 // Find(&links3).Error; err != nil { // return errors.Wrap(err, "获取links数据失败3") // } links4mp := []map[string]any{} if err = e.ChOrm.Model(&model). // 暂定每500毫秒一个区间 // Select("CONCAT('Target:', `Target`) AS `Source`, CEIL(Duration/1e6/500)*500 AS Target2, COUNT() Value"). Select("CONCAT('Route:', `Route`) AS `Source`, CEIL(Duration/1e6/500)*500 AS Target, COUNT() Value"). Scopes(scope, pcond). // Group("Source, Target2"). // Order("Source, Target2"). Group("Source, Target"). Order("Source, Target"). Find(&links4mp).Error; err != nil { return errors.Wrap(err, "获取links数据失败4") } links4 := make([]dto.SankeyLink, len(links4mp)) var prevTarget, curTarget string var prevSource, curSource string var k int for i, link := range links4mp { curSource = link["Source"].(string) // curTarget = strconv.Itoa(int(link["Target2"].(float64))) curTarget = link["Target"].(string) target := "" if prevSource != curSource { k = 0 } if k == 0 { target = fmt.Sprintf("%sms以下", curTarget) prevTarget = curTarget } else if k > 9 { target = fmt.Sprintf("%sms以上", prevTarget) } else { target = fmt.Sprintf("%s~%sms", prevTarget, curTarget) prevTarget = curTarget } prevSource = curSource k++ cur := dto.SankeyLink{ Source: link["Source"].(string), Target: target, Value: int(link["Value"].(uint64)), } links4[i] = cur } // links := make([]dto.SankeyLink, 0, len(links1)+len(links2)+len(links3)+len(links4)) links := make([]dto.SankeyLink, 0, len(links1)+len(links2)+len(links4)) links = append(links, links1...) links = append(links, links2...) // links = append(links, links3...) links = append(links, links4...) if len(links) == 0 { result.Links = []dto.SankeyLink{} result.Nodes = []dto.SankeyNode{} return nil } result.Links = links nodeNameMap := map[string]struct{}{ links[0].Source: {}, } for _, link := range links { nodeNameMap[link.Target] = struct{}{} } nodes := make([]dto.SankeyNode, 0, len(nodeNameMap)) for name := range nodeNameMap { nodes = append(nodes, dto.SankeyNode{ Name: name, }) } result.Nodes = nodes return nil } func (s *UrlMapping) QualityFromClickhouse(req dto.UrlMappingQualityReq, resp *dto.UrlMappingQualityResp) error { req.CheckFilling(time.Hour) type BucketCount struct { Bucket string Total int64 } appAlias := "" if err := s.Orm.Model(&models.App{}).Where("id", req.AppId).Pluck("alias", &appAlias).Error; err != nil { return errors.Wrap(err, "获取应用别名失败") } genBucketCounts := func(start, end int64) ([]BucketCount, error) { bucketCounts := []BucketCount{} db := s.ChOrm.Model(&models.TracesURL{}). Where("AppAlias=?", appAlias). Where("Timestamp>toDateTime(?) and TimestamptoDateTime(?) and TimestamptoDateTime(?) and Timestamp=400").Group("StartTime").Order("StartTime asc").Find(&list).Error; err != nil { return errors.Wrap(err, "查询错误分布数据失败") } resp.Times = make([]string, len(list)) resp.Totals = make([]int64, len(list)) for i, item := range list { resp.Times[i] = item.StartTime resp.Totals[i] = item.Total } return nil } // 接口错误相关统计 func (s *UrlMapping) Errors(req dto.UrlMappingErrorsReq, resp *dto.UrlMappingErrorsResp) error { labels := map[string]string{ "app_id": req.AppId, "error": "true", } if req.UrlType != "" { labels["url_type"] = req.UrlType } mins := int64(0) unit := "m" if req.TimeType == "day" { mins = int64(req.TimeValue) * 24 * 60 unit = "d" } else if req.TimeType == "hour" { unit = "h" mins = int64(req.TimeValue) * 60 } else { return errors.New("无效的time type") } name := "observe_server_duration_milliseconds" hist := prometheus.NewHistogram(nil, name, labels, time.Unix(int64(req.TimeBase), 0), mins) times := []time.Time{} totals := []float64{} err := hist.TotalUnits(unit, ×, &totals) if err != nil { return err } resp.Times = make([]string, len(times)) resp.Totals = make([]int64, len(totals)) for i, t := range times { resp.Times[i] = t.Format(time.DateOnly) if req.TimeType == "hour" { resp.Times[i] = t.Format("2006-01-02 15:00:00") } resp.Totals[i] = int64(totals[i]) } return nil } func (s *UrlMapping) SlowTop(req *dto.UrlMappingSlowTopReq, resp *[]dto.UrlMappingSlowTopResp) error { req.CheckFilling(time.Hour) if req.EndTime-req.StartTime > 3600 { req.StartTime = req.EndTime - 3600 } app := models.App{} if err := s.Orm.Model(&models.App{}).Where("id", req.AppId).Find(&app).Error; err != nil { return errors.Wrap(err, "appid非法") } if err := s.ChOrm.Model(&models.TracesURL{}). Select("AppAlias, ServiceName, IF(Route!='', Route, Path) AS Route, Method, SUM(IF(StatusCode>=400, 1, 0))/COUNT() ErrorRate, AVG(Duration)/1e6 Duration"). Where("AppAlias", app.Alias). Where("Timestamp>=toDateTime(?) AND Timestamp= 3600 { timeField = "formatDateTime(toStartOfFiveMinutes(Timestamp), '%F %H:%i', 'PRC') as StartTime" } fields := []string{ timeField, "quantile(0.5)(Duration)/1e6 as P50Duration", "quantile(0.90)(Duration)/1e6 as P90Duration", "quantile(0.99)(Duration)/1e6 as P99Duration", } quantiles := []struct { StartTime string P50Duration float64 P90Duration float64 P99Duration float64 }{} if err := s.ChOrm.Model(&models.TracesURL{}).Select(fields). Where("Route", item.Route). Where("Method", item.Method). Where("ServiceName", item.ServiceName). Where("AppAlias", item.AppAlias). Where("Timestamp>toDateTime(?) and Timestamp=400, 1, 0)) as ErrorNum, quantile(0.5)(Duration)/1e6 as MedianDuration, max(Duration)/1e6 as MaxDuration"). // Where("Route like ?", um.Url+"%").Where("Method", um.Method). // Where("ServiceName", um.ServiceName). Where("Timestamp>=? and Timestamp 0 { db.Where("AppAlias=? and ServiceName=?", um.AppAlias, um.ServiceName) db.Where("(Route=? OR Path=?) and Method=?", um.Url, um.Url, um.Method) } else if req.AppAlias != "" { db.Where("AppAlias", req.AppAlias) } if err := db.Find(&digit).Error; err != nil { return errors.Wrap(err, "查询数字视图数据失败") } if math.IsNaN(digit.MedianDuration) { digit.MedianDuration = 0 } resp.Total = digit.Total if digit.Total > 0 { resp.ErrorRate = float64(digit.ErrorNum) / float64(digit.Total) } mins := float64(req.EndTime-req.StartTime) / 60 resp.QPM = math.Round(float64(resp.Total)/mins*100) / 100 resp.MedianDuration = math.Round(digit.MedianDuration*100) / 100 resp.MaxDuration = math.Round(digit.MaxDuration*100) / 100 // wg := sync.WaitGroup{} // metric := "observe_server_duration_milliseconds" // labels := map[string]string{ // "url_level": strconv.Itoa(int(um.Level)), // "url_method": um.Method, // "url_prefix": um.Url, // "service_name": um.ServiceName, // "app_alias": um.AppAlias, // } // ts := time.Unix(req.EndTime, 0) // mins := time.Unix(req.EndTime, 0).Sub(time.Unix(req.StartTime, 0)).Minutes() // hist := prometheus.NewHistogram(&wg, metric, labels, ts, int64(mins)) // var errRate float64 // wg.Add(4) // go hist.Total(&resp.Total) // go hist.ErrorRate(&errRate) // go hist.Quantile(0.90, &resp.MaxDuration) // go hist.Quantile(0.5, &resp.MedianDuration) // wg.Wait() // resp.ErrorRate = fmt.Sprintf("%.2f%%", errRate*float64(100)) // resp.QPM = math.Round(float64(resp.Total)/mins*100) / 100 // resp.MedianDuration = math.Round(resp.MedianDuration*100) / 100 // resp.MaxDuration = math.Round(resp.MaxDuration*100) / 100 return nil } func (s *UrlMapping) SubList(req *dto.UrlMappingSubListReq, resp *[]dto.UrlMappingSubListResp, count *int64) error { req.CheckFilling(time.Hour) // um := models.UrlMapping{} // if err := s.Orm.Where("id", req.ID).Find(&um).Error; err != nil { // return errors.Wrap(err, "获取url mapping信息失败") // } list := []models.UrlMapping{} db := s.Orm.Select("id, name, url, method, service_name, app_alias, level, favor, module, summary, type"). Where("app_alias=?", req.AppAlias). // Where("url like ? AND is_perfect_match=?", um.Url+"%", 1) Where("is_perfect_match=?", 1). Where("url!=?", "") var serviceNames []string if req.ServiceName != "" { serviceNames = strings.Split(req.ServiceName, ",") if len(serviceNames) > 0 { db.Where("service_name in ?", serviceNames) } } urlMappingIds := []string{} if req.BizHash != "" { err := s.Orm.Model(&models.BizEdge{}). Joins("inner join ot_biz_node on ot_biz_node.id=ot_biz_edge.source or ot_biz_node.id=ot_biz_edge.target"). Where("ot_biz_edge.biz_hash=? and external_id>0", req.BizHash). Pluck("external_id", &urlMappingIds).Error if err != nil { return errors.Wrap(err, "获取extend id失败") } } if len(urlMappingIds) > 0 { db.Where("id in ?", urlMappingIds) } if req.Url != "" { db.Where("url like ?", "%"+req.Url+"%") } if err := db. Scopes(cDto.Paginate(req.GetPageSize(), req.GetPageIndex())). Find(&list).Offset(-1).Limit(-1).Count(count).Error; err != nil { return errors.Wrap(err, "获取接口映射子列表失败") } *resp = make([]dto.UrlMappingSubListResp, len(list)) svcMap, err := query.NewService().ServiceMap(req.AppAlias) if err != nil { return errors.Wrap(err, "获取service映射失败") } wg := sync.WaitGroup{} wg.Add(len(list)) for i := range list { item := list[i] (*resp)[i] = dto.UrlMappingSubListResp{ ID: item.ID, Name: item.Name, Route: item.Url, Method: item.Method, ServiceName: item.ServiceName, ServiceNameCN: svcMap[item.ServiceName], AppAlias: item.AppAlias, Favor: int(item.Favor), Module: item.Module, Summary: item.Summary, } if config.ExtConfig.ClickhouseMetrics { go s.subStatsFromClickhouse(&wg, req, &(*resp)[i]) } else { go s.SubStats(&wg, &item, req, &(*resp)[i]) } } wg.Wait() return nil } func (s *UrlMapping) subStatsFromClickhouse(wg *sync.WaitGroup, req *dto.UrlMappingSubListReq, item *dto.UrlMappingSubListResp) { defer wg.Done() db := s.ChOrm.Model(&models.TracesURL{}). Where("AppAlias=? and ServiceName=? and Method=? and Route=?", item.AppAlias, item.ServiceName, item.Method, item.Route). Where("Timestamp>toDateTime(?) and Timestamp=400, 1, 0)) as ErrorNum", "quantile(0.5)(Duration) as P50Duration", "quantile(0.90)(Duration) as P90Duration", "quantile(0.99)(Duration) as P99Duration", } row := struct { Total int64 MaxDuration int64 ErrorNum int64 AvgDuration float64 P50Duration float64 P90Duration float64 P99Duration float64 }{} err := db.Select(fields).Find(&row).Error if err != nil { return } if row.Total == 0 { item.DurationStats = dto.DurationStats{ Time: []string{}, P50: []float64{}, P90: []float64{}, P99: []float64{}, } return } timeField := "formatDateTime(toStartOfMinute(Timestamp), '%F %H:%i', 'PRC') as StartTime" if req.EndTime-req.StartTime >= 60*60 { timeField = "formatDateTime(toStartOfFiveMinutes(Timestamp), '%F %H:%i', 'PRC') as StartTime" } fields = []string{ timeField, "quantile(0.5)(Duration) as P50Duration", "quantile(0.90)(Duration) as P90Duration", "quantile(0.99)(Duration) as P99Duration", } quantiles := []struct { StartTime string P50Duration float64 P90Duration float64 P99Duration float64 }{} if err := db.Select(fields).Group(timeField).Order(fmt.Sprintf("%s asc", timeField)).Find(&quantiles).Error; err != nil { return } item.Total = row.Total item.DurationStats = dto.DurationStats{ Time: make([]string, len(quantiles)), P50: make([]float64, len(quantiles)), P90: make([]float64, len(quantiles)), P99: make([]float64, len(quantiles)), } for i, quantile := range quantiles { item.DurationStats.Time[i] = quantile.StartTime item.DurationStats.P50[i] = math.Round(quantile.P50Duration/1e6*100) / 100 item.DurationStats.P90[i] = math.Round(quantile.P90Duration/1e6*100) / 100 item.DurationStats.P99[i] = math.Round(quantile.P99Duration/1e6*100) / 100 } } func (s *UrlMapping) SubStats(wg *sync.WaitGroup, um *models.UrlMapping, req *dto.UrlMappingSubListReq, resp *dto.UrlMappingSubListResp) error { defer wg.Done() wg2 := sync.WaitGroup{} metric := "observe_server_duration_milliseconds" labels := map[string]string{ "url_level": strconv.Itoa(int(um.Level)), "url_is_perfect_match": "1", "url_method": um.Method, "url_prefix": um.Url, "service_name": um.ServiceName, "app_alias": um.AppAlias, } ts := time.Unix(req.EndTime, 0) mins := time.Unix(req.EndTime, 0).Sub(time.Unix(req.StartTime, 0)).Minutes() hist := prometheus.NewHistogram(&wg2, metric, labels, ts, int64(mins)) wg2.Add(5) var errorRate float64 go hist.Total(&resp.Total) go hist.ErrorRate(&errorRate) times, p50s, p90s, p99s := []string{}, []float64{}, []float64{}, []float64{} go hist.QuantileMinutes(0.5, &[]string{}, &p50s) go hist.QuantileMinutes(0.9, &[]string{}, &p90s) go hist.QuantileMinutes(0.99, ×, &p99s) wg2.Wait() resp.ErrorRate = errorRate resp.DurationStats = dto.DurationStats{ Time: times, P50: p50s, P90: p90s, P99: p99s, } return nil } func (s *UrlMapping) ChartQPS(req *dto.UrlMappingChartReq, resp *dto.UrlMappingChartResp) error { wg := sync.WaitGroup{} hist, timeUnit, timeList := s.chartCommon(req, &wg) group := "" if req.Type == "status_trend" { group = "status_code" } times, values := map[string][]string{}, map[string][]float64{} wg.Add(1) go hist.QPS(timeUnit, group, ×, &values) wg.Wait() resp.Times = timeList if group == "status_code" { resp.Values = map[string][]float64{ "2XX": make([]float64, len(timeList)), "3XX": make([]float64, len(timeList)), "4XX": make([]float64, len(timeList)), "5XX": make([]float64, len(timeList)), } for key := range times { if key < "200" { continue } k := fmt.Sprintf("%cXX", key[0]) for i, j := 0, 0; j < len(times[key]) && i < len(timeList); i++ { if timeList[i] == times[key][j] { resp.Values[k][i] += math.Round(values[key][j]*60*100) / 100 // *60是为了将QPS转为数量 j++ } // else 此时 times[key]中缺少某一时间 } } } else { key := "" resp.Values = map[string][]float64{ key: make([]float64, len(timeList)), } for i, j := 0, 0; j < len(times[key]) && i < len(timeList); i++ { if timeList[i] == times[key][j] { resp.Values[key][i] += values[key][j] j++ } // else 此时 times[key]中缺少某一时间 } } return nil } func (s *UrlMapping) ChartQPSFromClickhouse(req *dto.UrlMappingChartReq, resp *dto.UrlMappingChartResp) error { req.CheckFilling(time.Minute * 5) timeField := "formatDateTime(toStartOfMinute(Timestamp), '%F %H:%i', 'PRC') as StartTime" if req.EndTime-req.StartTime >= 60*60 { timeField = "formatDateTime(toStartOfFiveMinutes(Timestamp), '%F %H:%i', 'PRC') as StartTime" } db := s.ChOrm.Model(&models.TracesURL{}). Where("AppAlias", req.AppAlias). Where("ServiceName", req.ServiceName). Where("Method", req.Method). Where("Route=? or Path=?", req.Route, req.Route). Where("Timestamp>toDateTime(?) and Timestamp=500, '5XX', if(StatusCode>=400, '4XX', if(StatusCode>=300, '3XX', '2XX'))) StatusCodes, count() Total", timeField) if err := db.Select(fields). Group("StartTime, StatusCodes").Order("StartTime asc, StatusCodes asc").Find(&list).Error; err != nil { return errors.Wrap(err, "获取QPS相关数据失败") } resp.Times = []string{} resp.Values = map[string][]float64{ "2XX": {}, "3XX": {}, "4XX": {}, "5XX": {}, } for _, item := range list { if len(resp.Times) > 0 { if item.StartTime != resp.Times[len(resp.Times)-1] { resp.Times = append(resp.Times, item.StartTime) } } else { resp.Times = append(resp.Times, item.StartTime) } resp.Values[item.StatusCodes] = append(resp.Values[item.StatusCodes], float64(item.Total)) } } else { list := []struct { StartTime string Total int64 }{} if err := db.Select(fmt.Sprintf("%s, count() Total", timeField)). Group("StartTime").Order("StartTime asc").Find(&list).Error; err != nil { return errors.Wrap(err, "获取QPS相关数据失败") } resp.Times = []string{} resp.Values = map[string][]float64{ "": {}, } for _, item := range list { resp.Times = append(resp.Times, item.StartTime) resp.Values[""] = append(resp.Values[""], float64(item.Total)) } } return nil } func (s *UrlMapping) ChartLatencyFromClickhouse(req *dto.UrlMappingChartReq, resp *dto.UrlMappingChartResp) error { req.CheckFilling(time.Minute * 5) timeField := "formatDateTime(toStartOfMinute(Timestamp), '%F %H:%i', 'PRC') as StartTime" if req.EndTime-req.StartTime >= 60*60 { timeField = "formatDateTime(toStartOfFiveMinutes(Timestamp), '%F %H:%i', 'PRC') as StartTime" } fields := []string{ timeField, "quantile(0.5)(Duration)/1e6 as P50Duration", "quantile(0.90)(Duration)/1e6 as P90Duration", "quantile(0.99)(Duration)/1e6 as P99Duration", } quantiles := []struct { StartTime string P50Duration float64 P90Duration float64 P99Duration float64 }{} if err := s.ChOrm.Model(&models.TracesURL{}).Select(fields). Where("AppAlias", req.AppAlias). Where("ServiceName", req.ServiceName). Where("Method", req.Method). Where("Route=? or Path=?", req.Route, req.Route). Where("Timestamp>toDateTime(?) and Timestamp