diff --git a/docs/INDEX.md b/docs/INDEX.md index 7d8be9a..4186dec 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -8,6 +8,6 @@ Main idea for Metricator is to provide high-speed proxy between classic NMSes (l Why proxy? Let's go by example. -Imagine that software you wish to monitor with classic NMS exposes 250 metric items and you wrote a simple script which returns needed data. Classic NMSes is able to process only one value per checker (usually), which means that processing every metric will do 250 HTTP requests to monitored software which is obviosly not good. +Imagine that software you wish to monitor with classic NMS exposes 250 metric items and you wrote a simple script which returns needed data. Classic NMSes is able to process only one value per checker (usually), which means that processing every metric will do 250 HTTP requests to monitored software which is obviously not good. Metricator instead acts like Prometheus itself from monitored software PoV: it performs one HTTP request each timeout (configured in config), parses data and making it available to other requesters. As parsed metric data stored in memory it will be blazing fast and won't overload your system. diff --git a/internal/application/api_responder.go b/internal/application/api_responder.go index 9aa76ae..8dc851a 100644 --- a/internal/application/api_responder.go +++ b/internal/application/api_responder.go @@ -1,13 +1,16 @@ package application import ( - "context" - - "go.dev.pztrn.name/metricator/internal/common" + "go.dev.pztrn.name/metricator/internal/models" ) -func (a *Application) respond(ctx context.Context) string { - metricName := ctx.Value(common.ContextKeyMetric).(string) +// Responds with needed data. First parameter is a type of data needed (like metric name), +// second parameter is actual metric name. Second parameter also can be empty. +func (a *Application) respond(rInfo *models.RequestInfo) string { + metric, err := a.storage.Get(rInfo.Metric) + if err != nil { + return "" + } - return a.storage.Get(metricName) + return metric.GetValue() } diff --git a/internal/application/parser.go b/internal/application/parser.go index 9be872d..1bfc670 100644 --- a/internal/application/parser.go +++ b/internal/application/parser.go @@ -3,11 +3,13 @@ package application import ( "log" "strings" + + "go.dev.pztrn.name/metricator/internal/models" ) // Parses passed body and returns a map suitable for pushing into storage. -func (a *Application) parse(body string) map[string]string { - data := make(map[string]string) +func (a *Application) parse(body string) map[string]models.Metric { + data := make(map[string]models.Metric) // ToDo: switch to bytes buffer and maybe do not read body in caller? splittedBody := strings.Split(body, "\n") @@ -46,7 +48,10 @@ func (a *Application) parse(body string) map[string]string { } } - data[name] = value + metric := models.NewMetric(name, "", params) + metric.SetValue(value) + + data[name] = metric } log.Printf("Data parsed: %+v\n", data) diff --git a/internal/common/context_keys.go b/internal/common/context_keys.go deleted file mode 100644 index 8a4d759..0000000 --- a/internal/common/context_keys.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -// ContextKey is a type of context.Context keys. -type ContextKey string - -const ( - // ContextKeyApplication specifies that returned value is a name of application. - ContextKeyApplication ContextKey = "applicationName" - // ContextKeyMetric specifies that returned value is a name of metric of application. - ContextKeyMetric ContextKey = "metricName" -) diff --git a/internal/common/http_handler.go b/internal/common/http_handler.go index 09808d2..31f97a7 100644 --- a/internal/common/http_handler.go +++ b/internal/common/http_handler.go @@ -1,6 +1,6 @@ package common -import "context" +import "go.dev.pztrn.name/metricator/internal/models" // HTTPHandlerFunc describes signature of HTTP requests handling function. -type HTTPHandlerFunc func(context.Context) string +type HTTPHandlerFunc func(*models.RequestInfo) string diff --git a/internal/httpserver/handler.go b/internal/httpserver/handler.go index dd30a55..a54842c 100644 --- a/internal/httpserver/handler.go +++ b/internal/httpserver/handler.go @@ -1,11 +1,26 @@ package httpserver import ( - "context" + "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. @@ -13,41 +28,135 @@ 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(name string, hndl common.HTTPHandlerFunc) { - h.handlers[name] = hndl +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) { - if !strings.HasPrefix(r.URL.Path, "/api") { + 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 - invalid path")) + _, _ = w.Write([]byte("400 bad request - " + err.Error())) return } - // Request validation. - pathSplitted := strings.Split(r.URL.Path, "/") - if len(pathSplitted) < 3 { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("400 Bad Request - invalid path")) - } + // 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())) - handler, found := h.handlers[pathSplitted[2]] - if !found { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("400 Bad Request - invalid application name")) + 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 } - requestContext := r.Context() - - // Compose metric name. - metricName := strings.Join(pathSplitted[3:], "/") - ctx := context.WithValue(requestContext, common.ContextKeyMetric, metricName) - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(handler(ctx))) + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("400 bad request - " + errInvalidPath.Error())) } diff --git a/internal/models/metric.go b/internal/models/metric.go new file mode 100644 index 0000000..dca7cf9 --- /dev/null +++ b/internal/models/metric.go @@ -0,0 +1,34 @@ +package models + +// Metric is a generic metric structure. +type Metric struct { + // Metric name. + name string + // HELP data, if present. + description string + // Additional parameters, data inside "{}". + params []string + // Metric value. + value string +} + +// NewMetric creates new structure for storing single metric data. +func NewMetric(name, description string, params []string) Metric { + m := Metric{ + name: name, + description: description, + params: params, + } + + return m +} + +// GetValue returns metric's value. +func (m *Metric) GetValue() string { + return m.value +} + +// SetValue sets value for metric. +func (m *Metric) SetValue(value string) { + m.value = value +} diff --git a/internal/models/request_info.go b/internal/models/request_info.go new file mode 100644 index 0000000..b58d8f8 --- /dev/null +++ b/internal/models/request_info.go @@ -0,0 +1,9 @@ +package models + +// RequestInfo is a parsed request information to throw into application's handler. +type RequestInfo struct { + ApiVersion int + Application string + Metric string + RequestType string +} diff --git a/internal/storage/generic.go b/internal/storage/generic.go index d88a4d3..6326937 100644 --- a/internal/storage/generic.go +++ b/internal/storage/generic.go @@ -1,15 +1,17 @@ package storage +import "go.dev.pztrn.name/metricator/internal/models" + // GenericStorage describes interface every other storage should embed // and conform to as it contains essential things like context handling. type GenericStorage interface { // Get returns data from storage by key. - Get(string) string + Get(string) (models.Metric, error) // GetDoneChan returns a channel which should be used to block execution // until storage's routines are completed. GetDoneChan() chan struct{} // Put puts passed data into storage. - Put(map[string]string) + Put(map[string]models.Metric) // Start starts asynchronous things if needed. Start() } diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index cda2fed..215aebc 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -2,17 +2,22 @@ package memory import ( "context" + "errors" "log" "sync" + + "go.dev.pztrn.name/metricator/internal/models" ) +var ErrMetricNotFound = errors.New("metric not found") + // Storage is an in-memory storage. type Storage struct { ctx context.Context doneChan chan struct{} name string - data map[string]string + data map[string]models.Metric dataMutex sync.RWMutex } @@ -29,16 +34,16 @@ func NewStorage(ctx context.Context, name string) (*Storage, chan struct{}) { } // Get returns data from storage by key. -func (s *Storage) Get(key string) string { +func (s *Storage) Get(key string) (models.Metric, error) { s.dataMutex.RLock() defer s.dataMutex.RUnlock() data, found := s.data[key] if !found { - return "Not found" + return models.NewMetric("", "", nil), ErrMetricNotFound } - return data + return data, nil } // GetDoneChan returns a channel which should be used to block execution @@ -49,11 +54,11 @@ func (s *Storage) GetDoneChan() chan struct{} { // Initializes internal things. func (s *Storage) initialize() { - s.data = make(map[string]string) + s.data = make(map[string]models.Metric) } // Put puts passed data into storage. -func (s *Storage) Put(data map[string]string) { +func (s *Storage) Put(data map[string]models.Metric) { s.dataMutex.Lock() defer s.dataMutex.Unlock()