diff --git a/server/handlerequest.go b/server/handlerequest.go index b03e817..f3be21f 100644 --- a/server/handlerequest.go +++ b/server/handlerequest.go @@ -4,19 +4,26 @@ import ( "encoding/json" "errors" "fmt" + "time" "github.com/foomo/contentserver/log" "github.com/foomo/contentserver/repo" "github.com/foomo/contentserver/requests" "github.com/foomo/contentserver/responses" + "github.com/foomo/contentserver/status" + "github.com/prometheus/client_golang/prometheus" ) -func handleRequest(r *repo.Repo, handler Handler, jsonBytes []byte) (replyBytes []byte, err error) { +func handleRequest(r *repo.Repo, handler Handler, jsonBytes []byte, metrics *status.Metrics) (replyBytes []byte, err error) { + // variables var reply interface{} var apiErr error var jsonErr error + start := time.Now() + + // helper processor processIfJSONIsOk := func(err error, processingFunc func()) { if err != nil { jsonErr = err @@ -25,37 +32,46 @@ func handleRequest(r *repo.Repo, handler Handler, jsonBytes []byte) (replyBytes processingFunc() } + // handle and process switch handler { case HandlerGetURIs: getURIRequest := &requests.URIs{} processIfJSONIsOk(json.Unmarshal(jsonBytes, &getURIRequest), func() { reply = r.GetURIs(getURIRequest.Dimension, getURIRequest.IDs) }) + addMetrics(metrics, HandlerGetURIs, start, jsonErr, apiErr) case HandlerGetContent: contentRequest := &requests.Content{} processIfJSONIsOk(json.Unmarshal(jsonBytes, &contentRequest), func() { reply, apiErr = r.GetContent(contentRequest) }) + addMetrics(metrics, HandlerGetContent, start, jsonErr, apiErr) case HandlerGetNodes: nodesRequest := &requests.Nodes{} processIfJSONIsOk(json.Unmarshal(jsonBytes, &nodesRequest), func() { reply = r.GetNodes(nodesRequest) }) + addMetrics(metrics, HandlerGetNodes, start, jsonErr, apiErr) case HandlerUpdate: updateRequest := &requests.Update{} processIfJSONIsOk(json.Unmarshal(jsonBytes, &updateRequest), func() { reply = r.Update() }) + addMetrics(metrics, HandlerUpdate, start, jsonErr, apiErr) case HandlerGetRepo: repoRequest := &requests.Repo{} processIfJSONIsOk(json.Unmarshal(jsonBytes, &repoRequest), func() { reply = r.GetRepo() }) + addMetrics(metrics, HandlerGetRepo, start, jsonErr, apiErr) default: err = errors.New(log.Error(" can not handle this one " + handler)) errorResponse := responses.NewError(1, "unknown handler") reply = errorResponse + addMetrics(metrics, "default", start, jsonErr, apiErr) } + + // error handling if jsonErr != nil { err = jsonErr log.Error(" could not read incoming json:", jsonErr) @@ -66,9 +82,30 @@ func handleRequest(r *repo.Repo, handler Handler, jsonBytes []byte) (replyBytes err = apiErr reply = responses.NewError(3, "internal error "+apiErr.Error()) } + return encodeReply(reply) } +func addMetrics(metrics *status.Metrics, handlerName Handler, start time.Time, errJSON error, errAPI error) { + + duration := time.Since(start) + + s := "succeeded" + if errJSON != nil || errAPI != nil { + s = "failed" + } + + metrics.ServiceRequestCounter.With(prometheus.Labels{ + status.MetricLabelHandler: string(handlerName), + status.MetricLabelStatus: s, + }).Inc() + + metrics.ServiceRequestDuration.With(prometheus.Labels{ + status.MetricLabelHandler: string(handlerName), + status.MetricLabelStatus: s, + }).Observe(float64(duration.Nanoseconds())) +} + func encodeReply(reply interface{}) (replyBytes []byte, err error) { encodedBytes, jsonReplyErr := json.MarshalIndent(map[string]interface{}{ "reply": reply, diff --git a/server/server.go b/server/server.go index 06f1329..60e51de 100644 --- a/server/server.go +++ b/server/server.go @@ -74,19 +74,23 @@ func runSocketServer( address string, chanErr chan error, ) { - s := &socketServer{ - stats: newStats(), - repo: repo, - } - ln, err := net.Listen("tcp", address) - if err != nil { - err = errors.New("RunSocketServer: could not start the on \"" + address + "\" - error: " + fmt.Sprint(err)) - // failed to create socket - log.Error(err) - chanErr <- err + // create socket server + s, errSocketServer := newSocketServer(repo) + if errSocketServer != nil { + log.Error(errSocketServer) + chanErr <- errSocketServer return } - // there we go + + // listen on socket + ln, errListen := net.Listen("tcp", address) + if errListen != nil { + errListenSocket := errors.New("RunSocketServer: could not start the on \"" + address + "\" - error: " + fmt.Sprint(errListen)) + log.Error(errListenSocket) + chanErr <- errListenSocket + return + } + log.Record("RunSocketServer: started to listen on " + address) for { // this blocks until connection or error diff --git a/server/socketserver.go b/server/socketserver.go index 0b2be0a..ec92a86 100644 --- a/server/socketserver.go +++ b/server/socketserver.go @@ -10,39 +10,21 @@ import ( "github.com/foomo/contentserver/log" "github.com/foomo/contentserver/repo" "github.com/foomo/contentserver/responses" + "github.com/foomo/contentserver/status" ) -// simple internal request counter -type stats struct { - requests int64 - chanCount chan int -} - -func newStats() *stats { - s := &stats{ - requests: 0, - chanCount: make(chan int), - } - go func() { - for { - select { - case <-s.chanCount: - s.requests++ - s.chanCount <- 1 - } - } - }() - return s -} - -func (s *stats) countRequest() { - s.chanCount <- 1 - <-s.chanCount -} - type socketServer struct { - stats *stats - repo *repo.Repo + repo *repo.Repo + metrics *status.Metrics +} + +// newSocketServer returns a shiny new socket server +func newSocketServer(repo *repo.Repo) (s *socketServer, err error) { + s = &socketServer{ + repo: repo, + metrics: status.NewMetrics("socketserver"), + } + return } func extractHandlerAndJSONLentgh(header string) (handler Handler, jsonLength int, err error) { @@ -58,12 +40,10 @@ func extractHandlerAndJSONLentgh(header string) (handler Handler, jsonLength int } func (s *socketServer) execute(handler Handler, jsonBytes []byte) (reply []byte) { - s.stats.countRequest() - log.Notice("socketServer.execute: ", s.stats.requests, ", ", handler) if log.SelectedLevel == log.LevelDebug { log.Debug(" incoming json buffer:", string(jsonBytes)) } - reply, handlingError := handleRequest(s.repo, handler, jsonBytes) + reply, handlingError := handleRequest(s.repo, handler, jsonBytes, s.metrics) if handlingError != nil { log.Error("socketServer.execute handlingError :", handlingError) } diff --git a/server/webserver.go b/server/webserver.go index ba113d8..3f59c5b 100644 --- a/server/webserver.go +++ b/server/webserver.go @@ -5,19 +5,23 @@ import ( "net/http" "strings" + "github.com/foomo/contentserver/status" + "github.com/foomo/contentserver/repo" ) type webServer struct { - r *repo.Repo - path string + r *repo.Repo + path string + metrics *status.Metrics } // NewWebServer returns a shiny new web server func NewWebServer(path string, r *repo.Repo) (s http.Handler, err error) { s = &webServer{ - r: r, - path: path, + r: r, + path: path, + metrics: status.NewMetrics("webserver"), } return } @@ -33,7 +37,7 @@ func (s *webServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "failed to read incoming request", http.StatusBadRequest) return } - reply, errReply := handleRequest(s.r, Handler(strings.TrimPrefix(r.URL.Path, s.path+"/")), jsonBytes) + reply, errReply := handleRequest(s.r, Handler(strings.TrimPrefix(r.URL.Path, s.path+"/")), jsonBytes, s.metrics) if errReply != nil { http.Error(w, errReply.Error(), http.StatusInternalServerError) return diff --git a/status/metrics.go b/status/metrics.go new file mode 100644 index 0000000..212ae29 --- /dev/null +++ b/status/metrics.go @@ -0,0 +1,43 @@ +package status + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const MetricLabelHandler = "handler" +const MetricLabelStatus = "status" + +type Metrics struct { + ServiceRequestCounter *prometheus.CounterVec // count the number of requests for each service function + ServiceRequestDuration *prometheus.SummaryVec // count the duration of requests for each service function +} + +func NewMetrics(namespace string) *Metrics { + return &Metrics{ + ServiceRequestCounter: serviceRequestCounter("api", namespace), + ServiceRequestDuration: serviceRequestDuration("api", namespace), + } +} + +func serviceRequestCounter(subsystem, namespace string) *prometheus.CounterVec { + vec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "count_service_requests", + Help: "count of requests per func", + }, []string{MetricLabelHandler, MetricLabelStatus}) + prometheus.MustRegister(vec) + return vec +} + +func serviceRequestDuration(subsystem, namespace string) *prometheus.SummaryVec { + vec := prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "time_nanoseconds", + Help: "nanoseconds to unmarshal requests, execute a service function and marshal its reponses", + }, []string{MetricLabelHandler, MetricLabelStatus}) + prometheus.MustRegister(vec) + return vec +}