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.
This commit is contained in:
Stanislav Nikitin 2020-12-23 13:27:17 +05:00
parent de5b55da43
commit 529113a41a
Signed by: pztrn
GPG Key ID: 1E944A0F0568B550
10 changed files with 210 additions and 54 deletions

View File

@ -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.

View File

@ -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()
}

View File

@ -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)

View File

@ -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"
)

View File

@ -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

View File

@ -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()))
}

34
internal/models/metric.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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()