Files
iotlcm/server/internal/logic/app/app_diagnosis_traffic.go
T

283 lines
8.9 KiB
Go

package app
import (
"context"
"database/sql"
"fmt"
"hash/fnv"
"regexp"
"strings"
"time"
"unicode"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
_ "github.com/taosdata/driver-go/v3/taosRestful"
_ "github.com/taosdata/driver-go/v3/taosWS"
)
const (
appDiagnosisDailyStable = "t_sim_card_usage_daily"
appDiagnosisMonthlyStable = "t_sim_card_usage_monthly"
)
var appDiagnosisIdentifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
type appDiagnosisTrafficConfig struct {
Enabled bool
DriverName string
Dsn string
DbName string
DailyDbName string
MonthlyDbName string
MaxOpenConns int
MaxIdleConns int
}
func (cfg appDiagnosisTrafficConfig) databaseFor(stable string) string {
switch stable {
case appDiagnosisDailyStable:
if cfg.DailyDbName != "" {
return cfg.DailyDbName
}
case appDiagnosisMonthlyStable:
if cfg.MonthlyDbName != "" {
return cfg.MonthlyDbName
}
}
return cfg.DbName
}
type appDiagnosisTrafficRecord struct {
Ts time.Time
UsageMb float64
PackageCode string
Iccid string
}
func (s *sAppDiagnosis) queryDiagnosisDailyFlow(ctx context.Context, iccid string, days int) (*model.AppDiagnosisFlowModel, error) {
if days <= 0 {
days = 7
}
now := time.Now()
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -(days - 1))
endTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
records, err := appDiagnosisQueryTrafficRows(ctx, iccid, appDiagnosisDailyStable, startTime, endTime)
if err != nil {
g.Log().Errorf(ctx, "app diagnosis daily traffic query failed, iccid:%s, days:%d, err:%+v", appDiagnosisMaskIdentifier(iccid), days, err)
return nil, gerror.New("流量趋势服务暂时不可用,请稍后重试")
}
usageMap := make(map[string]float64, len(records))
for _, item := range records {
usageMap[item.Ts.Format("2006-01-02")] += item.UsageMb
}
res := &model.AppDiagnosisFlowModel{
Times: make([]string, 0, days),
Flow: make([]float64, 0, days),
FlowAvailable: make([]bool, 0, days),
Unit: "MB",
Granularity: "day",
HasData: len(records) > 0,
}
for i := 0; i < days; i++ {
current := startTime.AddDate(0, 0, i)
res.Times = append(res.Times, current.Format("01-02"))
usage, ok := usageMap[current.Format("2006-01-02")]
res.Flow = append(res.Flow, usage)
res.FlowAvailable = append(res.FlowAvailable, ok)
}
return res, nil
}
func (s *sAppDiagnosis) queryDiagnosisMonthlyFlow(ctx context.Context, iccid string, months int) (*model.AppDiagnosisFlowModel, error) {
if months <= 0 {
months = 6
}
now := time.Now()
currentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
startMonth := currentMonth.AddDate(0, -(months - 1), 0)
endTime := time.Date(now.Year(), now.Month()+1, 0, 23, 59, 59, 0, now.Location())
records, err := appDiagnosisQueryTrafficRows(ctx, iccid, appDiagnosisMonthlyStable, startMonth, endTime)
if err != nil {
g.Log().Errorf(ctx, "app diagnosis monthly traffic query failed, iccid:%s, months:%d, err:%+v", appDiagnosisMaskIdentifier(iccid), months, err)
return nil, gerror.New("流量趋势服务暂时不可用,请稍后重试")
}
usageMap := make(map[string]float64, len(records))
for _, item := range records {
usageMap[item.Ts.Format("2006-01")] += item.UsageMb
}
res := &model.AppDiagnosisFlowModel{
Times: make([]string, 0, months),
Flow: make([]float64, 0, months),
FlowAvailable: make([]bool, 0, months),
Unit: "MB",
Granularity: "month",
HasData: len(records) > 0,
}
for i := 0; i < months; i++ {
current := startMonth.AddDate(0, i, 0)
key := current.Format("2006-01")
res.Times = append(res.Times, key)
usage, ok := usageMap[key]
res.Flow = append(res.Flow, usage)
res.FlowAvailable = append(res.FlowAvailable, ok)
}
return res, nil
}
func appDiagnosisQueryTrafficRows(ctx context.Context, iccid string, stable string, startTime, endTime time.Time) ([]appDiagnosisTrafficRecord, error) {
iccid = strings.TrimSpace(iccid)
if iccid == "" {
return nil, gerror.New("traffic usage query iccid is empty")
}
cfg := appDiagnosisGetTrafficConfig(ctx)
if !cfg.Enabled || strings.TrimSpace(cfg.Dsn) == "" {
g.Log().Debug(ctx, "app diagnosis traffic query skipped, tdengine disabled")
return []appDiagnosisTrafficRecord{}, nil
}
database := cfg.databaseFor(stable)
if err := appDiagnosisValidateTableConfig(database, stable); err != nil {
return nil, err
}
driverName := strings.TrimSpace(cfg.DriverName)
if driverName == "" {
driverName = "taosWS"
}
db, err := sql.Open(driverName, cfg.Dsn)
if err != nil {
return nil, gerror.Wrap(err, "tdengine open failed")
}
defer db.Close()
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
table := appDiagnosisQualify(database, appDiagnosisChildTrafficTableName(stable, iccid))
rows, err := db.QueryContext(ctx, fmt.Sprintf(
"SELECT ts, usage_mb, package_code, iccid FROM %s%s ORDER BY ts ASC",
table,
appDiagnosisWhereRangeSQL(startTime, endTime),
))
if err != nil {
if appDiagnosisIsTableNotExist(err) {
return []appDiagnosisTrafficRecord{}, nil
}
return nil, gerror.Wrap(err, "tdengine query traffic usage failed")
}
defer rows.Close()
list := make([]appDiagnosisTrafficRecord, 0)
for rows.Next() {
var item appDiagnosisTrafficRecord
if err = rows.Scan(&item.Ts, &item.UsageMb, &item.PackageCode, &item.Iccid); err != nil {
return nil, gerror.Wrap(err, "tdengine scan traffic usage failed")
}
list = append(list, item)
}
if err = rows.Err(); err != nil {
return nil, gerror.Wrap(err, "tdengine rows traffic usage failed")
}
return list, nil
}
func appDiagnosisGetTrafficConfig(ctx context.Context) appDiagnosisTrafficConfig {
cfg := appDiagnosisTrafficConfig{
Enabled: strings.EqualFold(g.Cfg().MustGet(ctx, "tsdb.database", "TdEngine").String(), "TdEngine"),
DriverName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.type", "taosWS").String()),
Dsn: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dsn", "").String()),
DbName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dbName", "iotlcm_daily").String()),
DailyDbName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dailyDbName", "").String()),
MonthlyDbName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.monthlyDbName", "").String()),
MaxOpenConns: g.Cfg().MustGet(ctx, "tsdb.tdengine.maxOpenConns", 100).Int(),
MaxIdleConns: g.Cfg().MustGet(ctx, "tsdb.tdengine.maxIdleConns", 10).Int(),
}
if cfg.DailyDbName == "" {
cfg.DailyDbName = cfg.DbName
}
if cfg.MonthlyDbName == "" {
cfg.MonthlyDbName = cfg.DbName
}
return cfg
}
func appDiagnosisValidateTableConfig(database string, stable string) error {
if !appDiagnosisIdentifierPattern.MatchString(database) {
return gerror.Newf("invalid tdengine database name: %s", database)
}
if !appDiagnosisIdentifierPattern.MatchString(stable) {
return gerror.Newf("invalid tdengine stable name: %s", stable)
}
return nil
}
func appDiagnosisQualify(database, table string) string {
return fmt.Sprintf("%s.%s", database, table)
}
func appDiagnosisChildTrafficTableName(stable string, iccid string) string {
prefix := "sim_card_usage_m_"
if stable == appDiagnosisDailyStable {
prefix = "sim_card_usage_d_"
}
return appDiagnosisChildTableNameWithPrefix(prefix, iccid)
}
func appDiagnosisChildTableNameWithPrefix(prefix string, value string) string {
var builder strings.Builder
builder.WriteString(prefix)
for _, r := range strings.ToLower(value) {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
builder.WriteRune(r)
}
}
name := builder.String()
if name == prefix || len(name) > 190 {
return fmt.Sprintf("%s%x", prefix, appDiagnosisHashString(value))
}
return name
}
func appDiagnosisHashString(value string) uint32 {
hash := fnv.New32a()
_, _ = hash.Write([]byte(value))
return hash.Sum32()
}
func appDiagnosisWhereRangeSQL(startTime, endTime time.Time) string {
clauses := make([]string, 0, 2)
if !startTime.IsZero() {
clauses = append(clauses, "ts >= "+appDiagnosisSQLTime(startTime))
}
if !endTime.IsZero() {
clauses = append(clauses, "ts <= "+appDiagnosisSQLTime(endTime))
}
if len(clauses) == 0 {
return ""
}
return " WHERE " + strings.Join(clauses, " AND ")
}
func appDiagnosisSQLTime(value time.Time) string {
return appDiagnosisSQLString(value.Format("2006-01-02 15:04:05.000"))
}
func appDiagnosisSQLString(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func appDiagnosisIsTableNotExist(err error) bool {
if err == nil {
return false
}
text := strings.ToLower(err.Error())
return strings.Contains(text, "table does not exist") ||
strings.Contains(text, "table not exist") ||
strings.Contains(text, "does not exist")
}