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¶m=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 }