mirror of
https://github.com/bloomberg/goldpinger.git
synced 2026-02-16 19:09:50 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f99ba84d17 | ||
|
|
75315a872a | ||
|
|
ae67cf3594 | ||
|
|
433a6b8b88 | ||
|
|
0660db7922 | ||
|
|
58285945f6 | ||
|
|
8a014ad7f4 | ||
|
|
096b149afa | ||
|
|
5840353d9f | ||
|
|
ac9bf4224d | ||
|
|
bcbc2ac6fc | ||
|
|
192fc433a2 | ||
|
|
dc3864f170 | ||
|
|
8a759433eb | ||
|
|
1b12b7dc6b | ||
|
|
9adf26e2cb | ||
|
|
927125bbc5 | ||
|
|
7585d010a6 | ||
|
|
5e0ecdd8d1 | ||
|
|
d834433d30 | ||
|
|
b64f5152f2 | ||
|
|
bbac97c17e | ||
|
|
74ffcc8d2e | ||
|
|
9586e16237 | ||
|
|
6541250aa9 | ||
|
|
31a503d831 | ||
|
|
195691518b | ||
|
|
aa66c94d47 | ||
|
|
7b2affea29 | ||
|
|
d1a86eae93 | ||
|
|
a2e5925210 | ||
|
|
d243f0fb59 | ||
|
|
477ba69a72 | ||
|
|
86064f208e | ||
|
|
d8f8c20927 | ||
|
|
7c43626b1e | ||
|
|
11ec058b3b | ||
|
|
c006eede86 | ||
|
|
a585b103f3 | ||
|
|
32823fd105 | ||
|
|
87792cffca | ||
|
|
369e9ece78 | ||
|
|
28af41e352 | ||
|
|
52ea5546aa | ||
|
|
9040b69933 | ||
|
|
3ce0c46f91 | ||
|
|
7b156add72 | ||
|
|
8b1dd49506 | ||
|
|
bd13d4d673 | ||
|
|
895af850a1 | ||
|
|
771f303062 | ||
|
|
b7c1d2dfb4 | ||
|
|
86ddcf9505 | ||
|
|
bf43dcff98 | ||
|
|
6de72deee0 | ||
|
|
6cd12ef2d5 | ||
|
|
8182369c02 | ||
|
|
82a0d6ae8c | ||
|
|
057e360c5b | ||
|
|
4cf5dea621 | ||
|
|
16a1d52741 | ||
|
|
0472791dae | ||
|
|
84a43a3d28 | ||
|
|
3f70a24804 | ||
|
|
593307dc01 | ||
|
|
950e2e4ab7 | ||
|
|
d8f0d696ea | ||
|
|
6deb5c3044 | ||
|
|
767d2dba7f | ||
|
|
2efee0f5e5 | ||
|
|
3a729bf196 | ||
|
|
fd84599157 | ||
|
|
f5c2763000 | ||
|
|
22b96d001d |
@@ -1,2 +1 @@
|
||||
.git/
|
||||
/vendor/
|
||||
|
||||
14
.travis.yml
14
.travis.yml
@@ -24,3 +24,17 @@ script:
|
||||
- make clean && make build-multistage
|
||||
- docker images
|
||||
- docker run `make version` --help
|
||||
|
||||
# build an image with the vendor folder
|
||||
- make clean && make vendor && make vendor-build
|
||||
- docker images
|
||||
- docker run `make version`-vendor --help
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: bash docker_deploy.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
go: "1.10.x"
|
||||
condition: -n "$DOCKER_PASSWORD"
|
||||
|
||||
@@ -21,5 +21,4 @@ RUN make bin/goldpinger
|
||||
FROM scratch
|
||||
COPY --from=builder /go/src/github.com/bloomberg/goldpinger/bin/goldpinger /goldpinger
|
||||
COPY ./static /static
|
||||
ENTRYPOINT ["/goldpinger", "--static-file-path", "/static"]
|
||||
|
||||
ENTRYPOINT ["/goldpinger", "--static-file-path", "/static"]
|
||||
35
Gopkg.lock
generated
35
Gopkg.lock
generated
@@ -33,6 +33,14 @@
|
||||
pruneopts = "UT"
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:998cf998358a303ac2430c386ba3fd3398477d6013153d3c6e11432765cc9ae6"
|
||||
name = "github.com/cespare/xxhash"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3b82fb7d186719faeedd0c2864f868c74fbf79a1"
|
||||
version = "v2.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6f82cacd0af5921e99bf3f46748705239b36489464f4529a1589bc895764fb18"
|
||||
name = "github.com/docker/go-units"
|
||||
@@ -355,6 +363,14 @@
|
||||
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2731ee5852c26a3585f70d1a6cff8422383b98bad405eb6611cd4a2334be1df6"
|
||||
name = "github.com/stuartnelson3/go-rendezvous"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "16dc0292e5f420d89a7dc4319775a1e5e461dc01"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3f3a05ae0b95893d90b9b3b5afdb79a9b3d96e4e36e099d841ae602e4aca0da8"
|
||||
name = "golang.org/x/crypto"
|
||||
@@ -362,6 +378,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"
|
||||
@@ -612,6 +641,7 @@
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/cespare/xxhash",
|
||||
"github.com/go-openapi/errors",
|
||||
"github.com/go-openapi/loads",
|
||||
"github.com/go-openapi/runtime",
|
||||
@@ -626,7 +656,10 @@
|
||||
"github.com/jessevdk/go-flags",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"golang.org/x/net/context",
|
||||
"github.com/stuartnelson3/go-rendezvous",
|
||||
"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",
|
||||
|
||||
@@ -73,6 +73,10 @@
|
||||
name = "k8s.io/client-go"
|
||||
version = "9.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stuartnelson3/go-rendezvous"
|
||||
version = "0.2.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
19
Makefile
19
Makefile
@@ -1,10 +1,10 @@
|
||||
name ?= goldpinger
|
||||
version ?= 1.3.0
|
||||
version ?= 2.0.0
|
||||
bin ?= goldpinger
|
||||
pkg ?= "github.com/bloomberg/goldpinger"
|
||||
tag = $(name):$(version)
|
||||
goos ?= ${GOOS}
|
||||
namespace ?= ""
|
||||
namespace ?= "bloomberg/"
|
||||
files = $(shell find . -iname "*.go")
|
||||
|
||||
|
||||
@@ -38,8 +38,19 @@ push:
|
||||
|
||||
run:
|
||||
go run ./cmd/goldpinger/main.go
|
||||
|
||||
|
||||
version:
|
||||
@echo $(tag)
|
||||
|
||||
.PHONY: clean vendor swagger build build-multistage tag push run version
|
||||
|
||||
vendor-build:
|
||||
docker build -t $(tag)-vendor --build-arg TAG=$(tag) -f ./build/Dockerfile-vendor .
|
||||
|
||||
vendor-tag:
|
||||
docker tag $(tag)-vendor $(namespace)$(tag)-vendor
|
||||
|
||||
vendor-push:
|
||||
docker push $(namespace)$(tag)-vendor
|
||||
|
||||
|
||||
.PHONY: clean vendor swagger build build-multistage vendor-build vendor-tag vendor-push tag push run version
|
||||
|
||||
65
README.md
65
README.md
@@ -18,6 +18,7 @@ Oh, and it gives you the graph below for your cluster. Check out the [video expl
|
||||
- [Installation](#installation)
|
||||
- [Authentication with Kubernetes API](#authentication-with-kubernetes-api)
|
||||
- [Example YAML](#example-yaml)
|
||||
- [Note on DNS](#note-on-dns)
|
||||
- [Usage](#usage)
|
||||
- [UI](#ui)
|
||||
- [API](#api)
|
||||
@@ -38,11 +39,20 @@ If you'd like to know more, you can watch [our presentation at Kubecon 2018 Seat
|
||||
|
||||
## Quick start
|
||||
|
||||
Getting from sources:
|
||||
|
||||
```sh
|
||||
go get github.com/bloomberg/goldpinger/cmd/goldpinger
|
||||
goldpinger --help
|
||||
```
|
||||
|
||||
Getting from [docker hub](https://hub.docker.com/r/bloomberg/goldpinger):
|
||||
|
||||
```sh
|
||||
# get from docker hub
|
||||
docker pull bloomberg/goldpinger
|
||||
```
|
||||
|
||||
Note, that in order to guarantee correct versions of dependencies, the project [uses `dep`](./Makefile).
|
||||
|
||||
|
||||
@@ -73,7 +83,7 @@ Building from source code consists of compiling the binary and building a [Docke
|
||||
|
||||
```sh
|
||||
# step 0: check out the code into your $GOPATH
|
||||
go get github.com/bloomberg/goldpinger/cmd/goldpinger
|
||||
go get github.com/bloomberg/goldpinger/cmd/goldpinger
|
||||
cd $GOPATH/src/github.com/bloomberg/goldpinger
|
||||
|
||||
# step 1: download the dependencies via dep ensure
|
||||
@@ -103,9 +113,7 @@ namespace="docker.io/myhandle/" make push
|
||||
### Example YAML
|
||||
|
||||
|
||||
Here's an example of what you can do (using the in-cluster authentication to `Kubernetes` apiserver).
|
||||
|
||||
:warning: Replace `docker.io/mynamespace-replaceme/goldpinger:1.0.0` with the actual tag you built.
|
||||
Here's an example of what you can do (using the in-cluster authentication to `Kubernetes` apiserver).
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -119,12 +127,12 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: goldpinger
|
||||
version: "1.0.0"
|
||||
version: "1.5.0"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: goldpinger
|
||||
version: "1.0.0"
|
||||
version: "1.5.0"
|
||||
spec:
|
||||
containers:
|
||||
- name: goldpinger
|
||||
@@ -138,10 +146,27 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
image: "docker.io/mynamespace-replaceme/goldpinger:1.0.0"
|
||||
# podIP is used to select a randomized subset of nodes to ping.
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
image: "docker.io/bloomberg/goldpinger:1.5.0"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 80
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 80
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -179,6 +204,28 @@ subjects:
|
||||
|
||||
You can also see [an example of using `kubeconfig` in the `./extras`](./extras/example-with-kubeconfig.yaml).
|
||||
|
||||
### Note on DNS
|
||||
|
||||
Note, that on top of resolving the other pods, all instances can also try to resolve arbitrary DNS. This allows you to test your DNS setup.
|
||||
|
||||
From `--help`:
|
||||
|
||||
```sh
|
||||
--host-to-resolve= A host to attempt dns resolve on (space delimited) [$HOSTS_TO_RESOLVE]
|
||||
```
|
||||
|
||||
So in order to test two domains, we could add an extra env var to the example above:
|
||||
|
||||
```yaml
|
||||
- name: HOSTS_TO_RESOLVE
|
||||
value: "www.bloomberg.com one.two.three"
|
||||
```
|
||||
|
||||
and `goldpinger` should show something like this:
|
||||
|
||||

|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### UI
|
||||
@@ -191,7 +238,7 @@ You can click on various nodes to gray out the clutter and see more information.
|
||||
|
||||
### API
|
||||
|
||||
The API exposed is via a well-defined [`Swagger` spec](./swagger.yml).
|
||||
The API exposed is via a well-defined [`Swagger` spec](./swagger.yml).
|
||||
|
||||
The spec is used to generate both the server and the client of `Goldpinger`. If you make changes, you can re-generate them using [go-swagger](https://github.com/go-swagger/go-swagger) via [`make swagger`](./Makefile)
|
||||
|
||||
@@ -247,3 +294,5 @@ Before you create that PR, please make sure you read [CONTRIBUTING](./CONTRIBUTI
|
||||
## License
|
||||
|
||||
Please read the [LICENSE](./LICENSE) file here.
|
||||
|
||||
For each version built by travis, there is also an additional version, appended with `-vendor`, which contains all source code of the dependencies used in `goldpinger`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM scratch
|
||||
|
||||
ADD bin/goldpinger /goldpinger
|
||||
COPY static/index.html /static/index.html
|
||||
COPY bin/goldpinger /goldpinger
|
||||
COPY static /static
|
||||
|
||||
ENTRYPOINT ["/goldpinger", "--static-file-path", "/static"]
|
||||
|
||||
3
build/Dockerfile-vendor
Normal file
3
build/Dockerfile-vendor
Normal file
@@ -0,0 +1,3 @@
|
||||
ARG TAG
|
||||
FROM $TAG
|
||||
COPY vendor /goldpinger-vendor-sources
|
||||
@@ -96,6 +96,14 @@ func main() {
|
||||
goldpinger.GoldpingerConfig.Port = server.Port
|
||||
}
|
||||
|
||||
if goldpinger.GoldpingerConfig.PodIP == "" {
|
||||
log.Println("PodIP not set: pinging all pods")
|
||||
}
|
||||
if goldpinger.GoldpingerConfig.PingNumber == 0 {
|
||||
log.Println("--ping-number set to 0: pinging all pods")
|
||||
}
|
||||
goldpinger.GoldpingerConfig.PodSelecter = goldpinger.NewPodSelecter(goldpinger.GoldpingerConfig.PingNumber, goldpinger.GoldpingerConfig.PodIP, goldpinger.GetAllPods)
|
||||
|
||||
server.ConfigureAPI()
|
||||
goldpinger.StartUpdater()
|
||||
|
||||
|
||||
8
docker_deploy.sh
Executable file
8
docker_deploy.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" \
|
||||
&& make tag \
|
||||
&& make push \
|
||||
&& make vendor-tag \
|
||||
&& make vendor-push
|
||||
|
||||
BIN
extras/dns-screenshot.png
Normal file
BIN
extras/dns-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 374 KiB |
@@ -1,4 +1,3 @@
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
@@ -38,7 +37,12 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
image: "docker.io/mynamespace-replaceme/goldpinger:1.1.0"
|
||||
# podIP is used to select a randomized subset of nodes to ping.
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
image: "docker.io/bloomberg/goldpinger:1.4.0"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
|
||||
@@ -5,19 +5,19 @@ metadata:
|
||||
name: goldpinger
|
||||
labels:
|
||||
app: goldpinger
|
||||
version: "1.1.0"
|
||||
version: "1.5.0"
|
||||
spec:
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: goldpinger
|
||||
version: "1.1.0"
|
||||
version: "1.5.0"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: goldpinger
|
||||
version: "1.1.0"
|
||||
version: "1.5.0"
|
||||
spec:
|
||||
# if you'd like to use a secret to inject a kubeconfig, you can do it like this
|
||||
volumes:
|
||||
@@ -43,10 +43,27 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
image: "docker.io/mynamespace-replaceme/goldpinger:1.1.0"
|
||||
# podIP is used to select randomized subset of nodes to ping.
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
image: "docker.io/bloomberg/goldpinger:1.5.0"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 80
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 80
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 5
|
||||
volumeMounts:
|
||||
- mountPath: /.kube/
|
||||
name: kubeconfig
|
||||
|
||||
@@ -47,7 +47,7 @@ func NewCheckServicePodsOK() *CheckServicePodsOK {
|
||||
Success, return response
|
||||
*/
|
||||
type CheckServicePodsOK struct {
|
||||
Payload models.CheckResults
|
||||
Payload *models.CheckResults
|
||||
}
|
||||
|
||||
func (o *CheckServicePodsOK) Error() string {
|
||||
@@ -56,8 +56,10 @@ func (o *CheckServicePodsOK) Error() string {
|
||||
|
||||
func (o *CheckServicePodsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
|
||||
|
||||
o.Payload = new(models.CheckResults)
|
||||
|
||||
// response payload
|
||||
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
|
||||
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package goldpinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -27,14 +28,14 @@ import (
|
||||
|
||||
// CheckNeighbours queries the kubernetes API server for all other goldpinger pods
|
||||
// then calls Ping() on each one
|
||||
func CheckNeighbours() models.CheckResults {
|
||||
return PingAllPods(GetAllPods())
|
||||
func CheckNeighbours(ps *PodSelecter) *models.CheckResults {
|
||||
return PingAllPods(ps.SelectPods())
|
||||
}
|
||||
|
||||
// CheckNeighboursNeighbours queries the kubernetes API server for all other goldpinger
|
||||
// pods then calls Check() on each one
|
||||
func CheckNeighboursNeighbours() *models.CheckAllResults {
|
||||
return CheckAllPods(GetAllPods())
|
||||
func CheckNeighboursNeighbours(ps *PodSelecter) *models.CheckAllResults {
|
||||
return CheckAllPods(ps.SelectPods())
|
||||
}
|
||||
|
||||
type PingAllPodsResult struct {
|
||||
@@ -50,7 +51,25 @@ func pickPodHostIP(podIP, hostIP string) string {
|
||||
return podIP
|
||||
}
|
||||
|
||||
func PingAllPods(pods map[string]string) models.CheckResults {
|
||||
func checkDNS() *models.DNSResults {
|
||||
results := models.DNSResults{}
|
||||
for _, host := range GoldpingerConfig.DnsHosts{
|
||||
|
||||
var dnsResult models.DNSResult
|
||||
|
||||
start := time.Now()
|
||||
_, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
dnsResult.Error = err.Error()
|
||||
CountDnsError(host)
|
||||
}
|
||||
dnsResult.ResponseTimeMs = time.Since(start).Nanoseconds() / int64(time.Millisecond)
|
||||
results[host] = dnsResult
|
||||
}
|
||||
return &results
|
||||
}
|
||||
|
||||
func PingAllPods(pods map[string]string) *models.CheckResults {
|
||||
|
||||
result := models.CheckResults{}
|
||||
|
||||
@@ -84,11 +103,15 @@ func PingAllPods(pods map[string]string) models.CheckResults {
|
||||
wg.Done()
|
||||
}(podIP, hostIP)
|
||||
}
|
||||
if len(GoldpingerConfig.DnsHosts) > 0 {
|
||||
result.DNSResults = *checkDNS()
|
||||
}
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
|
||||
counterHealthy, counterUnhealthy := 0.0, 0.0
|
||||
|
||||
result.PodResults = make(map[string]models.PodResult)
|
||||
for response := range ch {
|
||||
var podIPv4 strfmt.IPv4
|
||||
podIPv4.UnmarshalText([]byte(response.podIP))
|
||||
@@ -97,10 +120,10 @@ func PingAllPods(pods map[string]string) models.CheckResults {
|
||||
} else {
|
||||
counterUnhealthy++
|
||||
}
|
||||
result[response.podIP] = response.podResult
|
||||
result.PodResults[response.podIP] = response.podResult
|
||||
}
|
||||
CountHealthyUnhealthyNodes(counterHealthy, counterUnhealthy)
|
||||
return result
|
||||
return &result
|
||||
}
|
||||
|
||||
type CheckServicePodsResult struct {
|
||||
@@ -161,6 +184,18 @@ func CheckAllPods(pods map[string]string) *models.CheckAllResults {
|
||||
HostIP: response.hostIPv4,
|
||||
PodIP: podIPv4,
|
||||
})
|
||||
if response.checkAllPodResult.Response != nil &&
|
||||
response.checkAllPodResult.Response.DNSResults != nil {
|
||||
if result.DNSResults == nil {
|
||||
result.DNSResults = make(map[string]models.DNSResults)
|
||||
}
|
||||
for host := range response.checkAllPodResult.Response.DNSResults {
|
||||
if result.DNSResults[host] == nil {
|
||||
result.DNSResults[host] = make(map[string]models.DNSResult)
|
||||
}
|
||||
result.DNSResults[host][response.podIP] = response.checkAllPodResult.Response.DNSResults[host]
|
||||
}
|
||||
}
|
||||
}
|
||||
return &result
|
||||
}
|
||||
|
||||
@@ -24,8 +24,13 @@ var GoldpingerConfig = struct {
|
||||
KubeConfigPath string `long:"kubeconfig" description:"Path to kubeconfig file" env:"KUBECONFIG"`
|
||||
RefreshInterval int `long:"refresh-interval" description:"If > 0, will create a thread and collect stats every n seconds" env:"REFRESH_INTERVAL" default:"30"`
|
||||
Hostname string `long:"hostname" description:"Hostname to use" env:"HOSTNAME"`
|
||||
PodIP string `long:"pod-ip" description:"Pod IP to use" env:"POD_IP"`
|
||||
PingNumber uint `long:"ping-number" description:"Number of peers to ping. A value of 0 indicates all peers should be pinged." default:"0" env:"PING_NUMBER"`
|
||||
Port int `long:"client-port-override" description:"(for testing) use this port when calling other instances" env:"CLIENT_PORT_OVERRIDE"`
|
||||
UseHostIP bool `long:"use-host-ip" description:"When making the calls, use host ip (defaults to pod ip)" env:"USE_HOST_IP"`
|
||||
LabelSelector string `long:"label-selector" description:"label selector to use to discover goldpinger pods in the cluster" env:"LABEL_SELECTOR" default:"app=goldpinger"`
|
||||
KubernetesClient *kubernetes.Clientset
|
||||
*PodSelecter
|
||||
|
||||
DnsHosts []string `long:"host-to-resolve" description:"A host to attempt dns resolve on (space delimited)" env:"HOSTS_TO_RESOLVE" env-delim:" "`
|
||||
}{}
|
||||
|
||||
146
pkg/goldpinger/heatmap.go
Normal file
146
pkg/goldpinger/heatmap.go
Normal file
@@ -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.PodResults {
|
||||
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)
|
||||
}
|
||||
}
|
||||
61
pkg/goldpinger/pod_selecter.go
Normal file
61
pkg/goldpinger/pod_selecter.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
package goldpinger
|
||||
|
||||
import (
|
||||
"github.com/cespare/xxhash"
|
||||
rendezvous "github.com/stuartnelson3/go-rendezvous"
|
||||
)
|
||||
|
||||
// PodSelecter selects the result of getPods() down to count instances
|
||||
// according to a rendezvous hash.
|
||||
type PodSelecter struct {
|
||||
count uint
|
||||
podIP string
|
||||
getPods func() map[string]string
|
||||
}
|
||||
|
||||
// NewPodSelecter creates a new PodSelecter struct.
|
||||
func NewPodSelecter(count uint, podIP string, getPods func() map[string]string) *PodSelecter {
|
||||
if podIP == "" {
|
||||
// If podIP is blank, then we can't use the rendezvous hash to
|
||||
// assign the IP correctly. Setting count=0 will force all pods
|
||||
// to be pinged.
|
||||
count = 0
|
||||
}
|
||||
return &PodSelecter{
|
||||
count: count,
|
||||
podIP: podIP,
|
||||
getPods: getPods,
|
||||
}
|
||||
}
|
||||
|
||||
// SelectPods returns a map of pods filtered according to its configuration.
|
||||
func (p *PodSelecter) SelectPods() map[string]string {
|
||||
allPods := p.getPods()
|
||||
if p.count == 0 || p.count >= uint(len(allPods)) {
|
||||
return allPods
|
||||
}
|
||||
rzv := rendezvous.New([]string{}, rendezvous.Hasher(xxhash.Sum64String))
|
||||
for podIP := range allPods {
|
||||
rzv.Add(podIP)
|
||||
}
|
||||
matches := rzv.LookupN(p.podIP, p.count)
|
||||
toPing := make(map[string]string)
|
||||
for _, podIP := range matches {
|
||||
toPing[podIP] = allPods[podIP]
|
||||
}
|
||||
return toPing
|
||||
}
|
||||
@@ -82,6 +82,16 @@ var (
|
||||
"type",
|
||||
},
|
||||
)
|
||||
goldpingerDnsErrorsCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "goldpinger_dns_errors_total",
|
||||
Help: "Statistics of DNS errors per instance",
|
||||
},
|
||||
[]string{
|
||||
"goldpinger_instance",
|
||||
"host",
|
||||
},
|
||||
)
|
||||
|
||||
bootTime = time.Now()
|
||||
)
|
||||
@@ -92,6 +102,7 @@ func init() {
|
||||
prometheus.MustRegister(goldpingerResponseTimePeersHistogram)
|
||||
prometheus.MustRegister(goldpingerResponseTimeKubernetesHistogram)
|
||||
prometheus.MustRegister(goldpingerErrorsCounter)
|
||||
prometheus.MustRegister(goldpingerDnsErrorsCounter)
|
||||
log.Println("Metrics setup - see /metrics")
|
||||
}
|
||||
|
||||
@@ -131,6 +142,14 @@ func CountError(errorType string) {
|
||||
).Inc()
|
||||
}
|
||||
|
||||
// counts instances of dns errors
|
||||
func CountDnsError(host string) {
|
||||
goldpingerDnsErrorsCounter.WithLabelValues(
|
||||
GoldpingerConfig.Hostname,
|
||||
host,
|
||||
).Inc()
|
||||
}
|
||||
|
||||
// returns a timer for easy observing of the durations of calls to kubernetes API
|
||||
func GetLabeledKubernetesCallsTimer() *prometheus.Timer {
|
||||
return prometheus.NewTimer(
|
||||
|
||||
@@ -21,17 +21,17 @@ import (
|
||||
)
|
||||
|
||||
func StartUpdater() {
|
||||
|
||||
if GoldpingerConfig.RefreshInterval <= 0 {
|
||||
log.Println("Not creating updater, period is 0")
|
||||
return
|
||||
}
|
||||
|
||||
// start the updater
|
||||
go func() {
|
||||
for {
|
||||
results := PingAllPods(GetAllPods())
|
||||
results := PingAllPods(GoldpingerConfig.PodSelecter.SelectPods())
|
||||
var troublemakers []string
|
||||
for podIP, value := range results {
|
||||
for podIP, value := range results.PodResults {
|
||||
if *value.OK != true {
|
||||
troublemakers = append(troublemakers, fmt.Sprintf("%s (%s)", podIP, value.HostIP.String()))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ type CheckAllPodResult struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
|
||||
// response
|
||||
Response CheckResults `json:"response,omitempty"`
|
||||
Response *CheckResults `json:"response,omitempty"`
|
||||
|
||||
// status code
|
||||
StatusCode int32 `json:"status-code,omitempty"`
|
||||
@@ -71,11 +71,13 @@ func (m *CheckAllPodResult) validateResponse(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.Response.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("response")
|
||||
if m.Response != nil {
|
||||
if err := m.Response.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("response")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -22,6 +22,9 @@ type CheckAllResults struct {
|
||||
// o k
|
||||
OK *bool `json:"OK,omitempty"`
|
||||
|
||||
// dns results
|
||||
DNSResults map[string]DNSResults `json:"dnsResults,omitempty"`
|
||||
|
||||
// hosts
|
||||
Hosts []*CheckAllResultsHostsItems0 `json:"hosts"`
|
||||
|
||||
@@ -39,6 +42,10 @@ type CheckAllResults struct {
|
||||
func (m *CheckAllResults) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.validateDNSResults(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateHosts(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
@@ -53,6 +60,25 @@ func (m *CheckAllResults) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CheckAllResults) validateDNSResults(formats strfmt.Registry) error {
|
||||
|
||||
if swag.IsZero(m.DNSResults) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
for k := range m.DNSResults {
|
||||
|
||||
if val, ok := m.DNSResults[k]; ok {
|
||||
if err := val.Validate(formats); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CheckAllResults) validateHosts(formats strfmt.Registry) error {
|
||||
|
||||
if swag.IsZero(m.Hosts) { // not required
|
||||
|
||||
@@ -9,28 +9,31 @@ import (
|
||||
strfmt "github.com/go-openapi/strfmt"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/go-openapi/validate"
|
||||
)
|
||||
|
||||
// CheckResults check results
|
||||
// swagger:model CheckResults
|
||||
type CheckResults map[string]PodResult
|
||||
type CheckResults struct {
|
||||
|
||||
// dns results
|
||||
DNSResults DNSResults `json:"dnsResults,omitempty"`
|
||||
|
||||
// pod results
|
||||
PodResults map[string]PodResult `json:"podResults,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this check results
|
||||
func (m CheckResults) Validate(formats strfmt.Registry) error {
|
||||
func (m *CheckResults) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
for k := range m {
|
||||
|
||||
if err := validate.Required(k, "body", m[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
if val, ok := m[k]; ok {
|
||||
if err := val.Validate(formats); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := m.validateDNSResults(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validatePodResults(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
@@ -38,3 +41,59 @@ func (m CheckResults) Validate(formats strfmt.Registry) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CheckResults) validateDNSResults(formats strfmt.Registry) error {
|
||||
|
||||
if swag.IsZero(m.DNSResults) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.DNSResults.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("dnsResults")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CheckResults) validatePodResults(formats strfmt.Registry) error {
|
||||
|
||||
if swag.IsZero(m.PodResults) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
for k := range m.PodResults {
|
||||
|
||||
if err := validate.Required("podResults"+"."+k, "body", m.PodResults[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
if val, ok := m.PodResults[k]; ok {
|
||||
if err := val.Validate(formats); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *CheckResults) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return swag.WriteJSON(m)
|
||||
}
|
||||
|
||||
// UnmarshalBinary interface implementation
|
||||
func (m *CheckResults) UnmarshalBinary(b []byte) error {
|
||||
var res CheckResults
|
||||
if err := swag.ReadJSON(b, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = res
|
||||
return nil
|
||||
}
|
||||
|
||||
46
pkg/models/dns_result.go
Normal file
46
pkg/models/dns_result.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
package models
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
strfmt "github.com/go-openapi/strfmt"
|
||||
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
// DNSResult Dns result
|
||||
// swagger:model DnsResult
|
||||
type DNSResult struct {
|
||||
|
||||
// error
|
||||
Error string `json:"error,omitempty"`
|
||||
|
||||
// response time ms
|
||||
ResponseTimeMs int64 `json:"response-time-ms,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this Dns result
|
||||
func (m *DNSResult) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *DNSResult) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return swag.WriteJSON(m)
|
||||
}
|
||||
|
||||
// UnmarshalBinary interface implementation
|
||||
func (m *DNSResult) UnmarshalBinary(b []byte) error {
|
||||
var res DNSResult
|
||||
if err := swag.ReadJSON(b, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = res
|
||||
return nil
|
||||
}
|
||||
40
pkg/models/dns_results.go
Normal file
40
pkg/models/dns_results.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
package models
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
strfmt "github.com/go-openapi/strfmt"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/validate"
|
||||
)
|
||||
|
||||
// DNSResults Dns results
|
||||
// swagger:model DnsResults
|
||||
type DNSResults map[string]DNSResult
|
||||
|
||||
// Validate validates this Dns results
|
||||
func (m DNSResults) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
for k := range m {
|
||||
|
||||
if err := validate.Required(k, "body", m[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
if val, ok := m[k]; ok {
|
||||
if err := val.Validate(formats); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -47,6 +47,7 @@ func configureFlags(api *operations.GoldpingerAPI) {
|
||||
|
||||
func configureAPI(api *operations.GoldpingerAPI) http.Handler {
|
||||
// configure the api here
|
||||
ps := goldpinger.GoldpingerConfig.PodSelecter
|
||||
api.ServeError = errors.ServeError
|
||||
|
||||
api.JSONConsumer = runtime.JSONConsumer()
|
||||
@@ -61,13 +62,13 @@ func configureAPI(api *operations.GoldpingerAPI) http.Handler {
|
||||
api.CheckServicePodsHandler = operations.CheckServicePodsHandlerFunc(
|
||||
func(params operations.CheckServicePodsParams) middleware.Responder {
|
||||
goldpinger.CountCall("received", "check")
|
||||
return operations.NewCheckServicePodsOK().WithPayload(goldpinger.CheckNeighbours())
|
||||
return operations.NewCheckServicePodsOK().WithPayload(goldpinger.CheckNeighbours(ps))
|
||||
})
|
||||
|
||||
api.CheckAllPodsHandler = operations.CheckAllPodsHandlerFunc(
|
||||
func(params operations.CheckAllPodsParams) middleware.Responder {
|
||||
goldpinger.CountCall("received", "check_all")
|
||||
return operations.NewCheckAllPodsOK().WithPayload(goldpinger.CheckNeighboursNeighbours())
|
||||
return operations.NewCheckAllPodsOK().WithPayload(goldpinger.CheckNeighboursNeighbours(ps))
|
||||
})
|
||||
|
||||
api.HealthzHandler = operations.HealthzHandlerFunc(
|
||||
@@ -110,6 +111,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 {
|
||||
|
||||
@@ -7,7 +7,7 @@ Package restapi Goldpinger
|
||||
http
|
||||
Host: localhost
|
||||
BasePath: /
|
||||
Version: 1.0.0
|
||||
Version: 2.0.0
|
||||
|
||||
Consumes:
|
||||
- application/json
|
||||
|
||||
@@ -21,7 +21,7 @@ func init() {
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Goldpinger",
|
||||
"version": "1.0.0"
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/check": {
|
||||
@@ -143,6 +143,12 @@ func init() {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"dnsResults": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/DnsResults"
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -176,9 +182,34 @@ func init() {
|
||||
}
|
||||
},
|
||||
"CheckResults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dnsResults": {
|
||||
"$ref": "#/definitions/DnsResults"
|
||||
},
|
||||
"podResults": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PodResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DnsResult": {
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"response-time-ms": {
|
||||
"type": "number",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DnsResults": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PodResult"
|
||||
"$ref": "#/definitions/DnsResult"
|
||||
}
|
||||
},
|
||||
"HealthCheckResults": {
|
||||
@@ -244,7 +275,7 @@ func init() {
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Goldpinger",
|
||||
"version": "1.0.0"
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/check": {
|
||||
@@ -366,6 +397,12 @@ func init() {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"dnsResults": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/DnsResults"
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -399,9 +436,34 @@ func init() {
|
||||
}
|
||||
},
|
||||
"CheckResults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dnsResults": {
|
||||
"$ref": "#/definitions/DnsResults"
|
||||
},
|
||||
"podResults": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PodResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DnsResult": {
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"response-time-ms": {
|
||||
"type": "number",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DnsResults": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PodResult"
|
||||
"$ref": "#/definitions/DnsResult"
|
||||
}
|
||||
},
|
||||
"HealthCheckResults": {
|
||||
|
||||
@@ -25,7 +25,7 @@ type CheckServicePodsOK struct {
|
||||
/*
|
||||
In: Body
|
||||
*/
|
||||
Payload models.CheckResults `json:"body,omitempty"`
|
||||
Payload *models.CheckResults `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// NewCheckServicePodsOK creates CheckServicePodsOK with default headers values
|
||||
@@ -35,13 +35,13 @@ func NewCheckServicePodsOK() *CheckServicePodsOK {
|
||||
}
|
||||
|
||||
// WithPayload adds the payload to the check service pods o k response
|
||||
func (o *CheckServicePodsOK) WithPayload(payload models.CheckResults) *CheckServicePodsOK {
|
||||
func (o *CheckServicePodsOK) WithPayload(payload *models.CheckResults) *CheckServicePodsOK {
|
||||
o.Payload = payload
|
||||
return o
|
||||
}
|
||||
|
||||
// SetPayload sets the payload to the check service pods o k response
|
||||
func (o *CheckServicePodsOK) SetPayload(payload models.CheckResults) {
|
||||
func (o *CheckServicePodsOK) SetPayload(payload *models.CheckResults) {
|
||||
o.Payload = payload
|
||||
}
|
||||
|
||||
@@ -49,13 +49,10 @@ func (o *CheckServicePodsOK) SetPayload(payload models.CheckResults) {
|
||||
func (o *CheckServicePodsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
|
||||
|
||||
rw.WriteHeader(200)
|
||||
payload := o.Payload
|
||||
if payload == nil {
|
||||
// return empty map
|
||||
payload = models.CheckResults{}
|
||||
}
|
||||
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
if o.Payload != nil {
|
||||
payload := o.Payload
|
||||
if err := producer.Produce(rw, payload); err != nil {
|
||||
panic(err) // let the recovery middleware deal with this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +65,13 @@ type GoldpingerAPI struct {
|
||||
Middleware func(middleware.Builder) http.Handler
|
||||
|
||||
// BasicAuthenticator generates a runtime.Authenticator from the supplied basic auth function.
|
||||
// It has a default implemention in the security package, however you can replace it for your particular usage.
|
||||
// It has a default implementation in the security package, however you can replace it for your particular usage.
|
||||
BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator
|
||||
// APIKeyAuthenticator generates a runtime.Authenticator from the supplied token auth function.
|
||||
// It has a default implemention in the security package, however you can replace it for your particular usage.
|
||||
// It has a default implementation in the security package, however you can replace it for your particular usage.
|
||||
APIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator
|
||||
// BearerAuthenticator generates a runtime.Authenticator from the supplied bearer token auth function.
|
||||
// It has a default implemention in the security package, however you can replace it for your particular usage.
|
||||
// It has a default implementation in the security package, however you can replace it for your particular usage.
|
||||
BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator
|
||||
|
||||
// JSONConsumer registers a consumer for a "application/json" mime type
|
||||
|
||||
@@ -87,7 +87,7 @@ type Server struct {
|
||||
TLSHost string `long:"tls-host" description:"the IP to listen on for tls, when not specified it's the same as --host" env:"TLS_HOST"`
|
||||
TLSPort int `long:"tls-port" description:"the port to listen on for secure connections, defaults to a random value" env:"TLS_PORT"`
|
||||
TLSCertificate flags.Filename `long:"tls-certificate" description:"the certificate to use for secure connections" env:"TLS_CERTIFICATE"`
|
||||
TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure conections" env:"TLS_PRIVATE_KEY"`
|
||||
TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure connections" env:"TLS_PRIVATE_KEY"`
|
||||
TLSCACertificate flags.Filename `long:"tls-ca" description:"the certificate authority file to be used with mutual tls auth" env:"TLS_CA_CERTIFICATE"`
|
||||
TLSListenLimit int `long:"tls-listen-limit" description:"limit the number of outstanding requests"`
|
||||
TLSKeepAlive time.Duration `long:"tls-keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)"`
|
||||
|
||||
@@ -49,7 +49,7 @@ limitations under the License.
|
||||
<script type="text/javascript" src="static/sigma.js/1.1.0/plugins/sigma.renderers.parallelEdges.min.js"></script>
|
||||
<script type="text/javascript" src="static/sigma.js/1.1.0/plugins/sigma.renderers.snapshot.min.js"></script>
|
||||
<script type="text/javascript" src="static/sigma.js/1.1.0/plugins/sigma.statistics.HITS.min.js"></script>
|
||||
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link rel="stylesheet" href="static/bootstrap/3.3.7/css/bootstrap.min.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="static/bootstrap/3.3.7/css/bootstrap-theme.min.css" crossorigin="anonymous">
|
||||
@@ -86,6 +86,7 @@ limitations under the License.
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="#">Graph</a></li>
|
||||
<li><a href="#" id="show-data">Data</a></li>
|
||||
<li><a href="#" id="show-heatmap">Heatmap</a></li>
|
||||
<li><a href="check_all" >Raw</a></li>
|
||||
<li><a href="metrics" >Metrics</a></li>
|
||||
</ul>
|
||||
@@ -106,24 +107,70 @@ limitations under the License.
|
||||
|
||||
</div>
|
||||
|
||||
<div id="modal-window-code" class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="modal-title">title</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-body">
|
||||
body
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
<div id="modal-window-code" class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="modal-title">title</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-body">
|
||||
body
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="modal-window-heatmap" class="modal fade bs-example-modal-lg" tabindex="-2" role="dialog" aria-labelledby="myLargeModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="modal-title">Heatmap</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div id="heatmap-body" style="padding: 10px"></div>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" style="width: 200px">Good treshold</span>
|
||||
<input id="t0" type="number" step="1" class="form-control" placeholder="2" value="2">
|
||||
<span class="input-group-addon">milliseconds</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" style="width: 200px">Warning treshold</span>
|
||||
<input id="t1" type="number" step="1" class="form-control" placeholder="5" value="5">
|
||||
<span class="input-group-addon">milliseconds</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" style="width: 200px">Problem treshold</span>
|
||||
<input id="t2" type="number" step="1" class="form-control" placeholder="100" value="100">
|
||||
<span class="input-group-addon">milliseconds</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-success" id="update-heatmap" type="button">refresh</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
var getHeatmapUrl = function(){
|
||||
return "heatmap.png?"
|
||||
+ "t0=" + Number($("#t0").val())
|
||||
+ "&t1=" + Number($("#t1").val())
|
||||
+ "&t2=" + Number($("#t2").val())
|
||||
+ "&now=" + Date.now();
|
||||
}
|
||||
|
||||
var fetchJSON = function(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
console.log("calling " + url);
|
||||
@@ -172,7 +219,7 @@ var main = function(timeout){
|
||||
|
||||
loadingDialog(true);
|
||||
|
||||
fetchJSON("/check_all?timeout="+timeout)
|
||||
fetchJSON("check_all?timeout="+timeout)
|
||||
.then(function(data){
|
||||
|
||||
loadingDialog(false);
|
||||
@@ -182,6 +229,8 @@ var main = function(timeout){
|
||||
|
||||
// prepare nodes
|
||||
var nodes = [];
|
||||
var dnsCheckNodes = [];
|
||||
var podIPs = [];
|
||||
var resp = data.responses;
|
||||
for (let podIP in resp) {
|
||||
let value = resp[podIP];
|
||||
@@ -192,14 +241,14 @@ var main = function(timeout){
|
||||
"y": 0,
|
||||
_data: value,
|
||||
});
|
||||
podIPs.push(podIP);
|
||||
};
|
||||
//console.log(nodes);
|
||||
|
||||
// prepare the edges
|
||||
var edges = [];
|
||||
var resp = data.responses;
|
||||
for (let callId in resp) {
|
||||
var call = resp[callId].response;
|
||||
var call = resp[callId].response['podResults'];
|
||||
if (typeof call !== 'string'){
|
||||
for (let target in call) {
|
||||
edges.push({
|
||||
@@ -210,8 +259,61 @@ var main = function(timeout){
|
||||
};
|
||||
}
|
||||
};
|
||||
//console.log(edges);
|
||||
|
||||
console.log(edges);
|
||||
console.log(nodes);
|
||||
|
||||
// if running goldpinger with PING_NUMBER, then we need to add nodes for the edges
|
||||
// that are not reporting but are reported to
|
||||
edges.forEach(function(edge) {
|
||||
[edge.source, edge.target].forEach(function(value) {
|
||||
if (podIPs.indexOf(value) == -1){
|
||||
nodes.push({
|
||||
"label": value,
|
||||
"id": value,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
_data: {},
|
||||
});
|
||||
podIPs.push(value);
|
||||
}
|
||||
})
|
||||
})
|
||||
console.log(nodes);
|
||||
|
||||
if ('dnsResults' in data) {
|
||||
var yoffset = 0;
|
||||
for (let checkedHost in data.dnsResults) {
|
||||
let value = data.dnsResults[checkedHost];
|
||||
var allOk = true;
|
||||
for (let pod in value) {
|
||||
var podData = value[pod];
|
||||
var elapsed = 0;
|
||||
if ('response-time-ms' in podData) {
|
||||
elapsed = podData['response-time-ms'];
|
||||
}
|
||||
var podOk = (!('error' in podData));
|
||||
allOk = allOk && podOk
|
||||
edges.push({
|
||||
source: pod,
|
||||
target: checkedHost,
|
||||
_data: {
|
||||
"OK": podOk,
|
||||
"elapsed": elapsed,
|
||||
"isDNSCheckNode": true,
|
||||
}
|
||||
});
|
||||
}
|
||||
value["OK"] = allOk
|
||||
dnsCheckNodes.push({
|
||||
"label": checkedHost,
|
||||
"id": checkedHost,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
_data: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// build the actual graph
|
||||
s = new sigma({
|
||||
@@ -244,6 +346,19 @@ var main = function(timeout){
|
||||
s.graph.addNode(node);
|
||||
});
|
||||
|
||||
// generate any dns nodes on the graph
|
||||
dnsCheckNodes.forEach(function(node, i, a){
|
||||
node.x = 2;
|
||||
node.y = (0.6 * i / a.length) - 0.8;
|
||||
node.size = 10;
|
||||
node.color = "#4CC40B";
|
||||
if (!node._data.OK) {
|
||||
node.color = "red";
|
||||
}
|
||||
//console.log(node);
|
||||
s.graph.addNode(node);
|
||||
});
|
||||
|
||||
// generate the edges
|
||||
edges.forEach(function(edge, i){
|
||||
var color = "#ccc";
|
||||
@@ -255,6 +370,9 @@ var main = function(timeout){
|
||||
if (!edge._data.OK) {
|
||||
color = "red";
|
||||
}
|
||||
if ("isDNSCheckNode" in edge._data) {
|
||||
type = "dashed";
|
||||
}
|
||||
var edge = {
|
||||
id: "e" + i,
|
||||
source: edge.source,
|
||||
@@ -305,7 +423,7 @@ var main = function(timeout){
|
||||
$('#modal-title').html(node.id);
|
||||
$('#modal-body').html(prepareJSON(node._data));
|
||||
|
||||
// show the things
|
||||
// show the things
|
||||
$('#modal-window-code').modal('show');
|
||||
});
|
||||
s.bind("clickStage", function (e) {
|
||||
@@ -321,7 +439,7 @@ var main = function(timeout){
|
||||
$('#modal-title').html(edge.id);
|
||||
$('#modal-body').html(prepareJSON(edge._data));
|
||||
|
||||
// show the things
|
||||
// show the things
|
||||
$('#modal-window-code').modal('show');
|
||||
});
|
||||
})
|
||||
@@ -337,18 +455,31 @@ main();
|
||||
|
||||
$("#show-data").click(function (e) {
|
||||
// fill the voids
|
||||
$('#modal-title').html("/check_all");
|
||||
$('#modal-title').html("check_all");
|
||||
$('#modal-body').html(prepareJSON(global_data));
|
||||
|
||||
// show the things
|
||||
// show the things
|
||||
$('#modal-window-code').modal('show');
|
||||
});
|
||||
$("#reload-graph").click(function (e) {
|
||||
s.kill();
|
||||
main();
|
||||
});
|
||||
$("#show-heatmap").click(function (e) {
|
||||
updateHeatmap();
|
||||
$('#modal-window-heatmap').modal('show');
|
||||
});
|
||||
var updateHeatmap = function(){
|
||||
$('#heatmap-body').html(
|
||||
'<img src="' + getHeatmapUrl() + '" />'
|
||||
);
|
||||
}
|
||||
$("#update-heatmap").click(function (e) {
|
||||
updateHeatmap();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
26
swagger.yml
26
swagger.yml
@@ -1,7 +1,7 @@
|
||||
---
|
||||
swagger: '2.0'
|
||||
info:
|
||||
version: 1.0.0
|
||||
version: 2.0.0
|
||||
title: Goldpinger
|
||||
definitions:
|
||||
CallStats:
|
||||
@@ -12,6 +12,17 @@ definitions:
|
||||
type: integer
|
||||
ping:
|
||||
type: integer
|
||||
DnsResult:
|
||||
properties:
|
||||
response-time-ms:
|
||||
type: number
|
||||
format: int64
|
||||
error:
|
||||
type: string
|
||||
DnsResults:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/DnsResult'
|
||||
PingResults:
|
||||
type: object
|
||||
properties:
|
||||
@@ -42,8 +53,13 @@ definitions:
|
||||
description: wall clock time in milliseconds
|
||||
CheckResults:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/PodResult'
|
||||
properties:
|
||||
dnsResults:
|
||||
$ref: '#/definitions/DnsResults'
|
||||
podResults:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/PodResult'
|
||||
CheckAllPodResult:
|
||||
type: object
|
||||
properties:
|
||||
@@ -83,6 +99,10 @@ definitions:
|
||||
podIP:
|
||||
type: string
|
||||
format: ipv4
|
||||
dnsResults:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/DnsResults'
|
||||
responses:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
||||
Reference in New Issue
Block a user