Added request_aggregates input plugin
This commit is contained in:
parent
90a98c76a0
commit
b2a05a3b58
|
@ -0,0 +1,90 @@
|
||||||
|
# Request aggregates plugin
|
||||||
|
|
||||||
|
The request aggregates plugin generates a set of aggregate values for a response time column in a CSV file within a
|
||||||
|
given interval. This is especially useful when calculating throughput of systems with high request frequency
|
||||||
|
for which storing every single request might require an unnecessary infrastructure. Aggregating values on the client
|
||||||
|
side minimises the number of writes to the InfluxDB server.
|
||||||
|
|
||||||
|
The plugin generates data points at the end of the given window. If no lines were added to the file during a specific
|
||||||
|
window, no data points are generated.
|
||||||
|
|
||||||
|
### Configuration:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Aggregates values for requests written to a log file
|
||||||
|
[[inputs.request_aggregates]]
|
||||||
|
# File to monitor.
|
||||||
|
file = "/var/server/access.csv"
|
||||||
|
# Position of the timestamp of the request in every line
|
||||||
|
timestamp_position = 0
|
||||||
|
# Format of the timestamp (any layout accepted by Go Time.Parse or s/ms/us/ns for epoch time)
|
||||||
|
timestamp_format = "ms"
|
||||||
|
# Position of the time value to calculate in the log file (starting from 0)
|
||||||
|
time_position = 1
|
||||||
|
# Window to consider for time percentiles
|
||||||
|
time_window_size = "60s"
|
||||||
|
# Windows to keep in memory before flushing in order to avoid requests coming in after a window is shut.
|
||||||
|
# If the CSV file is sorted by timestamp, this can be set to 1
|
||||||
|
time_windows = 5
|
||||||
|
# List of percentiles to calculate
|
||||||
|
time_percentiles = [90.0, 95.0, 99.0, 99.99]
|
||||||
|
# Position of the result column (success or failure)
|
||||||
|
result_position = 3
|
||||||
|
# Regular expression used to determine if the result is successful or not (if empty only request_aggregates_all
|
||||||
|
# time series) will be generated
|
||||||
|
result_success_regex = ".*true.*"
|
||||||
|
# Time window to calculate throughput counters
|
||||||
|
throughput_window_size = "1s"
|
||||||
|
# Number of windows to keep in memory for throughput calculation
|
||||||
|
throughput_windows = 300
|
||||||
|
# List of tags and their values to add to every data point
|
||||||
|
[inputs.aggregates.tags]
|
||||||
|
name = "myserver"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Measurements & Fields:
|
||||||
|
Note: There are as many `perc[_percentile]` as percentiles defined in the configuration.
|
||||||
|
|
||||||
|
- request_aggregates
|
||||||
|
- requests (integer)
|
||||||
|
- time_min (float)
|
||||||
|
- time_max (float)
|
||||||
|
- time_mean (float)
|
||||||
|
- time_perc_90 (float)
|
||||||
|
- time_perc_95 (float)
|
||||||
|
- [...]
|
||||||
|
- time_perc_99_99 (float)
|
||||||
|
- request_aggregates_success
|
||||||
|
- requests (integer)
|
||||||
|
- time_min (float)
|
||||||
|
- time_max (float)
|
||||||
|
- time_mean (float)
|
||||||
|
- time_perc_90 (float)
|
||||||
|
- time_perc_95 (float)
|
||||||
|
- [...]
|
||||||
|
- time_perc_99_99 (float)
|
||||||
|
- request_aggregates_failure
|
||||||
|
- requests (integer)
|
||||||
|
- time_min (float)
|
||||||
|
- time_max (float)
|
||||||
|
- time_mean (float)
|
||||||
|
- time_perc_90 (float)
|
||||||
|
- time_perc_95 (float)
|
||||||
|
- [...]
|
||||||
|
- time_perc_99_99 (float)
|
||||||
|
- request_aggregates_throughput
|
||||||
|
- requests_total (integer)
|
||||||
|
- requests_failed (integer)
|
||||||
|
|
||||||
|
### Tags:
|
||||||
|
Tags are user defined in `[inputs.aggregates.tags]`
|
||||||
|
|
||||||
|
### Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./telegraf -config telegraf.conf -input-filter request_aggregates -test
|
||||||
|
request_aggregates,name=myserver requests=186,time_max=380,time_min=86,time_mean=258.54,time_perc_90=200,time_perc_95=220,time_perc_99=225,time_perc_99_99=229 1462270026000000000
|
||||||
|
request_aggregates_success,name=myserver requests=123,time_max=230,time_min=86,time_mean=120.23,time_perc_90=200,time_perc_95=220,time_perc_99=225,time_perc_99_99=229 1462270026000000000
|
||||||
|
request_aggregates_failure,name=myserver requests=63,time_max=380,time_min=132,time_mean=298.54,time_perc_90=250,time_perc_95=270,time_perc_99=285,time_perc_99_99=290 1462270026000000000
|
||||||
|
request_aggregates_throughput,name=myserver requests_total=186,requests_failed=63 1462270026000000000
|
||||||
|
```
|
|
@ -0,0 +1,67 @@
|
||||||
|
package request_aggregates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
Time float64
|
||||||
|
Failure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestParser struct {
|
||||||
|
TimestampPosition int
|
||||||
|
TimestampFormat string
|
||||||
|
IsTimeEpoch bool
|
||||||
|
TimePosition int
|
||||||
|
ResultPosition int
|
||||||
|
SuccessRegexp *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses a CSV line and generates a Request
|
||||||
|
func (rp *RequestParser) ParseLine(line string) (*Request, error) {
|
||||||
|
var request Request
|
||||||
|
|
||||||
|
// Split fields and assign values
|
||||||
|
reader := strings.NewReader(line)
|
||||||
|
fields, err := csv.NewReader(reader).Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ERROR: could not pass CSV line, Error: %s", err)
|
||||||
|
}
|
||||||
|
if rp.ResultPosition < 0 || len(fields) <= rp.ResultPosition ||
|
||||||
|
rp.TimePosition < 0 || len(fields) <= rp.TimePosition ||
|
||||||
|
rp.TimestampPosition < 0 || len(fields) <= rp.TimestampPosition {
|
||||||
|
return nil, fmt.Errorf("ERROR: column position out of range")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rp.IsTimeEpoch {
|
||||||
|
var dur time.Duration
|
||||||
|
dur, err = time.ParseDuration(fields[rp.TimestampPosition] + rp.TimestampFormat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ERROR: could not parse epoch date, Error: %s", err)
|
||||||
|
}
|
||||||
|
request.Timestamp = time.Unix(0, dur.Nanoseconds())
|
||||||
|
} else {
|
||||||
|
request.Timestamp, err = time.Parse(rp.TimestampFormat, fields[rp.TimestampPosition])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ERROR: could not parse date, Error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Time, err = strconv.ParseFloat(fields[rp.TimePosition], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ERROR: could not parse time value, Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rp.SuccessRegexp != nil {
|
||||||
|
request.Failure = !rp.SuccessRegexp.MatchString(fields[rp.ResultPosition])
|
||||||
|
}
|
||||||
|
|
||||||
|
return &request, nil
|
||||||
|
}
|
|
@ -0,0 +1,356 @@
|
||||||
|
package request_aggregates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/hpcloud/tail"
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestAggregates struct {
|
||||||
|
File string
|
||||||
|
TimestampPosition int
|
||||||
|
TimestampFormat string
|
||||||
|
TimePosition int
|
||||||
|
TimePercentiles []float32
|
||||||
|
TimeWindowSize internal.Duration
|
||||||
|
TimeWindows int
|
||||||
|
ResultPosition int
|
||||||
|
ResultSuccessRegex string
|
||||||
|
ThroughputWindowSize internal.Duration
|
||||||
|
ThroughputWindows int
|
||||||
|
|
||||||
|
isTimestampEpoch bool
|
||||||
|
successRegexp *regexp.Regexp
|
||||||
|
tailer *tail.Tail
|
||||||
|
timeWindowSlice []Window
|
||||||
|
throughputWindowSlice []Window
|
||||||
|
timeTimer *time.Timer
|
||||||
|
throughputTimer *time.Timer
|
||||||
|
stopTimeChan chan bool
|
||||||
|
stopThroughputChan chan bool
|
||||||
|
timeMutex sync.Mutex
|
||||||
|
throughputMutex sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequestAggregates() *RequestAggregates {
|
||||||
|
return &RequestAggregates{
|
||||||
|
TimeWindows: 2,
|
||||||
|
ThroughputWindows: 10}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleConfig = `
|
||||||
|
# File to monitor.
|
||||||
|
file = "/var/server/access.csv"
|
||||||
|
# Position of the timestamp of the request in every line
|
||||||
|
timestamp_position = 0
|
||||||
|
# Format of the timestamp (any layout accepted by Go Time.Parse or s/ms/us/ns for epoch time)
|
||||||
|
timestamp_format = "ms"
|
||||||
|
# Position of the time value to calculate in the log file (starting from 0)
|
||||||
|
time_position = 1
|
||||||
|
# Window to consider for time percentiles
|
||||||
|
time_window_size = "60s"
|
||||||
|
# Windows to keep in memory before flushing in order to avoid requests coming in after a window is shut.
|
||||||
|
# If the CSV file is sorted by timestamp, this can be set to 1
|
||||||
|
time_windows = 5
|
||||||
|
# List of percentiles to calculate
|
||||||
|
time_percentiles = [90.0, 95.0, 99.0, 99.99]
|
||||||
|
# Position of the result column (success or failure)
|
||||||
|
result_position = 3
|
||||||
|
# Regular expression used to determine if the result is successful or not (if empty only request_aggregates_all
|
||||||
|
# time series) will be generated
|
||||||
|
result_success_regex = ".*true.*"
|
||||||
|
# Time window to calculate throughput counters
|
||||||
|
throughput_window_size = "1s"
|
||||||
|
# Number of windows to keep in memory for throughput calculation
|
||||||
|
throughput_windows = 300
|
||||||
|
# List of tags and their values to add to every data point
|
||||||
|
[inputs.aggregates.tags]
|
||||||
|
name = "myserver"
|
||||||
|
`
|
||||||
|
|
||||||
|
func (ra *RequestAggregates) SampleConfig() string {
|
||||||
|
return sampleConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ra *RequestAggregates) Description() string {
|
||||||
|
return "Generates a set of aggregate values for a requests and their response times."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ra *RequestAggregates) Gather(acc telegraf.Accumulator) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ra *RequestAggregates) Start(acc telegraf.Accumulator) error {
|
||||||
|
ra.Lock()
|
||||||
|
defer ra.Unlock()
|
||||||
|
|
||||||
|
err := ra.validateConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tailer
|
||||||
|
ra.tailer, err = tail.TailFile(ra.File, tail.Config{
|
||||||
|
Follow: true,
|
||||||
|
ReOpen: true,
|
||||||
|
Location: &tail.SeekInfo{Whence: 2, Offset: 0}})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ERROR tailing file %s, Error: %s", ra.File, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create first time window and start go routine to manage them
|
||||||
|
now := time.Now()
|
||||||
|
ra.timeWindowSlice = append(ra.timeWindowSlice, &TimeWindow{
|
||||||
|
StartTime: now, EndTime: now.Add(ra.TimeWindowSize.Duration),
|
||||||
|
OnlyTotal: ra.successRegexp == nil, Percentiles: ra.TimePercentiles})
|
||||||
|
ra.timeTimer = time.NewTimer(ra.TimeWindowSize.Duration)
|
||||||
|
ra.stopTimeChan = make(chan bool, 1)
|
||||||
|
ra.wg.Add(1)
|
||||||
|
go ra.manageTimeWindows(acc)
|
||||||
|
|
||||||
|
// Create first throughput window and start go routine to manage them
|
||||||
|
ra.throughputWindowSlice = append(ra.throughputWindowSlice, &ThroughputWindow{
|
||||||
|
StartTime: now, EndTime: now.Add(ra.ThroughputWindowSize.Duration)})
|
||||||
|
ra.throughputTimer = time.NewTimer(ra.ThroughputWindowSize.Duration)
|
||||||
|
ra.stopThroughputChan = make(chan bool, 1)
|
||||||
|
ra.wg.Add(1)
|
||||||
|
go ra.manageThroughputWindows(acc)
|
||||||
|
|
||||||
|
// Start go routine to tail the file and put requests in windows
|
||||||
|
ra.wg.Add(1)
|
||||||
|
go ra.gatherFromFile(ra.tailer, acc)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ra *RequestAggregates) Stop() {
|
||||||
|
ra.Lock()
|
||||||
|
defer ra.Unlock()
|
||||||
|
|
||||||
|
err := ra.tailer.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: could not stop tail on file %s\n", ra.File)
|
||||||
|
}
|
||||||
|
ra.tailer.Cleanup()
|
||||||
|
|
||||||
|
ra.timeTimer.Stop()
|
||||||
|
ra.stopTimeChan <- true
|
||||||
|
ra.throughputTimer.Stop()
|
||||||
|
ra.stopThroughputChan <- true
|
||||||
|
|
||||||
|
ra.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates the configuration in the struct
|
||||||
|
func (ra *RequestAggregates) validateConfig() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Compile regex to identify success
|
||||||
|
if ra.ResultSuccessRegex != "" {
|
||||||
|
ra.successRegexp, err = regexp.Compile(ra.ResultSuccessRegex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ERROR: success regexp is not valid, Error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if timestamp format is valid
|
||||||
|
switch ra.TimestampFormat {
|
||||||
|
case "s", "ms", "us", "ns":
|
||||||
|
ra.isTimestampEpoch = true
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if time.Now().Format(ra.TimestampFormat) == ra.TimestampFormat {
|
||||||
|
return fmt.Errorf("ERROR: incorrect timestamp format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check percentiles are valid
|
||||||
|
for _, percentile := range ra.TimePercentiles {
|
||||||
|
if percentile <= 0 || percentile >= 100 {
|
||||||
|
return fmt.Errorf("ERROR: percentiles must be numbers between 0 and 100 (not inclusive)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Check duration of windows
|
||||||
|
if ra.TimeWindowSize.Duration <= time.Duration(0) || ra.ThroughputWindowSize.Duration <= time.Duration(0) {
|
||||||
|
return fmt.Errorf("ERROR: windows need to be a positive duration")
|
||||||
|
}
|
||||||
|
// Check number of windows
|
||||||
|
if ra.TimeWindows <= 0 || ra.ThroughputWindows <= 0 {
|
||||||
|
return fmt.Errorf("ERROR: at least one window is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executed as a go routine, tails a given file and puts the parsed requests into their respective windows.
|
||||||
|
func (ra *RequestAggregates) gatherFromFile(tailer *tail.Tail, acc telegraf.Accumulator) {
|
||||||
|
defer ra.wg.Done()
|
||||||
|
|
||||||
|
requestParser := &RequestParser{
|
||||||
|
TimestampPosition: ra.TimestampPosition,
|
||||||
|
TimestampFormat: ra.TimestampFormat,
|
||||||
|
IsTimeEpoch: ra.isTimestampEpoch,
|
||||||
|
TimePosition: ra.TimePosition,
|
||||||
|
ResultPosition: ra.ResultPosition,
|
||||||
|
SuccessRegexp: ra.successRegexp}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var line *tail.Line
|
||||||
|
var request *Request
|
||||||
|
for line = range tailer.Lines {
|
||||||
|
// Parse and validate line
|
||||||
|
if line.Err != nil {
|
||||||
|
log.Printf("ERROR: could not tail file %s, Error: %s\n", tailer.Filename, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
request, err = requestParser.ParseLine(line.Text)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: malformed line in %s: [%s], Error: %s\n", tailer.Filename, line.Text, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the window is created (it is possible that the line is read before the time ticks)
|
||||||
|
for ra.timeWindowSlice[len(ra.timeWindowSlice)-1].End().Before(request.Timestamp) {
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
|
}
|
||||||
|
// Add request to time window
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
err = addToWindow(ra.timeWindowSlice, request)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: could not find a time window, Request: %v, Error %s\n", request, err)
|
||||||
|
}
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
|
||||||
|
// Wait until the window is created (it is possible that the line is read before the time ticks)
|
||||||
|
for ra.throughputWindowSlice[len(ra.throughputWindowSlice)-1].End().Before(request.Timestamp) {
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
|
}
|
||||||
|
// Add request to throughput window
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
err = addToWindow(ra.throughputWindowSlice, request)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: could not find a throughput window, Request: %v, Error %s\n", request, err)
|
||||||
|
}
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executed as a go routine, manages the windows related to time measures, creating new ones and flushing old ones
|
||||||
|
func (ra *RequestAggregates) manageTimeWindows(acc telegraf.Accumulator) {
|
||||||
|
defer ra.wg.Done()
|
||||||
|
onlyTotal := ra.successRegexp == nil
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// If the timer is triggered
|
||||||
|
case <-ra.timeTimer.C:
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
// Create new window with the start time of the last one's end time
|
||||||
|
startTime := ra.timeWindowSlice[len(ra.timeWindowSlice)-1].End()
|
||||||
|
endTime := startTime.Add(ra.TimeWindowSize.Duration)
|
||||||
|
ra.timeWindowSlice = append(ra.timeWindowSlice, &TimeWindow{
|
||||||
|
StartTime: startTime, EndTime: endTime,
|
||||||
|
OnlyTotal: onlyTotal, Percentiles: ra.TimePercentiles})
|
||||||
|
// Flush oldest one if necessary
|
||||||
|
if len(ra.timeWindowSlice) > ra.TimeWindows {
|
||||||
|
ra.timeWindowSlice = flushWindow(ra.timeWindowSlice, acc)
|
||||||
|
}
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
// Reset time till the end of the window
|
||||||
|
ra.timeTimer.Reset(endTime.Sub(time.Now()))
|
||||||
|
// If the stop signal is received
|
||||||
|
case <-ra.stopTimeChan:
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
ra.timeWindowSlice = flushAllWindows(ra.timeWindowSlice, acc)
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executed as a go routine, manages the windows related to throughput measures, creating new ones and flushing old ones
|
||||||
|
func (ra *RequestAggregates) manageThroughputWindows(acc telegraf.Accumulator) {
|
||||||
|
defer ra.wg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// If the timer is triggered
|
||||||
|
case <-ra.throughputTimer.C:
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
// Create new window with the start time of the last one's end time
|
||||||
|
startTime := ra.throughputWindowSlice[len(ra.throughputWindowSlice)-1].End()
|
||||||
|
endTime := startTime.Add(ra.ThroughputWindowSize.Duration)
|
||||||
|
ra.throughputWindowSlice = append(ra.throughputWindowSlice, &ThroughputWindow{
|
||||||
|
StartTime: startTime, EndTime: endTime})
|
||||||
|
// Flush oldest one if necessary
|
||||||
|
if len(ra.throughputWindowSlice) > ra.ThroughputWindows {
|
||||||
|
ra.throughputWindowSlice = flushWindow(ra.throughputWindowSlice, acc)
|
||||||
|
}
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
ra.throughputTimer.Reset(endTime.Sub(time.Now()))
|
||||||
|
// If the stop signal is received
|
||||||
|
case <-ra.stopThroughputChan:
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
ra.throughputWindowSlice = flushAllWindows(ra.throughputWindowSlice, acc)
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes the window at the front of the slice of windows and flushes its aggregated metrics to the accumulator
|
||||||
|
func flushWindow(windows []Window, acc telegraf.Accumulator) []Window {
|
||||||
|
if len(windows) > 0 {
|
||||||
|
var window Window
|
||||||
|
window, windows = windows[0], windows[1:]
|
||||||
|
metrics, err := window.Aggregate()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: could not flush window, Error: %s\n", err)
|
||||||
|
}
|
||||||
|
for _, metric := range metrics {
|
||||||
|
acc.AddFields(metric.Name(), metric.Fields(), metric.Tags(), metric.Time())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return windows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flushes all windows ot the accumulator
|
||||||
|
func flushAllWindows(windows []Window, acc telegraf.Accumulator) []Window {
|
||||||
|
for len(windows) > 0 {
|
||||||
|
windows = flushWindow(windows, acc)
|
||||||
|
}
|
||||||
|
return windows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a request to a window, returns and error if it could not be added
|
||||||
|
func addToWindow(windows []Window, request *Request) error {
|
||||||
|
if len(windows) == 0 {
|
||||||
|
return fmt.Errorf("ERROR: no windows found")
|
||||||
|
}
|
||||||
|
first := windows[len(windows)-1]
|
||||||
|
if first.End().Before(request.Timestamp) {
|
||||||
|
return fmt.Errorf("ERROR: request is newer than any window")
|
||||||
|
}
|
||||||
|
last := windows[0]
|
||||||
|
if last.Start().After(request.Timestamp) {
|
||||||
|
return fmt.Errorf("ERROR: request is older than any window, try adding more windows")
|
||||||
|
}
|
||||||
|
for i := range windows {
|
||||||
|
window := windows[i]
|
||||||
|
if (window.Start().Before(request.Timestamp) || window.Start().Equal(request.Timestamp)) &&
|
||||||
|
window.End().After(request.Timestamp) {
|
||||||
|
return window.Add(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ERROR: no window could be found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
inputs.Add("request_aggregates", func() telegraf.Input {
|
||||||
|
return NewRequestAggregates()
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,385 @@
|
||||||
|
package request_aggregates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
"github.com/influxdata/telegraf/testutil"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This tests the start/stop and gather functionality
|
||||||
|
func TestNewRequestAggregates(t *testing.T) {
|
||||||
|
tmpfile, err := ioutil.TempFile("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
tmpfile.WriteString(fmt.Sprintf("%v", time.Now().UnixNano()) + ",123\n")
|
||||||
|
defer tmpfile.Close()
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
windowSize := internal.Duration{Duration: time.Millisecond * 100}
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
ra := &RequestAggregates{
|
||||||
|
File: tmpfile.Name(),
|
||||||
|
TimestampFormat: "ns",
|
||||||
|
TimeWindowSize: windowSize,
|
||||||
|
TimeWindows: 1,
|
||||||
|
ThroughputWindowSize: windowSize,
|
||||||
|
ThroughputWindows: 10,
|
||||||
|
TimestampPosition: 0,
|
||||||
|
TimePosition: 1}
|
||||||
|
// When we start
|
||||||
|
ra.Start(acc)
|
||||||
|
// A tailer is created
|
||||||
|
ra.Lock()
|
||||||
|
require.NotNil(t, ra.tailer)
|
||||||
|
require.Equal(t, tmpfile.Name(), ra.tailer.Filename)
|
||||||
|
ra.Unlock()
|
||||||
|
// The windows are initialised
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 1, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, 1, len(ra.throughputWindowSlice))
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 0, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
|
||||||
|
// When we put a request in the file
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
tmpfile.WriteString(fmt.Sprintf("%v", time.Now().UnixNano()) + ",456\n")
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
// One metric is stored in the windows
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, int64(1), ra.throughputWindowSlice[0].(*ThroughputWindow).RequestsTotal)
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 1, len(ra.timeWindowSlice[0].(*TimeWindow).TimesTotal))
|
||||||
|
require.Equal(t, float64(456), ra.timeWindowSlice[0].(*TimeWindow).TimesTotal[0])
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
|
||||||
|
// After the first window is expired
|
||||||
|
time.Sleep(windowSize.Duration)
|
||||||
|
// One of the windows has flushed one metric
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 1, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, 2, len(ra.throughputWindowSlice))
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 1, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
|
||||||
|
// When we stop
|
||||||
|
ra.Stop()
|
||||||
|
// All the metrics should have been flushed
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 0, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, 0, len(ra.throughputWindowSlice))
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 4, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAggregates_validateConfig(t *testing.T) {
|
||||||
|
// Empty config
|
||||||
|
ra := &RequestAggregates{}
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
// Minimum config
|
||||||
|
ra = &RequestAggregates{
|
||||||
|
TimestampFormat: "ms",
|
||||||
|
TimeWindowSize: internal.Duration{Duration: time.Millisecond * 10},
|
||||||
|
TimeWindows: 2,
|
||||||
|
ThroughputWindowSize: internal.Duration{Duration: time.Millisecond * 10},
|
||||||
|
ThroughputWindows: 10}
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
// Regexp for success
|
||||||
|
ra.ResultSuccessRegex = "*success.*"
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.ResultSuccessRegex = ".*success.*"
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
// Time format
|
||||||
|
ra.TimestampFormat = "thisisnotavalidformat"
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimestampFormat = ""
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimestampFormat = "Mon Jan _2 15:04:05 2006"
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
// Percentiles
|
||||||
|
ra.TimePercentiles = []float32{80, 90, 100}
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimePercentiles = []float32{0, 90, 99}
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimePercentiles = []float32{80, 90, 99}
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
// Window size
|
||||||
|
ra.TimeWindowSize = internal.Duration{Duration: time.Duration(0)}
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimeWindowSize = internal.Duration{Duration: time.Duration(-1)}
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimeWindowSize = internal.Duration{Duration: time.Duration(1)}
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
ra.ThroughputWindowSize = internal.Duration{Duration: time.Duration(0)}
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.ThroughputWindowSize = internal.Duration{Duration: time.Duration(-1)}
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.ThroughputWindowSize = internal.Duration{Duration: time.Duration(1)}
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
// Number of windows
|
||||||
|
ra.TimeWindows = 0
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimeWindows = -1
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.TimeWindows = 1
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
ra.ThroughputWindows = 0
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.ThroughputWindows = -1
|
||||||
|
require.Error(t, ra.validateConfig())
|
||||||
|
ra.ThroughputWindows = 1
|
||||||
|
require.NoError(t, ra.validateConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAggregates_manageTimeWindows_OnlyTotal(t *testing.T) {
|
||||||
|
windowSize := internal.Duration{Duration: time.Millisecond * 100}
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
now := time.Now()
|
||||||
|
ra := &RequestAggregates{
|
||||||
|
TimeWindows: 2,
|
||||||
|
TimeWindowSize: windowSize,
|
||||||
|
TimePercentiles: []float32{70, 80, 90},
|
||||||
|
timeTimer: time.NewTimer(windowSize.Duration),
|
||||||
|
stopTimeChan: make(chan bool, 1)}
|
||||||
|
|
||||||
|
// Add first window and start routine
|
||||||
|
ra.timeWindowSlice = append(ra.timeWindowSlice, &TimeWindow{
|
||||||
|
StartTime: now, EndTime: now.Add(windowSize.Duration), OnlyTotal: true, Percentiles: ra.TimePercentiles})
|
||||||
|
ra.wg.Add(1)
|
||||||
|
go ra.manageTimeWindows(acc)
|
||||||
|
|
||||||
|
// Check values at different points
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 1, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 0, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
time.Sleep(windowSize.Duration)
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 2, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 0, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
time.Sleep(windowSize.Duration)
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 2, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 1, len(acc.Metrics))
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration), acc.Metrics[0].Time)
|
||||||
|
acc.Unlock()
|
||||||
|
|
||||||
|
// Stop and wait for the process to finish
|
||||||
|
ra.timeTimer.Stop()
|
||||||
|
ra.stopTimeChan <- true
|
||||||
|
ra.wg.Wait()
|
||||||
|
|
||||||
|
// Check that all metrics were flushed
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 0, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 3, len(acc.Metrics))
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[1].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[2].Time)
|
||||||
|
acc.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAggregates_manageTimeWindows_All(t *testing.T) {
|
||||||
|
windowSize := internal.Duration{Duration: time.Millisecond * 100}
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
now := time.Now()
|
||||||
|
ra := &RequestAggregates{
|
||||||
|
TimeWindows: 2,
|
||||||
|
TimeWindowSize: windowSize,
|
||||||
|
TimePercentiles: []float32{70, 80, 90},
|
||||||
|
successRegexp: regexp.MustCompile(".*success.*"),
|
||||||
|
timeTimer: time.NewTimer(windowSize.Duration),
|
||||||
|
stopTimeChan: make(chan bool, 1)}
|
||||||
|
|
||||||
|
// Add first window and start routine
|
||||||
|
ra.timeWindowSlice = append(ra.timeWindowSlice, &TimeWindow{
|
||||||
|
StartTime: now, EndTime: now.Add(windowSize.Duration), OnlyTotal: false, Percentiles: ra.TimePercentiles})
|
||||||
|
ra.wg.Add(1)
|
||||||
|
go ra.manageTimeWindows(acc)
|
||||||
|
|
||||||
|
// Check values at different points
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 1, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 0, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
time.Sleep(windowSize.Duration)
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 2, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 0, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
time.Sleep(windowSize.Duration)
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 2, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 3, len(acc.Metrics))
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration), acc.Metrics[0].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration), acc.Metrics[1].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration), acc.Metrics[2].Time)
|
||||||
|
acc.Unlock()
|
||||||
|
|
||||||
|
// Stop and wait for the process to finish
|
||||||
|
ra.timeTimer.Stop()
|
||||||
|
ra.stopTimeChan <- true
|
||||||
|
ra.wg.Wait()
|
||||||
|
|
||||||
|
// Check that all metrics were flushed
|
||||||
|
ra.timeMutex.Lock()
|
||||||
|
require.Equal(t, 0, len(ra.timeWindowSlice))
|
||||||
|
ra.timeMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 9, len(acc.Metrics))
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[3].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[4].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[5].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[6].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[7].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[8].Time)
|
||||||
|
acc.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAggregates_manageThroughputWindows(t *testing.T) {
|
||||||
|
windowSize := internal.Duration{Duration: time.Millisecond * 100}
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
now := time.Now()
|
||||||
|
ra := &RequestAggregates{
|
||||||
|
ThroughputWindows: 2,
|
||||||
|
ThroughputWindowSize: windowSize,
|
||||||
|
throughputTimer: time.NewTimer(windowSize.Duration),
|
||||||
|
stopThroughputChan: make(chan bool, 1)}
|
||||||
|
|
||||||
|
// Add first window and start routine
|
||||||
|
ra.throughputWindowSlice = append(ra.throughputWindowSlice, &ThroughputWindow{
|
||||||
|
StartTime: now, EndTime: now.Add(windowSize.Duration)})
|
||||||
|
ra.wg.Add(1)
|
||||||
|
go ra.manageThroughputWindows(acc)
|
||||||
|
|
||||||
|
// Check values at different points
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, 1, len(ra.throughputWindowSlice))
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 0, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
time.Sleep(windowSize.Duration)
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, 2, len(ra.throughputWindowSlice))
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 0, len(acc.Metrics))
|
||||||
|
acc.Unlock()
|
||||||
|
time.Sleep(windowSize.Duration)
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, 2, len(ra.throughputWindowSlice))
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 1, len(acc.Metrics))
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration), acc.Metrics[0].Time)
|
||||||
|
acc.Unlock()
|
||||||
|
|
||||||
|
// Stop and wait for the process to finish
|
||||||
|
ra.throughputTimer.Stop()
|
||||||
|
ra.stopThroughputChan <- true
|
||||||
|
ra.wg.Wait()
|
||||||
|
|
||||||
|
// Check that all metrics were flushed
|
||||||
|
ra.throughputMutex.Lock()
|
||||||
|
require.Equal(t, 0, len(ra.throughputWindowSlice))
|
||||||
|
ra.throughputMutex.Unlock()
|
||||||
|
acc.Lock()
|
||||||
|
require.Equal(t, 3, len(acc.Metrics))
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[1].Time)
|
||||||
|
require.Equal(t, now.Add(windowSize.Duration).Add(windowSize.Duration).Add(windowSize.Duration), acc.Metrics[2].Time)
|
||||||
|
acc.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAggregates_flushWindow(t *testing.T) {
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
now := time.Now()
|
||||||
|
windows := []Window{&ThroughputWindow{StartTime: now, EndTime: now.Add(time.Duration(60))}}
|
||||||
|
windows = flushWindow(windows, acc)
|
||||||
|
require.Equal(t, 0, len(windows))
|
||||||
|
require.Equal(t, 1, len(acc.Metrics))
|
||||||
|
require.Equal(t, MeasurementThroughput, acc.Metrics[0].Measurement)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAggregates_flushAllWindows(t *testing.T) {
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
now := time.Now()
|
||||||
|
windows := []Window{&ThroughputWindow{StartTime: now, EndTime: now.Add(time.Duration(60))},
|
||||||
|
&ThroughputWindow{StartTime: now.Add(time.Duration(60)), EndTime: now.Add(time.Duration(120))},
|
||||||
|
&ThroughputWindow{StartTime: now.Add(time.Duration(120)), EndTime: now.Add(time.Duration(180))}}
|
||||||
|
windows = flushAllWindows(windows, acc)
|
||||||
|
require.Equal(t, 0, len(windows))
|
||||||
|
require.Equal(t, 3, len(acc.Metrics))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAggregates_addToWindow(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
var windows []Window
|
||||||
|
// Error if there are no windows (not added)
|
||||||
|
err := addToWindow(windows, &Request{Timestamp: now.Add(time.Duration(30))})
|
||||||
|
require.Error(t, err)
|
||||||
|
// Okay when one window
|
||||||
|
firstWindow := &ThroughputWindow{StartTime: now, EndTime: now.Add(time.Duration(60))}
|
||||||
|
windows = append(windows, firstWindow)
|
||||||
|
err = addToWindow(windows, &Request{Timestamp: now.Add(time.Duration(30))})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), firstWindow.RequestsTotal)
|
||||||
|
// Okay when timestamp equal to start of window
|
||||||
|
err = addToWindow(windows, &Request{Timestamp: now})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(2), firstWindow.RequestsTotal)
|
||||||
|
// Error when timestamp equal to end of window
|
||||||
|
err = addToWindow(windows, &Request{Timestamp: now.Add(time.Duration(60))})
|
||||||
|
require.Error(t, err)
|
||||||
|
// Okay with more windows
|
||||||
|
middleWindow := &ThroughputWindow{StartTime: now.Add(time.Duration(60)), EndTime: now.Add(time.Duration(120))}
|
||||||
|
lastWindow := &ThroughputWindow{StartTime: now.Add(time.Duration(120)), EndTime: now.Add(time.Duration(180))}
|
||||||
|
windows = append(windows, middleWindow)
|
||||||
|
windows = append(windows, lastWindow)
|
||||||
|
err = addToWindow(windows, &Request{Timestamp: now.Add(time.Duration(90))})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), middleWindow.RequestsTotal)
|
||||||
|
err = addToWindow(windows, &Request{Timestamp: now.Add(time.Duration(150))})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), lastWindow.RequestsTotal)
|
||||||
|
// Error when later than last window
|
||||||
|
err = addToWindow(windows, &Request{Timestamp: now.Add(time.Duration(220))})
|
||||||
|
require.Error(t, err)
|
||||||
|
// Error when before first window
|
||||||
|
err = addToWindow(windows, &Request{Timestamp: now.Add(time.Duration(-20))})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package request_aggregates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Nanos(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "ns", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541003228260,123,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, time.Unix(0, 1462380541003228260), r.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Micros(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "us", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541003228,123,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, time.Unix(0, 1462380541003228000), r.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Milis(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "ms", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541003,123,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, time.Unix(0, 1462380541003000000), r.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Seconds(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541,123,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, time.Unix(1462380541, 0), r.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_WrongUnit(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
_, err := rp.ParseLine("1462380541003228260,123,\"thisissuccessful\"")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Layout(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: time.RFC3339Nano,
|
||||||
|
IsTimeEpoch: false, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("2006-01-02T15:04:05.999999999Z,123,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
parsed, _ := time.Parse(time.RFC3339Nano, "2006-01-02T15:04:05.999999999Z")
|
||||||
|
require.Equal(t, parsed, r.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_WrongLayout(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: time.RFC3339Nano,
|
||||||
|
IsTimeEpoch: false, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
_, err := rp.ParseLine("2006-01-02T15:04:05,123,\"thisissuccessful\"")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Int(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541,123,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, float64(123), r.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Float(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541,123.45,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, float64(123.45), r.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_NoRegexp(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541,123.45,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, r.Failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Success(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1,
|
||||||
|
ResultPosition: 2, SuccessRegexp: regexp.MustCompile(".*success.*")}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541,123.45,\"thisissuccessful\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, false, r.Failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_Failure(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1,
|
||||||
|
ResultPosition: 2, SuccessRegexp: regexp.MustCompile(".*success.*")}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
r, err := rp.ParseLine("1462380541,123.45,\"thisonefailed\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, r.Failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_TimestampOutOfBounds(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 6, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
_, err := rp.ParseLine("1462380541,123.45,\"thisissuccessful\"")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_TimeOutOfBounds(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 6}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
_, err := rp.ParseLine("1462380541,123.45,\"thisissuccessful\"")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestParser_ParseLine_SuccessOutOfBounds(t *testing.T) {
|
||||||
|
rp := &RequestParser{TimestampPosition: 0, TimestampFormat: "s", IsTimeEpoch: true, TimePosition: 1,
|
||||||
|
ResultPosition: 8, SuccessRegexp: regexp.MustCompile(".*success.*")}
|
||||||
|
|
||||||
|
// Test format nanoseconds
|
||||||
|
_, err := rp.ParseLine("1462380541,123.45,\"thisissuccessful\"")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package request_aggregates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MeasurementTime = "request_aggregates_total"
|
||||||
|
MeasurementTimeFail = "request_aggregates_fail"
|
||||||
|
MeasurementTimeSuccess = "request_aggregates_success"
|
||||||
|
FieldTimeRequests = "requests"
|
||||||
|
FieldTimeMin = "time_min"
|
||||||
|
FieldTimeMax = "time_max"
|
||||||
|
FieldTimeMean = "time_mean"
|
||||||
|
FieldTimePerc = "time_perc_"
|
||||||
|
|
||||||
|
MeasurementThroughput = "request_aggregates_throughput"
|
||||||
|
FieldThroughputTotal = "requests_total"
|
||||||
|
FieldThroughputFailed = "requests_failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Window interface {
|
||||||
|
Aggregate() ([]telegraf.Metric, error)
|
||||||
|
Add(request *Request) error
|
||||||
|
Start() time.Time
|
||||||
|
End() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeWindow struct {
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
TimesTotal []float64
|
||||||
|
TimesSuccess []float64
|
||||||
|
TimesFail []float64
|
||||||
|
Percentiles []float32
|
||||||
|
OnlyTotal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThroughputWindow struct {
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
RequestsTotal int64
|
||||||
|
RequestsFail int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *TimeWindow) Aggregate() ([]telegraf.Metric, error) {
|
||||||
|
metrics := make([]telegraf.Metric, 3)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
metrics[0], err = aggregateTimes(MeasurementTime, tw.TimesTotal, tw.Percentiles, tw.EndTime)
|
||||||
|
if err != nil {
|
||||||
|
return metrics, err
|
||||||
|
}
|
||||||
|
if !tw.OnlyTotal {
|
||||||
|
metrics[1], err = aggregateTimes(MeasurementTimeFail, tw.TimesFail, tw.Percentiles, tw.EndTime)
|
||||||
|
if err != nil {
|
||||||
|
return metrics, err
|
||||||
|
}
|
||||||
|
metrics[2], err = aggregateTimes(MeasurementTimeSuccess, tw.TimesSuccess, tw.Percentiles, tw.EndTime)
|
||||||
|
} else {
|
||||||
|
metrics = metrics[:1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *TimeWindow) Add(request *Request) error {
|
||||||
|
tw.TimesTotal = append(tw.TimesTotal, request.Time)
|
||||||
|
if !tw.OnlyTotal {
|
||||||
|
if request.Failure {
|
||||||
|
tw.TimesFail = append(tw.TimesFail, request.Time)
|
||||||
|
} else {
|
||||||
|
tw.TimesSuccess = append(tw.TimesSuccess, request.Time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *TimeWindow) Start() time.Time {
|
||||||
|
return tw.StartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *TimeWindow) End() time.Time {
|
||||||
|
return tw.EndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *ThroughputWindow) Aggregate() ([]telegraf.Metric, error) {
|
||||||
|
metrics := make([]telegraf.Metric, 1)
|
||||||
|
|
||||||
|
metric, err := telegraf.NewMetric(MeasurementThroughput, nil, map[string]interface{}{
|
||||||
|
FieldThroughputTotal: tw.RequestsTotal,
|
||||||
|
FieldThroughputFailed: tw.RequestsFail}, tw.EndTime)
|
||||||
|
metrics[0] = metric
|
||||||
|
|
||||||
|
return metrics, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *ThroughputWindow) Add(request *Request) error {
|
||||||
|
tw.RequestsTotal++
|
||||||
|
if request.Failure {
|
||||||
|
tw.RequestsFail++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *ThroughputWindow) Start() time.Time {
|
||||||
|
return tw.StartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *ThroughputWindow) End() time.Time {
|
||||||
|
return tw.EndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces a metric with the aggregates for the given times and percentiles
|
||||||
|
func aggregateTimes(name string, times []float64, percentiles []float32, endTime time.Time) (telegraf.Metric, error) {
|
||||||
|
sort.Float64s(times)
|
||||||
|
|
||||||
|
fields := map[string]interface{}{FieldTimeRequests: len(times)}
|
||||||
|
if len(times) > 0 {
|
||||||
|
fields[FieldTimeMin] = times[0]
|
||||||
|
fields[FieldTimeMax] = times[len(times)-1]
|
||||||
|
totalSum := float64(0)
|
||||||
|
for _, time := range times {
|
||||||
|
totalSum += time
|
||||||
|
}
|
||||||
|
fields[FieldTimeMean] = totalSum / float64(len(times))
|
||||||
|
|
||||||
|
for _, perc := range percentiles {
|
||||||
|
i := int(float64(len(times)) * float64(perc) / float64(100))
|
||||||
|
if i < 0 {
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
fields[FieldTimePerc+strings.Replace(fmt.Sprintf("%v", perc), ".", "_", -1)] = times[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return telegraf.NewMetric(name, nil, fields, endTime)
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
package request_aggregates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeWindow_Add(t *testing.T) {
|
||||||
|
tw := &TimeWindow{}
|
||||||
|
|
||||||
|
tw.Add(&Request{Time: 123.45})
|
||||||
|
require.Equal(t, 1, len(tw.TimesTotal))
|
||||||
|
require.Equal(t, 1, len(tw.TimesSuccess))
|
||||||
|
require.Equal(t, 0, len(tw.TimesFail))
|
||||||
|
require.Equal(t, float64(123.45), tw.TimesTotal[0])
|
||||||
|
require.Equal(t, float64(123.45), tw.TimesSuccess[0])
|
||||||
|
|
||||||
|
tw.Add(&Request{Time: 100, Failure: false})
|
||||||
|
require.Equal(t, 2, len(tw.TimesTotal))
|
||||||
|
require.Equal(t, 2, len(tw.TimesSuccess))
|
||||||
|
require.Equal(t, 0, len(tw.TimesFail))
|
||||||
|
require.Equal(t, float64(100), tw.TimesTotal[1])
|
||||||
|
require.Equal(t, float64(100), tw.TimesSuccess[1])
|
||||||
|
|
||||||
|
tw.Add(&Request{Time: 200, Failure: true})
|
||||||
|
require.Equal(t, 3, len(tw.TimesTotal))
|
||||||
|
require.Equal(t, 2, len(tw.TimesSuccess))
|
||||||
|
require.Equal(t, 1, len(tw.TimesFail))
|
||||||
|
require.Equal(t, float64(200), tw.TimesTotal[2])
|
||||||
|
require.Equal(t, float64(200), tw.TimesFail[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeWindow_Start(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
tw := &TimeWindow{StartTime: now}
|
||||||
|
require.Equal(t, now, tw.Start())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeWindow_End(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
tw := &TimeWindow{EndTime: now}
|
||||||
|
require.Equal(t, now, tw.End())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeWindow_Aggregate_All(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
end := start.Add(time.Duration(60))
|
||||||
|
tw := &TimeWindow{StartTime: start, EndTime: end, OnlyTotal: false}
|
||||||
|
metrics, err := tw.Aggregate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 3, len(metrics))
|
||||||
|
require.Equal(t, end, metrics[0].Time())
|
||||||
|
require.Equal(t, MeasurementTime, metrics[0].Name())
|
||||||
|
require.Equal(t, end, metrics[1].Time())
|
||||||
|
require.Equal(t, MeasurementTimeFail, metrics[1].Name())
|
||||||
|
require.Equal(t, end, metrics[0].Time())
|
||||||
|
require.Equal(t, MeasurementTimeSuccess, metrics[2].Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeWindow_Aggregate_OnlyTotal(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
end := start.Add(time.Duration(60))
|
||||||
|
tw := &TimeWindow{StartTime: start, EndTime: end, OnlyTotal: true}
|
||||||
|
metrics, err := tw.Aggregate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(metrics))
|
||||||
|
require.Equal(t, end, metrics[0].Time())
|
||||||
|
require.Equal(t, MeasurementTime, metrics[0].Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeWindow_aggregateTimes(t *testing.T) {
|
||||||
|
end := time.Now()
|
||||||
|
metric, err := aggregateTimes(MeasurementTime, []float64{500, 900, 300, 1000, 100, 600, 700, 800, 200, 400},
|
||||||
|
[]float32{60, 80, 99.9}, end)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, MeasurementTime, metric.Name())
|
||||||
|
require.Equal(t, int64(10), metric.Fields()[FieldTimeRequests])
|
||||||
|
require.Equal(t, float64(1000), metric.Fields()[FieldTimeMax])
|
||||||
|
require.Equal(t, float64(100), metric.Fields()[FieldTimeMin])
|
||||||
|
require.Equal(t, float64(550), metric.Fields()[FieldTimeMean])
|
||||||
|
require.Equal(t, float64(700), metric.Fields()[FieldTimePerc+"60"])
|
||||||
|
require.Equal(t, float64(900), metric.Fields()[FieldTimePerc+"80"])
|
||||||
|
require.Equal(t, float64(1000), metric.Fields()[FieldTimePerc+"99_9"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThroughputWindow_Add(t *testing.T) {
|
||||||
|
tw := &ThroughputWindow{}
|
||||||
|
|
||||||
|
tw.Add(&Request{})
|
||||||
|
require.Equal(t, int64(1), tw.RequestsTotal)
|
||||||
|
require.Equal(t, int64(0), tw.RequestsFail)
|
||||||
|
|
||||||
|
tw.Add(&Request{Failure: false})
|
||||||
|
require.Equal(t, int64(2), tw.RequestsTotal)
|
||||||
|
require.Equal(t, int64(0), tw.RequestsFail)
|
||||||
|
|
||||||
|
tw.Add(&Request{Failure: true})
|
||||||
|
require.Equal(t, int64(3), tw.RequestsTotal)
|
||||||
|
require.Equal(t, int64(1), tw.RequestsFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThroughputWindow_Start(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
tw := &ThroughputWindow{StartTime: now}
|
||||||
|
require.Equal(t, now, tw.Start())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThroughputWindow_End(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
tw := &ThroughputWindow{EndTime: now}
|
||||||
|
require.Equal(t, now, tw.End())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThroughputWindow_Aggregate(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
end := start.Add(time.Duration(60))
|
||||||
|
tw := &ThroughputWindow{StartTime: start, EndTime: end, RequestsTotal: 33, RequestsFail: 11}
|
||||||
|
metrics, err := tw.Aggregate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(metrics))
|
||||||
|
require.Equal(t, end, metrics[0].Time())
|
||||||
|
require.Equal(t, MeasurementThroughput, metrics[0].Name())
|
||||||
|
require.Equal(t, int64(33), metrics[0].Fields()[FieldThroughputTotal])
|
||||||
|
require.Equal(t, int64(11), metrics[0].Fields()[FieldThroughputFailed])
|
||||||
|
}
|
Loading…
Reference in New Issue