metrics.go (144 lines of code) (raw):
// Copyright (c) 2015 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
"github.com/jinzhu/gorm"
)
// Metric represents code coverage
type Metric struct {
ID int64 `gorm:"primary_key:yes" json:"id"`
Repository string `sql:"not null" json:"repository"`
Sha string `sql:"not null" json:"sha"`
Branch string `json:"branch"`
PackageCoverage float64 `sql:"not null" json:"packageCoverage"`
FilesCoverage float64 `sql:"not null" json:"filesCoverage"`
ClassesCoverage float64 `sql:"not null" json:"classesCoverage"`
MethodCoverage float64 `sql:"not null" json:"methodCoverage"`
LineCoverage float64 `sql:"not null" json:"lineCoverage"`
ConditionalCoverage float64 `sql:"not null" json:"conditionalCoverage"`
Timestamp int64 `sql:"not null" json:"timestamp"`
LinesCovered int64 `sql:"not null" json:"linesCovered"`
LinesTested int64 `sql:"not null" json:"linesTested"`
}
type errorResponse struct {
Error string `json:"error"`
}
// MetricsHandler represents a metrics handler
type MetricsHandler struct {
db *gorm.DB
}
const defaultBranch = "origin/master"
func writeError(w io.Writer, message string, err error) {
formattedMessage := fmt.Sprintf("%s: %v", message, err)
log.Println(formattedMessage)
errorMsg := errorResponse{
Error: formattedMessage,
}
errorString, encodingError := json.Marshal(errorMsg)
if encodingError != nil {
encodingErrorMessage := fmt.Sprintf("Unable to encode response message %v", encodingError)
log.Printf(encodingErrorMessage)
}
w.Write(errorString)
}
func respondWithMetric(w http.ResponseWriter, m Metric) {
bodyString, err := json.Marshal(m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeError(w, "unable to encode response", err)
return
}
w.Write([]byte(bodyString))
}
// ExtractMetricQuery extracts a query from the request
func ExtractMetricQuery(form url.Values) Metric {
repository := form["repository"][0]
query := Metric{
Repository: repository,
}
if len(form["sha"]) < 1 {
if len(form["branch"]) < 1 {
query.Branch = defaultBranch
} else {
query.Branch = form["branch"][0]
}
} else {
query.Sha = form["sha"][0]
}
return query
}
func (mh MetricsHandler) handleMetricsQuery(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeError(w, "error parsing params", err)
return
}
log.Printf("Handling incoming request: %s", r.Form)
if len(r.Form["repository"]) < 1 {
w.WriteHeader(http.StatusBadRequest)
writeError(w, "missing 'repository'", errors.New("need repository"))
return
}
query := ExtractMetricQuery(r.Form)
m := new(Metric)
dbQuery := mh.db.Where(&query)
if len(r.Form["until"]) > 0 {
dbQuery = dbQuery.Where("timestamp <= ? ", r.Form["until"][0])
}
dbQuery.Order("timestamp desc").First(m)
if m.ID == 0 {
w.WriteHeader(http.StatusNotFound)
writeError(w, "no rows found", errors.New("-"))
return
}
respondWithMetric(w, *m)
}
func (mh MetricsHandler) handleMetricsSave(w http.ResponseWriter, r *http.Request) {
if r.Body == nil {
w.WriteHeader(http.StatusBadRequest)
writeError(w, "no response body", errors.New("nil body"))
return
}
decoder := json.NewDecoder(r.Body)
m := new(Metric)
if err := decoder.Decode(m); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeError(w, "unable to decode body", err)
return
}
log.Printf("Recording metric %v", m)
if err := mh.RecordMetric(m); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeError(w, "error recording metric", err)
} else {
respondWithMetric(w, *m)
}
}
// RecordMetric saves a Metric to the database
func (mh MetricsHandler) RecordMetric(m *Metric) error {
if m.Repository == "" || m.Sha == "" {
return errors.New("missing required field")
}
if m.Timestamp == 0 {
m.Timestamp = time.Now().Unix()
}
mh.db.Create(m)
return nil
}
type handler func(w http.ResponseWriter, r *http.Request)
// NewMetricsHandler creates a new MetricsHandler
func NewMetricsHandler(db *gorm.DB) MetricsHandler {
return MetricsHandler{
db: db,
}
}
// ServeHTTP handles an HTTP request for metrics
func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
mh.handleMetricsQuery(w, r)
} else {
mh.handleMetricsSave(w, r)
}
return
}