Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: false
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v8
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.49
version: v2.7
args: --issues-exit-code=0
only-new-issues: true
# Optional: golangci-lint command line arguments.
Expand Down
98 changes: 47 additions & 51 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,76 +1,72 @@
version: "2"

linters:
disable-all: true
default: none
enable:
- gosimple
- ineffassign
- errcheck
- misspell
- unparam
- gofmt
- goimports
- deadcode
- nestif
- govet
- golint
- revive
- prealloc
- depguard
- dogsled
- dupl
- goconst
- gocritic
- gocyclo
- goprintffuncname
- gosec
- nakedret
- rowserrcheck
- scopelint
- structcheck
- stylecheck
- typecheck
- staticcheck
- unconvert
- varcheck
- exhaustive
- exportloopref
- goerr113
- gofumpt
- copyloopvar
- err113
- unused
exclusions:
paths:
- testdata
rules:
- linters:
- gosec
text: "G204: Subprocess launched"
- linters:
- err113
text: "err113: do not define dynamic errors"
- linters:
- staticcheck
text: "ST1003: struct field Https"
- linters:
- staticcheck
text: "ST1003: struct field Id"
settings:
dupl:
threshold: 100
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
misspell:
locale: US

formatters:
enable:
- gofmt
- goimports
- gofumpt
exclusions:
paths:
- testdata
settings:
goimports:
local-prefixes:
- github.com/nakabonne/ali

run:
issues-exit-code: 0
tests: false
skip-dirs:
- testdata

issues:
exclude-rules:
- linters:
- gosec
text: "G204: Subprocess launched"
- linters:
- goerr113
text: "err113: do not define dynamic errors"
- linters:
- stylecheck
text: "ST1003: struct field Https"
- linters:
- stylecheck
text: "ST1003: struct field Id"

linters-settings:
dupl:
threshold: 100
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
goimports:
local-prefixes: github.com/nakabonne/ali
golint:
min-confidence: 0.3
maligned:
suggest-new: true
misspell:
locale: US

13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Flags:
-c, --connections int Amount of maximum open idle connections per target host (default 10000)
--debug Run in debug mode.
-d, --duration duration The amount of time to issue requests to the targets. Give 0s for an infinite attack. (default 10s)
--export-to string Export results to the given directory
-H, --header stringArray A request header to be sent. Can be used multiple times to send multiple headers.
--insecure Skip TLS verification
--key string PEM encoded tls private key file to use
Expand All @@ -100,7 +101,7 @@ Flags:
-K, --no-keepalive Don't use HTTP persistent connection.
--query-range duration The results within the given time range will be drawn on the charts (default 30s)
-r, --rate int The request rate per second to issue against the targets. Give 0 then it will send requests as fast as possible. (default 50)
--redraw-interval duration The time interval to redraw charts (default 250ms)
--redraw-interval duration Specify how often it redraws the screen (default 250ms)
--resolvers string Custom DNS resolver addresses; comma-separated list.
-t, --timeout duration The timeout for each request. 0s means to disable timeouts. (default 30s)
-v, --version Print the current version.
Expand Down Expand Up @@ -174,6 +175,16 @@ With the help of [mum4k/termdash](https://github.com/mum4k/termdash) can be used

![Screenshot](images/mouse-support.gif)

### Export results

You can persist load test results for downstream processing.

```bash
ali --export-to ./results/
```

See [here](./docs/export.md) more details.

## Acknowledgements
This project would not have been possible without the effort of many individuals and projects but especially [vegeta](https://github.com/tsenart/vegeta) for the inspiration and powerful API.
Besides, `ali` is built with [termdash](https://github.com/mum4k/termdash) (as well as [termbox-go](https://github.com/nsf/termbox-go)) for the rendering of all those fancy graphs on the terminal.
Expand Down
77 changes: 73 additions & 4 deletions attacker/attacker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"fmt"
"log"
"math"
Expand All @@ -13,6 +15,7 @@

vegeta "github.com/tsenart/vegeta/v12/lib"

"github.com/nakabonne/ali/export"

Check failure on line 18 in attacker/attacker.go

View workflow job for this annotation

GitHub Actions / lint

import 'github.com/nakabonne/ali/export' is not allowed from list 'Main' (depguard)
"github.com/nakabonne/ali/storage"
)

Expand Down Expand Up @@ -52,14 +55,17 @@
TLSCertificates []tls.Certificate

Attacker backedAttacker

Exporter *export.FileExporter
IDGenerator func() string
}

type Attacker interface {
// Attack keeps the request running for the specified period of time.
// Results are sent to the given channel as soon as they arrive.
// When the attack is over, it gives back final statistics.
// TODO: Use storage instead of metricsCh
Attack(ctx context.Context, metricsCh chan *Metrics)
Attack(ctx context.Context, metricsCh chan *Metrics) error

// Rate gives back the rate set to itself.
Rate() int
Expand Down Expand Up @@ -140,6 +146,8 @@
tlsCertificates: opts.TLSCertificates,
attacker: opts.Attacker,
storage: storage,
exporter: opts.Exporter,
idGenerator: opts.IDGenerator,
}, nil
}

Expand Down Expand Up @@ -171,9 +179,12 @@

attacker backedAttacker
storage storage.Writer

exporter *export.FileExporter
idGenerator func() string
}

func (a *attacker) Attack(ctx context.Context, metricsCh chan *Metrics) {
func (a *attacker) Attack(ctx context.Context, metricsCh chan *Metrics) error {
rate := vegeta.Rate{Freq: a.rate, Per: time.Second}
targeter := vegeta.NewStaticTargeter(vegeta.Target{
Method: a.method,
Expand All @@ -186,12 +197,34 @@
if len(a.buckets) > 0 {
metrics.Histogram = &vegeta.Histogram{Buckets: a.buckets}
}
idGenerator := a.idGenerator
if idGenerator == nil {
idGenerator = defaultIDGenerator
}

var runExporter *export.Run
if a.exporter != nil {
var err error
runExporter, err = a.exporter.StartRun(export.Meta{
ID: idGenerator(),
TargetURL: a.target,
Method: a.method,
Rate: a.rate,
Duration: a.duration,
})
if err != nil {
return err
}
}

for res := range a.attacker.Attack(targeter, rate, a.duration, "main") {
select {
case <-ctx.Done():
a.attacker.Stop()
return
if runExporter != nil {
_ = runExporter.Abort()
}
return nil
default:
metrics.Add(res)
m := newMetrics(metrics)
Expand All @@ -208,17 +241,53 @@
log.Printf("failed to insert results")
continue
}
if runExporter != nil {
if err := runExporter.WriteResult(export.Result{
Timestamp: res.Timestamp,
LatencyNS: float64(res.Latency.Nanoseconds()),
URL: a.target,
Method: a.method,
StatusCode: res.Code,
}); err != nil {
_ = runExporter.Abort()
return err
}
}
metricsCh <- m
}
}
metrics.Close()
metricsCh <- newMetrics(metrics)
finalMetrics := newMetrics(metrics)
metricsCh <- finalMetrics
if runExporter != nil {
if err := runExporter.Close(newSummary(a.target, a.method, a.rate, a.duration, finalMetrics)); err != nil {
return err
}
}
return nil
}

func (a *attacker) Rate() int {
return a.rate
}

func defaultIDGenerator() string {
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {
return "00000000-0000-0000-0000-000000000000"
}
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80

part1 := binary.BigEndian.Uint32(b[0:4])
part2 := binary.BigEndian.Uint16(b[4:6])
part3 := binary.BigEndian.Uint16(b[6:8])
part4 := binary.BigEndian.Uint16(b[8:10])
part5 := uint64(b[10])<<40 | uint64(b[11])<<32 | uint64(b[12])<<24 | uint64(b[13])<<16 | uint64(b[14])<<8 | uint64(b[15])

return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", part1, part2, part3, part4, part5)
}

func (a *attacker) Duration() time.Duration {
return a.duration
}
Expand Down
3 changes: 2 additions & 1 deletion attacker/attacker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ func TestAttack(t *testing.T) {
a, err := NewAttacker(&storage.FakeStorage{}, tt.target, &tt.opts)
require.NoError(t, err)
metricsCh := make(chan *Metrics, 100)
a.Attack(ctx, metricsCh)
err = a.Attack(ctx, metricsCh)
require.NoError(t, err)
})
}
}
4 changes: 2 additions & 2 deletions attacker/fake_attacker.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ type FakeAttacker struct {
method string
}

func (f *FakeAttacker) Attack(ctx context.Context, metricsCh chan *Metrics) {

func (f *FakeAttacker) Attack(ctx context.Context, metricsCh chan *Metrics) error {
return nil
}

func (f *FakeAttacker) Rate() int {
Expand Down
Loading
Loading