1 Commits

Author SHA1 Message Date
4f66db0d1b Merge 2e6110e54a into e3cbe1e9b4 2024-11-26 21:30:28 +00:00
11 changed files with 119 additions and 156 deletions

View File

@ -49,7 +49,7 @@ dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
``` ```
With Docker: With Docker:
```sh ```sh
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 docker run --rm aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
``` ```
### 2. JSON config file ### 2. JSON config file
@ -85,7 +85,7 @@ dodo -c /path/config.json
``` ```
With Docker: With Docker:
```sh ```sh
docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo
``` ```
### 3. Both (CLI & JSON) ### 3. Both (CLI & JSON)
@ -96,7 +96,7 @@ dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000
``` ```
With Docker: With Docker:
```sh ```sh
docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
``` ```
## CLI and JSON Config Parameters ## CLI and JSON Config Parameters

View File

@ -6,13 +6,12 @@ import (
"os" "os"
"time" "time"
. "github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
) )
const ( const (
VERSION string = "0.5.3" VERSION string = "0.5.2"
DefaultUserAgent string = "Dodo/" + VERSION DefaultUserAgent string = "Dodo/" + VERSION
ProxyCheckURL string = "https://www.google.com" ProxyCheckURL string = "https://www.google.com"
DefaultMethod string = "GET" DefaultMethod string = "GET"
@ -98,7 +97,7 @@ type Config struct {
Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"` Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"`
DodosCount uint `json:"dodos_count" validate:"gte=1"` DodosCount uint `json:"dodos_count" validate:"gte=1"`
RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"` RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"`
NoProxyCheck Option[bool] `json:"no_proxy_check"` NoProxyCheck utils.Option[bool] `json:"no_proxy_check"`
} }
func NewConfig( func NewConfig(
@ -106,10 +105,10 @@ func NewConfig(
timeout uint32, timeout uint32,
dodosCount uint, dodosCount uint,
requestCount uint, requestCount uint,
noProxyCheck Option[bool], noProxyCheck utils.Option[bool],
) *Config { ) *Config {
if noProxyCheck == nil { if noProxyCheck == nil {
noProxyCheck = NewNoneOption[bool]() noProxyCheck = utils.NewNoneOption[bool]()
} }
return &Config{ return &Config{
@ -156,7 +155,7 @@ func (config *Config) SetDefaults() {
config.RequestCount = DefaultRequestCount config.RequestCount = DefaultRequestCount
} }
if config.NoProxyCheck.IsNone() { if config.NoProxyCheck.IsNone() {
config.NoProxyCheck = NewOption(false) config.NoProxyCheck = utils.NewOption(false)
} }
} }

14
go.mod
View File

@ -4,11 +4,11 @@ go 1.23.2
require ( require (
github.com/go-playground/validator/v10 v10.23.0 github.com/go-playground/validator/v10 v10.23.0
github.com/jedib0t/go-pretty/v6 v6.6.5 github.com/jedib0t/go-pretty/v6 v6.6.2
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/valyala/fasthttp v1.58.0 github.com/valyala/fasthttp v1.57.0
golang.org/x/net v0.32.0 golang.org/x/net v0.31.0
) )
require ( require (
@ -22,8 +22,8 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.20.0 // indirect
) )

28
go.sum
View File

@ -15,8 +15,8 @@ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo= github.com/jedib0t/go-pretty/v6 v6.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM=
github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@ -37,20 +37,20 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,7 +5,6 @@ import (
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
"github.com/aykhans/dodo/custom_errors" "github.com/aykhans/dodo/custom_errors"
. "github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -62,7 +61,7 @@ func CLIConfigReader() (*config.CLIConfig, error) {
case "timeout": case "timeout":
cliConfig.Timeout = timeout cliConfig.Timeout = timeout
case "no-proxy-check": case "no-proxy-check":
cliConfig.NoProxyCheck = NewOption(noProxyCheck) cliConfig.NoProxyCheck = utils.NewOption(noProxyCheck)
} }
}) })
if returnNil { if returnNil {

View File

@ -1,106 +1,85 @@
package requests package requests
import ( import (
"fmt"
"os" "os"
"time" "time"
. "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
) )
type Response struct { type Response struct {
Response string StatusCode int
Error error
Time time.Duration Time time.Duration
} }
type Responses []*Response type Responses []*Response
// Print prints the responses in a tabular format, including information such as // Print prints the responses in a tabular format, including information such as
// response count, minimum time, maximum time, average time, and latency percentiles. // response count, minimum time, maximum time, and average time.
func (respones Responses) Print() { func (respones Responses) Print() {
total := struct { var (
Count int totalMinDuration time.Duration = respones[0].Time
Min time.Duration totalMaxDuration time.Duration = respones[0].Time
Max time.Duration totalDuration time.Duration
Sum time.Duration totalCount int = len(respones)
P90 time.Duration )
P95 time.Duration mergedResponses := make(map[string][]time.Duration)
P99 time.Duration
}{
Count: len(respones),
Min: respones[0].Time,
Max: respones[0].Time,
}
mergedResponses := make(map[string]Durations)
var allDurations Durations
for _, response := range respones { for _, response := range respones {
if response.Time < total.Min { if response.Time < totalMinDuration {
total.Min = response.Time totalMinDuration = response.Time
} }
if response.Time > total.Max { if response.Time > totalMaxDuration {
total.Max = response.Time totalMaxDuration = response.Time
} }
total.Sum += response.Time totalDuration += response.Time
mergedResponses[response.Response] = append( if response.Error != nil {
mergedResponses[response.Response], mergedResponses[response.Error.Error()] = append(
mergedResponses[response.Error.Error()],
response.Time,
)
} else {
mergedResponses[fmt.Sprintf("%d", response.StatusCode)] = append(
mergedResponses[fmt.Sprintf("%d", response.StatusCode)],
response.Time, response.Time,
) )
allDurations = append(allDurations, response.Time)
} }
allDurations.Sort() }
allDurationsLenAsFloat := float64(len(allDurations) - 1)
total.P90 = allDurations[int(0.90*allDurationsLenAsFloat)]
total.P95 = allDurations[int(0.95*allDurationsLenAsFloat)]
total.P99 = allDurations[int(0.99*allDurationsLenAsFloat)]
t := table.NewWriter() t := table.NewWriter()
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight) t.SetStyle(table.StyleLight)
t.SetColumnConfigs([]table.ColumnConfig{ t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, WidthMax: 40}, {Number: 1, WidthMax: 80},
}) })
t.AppendHeader(table.Row{ t.AppendHeader(table.Row{
"Response", "Response",
"Count", "Count",
"Min Time", "Min Time",
"Max Time", "Max Time",
"Average Time", "Average Time",
"P90",
"P95",
"P99",
}) })
for key, durations := range mergedResponses { for key, durations := range mergedResponses {
durations.Sort()
durationsLen := len(durations)
durationsLenAsFloat := float64(durationsLen - 1)
t.AppendRow(table.Row{ t.AppendRow(table.Row{
key, key,
durationsLen, len(durations),
durations.First(), utils.MinDuration(durations...),
durations.Last(), utils.MaxDuration(durations...),
durations.Avg(), utils.AvgDuration(durations...),
durations[int(0.90*durationsLenAsFloat)],
durations[int(0.95*durationsLenAsFloat)],
durations[int(0.99*durationsLenAsFloat)],
}) })
t.AppendSeparator() t.AppendSeparator()
} }
if len(mergedResponses) > 1 {
t.AppendRow(table.Row{ t.AppendRow(table.Row{
"Total", "Total",
total.Count, totalCount,
total.Min, totalMinDuration,
total.Max, totalMaxDuration,
total.Sum / time.Duration(total.Count), // Average totalDuration / time.Duration(totalCount),
total.P90,
total.P95,
total.P99,
}) })
}
t.Render() t.Render()
} }

View File

@ -2,7 +2,6 @@ package requests
import ( import (
"context" "context"
"strconv"
"sync" "sync"
"time" "time"
@ -143,7 +142,8 @@ func sendRequest(
return return
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
Response: err.Error(), StatusCode: 0,
Error: err,
Time: completedTime, Time: completedTime,
}) })
increase <- 1 increase <- 1
@ -151,7 +151,8 @@ func sendRequest(
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
Response: strconv.Itoa(response.StatusCode()), StatusCode: response.StatusCode(),
Error: nil,
Time: completedTime, Time: completedTime,
}) })
increase <- 1 increase <- 1

View File

@ -1,41 +0,0 @@
package types
import (
"sort"
"time"
)
type Durations []time.Duration
func (d Durations) Sort(ascending ...bool) {
// If ascending is provided and is false, sort in descending order
if len(ascending) > 0 && ascending[0] == false {
sort.Slice(d, func(i, j int) bool {
return d[i] > d[j]
})
} else { // Otherwise, sort in ascending order
sort.Slice(d, func(i, j int) bool {
return d[i] < d[j]
})
}
}
func (d Durations) First() *time.Duration {
return &d[0]
}
func (d Durations) Last() *time.Duration {
return &d[len(d)-1]
}
func (d Durations) Sum() time.Duration {
sum := time.Duration(0)
for _, duration := range d {
sum += duration
}
return sum
}
func (d Durations) Avg() time.Duration {
return d.Sum() / time.Duration(len(d))
}

31
utils/time.go Normal file
View File

@ -0,0 +1,31 @@
package utils
import "time"
func MinDuration(durations ...time.Duration) time.Duration {
min := durations[0]
for _, d := range durations {
if d < min {
min = d
}
}
return min
}
func MaxDuration(durations ...time.Duration) time.Duration {
max := durations[0]
for _, d := range durations {
if d > max {
max = d
}
}
return max
}
func AvgDuration(durations ...time.Duration) time.Duration {
total := time.Duration(0)
for _, d := range durations {
total += d
}
return total / time.Duration(len(durations))
}

View File

@ -1,4 +1,4 @@
package types package utils
import ( import (
"encoding/json" "encoding/json"
@ -65,17 +65,12 @@ func (o *option[T]) SetNone() {
} }
func (o *option[T]) UnmarshalJSON(data []byte) error { func (o *option[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" || len(data) == 0 { if string(data) == "null" {
o.SetNone() o.none = true
return nil return nil
} }
if err := json.Unmarshal(data, &o.value); err != nil {
o.SetNone()
return err
}
o.none = false o.none = false
return nil return json.Unmarshal(data, &o.value)
} }
func NewOption[T NonNilT](value T) *option[T] { func NewOption[T NonNilT](value T) *option[T] {