| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package weblog |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "net/http" |
| 9 | "runtime" |
| 10 | "strconv" |
| 11 | "strings" |
| 12 | |
| 13 | "github.com/netdata/netdata/go/plugins/pkg/stm" |
| 14 | "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi" |
| 15 | "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/logs" |
| 16 | ) |
| 17 | |
| 18 | func (c *Collector) logPanicStackIfAny() { |
| 19 | err := recover() |
| 20 | if err == nil { |
| 21 | return |
| 22 | } |
| 23 | c.Errorf("[ERROR] %s\n", err) |
| 24 | for depth := 0; ; depth++ { |
| 25 | _, file, line, ok := runtime.Caller(depth) |
| 26 | if !ok { |
| 27 | break |
| 28 | } |
| 29 | c.Errorf("======> %d: %v:%d", depth, file, line) |
| 30 | } |
| 31 | panic(err) |
| 32 | } |
| 33 | |
| 34 | func (c *Collector) collect() (map[string]int64, error) { |
| 35 | defer c.logPanicStackIfAny() |
| 36 | c.mx.reset() |
| 37 | |
| 38 | var mx map[string]int64 |
| 39 | |
| 40 | n, err := c.collectLogLines() |
| 41 | |
| 42 | if n > 0 || err == nil { |
| 43 | mx = stm.ToMap(c.mx) |
| 44 | } |
| 45 | return mx, err |
| 46 | } |
| 47 | |
| 48 | func (c *Collector) collectLogLines() (int, error) { |
| 49 | logOnce := true |
| 50 | var n int |
| 51 | for { |
| 52 | c.line.reset() |
| 53 | err := c.parser.ReadLine(c.line) |
| 54 | if err != nil { |
| 55 | if err == io.EOF { |
| 56 | return n, nil |
| 57 | } |
| 58 | if !logs.IsParseError(err) { |
| 59 | return n, err |
| 60 | } |
| 61 | n++ |
| 62 | if logOnce { |
| 63 | c.Infof("unmatched line: %v (parser: %s)", err, c.parser.Info()) |
| 64 | logOnce = false |
| 65 | } |
| 66 | c.collectUnmatched() |
| 67 | continue |
| 68 | } |
| 69 | n++ |
| 70 | if c.line.empty() { |
| 71 | c.collectUnmatched() |
| 72 | } else { |
| 73 | c.collectLogLine() |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func (c *Collector) collectLogLine() { |
| 79 | // https://github.com/netdata/netdata/issues/17716 |
| 80 | if c.line.hasReqProcTime() && c.line.respCode == http.StatusSwitchingProtocols { |
| 81 | c.line.reqProcTime = emptyNumber |
| 82 | } |
| 83 | c.mx.Requests.Inc() |
| 84 | c.collectVhost() |
| 85 | c.collectPort() |
| 86 | c.collectReqScheme() |
| 87 | c.collectReqClient() |
| 88 | c.collectReqMethod() |
| 89 | c.collectReqURL() |
| 90 | c.collectReqProto() |
| 91 | c.collectRespCode() |
| 92 | c.collectReqSize() |
| 93 | c.collectRespSize() |
| 94 | c.collectReqProcTime() |
| 95 | c.collectUpsRespTime() |
| 96 | c.collectSSLProto() |
| 97 | c.collectSSLCipherSuite() |
| 98 | c.collectCustomFields() |
| 99 | } |
| 100 | |
| 101 | func (c *Collector) collectUnmatched() { |
| 102 | c.mx.Requests.Inc() |
| 103 | c.mx.ReqUnmatched.Inc() |
| 104 | } |
| 105 | |
| 106 | func (c *Collector) collectVhost() { |
| 107 | if !c.line.hasVhost() { |
| 108 | return |
| 109 | } |
| 110 | cntr, ok := c.mx.ReqVhost.GetP(c.line.vhost) |
| 111 | if !ok { |
| 112 | c.addDimToVhostChart(c.line.vhost) |
| 113 | } |
| 114 | cntr.Inc() |
| 115 | } |
| 116 | |
| 117 | func (c *Collector) collectPort() { |
| 118 | if !c.line.hasPort() { |
| 119 | return |
| 120 | } |
| 121 | cntr, ok := c.mx.ReqPort.GetP(c.line.port) |
| 122 | if !ok { |
| 123 | c.addDimToPortChart(c.line.port) |
| 124 | } |
| 125 | cntr.Inc() |
| 126 | } |
| 127 | |
| 128 | func (c *Collector) collectReqClient() { |
| 129 | if !c.line.hasReqClient() { |
| 130 | return |
| 131 | } |
| 132 | if strings.ContainsRune(c.line.reqClient, ':') { |
| 133 | c.mx.ReqIPv6.Inc() |
| 134 | c.mx.UniqueIPv6.Insert(c.line.reqClient) |
| 135 | return |
| 136 | } |
| 137 | // NOTE: count hostname as IPv4 address |
| 138 | c.mx.ReqIPv4.Inc() |
| 139 | c.mx.UniqueIPv4.Insert(c.line.reqClient) |
| 140 | } |
| 141 | |
| 142 | func (c *Collector) collectReqScheme() { |
| 143 | if !c.line.hasReqScheme() { |
| 144 | return |
| 145 | } |
| 146 | if c.line.reqScheme == "https" { |
| 147 | c.mx.ReqHTTPSScheme.Inc() |
| 148 | } else { |
| 149 | c.mx.ReqHTTPScheme.Inc() |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | func (c *Collector) collectReqMethod() { |
| 154 | if !c.line.hasReqMethod() { |
| 155 | return |
| 156 | } |
| 157 | cntr, ok := c.mx.ReqMethod.GetP(c.line.reqMethod) |
| 158 | if !ok { |
| 159 | c.addDimToReqMethodChart(c.line.reqMethod) |
| 160 | } |
| 161 | cntr.Inc() |
| 162 | } |
| 163 | |
| 164 | func (c *Collector) collectReqURL() { |
| 165 | if !c.line.hasReqURL() { |
| 166 | return |
| 167 | } |
| 168 | for _, p := range c.urlPatterns { |
| 169 | if !p.MatchString(c.line.reqURL) { |
| 170 | continue |
| 171 | } |
| 172 | cntr, _ := c.mx.ReqURLPattern.GetP(p.name) |
| 173 | cntr.Inc() |
| 174 | |
| 175 | c.collectURLPatternStats(p.name) |
| 176 | return |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | func (c *Collector) collectReqProto() { |
| 181 | if !c.line.hasReqProto() { |
| 182 | return |
| 183 | } |
| 184 | cntr, ok := c.mx.ReqVersion.GetP(c.line.reqProto) |
| 185 | if !ok { |
| 186 | c.addDimToReqVersionChart(c.line.reqProto) |
| 187 | } |
| 188 | cntr.Inc() |
| 189 | } |
| 190 | |
| 191 | func (c *Collector) collectRespCode() { |
| 192 | if !c.line.hasRespCode() { |
| 193 | return |
| 194 | } |
| 195 | |
| 196 | code := c.line.respCode |
| 197 | switch { |
| 198 | case code >= 100 && code < 300, code == 304, code == 401, code == 429: |
| 199 | c.mx.ReqSuccess.Inc() |
| 200 | case code >= 300 && code < 400: |
| 201 | c.mx.ReqRedirect.Inc() |
| 202 | case code >= 400 && code < 500: |
| 203 | c.mx.ReqBad.Inc() |
| 204 | case code >= 500 && code < 600: |
| 205 | c.mx.ReqError.Inc() |
| 206 | } |
| 207 | |
| 208 | switch code / 100 { |
| 209 | case 1: |
| 210 | c.mx.Resp1xx.Inc() |
| 211 | case 2: |
| 212 | c.mx.Resp2xx.Inc() |
| 213 | case 3: |
| 214 | c.mx.Resp3xx.Inc() |
| 215 | case 4: |
| 216 | c.mx.Resp4xx.Inc() |
| 217 | case 5: |
| 218 | c.mx.Resp5xx.Inc() |
| 219 | } |
| 220 | |
| 221 | codeStr := strconv.Itoa(code) |
| 222 | cntr, ok := c.mx.RespCode.GetP(codeStr) |
| 223 | if !ok { |
| 224 | c.addDimToRespCodesChart(codeStr) |
| 225 | } |
| 226 | cntr.Inc() |
| 227 | } |
| 228 | |
| 229 | func (c *Collector) collectReqSize() { |
| 230 | if !c.line.hasReqSize() { |
| 231 | return |
| 232 | } |
| 233 | c.mx.BytesReceived.Add(float64(c.line.reqSize)) |
| 234 | } |
| 235 | |
| 236 | func (c *Collector) collectRespSize() { |
| 237 | if !c.line.hasRespSize() { |
| 238 | return |
| 239 | } |
| 240 | c.mx.BytesSent.Add(float64(c.line.respSize)) |
| 241 | } |
| 242 | |
| 243 | func (c *Collector) collectReqProcTime() { |
| 244 | if !c.line.hasReqProcTime() { |
| 245 | return |
| 246 | } |
| 247 | c.mx.ReqProcTime.Observe(c.line.reqProcTime) |
| 248 | if c.mx.ReqProcTimeHist == nil { |
| 249 | return |
| 250 | } |
| 251 | c.mx.ReqProcTimeHist.Observe(c.line.reqProcTime) |
| 252 | } |
| 253 | |
| 254 | func (c *Collector) collectUpsRespTime() { |
| 255 | if !c.line.hasUpsRespTime() { |
| 256 | return |
| 257 | } |
| 258 | c.mx.UpsRespTime.Observe(c.line.upsRespTime) |
| 259 | if c.mx.UpsRespTimeHist == nil { |
| 260 | return |
| 261 | } |
| 262 | c.mx.UpsRespTimeHist.Observe(c.line.upsRespTime) |
| 263 | } |
| 264 | |
| 265 | func (c *Collector) collectSSLProto() { |
| 266 | if !c.line.hasSSLProto() { |
| 267 | return |
| 268 | } |
| 269 | cntr, ok := c.mx.ReqSSLProto.GetP(c.line.sslProto) |
| 270 | if !ok { |
| 271 | c.addDimToSSLProtoChart(c.line.sslProto) |
| 272 | } |
| 273 | cntr.Inc() |
| 274 | } |
| 275 | |
| 276 | func (c *Collector) collectSSLCipherSuite() { |
| 277 | if !c.line.hasSSLCipherSuite() { |
| 278 | return |
| 279 | } |
| 280 | cntr, ok := c.mx.ReqSSLCipherSuite.GetP(c.line.sslCipherSuite) |
| 281 | if !ok { |
| 282 | c.addDimToSSLCipherSuiteChart(c.line.sslCipherSuite) |
| 283 | } |
| 284 | cntr.Inc() |
| 285 | } |
| 286 | |
| 287 | func (c *Collector) collectURLPatternStats(name string) { |
| 288 | v, ok := c.mx.URLPatternStats[name] |
| 289 | if !ok { |
| 290 | return |
| 291 | } |
| 292 | if c.line.hasRespCode() { |
| 293 | status := strconv.Itoa(c.line.respCode) |
| 294 | cntr, ok := v.RespCode.GetP(status) |
| 295 | if !ok { |
| 296 | c.addDimToURLPatternRespCodesChart(name, status) |
| 297 | } |
| 298 | cntr.Inc() |
| 299 | } |
| 300 | |
| 301 | if c.line.hasReqMethod() { |
| 302 | cntr, ok := v.ReqMethod.GetP(c.line.reqMethod) |
| 303 | if !ok { |
| 304 | c.addDimToURLPatternReqMethodsChart(name, c.line.reqMethod) |
| 305 | } |
| 306 | cntr.Inc() |
| 307 | } |
| 308 | |
| 309 | if c.line.hasReqSize() { |
| 310 | v.BytesReceived.Add(float64(c.line.reqSize)) |
| 311 | } |
| 312 | |
| 313 | if c.line.hasRespSize() { |
| 314 | v.BytesSent.Add(float64(c.line.respSize)) |
| 315 | } |
| 316 | if c.line.hasReqProcTime() { |
| 317 | v.ReqProcTime.Observe(c.line.reqProcTime) |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | func (c *Collector) collectCustomFields() { |
| 322 | if !c.line.hasCustomFields() { |
| 323 | return |
| 324 | } |
| 325 | |
| 326 | for _, cv := range c.line.custom.values { |
| 327 | _, _ = cv.name, cv.value |
| 328 | |
| 329 | if patterns, ok := c.customFields[cv.name]; ok { |
| 330 | for _, pattern := range patterns { |
| 331 | if !pattern.MatchString(cv.value) { |
| 332 | continue |
| 333 | } |
| 334 | v, ok := c.mx.ReqCustomField[cv.name] |
| 335 | if !ok { |
| 336 | break |
| 337 | } |
| 338 | c, _ := v.GetP(pattern.name) |
| 339 | c.Inc() |
| 340 | break |
| 341 | } |
| 342 | } else if histogram, ok := c.customTimeFields[cv.name]; ok { |
| 343 | v, ok := c.mx.ReqCustomTimeField[cv.name] |
| 344 | if !ok { |
| 345 | continue |
| 346 | } |
| 347 | ctf, err := strconv.ParseFloat(cv.value, 64) |
| 348 | if err != nil || !isTimeValid(ctf) { |
| 349 | continue |
| 350 | } |
| 351 | v.Time.Observe(ctf) |
| 352 | if histogram != nil { |
| 353 | v.TimeHist.Observe(ctf * timeMultiplier(cv.value)) |
| 354 | } |
| 355 | } else if c.customNumericFields[cv.name] { |
| 356 | m, ok := c.mx.ReqCustomNumericField[cv.name] |
| 357 | if !ok { |
| 358 | continue |
| 359 | } |
| 360 | v, err := strconv.ParseFloat(cv.value, 64) |
| 361 | if err != nil { |
| 362 | continue |
| 363 | } |
| 364 | v *= float64(m.multiplier) |
| 365 | m.Summary.Observe(v) |
| 366 | } |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | func (c *Collector) addDimToVhostChart(vhost string) { |
| 371 | chart := c.Charts().Get(reqByVhost.ID) |
| 372 | if chart == nil { |
| 373 | c.Warningf("add dimension: no '%s' chart", reqByVhost.ID) |
| 374 | return |
| 375 | } |
| 376 | dim := &Dim{ |
| 377 | ID: "req_vhost_" + vhost, |
| 378 | Name: vhost, |
| 379 | Algo: collectorapi.Incremental, |
| 380 | } |
| 381 | if err := chart.AddDim(dim); err != nil { |
| 382 | c.Warning(err) |
| 383 | return |
| 384 | } |
| 385 | chart.MarkNotCreated() |
| 386 | } |
| 387 | |
| 388 | func (c *Collector) addDimToPortChart(port string) { |
| 389 | chart := c.Charts().Get(reqByPort.ID) |
| 390 | if chart == nil { |
| 391 | c.Warningf("add dimension: no '%s' chart", reqByPort.ID) |
| 392 | return |
| 393 | } |
| 394 | dim := &Dim{ |
| 395 | ID: "req_port_" + port, |
| 396 | Name: port, |
| 397 | Algo: collectorapi.Incremental, |
| 398 | } |
| 399 | if err := chart.AddDim(dim); err != nil { |
| 400 | c.Warning(err) |
| 401 | return |
| 402 | } |
| 403 | chart.MarkNotCreated() |
| 404 | } |
| 405 | |
| 406 | func (c *Collector) addDimToReqMethodChart(method string) { |
| 407 | chart := c.Charts().Get(reqByMethod.ID) |
| 408 | if chart == nil { |
| 409 | c.Warningf("add dimension: no '%s' chart", reqByMethod.ID) |
| 410 | return |
| 411 | } |
| 412 | dim := &Dim{ |
| 413 | ID: "req_method_" + method, |
| 414 | Name: method, |
| 415 | Algo: collectorapi.Incremental, |
| 416 | } |
| 417 | if err := chart.AddDim(dim); err != nil { |
| 418 | c.Warning(err) |
| 419 | return |
| 420 | } |
| 421 | chart.MarkNotCreated() |
| 422 | } |
| 423 | |
| 424 | func (c *Collector) addDimToReqVersionChart(version string) { |
| 425 | chart := c.Charts().Get(reqByVersion.ID) |
| 426 | if chart == nil { |
| 427 | c.Warningf("add dimension: no '%s' chart", reqByVersion.ID) |
| 428 | return |
| 429 | } |
| 430 | dim := &Dim{ |
| 431 | ID: "req_version_" + version, |
| 432 | Name: version, |
| 433 | Algo: collectorapi.Incremental, |
| 434 | } |
| 435 | if err := chart.AddDim(dim); err != nil { |
| 436 | c.Warning(err) |
| 437 | return |
| 438 | } |
| 439 | chart.MarkNotCreated() |
| 440 | } |
| 441 | |
| 442 | func (c *Collector) addDimToSSLProtoChart(proto string) { |
| 443 | chart := c.Charts().Get(reqBySSLProto.ID) |
| 444 | if chart == nil { |
| 445 | chart = reqBySSLProto.Copy() |
| 446 | if err := c.Charts().Add(chart); err != nil { |
| 447 | c.Warning(err) |
| 448 | return |
| 449 | } |
| 450 | } |
| 451 | dim := &Dim{ |
| 452 | ID: "req_ssl_proto_" + proto, |
| 453 | Name: proto, |
| 454 | Algo: collectorapi.Incremental, |
| 455 | } |
| 456 | if err := chart.AddDim(dim); err != nil { |
| 457 | c.Warning(err) |
| 458 | return |
| 459 | } |
| 460 | chart.MarkNotCreated() |
| 461 | } |
| 462 | |
| 463 | func (c *Collector) addDimToSSLCipherSuiteChart(cipher string) { |
| 464 | chart := c.Charts().Get(reqBySSLCipherSuite.ID) |
| 465 | if chart == nil { |
| 466 | chart = reqBySSLCipherSuite.Copy() |
| 467 | if err := c.Charts().Add(chart); err != nil { |
| 468 | c.Warning(err) |
| 469 | return |
| 470 | } |
| 471 | } |
| 472 | dim := &Dim{ |
| 473 | ID: "req_ssl_cipher_suite_" + cipher, |
| 474 | Name: cipher, |
| 475 | Algo: collectorapi.Incremental, |
| 476 | } |
| 477 | if err := chart.AddDim(dim); err != nil { |
| 478 | c.Warning(err) |
| 479 | return |
| 480 | } |
| 481 | chart.MarkNotCreated() |
| 482 | } |
| 483 | |
| 484 | func (c *Collector) addDimToRespCodesChart(code string) { |
| 485 | chart := c.findRespCodesChart(code) |
| 486 | if chart == nil { |
| 487 | c.Warning("add dimension: cant find resp codes chart") |
| 488 | return |
| 489 | } |
| 490 | dim := &Dim{ |
| 491 | ID: "resp_code_" + code, |
| 492 | Name: code, |
| 493 | Algo: collectorapi.Incremental, |
| 494 | } |
| 495 | if err := chart.AddDim(dim); err != nil { |
| 496 | c.Warning(err) |
| 497 | return |
| 498 | } |
| 499 | chart.MarkNotCreated() |
| 500 | } |
| 501 | |
| 502 | func (c *Collector) addDimToURLPatternRespCodesChart(name, code string) { |
| 503 | id := fmt.Sprintf(urlPatternRespCodes.ID, name) |
| 504 | chart := c.Charts().Get(id) |
| 505 | if chart == nil { |
| 506 | c.Warningf("add dimension: no '%s' chart", id) |
| 507 | return |
| 508 | } |
| 509 | dim := &Dim{ |
| 510 | ID: fmt.Sprintf("url_ptn_%s_resp_code_%s", name, code), |
| 511 | Name: code, |
| 512 | Algo: collectorapi.Incremental, |
| 513 | } |
| 514 | |
| 515 | if err := chart.AddDim(dim); err != nil { |
| 516 | c.Warning(err) |
| 517 | return |
| 518 | } |
| 519 | chart.MarkNotCreated() |
| 520 | } |
| 521 | |
| 522 | func (c *Collector) addDimToURLPatternReqMethodsChart(name, method string) { |
| 523 | id := fmt.Sprintf(urlPatternReqMethods.ID, name) |
| 524 | chart := c.Charts().Get(id) |
| 525 | if chart == nil { |
| 526 | c.Warningf("add dimension: no '%s' chart", id) |
| 527 | return |
| 528 | } |
| 529 | dim := &Dim{ |
| 530 | ID: fmt.Sprintf("url_ptn_%s_req_method_%s", name, method), |
| 531 | Name: method, |
| 532 | Algo: collectorapi.Incremental, |
| 533 | } |
| 534 | |
| 535 | if err := chart.AddDim(dim); err != nil { |
| 536 | c.Warning(err) |
| 537 | return |
| 538 | } |
| 539 | chart.MarkNotCreated() |
| 540 | } |
| 541 | |
| 542 | func (c *Collector) findRespCodesChart(code string) *Chart { |
| 543 | if !c.GroupRespCodes { |
| 544 | return c.Charts().Get(respCodes.ID) |
| 545 | } |
| 546 | |
| 547 | var id string |
| 548 | switch class := code[:1]; class { |
| 549 | case "1": |
| 550 | id = respCodes1xx.ID |
| 551 | case "2": |
| 552 | id = respCodes2xx.ID |
| 553 | case "3": |
| 554 | id = respCodes3xx.ID |
| 555 | case "4": |
| 556 | id = respCodes4xx.ID |
| 557 | case "5": |
| 558 | id = respCodes5xx.ID |
| 559 | default: |
| 560 | return nil |
| 561 | } |
| 562 | return c.Charts().Get(id) |
| 563 | } |