Skip to content

Commit

Permalink
resolve issue #5 and #12
Browse files Browse the repository at this point in the history
  • Loading branch information
wangchen615 committed Feb 15, 2021
1 parent 251e1a0 commit 498c7a8
Show file tree
Hide file tree
Showing 16 changed files with 460 additions and 113 deletions.
12 changes: 9 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
FROM golang:1.5-alpine
FROM golang:1.15.5

ADD ./load-watcher /usr/local/bin/load-watcher
WORKDIR /go/src/github.com/paypal/load-watcher
COPY . .
RUN make build

CMD ["/usr/local/bin/load-watcher"]
FROM alpine:3.12

COPY --from=0 /go/src/github.com/paypal/load-watcher/bin/load-watcher /bin/load-watcher

CMD ["/bin/load-watcher"]
27 changes: 27 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2020 PayPal
#
# 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.

COMMONENVVAR=GOOS=$(shell uname -s | tr A-Z a-z) GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m)))
BUILDENVVAR=CGO_ENABLED=0

.PHONY: all
all: build

.PHONY: build
build:
$(COMMONENVVAR) $(BUILDENVVAR) go build -o bin/load-watcher main.go

.PHONY: clean
clean:
rm -rf ./bin
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,25 @@ GET /watcher

This will return metrics for all nodes. A query parameter to filter by host can be added with `host`.

## Client Configuration
- To use the Kubernetes metric server client out of a cluster, please configure your `KUBE_CONFIG` environment varirables to your
kubernetes client configuration file path.

- To use the prometheus client out of a cluster, please configure `PROM_HOST` and `PROM_TOKEN` environment variables to
your Prometheus endpoint and token. Please ignore `PROM_TOKEN` as empty string if no authentication is needed to access
the Prometheus APIs. When using the prometheus in a cluster, the default endpoint is `prometheus-k8s:9090`. You need to
configure `PROM_HOST` if your Prometheus endpoint is different.
## Third Party Client Configuration
- To use the Kubernetes metric server client, please configure environment varirables `METRIC_CLIENT=k8s` and your `KUBE_CONFIG` to your
kubernetes client configuration file path if running out of cluster.

- To use the prometheus client, please configure environment variables as `METRIC_CLIENT=prometheus`, and configure
`METRIC_HOST` and `METRIC_AUTH_TOKEN` to your Prometheus endpoint and token. Please ignore `PROM_TOKEN` as empty string if no authentication
is needed to access the Prometheus APIs. When using the prometheus in a cluster, the default endpoint is
`http://prometheus-k8s.monitoring.svc.cluster.local:9090`. You need to configure `PROM_HOST` if your Prometheus endpoint is different.

- To use the signalFx client, please configure environment variables as `METRIC_CLIENT=signalfx`, and configure
`METRIC_HOST` and `METRIC_AUTH_TOKEN` to your signalFx endpoint and token. Please ignore `METRIC_AUTH_TOKEN` as empty string if no authentication
is needed to access the signalFx APIs.

## Deploy `load-watcher` as a service
To deploy `load-watcher` as a monitoring service in your Kubernetes cluster, you can run the following.
```bash
> kubectl create -f manifests/load-watcher-deployment.yaml
```

## Using `load-watcher` client
- `load-watcher-client.go` shows an example to use `load-watcher` packages as libraries in a client mode. When `load-watcher` is running as a
service exposing an endpoint in a cluster, a client, such as Trimaran plugins, can use its libraries to create a client getting the latest metrics.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ require (
github.com/stretchr/testify v1.5.1
k8s.io/apimachinery v0.19.0
k8s.io/client-go v0.19.0
k8s.io/klog/v2 v2.2.0
k8s.io/metrics v0.19.0
)
38 changes: 38 additions & 0 deletions load-watcher-client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"log"

"github.com/paypal/load-watcher/pkg/utils"
"github.com/paypal/load-watcher/pkg/watcher"
)

func main() {
metricProviderType := "load-watcher"
metricEndpoint := "http://localhost:2020"

client, err := utils.CreateClient(metricProviderType, metricEndpoint, "")
if err != nil {
log.Fatalf("unable to create new client: %v", err)
}

curWindow := watcher.CurrentFifteenMinuteWindow()
watchermetrics, err := client.FetchAllHostsMetrics(curWindow)

if err != nil {
log.Fatalf("unable to fetch data from client: %v", err)
}

log.Printf("Timestamp: %v", watchermetrics.Timestamp)
log.Printf("Source: %v", watchermetrics.Source)
log.Printf("Window, start: %v", watchermetrics.Window.Start)
log.Printf("Window, end: %v", watchermetrics.Window.End)
log.Printf("Window, duration: %v", watchermetrics.Window.Duration)

for k, v := range watchermetrics.Data.NodeMetricsMap {
log.Printf("Host: %v", k)
for _, metric := range v.Metrics {
log.Printf("%+v\n", metric)
}
}
}
7 changes: 4 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ package main
import (
"log"

"github.com/paypal/load-watcher/pkg/metricsprovider"
"github.com/paypal/load-watcher/pkg/utils"
"github.com/paypal/load-watcher/pkg/watcher"
)


func main() {
// client, err := metricsprovider.NewMetricsServerClient()
client, err := metricsprovider.NewPromClient()
utils.InitEnvVars()
client, err := utils.CreateClient(utils.MetricProviderType, utils.MetricEndpoint, utils.MetricAuthToken)
if err != nil {
log.Fatalf("unable to create new client: %v", err)
}
Expand Down
42 changes: 42 additions & 0 deletions manifests/load-watcher-deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: load-watcher
namespace: monitoring
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: load-watcher
replicas: 1 # tells deployment to run 1 pods matching the template
template: # create pods using pod definition in this template
metadata:
labels:
app: load-watcher
spec:
containers:
- name: watcher
image: chenw/load-watcher:latest
ports:
- containerPort: 2020
---
apiVersion: v1
kind: Service
metadata:
name: load-watcher
namespace: monitoring
labels:
app: load-watcher
spec:
externalTrafficPolicy: Local
ports:
- name: http
port: 2020
protocol: TCP
targetPort: 2020
selector:
app: load-watcher
type: LoadBalancer

25 changes: 15 additions & 10 deletions pkg/metricsprovider/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var (
)

const (
k8sClientName = "k8s"
K8sClientName = "k8s"
// env variable that provides path to kube config file, if deploying from outside K8s cluster
kubeConfig = "KUBE_CONFIG"
)
Expand Down Expand Up @@ -80,37 +80,39 @@ func NewMetricsServerClient() (watcher.FetcherClient, error) {
}

func (m metricsServerClient) Name() string {
return k8sClientName
return K8sClientName
}

func (m metricsServerClient) FetchHostMetrics(host string, window *watcher.Window) ([]watcher.Metric, error) {
func (m metricsServerClient) FetchHostMetrics(host string, window *watcher.Window) (watcher.WatcherMetrics, error) {
var metrics = []watcher.Metric{}

nodeMetrics, err := m.metricsClientSet.MetricsV1beta1().NodeMetricses().Get(context.TODO(), host, metav1.GetOptions{})
if err != nil {
return metrics, err
return watcher.WatcherMetrics{}, err
}
var fetchedMetric watcher.Metric
node, err := m.coreClientSet.CoreV1().Nodes().Get(context.Background(), host, metav1.GetOptions{})
if err != nil {
return metrics, err
return watcher.WatcherMetrics{}, err
}
fetchedMetric.Value = float64(100*nodeMetrics.Usage.Cpu().MilliValue()) / float64(node.Status.Capacity.Cpu().MilliValue())
fetchedMetric.Type = watcher.CPU
fetchedMetric.Operator = watcher.Latest
metrics = append(metrics, fetchedMetric)
return metrics, nil
watchermetrics := watcher.MetricList2NodeMetricMap(host, metrics, m.Name(), *window)
return watchermetrics, nil
}

func (m metricsServerClient) FetchAllHostsMetrics(window *watcher.Window) (map[string][]watcher.Metric, error) {
func (m metricsServerClient) FetchAllHostsMetrics(window *watcher.Window) (watcher.WatcherMetrics, error) {
metrics := make(map[string][]watcher.Metric)

nodeMetricsList, err := m.metricsClientSet.MetricsV1beta1().NodeMetricses().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return metrics, err
return watcher.WatcherMetrics{}, err
}
nodeList, err := m.coreClientSet.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return metrics, err
return watcher.WatcherMetrics{}, err
}
nodeCapacityMap := make(map[string]int64)
for _, host := range nodeList.Items {
Expand All @@ -119,12 +121,15 @@ func (m metricsServerClient) FetchAllHostsMetrics(window *watcher.Window) (map[s
for _, host := range nodeMetricsList.Items {
var fetchedMetric watcher.Metric
fetchedMetric.Type = watcher.CPU
fetchedMetric.Operator = watcher.Latest
if _, ok := nodeCapacityMap[host.Name]; !ok {
log.Errorf("unable to find host %v in node list", host.Name)
continue
}
fetchedMetric.Value = float64(host.Usage.Cpu().MilliValue()) / float64(nodeCapacityMap[host.Name])
metrics[host.Name] = append(metrics[host.Name], fetchedMetric)
}
return metrics, nil

watchermetrics := watcher.MetricListMap2NodeMetricMap(metrics, m.Name(), *window)
return watchermetrics, nil
}
110 changes: 110 additions & 0 deletions pkg/metricsprovider/loadwatcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright 2020
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 metricsprovider

import (
"crypto/tls"
"github.com/francoispqt/gojay"
"github.com/paypal/load-watcher/pkg/watcher"
"k8s.io/klog/v2"
"net/http"
)

const (
LoadWatcherClientName = "load-watcher"
DefaultLoadWatcherClientEndpoint = "https://load-watcher.monitoring.svc.cluster.local:2020"
watcherQuery = "/watcher"
defaultRetries = 3
)

var (
loadWatcherEndpoint string
)

type loadWatcherClient struct {
client http.Client
}

func NewLoadWatcherClient(loadwatcherurl string) (watcher.FetcherClient, error) {
tlsConfig := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}

loadWatcherEndpoint = loadwatcherurl

return loadWatcherClient{client: http.Client{
Timeout: httpClientTimeout,
Transport: tlsConfig}}, nil
}

func (s loadWatcherClient) Name() string {
return LoadWatcherClientName
}

func (s loadWatcherClient) FetchAllHostsMetrics(window *watcher.Window) (watcher.WatcherMetrics, error) {
allhostQueryURL := loadWatcherEndpoint+watcherQuery
return s.queryMetrics(allhostQueryURL)
}

func (s loadWatcherClient) FetchHostMetrics(host string, window *watcher.Window) (watcher.WatcherMetrics, error) {
hostQueryURL := loadWatcherEndpoint+watcherQuery + "?host=" + host
return s.queryMetrics(hostQueryURL)
}

func (s loadWatcherClient) queryMetrics(queryURL string) (watcher.WatcherMetrics, error) {
metrics := watcher.WatcherMetrics{}
req, err := http.NewRequest(http.MethodGet, queryURL, nil)
if err != nil {
return metrics, err
}
req.Header.Set("Content-Type", "application/json")

var retries int = defaultRetries
var resp *http.Response
for retries > 0 {
resp, err = s.client.Do(req)

if err != nil {
retries -= 1
} else {
break
}
}
if err != nil {
return metrics, err
}

defer resp.Body.Close()
klog.V(6).Infof("received status code %v from watcher", resp.StatusCode)
if resp.StatusCode == http.StatusOK {
data := watcher.Data{NodeMetricsMap: make(map[string]watcher.NodeMetrics)}
metrics = watcher.WatcherMetrics{Data: data}
dec := gojay.BorrowDecoder(resp.Body)
defer dec.Release()
err = dec.Decode(&metrics)
if err != nil {
klog.Errorf("unable to decode watcher metrics: %v", err)
return metrics, err
}

return metrics, nil
} else {
klog.Errorf("received status code %v from watcher", resp.StatusCode)
}

return metrics, nil
}
Loading

0 comments on commit 498c7a8

Please sign in to comment.