Skip to content
This repository has been archived by the owner on Mar 5, 2022. It is now read-only.

Commit

Permalink
it's important to stay healthy in 2020
Browse files Browse the repository at this point in the history
Add a simple HTTP healthcheck. Or is it "health check"?

Now more than ever I need to clean up some of this code. Put this
healthcheck stuff into a "health" module just to help get it out of
the way for now. I both love having most logic in proxy.go...and hate
it at the same time. I like just running `go build` with no args.

Anyways: you can now send an "HTTP GET /health" to the proxy and it
will report back a 200 OK...if it's alive. No other "checks" are made
yet, but downline can wire in details on the cluster status
potentially. Basically if the proxy can't reach the backend, it's not
worth sending traffic to it!
  • Loading branch information
voutilad committed Dec 16, 2020
1 parent e876e71 commit c25507c
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 6 deletions.
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

KEYGEN = openssl req -x509 -newkey rsa:4096 -keyout key.pem \
-out cert.pem -days 30 -nodes -subj '/CN=localhost'
bolt-proxy:
go build -v

bolt-proxy: clean
go build -o bolt-proxy proxy.go

test:
go test -v ./...
go test ./...

clean:
go clean -x
go clean

certs: cert.pem key.pem
cert.pem:
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ Now that I'm neck deep in this...here's where `bolt-proxy` stands:
7. Picking leader vs. follower for write or read (respectively)
transactions works.
8. TLS support for client-side with default verification rules.
9. Basic HTTP healthcheck available if sending HTTP GET with path of
/health to the listening port. (Will respond with 200 OK.)

## What doesn't (yet) work:
1. No emulation of routing table, so if you use `neo4j://` schemes on
Expand Down Expand Up @@ -142,5 +144,31 @@ connection to the database :-P
> reported by the backend. If you have advertised addresses set, make
> sure they are resolvable **by this proxy**.
### Monitoring
If using healthchecks in k8s or something else, a basic healthcheck is
currently implemented. Sending a simple HTTP GET with a path of
`/health` should respond with a 200 OK. For instance, if binding to
`localhost:8888`:

```
kogelvis[bolt-proxy]$ curl -v http://localhost:8888/health
* Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET /health HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.73.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
* Connection #0 to host localhost left intact
```

A bad request takes 2 forms and each has a different result:
1. A request to a path other than `/health` will result in the
connection being closed immediately.
2. A request to `/health` that's not a valid HTTP request will result
in an `HTTP/1.1 400 Bad Request` response.

# License
Provided under MIT. See [LICENSE](./LICENSE).
39 changes: 39 additions & 0 deletions health/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package health

import (
"bufio"
"bytes"
"errors"
"net"
"net/http"
)

const (
HEALTH_REQ = "GET /health HTTP"
OK_RESPONSE = "HTTP/1.1 200 OK\r\n"
BAD_RESPONSE = "HTTP/1.1 400 Bad Request\r\n"
)

// Check if the given buf looks like an HTTP GET to our /health endpoint
func IsHealthCheck(buf []byte) bool {
return bytes.HasPrefix(buf, []byte(HEALTH_REQ))
}

// Given a connectioned client conn and its message as a byte-slice buf,
// validate it's an HTTP request. If so, write a "204 No Content" http
// response letting the caller know bolt-proxy is alive.
func HandleHealthCheck(conn net.Conn, buf []byte) error {
reader := bytes.NewReader(buf)
bufioReader := bufio.NewReader(reader)

_, err := http.ReadRequest(bufioReader)
if err != nil {
conn.Write([]byte(BAD_RESPONSE))
return errors.New("malformed http health check request")
}

// TODO: eventually we should check things are working right, but for
// now, just consider it a liveness check.
conn.Write([]byte(OK_RESPONSE))
return nil
}
43 changes: 43 additions & 0 deletions health/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package health

import (
"bytes"
"net"
"testing"
)

func TestHealthCheckHandler(t *testing.T) {
left, right := net.Pipe()
c := make(chan []byte)
bad := []byte("GET /health HTTP/xxxx")
ok := []byte("GET /health HTTP/1.1\r\n\r\n")

go func() {
for i := 0; i < 2; i++ {
buf := make([]byte, 128)
n, err := right.Read(buf)
if err != nil {
t.Fatal(err)
}
c <- buf[:n]
}
}()

err := HandleHealthCheck(left, bad)
if err == nil {
t.Fatal("expected to fail with bad healthcheck request")
}
msg := <-c
if !bytes.Equal([]byte(BAD_RESPONSE), msg) {
t.Fatal("expected bad response to healthcheck request")
}

err = HandleHealthCheck(left, ok)
if err != nil {
t.Fatal(err)
}
msg = <-c
if !bytes.Equal([]byte(OK_RESPONSE), msg) {
t.Fatal("expected OK response to healthcheck request")
}
}
20 changes: 18 additions & 2 deletions proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/voutilad/bolt-proxy/backend"
"github.com/voutilad/bolt-proxy/bolt"
"github.com/voutilad/bolt-proxy/health"

"github.com/gobwas/ws"
)
Expand Down Expand Up @@ -123,8 +124,23 @@ func handleClient(conn net.Conn, b *backend.Backend) {

} else if bytes.Equal(buf[:4], []byte{0x47, 0x45, 0x54, 0x20}) {
// Second case, we have an HTTP connection that might just
// be a WebSocket upgrade
n, _ = conn.Read(buf[4:])
// be a WebSocket upgrade OR a health check.

// Read the rest of the request
n, err = conn.Read(buf[4:])
if err != nil {
log.Printf("failed reading rest of GET request: %s\n", err)
return
}

// Health check, maybe? If so, handle and bail.
if health.IsHealthCheck(buf[:n+4]) {
err = health.HandleHealthCheck(conn, buf[:n+4])
if err != nil {
log.Println(err)
}
return
}

// Build something implementing the io.ReadWriter interface
// to pass to the upgrader routine
Expand Down

0 comments on commit c25507c

Please sign in to comment.