diff --git a/README.md b/README.md index e6c1cd14..9056b1d7 100644 --- a/README.md +++ b/README.md @@ -535,9 +535,9 @@ spec: tag-application: my-custom-app aggregators: avg # comma separated list of aggregation functions, default: last duration: 5m # default: 10m - target: - averageValue: "30" - type: AverageValue + target: + averageValue: "30" + type: AverageValue ``` The `check-id` specifies the ZMON check to query for the metrics. `key` @@ -585,3 +585,60 @@ you need to define a `key` or other `tag` with a "star" query syntax like `values.*`. This *hack* is in place because it's not allowed to use `*` in the metric label definitions. If both annotations and corresponding label is defined, then the annotation takes precedence. + +## HTTP Collector + +The http collector allows collecting metrics from an external endpoint specified in the HPA. +Currently only `json-path` collection is supported. + +### Supported metrics + +| Metric | Description | Type | K8s Versions | +| ------------ | -------------- | ------- | -- | +| *custom* | No predefined metrics. Metrics are generated from user defined queries. | Pods | `>=1.12` | + +### Example + +This is an example of using the HTTP collector to collect metrics from a json +metrics endpoint specified in the annotations. + +```yaml +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: myapp-hpa + annotations: + # metric-config.../ + metric-config.external.http.json/json-key: "$.some-metric.value" + metric-config.external.http.json/endpoint: "http://metric-source.app-namespace:8080/metrics" + metric-config.external.http.json/aggregator: "max" +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: myapp + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: External + external: + metric: + name: http + selector: + matchLabels: + identifier: unique-metric-name + target: + averageValue: 1 + type: AverageValue +``` + +The HTTP collector similar to the Pod Metrics collector. The metric name should always be `http`. +This value is also used in the annotations to configure the metrics adapter to query the required +target. The following configuration values are supported: + +- `json-key` to specify the JSON path of the metric to be queried +- `endpoint` the fully formed path to query for the metric. In the above example a Kubernetes _Service_ + in the namespace `app-namespace` is called. +- `aggregator` is only required if the metric is an array of values and specifies how the values + are aggregated. Currently this option can support the values: `sum`, `max`, `min`, `avg`. + diff --git a/go.mod b/go.mod index d0c8c2a4..f10dae80 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,21 @@ require ( github.com/googleapis/gnostic v0.2.0 // indirect github.com/influxdata/influxdb-client-go v0.1.5 github.com/kubernetes-incubator/custom-metrics-apiserver v0.0.0-20200323093244-5046ce1afe6b + github.com/lib/pq v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 + github.com/onsi/ginkgo v1.11.0 // indirect + github.com/onsi/gomega v1.8.1 // indirect github.com/prometheus/client_golang v1.5.1 - github.com/prometheus/common v0.4.1 + github.com/prometheus/common v0.9.1 github.com/sirupsen/logrus v1.5.0 github.com/spf13/cobra v0.0.7 github.com/stretchr/testify v1.5.1 github.com/zalando-incubator/cluster-lifecycle-manager v0.0.0-20180921141935-824b77fb1f84 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 // indirect golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/tools v0.0.0-20200204192400-7124308813f3 // indirect + honnef.co/go/tools v0.0.1-2020.1.3 // indirect k8s.io/api v0.17.3 k8s.io/apimachinery v0.17.4 k8s.io/client-go v0.17.3 diff --git a/go.sum b/go.sum index 5c035066..f93a18f9 100644 --- a/go.sum +++ b/go.sum @@ -28,11 +28,11 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= @@ -43,15 +43,13 @@ github.com/apex/log v1.1.0 h1:J5rld6WVFi6NxA6m8GJ1LJqu3+GiTFIt3mYv27gdQWI= github.com/apex/log v1.1.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.15.64/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.29.33 h1:WP85+WHalTFQR2wYp5xR2sjiVAZXew2bBQXGU1QJBXI= -github.com/aws/aws-sdk-go v1.29.33/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= github.com/aws/aws-sdk-go v1.30.0 h1:7NDwnnQrI1Ivk0bXLzMmuX5ozzOwteHOsAs4druW7gI= github.com/aws/aws-sdk-go v1.30.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.30.2 h1:0vuroAsbPwVbP91MMaUmFLnrQcFBhmjQnnXaH1kcnPw= github.com/aws/aws-sdk-go v1.30.2/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2 h1:oMCHnXa6CCCafdPDbMh/lWRhRByN0VFLvv+g+ayx1SI= @@ -65,6 +63,7 @@ github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e h1:V9a67dfYqPLAvzk5h github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= @@ -160,9 +159,9 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v1.10.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -222,8 +221,6 @@ github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mq github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= @@ -231,8 +228,8 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -251,18 +248,21 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kubernetes-incubator/custom-metrics-apiserver v0.0.0-20200323093244-5046ce1afe6b h1:+hyh/xJbvel82RP6HBASIudJ1O8/bH8RA2SaRJ8v7+E= github.com/kubernetes-incubator/custom-metrics-apiserver v0.0.0-20200323093244-5046ce1afe6b/go.mod h1:ipPARShJU/8FZINT0WNtWoAD6BZkc7ZkU/K40Gg/mRk= -github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= @@ -293,11 +293,13 @@ github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjG github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= @@ -316,23 +318,22 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= @@ -359,8 +360,6 @@ github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -414,6 +413,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -429,6 +429,8 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -457,8 +459,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -471,12 +473,13 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -502,9 +505,13 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200204192400-7124308813f3 h1:Ms82wn6YK4ZycO6Bxyh0kxX3gFFVGo79CCuc52xgcys= +golang.org/x/tools v0.0.0-20200204192400-7124308813f3/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw= @@ -530,8 +537,8 @@ google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -561,8 +568,9 @@ honnef.co/go/tools v0.0.0-20181108184350-ae8f1f9103cc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.2 h1:TEgegKbBqByGUb1Coo1pc2qIdf2xw6v0mYyLSYtyopE= honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0= k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= diff --git a/pkg/annotations/parser_test.go b/pkg/annotations/parser_test.go index 554b2063..612ff509 100644 --- a/pkg/annotations/parser_test.go +++ b/pkg/annotations/parser_test.go @@ -86,6 +86,21 @@ func TestParser(t *testing.T) { "org-id": "deadbeef", }, }, + { + Name: "http metrics", + Annotations: map[string]string{ + "metric-config.external.http.json/json-key": "$.metric.value", + "metric-config.external.http.json/endpoint": "http://metric-source.source-namespace.svc.cluster.local:8000/metrics", + "metric-config.external.http.json/aggregator": "avg", + }, + MetricName: "http", + MetricType: autoscalingv2.ExternalMetricSourceType, + ExpectedConfig: map[string]string{ + "json-key": "$.metric.value", + "endpoint": "http://metric-source.source-namespace.svc.cluster.local:8000/metrics", + "aggregator": "avg", + }, + }, } { t.Run(tc.Name, func(t *testing.T) { hpaMap := make(AnnotationConfigMap) diff --git a/pkg/collector/http_collector.go b/pkg/collector/http_collector.go new file mode 100644 index 00000000..27495c4a --- /dev/null +++ b/pkg/collector/http_collector.go @@ -0,0 +1,103 @@ +package collector + +import ( + "fmt" + "net/url" + "time" + + "github.com/zalando-incubator/kube-metrics-adapter/pkg/collector/httpmetrics" + + "github.com/oliveagle/jsonpath" + "k8s.io/api/autoscaling/v2beta2" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/metrics/pkg/apis/external_metrics" +) + +const ( + HTTPMetricName = "http" + HTTPEndpointAnnotationKey = "endpoint" + HTTPJsonPathAnnotationKey = "json-key" + identifierLabel = "identifier" +) + +type HTTPCollectorPlugin struct{} + +func NewHTTPCollectorPlugin() (*HTTPCollectorPlugin, error) { + return &HTTPCollectorPlugin{}, nil +} + +func (p *HTTPCollectorPlugin) NewCollector(_ *v2beta2.HorizontalPodAutoscaler, config *MetricConfig, interval time.Duration) (Collector, error) { + collector := &HTTPCollector{} + var ( + value string + ok bool + ) + if value, ok = config.Config[HTTPJsonPathAnnotationKey]; !ok { + return nil, fmt.Errorf("config value %s not found", HTTPJsonPathAnnotationKey) + } + jsonPath, err := jsonpath.Compile(value) + if err != nil { + return nil, fmt.Errorf("failed to parse json path: %v", err) + } + collector.jsonPath = jsonPath + if value, ok = config.Config[HTTPEndpointAnnotationKey]; !ok { + return nil, fmt.Errorf("config value %s not found", HTTPEndpointAnnotationKey) + } + collector.endpoint, err = url.Parse(value) + if err != nil { + return nil, err + } + collector.interval = interval + collector.metricType = config.Type + if config.Metric.Selector == nil || config.Metric.Selector.MatchLabels == nil { + return nil, fmt.Errorf("no label selector specified for metric: %s", config.Metric.Name) + } + if _, ok := config.Metric.Selector.MatchLabels[identifierLabel]; !ok { + return nil, fmt.Errorf("%s is not specified as a label for metric %s", identifierLabel, config.Metric.Name) + } + collector.metric = config.Metric + var aggFunc httpmetrics.AggregatorFunc + + if val, ok := config.Config["aggregator"]; ok { + aggFunc, err = httpmetrics.ParseAggregator(val) + if err != nil { + return nil, err + } + } + collector.metricsGetter = httpmetrics.NewJSONPathMetricsGetter(httpmetrics.DefaultMetricsHTTPClient(), aggFunc, jsonPath) + return collector, nil +} + +type HTTPCollector struct { + endpoint *url.URL + jsonPath *jsonpath.Compiled + interval time.Duration + metricType v2beta2.MetricSourceType + metricsGetter *httpmetrics.JSONPathMetricsGetter + metric v2beta2.MetricIdentifier +} + +func (c *HTTPCollector) GetMetrics() ([]CollectedMetric, error) { + metric, err := c.metricsGetter.GetMetric(*c.endpoint) + if err != nil { + return nil, err + } + + value := CollectedMetric{ + Type: c.metricType, + External: external_metrics.ExternalMetricValue{ + MetricName: c.metric.Name, + MetricLabels: c.metric.Selector.MatchLabels, + Timestamp: metav1.Time{ + Time: time.Now(), + }, + Value: *resource.NewMilliQuantity(int64(metric*1000), resource.DecimalSI), + }, + } + return []CollectedMetric{value}, nil +} + +func (c *HTTPCollector) Interval() time.Duration { + return c.interval +} diff --git a/pkg/collector/http_collector_test.go b/pkg/collector/http_collector_test.go new file mode 100644 index 00000000..5e6e0420 --- /dev/null +++ b/pkg/collector/http_collector_test.go @@ -0,0 +1,94 @@ +package collector + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/api/autoscaling/v2beta2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type testExternalMetricsHandler struct { + values []int64 + test *testing.T +} + +func (t testExternalMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + response, err := json.Marshal(testMetricResponse{t.values}) + require.NoError(t.test, err) + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(response) + require.NoError(t.test, err) +} + +func makeHTTPTestServer(t *testing.T, values []int64) string { + server := httptest.NewServer(&testExternalMetricsHandler{values: values, test: t}) + return server.URL +} + +func TestHTTPCollector(t *testing.T) { + for _, tc := range []struct { + name string + values []int64 + output int + aggregator string + }{ + { + name: "basic", + values: []int64{3}, + output: 3, + aggregator: "sum", + }, + { + name: "sum", + values: []int64{3, 5, 6}, + aggregator: "sum", + output: 14, + }, + { + name: "average", + values: []int64{3, 5, 6}, + aggregator: "sum", + output: 14, + }, + } { + t.Run(tc.name, func(t *testing.T) { + testServer := makeHTTPTestServer(t, tc.values) + plugin, err := NewHTTPCollectorPlugin() + require.NoError(t, err) + testConfig := makeTestHTTPCollectorConfig(testServer, tc.aggregator) + collector, err := plugin.NewCollector(nil, testConfig, testInterval) + require.NoError(t, err) + metrics, err := collector.GetMetrics() + require.NoError(t, err) + require.NotNil(t, metrics) + require.Len(t, metrics, 1) + require.EqualValues(t, metrics[0].External.Value.Value(), tc.output) + }) + } +} + +func makeTestHTTPCollectorConfig(endpoint, aggregator string) *MetricConfig { + config := &MetricConfig{ + MetricTypeName: MetricTypeName{ + Type: v2beta2.ExternalMetricSourceType, + Metric: v2beta2.MetricIdentifier{ + Name: "test-metric", + Selector: &v1.LabelSelector{ + MatchLabels: map[string]string{identifierLabel: "test-metric"}, + }, + }, + }, + Config: map[string]string{ + HTTPJsonPathAnnotationKey: "$.values", + HTTPEndpointAnnotationKey: endpoint, + }, + } + if aggregator != "" { + config.Config["aggregator"] = aggregator + } + return config +} diff --git a/pkg/collector/httpmetrics/aggregator.go b/pkg/collector/httpmetrics/aggregator.go new file mode 100644 index 00000000..c7c5e46d --- /dev/null +++ b/pkg/collector/httpmetrics/aggregator.go @@ -0,0 +1,65 @@ +package httpmetrics + +import ( + "fmt" + "math" +) + +type AggregatorFunc func(...float64) float64 + +// Average implements the average mathematical function over a slice of float64 +func Average(values ...float64) float64 { + sum := Sum(values...) + return sum / float64(len(values)) +} + +// Minimum implements the absolute minimum mathematical function over a slice of float64 +func Minimum(values ...float64) float64 { + // initialized with positive infinity, all finite numbers are smaller than it + curMin := math.Inf(1) + for _, v := range values { + if v < curMin { + curMin = v + } + } + return curMin +} + +// Maximum implements the absolute maximum mathematical function over a slice of float64 +func Maximum(values ...float64) float64 { + // initialized with negative infinity, all finite numbers are bigger than it + curMax := math.Inf(-1) + for _, v := range values { + if v > curMax { + curMax = v + } + } + return curMax +} + +// Sum implements the summation mathematical function over a slice of float64 +func Sum(values ...float64) float64 { + res := 0.0 + + for _, v := range values { + res += v + } + + return res +} + +// reduce will reduce a slice of numbers given a aggregator function's name. If it's empty or not recognized, an error is returned. +func ParseAggregator(aggregator string) (AggregatorFunc, error) { + switch aggregator { + case "avg": + return Average, nil + case "min": + return Minimum, nil + case "max": + return Maximum, nil + case "sum": + return Sum, nil + default: + return nil, fmt.Errorf("aggregator function: %s is unknown", aggregator) + } +} diff --git a/pkg/collector/httpmetrics/aggregator_test.go b/pkg/collector/httpmetrics/aggregator_test.go new file mode 100644 index 00000000..60dbf48f --- /dev/null +++ b/pkg/collector/httpmetrics/aggregator_test.go @@ -0,0 +1,57 @@ +package httpmetrics + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReduce(t *testing.T) { + for _, tc := range []struct { + input []float64 + output float64 + aggregator string + parseError bool + }{ + { + input: []float64{1, 2, 3}, + output: 2.0, + aggregator: "avg", + parseError: false, + }, + { + input: []float64{1, 2, 3}, + output: 1.0, + aggregator: "min", + parseError: false, + }, + { + input: []float64{1, 2, 3}, + output: 3.0, + aggregator: "max", + parseError: false, + }, + { + input: []float64{1, 2, 3}, + output: 6.0, + aggregator: "sum", + parseError: false, + }, + { + input: []float64{1, 2, 3}, + aggregator: "non-existent", + parseError: true, + }, + } { + t.Run(fmt.Sprintf("Test function: %s", tc.aggregator), func(t *testing.T) { + aggFunc, err := ParseAggregator(tc.aggregator) + if tc.parseError { + require.Error(t, err) + } else { + val := aggFunc(tc.input...) + require.Equal(t, tc.output, val) + } + }) + } +} diff --git a/pkg/collector/httpmetrics/json_path.go b/pkg/collector/httpmetrics/json_path.go new file mode 100644 index 00000000..2c576fd4 --- /dev/null +++ b/pkg/collector/httpmetrics/json_path.go @@ -0,0 +1,129 @@ +package httpmetrics + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "time" + + "github.com/oliveagle/jsonpath" +) + +// JSONPathMetricsGetter is a metrics getter which looks up pod metrics by +// querying the pods metrics endpoint and lookup the metric value as defined by +// the json path query. +type JSONPathMetricsGetter struct { + jsonPath *jsonpath.Compiled + aggregator AggregatorFunc + client *http.Client +} + +// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter. +func NewJSONPathMetricsGetter(httpClient *http.Client, aggregatorFunc AggregatorFunc, compiledPath *jsonpath.Compiled) *JSONPathMetricsGetter { + return &JSONPathMetricsGetter{client: httpClient, aggregator: aggregatorFunc, jsonPath: compiledPath} +} + +func DefaultMetricsHTTPClient() *http.Client { + client := &http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 15 * time.Second, + }).DialContext, + MaxIdleConns: 50, + IdleConnTimeout: 90 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + Timeout: 15 * time.Second, + } + return client +} + +// GetMetric gets metric from pod by fetching json metrics from the pods metric +// endpoint and extracting the desired value using the specified json path +// query. +func (g *JSONPathMetricsGetter) GetMetric(metricsURL url.URL) (float64, error) { + data, err := g.fetchMetrics(metricsURL) + if err != nil { + return 0, err + } + + // parse data + var jsonData interface{} + err = json.Unmarshal(data, &jsonData) + if err != nil { + return 0, err + } + + res, err := g.jsonPath.Lookup(jsonData) + if err != nil { + return 0, err + } + + switch res := res.(type) { + case int: + return float64(res), nil + case float32: + return float64(res), nil + case float64: + return res, nil + case []interface{}: + if g.aggregator == nil { + return 0, fmt.Errorf("no aggregator function has been specified") + } + s, err := castSlice(res) + if err != nil { + return 0, err + } + return g.aggregator(s...), nil + default: + return 0, fmt.Errorf("unsupported type %T", res) + } +} + +// castSlice takes a slice of interface and returns a slice of float64 if all +// values in slice were castable, else returns an error +func castSlice(in []interface{}) ([]float64, error) { + var out []float64 + + for _, v := range in { + switch v := v.(type) { + case int: + out = append(out, float64(v)) + case float32: + out = append(out, float64(v)) + case float64: + out = append(out, v) + default: + return nil, fmt.Errorf("slice was returned by JSONPath, but value inside is unsupported: %T", v) + } + } + + return out, nil +} + +func (g *JSONPathMetricsGetter) fetchMetrics(metricsURL url.URL) ([]byte, error) { + request, err := http.NewRequest(http.MethodGet, metricsURL.String(), nil) + if err != nil { + return nil, err + } + + resp, err := g.client.Do(request) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unsuccessful response: %s", resp.Status) + } + + return data, nil +} diff --git a/pkg/collector/httpmetrics/json_path_test.go b/pkg/collector/httpmetrics/json_path_test.go new file mode 100644 index 00000000..c6fccef2 --- /dev/null +++ b/pkg/collector/httpmetrics/json_path_test.go @@ -0,0 +1,89 @@ +package httpmetrics + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/oliveagle/jsonpath" + "github.com/stretchr/testify/require" +) + +func TestCastSlice(t *testing.T) { + res1, err1 := castSlice([]interface{}{1, 2, 3}) + require.NoError(t, err1) + require.Equal(t, []float64{1.0, 2.0, 3.0}, res1) + + res2, err2 := castSlice([]interface{}{float32(1.0), float32(2.0), float32(3.0)}) + require.NoError(t, err2) + require.Equal(t, []float64{1.0, 2.0, 3.0}, res2) + + res3, err3 := castSlice([]interface{}{float64(1.0), float64(2.0), float64(3.0)}) + require.NoError(t, err3) + require.Equal(t, []float64{1.0, 2.0, 3.0}, res3) + + res4, err4 := castSlice([]interface{}{1, 2, "some string"}) + require.Errorf(t, err4, "slice was returned by JSONPath, but value inside is unsupported: %T", "string") + require.Equal(t, []float64(nil), res4) +} + +type testValueResponse struct { + Value int64 `json:"value"` +} + +type testValueArrayResponse struct { + Value []int64 `json:"value"` +} + +func makeTestHTTPServer(t *testing.T, values ...int64) *httptest.Server { + h := func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, r.URL.Path, "/metrics") + w.Header().Set("Content-Type", "application/json") + var ( + response []byte + err error + ) + if len(values) == 1 { + response, err = json.Marshal(testValueResponse{Value: values[0]}) + require.NoError(t, err) + } else { + response, err = json.Marshal(testValueArrayResponse{Value: values}) + require.NoError(t, err) + } + _, err = w.Write(response) + require.NoError(t, err) + } + return httptest.NewServer(http.HandlerFunc(h)) +} + +func TestJSONPathMetricsGetter(t *testing.T) { + for _, tc := range []struct { + name string + input []int64 + output float64 + aggregator AggregatorFunc + }{ + { + name: "basic average", + input: []int64{3, 4, 5}, + output: 4, + aggregator: Average, + }, + } { + t.Run(tc.name, func(t *testing.T) { + server := makeTestHTTPServer(t, tc.input...) + defer server.Close() + path, err := jsonpath.Compile("$.value") + require.NoError(t, err) + getter := NewJSONPathMetricsGetter(DefaultMetricsHTTPClient(), tc.aggregator, path) + url, err := url.Parse(fmt.Sprintf("%s/metrics", server.URL)) + require.NoError(t, err) + metric, err := getter.GetMetric(*url) + require.NoError(t, err) + require.Equal(t, tc.output, metric) + }) + } +} diff --git a/pkg/collector/httpmetrics/pod_metrics.go b/pkg/collector/httpmetrics/pod_metrics.go new file mode 100644 index 00000000..ae08d854 --- /dev/null +++ b/pkg/collector/httpmetrics/pod_metrics.go @@ -0,0 +1,93 @@ +package httpmetrics + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/oliveagle/jsonpath" + v1 "k8s.io/api/core/v1" +) + +type PodMetricsGetter interface { + GetMetric(pod *v1.Pod) (float64, error) +} + +type PodMetricsJSONPathGetter struct { + scheme string + path string + rawQuery string + port int + metricGetter *JSONPathMetricsGetter +} + +func (g PodMetricsJSONPathGetter) GetMetric(pod *v1.Pod) (float64, error) { + if pod.Status.PodIP == "" { + return 0, fmt.Errorf("pod %s/%s does not have a pod IP", pod.Namespace, pod.Name) + } + metricsURL := g.buildMetricsURL(pod.Status.PodIP) + return g.metricGetter.GetMetric(metricsURL) +} + +func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathGetter, error) { + getter := PodMetricsJSONPathGetter{} + var ( + jsonPath *jsonpath.Compiled + aggregator AggregatorFunc + err error + ) + + if v, ok := config["json-key"]; ok { + path, err := jsonpath.Compile(v) + if err != nil { + return nil, fmt.Errorf("failed to parse json path definition: %v", err) + } + + jsonPath = path + } + + if v, ok := config["scheme"]; ok { + getter.scheme = v + } + + if v, ok := config["path"]; ok { + getter.path = v + } + + if v, ok := config["raw-query"]; ok { + getter.rawQuery = v + } + + if v, ok := config["port"]; ok { + n, err := strconv.Atoi(v) + if err != nil { + return nil, err + } + getter.port = n + } + + if v, ok := config["aggregator"]; ok { + aggregator, err = ParseAggregator(v) + if err != nil { + return nil, err + } + } + getter.metricGetter = NewJSONPathMetricsGetter(DefaultMetricsHTTPClient(), aggregator, jsonPath) + return &getter, nil +} + +// buildMetricsURL will build the full URL needed to hit the pod metric endpoint. +func (g *PodMetricsJSONPathGetter) buildMetricsURL(podIP string) url.URL { + var scheme = g.scheme + + if scheme == "" { + scheme = "http" + } + + return url.URL{ + Scheme: scheme, + Host: fmt.Sprintf("%s:%d", podIP, g.port), + Path: g.path, + RawQuery: g.rawQuery, + } +} diff --git a/pkg/collector/json_path_collector_test.go b/pkg/collector/httpmetrics/pod_metrics_test.go similarity index 50% rename from pkg/collector/json_path_collector_test.go rename to pkg/collector/httpmetrics/pod_metrics_test.go index ffc803ab..350365e4 100644 --- a/pkg/collector/json_path_collector_test.go +++ b/pkg/collector/httpmetrics/pod_metrics_test.go @@ -1,4 +1,4 @@ -package collector +package httpmetrics import ( "fmt" @@ -8,14 +8,14 @@ import ( "github.com/stretchr/testify/require" ) -func compareMetricsGetter(t *testing.T, first, second *JSONPathMetricsGetter) { - require.Equal(t, first.jsonPath, second.jsonPath) +func compareMetricsGetter(t *testing.T, first, second *PodMetricsJSONPathGetter) { + require.Equal(t, first.metricGetter.jsonPath, second.metricGetter.jsonPath) require.Equal(t, first.scheme, second.scheme) require.Equal(t, first.path, second.path) require.Equal(t, first.port, second.port) } -func TestNewJSONPathMetricsGetter(t *testing.T) { +func TestNewPodJSONPathMetricsGetter(t *testing.T) { configNoAggregator := map[string]string{ "json-key": "$.value", "scheme": "http", @@ -23,14 +23,14 @@ func TestNewJSONPathMetricsGetter(t *testing.T) { "port": "9090", } jpath1, _ := jsonpath.Compile(configNoAggregator["json-key"]) - getterNoAggregator, err1 := NewJSONPathMetricsGetter(configNoAggregator) + getterNoAggregator, err1 := NewPodMetricsJSONPathGetter(configNoAggregator) require.NoError(t, err1) - compareMetricsGetter(t, &JSONPathMetricsGetter{ - jsonPath: jpath1, - scheme: "http", - path: "/metrics", - port: 9090, + compareMetricsGetter(t, &PodMetricsJSONPathGetter{ + metricGetter: &JSONPathMetricsGetter{jsonPath: jpath1}, + scheme: "http", + path: "/metrics", + port: 9090, }, getterNoAggregator) configAggregator := map[string]string{ @@ -41,15 +41,14 @@ func TestNewJSONPathMetricsGetter(t *testing.T) { "aggregator": "avg", } jpath2, _ := jsonpath.Compile(configAggregator["json-key"]) - getterAggregator, err2 := NewJSONPathMetricsGetter(configAggregator) + getterAggregator, err2 := NewPodMetricsJSONPathGetter(configAggregator) require.NoError(t, err2) - compareMetricsGetter(t, &JSONPathMetricsGetter{ - jsonPath: jpath2, - scheme: "http", - path: "/metrics", - port: 9090, - aggregator: "avg", + compareMetricsGetter(t, &PodMetricsJSONPathGetter{ + metricGetter: &JSONPathMetricsGetter{jsonPath: jpath2, aggregator: Average}, + scheme: "http", + path: "/metrics", + port: 9090, }, getterAggregator) configErrorJSONPath := map[string]string{ @@ -59,7 +58,7 @@ func TestNewJSONPathMetricsGetter(t *testing.T) { "port": "9090", } - _, err3 := NewJSONPathMetricsGetter(configErrorJSONPath) + _, err3 := NewPodMetricsJSONPathGetter(configErrorJSONPath) require.Error(t, err3) configErrorPort := map[string]string{ @@ -69,7 +68,7 @@ func TestNewJSONPathMetricsGetter(t *testing.T) { "port": "a9090", } - _, err4 := NewJSONPathMetricsGetter(configErrorPort) + _, err4 := NewPodMetricsJSONPathGetter(configErrorPort) require.Error(t, err4) configWithRawQuery := map[string]string{ @@ -80,57 +79,18 @@ func TestNewJSONPathMetricsGetter(t *testing.T) { "raw-query": "foo=bar&baz=bop", } jpath5, _ := jsonpath.Compile(configWithRawQuery["json-key"]) - getterWithRawQuery, err5 := NewJSONPathMetricsGetter(configWithRawQuery) + getterWithRawQuery, err5 := NewPodMetricsJSONPathGetter(configWithRawQuery) require.NoError(t, err5) - compareMetricsGetter(t, &JSONPathMetricsGetter{ - jsonPath: jpath5, - scheme: "http", - path: "/metrics", - port: 9090, - rawQuery: "foo=bar&baz=bop", + compareMetricsGetter(t, &PodMetricsJSONPathGetter{ + metricGetter: &JSONPathMetricsGetter{jsonPath: jpath5}, + scheme: "http", + path: "/metrics", + port: 9090, + rawQuery: "foo=bar&baz=bop", }, getterWithRawQuery) } -func TestCastSlice(t *testing.T) { - res1, err1 := castSlice([]interface{}{1, 2, 3}) - require.NoError(t, err1) - require.Equal(t, []float64{1.0, 2.0, 3.0}, res1) - - res2, err2 := castSlice([]interface{}{float32(1.0), float32(2.0), float32(3.0)}) - require.NoError(t, err2) - require.Equal(t, []float64{1.0, 2.0, 3.0}, res2) - - res3, err3 := castSlice([]interface{}{float64(1.0), float64(2.0), float64(3.0)}) - require.NoError(t, err3) - require.Equal(t, []float64{1.0, 2.0, 3.0}, res3) - - res4, err4 := castSlice([]interface{}{1, 2, "some string"}) - require.Errorf(t, err4, "slice was returned by JSONPath, but value inside is unsupported: %T", "string") - require.Equal(t, []float64(nil), res4) -} - -func TestReduce(t *testing.T) { - average, err1 := reduce([]float64{1, 2, 3}, "avg") - require.NoError(t, err1) - require.Equal(t, 2.0, average) - - min, err2 := reduce([]float64{1, 2, 3}, "min") - require.NoError(t, err2) - require.Equal(t, 1.0, min) - - max, err3 := reduce([]float64{1, 2, 3}, "max") - require.NoError(t, err3) - require.Equal(t, 3.0, max) - - sum, err4 := reduce([]float64{1, 2, 3}, "sum") - require.NoError(t, err4) - require.Equal(t, 6.0, sum) - - _, err5 := reduce([]float64{1, 2, 3}, "inexistent_function") - require.Errorf(t, err5, "slice of numbers was returned by JSONPath, but no valid aggregator function was specified: %v", "inexistent_function") -} - func TestBuildMetricsURL(t *testing.T) { scheme := "http" ip := "1.2.3.4" @@ -148,7 +108,7 @@ func TestBuildMetricsURL(t *testing.T) { } _, err := jsonpath.Compile(configWithRawQuery["json-key"]) require.NoError(t, err) - getterWithRawQuery, err1 := NewJSONPathMetricsGetter(configWithRawQuery) + getterWithRawQuery, err1 := NewPodMetricsJSONPathGetter(configWithRawQuery) require.NoError(t, err1) expectedURLWithQuery := fmt.Sprintf("%s://%s:%s%s?%s", scheme, ip, port, path, rawQuery) @@ -164,7 +124,7 @@ func TestBuildMetricsURL(t *testing.T) { } _, err2 := jsonpath.Compile(configWithNoQuery["json-key"]) require.NoError(t, err2) - getterWithNoQuery, err3 := NewJSONPathMetricsGetter(configWithNoQuery) + getterWithNoQuery, err3 := NewPodMetricsJSONPathGetter(configWithNoQuery) require.NoError(t, err3) expectedURLNoQuery := fmt.Sprintf("%s://%s:%s%s", scheme, ip, port, path) diff --git a/pkg/collector/json_path_collector.go b/pkg/collector/json_path_collector.go deleted file mode 100644 index a74072a7..00000000 --- a/pkg/collector/json_path_collector.go +++ /dev/null @@ -1,253 +0,0 @@ -package collector - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "math" - "net" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/oliveagle/jsonpath" - corev1 "k8s.io/api/core/v1" -) - -// JSONPathMetricsGetter is a metrics getter which looks up pod metrics by -// querying the pods metrics endpoint and lookup the metric value as defined by -// the json path query. -type JSONPathMetricsGetter struct { - jsonPath *jsonpath.Compiled - scheme string - path string - port int - aggregator string - client *http.Client - rawQuery string -} - -// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter. -func NewJSONPathMetricsGetter(config map[string]string) (*JSONPathMetricsGetter, error) { - httpClient := defaultHTTPClient() - getter := &JSONPathMetricsGetter{client: httpClient} - - if v, ok := config["json-key"]; ok { - path, err := jsonpath.Compile(v) - if err != nil { - return nil, fmt.Errorf("failed to parse json path definition: %v", err) - } - - getter.jsonPath = path - } - - if v, ok := config["scheme"]; ok { - getter.scheme = v - } - - if v, ok := config["path"]; ok { - getter.path = v - } - - if v, ok := config["raw-query"]; ok { - getter.rawQuery = v - } - - if v, ok := config["port"]; ok { - n, err := strconv.Atoi(v) - if err != nil { - return nil, err - } - getter.port = n - } - - if v, ok := config["aggregator"]; ok { - getter.aggregator = v - } - - return getter, nil -} - -func defaultHTTPClient() *http.Client { - client := &http.Client{ - Transport: &http.Transport{ - DialContext: (&net.Dialer{ - Timeout: 15 * time.Second, - }).DialContext, - MaxIdleConns: 50, - IdleConnTimeout: 90 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - Timeout: 15 * time.Second, - } - return client -} - -// GetMetric gets metric from pod by fetching json metrics from the pods metric -// endpoint and extracting the desired value using the specified json path -// query. -func (g *JSONPathMetricsGetter) GetMetric(pod *corev1.Pod) (float64, error) { - data, err := g.getPodMetrics(pod) - if err != nil { - return 0, err - } - - // parse data - var jsonData interface{} - err = json.Unmarshal(data, &jsonData) - if err != nil { - return 0, err - } - - res, err := g.jsonPath.Lookup(jsonData) - if err != nil { - return 0, err - } - - switch res := res.(type) { - case int: - return float64(res), nil - case float32: - return float64(res), nil - case float64: - return res, nil - case []interface{}: - s, err := castSlice(res) - if err != nil { - return 0, err - } - return reduce(s, g.aggregator) - default: - return 0, fmt.Errorf("unsupported type %T", res) - } -} - -// castSlice takes a slice of interface and returns a slice of float64 if all -// values in slice were castable, else returns an error -func castSlice(in []interface{}) ([]float64, error) { - out := []float64{} - - for _, v := range in { - switch v := v.(type) { - case int: - out = append(out, float64(v)) - case float32: - out = append(out, float64(v)) - case float64: - out = append(out, v) - default: - return nil, fmt.Errorf("slice was returned by JSONPath, but value inside is unsupported: %T", v) - } - } - - return out, nil -} - -// getPodMetrics returns the content of the pods metrics endpoint. -func (g *JSONPathMetricsGetter) getPodMetrics(pod *corev1.Pod) ([]byte, error) { - if pod.Status.PodIP == "" { - return nil, fmt.Errorf("pod %s/%s does not have a pod IP", pod.Namespace, pod.Name) - } - - metricsURL := g.buildMetricsURL(pod.Status.PodIP) - - request, err := http.NewRequest(http.MethodGet, metricsURL.String(), nil) - if err != nil { - return nil, err - } - - resp, err := g.client.Do(request) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unsuccessful response: %s", resp.Status) - } - - return data, nil -} - -// buildMetricsURL will build the full URL needed to hit the pod metric endpoint. -func (g *JSONPathMetricsGetter) buildMetricsURL(podIP string) url.URL { - var scheme = g.scheme - - if scheme == "" { - scheme = "http" - } - - return url.URL{ - Scheme: scheme, - Host: fmt.Sprintf("%s:%d", podIP, g.port), - Path: g.path, - RawQuery: g.rawQuery, - } -} - -// reduce will reduce a slice of numbers given a aggregator function's name. If it's empty or not recognized, an error is returned. -func reduce(values []float64, aggregator string) (float64, error) { - switch aggregator { - case "avg": - return avg(values), nil - case "min": - return min(values), nil - case "max": - return max(values), nil - case "sum": - return sum(values), nil - default: - return 0, fmt.Errorf("slice of numbers was returned by JSONPath, but no valid aggregator function was specified: %v", aggregator) - } -} - -// avg implements the average mathematical function over a slice of float64 -func avg(values []float64) float64 { - sum := sum(values) - return sum / float64(len(values)) -} - -// min implements the absolute minimum mathematical function over a slice of float64 -func min(values []float64) float64 { - // initialized with positive infinity, all finite numbers are smaller than it - curMin := math.Inf(1) - - for _, v := range values { - if v < curMin { - curMin = v - } - } - - return curMin -} - -// max implements the absolute maximum mathematical function over a slice of float64 -func max(values []float64) float64 { - // initialized with negative infinity, all finite numbers are bigger than it - curMax := math.Inf(-1) - - for _, v := range values { - if v > curMax { - curMax = v - } - } - - return curMax -} - -// sum implements the summation mathematical function over a slice of float64 -func sum(values []float64) float64 { - res := 0.0 - - for _, v := range values { - res += v - } - - return res -} diff --git a/pkg/collector/pod_collector.go b/pkg/collector/pod_collector.go index 014e6970..e2459355 100644 --- a/pkg/collector/pod_collector.go +++ b/pkg/collector/pod_collector.go @@ -6,8 +6,8 @@ import ( "time" log "github.com/sirupsen/logrus" + "github.com/zalando-incubator/kube-metrics-adapter/pkg/collector/httpmetrics" autoscalingv2 "k8s.io/api/autoscaling/v2beta2" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -31,7 +31,7 @@ func (p *PodCollectorPlugin) NewCollector(hpa *autoscalingv2.HorizontalPodAutosc type PodCollector struct { client kubernetes.Interface - Getter PodMetricsGetter + Getter httpmetrics.PodMetricsGetter podLabelSelector *metav1.LabelSelector namespace string metric autoscalingv2.MetricIdentifier @@ -41,10 +41,6 @@ type PodCollector struct { httpClient *http.Client } -type PodMetricsGetter interface { - GetMetric(pod *corev1.Pod) (float64, error) -} - func NewPodCollector(client kubernetes.Interface, hpa *autoscalingv2.HorizontalPodAutoscaler, config *MetricConfig, interval time.Duration) (*PodCollector, error) { // get pod selector based on HPA scale target ref selector, err := getPodLabelSelector(client, hpa) @@ -62,11 +58,11 @@ func NewPodCollector(client kubernetes.Interface, hpa *autoscalingv2.HorizontalP logger: log.WithFields(log.Fields{"Collector": "Pod"}), } - var getter PodMetricsGetter + var getter httpmetrics.PodMetricsGetter switch config.CollectorName { case "json-path": var err error - getter, err = NewJSONPathMetricsGetter(config.Config) + getter, err = httpmetrics.NewPodMetricsJSONPathGetter(config.Config) if err != nil { return nil, err } diff --git a/pkg/collector/pod_collector_test.go b/pkg/collector/pod_collector_test.go new file mode 100644 index 00000000..45a7f930 --- /dev/null +++ b/pkg/collector/pod_collector_test.go @@ -0,0 +1,150 @@ +package collector + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2beta2" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +const ( + testNamespace = "test-namespace" + applicationLabelName = "application" + applicationLabelValue = "test-application" + testDeploymentName = "test-application" + testInterval = 10 * time.Second +) + +func TestPodCollector(t *testing.T) { + for _, tc := range []struct { + name string + metrics [][]int64 + result []int64 + }{ + { + name: "simple", + metrics: [][]int64{{1}, {3}, {8}, {5}, {2}}, + result: []int64{1, 3, 8, 5, 2}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + client := fake.NewSimpleClientset() + plugin := NewPodCollectorPlugin(client) + makeTestDeployment(t, client) + host, port, metricsHandler := makeTestHTTPServer(t, tc.metrics) + makeTestPods(t, host, port, "test-metric", client, 5) + testHPA := makeTestHPA(t, client) + testConfig := makeTestConfig(port) + collector, err := plugin.NewCollector(testHPA, testConfig, testInterval) + require.NoError(t, err) + metrics, err := collector.GetMetrics() + require.NoError(t, err) + require.Equal(t, len(metrics), int(metricsHandler.calledCounter)) + var values []int64 + for _, m := range metrics { + values = append(values, m.Custom.Value.Value()) + } + require.Equal(t, tc.result, values) + }) + } +} + +type testMetricResponse struct { + Values []int64 `json:"values"` +} +type testMetricsHandler struct { + values [][]int64 + calledCounter uint + t *testing.T + metricsPath string +} + +func (h *testMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + require.Equal(h.t, h.metricsPath, r.URL.Path) + require.Less(h.t, int(h.calledCounter), len(h.values)) + response, err := json.Marshal(testMetricResponse{Values: h.values[h.calledCounter]}) + require.NoError(h.t, err) + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(response) + require.NoError(h.t, err) + h.calledCounter++ +} + +func makeTestHTTPServer(t *testing.T, values [][]int64) (string, string, *testMetricsHandler) { + metricsHandler := &testMetricsHandler{values: values, t: t, metricsPath: "/metrics"} + server := httptest.NewServer(metricsHandler) + url, err := url.Parse(server.URL) + require.NoError(t, err) + return url.Hostname(), url.Port(), metricsHandler +} + +func makeTestConfig(port string) *MetricConfig { + return &MetricConfig{ + CollectorName: "json-path", + Config: map[string]string{"json-key": "$.values", "port": port, "path": "/metrics", "aggregator": "sum"}, + } +} + +func makeTestPods(t *testing.T, testServer string, metricName string, port string, client kubernetes.Interface, replicas int) { + for i := 0; i < replicas; i++ { + testPod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: fmt.Sprintf("test-pod-%d", i), + Labels: map[string]string{applicationLabelName: applicationLabelValue}, + Annotations: map[string]string{ + fmt.Sprintf("metric-config.pods.%s.json-path/port", metricName): port, + }, + }, + Status: corev1.PodStatus{ + PodIP: testServer, + }, + } + _, err := client.CoreV1().Pods(testNamespace).Create(testPod) + require.NoError(t, err) + } +} + +func makeTestDeployment(t *testing.T, client kubernetes.Interface) *appsv1.Deployment { + deployment := appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{Name: testDeploymentName}, + Spec: appsv1.DeploymentSpec{ + Selector: &v1.LabelSelector{ + MatchLabels: map[string]string{applicationLabelName: applicationLabelValue}, + }, + }, + } + _, err := client.AppsV1().Deployments(testNamespace).Create(&deployment) + require.NoError(t, err) + return &deployment + +} + +func makeTestHPA(t *testing.T, client kubernetes.Interface) *autoscalingv2.HorizontalPodAutoscaler { + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-hpa", + Namespace: testNamespace, + }, + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: testDeploymentName, + APIVersion: "apps/v1", + }, + }, + } + _, err := client.AutoscalingV2beta2().HorizontalPodAutoscalers("test-namespace").Create(hpa) + require.NoError(t, err) + return hpa +} diff --git a/pkg/server/start.go b/pkg/server/start.go index 7e432e1b..a3907f88 100644 --- a/pkg/server/start.go +++ b/pkg/server/start.go @@ -190,6 +190,8 @@ func (o AdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-chan struct collectorFactory.RegisterExternalCollector([]string{collector.InfluxDBMetricName}, influxdbPlugin) } + plugin, _ := collector.NewHTTPCollectorPlugin() + collectorFactory.RegisterExternalCollector([]string{collector.HTTPMetricName}, plugin) // register generic pod collector err = collectorFactory.RegisterPodsCollector("", collector.NewPodCollectorPlugin(client)) if err != nil {