github.com/GuanceCloud/cliutils@v1.1.21/dialtesting/tcp.go (about)

     1  // Unless explicitly stated otherwise all files in this repository are licensed
     2  // under the MIT License.
     3  // This product includes software developed at Guance Cloud (https://www.guance.com/).
     4  // Copyright 2021-present Guance, Inc.
     5  
     6  package dialtesting
     7  
     8  import (
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"net"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/GuanceCloud/cliutils"
    17  )
    18  
    19  const defaultTCPTimeout = 30 * time.Second
    20  
    21  type TCPResponseTime struct {
    22  	IsContainDNS bool   `json:"is_contain_dns"`
    23  	Target       string `json:"target"`
    24  
    25  	targetTime time.Duration
    26  }
    27  
    28  type TCPSuccess struct {
    29  	ResponseTime    []*TCPResponseTime `json:"response_time,omitempty"`
    30  	Hops            []*ValueSuccess    `json:"hops,omitempty"`
    31  	ResponseMessage []*SuccessOption   `json:"response_message,omitempty"`
    32  }
    33  
    34  type TCPTask struct {
    35  	Host              string            `json:"host"`
    36  	Port              string            `json:"port"`
    37  	Message           string            `json:"message"`
    38  	Timeout           string            `json:"timeout"`
    39  	EnableTraceroute  bool              `json:"enable_traceroute"`
    40  	TracerouteConfig  *TracerouteOption `json:"traceroute_config"`
    41  	SuccessWhen       []*TCPSuccess     `json:"success_when"`
    42  	SuccessWhenLogic  string            `json:"success_when_logic"`
    43  	ExternalID        string            `json:"external_id"`
    44  	Name              string            `json:"name"`
    45  	AK                string            `json:"access_key"`
    46  	PostURL           string            `json:"post_url"`
    47  	CurStatus         string            `json:"status"`
    48  	Frequency         string            `json:"frequency"`
    49  	Region            string            `json:"region"`
    50  	OwnerExternalID   string            `json:"owner_external_id"`
    51  	Tags              map[string]string `json:"tags,omitempty"`
    52  	Labels            []string          `json:"labels,omitempty"`
    53  	UpdateTime        int64             `json:"update_time,omitempty"`
    54  	WorkspaceLanguage string            `json:"workspace_language,omitempty"`
    55  	TagsInfo          string            `json:"tags_info,omitempty"`
    56  
    57  	reqCost         time.Duration
    58  	reqDNSCost      time.Duration
    59  	reqError        string
    60  	destIP          string
    61  	responseMessage string
    62  	timeout         time.Duration
    63  	ticker          *time.Ticker
    64  	traceroute      []*Route
    65  }
    66  
    67  func (t *TCPTask) InitDebug() error {
    68  	return t.init(true)
    69  }
    70  
    71  func (t *TCPTask) init(debug bool) error {
    72  	if len(t.Timeout) == 0 {
    73  		t.timeout = 10 * time.Second
    74  	} else {
    75  		if timeout, err := time.ParseDuration(t.Timeout); err != nil {
    76  			return err
    77  		} else {
    78  			t.timeout = timeout
    79  		}
    80  	}
    81  
    82  	if !debug {
    83  		du, err := time.ParseDuration(t.Frequency)
    84  		if err != nil {
    85  			return err
    86  		}
    87  		if t.ticker != nil {
    88  			t.ticker.Stop()
    89  		}
    90  		t.ticker = time.NewTicker(du)
    91  	}
    92  
    93  	if strings.EqualFold(t.CurStatus, StatusStop) {
    94  		return nil
    95  	}
    96  
    97  	if len(t.SuccessWhen) == 0 {
    98  		return fmt.Errorf(`no any check rule`)
    99  	}
   100  
   101  	for _, checker := range t.SuccessWhen {
   102  		if checker.ResponseTime != nil {
   103  			for _, v := range checker.ResponseTime {
   104  				du, err := time.ParseDuration(v.Target)
   105  				if err != nil {
   106  					return err
   107  				}
   108  				v.targetTime = du
   109  			}
   110  		}
   111  
   112  		// if [checker.Hops] is not nil, set traceroute to be true
   113  		if checker.Hops != nil {
   114  			t.EnableTraceroute = true
   115  		}
   116  
   117  		for _, v := range checker.ResponseMessage {
   118  			err := genReg(v)
   119  			if err != nil {
   120  				return err
   121  			}
   122  		}
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  func (t *TCPTask) Init() error {
   129  	return t.init(false)
   130  }
   131  
   132  func (t *TCPTask) Check() error {
   133  	if t.ExternalID == "" {
   134  		return fmt.Errorf("external ID missing")
   135  	}
   136  
   137  	if len(t.Host) == 0 {
   138  		return fmt.Errorf("host should not be empty")
   139  	}
   140  
   141  	if len(t.Port) == 0 {
   142  		return fmt.Errorf("port should not be empty")
   143  	}
   144  
   145  	return t.Init()
   146  }
   147  
   148  func (t *TCPTask) CheckResult() (reasons []string, succFlag bool) {
   149  	for _, chk := range t.SuccessWhen {
   150  		// check response time
   151  		if chk.ResponseTime != nil {
   152  			for _, v := range chk.ResponseTime {
   153  				reqCost := t.reqCost
   154  
   155  				if v.IsContainDNS {
   156  					reqCost += t.reqDNSCost
   157  				}
   158  
   159  				if reqCost >= v.targetTime {
   160  					reasons = append(reasons,
   161  						fmt.Sprintf("TCP response time(%v) larger equal than %v", reqCost, v.targetTime))
   162  				} else if v.targetTime > 0 {
   163  					succFlag = true
   164  				}
   165  			}
   166  		}
   167  
   168  		// check message
   169  		if chk.ResponseMessage != nil {
   170  			for _, v := range chk.ResponseMessage {
   171  				if err := v.check(t.responseMessage, "response message"); err != nil {
   172  					reasons = append(reasons, err.Error())
   173  				} else {
   174  					succFlag = true
   175  				}
   176  			}
   177  		}
   178  
   179  		// check traceroute
   180  		if t.EnableTraceroute {
   181  			hops := float64(len(t.traceroute))
   182  
   183  			if hops == 0 {
   184  				reasons = append(reasons, "traceroute failed with no hops")
   185  			} else {
   186  				for _, v := range chk.Hops {
   187  					if err := v.check(hops); err != nil {
   188  						reasons = append(reasons, fmt.Sprintf("traceroute hops check failed: %s", err.Error()))
   189  					} else {
   190  						succFlag = true
   191  					}
   192  				}
   193  			}
   194  		}
   195  	}
   196  
   197  	return reasons, succFlag
   198  }
   199  
   200  func (t *TCPTask) GetResults() (tags map[string]string, fields map[string]interface{}) {
   201  	tags = map[string]string{
   202  		"name":      t.Name,
   203  		"dest_host": t.Host,
   204  		"dest_port": t.Port,
   205  		"dest_ip":   t.destIP,
   206  		"status":    "FAIL",
   207  		"proto":     "tcp",
   208  	}
   209  
   210  	responseTime := int64(t.reqCost) / 1000                     // us
   211  	responseTimeWithDNS := int64(t.reqCost+t.reqDNSCost) / 1000 // us
   212  
   213  	fields = map[string]interface{}{
   214  		"response_time":          responseTime,
   215  		"response_time_with_dns": responseTimeWithDNS,
   216  		"success":                int64(-1),
   217  	}
   218  
   219  	if t.responseMessage != "" {
   220  		fields["response_message"] = t.responseMessage
   221  	}
   222  
   223  	if t.EnableTraceroute {
   224  		fields["hops"] = 0
   225  		if t.traceroute == nil {
   226  			fields["traceroute"] = "[]"
   227  		} else {
   228  			tracerouteData, err := json.Marshal(t.traceroute)
   229  			if err == nil && len(tracerouteData) > 0 {
   230  				fields["traceroute"] = string(tracerouteData)
   231  				fields["hops"] = len(t.traceroute)
   232  			} else {
   233  				fields["traceroute"] = "[]"
   234  			}
   235  		}
   236  	}
   237  
   238  	for k, v := range t.Tags {
   239  		tags[k] = v
   240  	}
   241  
   242  	message := map[string]interface{}{}
   243  
   244  	reasons, succFlag := t.CheckResult()
   245  	if t.reqError != "" {
   246  		reasons = append(reasons, t.reqError)
   247  	}
   248  
   249  	switch t.SuccessWhenLogic {
   250  	case "or":
   251  		if succFlag && t.reqError == "" {
   252  			tags["status"] = "OK"
   253  			fields["success"] = int64(1)
   254  			message["response_time"] = responseTime
   255  		} else {
   256  			message[`fail_reason`] = strings.Join(reasons, `;`)
   257  			fields[`fail_reason`] = strings.Join(reasons, `;`)
   258  		}
   259  	default:
   260  		if len(reasons) != 0 {
   261  			message[`fail_reason`] = strings.Join(reasons, `;`)
   262  			fields[`fail_reason`] = strings.Join(reasons, `;`)
   263  		} else {
   264  			message["response_time"] = responseTime
   265  		}
   266  
   267  		if t.reqError == "" && len(reasons) == 0 {
   268  			tags["status"] = "OK"
   269  			fields["success"] = int64(1)
   270  		}
   271  	}
   272  
   273  	data, err := json.Marshal(message)
   274  	if err != nil {
   275  		fields[`message`] = err.Error()
   276  	}
   277  
   278  	if len(data) > MaxMsgSize {
   279  		fields[`message`] = string(data[:MaxMsgSize])
   280  	} else {
   281  		fields[`message`] = string(data)
   282  	}
   283  
   284  	return tags, fields
   285  }
   286  
   287  func (t *TCPTask) MetricName() string {
   288  	return `tcp_dial_testing`
   289  }
   290  
   291  func (t *TCPTask) Clear() {
   292  	t.reqCost = 0
   293  	t.reqError = ""
   294  	t.traceroute = nil
   295  }
   296  
   297  func (t *TCPTask) Run() error {
   298  	t.Clear()
   299  
   300  	var d net.Dialer
   301  	ctx, cancel := context.WithTimeout(context.Background(), t.timeout)
   302  	defer cancel()
   303  
   304  	hostIP := net.ParseIP(t.Host)
   305  
   306  	if hostIP == nil { // host name
   307  		start := time.Now()
   308  		if ips, err := net.LookupIP(t.Host); err != nil {
   309  			t.reqError = err.Error()
   310  			return err
   311  		} else {
   312  			if len(ips) == 0 {
   313  				err := fmt.Errorf("invalid host: %s, found no ip record", t.Host)
   314  				t.reqError = err.Error()
   315  				return err
   316  			} else {
   317  				t.reqDNSCost = time.Since(start)
   318  				hostIP = ips[0] // TODO: support mutiple ip for one host
   319  			}
   320  		}
   321  	}
   322  
   323  	t.destIP = hostIP.String()
   324  	tcpIPAddr := net.JoinHostPort(hostIP.String(), t.Port)
   325  
   326  	start := time.Now()
   327  	conn, err := d.DialContext(ctx, "tcp", tcpIPAddr)
   328  
   329  	if err != nil {
   330  		t.reqError = err.Error()
   331  		t.reqDNSCost = 0
   332  	} else {
   333  		t.reqCost = time.Since(start)
   334  		if t.Message != "" { // send message and get response
   335  			if err := conn.SetDeadline(time.Now().Add(defaultTCPTimeout)); err != nil {
   336  				t.reqError = err.Error()
   337  			} else if _, err := conn.Write([]byte(t.Message)); err != nil {
   338  				t.reqError = err.Error()
   339  			} else {
   340  				buf := make([]byte, 1024)
   341  				if n, err := conn.Read(buf); err != nil {
   342  					t.reqError = err.Error()
   343  				} else {
   344  					t.responseMessage = string(buf[:n])
   345  				}
   346  			}
   347  		}
   348  
   349  		_ = conn.Close() //nolint:errcheck
   350  	}
   351  
   352  	if t.EnableTraceroute {
   353  		routes, err := TracerouteIP(hostIP.String(), t.TracerouteConfig)
   354  		if err != nil {
   355  			t.reqError = err.Error()
   356  		} else {
   357  			t.traceroute = routes
   358  		}
   359  	}
   360  
   361  	return nil
   362  }
   363  
   364  func (t *TCPTask) Stop() error {
   365  	return nil
   366  }
   367  
   368  func (t *TCPTask) UpdateTimeUs() int64 {
   369  	return t.UpdateTime
   370  }
   371  
   372  func (t *TCPTask) ID() string {
   373  	if t.ExternalID == `` {
   374  		return cliutils.XID("dtst_")
   375  	}
   376  	return fmt.Sprintf("%s_%s", t.AK, t.ExternalID)
   377  }
   378  
   379  func (t *TCPTask) GetOwnerExternalID() string {
   380  	return t.OwnerExternalID
   381  }
   382  
   383  func (t *TCPTask) SetOwnerExternalID(exid string) {
   384  	t.OwnerExternalID = exid
   385  }
   386  
   387  func (t *TCPTask) SetRegionID(regionID string) {
   388  	t.Region = regionID
   389  }
   390  
   391  func (t *TCPTask) SetAk(ak string) {
   392  	t.AK = ak
   393  }
   394  
   395  func (t *TCPTask) SetStatus(status string) {
   396  	t.CurStatus = status
   397  }
   398  
   399  func (t *TCPTask) SetUpdateTime(ts int64) {
   400  	t.UpdateTime = ts
   401  }
   402  
   403  func (t *TCPTask) Status() string {
   404  	return t.CurStatus
   405  }
   406  
   407  func (t *TCPTask) Ticker() *time.Ticker {
   408  	return t.ticker
   409  }
   410  
   411  func (t *TCPTask) Class() string {
   412  	return ClassTCP
   413  }
   414  
   415  func (t *TCPTask) GetFrequency() string {
   416  	return t.Frequency
   417  }
   418  
   419  func (t *TCPTask) GetLineData() string {
   420  	return ""
   421  }
   422  
   423  func (t *TCPTask) RegionName() string {
   424  	return t.Region
   425  }
   426  
   427  func (t *TCPTask) PostURLStr() string {
   428  	return t.PostURL
   429  }
   430  
   431  func (t *TCPTask) AccessKey() string {
   432  	return t.AK
   433  }
   434  
   435  func (t *TCPTask) GetHostName() (string, error) {
   436  	return t.Host, nil
   437  }
   438  
   439  func (t *TCPTask) GetWorkspaceLanguage() string {
   440  	if t.WorkspaceLanguage == "en" {
   441  		return "en"
   442  	}
   443  	return "zh"
   444  }
   445  
   446  func (t *TCPTask) GetTagsInfo() string {
   447  	return t.TagsInfo
   448  }