diff --git a/.dockerignore b/.dockerignore index 3c266c2..7e32383 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,4 +15,4 @@ .git/ vendor/ -statuspage \ No newline at end of file +statuspage diff --git a/.drone.yml b/.drone.yml index c8ee33f..c92fe3b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,5 +8,5 @@ pipeline: commands: - go get -u github.com/golang/dep/cmd/dep - /go/bin/dep ensure - - go test . ./src/... - - go build \ No newline at end of file + - go test . ./pkg/... + - go build diff --git a/.gitignore b/.gitignore index ca2254d..b003e51 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ .glide/ vendor/ -statuspage \ No newline at end of file +statuspage diff --git a/Dockerfile b/Dockerfile index 8aaeb12..4834fe2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,4 @@ RUN set -e \ && dep ensure \ && go build -CMD ["./statuspage"] \ No newline at end of file +CMD ["./statuspage", "server"] diff --git a/Gopkg.lock b/Gopkg.lock index 4c33d7a..87e17cf 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "629574ca2a5df945712d3079857300b5e4da0236" + version = "v1.4.2" + [[projects]] branch = "master" name = "github.com/gin-contrib/sse" @@ -25,6 +31,18 @@ packages = ["proto"] revision = "130e6b02ab059e7b717a096f397c5b60111cae74" +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] + revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" + +[[projects]] + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + version = "v1.0" + [[projects]] branch = "master" name = "github.com/jinzhu/inflection" @@ -37,24 +55,96 @@ revision = "6ed27152e0428abfde127acb33b08b03a1e67cac" version = "1.0.2" +[[projects]] + name = "github.com/magiconair/properties" + packages = ["."] + revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73" + version = "v1.7.4" + [[projects]] name = "github.com/mattn/go-isatty" packages = ["."] revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "06020f85339e21b2478f756a78e295255ffa4d6a" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "16398bac157da96aa88f98a2df640c7f32af1da2" + version = "v1.0.1" + +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" + version = "v1.0.4" + +[[projects]] + name = "github.com/spf13/afero" + packages = [".","mem"] + revision = "ec3a3111d1e1bdff38a61e09d5a5f5e974905611" + version = "v1.0.1" + +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" + version = "v1.1.0" + +[[projects]] + name = "github.com/spf13/cobra" + packages = ["."] + revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" + version = "v0.0.1" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b" + +[[projects]] + name = "github.com/spf13/pflag" + packages = ["."] + revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" + version = "v1.0.0" + +[[projects]] + name = "github.com/spf13/viper" + packages = ["."] + revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" + version = "v1.0.0" + [[projects]] branch = "master" name = "github.com/ugorji/go" packages = ["codec"] revision = "54210f4e076c57f351166f0ed60e67d3fca57a36" +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8" + [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] + packages = ["unix","windows"] revision = "314a259e304ff91bd6985da2a7149bbf91237993" +[[projects]] + branch = "master" + name = "golang.org/x/text" + packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"] + revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" + [[projects]] name = "gopkg.in/go-playground/validator.v8" packages = ["."] @@ -70,6 +160,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4544073386bd8b7f1b7dac2351ee8b7e0aae1d8e408659078e4361597e6bd84e" + inputs-digest = "de9a7002b0063888c213a05fea4e6306ddb1cc0e4775d31c47b2dfc714dcd4ed" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index 9467582..bccabfe 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,18 @@ We use environment variables for configuration, this also includes the logo. ## Configuration We use environment variables to configure the service. The table bellow contains -all variables you can use. +all variables you can use. It's also possible to configure the service using +flags, camelCase is used here. Run `./statuspage server --help for more +information`. |Name |Description| |-----------------|-----------| -|API_TOKEN |This is the token clients should use to access the API (AUTHORIZATION header)| +|LISTEN_ADDRESS |The address the server should listen on| +|TOKEN |This is the token clients should use to access the API (AUTHORIZATION header)| |POSTGRES_ADDRESS |The address of the postgres instance| |POSTGRES_USER |The postgres username for authorization| |POSTGRES_PASSWORD|The postgres password for authorization| -|POSTGRES_DB |The postgres db name| +|POSTGRES_DATABASE|The postgres db name| |SITE_OWNER |The owner of the side, visible in page title| |SITE_COLOR |The background color applied on the header element| |SITE_LOGO |Custom logo, served from another site or local path inside the static folder| @@ -35,3 +38,11 @@ Create new incident ```bash curl -H "Authorization: 123" -v -d '{"time": "2018-01-01T13:50:00-08:00", "status":"Identified", "message":"oh no! i am broken", "Title":"User API is down"}' -H "Content-Type: application/json" -X POST http://localhost/api/incidents ``` + +The token used is configured on the server using the token flag or env variable. + +## CLI + +The statuspage binary contains a CLI tool that can be used to access the API. +For more information run `./statuspage cli --help`. Every api endpoint is +available in this tool. diff --git a/cmd/cli/incident.go b/cmd/cli/incident.go new file mode 100644 index 0000000..b04399c --- /dev/null +++ b/cmd/cli/incident.go @@ -0,0 +1,16 @@ +package cli + +import ( + "github.com/spf13/cobra" +) + +func init() { +} + +var IncidentCmd = &cobra.Command{ + Use: "incident", + Short: "Manage incidents and updates", + Run: func(cmd *cobra.Command, args []string) { + + }, +} diff --git a/cmd/cli/service.go b/cmd/cli/service.go new file mode 100644 index 0000000..723e614 --- /dev/null +++ b/cmd/cli/service.go @@ -0,0 +1,65 @@ +package cli + +import ( + "github.com/eirsyl/statuspage/pkg/api" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + log "github.com/sirupsen/logrus" +) + +func init() { + ServiceCmd.AddCommand(listServiceCmd) + ServiceCmd.AddCommand(createServiceCmd) + ServiceCmd.AddCommand(updateServiceCmd) + ServiceCmd.AddCommand(deleteServiceCmd) + ServiceCmd.AddCommand(getServiceCmd) +} + +var ServiceCmd = &cobra.Command{ + Use: "service", + Short: "Manage services", +} + +var listServiceCmd = &cobra.Command{ + Use: "list", + Run: func(cmd *cobra.Command, args []string) { + apiUrl, token := viper.GetString("apiUrl"), viper.GetString("token") + api, err := api.NewAPI(apiUrl, token) + if err != nil { + log.Fatal(err) + } + services, err := api.ListServices() + log.Info(services, err) + }, +} + +var createServiceCmd = &cobra.Command{ + Use: "create", + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var updateServiceCmd = &cobra.Command{ + Use: "update [id]", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var deleteServiceCmd = &cobra.Command{ + Use: "delete [id]", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var getServiceCmd = &cobra.Command{ + Use: "get [id]", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + }, +} diff --git a/cmd/cli_root.go b/cmd/cli_root.go new file mode 100644 index 0000000..cab1dcc --- /dev/null +++ b/cmd/cli_root.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/eirsyl/statuspage/cmd/cli" +) + +func init() { + cliCmd.PersistentFlags().StringP("apiUrl", "a", "http://127.0.0.1:8080", "The URL used to access the API") + cliCmd.PersistentFlags().StringP("token", "t", "", "The token used for authorizing with the API") + viper.BindPFlag("apiUrl", cliCmd.PersistentFlags().Lookup("apiUrl")) + viper.BindPFlag("token", cliCmd.PersistentFlags().Lookup("token")) + viper.BindEnv("apiUrl", "API_URL") + viper.BindEnv("token", "TOKEN") + cliCmd.AddCommand(cli.IncidentCmd) + cliCmd.AddCommand(cli.ServiceCmd) + RootCmd.AddCommand(cliCmd) +} + +var cliCmd = &cobra.Command{ + Use: "cli", + Short: "Access the statuspage api from the command line", +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..11bbb63 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func init() { + +} + +var RootCmd = &cobra.Command{ + Use: "statuspage", + Short: "Statuspage platform written in golang with postgres as the backing datastore.", + Long: ``, +} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..2f642cc --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "github.com/eirsyl/statuspage/pkg" + "github.com/eirsyl/statuspage/pkg/routes" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func init() { + serverCmd.PersistentFlags().StringP("listenAddress", "l", ":8080", "The address the server should listen on") + serverCmd.PersistentFlags().StringP("token", "t", "", "The token used for authorizing with the API") + serverCmd.PersistentFlags().StringP("postgresAddress", "a", "127.0.0.1:5432", "The address postgres is listening on") + serverCmd.PersistentFlags().StringP("postgresUser", "u", "statuspage", "The user statuspage should connect to postgres as") + serverCmd.PersistentFlags().StringP("postgresPassword", "p", "", "The postgres password") + serverCmd.PersistentFlags().StringP("postgresDatabase", "d", "statuspage", "The postgres database statuspage should use") + serverCmd.PersistentFlags().StringP("siteOwner", "", "Statuspage", "Site owner, used by the html templates") + serverCmd.PersistentFlags().StringP("siteColor", "", "#343434", "The top color used in the templates") + serverCmd.PersistentFlags().StringP("siteLogo", "", "static/img/logo.png", "Path to logo") + viper.BindPFlag("listenAddress", serverCmd.PersistentFlags().Lookup("listenAddress")) + viper.BindPFlag("token", serverCmd.PersistentFlags().Lookup("token")) + viper.BindPFlag("postgresAddress", serverCmd.PersistentFlags().Lookup("postgresAddress")) + viper.BindPFlag("postgresUser", serverCmd.PersistentFlags().Lookup("postgresUser")) + viper.BindPFlag("postgresPassword", serverCmd.PersistentFlags().Lookup("postgresPassword")) + viper.BindPFlag("postgresDatabase", serverCmd.PersistentFlags().Lookup("postgresDatabase")) + viper.BindPFlag("siteOwner", serverCmd.PersistentFlags().Lookup("siteOwner")) + viper.BindPFlag("siteColor", serverCmd.PersistentFlags().Lookup("siteColor")) + viper.BindPFlag("siteLogo", serverCmd.PersistentFlags().Lookup("siteLogo")) + viper.BindEnv("listenAddress", "LISTEN_ADDRESS") + viper.BindEnv("token", "TOKEN") + viper.BindEnv("postgresAddress", "POSTGRES_ADDRESS") + viper.BindEnv("postgresUser", "POSTGRES_USER") + viper.BindEnv("postgresPassword", "POSTGRES_PASSWORD") + viper.BindEnv("postgresDatabase", "POSTGRES_DATABASE") + viper.BindEnv("siteOwner", "SITE_OWNER") + viper.BindEnv("siteColor", "SITE_COLOR") + viper.BindEnv("siteLogo", "SITE_LOGO") + RootCmd.AddCommand(serverCmd) +} + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Start the statuspage server", + Run: func(cmd *cobra.Command, args []string) { + pkg.ConfigRuntime() + gin.SetMode(gin.ReleaseMode) + + router := gin.New() + router.Use(gin.Recovery()) + router.Use(pkg.State()) + router.Use(pkg.Logger()) + + binding.Validator.RegisterValidation("incidentstatus", pkg.IncidentStatus) + binding.Validator.RegisterValidation("servicestatus", pkg.ServiceStatus) + + router.Static("/static", "./static") + router.LoadHTMLGlob("templates/*") + + router.GET("/", routes.Dashboard) + + api := router.Group("/api") + api.Use(pkg.Auth()) + { + api.GET("/services", routes.ServiceList) + api.POST("/services", routes.ServicePost) + api.GET("/services/:id", routes.ServiceGet) + api.PATCH("/services/:id", routes.ServicePatch) + api.DELETE("/services/:id", routes.ServiceDelete) + + api.GET("/incidents", routes.IncidentList) + api.POST("/incidents", routes.IncidentPost) + api.GET("/incidents/:id", routes.IncidentGet) + api.DELETE("/incidents/:id", routes.IncidentDelete) + + api.GET("/incidents/:id/updates", routes.IncidentUpdateList) + api.POST("/incidents/:id/updates", routes.IncidentUpdatePost) + api.GET("/incidents/:id/updates/:updateId", routes.IncidentUpdateGet) + api.DELETE("/incidents/:id/updates/:updateId", routes.IncidentUpdateDelete) + } + + listenAddress := viper.GetString("listenAddress") + log.WithFields(log.Fields{"address": listenAddress}).Info("Starting server") + err := router.Run(listenAddress) + if err != nil { + log.Error("Server exited ", err) + } + }, +} diff --git a/docker-compose.yaml b/docker-compose.yaml index b2bf6c3..c91f28a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,6 +3,8 @@ version: "3" services: postgres: image: postgres:9.5 + ports: + - 5432:5432 volumes: - /var/lib/postgresql/data environment: @@ -13,16 +15,16 @@ services: build: context: . ports: - - 80:8080 + - 8080:8080 links: - postgres:postgres environment: - - API_TOKEN= # This is the token clients should use to access the API (AUTHORIZATION header) + - TOKEN=auth # This is the token clients should use to access the API (AUTHORIZATION header) - POSTGRES_ADDRESS=postgres:5432 # The address of the postgres instance - POSTGRES_USER=statuspage # The postgres username for authorization - POSTGRES_PASSWORD=statuspage # The postgres password for authorization - - POSTGRES_DB=statuspage # The postgres db name - - SITE_OWNER="DEMO" # The owner of the side, visible in page title + - POSTGRES_DATABASE=statuspage # The postgres db name + - SITE_OWNER=DEMO # The owner of the side, visible in page title - SITE_COLOR= # The background color applied on the header element - SITE_LOGO= # Custom logo, served from another site or local path inside the static folder depends_on: diff --git a/main.go b/main.go index dc64244..dca56a3 100644 --- a/main.go +++ b/main.go @@ -1,104 +1,14 @@ package main import ( - "github.com/eirsyl/statuspage/src" - "github.com/eirsyl/statuspage/src/routes" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" - "github.com/go-pg/pg" - "log" - "net/http" + "fmt" + "github.com/eirsyl/statuspage/cmd" "os" - "runtime" ) func main() { - - ConfigRuntime() - - gin.SetMode(gin.ReleaseMode) - - router := gin.Default() - router.Use(State()) - - binding.Validator.RegisterValidation("incidentstatus", src.IncidentStatus) - binding.Validator.RegisterValidation("servicestatus", src.ServiceStatus) - - router.Static("/static", "./static") - router.LoadHTMLGlob("templates/*") - - router.GET("/", routes.Dashboard) - - api := router.Group("/api") - api.Use(Auth()) - { - api.GET("/services", routes.ServiceList) - api.POST("/services", routes.ServicePost) - api.GET("/services/:id", routes.ServiceGet) - api.PATCH("/services/:id", routes.ServicePatch) - api.DELETE("/services/:id", routes.ServiceDelete) - - api.GET("/incidents", routes.IncidentList) - api.POST("/incidents", routes.IncidentPost) - api.GET("/incidents/:id", routes.IncidentGet) - api.DELETE("/incidents/:id", routes.IncidentDelete) - - api.GET("/incidents/:id/updates", routes.IncidentUpdateList) - api.POST("/incidents/:id/updates", routes.IncidentUpdatePost) - api.GET("/incidents/:id/updates/:updateId", routes.IncidentUpdateGet) - api.DELETE("/incidents/:id/updates/:updateId", routes.IncidentUpdateDelete) - } - - router.Run() - -} - -func ConfigRuntime() { - nuCPU := runtime.NumCPU() - runtime.GOMAXPROCS(nuCPU) - log.Printf("Running with %d CPUs\n", nuCPU) -} - -func State() gin.HandlerFunc { - pgAddr := os.Getenv("POSTGRES_ADDRESS") - pgUser := os.Getenv("POSTGRES_USER") - pgPassword := os.Getenv("POSTGRES_PASSWORD") - pgDB := os.Getenv("POSTGRES_DB") - - db := pg.Connect(&pg.Options{ - Addr: pgAddr, - User: pgUser, - Password: pgPassword, - Database: pgDB, - }) - - if err := src.CreateSchema(db); err != nil { - panic(err) - } - - services := src.Services{} - services.Initialize(*db) - - incidents := src.Incidents{} - incidents.Initialize(*db) - - return func(c *gin.Context) { - c.Set("services", services) - c.Set("incidents", incidents) - c.Next() - } -} - -func Auth() gin.HandlerFunc { - return func(c *gin.Context) { - token := c.GetHeader("Authorization") - validToken := os.Getenv("API_TOKEN") - - if !(len(validToken) > 0 && token == validToken) { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - c.Next() + if err := cmd.RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) } } diff --git a/pkg/api/client.go b/pkg/api/client.go new file mode 100644 index 0000000..043bbe2 --- /dev/null +++ b/pkg/api/client.go @@ -0,0 +1,54 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/eirsyl/statuspage/pkg" + "net/http" + "time" +) + +// NewClient returns a http.Client used to access the API +func NewClient() *http.Client { + return &http.Client{ + Timeout: 10 * time.Second, + } +} + +type API struct { + apiUrl string + token string + client *http.Client +} + +// NewAPI initializes a new API client that can be used to access the API +func NewAPI(apiUrl, token string) (*API, error) { + client := NewClient() + + if apiUrl == "" || token == "" { + return nil, errors.New("Both apiUrl and token is required") + } + + return &API{ + apiUrl: apiUrl, + token: token, + client: client, + }, nil +} + +func (a *API) CreateRequest(url, method string, payload interface{}) *http.Request { + var data []byte + + if payload != nil { + data, _ = json.Marshal(payload) + } + + request, _ := http.NewRequest( + method, fmt.Sprintf("%s%s", pkg.RemoveLastSlash(a.apiUrl), url), bytes.NewReader(data), + ) + request.Header.Add("AUTHORIZATION", a.token) + request.Header.Add("CONTENT-TYPE", "application/json") + return request +} diff --git a/pkg/api/services.go b/pkg/api/services.go new file mode 100644 index 0000000..a87db1c --- /dev/null +++ b/pkg/api/services.go @@ -0,0 +1,29 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/eirsyl/statuspage/pkg" + "io/ioutil" +) + +func (a *API) ListServices() ([]pkg.Service, error) { + request := a.CreateRequest("/api/services", "GET", nil) + response, err := a.client.Do(request) + if err == nil && response.StatusCode == 200 { + var result []pkg.Service + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return []pkg.Service{}, err + } + json.Unmarshal(body, &result) + return result, nil + } + + if err == nil && response.StatusCode != 200 { + return []pkg.Service{}, errors.New(fmt.Sprintf("Response failed %s", response.StatusCode)) + } + + return []pkg.Service{}, err +} diff --git a/src/incidents.go b/pkg/incidents.go similarity index 99% rename from src/incidents.go rename to pkg/incidents.go index a6a6373..17014f7 100644 --- a/src/incidents.go +++ b/pkg/incidents.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "github.com/go-pg/pg" diff --git a/pkg/middleware.go b/pkg/middleware.go new file mode 100644 index 0000000..bb9cc7c --- /dev/null +++ b/pkg/middleware.go @@ -0,0 +1,92 @@ +package pkg + +import ( + "github.com/gin-gonic/gin" + "github.com/go-pg/pg" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "net/http" + "time" +) + +// Auth enforces token authentication on the api endpoints. +func Auth() gin.HandlerFunc { + validToken := viper.GetString("token") + + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + + if validToken == "" || token != validToken { + log.WithFields(log.Fields{"token": token}).Warn("Permission denied, invalid token") + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.Next() + } +} + +// State provides the handler with access to the datastore +func State() gin.HandlerFunc { + pgUser, pgPassword := viper.GetString("postgresUser"), viper.GetString("postgresPassword") + pgAddress, pgDatabase := viper.GetString("postgresAddress"), viper.GetString("postgresDatabase") + + db := pg.Connect(&pg.Options{ + Addr: pgAddress, + User: pgUser, + Password: pgPassword, + Database: pgDatabase, + }) + + if err := CreateSchema(db); err != nil { + panic(err) + } + + services := Services{} + services.Initialize(*db) + + incidents := Incidents{} + incidents.Initialize(*db) + + return func(c *gin.Context) { + c.Set("services", services) + c.Set("incidents", incidents) + c.Next() + } +} + +// Logger add logrus logging to each request +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + // Start timer + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + // Process request + c.Next() + + // Stop timer + end := time.Now() + latency := end.Sub(start) + + clientIP := c.ClientIP() + method := c.Request.Method + statusCode := c.Writer.Status() + comment := c.Errors.ByType(gin.ErrorTypePrivate).String() + + if raw != "" { + path = path + "?" + raw + } + + log.WithFields(log.Fields{ + "statusCode": statusCode, + "latency": latency, + "clientIP": clientIP, + "method": method, + "path": path, + "comment": comment, + }).Info("Request") + + } +} diff --git a/src/routes/api.go b/pkg/routes/api.go similarity index 84% rename from src/routes/api.go rename to pkg/routes/api.go index 0e75bdd..0da282d 100644 --- a/src/routes/api.go +++ b/pkg/routes/api.go @@ -1,7 +1,7 @@ package routes import ( - "github.com/eirsyl/statuspage/src" + "github.com/eirsyl/statuspage/pkg" "github.com/gin-gonic/gin" "net/http" "strconv" @@ -12,7 +12,7 @@ import ( */ func ServiceList(c *gin.Context) { - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) s, err := services.GetServices() if err != nil { @@ -23,14 +23,14 @@ func ServiceList(c *gin.Context) { } func ServicePost(c *gin.Context) { - var service src.Service + var service pkg.Service err := c.BindJSON(&service) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) err = services.InsertService(&service) if err != nil { @@ -49,7 +49,7 @@ func ServiceGet(c *gin.Context) { return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) s, err := services.GetService(int64(id)) if err != nil { @@ -68,14 +68,14 @@ func ServicePatch(c *gin.Context) { return } - var service src.Service + var service pkg.Service err = c.BindJSON(&service) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) err = services.UpdateService(int64(id), &service) if err != nil { @@ -94,7 +94,7 @@ func ServiceDelete(c *gin.Context) { return } - services := c.Keys["services"].(src.Services) + services := c.Keys["services"].(pkg.Services) err = services.DeleteService(int64(id)) if err != nil { @@ -110,7 +110,7 @@ func ServiceDelete(c *gin.Context) { */ func IncidentList(c *gin.Context) { - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) i, err := incidents.GetLatestIncidents() if err != nil { @@ -121,7 +121,7 @@ func IncidentList(c *gin.Context) { } func IncidentPost(c *gin.Context) { - var incident src.Incident + var incident pkg.Incident err := c.BindJSON(&incident) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -130,7 +130,7 @@ func IncidentPost(c *gin.Context) { incident.Updates = nil - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.InsertIncident(&incident) if err != nil { @@ -149,7 +149,7 @@ func IncidentGet(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) i, err := incidents.GetIncident(int64(id)) if err != nil { @@ -168,7 +168,7 @@ func IncidentDelete(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.DeleteIncident(int64(id)) @@ -188,7 +188,7 @@ func IncidentUpdateList(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) incident, err := incidents.GetIncident(int64(id)) @@ -208,14 +208,14 @@ func IncidentUpdatePost(c *gin.Context) { return } - var incidentUpdate src.IncidentUpdate + var incidentUpdate pkg.IncidentUpdate err = c.BindJSON(&incidentUpdate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.InsertIncidentUpdate(int64(id), &incidentUpdate) @@ -235,7 +235,7 @@ func IncidentUpdateGet(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) incidentUpdate, err := incidents.GetIncidentUpdate(int64(id)) @@ -255,7 +255,7 @@ func IncidentUpdateDelete(c *gin.Context) { return } - incidents := c.Keys["incidents"].(src.Incidents) + incidents := c.Keys["incidents"].(pkg.Incidents) err = incidents.DeleteIncidentUpdate(int64(id)) diff --git a/pkg/routes/dashboard.go b/pkg/routes/dashboard.go new file mode 100644 index 0000000..c2db899 --- /dev/null +++ b/pkg/routes/dashboard.go @@ -0,0 +1,47 @@ +package routes + +import ( + "github.com/eirsyl/statuspage/pkg" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "net/http" +) + +func Dashboard(c *gin.Context) { + services := c.Keys["services"].(pkg.Services) + incidents := c.Keys["incidents"].(pkg.Incidents) + + res, err := services.GetServices() + if err != nil { + panic(err) + } + + inc, err := incidents.GetLatestIncidents() + if err != nil { + panic(err) + } + + owner, color, logo := viper.GetString("siteOwner"), viper.GetString("siteColor"), viper.GetString("siteLogo") + + if owner == "" { + log.Fatal("The owner cannot be empty") + } + + if color == "" { + log.Fatal("The color cannot be empty") + } + + if logo == "" { + log.Fatal("The logo cannot be empty") + } + + c.HTML(http.StatusOK, "index.tmpl", gin.H{ + "owner": owner, + "backgroundColor": color, + "logo": logo, + "services": pkg.AggregateServices(res), + "mostCriticalStatus": pkg.MostCriticalStatus(res), + "incidents": pkg.AggregateIncidents(inc), + }) +} diff --git a/src/services.go b/pkg/services.go similarity index 99% rename from src/services.go rename to pkg/services.go index 8a1b856..e89f003 100644 --- a/src/services.go +++ b/pkg/services.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "github.com/go-pg/pg" diff --git a/src/utils.go b/pkg/utils.go similarity index 83% rename from src/utils.go rename to pkg/utils.go index 374d7f9..8be0e4f 100644 --- a/src/utils.go +++ b/pkg/utils.go @@ -1,11 +1,19 @@ -package src +package pkg import ( "github.com/go-pg/pg" "github.com/go-pg/pg/orm" + log "github.com/sirupsen/logrus" + "runtime" "time" ) +func ConfigRuntime() { + nuCPU := runtime.NumCPU() + runtime.GOMAXPROCS(nuCPU) + log.Printf("Running with %d CPUs\n", nuCPU) +} + /* * Aggregate and group services by service group. */ @@ -108,3 +116,12 @@ func CreateSchema(db *pg.DB) error { } return nil } + +// RemoveLastSlash removes the last character in a string if the character is a string. +func RemoveLastSlash(url string) string { + l := len(url) + if l > 0 && url[l-1] == '/' { + return url[:l-2] + } + return url +} diff --git a/src/utils_test.go b/pkg/utils_test.go similarity index 96% rename from src/utils_test.go rename to pkg/utils_test.go index 992eac0..095d175 100644 --- a/src/utils_test.go +++ b/pkg/utils_test.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "testing" diff --git a/src/validators.go b/pkg/validators.go similarity index 98% rename from src/validators.go rename to pkg/validators.go index 7c059c2..b6627c7 100644 --- a/src/validators.go +++ b/pkg/validators.go @@ -1,4 +1,4 @@ -package src +package pkg import ( "gopkg.in/go-playground/validator.v8" diff --git a/src/routes/dashboard.go b/src/routes/dashboard.go deleted file mode 100644 index 2c89799..0000000 --- a/src/routes/dashboard.go +++ /dev/null @@ -1,47 +0,0 @@ -package routes - -import ( - "github.com/eirsyl/statuspage/src" - "github.com/gin-gonic/gin" - "net/http" - "os" -) - -func Dashboard(c *gin.Context) { - services := c.Keys["services"].(src.Services) - incidents := c.Keys["incidents"].(src.Incidents) - - res, err := services.GetServices() - if err != nil { - panic(err) - } - - inc, err := incidents.GetLatestIncidents() - if err != nil { - panic(err) - } - - owner := os.Getenv("SITE_OWNER") - if owner == "" { - owner = "Abakus" - } - - color := os.Getenv("SITE_COLOR") - if color == "" { - color = "#343434" - } - - logo := os.Getenv("SITE_LOGO") - if logo == "" { - logo = "static/img/logo.png" - } - - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "owner": owner, - "backgroundColor": color, - "logo": logo, - "services": src.AggregateServices(res), - "mostCriticalStatus": src.MostCriticalStatus(res), - "incidents": src.AggregateIncidents(inc), - }) -}