diff --git a/Gopkg.lock b/Gopkg.lock index 64ba6f1..0eed74c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -362,6 +362,19 @@ pruneopts = "UT" revision = "de0752318171da717af4ce24d0a2e8626afaeb11" +[[projects]] + branch = "master" + digest = "1:09c175055d303dadf3f82513c66e6da0b95ba22bc8e5d267a2674d16d95ea77a" + name = "golang.org/x/image" + packages = [ + "font", + "font/basicfont", + "font/plan9font", + "math/fixed", + ] + pruneopts = "UT" + revision = "31aff87c08e9a5e5d524279a564f96968336f886" + [[projects]] branch = "master" digest = "1:eb8583d4582ffbc5c6d3ec00132cd0835101edde42b6f24eaafa628d1596f881" @@ -626,7 +639,9 @@ "github.com/jessevdk/go-flags", "github.com/prometheus/client_golang/prometheus", "github.com/prometheus/client_golang/prometheus/promhttp", - "golang.org/x/net/context", + "golang.org/x/image/font", + "golang.org/x/image/font/basicfont", + "golang.org/x/image/math/fixed", "golang.org/x/net/netutil", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/client-go/kubernetes", diff --git a/Makefile b/Makefile index cd48870..3a6001d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ name ?= goldpinger -version ?= 1.3.0 +version ?= 1.4.0 bin ?= goldpinger pkg ?= "github.com/bloomberg/goldpinger" tag = $(name):$(version) diff --git a/build/Dockerfile-simple b/build/Dockerfile-simple index 54d9fe4..f3a9ae5 100644 --- a/build/Dockerfile-simple +++ b/build/Dockerfile-simple @@ -1,6 +1,6 @@ FROM scratch ADD bin/goldpinger /goldpinger -COPY static/index.html /static/index.html +COPY ./static /static ENTRYPOINT ["/goldpinger", "--static-file-path", "/static"] diff --git a/pkg/goldpinger/heatmap.go b/pkg/goldpinger/heatmap.go new file mode 100644 index 0000000..eff1e90 --- /dev/null +++ b/pkg/goldpinger/heatmap.go @@ -0,0 +1,146 @@ +// Copyright 2018 Bloomberg Finance L.P. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file is safe to edit. Once it exists it will not be overwritten + +package goldpinger + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/png" + "log" + "net/http" + "sort" + "strconv" + + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" + "golang.org/x/image/math/fixed" +) + +func addLabel(img *image.RGBA, x, y int, text string) { + drawer := &font.Drawer{ + Dst: img, + Src: image.NewUniform(color.RGBA{25, 200, 25, 255}), + Face: basicfont.Face7x13, + Dot: fixed.Point26_6{fixed.Int26_6(x * 64), fixed.Int26_6(y * 64)}, + } + drawer.DrawString(text) +} + +// Calculates the color of the box to draw based on the latency and tresholds +// We are aiming at slightly more palatable colors than just moving from 255 green to 255 red, +// so we will use 25B, and then move from (25R, 200G) to (200R, 25G), so our scale is effectively 350 points +func getPingBoxColor(latency int64, tresholdLatencies [3]int64) *color.RGBA { + var red, green uint8 = 25, 200 + if latency > tresholdLatencies[2] { + red, green = 200, 25 + } else if latency >= tresholdLatencies[1] { + red, green = 200, 200 + diff := (float32(latency-tresholdLatencies[1]) / float32(tresholdLatencies[2]-tresholdLatencies[1])) * 175 + green = green - uint8(diff) + } else if latency >= tresholdLatencies[0] { + red, green = 25, 200 + diff := (float32(latency-tresholdLatencies[0]) / float32(tresholdLatencies[1]-tresholdLatencies[0])) * 175 + red = red + uint8(diff) + } + return &color.RGBA{red, green, 25, 255} +} + +func drawPingBox(img *image.RGBA, _x, _y, size int, color *color.RGBA) { + for x := _x; x < _x+size; x++ { + for y := _y; y < _y+size; y++ { + img.Set(x, y, *color) + } + } +} + +func getPingBoxCoordinates(col, row, boxSize, padding int) (int, int) { + return col * (boxSize + padding), row * (boxSize + padding) +} + +// HeatmapHandler returns a PNG with a heatmap representation +func HeatmapHandler(w http.ResponseWriter, r *http.Request) { + + // parse the query to set the parameters + query := r.URL.Query() + + // get the results + checkResults := CheckAllPods(GetAllPods()) + + // set some sizes + numberOfPods := len(checkResults.Responses) + legendSize := 200 + boxSize := 14 + paddingSize := 1 + heatmapSize := numberOfPods*(boxSize+paddingSize) + boxSize*2 + tresholdLatencies := [3]int64{1, 10, 100} + for index := range tresholdLatencies { + stringValue := query["t"+fmt.Sprintf("%d", index)] + if len(stringValue) == 0 { + continue + } + if v, err := strconv.ParseInt(stringValue[0], 0, 64); err == nil && v >= 0 { + tresholdLatencies[index] = v + } + } + + canvas := image.NewRGBA(image.Rect(0, 0, heatmapSize+legendSize, heatmapSize)) + + // establish an order and fix the max delay + var keys []string + for sourceIP := range checkResults.Responses { + keys = append(keys, sourceIP) + } + sort.Strings(keys) + order := make(map[string]int) + for index, key := range keys { + order[key] = index + } + + // draw all the boxes + for sourceIP, results := range checkResults.Responses { + if *results.OK { + for destinationIP, response := range results.Response { + x, y := getPingBoxCoordinates(order[sourceIP], order[destinationIP], boxSize, paddingSize) + color := getPingBoxColor(response.ResponseTimeMs, tresholdLatencies) + drawPingBox(canvas, boxSize+x, boxSize+y, boxSize, color) + } + } + } + + // draw the legend + for index, ip := range keys { + // ip + addLabel(canvas, heatmapSize, (index+1)*(boxSize+paddingSize)+13, fmt.Sprintf("%d", index)+": "+ip) + // rows + addLabel(canvas, 0, (index+1)*(boxSize+paddingSize)+13, fmt.Sprintf("%d", index)) + // columns + addLabel(canvas, (index+1)*(boxSize+paddingSize), 13, fmt.Sprintf("%d", index)) + } + + buffer := new(bytes.Buffer) + if err := png.Encode(buffer, canvas); err != nil { + log.Println("error encoding png", err) + } + + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Content-Length", strconv.Itoa(len(buffer.Bytes()))) + if _, err := w.Write(buffer.Bytes()); err != nil { + log.Println("error writing heatmap buffer out", err) + } +} diff --git a/pkg/restapi/configure_goldpinger.go b/pkg/restapi/configure_goldpinger.go index bb5abf7..4bec7d0 100644 --- a/pkg/restapi/configure_goldpinger.go +++ b/pkg/restapi/configure_goldpinger.go @@ -110,6 +110,8 @@ func fileServerMiddleware(next http.Handler) http.Handler { fileServer := http.FileServer(http.Dir(goldpinger.GoldpingerConfig.StaticFilePath)) if r.URL.Path == "/" { http.StripPrefix("/", fileServer).ServeHTTP(w, r) + } else if r.URL.Path == "/heatmap.png" { + goldpinger.HeatmapHandler(w, r) } else if strings.HasPrefix(r.URL.Path, "/static/") { http.StripPrefix("/static/", fileServer).ServeHTTP(w, r) } else { diff --git a/static/index.html b/static/index.html index d2baae5..73cc6e6 100644 --- a/static/index.html +++ b/static/index.html @@ -86,6 +86,7 @@ limitations under the License.
@@ -106,24 +107,70 @@ limitations under the License. -