github.com/yandex/pandora@v0.5.32/components/guns/http/base.go (about)

     1  package phttp
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net"
    10  	"net/http"
    11  	"net/http/httptrace"
    12  	"net/http/httputil"
    13  	"net/url"
    14  
    15  	"github.com/pkg/errors"
    16  	"github.com/yandex/pandora/core"
    17  	"github.com/yandex/pandora/core/aggregator/netsample"
    18  	"github.com/yandex/pandora/core/clientpool"
    19  	"github.com/yandex/pandora/core/warmup"
    20  	"github.com/yandex/pandora/lib/netutil"
    21  	"go.uber.org/zap"
    22  )
    23  
    24  const (
    25  	EmptyTag = "__EMPTY__"
    26  )
    27  
    28  // AutoTagConfig configure automatic tags generation based on ammo URI. First AutoTag URI path elements becomes tag.
    29  // Example: /my/very/deep/page?id=23&param=33 -> /my/very when uri-elements: 2.
    30  type AutoTagConfig struct {
    31  	Enabled     bool `config:"enabled"`
    32  	URIElements int  `config:"uri-elements" validate:"min=1"` // URI elements used to autotagging
    33  	NoTagOnly   bool `config:"no-tag-only"`                   // When true, autotagged only ammo that has no tag before.
    34  }
    35  
    36  type AnswLogConfig struct {
    37  	Enabled bool   `config:"enabled"`
    38  	Path    string `config:"path"`
    39  	Filter  string `config:"filter" valid:"oneof=all warning error"`
    40  }
    41  
    42  type HTTPTraceConfig struct {
    43  	DumpEnabled  bool `config:"dump"`
    44  	TraceEnabled bool `config:"trace"`
    45  }
    46  
    47  func NewBaseGun(clientConstructor ClientConstructor, cfg GunConfig, answLog *zap.Logger) *BaseGun {
    48  	client := clientConstructor(cfg.Client, cfg.Target)
    49  	return &BaseGun{
    50  		Config: cfg,
    51  		OnClose: func() error {
    52  			client.CloseIdleConnections()
    53  			return nil
    54  		},
    55  		AnswLog: answLog,
    56  		Client:  client,
    57  		ClientConstructor: func() Client {
    58  			return clientConstructor(cfg.Client, cfg.Target)
    59  		},
    60  	}
    61  }
    62  
    63  type BaseGun struct {
    64  	DebugLog          bool // Automaticaly set in Bind if Log accepts debug messages.
    65  	Config            GunConfig
    66  	Connect           func(ctx context.Context) error // Optional hook.
    67  	OnClose           func() error                    // Optional. Called on Close().
    68  	Aggregator        netsample.Aggregator            // Lazy set via BindResultTo.
    69  	AnswLog           *zap.Logger
    70  	Client            Client
    71  	ClientConstructor func() Client
    72  
    73  	core.GunDeps
    74  }
    75  
    76  var _ Gun = (*BaseGun)(nil)
    77  var _ io.Closer = (*BaseGun)(nil)
    78  
    79  type SharedDeps struct {
    80  	clientPool *clientpool.Pool[Client]
    81  }
    82  
    83  func (b *BaseGun) WarmUp(opts *warmup.Options) (any, error) {
    84  	return b.createSharedDeps(opts)
    85  }
    86  
    87  func (b *BaseGun) createSharedDeps(opts *warmup.Options) (*SharedDeps, error) {
    88  	clientPool, err := b.prepareClientPool()
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	return &SharedDeps{
    93  		clientPool: clientPool,
    94  	}, nil
    95  }
    96  
    97  func (b *BaseGun) prepareClientPool() (*clientpool.Pool[Client], error) {
    98  	if !b.Config.SharedClient.Enabled {
    99  		return nil, nil
   100  	}
   101  	if b.Config.SharedClient.ClientNumber < 1 {
   102  		b.Config.SharedClient.ClientNumber = 1
   103  	}
   104  	clientPool, _ := clientpool.New[Client](b.Config.SharedClient.ClientNumber)
   105  	for i := 0; i < b.Config.SharedClient.ClientNumber; i++ {
   106  		client := b.ClientConstructor()
   107  		clientPool.Add(client)
   108  	}
   109  	return clientPool, nil
   110  }
   111  
   112  func (b *BaseGun) Bind(aggregator netsample.Aggregator, deps core.GunDeps) error {
   113  	log := deps.Log
   114  	if ent := log.Check(zap.DebugLevel, "Gun bind"); ent != nil {
   115  		// Enable debug level logging during shooting. Creating log entries isn't free.
   116  		b.DebugLog = true
   117  	}
   118  	extraDeps, ok := deps.Shared.(*SharedDeps)
   119  	if ok && extraDeps.clientPool != nil {
   120  		b.Client = extraDeps.clientPool.Next()
   121  	}
   122  
   123  	if b.Aggregator != nil {
   124  		log.Panic("already binded")
   125  	}
   126  	if aggregator == nil {
   127  		log.Panic("nil aggregator")
   128  	}
   129  	b.Aggregator = aggregator
   130  	b.GunDeps = deps
   131  
   132  	return nil
   133  }
   134  
   135  // Shoot is thread safe iff Do and Connect hooks are thread safe.
   136  func (b *BaseGun) Shoot(ammo Ammo) {
   137  	var bodyBytes []byte
   138  	if b.Aggregator == nil {
   139  		zap.L().Panic("must bind before shoot")
   140  	}
   141  	if b.Connect != nil {
   142  		err := b.Connect(b.Ctx)
   143  		if err != nil {
   144  			b.Log.Warn("Connect fail", zap.Error(err))
   145  			return
   146  		}
   147  	}
   148  
   149  	req, sample := ammo.Request()
   150  	if ammo.IsInvalid() {
   151  		sample.AddTag(EmptyTag)
   152  		sample.SetProtoCode(0)
   153  		b.Aggregator.Report(sample)
   154  		b.Log.Warn("Invalid ammo", zap.Uint64("request", ammo.ID()))
   155  		return
   156  	}
   157  
   158  	if b.Config.SSL {
   159  		req.URL.Scheme = "https"
   160  	} else {
   161  		req.URL.Scheme = "http"
   162  	}
   163  	if req.Host == "" {
   164  		req.Host = getHostWithoutPort(b.Config.Target)
   165  	}
   166  	req.URL.Host = b.Config.TargetResolved
   167  
   168  	if b.DebugLog {
   169  		b.Log.Debug("Prepared ammo to shoot", zap.Stringer("url", req.URL))
   170  	}
   171  	if b.Config.AutoTag.Enabled && (!b.Config.AutoTag.NoTagOnly || sample.Tags() == "") {
   172  		sample.AddTag(autotag(b.Config.AutoTag.URIElements, req.URL))
   173  	}
   174  	if sample.Tags() == "" {
   175  		sample.AddTag(EmptyTag)
   176  	}
   177  	if b.Config.AnswLog.Enabled {
   178  		bodyBytes = GetBody(req)
   179  	}
   180  
   181  	var err error
   182  	defer func() {
   183  		if err != nil {
   184  			sample.SetErr(err)
   185  		}
   186  		b.Aggregator.Report(sample)
   187  		err = errors.WithStack(err)
   188  	}()
   189  
   190  	var timings *TraceTimings
   191  	if b.Config.HTTPTrace.TraceEnabled {
   192  		var clientTracer *httptrace.ClientTrace
   193  		clientTracer, timings = CreateHTTPTrace()
   194  		req = req.WithContext(httptrace.WithClientTrace(req.Context(), clientTracer))
   195  	}
   196  	if b.Config.HTTPTrace.DumpEnabled {
   197  		requestDump, err := httputil.DumpRequest(req, true)
   198  		if err != nil {
   199  			b.Log.Error("DumpRequest error", zap.Error(err))
   200  		} else {
   201  			sample.SetRequestBytes(len(requestDump))
   202  		}
   203  	}
   204  	var res *http.Response
   205  	res, err = b.Client.Do(req)
   206  	if b.Config.HTTPTrace.TraceEnabled && timings != nil {
   207  		sample.SetReceiveTime(timings.GetReceiveTime())
   208  	}
   209  	if b.Config.HTTPTrace.DumpEnabled && res != nil {
   210  		responseDump, err := httputil.DumpResponse(res, true)
   211  		if err != nil {
   212  			b.Log.Error("DumpResponse error", zap.Error(err))
   213  		} else {
   214  			sample.SetResponseBytes(len(responseDump))
   215  		}
   216  	}
   217  	if b.Config.HTTPTrace.TraceEnabled && timings != nil {
   218  		sample.SetConnectTime(timings.GetConnectTime())
   219  		sample.SetSendTime(timings.GetSendTime())
   220  		sample.SetLatency(timings.GetLatency())
   221  	}
   222  
   223  	if err != nil {
   224  		b.Log.Warn("Request fail", zap.Error(err))
   225  		return
   226  	}
   227  
   228  	if b.DebugLog {
   229  		b.verboseLogging(res)
   230  	}
   231  	if b.Config.AnswLog.Enabled {
   232  		switch b.Config.AnswLog.Filter {
   233  		case "all":
   234  			b.answLogging(req, bodyBytes, res)
   235  
   236  		case "warning":
   237  			if res.StatusCode >= 400 {
   238  				b.answLogging(req, bodyBytes, res)
   239  			}
   240  
   241  		case "error":
   242  			if res.StatusCode >= 500 {
   243  				b.answLogging(req, bodyBytes, res)
   244  			}
   245  		}
   246  	}
   247  
   248  	sample.SetProtoCode(res.StatusCode)
   249  	defer res.Body.Close()
   250  	// TODO: measure body read time
   251  	_, err = io.Copy(ioutil.Discard, res.Body) // Buffers are pooled for ioutil.Discard
   252  	if err != nil {
   253  		b.Log.Warn("Body read fail", zap.Error(err))
   254  		return
   255  	}
   256  }
   257  
   258  func (b *BaseGun) Close() error {
   259  	if b.OnClose != nil {
   260  		return b.OnClose()
   261  	}
   262  	return nil
   263  }
   264  
   265  func (b *BaseGun) verboseLogging(res *http.Response) {
   266  	if res.Request.Body != nil {
   267  		reqBody, err := ioutil.ReadAll(res.Request.Body)
   268  		if err != nil {
   269  			b.Log.Debug("Body read failed for verbose logging of Request")
   270  		} else {
   271  			b.Log.Debug("Request body", zap.ByteString("Body", reqBody))
   272  		}
   273  	}
   274  	b.Log.Debug(
   275  		"Request debug info",
   276  		zap.String("URL", res.Request.URL.String()),
   277  		zap.String("Host", res.Request.Host),
   278  		zap.Any("Headers", res.Request.Header),
   279  	)
   280  
   281  	if res.Body != nil {
   282  		respBody, err := ioutil.ReadAll(res.Body)
   283  		if err != nil {
   284  			b.Log.Debug("Body read failed for verbose logging of Response")
   285  		} else {
   286  			b.Log.Debug("Response body", zap.ByteString("Body", respBody))
   287  		}
   288  	}
   289  	b.Log.Debug(
   290  		"Response debug info",
   291  		zap.Int("Status Code", res.StatusCode),
   292  		zap.String("Status", res.Status),
   293  		zap.Any("Headers", res.Header),
   294  	)
   295  }
   296  
   297  func (b *BaseGun) answLogging(req *http.Request, bodyBytes []byte, res *http.Response) {
   298  	isBody := false
   299  	if bodyBytes != nil {
   300  		req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
   301  		isBody = true
   302  	}
   303  	dump, err := httputil.DumpRequestOut(req, isBody)
   304  	if err != nil {
   305  		zap.L().Error("Error dumping request: %s", zap.Error(err))
   306  	}
   307  	msg := fmt.Sprintf("REQUEST:\n%s\n\n", string(dump))
   308  	b.AnswLog.Debug(msg)
   309  
   310  	dump, err = httputil.DumpResponse(res, true)
   311  	if err != nil {
   312  		zap.L().Error("Error dumping response: %s", zap.Error(err))
   313  	}
   314  	msg = fmt.Sprintf("RESPONSE:\n%s", string(dump))
   315  	b.AnswLog.Debug(msg)
   316  }
   317  
   318  func autotag(depth int, URL *url.URL) string {
   319  	path := URL.Path
   320  	var ind int
   321  	for ; ind < len(path); ind++ {
   322  		if path[ind] == '/' {
   323  			if depth == 0 {
   324  				break
   325  			}
   326  			depth--
   327  		}
   328  	}
   329  	return path[:ind]
   330  }
   331  
   332  func GetBody(req *http.Request) []byte {
   333  	if req.Body != nil && req.Body != http.NoBody {
   334  		bodyBytes, _ := ioutil.ReadAll(req.Body)
   335  		req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
   336  		return bodyBytes
   337  	}
   338  
   339  	return nil
   340  
   341  }
   342  
   343  // DNS resolve optimisation.
   344  // When DNSCache turned off - do nothing extra, host will be resolved on every shoot.
   345  // When using resolved target, don't use DNS caching logic - it is useless.
   346  // If we can resolve accessible target addr - use it as target, not use caching.
   347  // Otherwise just use DNS cache - we should not fail shooting, we should try to
   348  // connect on every shoot. DNS cache will save resolved addr after first successful connect.
   349  func PreResolveTargetAddr(clientConf *ClientConfig, target string) (string, error) {
   350  	if !clientConf.Dialer.DNSCache {
   351  		return target, nil
   352  	}
   353  	if endpointIsResolved(target) {
   354  		clientConf.Dialer.DNSCache = false
   355  		return target, nil
   356  	}
   357  	resolved, err := netutil.LookupReachable(target, clientConf.Dialer.Timeout)
   358  	if err != nil {
   359  		zap.L().Warn("DNS target pre resolve failed", zap.String("target", target), zap.Error(err))
   360  		return target, err
   361  	}
   362  	clientConf.Dialer.DNSCache = false
   363  	return resolved, nil
   364  }
   365  
   366  func endpointIsResolved(endpoint string) bool {
   367  	host, _, err := net.SplitHostPort(endpoint)
   368  	if err != nil {
   369  		return false
   370  	}
   371  	return net.ParseIP(host) != nil
   372  }