implement a metrics endpoint as well as a user get/one endpoint
Cette révision appartient à :
Parent
2cd7f4019c
révision
5cd05ba871
|
@ -1,9 +1,32 @@
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hbjydev/mangadex-next/database"
|
||||||
|
)
|
||||||
|
|
||||||
type HealthController struct{}
|
type HealthController struct{}
|
||||||
|
|
||||||
func (h *HealthController) Healthy(w http.ResponseWriter, r *http.Request) {
|
func (h *HealthController) Healthy(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HealthController) Metrics(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var output string
|
||||||
|
|
||||||
|
output += fmt.Sprintf("mangadex_item_count{type=\"user\"} %v\n", countUsers())
|
||||||
|
|
||||||
|
w.Write([]byte(output))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func countUsers() int {
|
||||||
|
row := database.DB.QueryRow("SELECT count(id) FROM users")
|
||||||
|
var count int
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/hbjydev/mangadex-next/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserController struct{}
|
||||||
|
|
||||||
|
func (u *UserController) GetAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
users, err := models.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
usersJSON, err := json.Marshal(users)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("/users: failed to marshal users array")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(usersJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserController) GetOne(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
user, err := models.UserByUsername(vars["username"])
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "sql: no rows in result set" {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userJSON, err := user.Normalize()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("/user/%v: failed to marshal user", vars["username"])
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(*userJSON))
|
||||||
|
}
|
|
@ -17,6 +17,8 @@ schemes:
|
||||||
tags:
|
tags:
|
||||||
- name: health
|
- name: health
|
||||||
description: 'Service health endpoints'
|
description: 'Service health endpoints'
|
||||||
|
- name: users
|
||||||
|
description: 'Service user endpoints'
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/-/healthy:
|
/-/healthy:
|
||||||
|
@ -28,4 +30,39 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 'Everything is healthy.'
|
description: 'Everything is healthy.'
|
||||||
|
/-/metrics:
|
||||||
|
get:
|
||||||
|
summary: Retrieves some metrics about the API server and database.
|
||||||
|
operationId: metrics
|
||||||
|
tags:
|
||||||
|
- health
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'A Prometheus-compatible metrics list.'
|
||||||
|
|
||||||
|
/users:
|
||||||
|
get:
|
||||||
|
summary: Retrieves every user in the database.
|
||||||
|
operationId: getAllUsers
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'All users in the database.'
|
||||||
|
|
||||||
|
/users/{username}:
|
||||||
|
get:
|
||||||
|
summary: Retrieves a single user from the database.
|
||||||
|
operationId: getOneUser
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: username
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: The username of the user you want information on.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'A single user from the database.'
|
||||||
|
|
3
main.go
3
main.go
|
@ -42,6 +42,9 @@ func main() {
|
||||||
healthRouter := routers.HealthRouter{}
|
healthRouter := routers.HealthRouter{}
|
||||||
healthRouter.RegisterRoutes(r)
|
healthRouter.RegisterRoutes(r)
|
||||||
|
|
||||||
|
userRouter := routers.UserRouter{}
|
||||||
|
userRouter.RegisterRoutes(r)
|
||||||
|
|
||||||
log.Println("Starting server on :3000...")
|
log.Println("Starting server on :3000...")
|
||||||
log.Fatal(http.ListenAndServe(":3000", r))
|
log.Fatal(http.ListenAndServe(":3000", r))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,10 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type password string
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password password `json:"password"`
|
Password string `json:"-"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
LevelID string `json:"level_id"`
|
LevelID string `json:"level_id"`
|
||||||
JoinedAt time.Time `json:"joined_at"`
|
JoinedAt time.Time `json:"joined_at"`
|
||||||
|
@ -30,8 +29,59 @@ type User struct {
|
||||||
AvatarURL string `json:"avatar"`
|
AvatarURL string `json:"avatar"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (password) MarshalJSON() ([]byte, error) {
|
func GetUsers() ([]User, error) {
|
||||||
return []byte(`""`), nil
|
rows, err := database.DB.Query(`
|
||||||
|
SELECT
|
||||||
|
hex(id), username, email, password, level_id, last_seen, website,
|
||||||
|
biography, views, uploads, premium, md_at_home, avatar_url,
|
||||||
|
joined_at, update_at
|
||||||
|
FROM users
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Users: error at SQL query: %v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
users := make([]User, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var u User
|
||||||
|
var ls mysql.NullTime
|
||||||
|
var ja mysql.NullTime
|
||||||
|
var ua mysql.NullTime
|
||||||
|
|
||||||
|
if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.Password, &u.LevelID,
|
||||||
|
&ls, &u.Website, &u.Biography, &u.Views, &u.Uploads, &u.Premium,
|
||||||
|
&u.MDAtHome, &u.AvatarURL, &ja, &ua); err != nil {
|
||||||
|
log.Printf("UserByUsername: error at SQL query: %v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.Valid {
|
||||||
|
u.LastSeen = ls.Time
|
||||||
|
} else {
|
||||||
|
log.Printf("UserByUsername: invalid SQL datetime value (id %v)\n", u.ID)
|
||||||
|
return nil, errors.New("invalid sql datetime value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ja.Valid {
|
||||||
|
u.JoinedAt = ja.Time
|
||||||
|
} else {
|
||||||
|
log.Printf("UserByUsername: invalid SQL datetime value (id %v)\n", u.ID)
|
||||||
|
return nil, errors.New("invalid sql datetime value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ua.Valid {
|
||||||
|
u.UpdateAt = ua.Time
|
||||||
|
} else {
|
||||||
|
log.Printf("UserByUsername: invalid SQL datetime value (id %v)\n", u.ID)
|
||||||
|
return nil, errors.New("invalid sql datetime value")
|
||||||
|
}
|
||||||
|
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserByUsername(username string) (*User, error) {
|
func UserByUsername(username string) (*User, error) {
|
||||||
|
@ -39,7 +89,8 @@ func UserByUsername(username string) (*User, error) {
|
||||||
SELECT
|
SELECT
|
||||||
hex(id), username, email, password, level_id, last_seen, website,
|
hex(id), username, email, password, level_id, last_seen, website,
|
||||||
biography, views, uploads, premium, md_at_home, avatar_url,
|
biography, views, uploads, premium, md_at_home, avatar_url,
|
||||||
joined_at, update_at FROM users
|
joined_at, update_at
|
||||||
|
FROM users
|
||||||
WHERE username = ?
|
WHERE username = ?
|
||||||
`, username)
|
`, username)
|
||||||
|
|
||||||
|
|
|
@ -7,17 +7,19 @@ import (
|
||||||
"github.com/hbjydev/mangadex-next/controllers"
|
"github.com/hbjydev/mangadex-next/controllers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthRouter struct {
|
type HealthRouter struct{}
|
||||||
Controller controllers.HealthController
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HealthRouter) healthcheck(w http.ResponseWriter, r *http.Request) {
|
func (h *HealthRouter) healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||||
controller := controllers.HealthController{}
|
controller := controllers.HealthController{}
|
||||||
controller.Healthy(w, r)
|
controller.Healthy(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HealthRouter) RegisterRoutes(r *mux.Router) {
|
func (h *HealthRouter) metrics(w http.ResponseWriter, r *http.Request) {
|
||||||
pathPrefix := r.PathPrefix("/-")
|
controller := controllers.HealthController{}
|
||||||
|
controller.Metrics(w, r)
|
||||||
pathPrefix.Path("/healthy").HandlerFunc(h.healthcheck).Methods("GET")
|
}
|
||||||
|
|
||||||
|
func (h *HealthRouter) RegisterRoutes(r *mux.Router) {
|
||||||
|
r.HandleFunc("/-/healthy", h.healthcheck)
|
||||||
|
r.HandleFunc("/-/metrics", h.metrics)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/hbjydev/mangadex-next/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRouter struct{}
|
||||||
|
|
||||||
|
func (u *UserRouter) getAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
controller := controllers.UserController{}
|
||||||
|
controller.GetAll(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserRouter) getOne(w http.ResponseWriter, r *http.Request) {
|
||||||
|
controller := controllers.UserController{}
|
||||||
|
controller.GetOne(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserRouter) RegisterRoutes(r *mux.Router) {
|
||||||
|
r.HandleFunc("/users", u.getAll)
|
||||||
|
r.HandleFunc("/users/{username}", u.getOne)
|
||||||
|
}
|
Référencer dans un nouveau ticket