metricator/internal/httpserver/handler.go

201 lines
4.5 KiB
Go

package httpserver
import (
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"strings"
"time"
"go.dev.pztrn.name/metricator/internal/common"
"go.dev.pztrn.name/metricator/internal/models"
)
// nolint:gochecknoglobals
var (
errInvalidAPIVersion = errors.New("invalid API version")
errInvalidApplication = errors.New("invalid application")
errInvalidPath = errors.New("invalid path")
errNoAppsRegistered = errors.New("no applications registered")
errNoData = errors.New("no data")
supportedAPIVersions = []int{1}
)
// HTTP requests handler.
type handler struct {
handlers map[string]common.HTTPHandlerFunc
}
// Gets applications list from handlers map.
func (h *handler) getAppsList() ([]byte, error) {
apps := make([]string, 0, len(h.handlers))
for appName := range h.handlers {
apps = append(apps, appName)
}
appsList, err := json.Marshal(apps)
if err != nil {
// ToDo: log error
return nil, errNoAppsRegistered
}
return appsList, nil
}
// Gets request information from URL. Returns a structure with filled request
// info and error if it occurs.
func (h *handler) getRequestInfo(req *http.Request) (*models.RequestInfo, error) {
// Request isn't for API or isn't versioned.
if !strings.HasPrefix(req.URL.Path, "/api/v") {
return nil, errInvalidPath
}
// Note: first element will always be empty!
pathSplitted := strings.Split(req.URL.Path, "/")
// Request is for API but not enough items in URL was passed.
if len(pathSplitted) < 4 {
return nil, errInvalidPath
}
var (
appName string
metricName string
requestType string
)
// Parse API version.
apiVersionRaw := strings.TrimLeft(pathSplitted[2], "v")
apiVersion, err := strconv.Atoi(apiVersionRaw)
if err != nil {
// ToDo: log error
return nil, errInvalidAPIVersion
}
// Check used API version.
var supportedAPIVersionUsed bool
for _, version := range supportedAPIVersions {
if apiVersion == version {
supportedAPIVersionUsed = true
break
}
}
if !supportedAPIVersionUsed {
return nil, errInvalidAPIVersion
}
// Get request type and key.
requestType = pathSplitted[3]
if len(pathSplitted) >= 5 {
appName = pathSplitted[4]
}
if len(pathSplitted) >= 6 {
metricName = strings.Join(pathSplitted[5:], "/")
}
reqInfo := &models.RequestInfo{
APIVersion: apiVersion,
Application: appName,
Metric: metricName,
RequestType: requestType,
}
return reqInfo, nil
}
// Registers request's handler.
func (h *handler) register(appName string, hndl common.HTTPHandlerFunc) {
h.handlers[appName] = hndl
}
// ServeHTTP handles every HTTP request.
func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
startTime := time.Now()
defer func() {
requestDuration := time.Since(startTime)
log.Printf("[HTTP Request] from %s to %s, duration %.4fs\n", req.RemoteAddr, req.URL.Path, requestDuration.Seconds())
}()
// Validate request and extract needed info.
rInfo, err := h.getRequestInfo(req)
if err != nil {
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + err.Error()))
return
}
// Process request type. Here we process only known requests types,
// all other requests will produce HTTP 400 error.
switch rInfo.RequestType {
// ToDo: move to constants.
case "apps_list":
appsList, err := h.getAppsList()
if err != nil {
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + err.Error()))
return
}
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write(appsList)
return
case "info":
infoData := struct {
Branch string
Build string
CommitHash string
Version string
}{
Branch: common.Branch,
Build: common.Build,
CommitHash: common.CommitHash,
Version: common.Version,
}
infoBytes, _ := json.Marshal(infoData)
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write(infoBytes)
return
case "metrics":
handler, found := h.handlers[rInfo.Application]
if !found {
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + errInvalidApplication.Error()))
return
}
// Get data from handler.
data := handler(rInfo)
if data == "" {
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + errNoData.Error()))
}
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte(data))
return
}
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + errInvalidPath.Error()))
}