├── pkg └── models │ └── models.go ├── .gitignore ├── docker-compose.yml ├── README.md ├── go.mod ├── cmd ├── producer │ └── producer.go └── consumer │ └── consumer.go └── go.sum /pkg/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID int `json:"id"` 5 | Name string `json:"name"` 6 | } 7 | 8 | type Notification struct { 9 | From User `json:"from"` 10 | To User `json:"to"` 11 | Message string `json:"message"` 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Ignore GoLand project settings 21 | .idea/ 22 | 23 | # Ignore VSCode project settings 24 | .vscode/ 25 | 26 | # Ignore the .golangci.yml file 27 | .golangci.yml -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Copyright VMware, Inc. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | version: "2" 5 | 6 | services: 7 | kafka: 8 | image: docker.io/bitnami/kafka:3.5 9 | ports: 10 | - "9092:9092" 11 | volumes: 12 | - "kafka_data:/bitnami" 13 | environment: 14 | # KRaft settings 15 | - KAFKA_CFG_NODE_ID=0 16 | - KAFKA_CFG_PROCESS_ROLES=controller,broker 17 | - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 18 | # Listeners 19 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 20 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 21 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT 22 | - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER 23 | - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT 24 | volumes: 25 | kafka_data: 26 | driver: local 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real-Time Notification System with Go and Kafka 2 | 3 | A simple real-time notification system built with Go and Apache Kafka. This project demonstrates the integration of Kafka's event streaming capabilities with Go to create a basic notification system. 4 | 5 | ## 📚 Detailed Tutorial 6 | 7 | For an in-depth explanation of this project, including step-by-step instructions and code breakdowns, check out the full article on freeCodeCamp: 8 | 9 | [How to Build a Real-Time Notification System with Go and Kafka](https://www.freecodecamp.org/news/build-a-real-time-notification-system-with-go-and-kafka/) 10 | 11 | ## 🚀 Features 12 | 13 | - **Basic real-time notifications**: Send messages between users 14 | - **Kafka integration**: Uses Kafka as a message broker 15 | - **Go implementation**: Leverages Go's concurrency for the consumer group 16 | - **Simple API**: HTTP endpoints for sending and retrieving notifications 17 | - **Docker setup**: Easy Kafka setup using Docker Compose 18 | 19 | ## 🛠️ Technologies Used 20 | 21 | - [Go 1.21+](https://go.dev/learn/) 22 | - [Apache Kafka](https://kafka.apache.org/) 23 | - [Sarama](https://github.com/IBM/sarama) (Kafka client for Go) 24 | - [Gin Web Framework](https://github.com/gin-gonic/gin) 25 | - [Docker & Docker Compose](https://docs.docker.com/compose/) 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module kafka-notify 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/IBM/sarama v1.40.1 7 | github.com/gin-gonic/gin v1.9.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.9.1 // indirect 12 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/eapache/go-resiliency v1.3.0 // indirect 15 | github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect 16 | github.com/eapache/queue v1.1.0 // indirect 17 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 18 | github.com/gin-contrib/sse v0.1.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.14.0 // indirect 22 | github.com/goccy/go-json v0.10.2 // indirect 23 | github.com/golang/snappy v0.0.4 // indirect 24 | github.com/hashicorp/errwrap v1.0.0 // indirect 25 | github.com/hashicorp/go-multierror v1.1.1 // indirect 26 | github.com/hashicorp/go-uuid v1.0.3 // indirect 27 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 28 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 29 | github.com/jcmturner/gofork v1.7.6 // indirect 30 | github.com/jcmturner/gokrb5/v8 v8.4.3 // indirect 31 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/klauspost/compress v1.16.6 // indirect 34 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 35 | github.com/kr/text v0.2.0 // indirect 36 | github.com/leodido/go-urn v1.2.4 // indirect 37 | github.com/mattn/go-isatty v0.0.19 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 41 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 42 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 43 | github.com/rogpeppe/go-internal v1.11.0 // indirect 44 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 45 | github.com/ugorji/go/codec v1.2.11 // indirect 46 | golang.org/x/arch v0.3.0 // indirect 47 | golang.org/x/crypto v0.11.0 // indirect 48 | golang.org/x/net v0.12.0 // indirect 49 | golang.org/x/sys v0.10.0 // indirect 50 | golang.org/x/text v0.11.0 // indirect 51 | google.golang.org/protobuf v1.30.0 // indirect 52 | gopkg.in/yaml.v3 v3.0.1 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /cmd/producer/producer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | 11 | "kafka-notify/pkg/models" 12 | 13 | "github.com/IBM/sarama" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | const ( 18 | ProducerPort = ":8080" 19 | KafkaServerAddress = "localhost:9092" 20 | KafkaTopic = "notifications" 21 | ) 22 | 23 | // ============== HELPER FUNCTIONS ============== 24 | var ErrUserNotFoundInProducer = errors.New("user not found") 25 | 26 | func findUserByID(id int, users []models.User) (models.User, error) { 27 | for _, user := range users { 28 | if user.ID == id { 29 | return user, nil 30 | } 31 | } 32 | return models.User{}, ErrUserNotFoundInProducer 33 | } 34 | 35 | func getIDFromRequest(formValue string, ctx *gin.Context) (int, error) { 36 | id, err := strconv.Atoi(ctx.PostForm(formValue)) 37 | if err != nil { 38 | return 0, fmt.Errorf( 39 | "failed to parse ID from form value %s: %w", formValue, err) 40 | } 41 | return id, nil 42 | } 43 | 44 | // ============== KAFKA RELATED FUNCTIONS ============== 45 | func sendKafkaMessage(producer sarama.SyncProducer, 46 | users []models.User, ctx *gin.Context, fromID, toID int) error { 47 | message := ctx.PostForm("message") 48 | 49 | fromUser, err := findUserByID(fromID, users) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | toUser, err := findUserByID(toID, users) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | notification := models.Notification{ 60 | From: fromUser, 61 | To: toUser, Message: message, 62 | } 63 | 64 | notificationJSON, err := json.Marshal(notification) 65 | if err != nil { 66 | return fmt.Errorf("failed to marshal notification: %w", err) 67 | } 68 | 69 | msg := &sarama.ProducerMessage{ 70 | Topic: KafkaTopic, 71 | Key: sarama.StringEncoder(strconv.Itoa(toUser.ID)), 72 | Value: sarama.StringEncoder(notificationJSON), 73 | } 74 | 75 | _, _, err = producer.SendMessage(msg) 76 | return err 77 | } 78 | 79 | func sendMessageHandler(producer sarama.SyncProducer, 80 | users []models.User) gin.HandlerFunc { 81 | return func(ctx *gin.Context) { 82 | fromID, err := getIDFromRequest("fromID", ctx) 83 | if err != nil { 84 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 85 | return 86 | } 87 | 88 | toID, err := getIDFromRequest("toID", ctx) 89 | if err != nil { 90 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 91 | return 92 | } 93 | 94 | err = sendKafkaMessage(producer, users, ctx, fromID, toID) 95 | if errors.Is(err, ErrUserNotFoundInProducer) { 96 | ctx.JSON(http.StatusNotFound, gin.H{"message": "User not found"}) 97 | return 98 | } 99 | if err != nil { 100 | ctx.JSON(http.StatusInternalServerError, gin.H{ 101 | "message": err.Error(), 102 | }) 103 | return 104 | } 105 | 106 | ctx.JSON(http.StatusOK, gin.H{ 107 | "message": "Notification sent successfully!", 108 | }) 109 | } 110 | } 111 | 112 | func setupProducer() (sarama.SyncProducer, error) { 113 | config := sarama.NewConfig() 114 | config.Producer.Return.Successes = true 115 | producer, err := sarama.NewSyncProducer([]string{KafkaServerAddress}, 116 | config) 117 | if err != nil { 118 | return nil, fmt.Errorf("failed to setup producer: %w", err) 119 | } 120 | return producer, nil 121 | } 122 | 123 | func main() { 124 | users := []models.User{ 125 | {ID: 1, Name: "Emma"}, 126 | {ID: 2, Name: "Bruno"}, 127 | {ID: 3, Name: "Rick"}, 128 | {ID: 4, Name: "Lena"}, 129 | } 130 | 131 | producer, err := setupProducer() 132 | if err != nil { 133 | log.Fatalf("failed to initialize producer: %v", err) 134 | } 135 | defer producer.Close() 136 | 137 | gin.SetMode(gin.ReleaseMode) 138 | router := gin.Default() 139 | router.POST("/send", sendMessageHandler(producer, users)) 140 | 141 | fmt.Printf("Kafka PRODUCER 📨 started at http://localhost%s\n", 142 | ProducerPort) 143 | 144 | if err := router.Run(ProducerPort); err != nil { 145 | log.Printf("failed to run the server: %v", err) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /cmd/consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "sync" 11 | 12 | "kafka-notify/pkg/models" 13 | 14 | "github.com/IBM/sarama" 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | const ( 19 | ConsumerGroup = "notifications-group" 20 | ConsumerTopic = "notifications" 21 | ConsumerPort = ":8081" 22 | KafkaServerAddress = "localhost:9092" 23 | ) 24 | 25 | // ============== HELPER FUNCTIONS ============== 26 | var ErrNoMessagesFound = errors.New("no messages found") 27 | 28 | func getUserIDFromRequest(ctx *gin.Context) (string, error) { 29 | userID := ctx.Param("userID") 30 | if userID == "" { 31 | return "", ErrNoMessagesFound 32 | } 33 | return userID, nil 34 | } 35 | 36 | // ====== NOTIFICATION STORAGE ====== 37 | type UserNotifications map[string][]models.Notification 38 | 39 | type NotificationStore struct { 40 | data UserNotifications 41 | mu sync.RWMutex 42 | } 43 | 44 | func (ns *NotificationStore) Add(userID string, 45 | notification models.Notification) { 46 | ns.mu.Lock() 47 | defer ns.mu.Unlock() 48 | ns.data[userID] = append(ns.data[userID], notification) 49 | } 50 | 51 | func (ns *NotificationStore) Get(userID string) []models.Notification { 52 | ns.mu.RLock() 53 | defer ns.mu.RUnlock() 54 | return ns.data[userID] 55 | } 56 | 57 | // ============== KAFKA RELATED FUNCTIONS ============== 58 | type Consumer struct { 59 | store *NotificationStore 60 | } 61 | 62 | func (*Consumer) Setup(sarama.ConsumerGroupSession) error { return nil } 63 | func (*Consumer) Cleanup(sarama.ConsumerGroupSession) error { return nil } 64 | 65 | func (consumer *Consumer) ConsumeClaim( 66 | sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { 67 | for msg := range claim.Messages() { 68 | userID := string(msg.Key) 69 | var notification models.Notification 70 | err := json.Unmarshal(msg.Value, ¬ification) 71 | if err != nil { 72 | log.Printf("failed to unmarshal notification: %v", err) 73 | continue 74 | } 75 | consumer.store.Add(userID, notification) 76 | sess.MarkMessage(msg, "") 77 | } 78 | return nil 79 | } 80 | 81 | func initializeConsumerGroup() (sarama.ConsumerGroup, error) { 82 | config := sarama.NewConfig() 83 | 84 | consumerGroup, err := sarama.NewConsumerGroup( 85 | []string{KafkaServerAddress}, ConsumerGroup, config) 86 | if err != nil { 87 | return nil, fmt.Errorf("failed to initialize consumer group: %w", err) 88 | } 89 | 90 | return consumerGroup, nil 91 | } 92 | 93 | func setupConsumerGroup(ctx context.Context, store *NotificationStore) { 94 | consumerGroup, err := initializeConsumerGroup() 95 | if err != nil { 96 | log.Printf("initialization error: %v", err) 97 | } 98 | defer consumerGroup.Close() 99 | 100 | consumer := &Consumer{ 101 | store: store, 102 | } 103 | 104 | for { 105 | err = consumerGroup.Consume(ctx, []string{ConsumerTopic}, consumer) 106 | if err != nil { 107 | log.Printf("error from consumer: %v", err) 108 | } 109 | if ctx.Err() != nil { 110 | return 111 | } 112 | } 113 | } 114 | 115 | func handleNotifications(ctx *gin.Context, store *NotificationStore) { 116 | userID, err := getUserIDFromRequest(ctx) 117 | if err != nil { 118 | ctx.JSON(http.StatusNotFound, gin.H{"message": err.Error()}) 119 | return 120 | } 121 | 122 | notes := store.Get(userID) 123 | if len(notes) == 0 { 124 | ctx.JSON(http.StatusOK, 125 | gin.H{ 126 | "message": "No notifications found for user", 127 | "notifications": []models.Notification{}, 128 | }) 129 | return 130 | } 131 | 132 | ctx.JSON(http.StatusOK, gin.H{"notifications": notes}) 133 | } 134 | 135 | func main() { 136 | store := &NotificationStore{ 137 | data: make(UserNotifications), 138 | } 139 | 140 | ctx, cancel := context.WithCancel(context.Background()) 141 | go setupConsumerGroup(ctx, store) 142 | defer cancel() 143 | 144 | gin.SetMode(gin.ReleaseMode) 145 | router := gin.Default() 146 | router.GET("/notifications/:userID", func(ctx *gin.Context) { 147 | handleNotifications(ctx, store) 148 | }) 149 | 150 | fmt.Printf("Kafka CONSUMER (Group: %s) 👥📥 "+ 151 | "started at http://localhost%s\n", ConsumerGroup, ConsumerPort) 152 | 153 | if err := router.Run(ConsumerPort); err != nil { 154 | log.Printf("failed to run the server: %v", err) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/IBM/sarama v1.40.1 h1:lL01NNg/iBeigUbT+wpPysuTYW6roHo6kc1QrffRf0k= 2 | github.com/IBM/sarama v1.40.1/go.mod h1:+5OFwA5Du9I6QrznhaMHsuwWdWZNMjaBSIxEWEgKOYE= 3 | github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc= 4 | github.com/Shopify/toxiproxy/v2 v2.5.0/go.mod h1:yhM2epWtAmel9CB8r2+L+PCmhH6yH2pITaPAo7jxJl0= 5 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 6 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 7 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 8 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 9 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 10 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0= 16 | github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= 17 | github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM= 18 | github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= 19 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 20 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 21 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 22 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 23 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 24 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 25 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 26 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 27 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 28 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 29 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 30 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 31 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 32 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 33 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 34 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 35 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 36 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 37 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 38 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 39 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 40 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 41 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 42 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 43 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 45 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 46 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 47 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 48 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 49 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 50 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 51 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 52 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 53 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 54 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 55 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 56 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 57 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 58 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 59 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 60 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 61 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 62 | github.com/jcmturner/gokrb5/v8 v8.4.3 h1:iTonLeSJOn7MVUtyMT+arAn5AKAPrkilzhGw8wE/Tq8= 63 | github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0= 64 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 65 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 66 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 67 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 68 | github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= 69 | github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 70 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 71 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 72 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 73 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 74 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 75 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 76 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 77 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 78 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 79 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 80 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 81 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 82 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 83 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 84 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 85 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 86 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 87 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 88 | github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= 89 | github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 90 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 91 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 92 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= 93 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 94 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 95 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 96 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 98 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 101 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 102 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 103 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 104 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 105 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 106 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 107 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 108 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 109 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 110 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 111 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 112 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 113 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 114 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 115 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 116 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 117 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 118 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 119 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 120 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 121 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 122 | golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= 123 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= 124 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 125 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 126 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 127 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 128 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 135 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 136 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 137 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 138 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 139 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 140 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 141 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 142 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 143 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 144 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 145 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 146 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 147 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 148 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 149 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 150 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 151 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 152 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 154 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 155 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 156 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 157 | --------------------------------------------------------------------------------