metricator/internal/httpserver/handler.go
Stanislav N. aka pztrn 529113a41a
Metrics data is stored as structure, HTTP requests logging, got rid of context.
Metrics data now stored as structures. This is a first step for autodiscovery
helping for NMSes.

HTTP requests now logged.

Got rid of context.Context for getting metric data in applications
because context.Context is useless here.
2020-12-23 13:27:17 +05:00

163 lines
3.8 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"
)
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(r *http.Request) (*models.RequestInfo, error) {
// Request isn't for API or isn't versioned.
if !strings.HasPrefix(r.URL.Path, "/api/v") {
return nil, errInvalidPath
}
// Note: first element will always be empty!
pathSplitted := strings.Split(r.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
}
// 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(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
defer func() {
requestDuration := time.Now().Sub(startTime)
log.Printf("[HTTP Request] from %s to %s, duration %.4fs\n", r.RemoteAddr, r.URL.Path, requestDuration.Seconds())
}()
// Validate request and extract needed info.
rInfo, err := h.getRequestInfo(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("400 bad request - " + err.Error()))
return
}
// Process request type. Here we process only known requests types,
// by default request should go to specific application's handler.
switch rInfo.RequestType {
// ToDo: move to constants.
case "apps_list":
appsList, err := h.getAppsList()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("400 bad request - " + err.Error()))
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(appsList)
return
case "metrics":
handler, found := h.handlers[rInfo.Application]
if !found {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("400 bad request - " + errInvalidApplication.Error()))
return
}
// Get data from handler.
data := handler(rInfo)
if data == "" {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("400 bad request - " + errNoData.Error()))
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(data))
return
}
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("400 bad request - " + errInvalidPath.Error()))
}