diff --git a/.env b/.env index bdeb4be..0ab81c5 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ +KDOORPI_DOOR=workshop KDOORPI_API_ALLOWED=http://127.0.0.1:3333/allowed KDOORPI_API_LONGPOLL=http://127.0.0.1:3333/longpoll KDOORPI_API_SWIPE=http://127.0.0.1:3333/cardswipe diff --git a/Makefile b/Makefile index 84d39f4..6d46028 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,13 @@ -dev: - GOOS=linux GOARCH=arm64 go build . +.PHONY: all build build_arm64 dev + +all: build + +build: + go build -ldflags="-X main.Version=`git describe --dirty`" . + +build_arm64: + GOOS=linux GOARCH=arm64 go build -ldflags="-X main.Version=`git describe --dirty`" . + +dev: build_arm64 scp godoor workshopdoor:/tmp/ ssh workshopdoor 'mv -f /tmp/godoor ~/ && sudo systemctl restart godoor' diff --git a/go.mod b/go.mod index 652424c..ae8573e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,20 @@ module godoor go 1.18 require ( - github.com/joho/godotenv v1.5.1 // indirect - github.com/warthog618/gpiod v0.8.0 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + github.com/joho/godotenv v1.5.1 + github.com/prometheus/client_golang v1.16.0 + github.com/warthog618/gpiod v0.8.0 + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + golang.org/x/sys v0.8.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect ) diff --git a/go.sum b/go.sum index 116fd91..b49225d 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,40 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/pilebones/go-udev v0.0.0-20180820235104-043677e09b13 h1:Y+ynP+0QIjUejN2tsuIlWOJG1CThJy6amRuWlBL94Vg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/warthog618/gpiod v0.8.0 h1:qxH9XVvWHpTxzWFSndBcujFyNH5zVRzHM63tcmm85o4= github.com/warthog618/gpiod v0.8.0/go.mod h1:a7Csa+IJtDBZ39++zC/6Srjo01qWejt/5velrDWuNkY= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/godoor.go b/godoor.go index b7e8e31..c3dda9d 100644 --- a/godoor.go +++ b/godoor.go @@ -7,11 +7,15 @@ import ( "encoding/json" "fmt" "github.com/joho/godotenv" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" "io" "log" "net/http" "os" "os/signal" + "runtime/debug" "strconv" "strings" "sync" @@ -55,6 +59,7 @@ type Config struct { wiegandB int solenoid int } + prometheusMetricsBind string } type KeepDoorOpen struct { @@ -67,22 +72,62 @@ type OpenedTimestamp struct { Closed *time.Time } +var Commit = func() string { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + return setting.Value + } + } + } + + return "" +}() + +var Version string + var config Config var globalLock sync.Mutex var validUids ValidUids var wiegand Wiegand var keepDoorOpen KeepDoorOpen -var lastSyncedTimestamp *time.Time -var openDoorTimestamps []OpenedTimestamp +var ( + godoorBuildInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "godoor_build_info", + Help: "Build Information", + }, []string{"version", "revision"}) + lastSyncTimestamp = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "godoor_last_allow_list_sync_timestamp_seconds", + Help: "Last time list of card hashes was pulled from the server", + }) + apiFailuresCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "godoor_api_request_failures_total", + Help: "HTTP API request failures count", + }, []string{"api", "endpoint"}) + nrCardsInAllowList = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "godoor_allowed_card_hashes_total", + Help: "Number of card hashes in memory that can open the door", + }) + doorOpenedCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "godoor_door_opens_total", + Help: "Number of times door was opened", + }, []string{"source"}) + cardSwipesCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "godoor_swipes_total", + Help: "Number of times a card has been swiped", + }, []string{"status"}) +) func main() { - + log.Printf("GoDoor ver: %s (%s)", Version, Commit) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() loadConfig() + godoorBuildInfo.WithLabelValues(Version, Commit).Set(1) + go func() { setup() }() @@ -113,6 +158,10 @@ func loadConfig() { config.doorOpenTime = 3 } _, config.mock = os.LookupEnv("KDOORPI_MOCK_HW") + config.prometheusMetricsBind = os.Getenv("KDOORPI_PROMETHEUS_METRICS_BIND") + if config.prometheusMetricsBind == "" { + config.prometheusMetricsBind = ":3334" + } config.pins.wiegandA, err = strconv.Atoi(os.Getenv("KDOORPI_PIN_WIEGAND_A")) if err != nil { @@ -142,16 +191,24 @@ func setup() { } log.Println("HW Setup done") + go runHttpServer() + http.DefaultClient.Timeout = 120 * time.Second go func() { for { err := waitEvents() if err != nil { + apiFailuresCount.WithLabelValues("longpoll", config.api.longpoll).Inc() log.Printf("LongPoll for events failed: %v", err) log.Println("Will try to LongPoll again in 120 seconds") time.Sleep(120 * time.Second) - go reloadTokens() + go func() { + err := reloadTokens() + if err != nil { + apiFailuresCount.WithLabelValues("allowed", config.api.allowed).Inc() + } + }() } time.Sleep(1 * time.Second) } @@ -165,13 +222,12 @@ func setup() { if err == nil { break } + apiFailuresCount.WithLabelValues("allowed", config.api.allowed).Inc() log.Printf("Initial token population failed. err: %v", err) log.Println("Retrying in 10 seconds...") time.Sleep(10 * time.Second) } - go runHttpServer() - log.Println("Initial token population success") go cardRunner(wiegand) @@ -180,26 +236,9 @@ func setup() { } func runHttpServer() { - http.HandleFunc("/lastsync", func(w http.ResponseWriter, r *http.Request) { - e := json.NewEncoder(w) - e.Encode(map[string]any{ - "last_synced": lastSyncedTimestamp, - }) - }) - http.HandleFunc("/opened", func(w http.ResponseWriter, r *http.Request) { - e := json.NewEncoder(w) - e.Encode(map[string]any{ - "open_timestamps": openDoorTimestamps, - }) - }) - http.HandleFunc("/isopen", func(w http.ResponseWriter, r *http.Request) { - e := json.NewEncoder(w) - open, _ := wiegand.IsDoorOpen() - e.Encode(map[string]any{ - "open": open, - }) - }) - http.ListenAndServe(":3334", nil) + http.Handle("/metrics", promhttp.Handler()) + log.Printf("Running prometheus metrics on http://%s/metrics", config.prometheusMetricsBind) + log.Fatal(http.ListenAndServe(config.prometheusMetricsBind, nil)) } func OpenAndCloseDoor(w Wiegand) error { @@ -232,7 +271,6 @@ func OpenDoor(w Wiegand) error { return nil } w.OpenDoor() - openDoorTimestamps = append(openDoorTimestamps, OpenedTimestamp{Opened: time.Now(), Closed: nil}) return nil } @@ -242,8 +280,6 @@ func CloseDoor(w Wiegand) error { return nil } w.CloseDoor() - t := time.Now() - openDoorTimestamps[len(openDoorTimestamps)-1].Closed = &t return nil } @@ -265,6 +301,7 @@ func cardRunner(w Wiegand) { go func() { err := sendSwipeEvent(hashedHex, ok) if err != nil { + apiFailuresCount.WithLabelValues("swipe", config.api.swipe).Inc() log.Println("Failed to send swipe event: %v", err) } }() @@ -272,10 +309,13 @@ func cardRunner(w Wiegand) { if ok { log.Println("Opening door") err := OpenAndCloseDoor(w) + cardSwipesCount.WithLabelValues("accepted").Inc() + doorOpenedCount.WithLabelValues("card").Inc() if err != nil { log.Println("There was an error opening and closing the Door") } } else { + cardSwipesCount.WithLabelValues("denied").Inc() log.Println("Unknown card") } @@ -333,6 +373,7 @@ func waitEvents() error { log.Printf("got server data: %q\n", data) if strings.TrimSpace(data) == config.door { err := OpenAndCloseDoor(wiegand) + doorOpenedCount.WithLabelValues("api").Inc() if err != nil { log.Println("There was an error opening and closing the Door") } @@ -381,13 +422,13 @@ func reloadTokens() error { totalCardCount = i } log.Printf("Got %d cards from server", totalCardCount) + nrCardsInAllowList.Set(float64(totalCardCount)) if cl.KeepOpenUntil != nil { updateKeepOpenDoor(*cl.KeepOpenUntil) } - t := time.Now() - lastSyncedTimestamp = &t + lastSyncTimestamp.SetToCurrentTime() return nil }