├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── logs.png ├── node ├── Dockerfile ├── Makefile └── main.go └── producer ├── Dockerfile ├── Makefile └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_STORE 2 | .vscode 3 | .idea 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Adil H 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go + Consul Distributed Loggers Demo 2 | Simple Go + Consul Distributed System 3 | 4 | Article: [Building a simple Distributed System with Go & Consul](https://medium.com/@didil/building-a-simple-distributed-system-with-go-consul-39b08ffc5d2c) 5 | 6 | ![Alt text](logs.png?raw=true "Demo") 7 | 8 | *DO NOT USE IN PRODUCTION* This is a Proof of Concept and does not handle all corner cases 9 | 10 | ## Components 11 | - **consul**: Consul instance that provides support for leader election and service discovery 12 | - **distributed-logger**: The Distributed Logger nodes expose a REST API that logs received messages to Stdout. Only the cluster leader accepts messages at any given time. A new node takes over in case of leader node failure. 13 | - **producer**: The producer queries Consul periodically to determine the distributed-logger leader and sends it a numbered message. 14 | 15 | ## Demo instructions 16 | Start services 17 | ``` 18 | docker-compose up -d --scale distributed-logger=3 19 | ``` 20 | Tail logs 21 | ``` 22 | docker-compose logs -f distributed-logger 23 | ``` 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | consul: 5 | image: consul 6 | command: "agent -dev -client 0.0.0.0" 7 | ports: 8 | - "8300:8300" 9 | - "8500:8500" 10 | distributed-logger: 11 | build: ./node/ 12 | depends_on: 13 | - consul 14 | producer: 15 | build: ./producer/ 16 | depends_on: 17 | - distributed-logger -------------------------------------------------------------------------------- /logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didil/go-consul-distributed-loggers/430265c212e9fb8502f6b69c008bad1015d04b26/logs.png -------------------------------------------------------------------------------- /node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12.1 2 | 3 | WORKDIR /app 4 | 5 | ADD Makefile Makefile 6 | 7 | RUN make deps 8 | 9 | ADD . . 10 | 11 | RUN make build 12 | 13 | CMD ["./main"] -------------------------------------------------------------------------------- /node/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o main main.go 3 | deps: 4 | go get -u github.com/hashicorp/consul/api -------------------------------------------------------------------------------- /node/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/hashicorp/consul/api" 14 | ) 15 | 16 | var isLeader bool 17 | 18 | func main() { 19 | go startAPI() 20 | 21 | // ttl in seconds 22 | ttl := 10 23 | ttlS := fmt.Sprintf("%ds", ttl) 24 | serviceKey := "service/distributed-logger/leader" 25 | serviceName := "distributed-logger" 26 | 27 | // build client 28 | config := api.DefaultConfig() 29 | config.Address = "consul:8500" 30 | client, err := api.NewClient(config) 31 | if err != nil { 32 | log.Fatalf("client err: %v", err) 33 | } 34 | 35 | // create session 36 | sEntry := &api.SessionEntry{ 37 | Name: serviceName, 38 | TTL: ttlS, 39 | LockDelay: 1 * time.Millisecond, 40 | } 41 | sID, _, err := client.Session().Create(sEntry, nil) 42 | if err != nil { 43 | log.Fatalf("session create err: %v", err) 44 | } 45 | 46 | // auto renew session 47 | doneCh := make(chan struct{}) 48 | go func() { 49 | err = client.Session().RenewPeriodic(ttlS, sID, nil, doneCh) 50 | if err != nil { 51 | log.Fatalf("session renew err: %v", err) 52 | } 53 | }() 54 | 55 | log.Printf("Consul session : %+v\n", sID) 56 | 57 | // Lock acquisition loop 58 | go func() { 59 | hostName, err := os.Hostname() 60 | if err != nil { 61 | log.Fatalf("hostname err: %v", err) 62 | } 63 | 64 | acquireKv := &api.KVPair{ 65 | Session: sID, 66 | Key: serviceKey, 67 | Value: []byte(hostName), 68 | } 69 | 70 | for { 71 | if !isLeader { 72 | acquired, _, err := client.KV().Acquire(acquireKv, nil) 73 | if err != nil { 74 | log.Fatalf("kv acquire err: %v", err) 75 | } 76 | 77 | if acquired { 78 | isLeader = true 79 | log.Printf("I'm the leader !\n") 80 | } 81 | } 82 | 83 | time.Sleep(time.Duration(ttl/2) * time.Second) 84 | } 85 | }() 86 | 87 | // wait for SIGINT or SIGTERM, clean up and exit 88 | sigCh := make(chan os.Signal) 89 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 90 | 91 | <-sigCh 92 | close(doneCh) 93 | log.Printf("Destroying session and leaving ...") 94 | _, err = client.Session().Destroy(sID, nil) 95 | if err != nil { 96 | log.Fatalf("session destroy err: %v", err) 97 | } 98 | os.Exit(0) 99 | } 100 | 101 | func startAPI() { 102 | mux := http.NewServeMux() 103 | mux.HandleFunc("/api/v1/log", func(w http.ResponseWriter, r *http.Request) { 104 | if !isLeader { 105 | http.Error(w, "Not Leader", http.StatusBadRequest) 106 | return 107 | } 108 | 109 | msg, err := ioutil.ReadAll(r.Body) 110 | if err != nil { 111 | http.Error(w, err.Error(), http.StatusInternalServerError) 112 | } 113 | 114 | // log msg 115 | log.Printf("Received %v", string(msg)) 116 | 117 | w.Write([]byte("OK")) 118 | if err != nil { 119 | http.Error(w, err.Error(), http.StatusInternalServerError) 120 | } 121 | }) 122 | 123 | port := "3000" 124 | log.Printf("Starting API on port %s ....\n", port) 125 | log.Fatal(http.ListenAndServe(":"+port, mux)) 126 | } 127 | -------------------------------------------------------------------------------- /producer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12.1 2 | 3 | WORKDIR /app 4 | 5 | ADD Makefile Makefile 6 | 7 | RUN make deps 8 | 9 | ADD . . 10 | 11 | RUN make build 12 | 13 | CMD ["./main"] -------------------------------------------------------------------------------- /producer/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o main main.go 3 | deps: 4 | go get -u github.com/hashicorp/consul/api -------------------------------------------------------------------------------- /producer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hashicorp/consul/api" 12 | ) 13 | 14 | func main() { 15 | log.Printf("Starting producer\n") 16 | serviceKey := "service/distributed-logger/leader" 17 | 18 | config := api.DefaultConfig() 19 | config.Address = "consul:8500" 20 | 21 | client, err := api.NewClient(config) 22 | if err != nil { 23 | log.Fatalf("client err: %v", err) 24 | } 25 | 26 | msgID := 1 27 | for { 28 | kv, _, err := client.KV().Get(serviceKey, nil) 29 | if err != nil { 30 | log.Fatalf("kv acquire err: %v", err) 31 | } 32 | 33 | if kv != nil && kv.Session != "" { 34 | // there is a leader 35 | leaderHostname := string(kv.Value) 36 | sendMsg(leaderHostname, msgID) 37 | msgID++ 38 | } 39 | 40 | time.Sleep(5 * time.Second) 41 | } 42 | } 43 | 44 | func sendMsg(hostname string, msgID int) { 45 | msg := fmt.Sprintf("Message: %d", msgID) 46 | log.Printf("Sending message %v\n", msgID) 47 | resp, err := http.Post(fmt.Sprintf("http://%v:3000/api/v1/log", hostname), "text/plain", strings.NewReader(msg)) 48 | if err != nil { 49 | log.Printf("http post err: %v", err) 50 | return 51 | } 52 | 53 | defer resp.Body.Close() 54 | _, err = ioutil.ReadAll(resp.Body) 55 | if resp.StatusCode != 200 { 56 | log.Printf("status not OK: %v", resp.StatusCode) 57 | return 58 | } 59 | 60 | log.Printf("msg sent OK") 61 | } 62 | --------------------------------------------------------------------------------