refactored, linted, formatted
This commit is contained in:
140
process/feta.go
Normal file
140
process/feta.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package process
|
||||
|
||||
import "os"
|
||||
import "time"
|
||||
|
||||
import "github.com/jinzhu/gorm"
|
||||
import _ "github.com/jinzhu/gorm/dialects/sqlite" // required for orm
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
import "github.com/rs/zerolog/log"
|
||||
import "github.com/mattn/go-isatty"
|
||||
|
||||
import "github.com/sneak/feta/ingester"
|
||||
import "github.com/sneak/feta/storage"
|
||||
import "github.com/sneak/feta/locator"
|
||||
import "github.com/sneak/feta/manager"
|
||||
import "github.com/sneak/feta/instance"
|
||||
|
||||
// CLIEntry is the main entrypoint for the feta process from the cli
|
||||
func CLIEntry(version string, buildarch string) int {
|
||||
f := new(Feta)
|
||||
f.version = version
|
||||
f.buildarch = buildarch
|
||||
f.setupLogging()
|
||||
return f.runForever()
|
||||
}
|
||||
|
||||
// Feta is the main structure/process of this app
|
||||
type Feta struct {
|
||||
version string
|
||||
buildarch string
|
||||
locator *locator.InstanceLocator
|
||||
manager *manager.InstanceManager
|
||||
ingester *ingester.TootIngester
|
||||
api *Server
|
||||
db *gorm.DB
|
||||
startup time.Time
|
||||
}
|
||||
|
||||
func (f *Feta) identify() {
|
||||
log.Info().
|
||||
Str("version", f.version).
|
||||
Str("buildarch", f.buildarch).
|
||||
Msg("starting")
|
||||
}
|
||||
|
||||
func (f *Feta) setupLogging() {
|
||||
|
||||
log.Logger = log.With().Caller().Logger()
|
||||
|
||||
tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
|
||||
|
||||
if tty {
|
||||
out := zerolog.NewConsoleWriter(
|
||||
func(w *zerolog.ConsoleWriter) {
|
||||
// Customize time format
|
||||
w.TimeFormat = time.RFC3339
|
||||
},
|
||||
)
|
||||
log.Logger = log.Output(out)
|
||||
}
|
||||
|
||||
// always log in UTC
|
||||
zerolog.TimestampFunc = func() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
f.identify()
|
||||
}
|
||||
|
||||
func (f *Feta) uptime() time.Duration {
|
||||
return time.Since(f.startup)
|
||||
}
|
||||
|
||||
/*
|
||||
func (f *Feta) setupDatabase() {
|
||||
var err error
|
||||
f.db, err = gorm.Open("sqlite3", "feta.sqlite")
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//f.databaseMigrations()
|
||||
}
|
||||
*/
|
||||
|
||||
func (f *Feta) runForever() int {
|
||||
f.startup = time.Now()
|
||||
|
||||
//f.setupDatabase()
|
||||
|
||||
// FIXME move this channel creation into the manager's constructor
|
||||
// and add getters/setters on the manager/locator
|
||||
newInstanceHostnameNotifications := make(chan instance.Hostname)
|
||||
|
||||
f.locator = locator.New()
|
||||
f.manager = manager.New()
|
||||
f.ingester = ingester.NewTootIngester()
|
||||
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
panic("can't find home directory")
|
||||
}
|
||||
|
||||
diskBackend := storage.NewTootFSStorage(home + "/.local/feta")
|
||||
f.ingester.SetStorageBackend(diskBackend)
|
||||
|
||||
f.api = new(Server)
|
||||
f.api.SetFeta(f) // api needs to get to us to access data
|
||||
|
||||
f.locator.SetInstanceNotificationChannel(newInstanceHostnameNotifications)
|
||||
f.manager.SetInstanceNotificationChannel(newInstanceHostnameNotifications)
|
||||
|
||||
f.manager.SetTootDestination(f.ingester.GetDeliveryChannel())
|
||||
|
||||
// ingester goroutine:
|
||||
go f.ingester.Ingest()
|
||||
|
||||
// locator goroutine:
|
||||
go f.locator.Locate()
|
||||
|
||||
// manager goroutine:
|
||||
go f.manager.Manage()
|
||||
|
||||
go f.api.Serve()
|
||||
|
||||
// this goroutine (main) does nothing until we handle signals
|
||||
// FIXME(sneak)
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
113
process/handlers.go
Normal file
113
process/handlers.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package process
|
||||
|
||||
import "time"
|
||||
import "net/http"
|
||||
import "encoding/json"
|
||||
import "runtime"
|
||||
import "fmt"
|
||||
import "strings"
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type hash map[string]interface{}
|
||||
|
||||
func (a *Server) instances() []hash {
|
||||
resp := make([]hash, 0)
|
||||
now := time.Now()
|
||||
for _, v := range a.feta.manager.ListInstances() {
|
||||
i := make(hash)
|
||||
// FIXME figure out why a very short lock here deadlocks
|
||||
v.Lock()
|
||||
i["hostname"] = v.Hostname
|
||||
i["nextCheck"] = v.NextFetch.UTC().Format(time.RFC3339)
|
||||
i["nextCheckAfter"] = (-1 * now.Sub(v.NextFetch)).String()
|
||||
i["successCount"] = v.SuccessCount
|
||||
i["errorCount"] = v.ErrorCount
|
||||
i["identified"] = v.Identified
|
||||
i["status"] = v.Status()
|
||||
i["software"] = "unknown"
|
||||
i["version"] = "unknown"
|
||||
if v.Identified {
|
||||
i["software"] = v.ServerImplementationString
|
||||
i["version"] = v.ServerVersionString
|
||||
}
|
||||
v.Unlock()
|
||||
resp = append(resp, i)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (a *Server) instanceSummary() map[string]int {
|
||||
resp := make(map[string]int)
|
||||
for _, v := range a.feta.manager.ListInstances() {
|
||||
v.Lock()
|
||||
resp[fmt.Sprintf("STATUS_%s", v.Status())]++
|
||||
if v.ServerImplementationString != "" {
|
||||
//FIXME(sneak) sanitize this to a-z0-9, it is server-provided
|
||||
resp[fmt.Sprintf("SOFTWARE_%s", strings.ToUpper(v.ServerImplementationString))]++
|
||||
}
|
||||
v.Unlock()
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (a *Server) getInstanceListHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result := &gin.H{
|
||||
"instances": a.instances(),
|
||||
}
|
||||
|
||||
json, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(json)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Server) getIndexHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
index := &gin.H{
|
||||
"server": &gin.H{
|
||||
"now": time.Now().UTC().Format(time.RFC3339),
|
||||
"uptime": a.feta.uptime().String(),
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
"goversion": runtime.Version(),
|
||||
"version": a.feta.version,
|
||||
"buildarch": a.feta.buildarch,
|
||||
},
|
||||
"instanceSummary": a.instanceSummary(),
|
||||
}
|
||||
|
||||
json, err := json.Marshal(index)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(json)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Server) getHealthCheckHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := &gin.H{
|
||||
"status": "ok",
|
||||
"now": time.Now().UTC().Format(time.RFC3339),
|
||||
"uptime": a.feta.uptime().String(),
|
||||
}
|
||||
|
||||
json, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(json)
|
||||
}
|
||||
}
|
||||
91
process/server.go
Normal file
91
process/server.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package process
|
||||
|
||||
import "fmt"
|
||||
import "net/http"
|
||||
import "os"
|
||||
import "strconv"
|
||||
import "time"
|
||||
|
||||
import "github.com/rs/zerolog/log"
|
||||
import "github.com/gin-gonic/gin"
|
||||
import "github.com/dn365/gin-zerolog"
|
||||
|
||||
// Server is the HTTP webserver object
|
||||
type Server struct {
|
||||
feta *Feta
|
||||
port uint
|
||||
router *gin.Engine
|
||||
server *http.Server
|
||||
debug bool
|
||||
}
|
||||
|
||||
// SetFeta tells the http Server where to find the Process object so that it
|
||||
// can pull stats and other information for serving via http
|
||||
func (a *Server) SetFeta(feta *Feta) {
|
||||
a.feta = feta
|
||||
}
|
||||
|
||||
// Serve is the entrypoint for the Server, which should run in its own
|
||||
// goroutine (started by the Process)
|
||||
func (a *Server) Serve() {
|
||||
if a.feta == nil {
|
||||
panic("must have feta app from which to serve stats")
|
||||
}
|
||||
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
a.debug = true
|
||||
}
|
||||
|
||||
a.port = 8080
|
||||
|
||||
if os.Getenv("PORT") != "" {
|
||||
s, err := strconv.ParseUint(os.Getenv("PORT"), 10, 64)
|
||||
if err != nil {
|
||||
a.port = uint(s)
|
||||
}
|
||||
}
|
||||
|
||||
a.initRouter()
|
||||
a.initServer()
|
||||
err := a.server.ListenAndServe()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("webserver failure")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Server) initRouter() {
|
||||
|
||||
// empty router
|
||||
r := gin.New()
|
||||
|
||||
// wrap panics:
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// attach logger middleware
|
||||
r.Use(ginzerolog.Logger("gin"))
|
||||
|
||||
r.GET("/.well-known/healthcheck.json", gin.WrapF(a.getHealthCheckHandler()))
|
||||
r.GET("/", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/feta") })
|
||||
r.GET("/feta", gin.WrapF(a.getIndexHandler()))
|
||||
r.GET("/feta/list/instances", gin.WrapF(a.getInstanceListHandler()))
|
||||
|
||||
a.router = r
|
||||
}
|
||||
|
||||
func (a *Server) initServer() {
|
||||
if !a.debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
log.Info().Uint("port", a.port).Msg("starting webserver")
|
||||
|
||||
a.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", a.port),
|
||||
Handler: a.router,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user