├── .github └── workflows │ ├── assets │ └── img │ │ ├── hash.png │ │ └── leastbytes.png │ ├── release.yml │ └── production.yml ├── Dockerfile ├── pkg ├── stringgenerator │ └── main.go ├── kafkadialer │ └── main.go ├── clients │ └── main.go └── fakejson │ └── main.go ├── go.mod ├── .goreleaser.yml ├── LICENSE ├── docs └── DEVELOPMENT.md ├── docker-compose.yml ├── main.go ├── README.md └── go.sum /.github/workflows/assets/img/hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msfidelis/kafka-stress/HEAD/.github/workflows/assets/img/hash.png -------------------------------------------------------------------------------- /.github/workflows/assets/img/leastbytes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msfidelis/kafka-stress/HEAD/.github/workflows/assets/img/leastbytes.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 AS builder 2 | 3 | WORKDIR $GOPATH/src/kafka-stress 4 | 5 | COPY . ./ 6 | 7 | RUN go get -u 8 | 9 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kafka-stress . 10 | 11 | 12 | FROM cgr.dev/chainguard/wolfi-base:latest 13 | 14 | COPY --from=builder /go/src/kafka-stress/kafka-stress ./ 15 | 16 | 17 | ENTRYPOINT ["./kafka-stress"] -------------------------------------------------------------------------------- /pkg/stringgenerator/main.go: -------------------------------------------------------------------------------- 1 | package stringgenerator 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 8 | 9 | // RandStringBytes Gerenerate random string bytes size 10 | func RandStringBytes(n int) string { 11 | b := make([]byte, n) 12 | for i := range b { 13 | b[i] = letterBytes[rand.Intn(len(letterBytes))] 14 | } 15 | return string(b) 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module kafka-stress 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/bxcodec/faker/v3 v3.8.1 7 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect 8 | github.com/frankban/quicktest v1.11.3 // indirect 9 | github.com/golang/snappy v0.0.4 // indirect 10 | github.com/google/uuid v1.3.0 11 | github.com/klauspost/compress v1.15.14 // indirect 12 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 13 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 14 | github.com/segmentio/kafka-go v0.4.38 15 | golang.org/x/tools v0.1.9 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | release: 2 | prerelease: false 3 | 4 | builds: 5 | - binary: kafka-stress 6 | env: 7 | - TOP=0 8 | goos: 9 | - windows 10 | - darwin 11 | - linux 12 | - freebsd 13 | goarch: 14 | - amd64 15 | - arm64 16 | 17 | # brews: 18 | # - github: 19 | # owner: msfidelis 20 | # name: homebrew-kafka-stress 21 | # homepage: "https://github.com/msfidelis/kafka-stress/" 22 | # description: "Kafka Producer / Consumer Stress Test Tool" 23 | 24 | archives: 25 | - format: binary 26 | format_overrides: 27 | - goos: windows 28 | format: zip -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'release packages' 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - 11 | name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - 16 | name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.22 20 | - 21 | name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v2 23 | with: 24 | version: latest 25 | args: release --rm-dist 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /pkg/kafkadialer/main.go: -------------------------------------------------------------------------------- 1 | package kafkadialer 2 | 3 | import ( 4 | "crypto/tls" 5 | "os" 6 | "time" 7 | 8 | "github.com/segmentio/kafka-go" 9 | ) 10 | 11 | // GetDialer generate a kafka.Dialer instance configured 12 | func GetDialer(ssl bool) kafka.Dialer { 13 | var dialer kafka.Dialer 14 | name, err := os.Hostname() 15 | 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | if ssl { 21 | dialer = kafka.Dialer{ 22 | Timeout: 20 * time.Second, 23 | DualStack: true, 24 | ClientID: name, 25 | TLS: &tls.Config{}, 26 | } 27 | } else { 28 | dialer = kafka.Dialer{ 29 | Timeout: 20 * time.Second, 30 | DualStack: true, 31 | ClientID: name, 32 | } 33 | } 34 | 35 | return dialer 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matheus Fidelis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/clients/main.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | "kafka-stress/pkg/kafkadialer" 5 | "strings" 6 | "time" 7 | 8 | "github.com/segmentio/kafka-go" 9 | ) 10 | 11 | // GetConsumer return a Kafka Consumer Client 12 | func GetConsumer(bootstrapServers, topic, consumerGroup string, consumer int, ssl bool) *kafka.Reader { 13 | dialer := kafkadialer.GetDialer(ssl) 14 | 15 | return kafka.NewReader(kafka.ReaderConfig{ 16 | Brokers: strings.Split(bootstrapServers, ","), 17 | Topic: topic, 18 | GroupID: consumerGroup, 19 | Dialer: &dialer, 20 | MinBytes: 10e3, // 10KB 21 | MaxBytes: 10e6, // 10MB 22 | CommitInterval: time.Second, 23 | }) 24 | } 25 | 26 | // GetProducer return a Kafka Producer Client 27 | func GetProducer(bootstrapServers string, topic string, batchSize int, acks int, ssl bool, balancer string) *kafka.Writer { 28 | 29 | dialer := kafkadialer.GetDialer(ssl) 30 | 31 | var config kafka.WriterConfig 32 | 33 | config = kafka.WriterConfig{ 34 | Brokers: strings.Split(bootstrapServers, ","), 35 | Topic: topic, 36 | Balancer: &kafka.LeastBytes{}, 37 | BatchSize: batchSize, 38 | BatchTimeout: 2 * time.Second, 39 | RequiredAcks: acks, 40 | Dialer: &dialer, 41 | WriteTimeout: 10 * time.Second, 42 | ReadTimeout: 10 * time.Second, 43 | } 44 | 45 | switch balancer { 46 | case "hash": 47 | config.Balancer = &kafka.Hash{} 48 | case "murmur2": 49 | config.Balancer = &kafka.Murmur2Balancer{} 50 | case "crc32": 51 | config.Balancer = &kafka.CRC32Balancer{} 52 | default: 53 | config.Balancer = &kafka.Hash{} 54 | } 55 | 56 | return kafka.NewWriter(config) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Setup local environment 2 | 3 | ### Setup Development Dependencies with Docker 4 | 5 | ```bash 6 | docker-compose up --force-recreate 7 | ``` 8 | 9 | ### Create a test topic 10 | 11 | 12 | ```bash 13 | docker-compose exec broker kafka-topics --create --topic kafka-stress --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper:2181 14 | ``` 15 | 16 | 17 | # Schema Registry 18 | 19 | ## Utils 20 | 21 | [Json to AVRO Converter](https://toolslick.com/generation/metadata/avro-schema-from-json) 22 | 23 | ## List Schemas 24 | 25 | ```bash 26 | curl -X GET http://0.0.0.0:8081/subjects 27 | ``` 28 | 29 | ## AVRO 30 | 31 | Create and Schema in AVRO format 32 | 33 | ``` 34 | curl http://0.0.0.0:8081/subjects/example/versions -X POST \ 35 | -H "Content-Type: application/vnd.schemaregistry.v1+json" \ 36 | -d ' 37 | { 38 | "schema":"{\n \"name\":\"Example\",\n \"type\":\"record\",\n \"namespace\":\"com.acme.avro\",\n \"fields\":[\n {\n \"name\":\"name\",\n \"type\":\"string\"\n },\n {\n \"name\":\"age\",\n \"type\":\"int\"\n }\n ]\n }" 39 | } 40 | ' 41 | ``` 42 | 43 | 44 | 45 | # Consumer 46 | 47 | ## Algoritms 48 | 49 | Producing 15000 events in 3 partitions topic 50 | 51 | ### Hash 52 | 53 | ```bash 54 | ❯ go run main.go --bootstrap-servers 0.0.0.0:9092 --events 15000 --topic brabo --test-mode producer --consumers 3 55 | Sent 15000 messages to topic brabo with 0 errors 56 | Tests finished in 1.406677516s. Producer mean time 10663.42/s 57 | ``` 58 | 59 | ### LeastBytes 60 | 61 | ```bash 62 | ❯ go run main.go --bootstrap-servers 0.0.0.0:9092 --events 15000 --topic brabo --test-mode producer --consumers 3 63 | Sent 15000 messages to topic brabo with 0 errors 64 | Tests finished in 885.728755ms. Producer mean time 16935.21/s 65 | ``` 66 | 67 | [!LeastBytes]() -------------------------------------------------------------------------------- /pkg/fakejson/main.go: -------------------------------------------------------------------------------- 1 | package fakejson 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bxcodec/faker/v3" 6 | "encoding/json" 7 | ) 8 | 9 | type fake struct { 10 | UserName string `faker:"username" json:"username"` 11 | PhoneNumber string `faker:"phone_number" json:"phone_number"` 12 | IPV4 string `faker:"ipv4" json:"ipv4"` 13 | IPV6 string `faker:"ipv6" json:"ipv6"` 14 | MacAddress string `faker:"mac_address" json:"mac_address"` 15 | URL string `faker:"url" json: "url"` 16 | DayOfWeek string `faker:"day_of_week" json: "day_of_week"` 17 | DayOfMonth string `faker:"day_of_month" json: "day_of_month"` 18 | Timestamp string `faker:"timestamp" json: "timestamp"` 19 | Century string `faker:"century" json: "century"` 20 | TimeZone string `faker:"timezone", json:"timezone"` 21 | TimePeriod string `faker:"time_period" json:"time_period"` 22 | Word string `faker:"word" json:"word"` 23 | Sentence string `faker:"sentence" json:"sentence"` 24 | Paragraph string `faker:"paragraph" json:"paragraph"` 25 | Currency string `faker:"currency" json:"currency"` 26 | Amount float64 `faker:"amount" json:"amount" ` 27 | AmountWithCurrency string `faker:"amount_with_currency" json:"amount_with_currency"` 28 | UUIDHypenated string `faker:"uuid_hyphenated" json:"uuid_hyphenated"` 29 | UUID string `faker:"uuid_digit" json:"uuid_digit"` 30 | PaymentMethod string `faker:"oneof: cc, paypal, check, money order"` 31 | } 32 | 33 | // RandJSONPayload Gerenerate random json with fake data 34 | func RandJSONPayload() string { 35 | 36 | a := fake{} 37 | err := faker.FakeData(&a) 38 | if err != nil { 39 | fmt.Println(err) 40 | return "{}" 41 | } 42 | 43 | b, err := json.Marshal(a) 44 | if err != nil { 45 | fmt.Println(err) 46 | return "{}" 47 | } 48 | return string(b) 49 | 50 | } -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: 'kafka-stress ci' 2 | on: 3 | push: 4 | pull_request: 5 | types: [ opened, reopened ] 6 | jobs: 7 | unit-test: 8 | strategy: 9 | matrix: 10 | go-version: [1.22.x] 11 | platform: [ubuntu-latest, macos-latest] 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | 15 | - uses: actions/setup-go@v1 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: setup GOPATH into PATH 20 | run: | 21 | echo "::set-env name=GOPATH::$(go env GOPATH)" 22 | echo "::add-path::$(go env GOPATH)/bin" 23 | shell: bash 24 | env: 25 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 26 | 27 | - uses: actions/checkout@v2 28 | 29 | - name: Install dependencies 30 | run: go get -u 31 | 32 | - name: Test 33 | run: go test -v 34 | 35 | build-docker-artifacts: 36 | needs: [ unit-test ] 37 | runs-on: ubuntu-latest 38 | if: contains(github.ref, 'main') 39 | steps: 40 | - uses: actions/setup-go@v1 41 | with: 42 | go-version: '1.22.x' 43 | 44 | - uses: actions/checkout@v1 45 | 46 | - name: Docker Build 47 | run: docker build -t kafka-stress:latest . 48 | 49 | - name: Docker Tag Latest 50 | run: docker tag kafka-stress:latest fidelissauro/kafka-stress:latest 51 | 52 | - name: Docker Tag Latest Release 53 | run: | 54 | TAG=$(git describe --tags --abbrev=0) 55 | docker tag kafka-stress:latest fidelissauro/kafka-stress:$TAG 56 | - name: Login to DockerHub 57 | uses: docker/login-action@v1 58 | with: 59 | username: ${{ secrets.DOCKER_USERNAME }} 60 | password: ${{ secrets.DOCKER_PASSWORD}} 61 | 62 | - name: Docker Push Latest 63 | run: docker push fidelissauro/kafka-stress:latest 64 | 65 | - name: Docker Push Release Tag 66 | run: | 67 | TAG=$(git describe --tags --abbrev=0) 68 | docker push fidelissauro/kafka-stress:$TAG -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: "3" 3 | 4 | services: 5 | zookeeper: 6 | image: confluentinc/cp-zookeeper:5.2.5 7 | hostname: zookeeper 8 | container_name: zookeeper 9 | ports: 10 | - "2181:2181" 11 | environment: 12 | ZOOKEEPER_CLIENT_PORT: 2181 13 | ZOOKEEPER_TICK_TIME: 2000 14 | 15 | broker: 16 | image: confluentinc/cp-server:5.4.0 17 | hostname: broker 18 | container_name: broker 19 | depends_on: 20 | - zookeeper 21 | ports: 22 | - "9092:9092" 23 | environment: 24 | KAFKA_BROKER_ID: 1 25 | KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" 26 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 27 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 28 | KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter 29 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 30 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 31 | KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 32 | CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092 33 | CONFLUENT_METRICS_REPORTER_ZOOKEEPER_CONNECT: zookeeper:2181 34 | CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 35 | CONFLUENT_METRICS_ENABLE: "true" 36 | CONFLUENT_SUPPORT_CUSTOMER_ID: "anonymous" 37 | 38 | schema-registry: 39 | image: confluentinc/cp-schema-registry:6.1.2 40 | hostname: schema-registry 41 | container_name: schema-registry 42 | depends_on: 43 | - zookeeper 44 | - broker 45 | ports: 46 | - "8081:8081" 47 | environment: 48 | SCHEMA_REGISTRY_HOST_NAME: schema-registry 49 | SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: "zookeeper:2181" 50 | 51 | control-center: 52 | image: confluentinc/cp-enterprise-control-center:5.4.0 53 | hostname: control-center 54 | container_name: control-center 55 | depends_on: 56 | - zookeeper 57 | - broker 58 | - schema-registry 59 | ports: 60 | - "9021:9021" 61 | environment: 62 | CONTROL_CENTER_BOOTSTRAP_SERVERS: 'broker:29092' 63 | CONTROL_CENTER_ZOOKEEPER_CONNECT: 'zookeeper:2181' 64 | CONTROL_CENTER_SCHEMA_REGISTRY_URL: "http://schema-registry:8081" 65 | CONTROL_CENTER_REPLICATION_FACTOR: 1 66 | CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1 67 | CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1 68 | CONFLUENT_METRICS_TOPIC_REPLICATION: 1 69 | PORT: 9021 -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "kafka-stress/pkg/clients" 13 | "kafka-stress/pkg/stringgenerator" 14 | "kafka-stress/pkg/fakejson" 15 | 16 | guuid "github.com/google/uuid" 17 | kafka "github.com/segmentio/kafka-go" 18 | ) 19 | 20 | func main() { 21 | topic := flag.String("topic", "kafka-stress", "Kafka Stress Topics") 22 | createTopic := flag.Bool("create-topic", false, "Auto Create Topic?") 23 | ssl := flag.Bool("ssl-enabled", false, "SSL Mode") 24 | testMode := flag.String("test-mode", "producer", "Test Type; Ex producer;consumer. Default: producer") 25 | bootstrapServers := flag.String("bootstrap-servers", "0.0.0.0:9092", "Kafka Bootstrap Servers Broker Lists") 26 | zookeeperServers := flag.String("zookeeper-servers", "0.0.0.0:2181", "Zookeeper Connection String") 27 | schemaRegistryURL := flag.String("schema-registry", "0.0.0.0:8081", "Schema Registry URL") 28 | size := flag.Int("size", 62, "Message size in bytes") 29 | acks := flag.Int("ack", 1, "Required ACKs to produce messages") 30 | batchSize := flag.Int("batch-size", 0, "Batch size for producer mode") 31 | schema := flag.String("schema", "", "Schema") 32 | events := flag.Int("events", 10000, "Numer of events will be created in topic") 33 | consumers := flag.Int("consumers", 1, "Number of consumers will be used in topic") 34 | consumerGroup := flag.String("consumer-group", "kafka-stress", "Consumer group name") 35 | format := flag.String("format", "string", "Events Format; ex string,json,avro") 36 | verbose := flag.Bool("verbose", false, "Verbose Mode; It Prints Events consumed") 37 | balancer := flag.String("balancer", "hash", "Balance algorithm for producer mode; Ex: hash,murmur2,crc32") 38 | 39 | 40 | flag.Parse() 41 | 42 | if *createTopic { 43 | createTopicBeforeTest(*topic, *zookeeperServers) 44 | } 45 | 46 | switch strings.ToLower(*testMode) { 47 | case "producer": 48 | produce(*bootstrapServers, *topic, *events, *size, *batchSize, *acks, *schemaRegistryURL, *schema, *ssl, *format, *balancer) 49 | break 50 | case "consumer": 51 | consume(*bootstrapServers, *topic, *consumerGroup, *consumers, *ssl, *verbose) 52 | default: 53 | return 54 | } 55 | } 56 | 57 | func produce(bootstrapServers string, topic string, events int, size int, batchSize int, acks int, schemaRegistryURL string, schema string, ssl bool, format string, balancer string) { 58 | 59 | var wg sync.WaitGroup 60 | var executions uint64 61 | var errors uint64 62 | var message string 63 | 64 | producer := clients.GetProducer(bootstrapServers, topic, batchSize, acks, ssl, balancer) 65 | defer producer.Close() 66 | 67 | start := time.Now() 68 | 69 | for i := 0; i < events; i++ { 70 | wg.Add(1) 71 | 72 | switch format { 73 | case "string": 74 | message = stringgenerator.RandStringBytes(size) 75 | break; 76 | case "json": 77 | message = fakejson.RandJSONPayload() 78 | break; 79 | default: 80 | message = stringgenerator.RandStringBytes(size) 81 | } 82 | 83 | go func() { 84 | msg := kafka.Message{ 85 | Key: []byte(guuid.New().String()), 86 | Value: []byte(message), 87 | } 88 | 89 | err := producer.WriteMessages(context.Background(), msg) 90 | if err != nil { 91 | atomic.AddUint64(&errors, 1) 92 | } else { 93 | atomic.AddUint64(&executions, 1) 94 | } 95 | 96 | var multiple = executions % 1000 97 | if multiple == 0 && executions != 0 { 98 | fmt.Printf("Sent %v messages to topic %s with %v errors \n", executions, topic, errors) 99 | } 100 | 101 | wg.Done() 102 | }() 103 | } 104 | 105 | wg.Wait() 106 | elapsed := time.Since(start) 107 | meanEventsSent := float64(executions) / elapsed.Seconds() 108 | 109 | fmt.Printf("Tests finished in %v. Produce %v messages with mean time %.2f/s using %s balance algorithm \n", elapsed, executions, meanEventsSent, balancer) 110 | } 111 | 112 | func consume(bootstrapServers, topic, consumerGroup string, consumers int, ssl bool, verbose bool) { 113 | 114 | var wg sync.WaitGroup 115 | var counter uint64 116 | 117 | for i := 0; i < consumers; i++ { 118 | wg.Add(1) 119 | var consumerID = i + 1 120 | consumer := clients.GetConsumer(bootstrapServers, topic, consumerGroup, consumerID, ssl) 121 | consumerName := fmt.Sprintf("%v-%v", consumerGroup, consumerID) 122 | 123 | fmt.Printf("[Consumer] Starting consumer %v\n", consumerName) 124 | 125 | go func() { 126 | for { 127 | m, err := consumer.ReadMessage(context.Background()) 128 | if err != nil { 129 | wg.Done() 130 | break 131 | } 132 | 133 | atomic.AddUint64(&counter, 1) 134 | 135 | if verbose == true { 136 | fmt.Printf("[Key] %s | [Value] %s\n\n\n", m.Key, m.Value) 137 | } 138 | 139 | var multiple = counter % 100 140 | if multiple == 0 && counter != 0 { 141 | fmt.Printf("[Consumer] %v Messages retrived from topic %v by consumer group %s \n", counter, m.Topic, consumerGroup) 142 | } 143 | } 144 | wg.Done() 145 | }() 146 | 147 | } 148 | wg.Wait() 149 | } 150 | 151 | func createTopicBeforeTest(topic string, zookeeper string) { 152 | fmt.Printf("Creating topic %s\n", topic) 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kafka Stress - Stress Test Tool for Kafka Clusters, Producers and Consumers Tunning 2 | 3 |

4 | 5 | Documentation 6 | 7 | 8 | License: MIT 9 | 10 | 11 | Build CI 12 | 13 | 14 | Release 15 | 16 | 17 | Twitter: fidelissauro 18 | 19 |

20 | 21 | # Introduction 22 | 23 | > Kafka Stress is a simple CLI Tool to produce and consume events on Kafka topics in high throughput to tests and validate brokers, topics, partitions and consumers performances. 24 | 25 | # Installation 26 | 27 | ### Docker 28 | 29 | ```bash 30 | docker pull fidelissauro/kafka-stress:latest 31 | ``` 32 | 33 | ```bash 34 | docker run --network host -it fidelissauro/kafka-stress:latest --bootstrap-servers 0.0.0.0:9092 --topic kafka-stress --test-mode consumer --consumers 6 35 | ``` 36 | 37 | ### MacOS amd64 38 | 39 | ```bash 40 | wget https://github.com/msfidelis/kafka-stress/releases/download/v0.0.9/kafka-stress_0.0.9_darwin_amd64 -O kafka-stress 41 | mv kafka-stress /usr/local/bin 42 | chmod +x /usr/local/bin/kafka-stress 43 | ``` 44 | 45 | ### MacOS arm64 46 | 47 | ```bash 48 | wget https://github.com/msfidelis/kafka-stress/releases/download/v0.0.9/kafka-stress_0.0.9_darwin_arm64 -O kafka-stress 49 | mv kafka-stress /usr/local/bin 50 | chmod +x /usr/local/bin/kafka-stress 51 | ``` 52 | 53 | ### Linux amd64 54 | 55 | ```bash 56 | wget https://github.com/msfidelis/kafka-stress/releases/download/v0.0.9/kafka-stress_0.0.9_linux_amd64 -O kafka-stress 57 | mv kafka-stress /usr/local/bin 58 | chmod +x /usr/local/bin/kafka-stress 59 | ``` 60 | 61 | ### Linux arm64 62 | 63 | ```bash 64 | wget https://github.com/msfidelis/kafka-stress/releases/download/v0.0.9/kafka-stress_0.0.9_linux_arm64 -O kafka-stress 65 | mv kafka-stress /usr/local/bin 66 | chmod +x /usr/local/bin/kafka-stress 67 | ``` 68 | 69 | ### Freebsd amd64 70 | 71 | ```bash 72 | wget https://github.com/msfidelis/kafka-stress/releases/download/v0.0.9/kafka-stress_0.0.9_freebsd_amd64 -O kafka-stress 73 | mv kafka-stress /usr/local/bin 74 | chmod +x /usr/local/bin/kafka-stress 75 | ``` 76 | 77 | ### Freebsd arm64 78 | 79 | ```bash 80 | wget https://github.com/msfidelis/kafka-stress/releases/download/v0.0.9/kafka-stress_0.0.9_freebsd_arm64 -O kafka-stress 81 | mv kafka-stress /usr/local/bin 82 | chmod +x /usr/local/bin/kafka-stress 83 | ``` 84 | 85 | # v0 Usage 86 | 87 | ```bash 88 | Usage of kafka-stress: 89 | -ack int 90 | Required ACKs to produce messages (default 1) 91 | -balancer string 92 | Balance algorithm for producer mode; Ex: hash,murmur2,crc32 (default "hash") 93 | -batch-size int 94 | Batch size for producer mode 95 | -bootstrap-servers string 96 | Kafka Bootstrap Servers Broker Lists (default "0.0.0.0:9092") 97 | -consumer-group string 98 | Consumer group name (default "kafka-stress") 99 | -consumers int 100 | Number of consumers will be used in topic (default 1) 101 | -create-topic 102 | Auto Create Topic? 103 | -events int 104 | Numer of events will be created in topic (default 10000) 105 | -format string 106 | Events Format; ex string,json,avro (default "string") 107 | -schema string 108 | Schema 109 | -schema-registry string 110 | Schema Registry URL (default "0.0.0.0:8081") 111 | -size int 112 | Message size in bytes (default 62) 113 | -ssl-enabled 114 | SSL Mode 115 | -test-mode string 116 | Test Type; Ex producer;consumer. Default: producer (default "producer") 117 | -topic string 118 | Kafka Stress Topics (default "kafka-stress") 119 | -verbose 120 | Verbose Mode; It Prints Events consumed 121 | -zookeeper-servers string 122 | Zookeeper Connection String (default "0.0.0.0:2181") 123 | ``` 124 | 125 | ## Producer 126 | 127 | ```bash 128 | kafka-stress --bootstrap-servers localhost:29092 --events 30000 --topic kafka-stress 129 | ``` 130 | 131 | ```bash 132 | kafka-stress --bootstrap-servers localhost:29092 --events 10000 --topic kafka-stress 133 | 134 | Sent 10000 messages to topic kafka-stress with 0 errors 135 | Tests finished in 1.232463918s. Producer mean time 8113.83/s 136 | ``` 137 | 138 | ### Customize ACK's 139 | 140 | Use `--ack` parameter to customize ack's quorum 141 | 142 | ```bash 143 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --events 20000 --topic tunning-3 --test-mode producer --ack 1 144 | ``` 145 | 146 | ### Producer Format 147 | 148 | You can produce random data in `string` and `json` format using `--format` flag 149 | 150 | ```bash 151 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --events 20000 --topic tunning-3 --test-mode producer --ack 1 --format json 152 | ``` 153 | 154 | 155 | ### Producer Balance Algorithms 156 | 157 | You can specify some algorithms to balance producer messages like `hash`, `murmur2` and `crc21` using `--balance` flag 158 | 159 | ```bash 160 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --events 20000 --topic tunning-3 --test-mode producer --balance murmur2 --format json 161 | ``` 162 | 163 | ## Consumer 164 | 165 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --topic kafka-stress --test-mode consumer --consumers 6 166 | ``` 167 | 168 | ```bash 169 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --events 10000 --topic kafka-stress --test-mode consumer --consumers 6 170 | 171 | ... 172 | [Consumer 6] Message from consumer group kafka-stress at topic/partition/offset kafka-stress/1/0: 65b27cc0-1053-4fbc-b7a9-a40972fcaca6 = a124e95d-9226-4eb9-9169-411318bb6e4e 173 | [Consumer 5] Message from consumer group kafka-stress at topic/partition/offset kafka-stress/2/0: 35fc905f-27f3-4b9f-81db-af89c1fa4c28 = 94f06092-589c-4030-8e3f-66a5a980123b 174 | ... 175 | ``` 176 | 177 | ### Customize consumer-group name 178 | 179 | Use `--consumer-group` to change customer group name used by workers. 180 | 181 | ```bash 182 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --topic kafka-stress --test-mode consumer --consumer-group custom-consumer-group 183 | ``` 184 | 185 | ### Verbose mode 186 | 187 | Use `--verbose` to print keys and events 188 | 189 | ```bash 190 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --topic kafka-stress --test-mode consumer --verbose 191 | ``` 192 | 193 | ## Authentication methods 194 | 195 | ### SSL 196 | 197 | Use `--ssl-enabled` to enable ssl authentication. 198 | 199 | ```bash 200 | kafka-stress --bootstrap-servers 0.0.0.0:9092 --topic kafka-stress --test-mode consumer --ssl-enabled 201 | ``` 202 | 203 | ## Roadmap 204 | 205 | Improve reports output 206 | * Add retry mechanism 207 | * Add message size options 208 | * Add test header 209 | * Add SCRAM authentication 210 | * Add SASL authetication 211 | * Add TLS authetication 212 | * Add IAM authentication for Amazon MSK 213 | * Add Unit Tests 214 | * Add time based tests 215 | * Add Schema Registry / AVRO, JSON, PROTO Support 216 | 217 | 218 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bxcodec/faker v1.5.0 h1:RIWOeAcM3ZHye1i8bQtHU2LfNOaLmHuRiCo60mNMOcQ= 2 | github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= 3 | github.com/bxcodec/faker/v3 v3.7.0 h1:qWAFFwcyVS0ukF0UoJju1wBLO0cuPQ7JdVBPggM8kNo= 4 | github.com/bxcodec/faker/v3 v3.7.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= 5 | github.com/bxcodec/faker/v3 v3.8.1 h1:qO/Xq19V6uHt2xujwpaetgKhraGCapqY2CRWGD/SqcM= 6 | github.com/bxcodec/faker/v3 v3.8.1/go.mod h1:DdSDccxF5msjFo5aO4vrobRQ8nIApg8kq3QWPEQD6+o= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 10 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 11 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 12 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 13 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 14 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 15 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 16 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 17 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 19 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 20 | github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA= 21 | github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 22 | github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 23 | github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc= 24 | github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= 25 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 28 | github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A= 29 | github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 30 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 31 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 32 | github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 33 | github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= 34 | github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/segmentio/kafka-go v0.4.16 h1:9dt78ehM9qzAkekA60D6A96RlqDzC3hnYYa8y5Szd+U= 37 | github.com/segmentio/kafka-go v0.4.16/go.mod h1:19+Eg7KwrNKy/PFhiIthEPkO8k+ac7/ZYXwYM9Df10w= 38 | github.com/segmentio/kafka-go v0.4.38 h1:iQdOBbUSdfuYlFpvjuALgj7N6DrdPA0HfB4AhREOdtg= 39 | github.com/segmentio/kafka-go v0.4.38/go.mod h1:ikyuGon/60MN/vXFgykf7Zm8P5Be49gJU6vezwjnnhU= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 42 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 45 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 46 | github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 47 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 48 | github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 49 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 50 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 51 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 52 | golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 53 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 54 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 55 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 56 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 57 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 58 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 60 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 61 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 62 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 63 | golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 64 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 76 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 79 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 80 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 81 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 82 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 83 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY= 84 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 85 | golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs= 86 | golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 87 | golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= 88 | golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 89 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 95 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | --------------------------------------------------------------------------------