Merge pull request #46 from DirectXMan12/feature/advanced-config

[WIP] Advanced Configuration
This commit is contained in:
Solly Ross 2018-06-27 17:04:27 -04:00 committed by GitHub
commit 7b606a79fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 2398 additions and 1234 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
*~ *~
vendor vendor
_output _output
deploy/adapter

View file

@ -1,17 +1,14 @@
language: go language: go
go: go:
- 1.8 - '1.10'
# blech, Travis downloads with capitals in DirectXMan12, which confuses go # blech, Travis downloads with capitals in DirectXMan12, which confuses go
go_import_path: github.com/directxman12/k8s-prometheus-adapter go_import_path: github.com/directxman12/k8s-prometheus-adapter
addons: before_install:
apt: - curl -L -s https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64 -o $GOPATH/bin/dep
sources: - chmod +x $GOPATH/bin/dep
- sourceline: 'ppa:masterminds/glide'
packages:
- glide
install: install:
- make -B vendor - make -B vendor
@ -20,7 +17,7 @@ script: make verify
cache: cache:
directories: directories:
- ~/.glide - ~/gopath/pkg/dep/
sudo: required sudo: required
services: services:

807
Gopkg.lock generated Normal file
View file

@ -0,0 +1,807 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "default"
name = "bitbucket.org/ww/goautoneg"
packages = ["."]
revision = "75cd24fc2f2c2a2088577d12123ddee5f54e0675"
[[projects]]
name = "github.com/NYTimes/gziphandler"
packages = ["."]
revision = "2600fb119af974220d3916a5916d6e31176aac1b"
version = "v1.0.1"
[[projects]]
name = "github.com/PuerkitoBio/purell"
packages = ["."]
revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/PuerkitoBio/urlesc"
packages = ["."]
revision = "de5bf2ad457846296e2031421a34e2568e304e35"
[[projects]]
branch = "master"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
name = "github.com/coreos/etcd"
packages = [
"auth/authpb",
"client",
"clientv3",
"etcdserver/api/v3rpc/rpctypes",
"etcdserver/etcdserverpb",
"mvcc/mvccpb",
"pkg/pathutil",
"pkg/srv",
"pkg/tlsutil",
"pkg/transport",
"pkg/types",
"version"
]
revision = "33245c6b5b49130ca99280408fadfab01aac0e48"
version = "v3.3.8"
[[projects]]
name = "github.com/coreos/go-semver"
packages = ["semver"]
revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6"
version = "v0.2.0"
[[projects]]
name = "github.com/coreos/go-systemd"
packages = ["daemon"]
revision = "39ca1b05acc7ad1220e09f133283b8859a8b71ab"
version = "v17"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/elazarl/go-bindata-assetfs"
packages = ["."]
revision = "30f82fa23fd844bd5bb1e5f216db87fd77b5eb43"
version = "v1.0.0"
[[projects]]
name = "github.com/emicklei/go-restful"
packages = [
".",
"log"
]
revision = "3658237ded108b4134956c1b3050349d93e7b895"
version = "v2.7.1"
[[projects]]
name = "github.com/emicklei/go-restful-swagger12"
packages = ["."]
revision = "dcef7f55730566d41eae5db10e7d6981829720f6"
version = "1.0.1"
[[projects]]
name = "github.com/evanphx/json-patch"
packages = ["."]
revision = "afac545df32f2287a079e2dfb7ba2745a643747e"
version = "v3.0.0"
[[projects]]
name = "github.com/ghodss/yaml"
packages = ["."]
revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/go-openapi/jsonpointer"
packages = ["."]
revision = "3a0015ad55fa9873f41605d3e8f28cd279c32ab2"
[[projects]]
branch = "master"
name = "github.com/go-openapi/jsonreference"
packages = ["."]
revision = "3fb327e6747da3043567ee86abd02bb6376b6be2"
[[projects]]
branch = "master"
name = "github.com/go-openapi/spec"
packages = ["."]
revision = "bcff419492eeeb01f76e77d2ebc714dc97b607f5"
[[projects]]
branch = "master"
name = "github.com/go-openapi/swag"
packages = ["."]
revision = "811b1089cde9dad18d4d0c2d09fbdbf28dbd27a5"
[[projects]]
name = "github.com/gogo/protobuf"
packages = [
"gogoproto",
"proto",
"protoc-gen-gogo/descriptor",
"sortkeys"
]
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/golang/glog"
packages = ["."]
revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998"
[[projects]]
name = "github.com/golang/protobuf"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp"
]
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/google/btree"
packages = ["."]
revision = "e89373fe6b4a7413d7acd6da1725b83ef713e6e4"
[[projects]]
branch = "master"
name = "github.com/google/gofuzz"
packages = ["."]
revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1"
[[projects]]
name = "github.com/googleapis/gnostic"
packages = [
"OpenAPIv2",
"compiler",
"extensions"
]
revision = "7c663266750e7d82587642f65e60bc4083f1f84e"
version = "v0.2.0"
[[projects]]
branch = "master"
name = "github.com/gregjones/httpcache"
packages = [
".",
"diskcache"
]
revision = "9cad4c3443a7200dd6400aef47183728de563a38"
[[projects]]
branch = "master"
name = "github.com/hashicorp/golang-lru"
packages = [
".",
"simplelru"
]
revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3"
[[projects]]
name = "github.com/imdario/mergo"
packages = ["."]
revision = "9316a62528ac99aaecb4e47eadd6dc8aa6533d58"
version = "v0.3.5"
[[projects]]
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
name = "github.com/json-iterator/go"
packages = ["."]
revision = "ca39e5af3ece67bbcda3d0f4f56a8e24d9f2dad4"
version = "1.1.3"
[[projects]]
branch = "master"
name = "github.com/kubernetes-incubator/custom-metrics-apiserver"
packages = [
"pkg/apiserver",
"pkg/apiserver/installer",
"pkg/cmd/server",
"pkg/dynamicmapper",
"pkg/provider",
"pkg/registry/custom_metrics",
"pkg/registry/external_metrics"
]
revision = "d8f23423aa1d0ff2bc9656da863d721725b3c68a"
[[projects]]
branch = "master"
name = "github.com/mailru/easyjson"
packages = [
"buffer",
"jlexer",
"jwriter"
]
revision = "3fdea8d05856a0c8df22ed4bc71b3219245e4485"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
version = "v1.0.1"
[[projects]]
name = "github.com/modern-go/concurrent"
packages = ["."]
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
version = "1.0.3"
[[projects]]
name = "github.com/modern-go/reflect2"
packages = ["."]
revision = "1df9eeb2bb81f327b96228865c5687bc2194af3f"
version = "1.0.0"
[[projects]]
name = "github.com/pborman/uuid"
packages = ["."]
revision = "e790cca94e6cc75c7064b1332e63811d4aae1a53"
version = "v1.1"
[[projects]]
branch = "master"
name = "github.com/petar/GoLLRB"
packages = ["llrb"]
revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
[[projects]]
name = "github.com/peterbourgon/diskv"
packages = ["."]
revision = "5f041e8faa004a95c88a202771f4cc3e991971e6"
version = "v2.0.1"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/prometheus/client_golang"
packages = ["prometheus"]
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
]
revision = "7600349dcfe1abd18d72d3a1770870d9800a7801"
[[projects]]
branch = "master"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
]
revision = "7d6f385de8bea29190f15ba9931442a0eaef9af7"
[[projects]]
name = "github.com/spf13/cobra"
packages = ["."]
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
version = "v0.0.3"
[[projects]]
name = "github.com/spf13/pflag"
packages = ["."]
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
version = "v1.0.1"
[[projects]]
name = "github.com/stretchr/testify"
packages = [
"assert",
"require"
]
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
name = "github.com/ugorji/go"
packages = ["codec"]
revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
version = "v1.1.1"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"trace",
"websocket"
]
revision = "afe8f62b1d6bbd81f31868121a50b06d8188e1f9"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "63fc586f45fe72d95d5240a5d5eb95e6503907d3"
[[projects]]
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
"width"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
name = "golang.org/x/time"
packages = ["rate"]
revision = "fbb02b2291d28baffd63558aa44b4b56f178d650"
[[projects]]
branch = "master"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
revision = "80063a038e333bbe006c878e4c5ce4c74d055498"
[[projects]]
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"codes",
"connectivity",
"credentials",
"encoding",
"encoding/proto",
"grpclog",
"health/grpc_health_v1",
"internal",
"internal/backoff",
"internal/channelz",
"internal/grpcrand",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
"transport"
]
revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8"
version = "v1.13.0"
[[projects]]
name = "gopkg.in/inf.v0"
packages = ["."]
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
version = "v0.9.1"
[[projects]]
name = "gopkg.in/natefinch/lumberjack.v2"
packages = ["."]
revision = "a96e63847dc3c67d17befa69c303767e2f84e54f"
version = "v2.1"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[[projects]]
name = "k8s.io/api"
packages = [
"admission/v1beta1",
"admissionregistration/v1alpha1",
"admissionregistration/v1beta1",
"apps/v1",
"apps/v1beta1",
"apps/v1beta2",
"authentication/v1",
"authentication/v1beta1",
"authorization/v1",
"authorization/v1beta1",
"autoscaling/v1",
"autoscaling/v2beta1",
"batch/v1",
"batch/v1beta1",
"batch/v2alpha1",
"certificates/v1beta1",
"core/v1",
"events/v1beta1",
"extensions/v1beta1",
"networking/v1",
"policy/v1beta1",
"rbac/v1",
"rbac/v1alpha1",
"rbac/v1beta1",
"scheduling/v1alpha1",
"scheduling/v1beta1",
"settings/v1alpha1",
"storage/v1",
"storage/v1alpha1",
"storage/v1beta1"
]
revision = "91b2d7a92a8930454bf5020e0595b8ea0f2a5047"
version = "kubernetes-1.11.0-rc.1"
[[projects]]
name = "k8s.io/apimachinery"
packages = [
"pkg/api/equality",
"pkg/api/errors",
"pkg/api/meta",
"pkg/api/resource",
"pkg/api/validation",
"pkg/api/validation/path",
"pkg/apis/meta/internalversion",
"pkg/apis/meta/v1",
"pkg/apis/meta/v1/unstructured",
"pkg/apis/meta/v1/validation",
"pkg/apis/meta/v1beta1",
"pkg/conversion",
"pkg/conversion/queryparams",
"pkg/fields",
"pkg/labels",
"pkg/runtime",
"pkg/runtime/schema",
"pkg/runtime/serializer",
"pkg/runtime/serializer/json",
"pkg/runtime/serializer/protobuf",
"pkg/runtime/serializer/recognizer",
"pkg/runtime/serializer/streaming",
"pkg/runtime/serializer/versioning",
"pkg/selection",
"pkg/types",
"pkg/util/cache",
"pkg/util/clock",
"pkg/util/diff",
"pkg/util/errors",
"pkg/util/framer",
"pkg/util/intstr",
"pkg/util/json",
"pkg/util/mergepatch",
"pkg/util/net",
"pkg/util/rand",
"pkg/util/runtime",
"pkg/util/sets",
"pkg/util/strategicpatch",
"pkg/util/uuid",
"pkg/util/validation",
"pkg/util/validation/field",
"pkg/util/wait",
"pkg/util/waitgroup",
"pkg/util/yaml",
"pkg/version",
"pkg/watch",
"third_party/forked/golang/json",
"third_party/forked/golang/reflect"
]
revision = "fda675fbe85280c4550452dae2a5ebf74e4a59b7"
version = "kubernetes-1.11.0-rc.1"
[[projects]]
name = "k8s.io/apiserver"
packages = [
"pkg/admission",
"pkg/admission/configuration",
"pkg/admission/initializer",
"pkg/admission/metrics",
"pkg/admission/plugin/initialization",
"pkg/admission/plugin/namespace/lifecycle",
"pkg/admission/plugin/webhook/config",
"pkg/admission/plugin/webhook/config/apis/webhookadmission",
"pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1",
"pkg/admission/plugin/webhook/errors",
"pkg/admission/plugin/webhook/generic",
"pkg/admission/plugin/webhook/mutating",
"pkg/admission/plugin/webhook/namespace",
"pkg/admission/plugin/webhook/request",
"pkg/admission/plugin/webhook/rules",
"pkg/admission/plugin/webhook/validating",
"pkg/apis/apiserver",
"pkg/apis/apiserver/install",
"pkg/apis/apiserver/v1alpha1",
"pkg/apis/audit",
"pkg/apis/audit/install",
"pkg/apis/audit/v1alpha1",
"pkg/apis/audit/v1beta1",
"pkg/apis/audit/validation",
"pkg/audit",
"pkg/audit/policy",
"pkg/authentication/authenticator",
"pkg/authentication/authenticatorfactory",
"pkg/authentication/group",
"pkg/authentication/request/anonymous",
"pkg/authentication/request/bearertoken",
"pkg/authentication/request/headerrequest",
"pkg/authentication/request/union",
"pkg/authentication/request/websocket",
"pkg/authentication/request/x509",
"pkg/authentication/serviceaccount",
"pkg/authentication/token/tokenfile",
"pkg/authentication/user",
"pkg/authorization/authorizer",
"pkg/authorization/authorizerfactory",
"pkg/authorization/union",
"pkg/endpoints",
"pkg/endpoints/discovery",
"pkg/endpoints/filters",
"pkg/endpoints/handlers",
"pkg/endpoints/handlers/negotiation",
"pkg/endpoints/handlers/responsewriters",
"pkg/endpoints/metrics",
"pkg/endpoints/openapi",
"pkg/endpoints/request",
"pkg/features",
"pkg/registry/generic",
"pkg/registry/generic/registry",
"pkg/registry/rest",
"pkg/server",
"pkg/server/filters",
"pkg/server/healthz",
"pkg/server/httplog",
"pkg/server/mux",
"pkg/server/options",
"pkg/server/resourceconfig",
"pkg/server/routes",
"pkg/server/routes/data/swagger",
"pkg/server/storage",
"pkg/storage",
"pkg/storage/errors",
"pkg/storage/etcd",
"pkg/storage/etcd/metrics",
"pkg/storage/etcd/util",
"pkg/storage/etcd3",
"pkg/storage/etcd3/preflight",
"pkg/storage/names",
"pkg/storage/storagebackend",
"pkg/storage/storagebackend/factory",
"pkg/storage/value",
"pkg/util/feature",
"pkg/util/flag",
"pkg/util/flushwriter",
"pkg/util/logs",
"pkg/util/openapi",
"pkg/util/trace",
"pkg/util/webhook",
"pkg/util/wsstream",
"plugin/pkg/audit/buffered",
"plugin/pkg/audit/log",
"plugin/pkg/audit/truncate",
"plugin/pkg/audit/webhook",
"plugin/pkg/authenticator/token/webhook",
"plugin/pkg/authorizer/webhook"
]
revision = "44b612291bb7545430c499a3882c610c727f37b0"
version = "kubernetes-1.11.0-rc.1"
[[projects]]
name = "k8s.io/client-go"
packages = [
"discovery",
"dynamic",
"dynamic/fake",
"informers",
"informers/admissionregistration",
"informers/admissionregistration/v1alpha1",
"informers/admissionregistration/v1beta1",
"informers/apps",
"informers/apps/v1",
"informers/apps/v1beta1",
"informers/apps/v1beta2",
"informers/autoscaling",
"informers/autoscaling/v1",
"informers/autoscaling/v2beta1",
"informers/batch",
"informers/batch/v1",
"informers/batch/v1beta1",
"informers/batch/v2alpha1",
"informers/certificates",
"informers/certificates/v1beta1",
"informers/core",
"informers/core/v1",
"informers/events",
"informers/events/v1beta1",
"informers/extensions",
"informers/extensions/v1beta1",
"informers/internalinterfaces",
"informers/networking",
"informers/networking/v1",
"informers/policy",
"informers/policy/v1beta1",
"informers/rbac",
"informers/rbac/v1",
"informers/rbac/v1alpha1",
"informers/rbac/v1beta1",
"informers/scheduling",
"informers/scheduling/v1alpha1",
"informers/scheduling/v1beta1",
"informers/settings",
"informers/settings/v1alpha1",
"informers/storage",
"informers/storage/v1",
"informers/storage/v1alpha1",
"informers/storage/v1beta1",
"kubernetes",
"kubernetes/scheme",
"kubernetes/typed/admissionregistration/v1alpha1",
"kubernetes/typed/admissionregistration/v1beta1",
"kubernetes/typed/apps/v1",
"kubernetes/typed/apps/v1beta1",
"kubernetes/typed/apps/v1beta2",
"kubernetes/typed/authentication/v1",
"kubernetes/typed/authentication/v1beta1",
"kubernetes/typed/authorization/v1",
"kubernetes/typed/authorization/v1beta1",
"kubernetes/typed/autoscaling/v1",
"kubernetes/typed/autoscaling/v2beta1",
"kubernetes/typed/batch/v1",
"kubernetes/typed/batch/v1beta1",
"kubernetes/typed/batch/v2alpha1",
"kubernetes/typed/certificates/v1beta1",
"kubernetes/typed/core/v1",
"kubernetes/typed/events/v1beta1",
"kubernetes/typed/extensions/v1beta1",
"kubernetes/typed/networking/v1",
"kubernetes/typed/policy/v1beta1",
"kubernetes/typed/rbac/v1",
"kubernetes/typed/rbac/v1alpha1",
"kubernetes/typed/rbac/v1beta1",
"kubernetes/typed/scheduling/v1alpha1",
"kubernetes/typed/scheduling/v1beta1",
"kubernetes/typed/settings/v1alpha1",
"kubernetes/typed/storage/v1",
"kubernetes/typed/storage/v1alpha1",
"kubernetes/typed/storage/v1beta1",
"listers/admissionregistration/v1alpha1",
"listers/admissionregistration/v1beta1",
"listers/apps/v1",
"listers/apps/v1beta1",
"listers/apps/v1beta2",
"listers/autoscaling/v1",
"listers/autoscaling/v2beta1",
"listers/batch/v1",
"listers/batch/v1beta1",
"listers/batch/v2alpha1",
"listers/certificates/v1beta1",
"listers/core/v1",
"listers/events/v1beta1",
"listers/extensions/v1beta1",
"listers/networking/v1",
"listers/policy/v1beta1",
"listers/rbac/v1",
"listers/rbac/v1alpha1",
"listers/rbac/v1beta1",
"listers/scheduling/v1alpha1",
"listers/scheduling/v1beta1",
"listers/settings/v1alpha1",
"listers/storage/v1",
"listers/storage/v1alpha1",
"listers/storage/v1beta1",
"pkg/apis/clientauthentication",
"pkg/apis/clientauthentication/v1alpha1",
"pkg/apis/clientauthentication/v1beta1",
"pkg/version",
"plugin/pkg/client/auth/exec",
"rest",
"rest/watch",
"restmapper",
"testing",
"tools/auth",
"tools/cache",
"tools/clientcmd",
"tools/clientcmd/api",
"tools/clientcmd/api/latest",
"tools/clientcmd/api/v1",
"tools/metrics",
"tools/pager",
"tools/reference",
"transport",
"util/buffer",
"util/cert",
"util/connrotation",
"util/flowcontrol",
"util/homedir",
"util/integer",
"util/retry"
]
revision = "4cacfee698b01630072bc41e3384280562a97d95"
version = "kubernetes-1.11.0-rc.1"
[[projects]]
branch = "master"
name = "k8s.io/kube-openapi"
packages = [
"pkg/builder",
"pkg/common",
"pkg/handler",
"pkg/util",
"pkg/util/proto"
]
revision = "91cfa479c814065e420cee7ed227db0f63a5854e"
[[projects]]
name = "k8s.io/metrics"
packages = [
"pkg/apis/custom_metrics",
"pkg/apis/custom_metrics/install",
"pkg/apis/custom_metrics/v1beta1",
"pkg/apis/external_metrics",
"pkg/apis/external_metrics/install",
"pkg/apis/external_metrics/v1beta1"
]
revision = "89f8a18a5efb0c0162a32c75db752bc53ed7f8ee"
version = "kubernetes-1.11.0-rc.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "922da691d7be0fa3bde2ab628c629fea6718792cb234a2e5c661a193f0545d6f"
solver-name = "gps-cdcl"
solver-version = 1

82
Gopkg.toml Normal file
View file

@ -0,0 +1,82 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
# Utility library deps
[[constraint]]
branch = "master"
name = "github.com/golang/glog"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
[[constraint]]
branch = "master"
name = "github.com/prometheus/common"
[[constraint]]
name = "github.com/spf13/cobra"
version = "0.0.3"
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "2.2.1"
# Kubernetes incubator deps
[[constraint]]
version = "kubernetes-1.11.0-rc.1"
name = "github.com/kubernetes-incubator/custom-metrics-apiserver"
# Core Kubernetes deps
[[constraint]]
name = "k8s.io/api"
version = "kubernetes-1.11.0-rc.1"
[[constraint]]
name = "k8s.io/apimachinery"
version = "kubernetes-1.11.0-rc.1"
[[constraint]]
name = "k8s.io/apiserver"
version = "kubernetes-1.11.0-rc.1"
[[constraint]]
name = "k8s.io/client-go"
version = "kubernetes-1.11.0-rc.1"
[[constraint]]
name = "k8s.io/metrics"
version = "kubernetes-1.11.0-rc.1"
# Test deps
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.2"
[prune]
go-tests = true
unused-packages = true

View file

@ -8,7 +8,7 @@ OUT_DIR?=./_output
VENDOR_DOCKERIZED=0 VENDOR_DOCKERIZED=0
VERSION?=latest VERSION?=latest
GOIMAGE=golang:1.8 GOIMAGE=golang:1.10
ifeq ($(ARCH),amd64) ifeq ($(ARCH),amd64)
BASEIMAGE?=busybox BASEIMAGE?=busybox
@ -24,25 +24,34 @@ ifeq ($(ARCH),ppc64le)
endif endif
ifeq ($(ARCH),s390x) ifeq ($(ARCH),s390x)
BASEIMAGE?=s390x/busybox BASEIMAGE?=s390x/busybox
GOIMAGE=s390x/golang:1.8 GOIMAGE=s390x/golang:1.10
endif endif
.PHONY: all build docker-build push-% push test verify-gofmt gofmt verify .PHONY: all docker-build push-% push test verify-gofmt gofmt verify build-local-image
all: build all: $(OUT_DIR)/$(ARCH)/adapter
build: vendor
CGO_ENABLED=0 GOARCH=$(ARCH) go build -a -tags netgo -o $(OUT_DIR)/$(ARCH)/adapter github.com/directxman12/k8s-prometheus-adapter/cmd/adapter src_deps=$(shell find pkg cmd -type f -name "*.go")
$(OUT_DIR)/%/adapter: vendor $(src_deps)
CGO_ENABLED=0 GOARCH=$* go build -tags netgo -o $(OUT_DIR)/$(ARCH)/adapter github.com/directxman12/k8s-prometheus-adapter/cmd/adapter
docker-build: vendor docker-build: vendor
cp deploy/Dockerfile $(TEMP_DIR) cp deploy/Dockerfile $(TEMP_DIR)
cd $(TEMP_DIR) && sed -i "s|BASEIMAGE|$(BASEIMAGE)|g" Dockerfile cd $(TEMP_DIR) && sed -i "s|BASEIMAGE|$(BASEIMAGE)|g" Dockerfile
docker run -it -v $(TEMP_DIR):/build -v $(shell pwd):/go/src/github.com/directxman12/k8s-prometheus-adapter -e GOARCH=$(ARCH) $(GOIMAGE) /bin/bash -c "\ docker run -it -v $(TEMP_DIR):/build -v $(shell pwd):/go/src/github.com/directxman12/k8s-prometheus-adapter -e GOARCH=$(ARCH) $(GOIMAGE) /bin/bash -c "\
CGO_ENABLED=0 go build -a -tags netgo -o /build/adapter github.com/directxman12/k8s-prometheus-adapter/cmd/adapter" CGO_ENABLED=0 go build -tags netgo -o /build/adapter github.com/directxman12/k8s-prometheus-adapter/cmd/adapter"
docker build -t $(REGISTRY)/$(IMAGE)-$(ARCH):$(VERSION) $(TEMP_DIR) docker build -t $(REGISTRY)/$(IMAGE)-$(ARCH):$(VERSION) $(TEMP_DIR)
rm -rf $(TEMP_DIR) rm -rf $(TEMP_DIR)
build-local-image: $(OUT_DIR)/$(ARCH)/adapter
cp deploy/Dockerfile $(TEMP_DIR)
cp $(OUT_DIR)/$(ARCH)/adapter $(TEMP_DIR)
cd $(TEMP_DIR) && sed -i "s|BASEIMAGE|scratch|g" Dockerfile
docker build -t $(REGISTRY)/$(IMAGE)-$(ARCH):$(VERSION) $(TEMP_DIR)
rm -rf $(TEMP_DIR)
push-%: push-%:
$(MAKE) ARCH=$* docker-build $(MAKE) ARCH=$* docker-build
docker push $(REGISTRY)/$(IMAGE)-$*:$(VERSION) docker push $(REGISTRY)/$(IMAGE)-$*:$(VERSION)
@ -54,13 +63,13 @@ push: ./manifest-tool $(addprefix push-,$(ALL_ARCH))
curl -sSL https://github.com/estesp/manifest-tool/releases/download/v0.5.0/manifest-tool-linux-amd64 > manifest-tool curl -sSL https://github.com/estesp/manifest-tool/releases/download/v0.5.0/manifest-tool-linux-amd64 > manifest-tool
chmod +x manifest-tool chmod +x manifest-tool
vendor: glide.lock vendor: Gopkg.lock
ifeq ($(VENDOR_DOCKERIZED),1) ifeq ($(VENDOR_DOCKERIZED),1)
docker run -it -v $(shell pwd):/go/src/github.com/directxman12/k8s-prometheus-adapter -w /go/src/github.com/directxman12/k8s-prometheus-adapter golang:1.8 /bin/bash -c "\ docker run -it -v $(shell pwd):/go/src/github.com/directxman12/k8s-prometheus-adapter -w /go/src/github.com/directxman12/k8s-prometheus-adapter golang:1.10 /bin/bash -c "\
curl https://glide.sh/get | sh \ curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh \
&& glide install -v" && dep ensure -vendor-only"
else else
glide install -v dep ensure -vendor-only
endif endif
test: vendor test: vendor

View file

@ -29,13 +29,14 @@ adapter talks to Prometheus and the main Kubernetes cluster:
- `--metrics-relist-interval=<duration>`: This is the interval at which to - `--metrics-relist-interval=<duration>`: This is the interval at which to
update the cache of available metrics from Prometheus. update the cache of available metrics from Prometheus.
- `--rate-interval=<duration>`: This is the duration used when requesting
rate metrics from Prometheus. It *must* be larger than your Prometheus
collection interval.
- `--prometheus-url=<url>`: This is the URL used to connect to Prometheus. - `--prometheus-url=<url>`: This is the URL used to connect to Prometheus.
It will eventually contain query parameters to configure the connection. It will eventually contain query parameters to configure the connection.
- `--config=<yaml-file>` (`-c`): This configures how the adapter discovers available
Prometheus metrics and the associated Kubernetes resources, and how it presents those
metrics in the custom metrics API. More information about this file can be found in
[docs/config.md](docs/config.md).
Presentation Presentation
------------ ------------
@ -43,18 +44,14 @@ The adapter gathers the names of available metrics from Prometheus
a regular interval (see [Configuration](#configuration) above), and then a regular interval (see [Configuration](#configuration) above), and then
only exposes metrics that follow specific forms. only exposes metrics that follow specific forms.
In general: The rules governing this discovery are specified in a [configuration file](docs/config.md).
If you were relying on the implicit rules from the previous version of the adapter,
you can use the included `config-gen` tool to generate a configuration that matches
the old implicit ruleset:
- Metrics must have the `namespace` label to be considered. ```shell
$ go run cmd/config-gen main.go [--rate-interval=<duration>] [--label-prefix=<prefix>]
- For each label on a metric, if that label name corresponds to ```
a Kubernetes resource (like `pod` or `service`), the metric will be
associated with that resource.
- Metrics ending in `_total` are assumed to be cumulative, and will be
exposed without the suffix as a rate metric.
Detailed information can be found under [docs/format.md](docs/format.md).
Example Example
------- -------
@ -65,7 +62,8 @@ Additionally, [@luxas](https://github.com/luxas) has an excellent example
deployment of Prometheus, this adapter, and a demo pod which serves deployment of Prometheus, this adapter, and a demo pod which serves
a metric `http_requests_total`, which becomes the custom metrics API a metric `http_requests_total`, which becomes the custom metrics API
metric `pods/http_requests`. It also autoscales on that metric using the metric `pods/http_requests`. It also autoscales on that metric using the
`autoscaling/v2beta1` HorizontalPodAutoscaler. `autoscaling/v2beta1` HorizontalPodAutoscaler. Note that @luxas's tutorial
uses a slightly older version of the adapter.
It can be found at https://github.com/luxas/kubeadm-workshop. Pay special It can be found at https://github.com/luxas/kubeadm-workshop. Pay special
attention to: attention to:

View file

@ -24,7 +24,6 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -32,6 +31,7 @@ import (
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
mprom "github.com/directxman12/k8s-prometheus-adapter/pkg/client/metrics" mprom "github.com/directxman12/k8s-prometheus-adapter/pkg/client/metrics"
adaptercfg "github.com/directxman12/k8s-prometheus-adapter/pkg/config"
cmprov "github.com/directxman12/k8s-prometheus-adapter/pkg/custom-provider" cmprov "github.com/directxman12/k8s-prometheus-adapter/pkg/custom-provider"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd/server" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd/server"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/dynamicmapper" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/dynamicmapper"
@ -43,9 +43,7 @@ func NewCommandStartPrometheusAdapterServer(out, errOut io.Writer, stopCh <-chan
o := PrometheusAdapterServerOptions{ o := PrometheusAdapterServerOptions{
CustomMetricsAdapterServerOptions: baseOpts, CustomMetricsAdapterServerOptions: baseOpts,
MetricsRelistInterval: 10 * time.Minute, MetricsRelistInterval: 10 * time.Minute,
RateInterval: 5 * time.Minute,
PrometheusURL: "https://localhost", PrometheusURL: "https://localhost",
DiscoveryInterval: 10 * time.Minute,
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -76,19 +74,20 @@ func NewCommandStartPrometheusAdapterServer(out, errOut io.Writer, stopCh <-chan
"any described objets") "any described objets")
flags.DurationVar(&o.MetricsRelistInterval, "metrics-relist-interval", o.MetricsRelistInterval, ""+ flags.DurationVar(&o.MetricsRelistInterval, "metrics-relist-interval", o.MetricsRelistInterval, ""+
"interval at which to re-list the set of all available metrics from Prometheus") "interval at which to re-list the set of all available metrics from Prometheus")
flags.DurationVar(&o.RateInterval, "rate-interval", o.RateInterval, ""+
"period of time used to calculate rate metrics from cumulative metrics")
flags.DurationVar(&o.DiscoveryInterval, "discovery-interval", o.DiscoveryInterval, ""+ flags.DurationVar(&o.DiscoveryInterval, "discovery-interval", o.DiscoveryInterval, ""+
"interval at which to refresh API discovery information") "interval at which to refresh API discovery information")
flags.StringVar(&o.PrometheusURL, "prometheus-url", o.PrometheusURL, flags.StringVar(&o.PrometheusURL, "prometheus-url", o.PrometheusURL,
"URL for connecting to Prometheus. Query parameters are used to configure the connection") "URL for connecting to Prometheus.")
flags.BoolVar(&o.PrometheusAuthInCluster, "prometheus-auth-incluster", o.PrometheusAuthInCluster, flags.BoolVar(&o.PrometheusAuthInCluster, "prometheus-auth-incluster", o.PrometheusAuthInCluster,
"use auth details from the in-cluster kubeconfig when connecting to prometheus.") "use auth details from the in-cluster kubeconfig when connecting to prometheus.")
flags.StringVar(&o.PrometheusAuthConf, "prometheus-auth-config", o.PrometheusAuthConf, flags.StringVar(&o.PrometheusAuthConf, "prometheus-auth-config", o.PrometheusAuthConf,
"kubeconfig file used to configure auth when connecting to Prometheus.") "kubeconfig file used to configure auth when connecting to Prometheus.")
flags.StringVar(&o.LabelPrefix, "label-prefix", o.LabelPrefix, flags.StringVar(&o.AdapterConfigFile, "config", o.AdapterConfigFile,
"Prefix to expect on labels referring to pod resources. For example, if the prefix is "+ "Configuration file containing details of how to transform between Prometheus metrics "+
"'kube_', any series with the 'kube_pod' label would be considered a pod metric") "and custom metrics API resources")
cmd.MarkFlagRequired("config")
return cmd return cmd
} }
@ -128,6 +127,15 @@ func makeHTTPClient(inClusterAuth bool, kubeConfigPath string) (*http.Client, er
} }
func (o PrometheusAdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-chan struct{}) error { func (o PrometheusAdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-chan struct{}) error {
if o.AdapterConfigFile == "" {
return fmt.Errorf("no discovery configuration file specified")
}
metricsConfig, err := adaptercfg.FromFile(o.AdapterConfigFile)
if err != nil {
return fmt.Errorf("unable to load metrics discovery configuration: %v", err)
}
config, err := o.Config() config, err := o.Config()
if err != nil { if err != nil {
return err return err
@ -153,12 +161,12 @@ func (o PrometheusAdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-c
return fmt.Errorf("unable to construct discovery client for dynamic client: %v", err) return fmt.Errorf("unable to construct discovery client for dynamic client: %v", err)
} }
dynamicMapper, err := dynamicmapper.NewRESTMapper(discoveryClient, apimeta.InterfacesForUnstructured, o.DiscoveryInterval) dynamicMapper, err := dynamicmapper.NewRESTMapper(discoveryClient, o.DiscoveryInterval)
if err != nil { if err != nil {
return fmt.Errorf("unable to construct dynamic discovery mapper: %v", err) return fmt.Errorf("unable to construct dynamic discovery mapper: %v", err)
} }
clientPool := dynamic.NewClientPool(clientConfig, dynamicMapper, dynamic.LegacyAPIPathResolverFunc) dynamicClient, err := dynamic.NewForConfig(clientConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to construct lister client to initialize provider: %v", err) return fmt.Errorf("unable to construct lister client to initialize provider: %v", err)
} }
@ -176,9 +184,15 @@ func (o PrometheusAdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-c
instrumentedGenericPromClient := mprom.InstrumentGenericAPIClient(genericPromClient, baseURL.String()) instrumentedGenericPromClient := mprom.InstrumentGenericAPIClient(genericPromClient, baseURL.String())
promClient := prom.NewClientForAPI(instrumentedGenericPromClient) promClient := prom.NewClientForAPI(instrumentedGenericPromClient)
cmProvider := cmprov.NewPrometheusProvider(dynamicMapper, clientPool, promClient, o.LabelPrefix, o.MetricsRelistInterval, o.RateInterval, stopCh) namers, err := cmprov.NamersFromConfig(metricsConfig, dynamicMapper)
if err != nil {
return fmt.Errorf("unable to construct naming scheme from metrics rules: %v", err)
}
server, err := config.Complete().New("prometheus-custom-metrics-adapter", cmProvider) cmProvider, runner := cmprov.NewPrometheusProvider(dynamicMapper, dynamicClient, promClient, namers, o.MetricsRelistInterval)
runner.RunUntil(stopCh)
server, err := config.Complete().New("prometheus-custom-metrics-adapter", cmProvider, nil)
if err != nil { if err != nil {
return err return err
} }
@ -192,8 +206,6 @@ type PrometheusAdapterServerOptions struct {
RemoteKubeConfigFile string RemoteKubeConfigFile string
// MetricsRelistInterval is the interval at which to relist the set of available metrics // MetricsRelistInterval is the interval at which to relist the set of available metrics
MetricsRelistInterval time.Duration MetricsRelistInterval time.Duration
// RateInterval is the period of time used to calculate rate metrics
RateInterval time.Duration
// DiscoveryInterval is the interval at which discovery information is refreshed // DiscoveryInterval is the interval at which discovery information is refreshed
DiscoveryInterval time.Duration DiscoveryInterval time.Duration
// PrometheusURL is the URL describing how to connect to Prometheus. Query parameters configure connection options. // PrometheusURL is the URL describing how to connect to Prometheus. Query parameters configure connection options.
@ -202,7 +214,6 @@ type PrometheusAdapterServerOptions struct {
PrometheusAuthInCluster bool PrometheusAuthInCluster bool
// PrometheusAuthConf is the kubeconfig file that contains auth details used to connect to Prometheus // PrometheusAuthConf is the kubeconfig file that contains auth details used to connect to Prometheus
PrometheusAuthConf string PrometheusAuthConf string
// LabelPrefix is the prefix to expect on labels for Kubernetes resources // AdapterConfigFile points to the file containing the metrics discovery configuration.
// (e.g. if the prefix is "kube_", we'd expect a "kube_pod" label for pod metrics). AdapterConfigFile string
LabelPrefix string
} }

44
cmd/config-gen/main.go Normal file
View file

@ -0,0 +1,44 @@
package main
import (
"fmt"
"os"
"time"
"github.com/spf13/cobra"
yaml "gopkg.in/yaml.v2"
"github.com/directxman12/k8s-prometheus-adapter/cmd/config-gen/utils"
)
func main() {
var labelPrefix string
var rateInterval time.Duration
cmd := &cobra.Command{
Short: "Generate a config matching the legacy discovery rules",
Long: `Generate a config that produces the same functionality
as the legacy discovery rules. This includes discovering metrics and associating
resources according to the Kubernetes instrumention conventions and the cAdvisor
conventions, and auto-converting cumulative metrics into rate metrics.`,
RunE: func(c *cobra.Command, args []string) error {
cfg := utils.DefaultConfig(rateInterval, labelPrefix)
enc := yaml.NewEncoder(os.Stdout)
if err := enc.Encode(cfg); err != nil {
return err
}
return enc.Close()
},
}
cmd.Flags().StringVar(&labelPrefix, "label-prefix", "",
"Prefix to expect on labels referring to pod resources. For example, if the prefix is "+
"'kube_', any series with the 'kube_pod' label would be considered a pod metric")
cmd.Flags().DurationVar(&rateInterval, "rate-interval", 5*time.Minute,
"Period of time used to calculate rate metrics from cumulative metrics")
if err := cmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Unable to generate config: %v\n", err)
os.Exit(1)
}
}

View file

@ -0,0 +1,93 @@
package utils
import (
"fmt"
"time"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
. "github.com/directxman12/k8s-prometheus-adapter/pkg/config"
pmodel "github.com/prometheus/common/model"
)
// DefaultConfig returns a configuration equivalent to the former
// pre-advanced-config settings. This means that "normal" series labels
// will be of the form `<prefix><<.Resource>>`, cadvisor series will be
// of the form `container_`, and have the label `pod_name`. Any series ending
// in total will be treated as a rate metric.
func DefaultConfig(rateInterval time.Duration, labelPrefix string) *MetricsDiscoveryConfig {
return &MetricsDiscoveryConfig{
Rules: []DiscoveryRule{
// container seconds rate metrics
{
SeriesQuery: string(prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", ""))),
Resources: ResourceMapping{
Overrides: map[string]GroupResource{
"namespace": {Resource: "namespace"},
"pod_name": {Resource: "pod"},
},
},
Name: NameMapping{Matches: "^container_(.*)_seconds_total$"},
MetricsQuery: fmt.Sprintf(`sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[%s])) by (<<.GroupBy>>)`, pmodel.Duration(rateInterval).String()),
},
// container rate metrics
{
SeriesQuery: string(prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", ""))),
SeriesFilters: []RegexFilter{{IsNot: "^container_.*_seconds_total$"}},
Resources: ResourceMapping{
Overrides: map[string]GroupResource{
"namespace": {Resource: "namespace"},
"pod_name": {Resource: "pod"},
},
},
Name: NameMapping{Matches: "^container_(.*)_total$"},
MetricsQuery: fmt.Sprintf(`sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[%s])) by (<<.GroupBy>>)`, pmodel.Duration(rateInterval).String()),
},
// container non-cumulative metrics
{
SeriesQuery: string(prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", ""))),
SeriesFilters: []RegexFilter{{IsNot: "^container_.*_total$"}},
Resources: ResourceMapping{
Overrides: map[string]GroupResource{
"namespace": {Resource: "namespace"},
"pod_name": {Resource: "pod"},
},
},
Name: NameMapping{Matches: "^container_(.*)$"},
MetricsQuery: `sum(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}) by (<<.GroupBy>>)`,
},
// normal non-cumulative metrics
{
SeriesQuery: string(prom.MatchSeries("", prom.LabelNeq(fmt.Sprintf("%snamespace", labelPrefix), ""), prom.NameNotMatches("^container_.*"))),
SeriesFilters: []RegexFilter{{IsNot: ".*_total$"}},
Resources: ResourceMapping{
Template: fmt.Sprintf("%s<<.Resource>>", labelPrefix),
},
MetricsQuery: "sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)",
},
// normal rate metrics
{
SeriesQuery: string(prom.MatchSeries("", prom.LabelNeq(fmt.Sprintf("%snamespace", labelPrefix), ""), prom.NameNotMatches("^container_.*"))),
SeriesFilters: []RegexFilter{{IsNot: ".*_seconds_total"}},
Name: NameMapping{Matches: "^(.*)_total$"},
Resources: ResourceMapping{
Template: fmt.Sprintf("%s<<.Resource>>", labelPrefix),
},
MetricsQuery: fmt.Sprintf("sum(rate(<<.Series>>{<<.LabelMatchers>>}[%s])) by (<<.GroupBy>>)", pmodel.Duration(rateInterval).String()),
},
// seconds rate metrics
{
SeriesQuery: string(prom.MatchSeries("", prom.LabelNeq(fmt.Sprintf("%snamespace", labelPrefix), ""), prom.NameNotMatches("^container_.*"))),
Name: NameMapping{Matches: "^(.*)_seconds_total$"},
Resources: ResourceMapping{
Template: fmt.Sprintf("%s<<.Resource>>", labelPrefix),
},
MetricsQuery: fmt.Sprintf("sum(rate(<<.Series>>{<<.LabelMatchers>>}[%s])) by (<<.GroupBy>>)", pmodel.Duration(rateInterval).String()),
},
},
}
}

View file

@ -28,15 +28,21 @@ spec:
- --logtostderr=true - --logtostderr=true
- --prometheus-url=http://prometheus.prom.svc:9090/ - --prometheus-url=http://prometheus.prom.svc:9090/
- --metrics-relist-interval=30s - --metrics-relist-interval=30s
- --rate-interval=5m
- --v=10 - --v=10
- --config=/default-config.yaml
ports: ports:
- containerPort: 6443 - containerPort: 6443
volumeMounts: volumeMounts:
- mountPath: /var/run/serving-cert - mountPath: /var/run/serving-cert
name: volume-serving-cert name: volume-serving-cert
readOnly: true readOnly: true
- mountPath: /etc/adapter/
name: config
readOnly: true
volumes: volumes:
- name: volume-serving-cert - name: volume-serving-cert
secret: secret:
secretName: cm-adapter-serving-certs secretName: cm-adapter-serving-certs
- name: config
configMap:
name: adapter-config

View file

@ -0,0 +1,74 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: adapter-config
namespace: custom-metrics
data:
config.yaml: |
rules:
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
seriesFilters: []
resources:
overrides:
namespace:
resource: namespace
pod_name:
resource: pod
name:
matches: ^container_(.*)_seconds_total$
as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[5m]))
by (<<.GroupBy>>)
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
seriesFilters:
- isNot: ^container_.*_seconds_total$
resources:
overrides:
namespace:
resource: namespace
pod_name:
resource: pod
name:
matches: ^container_(.*)_total$
as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[5m]))
by (<<.GroupBy>>)
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
seriesFilters:
- isNot: ^container_.*_total$
resources:
overrides:
namespace:
resource: namespace
pod_name:
resource: pod
name:
matches: ^container_(.*)$
as: ""
metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}) by (<<.GroupBy>>)
- seriesQuery: '{namespace!="",__name__!~"^container_.*"}'
seriesFilters:
- isNot: .*_total$
resources:
template: <<.Resource>>
name:
matches: ""
as: ""
metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)
- seriesQuery: '{namespace!="",__name__!~"^container_.*"}'
seriesFilters:
- isNot: .*_seconds_total
resources:
template: <<.Resource>>
name:
matches: ^(.*)_total$
as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>)
- seriesQuery: '{namespace!="",__name__!~"^container_.*"}'
seriesFilters: []
resources:
template: <<.Resource>>
name:
matches: ^(.*)_seconds_total$
as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>)

208
docs/config.md Normal file
View file

@ -0,0 +1,208 @@
Metrics Discovery and Presentation Configuration
================================================
The adapter determines which metrics to expose, and how to expose them,
through a set of "discovery" rules. Each rule is executed independently
(so make sure that your rules are mutually exclusive), and specifies each
of the steps the adapter needs to take to expose a metric in the API.
Each rule can be broken down into roughly four parts:
- *Discovery*, which specifies how the adapter should find all Prometheus
metrics for this rule.
- *Association*, which specifies how the adapter should determine which
Kubernetes resources a particular metric is associated with.
- *Naming*, which specifies how the adapter should expose the metric in
the custom metrics API.
- *Querying*, which specifies how a request for a particular metric on one
or more Kubernetes objects should be turned into a query to Prometheus.
A more comprehensive configuration file can be found in
[sample-config.yaml](sample-config.yaml), but a basic config with one rule
might look like:
```yaml
rules:
# this rule matches cumulative cAdvisor metrics measured in seconds
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
resources:
# skip specifying generic resource<->label mappings, and just
# attach only pod and namespace resources by mapping label names to group-resources
overrides:
namespace: {resource: "namespace"},
pod_name: {resource: "pod"},
# specify that the `container_` and `_seconds_total` suffixes should be removed.
# this also introduces an implicit filter on metric family names
name:
# we use the value of the capture group implicitly as the API name
# we could also explicitly write `as: "$1"`
matches: "^container_(.*)_seconds_total$"
# specify how to construct a query to fetch samples for a given series
# This is a Go template where the `.Series` and `.LabelMatchers` string values
# are available, and the delimiters are `<<` and `>>` to avoid conflicts with
# the prometheus query language
metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[2m])) by (<<.GroupBy>>)"
```
Discovery
---------
Discovery governs the process of finding the metrics that you want to
expose in the custom metrics API. There are two fields that factor into
discovery: `seriesQuery` and `seriesFilters`.
`seriesQuery` specifies Prometheus series query (as passed to the
`/api/v1/series` endpoint in Prometheus) to use to find some set of
Prometheus series. The adapter will strip the label values from this
series, and then use the resulting metric-name-label-names combinations
later on.
In many cases, `seriesQuery` will be sufficient to narrow down the list of
Prometheus series. However, sometimes (especially if two rules might
otherwise overlap), it's useful to do additional filtering on metric
names. In this case, `seriesFilters` can be used. After the list of
series is returned from `seriesQuery`, each series has its metric name
filtered through any specified filters.
Filters may be either:
- `is: <regex>`, which matches any series whose name matches the specified
regex.
- `isNot: <regex>`, which matches any series whose name does not match the
specified regex.
For example:
```yaml
# match all cAdvisor metrics that aren't measured in seconds
seriesQuery: '{__name__=~"^container_.*_total",container_name!="POD",namespace!="",pod_name!=""}'
seriesFilters:
isNot: "^container_.*_seconds_total"
```
Association
-----------
Association governs the process of figuring out which Kubernetes resources
a particular metric could be attached to. The `resources` field controls
this process.
There are two ways to associate resources with a particular metric. In
both cases, the value of the label becomes the name of the particular
object.
One way is to specify that any label name that matches some particular
pattern refers to some group-resource based on the label name. This can
be done using the `template` field. The pattern is specified as a Go
template, with the `Group` and `Resource` fields representing group and
resource. You don't necessarily have to use the `Group` field (in which
case the group is guessed by the system). For instance:
```yaml
# any label `kube_<group>_<resource>` becomes <group>.<resource> in Kubernetes
resources:
template: "kube_<<.Group>>_<<.Resource>>"
```
The other way is to specify that some particular label represents some
particular Kubernetes resource. This can be done using the `overrides`
field. Each override maps a Prometheus label to a Kubernetes
group-resource. For instance:
```yaml
# the microservice label corresponds to the apps.deployment resource
resource:
overrides:
microservice: {group: "apps", resource: "deployment"}
```
These two can be combined, so you can specify both a template and some
individual overrides.
Naming
------
Naming governs the process of converting a Prometheus metric name into
a metric in the custom metrics API, and vice version. It's controlled by
the `name` field.
Naming is controlled by specifying a pattern to extract an API name from
a Prometheus name, and potentially a transformation on that extracted
value.
The pattern is specified in the `matches` field, and is just a regular
expression. If not specified, it defaults to `.*`.
The transformation is specified by the `as` field. You can use any
capture groups defined in the `matches` field. If the `matches` field
doesn't contain capture groups, the `as` field defaults to `$0`. If it
contains a single capture group, the `as` field defautls to `$1`.
Otherwise, it's an error not to specify the as field.
For example:
```yaml
# match turn any name <name>_total to <name>_per_second
# e.g. http_requests_total becomes http_requests_per_second
name:
matches: "^(.*)_total$"
as: "<<1}_per_second"
```
Querying
--------
Querying governs the process of actually fetching values for a particular
metric. It's controlled by the `metricsQuery` field.
The `metricsQuery` field is a Go template that gets turned into
a Prometheus query, using input from a particular call to the custom
metrics API. A given call to the custom metrics API is distilled down to
a metric name, a group-resource, and one or more objects of that
group-resource. These get turned into the following fields in the
template:
- `Series`: the metric name
- `LabelMatchers`: a comma-separated list of label matchers matching the
given objects. Currently, this is the label for the particular
group-resource, plus the label for namespace, if the group-resource is
namespaced.
- `GroupBy`: a comma-separated list of labels to group by. Currently,
this contains the group-resoure label used in `LabelMarchers`.
For instance, suppose we had a series `http_requests_total` (exposed as
`http_requests_per_second` in the API) with labels `service`, `pod`,
`ingress`, `namespace`, and `verb`. The first four correspond to
Kubernetes resources. Then, if someone requested the metric
`pods/http_request_per_second` for the pods `pod1` and `pod2` in the
`somens` namespace, we'd have:
- `Series: "http_requests_total"
- `LabelMatchers: "pod=~\"pod1|pod2",namespace="somens"`
- `GroupBy`: `pod`
Additionally, there are two advanced fields that are "raw" forms of other
fields:
- `LabelValuesByName`: a map mapping the labels and values from the
`LabelMatchers` field. The values are pre-joined by `|`
(for used with the `=~` matcher in Prometheus).
- `GroupBySlice`: the slice form of `GroupBy`.
In general, you'll probably want to use the `Series`, `LabelMatchers`, and
`GroupBy` fields. The other two are for advanced usage.
The query is expected to return one value for each object requested. The
adapter will use the labels on the returned series to associate a given
series back to its corresponding object.
For example:
```yaml
# convert cumulative cAdvisor metrics into rates calculated over 2 minutes
metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[2m])) by (<<.GroupBy>>)"
```

69
docs/sample-config.yaml Normal file
View file

@ -0,0 +1,69 @@
rules:
# Each rule represents a some naming and discovery logic.
# Each rule is executed independently of the others, so
# take care to avoid overlap. As an optimization, rules
# with the same `seriesQuery` but different
# `name` or `seriesFilters` will use only one query to
# Prometheus for discovery.
# some of these rules are taken from the "default" configuration, which
# can be found in pkg/config/default.go
# this rule matches cumulative cAdvisor metrics measured in seconds
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
resources:
# skip specifying generic resource<->label mappings, and just
# attach only pod and namespace resources by mapping label names to group-resources
overrides:
namespace: {resource: "namespace"},
pod_name: {resource: "pod"},
# specify that the `container_` and `_seconds_total` suffixes should be removed.
# this also introduces an implicit filter on metric family names
name:
# we use the value of the capture group implicitly as the API name
# we could also explicitly write `as: "$1"`
matches: "^container_(.*)_seconds_total$"
# specify how to construct a query to fetch samples for a given series
# This is a Go template where the `.Series` and `.LabelMatchers` string values
# are available, and the delimiters are `<<` and `>>` to avoid conflicts with
# the prometheus query language
metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[2m])) by (<<.GroupBy>>)"
# this rule matches cumulative cAdvisor metrics not measured in seconds
- seriesQuery: '{__name__=~"^container_.*_total",container_name!="POD",namespace!="",pod_name!=""}'
resources:
overrides:
namespace: {resource: "namespace"},
pod_name: {resource: "pod"},
seriesFilters:
# since this is a superset of the query above, we introduce an additional filter here
- isNot: "^container_.*_seconds_total$"
name: {matches: "^container_(.*)_total$"}
metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[2m])) by (<<.GroupBy>>)"
# this rule matches cumulative non-cAdvisor metrics
- seriesQuery: '{namespace!="",__name__!="^container_.*"}'
name: {matches: "^(.*)_total$"}
resources:
# specify an a generic mapping between resources and labels. This
# is a template, like the `metricsQuery` template, except with the `.Group`
# and `.Resource` strings available. It will also be used to match labels,
# so avoid using template functions which truncate the group or resource.
# Group will be converted to a form acceptible for use as a label automatically.
template: "<<.Resource>>"
# if we wanted to, we could also specify overrides here
metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[2m])) by (<<.GroupBy>>)"
# this rule matches only a single metric, explicitly naming it something else
# It's series query *must* return only a single metric family
- seriesQuery: 'cheddar{sharp="true"}'
# this metric will appear as "cheesy_goodness" in the custom metrics API
name: {as: "cheesy_goodness"}
resources:
overrides:
# this should still resolve in our cluster
brand: {group: "cheese.io", resource: "brand"}
metricQuery: 'count(cheddar{sharp="true"})'
# TODO: should we be able to map to a constant instance of a resource
# (e.g. `resources: {constant: [{resource: "namespace", name: "kube-system"}}]`)?

571
glide.lock generated
View file

@ -1,571 +0,0 @@
hash: 1d2ad16816cec54a2561278dfd05ae9725247ea9b72db5c7d330f0074d069fad
updated: 2017-09-28T15:22:28.593829441-04:00
imports:
- name: bitbucket.org/ww/goautoneg
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
- name: github.com/beorn7/perks
version: 3ac7bf7a47d159a033b107610db8a1b6575507a4
subpackages:
- quantile
- name: github.com/coreos/etcd
version: 0520cb9304cb2385f7e72b8bc02d6e4d3257158a
subpackages:
- alarm
- auth
- auth/authpb
- client
- clientv3
- compactor
- discovery
- error
- etcdserver
- etcdserver/api
- etcdserver/api/v2http
- etcdserver/api/v2http/httptypes
- etcdserver/api/v3rpc
- etcdserver/api/v3rpc/rpctypes
- etcdserver/auth
- etcdserver/etcdserverpb
- etcdserver/membership
- etcdserver/stats
- integration
- lease
- lease/leasehttp
- lease/leasepb
- mvcc
- mvcc/backend
- mvcc/mvccpb
- pkg/adt
- pkg/contention
- pkg/cpuutil
- pkg/crc
- pkg/fileutil
- pkg/httputil
- pkg/idutil
- pkg/ioutil
- pkg/logutil
- pkg/monotime
- pkg/netutil
- pkg/pathutil
- pkg/pbutil
- pkg/runtime
- pkg/schedule
- pkg/testutil
- pkg/tlsutil
- pkg/transport
- pkg/types
- pkg/wait
- proxy/grpcproxy
- proxy/grpcproxy/cache
- raft
- raft/raftpb
- rafthttp
- snap
- snap/snappb
- store
- version
- wal
- wal/walpb
- name: github.com/coreos/go-systemd
version: 48702e0da86bd25e76cfef347e2adeb434a0d0a6
subpackages:
- daemon
- journal
- name: github.com/coreos/pkg
version: fa29b1d70f0beaddd4c7021607cc3c3be8ce94b8
subpackages:
- capnslog
- health
- httputil
- timeutil
- name: github.com/davecgh/go-spew
version: 782f4967f2dc4564575ca782fe2d04090b5faca8
subpackages:
- spew
- name: github.com/elazarl/go-bindata-assetfs
version: 3dcc96556217539f50599357fb481ac0dc7439b9
- name: github.com/emicklei/go-restful
version: ff4f55a206334ef123e4f79bbf348980da81ca46
subpackages:
- log
- name: github.com/emicklei/go-restful-swagger12
version: dcef7f55730566d41eae5db10e7d6981829720f6
- name: github.com/evanphx/json-patch
version: 944e07253867aacae43c04b2e6a239005443f33a
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/go-openapi/jsonpointer
version: 46af16f9f7b149af66e5d1bd010e3574dc06de98
- name: github.com/go-openapi/jsonreference
version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
- name: github.com/go-openapi/spec
version: 6aced65f8501fe1217321abf0749d354824ba2ff
- name: github.com/go-openapi/swag
version: 1d0bd113de87027671077d3c71eb3ac5d7dbba72
- name: github.com/gogo/protobuf
version: c0656edd0d9eab7c66d1eb0c568f9039345796f7
subpackages:
- proto
- sortkeys
- name: github.com/golang/glog
version: 44145f04b68cf362d9c4df2182967c2275eaefed
- name: github.com/golang/protobuf
version: 4bd1920723d7b7c925de087aa32e2187708897f7
subpackages:
- jsonpb
- proto
- ptypes
- ptypes/any
- ptypes/duration
- ptypes/timestamp
- name: github.com/google/btree
version: 7d79101e329e5a3adf994758c578dab82b90c017
- name: github.com/google/gofuzz
version: 44d81051d367757e1c7c6a5a86423ece9afcf63c
- name: github.com/googleapis/gnostic
version: 0c5108395e2debce0d731cf0287ddf7242066aba
subpackages:
- OpenAPIv2
- compiler
- extensions
- name: github.com/gregjones/httpcache
version: 787624de3eb7bd915c329cba748687a3b22666a6
subpackages:
- diskcache
- name: github.com/grpc-ecosystem/go-grpc-prometheus
version: 2500245aa6110c562d17020fb31a2c133d737799
- name: github.com/grpc-ecosystem/grpc-gateway
version: 84398b94e188ee336f307779b57b3aa91af7063c
subpackages:
- runtime
- runtime/internal
- utilities
- name: github.com/hashicorp/golang-lru
version: a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4
subpackages:
- simplelru
- name: github.com/howeyc/gopass
version: bf9dde6d0d2c004a008c27aaee91170c786f6db8
- name: github.com/imdario/mergo
version: 6633656539c1639d9d78127b7d47c622b5d7b6dc
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/json-iterator/go
version: 36b14963da70d11297d313183d7e6388c8510e1e
- name: github.com/juju/ratelimit
version: 5b9ff866471762aa2ab2dced63c9fb6f53921342
- name: github.com/kubernetes-incubator/custom-metrics-apiserver
version: fae01650d93f5de6151a024e36323344e14427aa
subpackages:
- pkg/apiserver
- pkg/apiserver/installer
- pkg/cmd/server
- pkg/dynamicmapper
- pkg/provider
- pkg/registry/custom_metrics
- name: github.com/mailru/easyjson
version: d5b7844b561a7bc640052f1b935f7b800330d7e0
subpackages:
- buffer
- jlexer
- jwriter
- name: github.com/matttproud/golang_protobuf_extensions
version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a
subpackages:
- pbutil
- name: github.com/mxk/go-flowrate
version: cca7078d478f8520f85629ad7c68962d31ed7682
subpackages:
- flowrate
- name: github.com/NYTimes/gziphandler
version: 56545f4a5d46df9a6648819d1664c3a03a13ffdb
- name: github.com/pborman/uuid
version: ca53cad383cad2479bbba7f7a1a05797ec1386e4
- name: github.com/peterbourgon/diskv
version: 5f041e8faa004a95c88a202771f4cc3e991971e6
- name: github.com/pkg/errors
version: a22138067af1c4942683050411a841ade67fe1eb
- name: github.com/prometheus/client_golang
version: e7e903064f5e9eb5da98208bae10b475d4db0f8c
subpackages:
- prometheus
- name: github.com/prometheus/client_model
version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6
subpackages:
- go
- name: github.com/prometheus/common
version: 13ba4ddd0caa9c28ca7b7bffe1dfa9ed8d5ef207
subpackages:
- expfmt
- internal/bitbucket.org/ww/goautoneg
- model
- name: github.com/prometheus/procfs
version: 65c1f6f8f0fc1e2185eb9863a3bc751496404259
subpackages:
- xfs
- name: github.com/PuerkitoBio/purell
version: 8a290539e2e8629dbc4e6bad948158f790ec31f4
- name: github.com/PuerkitoBio/urlesc
version: 5bd2802263f21d8788851d5305584c82a5c75d7e
- name: github.com/spf13/cobra
version: f62e98d28ab7ad31d707ba837a966378465c7b57
- name: github.com/spf13/pflag
version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7
- name: github.com/stretchr/testify
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
subpackages:
- assert
- require
- name: github.com/ugorji/go
version: ded73eae5db7e7a0ef6f55aace87a2873c5d2b74
subpackages:
- codec
- name: golang.org/x/crypto
version: 81e90905daefcd6fd217b62423c0908922eadb30
subpackages:
- bcrypt
- blowfish
- nacl/secretbox
- poly1305
- salsa20/salsa
- ssh/terminal
- name: golang.org/x/net
version: 1c05540f6879653db88113bc4a2b70aec4bd491f
subpackages:
- context
- html
- html/atom
- http2
- http2/hpack
- idna
- internal/timeseries
- lex/httplex
- trace
- websocket
- name: golang.org/x/sys
version: 7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce
subpackages:
- unix
- windows
- name: golang.org/x/text
version: b19bf474d317b857955b12035d2c5acb57ce8b01
subpackages:
- cases
- internal
- internal/tag
- language
- runes
- secure/bidirule
- secure/precis
- transform
- unicode/bidi
- unicode/norm
- width
- name: google.golang.org/genproto
version: 09f6ed296fc66555a25fe4ce95173148778dfa85
subpackages:
- googleapis/rpc/status
- name: google.golang.org/grpc
version: d2e1b51f33ff8c5e4a15560ff049d200e83726c5
subpackages:
- codes
- credentials
- grpclb/grpc_lb_v1
- grpclog
- internal
- keepalive
- metadata
- naming
- peer
- stats
- status
- tap
- transport
- name: gopkg.in/inf.v0
version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
- name: gopkg.in/natefinch/lumberjack.v2
version: 20b71e5b60d756d3d2f80def009790325acc2b23
- name: gopkg.in/yaml.v2
version: 53feefa2559fb8dfa8d81baad31be332c97d6c77
- name: k8s.io/api
version: cadaf100c0a3dd6b254f320d6d651df079ec8e0a
subpackages:
- admissionregistration/v1alpha1
- apps/v1beta1
- apps/v1beta2
- authentication/v1
- authentication/v1beta1
- authorization/v1
- authorization/v1beta1
- autoscaling/v1
- autoscaling/v2beta1
- batch/v1
- batch/v1beta1
- batch/v2alpha1
- certificates/v1beta1
- core/v1
- extensions/v1beta1
- networking/v1
- policy/v1beta1
- rbac/v1
- rbac/v1alpha1
- rbac/v1beta1
- scheduling/v1alpha1
- settings/v1alpha1
- storage/v1
- storage/v1beta1
- name: k8s.io/apimachinery
version: 3b05bbfa0a45413bfa184edbf9af617e277962fb
subpackages:
- pkg/api/equality
- pkg/api/errors
- pkg/api/meta
- pkg/api/resource
- pkg/api/validation
- pkg/api/validation/path
- pkg/apimachinery
- pkg/apimachinery/announced
- pkg/apimachinery/registered
- pkg/apis/meta/internalversion
- pkg/apis/meta/v1
- pkg/apis/meta/v1/unstructured
- pkg/apis/meta/v1/validation
- pkg/apis/meta/v1alpha1
- pkg/conversion
- pkg/conversion/queryparams
- pkg/conversion/unstructured
- pkg/fields
- pkg/labels
- pkg/runtime
- pkg/runtime/schema
- pkg/runtime/serializer
- pkg/runtime/serializer/json
- pkg/runtime/serializer/protobuf
- pkg/runtime/serializer/recognizer
- pkg/runtime/serializer/streaming
- pkg/runtime/serializer/versioning
- pkg/selection
- pkg/types
- pkg/util/cache
- pkg/util/clock
- pkg/util/diff
- pkg/util/errors
- pkg/util/framer
- pkg/util/httpstream
- pkg/util/intstr
- pkg/util/json
- pkg/util/mergepatch
- pkg/util/net
- pkg/util/proxy
- pkg/util/rand
- pkg/util/runtime
- pkg/util/sets
- pkg/util/strategicpatch
- pkg/util/uuid
- pkg/util/validation
- pkg/util/validation/field
- pkg/util/wait
- pkg/util/yaml
- pkg/version
- pkg/watch
- third_party/forked/golang/json
- third_party/forked/golang/netutil
- third_party/forked/golang/reflect
- name: k8s.io/apiserver
version: c1e53d745d0fe45bf7d5d44697e6eface25fceca
subpackages:
- pkg/admission
- pkg/admission/initializer
- pkg/admission/plugin/namespace/lifecycle
- pkg/apis/apiserver
- pkg/apis/apiserver/install
- pkg/apis/apiserver/v1alpha1
- pkg/apis/audit
- pkg/apis/audit/install
- pkg/apis/audit/v1alpha1
- pkg/apis/audit/v1beta1
- pkg/apis/audit/validation
- pkg/audit
- pkg/audit/policy
- pkg/authentication/authenticator
- pkg/authentication/authenticatorfactory
- pkg/authentication/group
- pkg/authentication/request/anonymous
- pkg/authentication/request/bearertoken
- pkg/authentication/request/headerrequest
- pkg/authentication/request/union
- pkg/authentication/request/websocket
- pkg/authentication/request/x509
- pkg/authentication/serviceaccount
- pkg/authentication/token/tokenfile
- pkg/authentication/user
- pkg/authorization/authorizer
- pkg/authorization/authorizerfactory
- pkg/authorization/union
- pkg/endpoints
- pkg/endpoints/discovery
- pkg/endpoints/filters
- pkg/endpoints/handlers
- pkg/endpoints/handlers/negotiation
- pkg/endpoints/handlers/responsewriters
- pkg/endpoints/metrics
- pkg/endpoints/openapi
- pkg/endpoints/request
- pkg/features
- pkg/registry/generic
- pkg/registry/generic/registry
- pkg/registry/rest
- pkg/server
- pkg/server/filters
- pkg/server/healthz
- pkg/server/httplog
- pkg/server/mux
- pkg/server/options
- pkg/server/routes
- pkg/server/routes/data/swagger
- pkg/server/storage
- pkg/storage
- pkg/storage/errors
- pkg/storage/etcd
- pkg/storage/etcd/metrics
- pkg/storage/etcd/util
- pkg/storage/etcd3
- pkg/storage/etcd3/preflight
- pkg/storage/names
- pkg/storage/storagebackend
- pkg/storage/storagebackend/factory
- pkg/storage/value
- pkg/util/feature
- pkg/util/flag
- pkg/util/flushwriter
- pkg/util/logs
- pkg/util/trace
- pkg/util/webhook
- pkg/util/wsstream
- plugin/pkg/audit/log
- plugin/pkg/audit/webhook
- plugin/pkg/authenticator/token/webhook
- plugin/pkg/authorizer/webhook
- name: k8s.io/client-go
version: 82aa063804cf055e16e8911250f888bc216e8b61
subpackages:
- discovery
- dynamic
- dynamic/fake
- informers
- informers/admissionregistration
- informers/admissionregistration/v1alpha1
- informers/apps
- informers/apps/v1beta1
- informers/apps/v1beta2
- informers/autoscaling
- informers/autoscaling/v1
- informers/autoscaling/v2beta1
- informers/batch
- informers/batch/v1
- informers/batch/v1beta1
- informers/batch/v2alpha1
- informers/certificates
- informers/certificates/v1beta1
- informers/core
- informers/core/v1
- informers/extensions
- informers/extensions/v1beta1
- informers/internalinterfaces
- informers/networking
- informers/networking/v1
- informers/policy
- informers/policy/v1beta1
- informers/rbac
- informers/rbac/v1
- informers/rbac/v1alpha1
- informers/rbac/v1beta1
- informers/scheduling
- informers/scheduling/v1alpha1
- informers/settings
- informers/settings/v1alpha1
- informers/storage
- informers/storage/v1
- informers/storage/v1beta1
- kubernetes
- kubernetes/scheme
- kubernetes/typed/admissionregistration/v1alpha1
- kubernetes/typed/apps/v1beta1
- kubernetes/typed/apps/v1beta2
- kubernetes/typed/authentication/v1
- kubernetes/typed/authentication/v1beta1
- kubernetes/typed/authorization/v1
- kubernetes/typed/authorization/v1beta1
- kubernetes/typed/autoscaling/v1
- kubernetes/typed/autoscaling/v2beta1
- kubernetes/typed/batch/v1
- kubernetes/typed/batch/v1beta1
- kubernetes/typed/batch/v2alpha1
- kubernetes/typed/certificates/v1beta1
- kubernetes/typed/core/v1
- kubernetes/typed/extensions/v1beta1
- kubernetes/typed/networking/v1
- kubernetes/typed/policy/v1beta1
- kubernetes/typed/rbac/v1
- kubernetes/typed/rbac/v1alpha1
- kubernetes/typed/rbac/v1beta1
- kubernetes/typed/scheduling/v1alpha1
- kubernetes/typed/settings/v1alpha1
- kubernetes/typed/storage/v1
- kubernetes/typed/storage/v1beta1
- listers/admissionregistration/v1alpha1
- listers/apps/v1beta1
- listers/apps/v1beta2
- listers/autoscaling/v1
- listers/autoscaling/v2beta1
- listers/batch/v1
- listers/batch/v1beta1
- listers/batch/v2alpha1
- listers/certificates/v1beta1
- listers/core/v1
- listers/extensions/v1beta1
- listers/networking/v1
- listers/policy/v1beta1
- listers/rbac/v1
- listers/rbac/v1alpha1
- listers/rbac/v1beta1
- listers/scheduling/v1alpha1
- listers/settings/v1alpha1
- listers/storage/v1
- listers/storage/v1beta1
- pkg/version
- rest
- rest/watch
- testing
- tools/auth
- tools/cache
- tools/clientcmd
- tools/clientcmd/api
- tools/clientcmd/api/latest
- tools/clientcmd/api/v1
- tools/metrics
- tools/pager
- tools/reference
- transport
- util/cert
- util/flowcontrol
- util/homedir
- util/integer
- name: k8s.io/kube-openapi
version: 868f2f29720b192240e18284659231b440f9cda5
subpackages:
- pkg/builder
- pkg/common
- pkg/handler
- pkg/util
- name: k8s.io/metrics
version: 4c7ac522b9daf7beeb53f6766722ba78b7e5712d
subpackages:
- pkg/apis/custom_metrics
- pkg/apis/custom_metrics/install
- pkg/apis/custom_metrics/v1beta1
testImports:
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib

View file

@ -1,23 +0,0 @@
package: github.com/directxman12/k8s-prometheus-adapter
import:
- package: github.com/spf13/cobra
- package: k8s.io/apimachinery
subpackages:
- pkg/util/wait
- package: k8s.io/apiserver
subpackages:
- pkg/util/logs
- package: k8s.io/client-go
subpackages:
- kubernetes/typed/core/v1
- rest
- tools/clientcmd
- package: github.com/kubernetes-incubator/custom-metrics-apiserver
subpackages:
- pkg/cmd/server
- pkg/provider
- package: github.com/stretchr/testify
version: ^1.1.4
subpackages:
- assert
- require

View file

@ -19,6 +19,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
@ -121,3 +122,11 @@ func (s *Series) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func (s *Series) String() string {
lblStrings := make([]string, 0, len(s.Labels))
for k, v := range s.Labels {
lblStrings = append(lblStrings, fmt.Sprintf("%s=%q", k, v))
}
return fmt.Sprintf("%s{%s}", s.Name, strings.Join(lblStrings, ","))
}

75
pkg/config/config.go Normal file
View file

@ -0,0 +1,75 @@
package config
type MetricsDiscoveryConfig struct {
// Rules specifies how to discover and map Prometheus metrics to
// custom metrics API resources. The rules are applied independently,
// and thus must be mutually exclusive. Rules will the same SeriesQuery
// will make only a single API call.
Rules []DiscoveryRule `yaml:"rules"`
}
// DiscoveryRule describes on set of rules for transforming Prometheus metrics to/from
// custom metrics API resources.
type DiscoveryRule struct {
// SeriesQuery specifies which metrics this rule should consider via a Prometheus query
// series selector query.
SeriesQuery string `yaml:"seriesQuery"`
// SeriesFilters specifies additional regular expressions to be applied on
// the series names returned from the query. This is useful for constraints
// that can't be represented in the SeriesQuery (e.g. series matching `container_.+`
// not matching `container_.+_total`. A filter will be automatically appended to
// match the form specified in Name.
SeriesFilters []RegexFilter `yaml:"seriesFilters"`
// Resources specifies how associated Kubernetes resources should be discovered for
// the given metrics.
Resources ResourceMapping `yaml:"resources"`
// Name specifies how the metric name should be transformed between custom metric
// API resources, and Prometheus metric names.
Name NameMapping `yaml:"name"`
// MetricsQuery specifies modifications to the metrics query, such as converting
// cumulative metrics to rate metrics. It is a template where `.LabelMatchers` is
// a the comma-separated base label matchers and `.Series` is the series name, and
// `.GroupBy` is the comma-separated expected group-by label names. The delimeters
// are `<<` and `>>`.
MetricsQuery string `yaml:"metricsQuery,omitempty"`
}
// RegexFilter is a filter that matches positively or negatively against a regex.
// Only one field may be set at a time.
type RegexFilter struct {
Is string `yaml:"is,omitempty"`
IsNot string `yaml:"isNot,omitempty"`
}
// ResourceMapping specifies how to map Kubernetes resources to Prometheus labels
type ResourceMapping struct {
// Template specifies a golang string template for converting a Kubernetes
// group-resource to a Prometheus label. The template object contains
// the `.Group` and `.Resource` fields. The `.Group` field will have
// dots replaced with underscores, and the `.Resource` field will be
// singularized. The delimiters are `<<` and `>>`.
Template string `yaml:"template,omitempty"`
// Overrides specifies exceptions to the above template, mapping label names
// to group-resources
Overrides map[string]GroupResource `yaml:"overrides,omitempty"`
}
// GroupResource represents a Kubernetes group-resource.
type GroupResource struct {
Group string `yaml:"group,omitempty"`
Resource string `yaml:"resource"`
}
// NameMapping specifies how to convert Prometheus metrics
// to/from custom metrics API resources.
type NameMapping struct {
// Matches is a regular expression that is used to match
// Prometheus series names. It may be left blank, in which
// case it is equivalent to `.*`.
Matches string `yaml:"matches"`
// As is the name used in the API. Captures from Matches
// are available for use here. If not specified, it defaults
// to $0 if no capture groups are present in Matches, or $1
// if only one is present, and will error if multiple are.
As string `yaml:"as"`
}

32
pkg/config/loader.go Normal file
View file

@ -0,0 +1,32 @@
package config
import (
"fmt"
"io/ioutil"
"os"
yaml "gopkg.in/yaml.v2"
)
// FromFile loads the configuration from a particular file.
func FromFile(filename string) (*MetricsDiscoveryConfig, error) {
file, err := os.Open(filename)
defer file.Close()
if err != nil {
return nil, fmt.Errorf("unable to load metrics discovery config file: %v", err)
}
contents, err := ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("unable to load metrics discovery config file: %v", err)
}
return FromYAML(contents)
}
// FromYAML loads the configuration from a blob of YAML.
func FromYAML(contents []byte) (*MetricsDiscoveryConfig, error) {
var cfg MetricsDiscoveryConfig
if err := yaml.Unmarshal(contents, &cfg); err != nil {
return nil, fmt.Errorf("unable to parse metrics discovery config: %v", err)
}
return &cfg, nil
}

View file

@ -1,367 +1,474 @@
/*
Copyright 2017 The Kubernetes Authors.
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 provider package provider
import ( import (
"bytes"
"fmt" "fmt"
"regexp"
"strings" "strings"
"sync" "sync"
"text/template"
"github.com/golang/glog"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
"github.com/golang/glog" "github.com/directxman12/k8s-prometheus-adapter/pkg/config"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
) )
// NB: container metrics sourced from cAdvisor don't consistently follow naming conventions, var nsGroupResource = schema.GroupResource{Resource: "namespaces"}
// so we need to whitelist them and handle them on a case-by-case basis. Metrics ending in `_total` var groupNameSanitizer = strings.NewReplacer(".", "_", "-", "_")
// *should* be counters, but may actually be guages in this case.
// SeriesType represents the kind of series backing a metric. // MetricNamer knows how to convert Prometheus series names and label names to
type SeriesType int // metrics API resources, and vice-versa. MetricNamers should be safe to access
// concurrently. Returned group-resources are "normalized" as per the
const ( // MetricInfo#Normalized method. Group-resources passed as arguments must
CounterSeries SeriesType = iota // themselves be normalized.
SecondsCounterSeries type MetricNamer interface {
GaugeSeries // Selector produces the appropriate Prometheus series selector to match all
) // series handlable by this namer.
Selector() prom.Selector
// SeriesRegistry provides conversions between Prometheus series and MetricInfo // FilterSeries checks to see which of the given series match any additional
type SeriesRegistry interface { // constrains beyond the series query. It's assumed that the series given
// Selectors produces the appropriate Prometheus selectors to match all series handlable // already matche the series query.
// by this registry, as an optimization for SetSeries. FilterSeries(series []prom.Series) []prom.Series
Selectors() []prom.Selector // ResourcesForSeries returns the group-resources associated with the given series,
// SetSeries replaces the known series in this registry // as well as whether or not the given series has the "namespace" resource).
SetSeries(series []prom.Series) error ResourcesForSeries(series prom.Series) (res []schema.GroupResource, namespaced bool)
// ListAllMetrics lists all metrics known to this registry // LabelForResource returns the appropriate label for the given resource.
ListAllMetrics() []provider.MetricInfo LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error)
// SeriesForMetric looks up the minimum required series information to make a query for the given metric // MetricNameForSeries returns the name (as presented in the API) for a given series.
// against the given resource (namespace may be empty for non-namespaced resources) MetricNameForSeries(series prom.Series) (string, error)
QueryForMetric(info provider.MetricInfo, namespace string, resourceNames ...string) (kind SeriesType, query prom.Selector, groupBy string, found bool) // QueryForSeries returns the query for a given series (not API metric name), with
// MatchValuesToNames matches result values to resource names for the given metric and value set // the given namespace name (if relevant), resource, and resource names.
MatchValuesToNames(metricInfo provider.MetricInfo, values pmodel.Vector) (matchedValues map[string]pmodel.SampleValue, found bool) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error)
} }
type seriesInfo struct { // labelGroupResExtractor extracts schema.GroupResources from series labels.
// baseSeries represents the minimum information to access a particular series type labelGroupResExtractor struct {
baseSeries prom.Series regex *regexp.Regexp
// kind is the type of this series
kind SeriesType resourceInd int
// isContainer indicates if the series is a cAdvisor container_ metric, and thus needs special handling groupInd *int
isContainer bool mapper apimeta.RESTMapper
} }
// overridableSeriesRegistry is a basic SeriesRegistry // newLabelGroupResExtractor creates a new labelGroupResExtractor for labels whose form
type basicSeriesRegistry struct { // matches the given template. It does so by creating a regular expression from the template,
mu sync.RWMutex // so anything in the template which limits resource or group name length will cause issues.
func newLabelGroupResExtractor(labelTemplate *template.Template) (*labelGroupResExtractor, error) {
// info maps metric info to information about the corresponding series labelRegexBuff := new(bytes.Buffer)
info map[provider.MetricInfo]seriesInfo if err := labelTemplate.Execute(labelRegexBuff, schema.GroupResource{"(?P<group>.+?)", "(?P<resource>.+?)"}); err != nil {
// metrics is the list of all known metrics return nil, fmt.Errorf("unable to convert label template to matcher: %v", err)
metrics []provider.MetricInfo
// namer is the metricNamer responsible for converting series to metric names and information
namer metricNamer
}
func (r *basicSeriesRegistry) Selectors() []prom.Selector {
// container-specific metrics from cAdvsior have their own form, and need special handling
// TODO: figure out how to determine which metrics on non-namespaced objects are kubernetes-related
containerSel := prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", ""))
namespacedSel := prom.MatchSeries("", prom.LabelNeq(r.namer.labelPrefix+"namespace", ""), prom.NameNotMatches("^container_.*"))
return []prom.Selector{containerSel, namespacedSel}
}
func (r *basicSeriesRegistry) SetSeries(newSeries []prom.Series) error {
newInfo := make(map[provider.MetricInfo]seriesInfo)
for _, series := range newSeries {
if strings.HasPrefix(series.Name, "container_") {
r.namer.processContainerSeries(series, newInfo)
} else if namespaceLabel, hasNamespaceLabel := series.Labels[pmodel.LabelName(r.namer.labelPrefix+"namespace")]; hasNamespaceLabel && namespaceLabel != "" {
// we also handle namespaced metrics here as part of the resource-association logic
if err := r.namer.processNamespacedSeries(series, newInfo); err != nil {
glog.Errorf("Unable to process namespaced series %q: %v", series.Name, err)
continue
}
} else {
if err := r.namer.processRootScopedSeries(series, newInfo); err != nil {
glog.Errorf("Unable to process root-scoped series %q: %v", series.Name, err)
continue
}
}
} }
if labelRegexBuff.Len() == 0 {
newMetrics := make([]provider.MetricInfo, 0, len(newInfo)) return nil, fmt.Errorf("unable to convert label template to matcher: empty template")
for info := range newInfo {
newMetrics = append(newMetrics, info)
} }
labelRegexRaw := "^" + labelRegexBuff.String() + "$"
r.mu.Lock() labelRegex, err := regexp.Compile(labelRegexRaw)
defer r.mu.Unlock()
r.info = newInfo
r.metrics = newMetrics
return nil
}
func (r *basicSeriesRegistry) ListAllMetrics() []provider.MetricInfo {
r.mu.RLock()
defer r.mu.RUnlock()
return r.metrics
}
func (r *basicSeriesRegistry) QueryForMetric(metricInfo provider.MetricInfo, namespace string, resourceNames ...string) (kind SeriesType, query prom.Selector, groupBy string, found bool) {
r.mu.RLock()
defer r.mu.RUnlock()
if len(resourceNames) == 0 {
glog.Errorf("no resource names requested while producing a query for metric %s", metricInfo.String())
return 0, "", "", false
}
metricInfo, singularResource, err := metricInfo.Normalized(r.namer.mapper)
if err != nil { if err != nil {
glog.Errorf("unable to normalize group resource while producing a query: %v", err) return nil, fmt.Errorf("unable to convert label template to matcher: %v", err)
return 0, "", "", false
}
resourceLbl := r.namer.labelPrefix + singularResource
// TODO: support container metrics
if info, found := r.info[metricInfo]; found {
targetValue := resourceNames[0]
matcher := prom.LabelEq
if len(resourceNames) > 1 {
targetValue = strings.Join(resourceNames, "|")
matcher = prom.LabelMatches
}
var expressions []string
if info.isContainer {
expressions = []string{matcher("pod_name", targetValue), prom.LabelNeq("container_name", "POD")}
groupBy = "pod_name"
} else {
// TODO: copy base series labels?
expressions = []string{matcher(resourceLbl, targetValue)}
groupBy = resourceLbl
}
if metricInfo.Namespaced {
prefix := r.namer.labelPrefix
if info.isContainer {
prefix = ""
}
expressions = append(expressions, prom.LabelEq(prefix+"namespace", namespace))
}
return info.kind, prom.MatchSeries(info.baseSeries.Name, expressions...), groupBy, true
} }
glog.V(10).Infof("metric %v not registered", metricInfo) var groupInd *int
return 0, "", "", false var resInd *int
for i, name := range labelRegex.SubexpNames() {
switch name {
case "group":
ind := i // copy to avoid iteration variable reference
groupInd = &ind
case "resource":
ind := i // copy to avoid iteration variable reference
resInd = &ind
}
}
if resInd == nil {
return nil, fmt.Errorf("must include at least `{{.Resource}}` in the label template")
}
return &labelGroupResExtractor{
regex: labelRegex,
resourceInd: *resInd,
groupInd: groupInd,
}, nil
} }
func (r *basicSeriesRegistry) MatchValuesToNames(metricInfo provider.MetricInfo, values pmodel.Vector) (matchedValues map[string]pmodel.SampleValue, found bool) { // GroupResourceForLabel extracts a schema.GroupResource from the given label, if possible.
r.mu.RLock() // The second argument indicates whether or not a potential group-resource was found in this label.
defer r.mu.RUnlock() func (e *labelGroupResExtractor) GroupResourceForLabel(lbl pmodel.LabelName) (schema.GroupResource, bool) {
matchGroups := e.regex.FindStringSubmatch(string(lbl))
metricInfo, singularResource, err := metricInfo.Normalized(r.namer.mapper) if matchGroups != nil {
if err != nil { group := ""
glog.Errorf("unable to normalize group resource while matching values to names: %v", err) if e.groupInd != nil {
return nil, false group = matchGroups[*e.groupInd]
}
resourceLbl := r.namer.labelPrefix + singularResource
if info, found := r.info[metricInfo]; found {
res := make(map[string]pmodel.SampleValue, len(values))
for _, val := range values {
if val == nil {
// skip empty values
continue
}
labelName := pmodel.LabelName(resourceLbl)
if info.isContainer {
labelName = pmodel.LabelName("pod_name")
}
res[string(val.Metric[labelName])] = val.Value
} }
return res, true return schema.GroupResource{
Group: group,
Resource: matchGroups[e.resourceInd],
}, true
} }
return nil, false return schema.GroupResource{}, false
} }
// metricNamer knows how to construct MetricInfo out of raw prometheus series descriptions. func (r *metricNamer) Selector() prom.Selector {
type metricNamer struct { return r.seriesQuery
// overrides contains the list of container metrics whose naming we want to override.
// This is used to properly convert certain cAdvisor container metrics.
overrides map[string]seriesSpec
mapper apimeta.RESTMapper
labelPrefix string
} }
// seriesSpec specifies how to produce metric info for a particular prometheus series source // reMatcher either positively or negatively matches a regex
type seriesSpec struct { type reMatcher struct {
// metricName is the desired output API metric name regex *regexp.Regexp
metricName string positive bool
// kind indicates whether or not this metric is cumulative,
// and thus has to be calculated as a rate when returning it
kind SeriesType
} }
// processContainerSeries performs special work to extract metric definitions func newReMatcher(cfg config.RegexFilter) (*reMatcher, error) {
// from cAdvisor-sourced container metrics, which don't particularly follow any useful conventions consistently. if cfg.Is != "" && cfg.IsNot != "" {
func (n *metricNamer) processContainerSeries(series prom.Series, infos map[provider.MetricInfo]seriesInfo) { return nil, fmt.Errorf("cannot have both an `is` (%q) and `isNot` (%q) expression in a single filter", cfg.Is, cfg.IsNot)
}
if cfg.Is == "" && cfg.IsNot == "" {
return nil, fmt.Errorf("must have either an `is` or `isNot` expression in a filter")
}
originalName := series.Name var positive bool
var regexRaw string
var name string if cfg.Is != "" {
metricKind := GaugeSeries positive = true
if override, hasOverride := n.overrides[series.Name]; hasOverride { regexRaw = cfg.Is
name = override.metricName
metricKind = override.kind
} else { } else {
// chop of the "container_" prefix positive = false
series.Name = series.Name[10:] regexRaw = cfg.IsNot
name, metricKind = n.metricNameFromSeries(series)
} }
info := provider.MetricInfo{ regex, err := regexp.Compile(regexRaw)
GroupResource: schema.GroupResource{Resource: "pods"},
Namespaced: true,
Metric: name,
}
infos[info] = seriesInfo{
kind: metricKind,
baseSeries: prom.Series{Name: originalName},
isContainer: true,
}
}
// processNamespacedSeries adds the metric info for the given generic namespaced series to
// the map of metric info.
func (n *metricNamer) processNamespacedSeries(series prom.Series, infos map[provider.MetricInfo]seriesInfo) error {
// NB: all errors must occur *before* we save the series info
name, metricKind := n.metricNameFromSeries(series)
resources, err := n.groupResourcesFromSeries(series)
if err != nil { if err != nil {
return fmt.Errorf("unable to process prometheus series %s: %v", series.Name, err) return nil, fmt.Errorf("unable to compile series filter %q: %v", regexRaw, err)
} }
// we add one metric for each resource that this could describe return &reMatcher{
for _, resource := range resources { regex: regex,
info := provider.MetricInfo{ positive: positive,
GroupResource: resource, }, nil
Namespaced: true,
Metric: name,
}
// metrics describing namespaces aren't considered to be namespaced
if resource == (schema.GroupResource{Resource: "namespaces"}) {
info.Namespaced = false
}
infos[info] = seriesInfo{
kind: metricKind,
baseSeries: prom.Series{Name: series.Name},
}
}
return nil
} }
// processesRootScopedSeries adds the metric info for the given generic namespaced series to func (m *reMatcher) Matches(val string) bool {
// the map of metric info. return m.regex.MatchString(val) == m.positive
func (n *metricNamer) processRootScopedSeries(series prom.Series, infos map[provider.MetricInfo]seriesInfo) error {
// NB: all errors must occur *before* we save the series info
name, metricKind := n.metricNameFromSeries(series)
resources, err := n.groupResourcesFromSeries(series)
if err != nil {
return fmt.Errorf("unable to process prometheus series %s: %v", series.Name, err)
}
// we add one metric for each resource that this could describe
for _, resource := range resources {
info := provider.MetricInfo{
GroupResource: resource,
Namespaced: false,
Metric: name,
}
infos[info] = seriesInfo{
kind: metricKind,
baseSeries: prom.Series{Name: series.Name},
}
}
return nil
} }
// groupResourceFromSeries collects the possible group-resources that this series could describe by type metricNamer struct {
// going through each label, checking to see if it corresponds to a known resource. For instance, seriesQuery prom.Selector
// a series `ingress_http_hits_total{pod="foo",service="bar",ingress="baz",namespace="ns"}` labelTemplate *template.Template
// would return three GroupResources: "pods", "services", and "ingresses". labelResExtractor *labelGroupResExtractor
// Returned MetricInfo is equilavent to the "normalized" info produced by metricInfo.Normalized. metricsQueryTemplate *template.Template
func (n *metricNamer) groupResourcesFromSeries(series prom.Series) ([]schema.GroupResource, error) { nameMatches *regexp.Regexp
var res []schema.GroupResource nameAs string
for label := range series.Labels { seriesMatchers []*reMatcher
if !strings.HasPrefix(string(label), n.labelPrefix) {
continue labelResourceMu sync.RWMutex
} labelToResource map[pmodel.LabelName]schema.GroupResource
label = label[len(n.labelPrefix):] resourceToLabel map[schema.GroupResource]pmodel.LabelName
// TODO: figure out a way to let people specify a fully-qualified name in label-form mapper apimeta.RESTMapper
gvr, err := n.mapper.ResourceFor(schema.GroupVersionResource{Resource: string(label)}) }
if err != nil {
if apimeta.IsNoMatchError(err) { // queryTemplateArgs are the arguments for the metrics query template.
continue type queryTemplateArgs struct {
Series string
LabelMatchers string
LabelValuesByName map[string][]string
GroupBy string
GroupBySlice []string
}
func (n *metricNamer) FilterSeries(initialSeries []prom.Series) []prom.Series {
if len(n.seriesMatchers) == 0 {
return initialSeries
}
finalSeries := make([]prom.Series, 0, len(initialSeries))
SeriesLoop:
for _, series := range initialSeries {
for _, matcher := range n.seriesMatchers {
if !matcher.Matches(series.Name) {
continue SeriesLoop
} }
return nil, err
} }
res = append(res, gvr.GroupResource()) finalSeries = append(finalSeries, series)
} }
return res, nil return finalSeries
} }
// metricNameFromSeries extracts a metric name from a series name, and indicates func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) {
// whether or not that series was a counter. It also has special logic to deal with time-based var exprs []string
// counters, which general get converted to milli-unit rate metrics. valuesByName := map[string][]string{}
func (n *metricNamer) metricNameFromSeries(series prom.Series) (name string, kind SeriesType) {
kind = GaugeSeries
name = series.Name
if strings.HasSuffix(name, "_total") {
kind = CounterSeries
name = name[:len(name)-6]
if strings.HasSuffix(name, "_seconds") { if namespace != "" {
kind = SecondsCounterSeries namespaceLbl, err := n.LabelForResource(nsGroupResource)
name = name[:len(name)-8] if err != nil {
return "", err
}
exprs = append(exprs, prom.LabelEq(string(namespaceLbl), namespace))
valuesByName[string(namespaceLbl)] = []string{namespace}
}
resourceLbl, err := n.LabelForResource(resource)
if err != nil {
return "", err
}
matcher := prom.LabelEq
targetValue := names[0]
if len(names) > 1 {
matcher = prom.LabelMatches
targetValue = strings.Join(names, "|")
}
exprs = append(exprs, matcher(string(resourceLbl), targetValue))
valuesByName[string(resourceLbl)] = names
args := queryTemplateArgs{
Series: series,
LabelMatchers: strings.Join(exprs, ","),
LabelValuesByName: valuesByName,
GroupBy: string(resourceLbl),
GroupBySlice: []string{string(resourceLbl)},
}
queryBuff := new(bytes.Buffer)
if err := n.metricsQueryTemplate.Execute(queryBuff, args); err != nil {
return "", err
}
if queryBuff.Len() == 0 {
return "", fmt.Errorf("empty query produced by metrics query template")
}
return prom.Selector(queryBuff.String()), nil
}
func (n *metricNamer) ResourcesForSeries(series prom.Series) ([]schema.GroupResource, bool) {
// use an updates map to avoid having to drop the read lock to update the cache
// until the end. Since we'll probably have few updates after the first run,
// this should mean that we rarely have to hold the write lock.
var resources []schema.GroupResource
updates := make(map[pmodel.LabelName]schema.GroupResource)
namespaced := false
// use an anon func to get the right defer behavior
func() {
n.labelResourceMu.RLock()
defer n.labelResourceMu.RUnlock()
for lbl := range series.Labels {
var groupRes schema.GroupResource
var ok bool
// check if we have an override
if groupRes, ok = n.labelToResource[lbl]; ok {
resources = append(resources, groupRes)
} else if groupRes, ok = updates[lbl]; ok {
resources = append(resources, groupRes)
} else if n.labelResExtractor != nil {
// if not, check if it matches the form we expect, and if so,
// convert to a group-resource.
if groupRes, ok = n.labelResExtractor.GroupResourceForLabel(lbl); ok {
info, _, err := provider.CustomMetricInfo{GroupResource: groupRes}.Normalized(n.mapper)
if err != nil {
glog.Errorf("unable to normalize group-resource %s from label %q, skipping: %v", groupRes.String(), lbl, err)
continue
}
groupRes = info.GroupResource
resources = append(resources, groupRes)
updates[lbl] = groupRes
}
}
if groupRes == nsGroupResource {
namespaced = true
}
}
}()
// update the cache for next time. This should only be called by discovery,
// so we don't really have to worry about the grap between read and write locks
// (plus, we don't care if someone else updates the cache first, since the results
// are necessarily the same, so at most we've done extra work).
if len(updates) > 0 {
n.labelResourceMu.Lock()
defer n.labelResourceMu.Unlock()
for lbl, groupRes := range updates {
n.labelToResource[lbl] = groupRes
} }
} }
return return resources, namespaced
}
func (n *metricNamer) LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
n.labelResourceMu.RLock()
// check if we have a cached copy or override
lbl, ok := n.resourceToLabel[resource]
n.labelResourceMu.RUnlock() // release before we call makeLabelForResource
if ok {
return lbl, nil
}
// NB: we don't actually care about the gap between releasing read lock
// and acquiring the write lock -- if we do duplicate work sometimes, so be
// it, as long as we're correct.
// otherwise, use the template and save the result
lbl, err := n.makeLabelForResource(resource)
if err != nil {
return "", fmt.Errorf("unable to convert resource %s into label: %v", resource.String(), err)
}
return lbl, nil
}
// makeLabelForResource constructs a label name for the given resource, and saves the result.
// It must *not* be called under an existing lock.
func (n *metricNamer) makeLabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
if n.labelTemplate == nil {
return "", fmt.Errorf("no generic resource label form specified for this metric")
}
buff := new(bytes.Buffer)
singularRes, err := n.mapper.ResourceSingularizer(resource.Resource)
if err != nil {
return "", fmt.Errorf("unable to singularize resource %s: %v", resource.String(), err)
}
convResource := schema.GroupResource{
Group: groupNameSanitizer.Replace(resource.Group),
Resource: singularRes,
}
if err := n.labelTemplate.Execute(buff, convResource); err != nil {
return "", err
}
if buff.Len() == 0 {
return "", fmt.Errorf("empty label produced by label template")
}
lbl := pmodel.LabelName(buff.String())
n.labelResourceMu.Lock()
defer n.labelResourceMu.Unlock()
n.resourceToLabel[resource] = lbl
n.labelToResource[lbl] = resource
return lbl, nil
}
func (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) {
matches := n.nameMatches.FindStringSubmatchIndex(series.Name)
if matches == nil {
return "", fmt.Errorf("series name %q did not match expected pattern %q", series.Name, n.nameMatches.String())
}
outNameBytes := n.nameMatches.ExpandString(nil, n.nameAs, series.Name, matches)
return string(outNameBytes), nil
}
// NamersFromConfig produces a MetricNamer for each rule in the given config.
func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMapper) ([]MetricNamer, error) {
namers := make([]MetricNamer, len(cfg.Rules))
for i, rule := range cfg.Rules {
var labelTemplate *template.Template
var labelResExtractor *labelGroupResExtractor
var err error
if rule.Resources.Template != "" {
labelTemplate, err = template.New("resource-label").Delims("<<", ">>").Parse(rule.Resources.Template)
if err != nil {
return nil, fmt.Errorf("unable to parse label template %q associated with series query %q: %v", rule.Resources.Template, rule.SeriesQuery, err)
}
labelResExtractor, err = newLabelGroupResExtractor(labelTemplate)
if err != nil {
return nil, fmt.Errorf("unable to generate label format from template %q associated with series query %q: %v", rule.Resources.Template, rule.SeriesQuery, err)
}
}
metricsQueryTemplate, err := template.New("metrics-query").Delims("<<", ">>").Parse(rule.MetricsQuery)
if err != nil {
return nil, fmt.Errorf("unable to parse metrics query template %q associated with series query %q: %v", rule.MetricsQuery, rule.SeriesQuery, err)
}
seriesMatchers := make([]*reMatcher, len(rule.SeriesFilters))
for i, filterRaw := range rule.SeriesFilters {
matcher, err := newReMatcher(filterRaw)
if err != nil {
return nil, fmt.Errorf("unable to generate series name filter associated with series query %q: %v", rule.SeriesQuery, err)
}
seriesMatchers[i] = matcher
}
if rule.Name.Matches != "" {
matcher, err := newReMatcher(config.RegexFilter{Is: rule.Name.Matches})
if err != nil {
return nil, fmt.Errorf("unable to generate series name filter from name rules associated with series query %q: %v", rule.SeriesQuery, err)
}
seriesMatchers = append(seriesMatchers, matcher)
}
var nameMatches *regexp.Regexp
if rule.Name.Matches != "" {
nameMatches, err = regexp.Compile(rule.Name.Matches)
if err != nil {
return nil, fmt.Errorf("unable to compile series name match expression %q associated with series query %q: %v", rule.Name.Matches, rule.SeriesQuery, err)
}
} else {
// this will always succeed
nameMatches = regexp.MustCompile(".*")
}
nameAs := rule.Name.As
if nameAs == "" {
// check if we have an obvious default
subexpNames := nameMatches.SubexpNames()
if len(subexpNames) == 1 {
// no capture groups, use the whole thing
nameAs = "$0"
} else if len(subexpNames) == 2 {
// one capture group, use that
nameAs = "$1"
} else {
return nil, fmt.Errorf("must specify an 'as' value for name matcher %q associated with series query %q", rule.Name.Matches, rule.SeriesQuery)
}
}
namer := &metricNamer{
seriesQuery: prom.Selector(rule.SeriesQuery),
labelTemplate: labelTemplate,
labelResExtractor: labelResExtractor,
metricsQueryTemplate: metricsQueryTemplate,
mapper: mapper,
nameMatches: nameMatches,
nameAs: nameAs,
seriesMatchers: seriesMatchers,
labelToResource: make(map[pmodel.LabelName]schema.GroupResource),
resourceToLabel: make(map[schema.GroupResource]pmodel.LabelName),
}
// invert the structure for consistency with the template
for lbl, groupRes := range rule.Resources.Overrides {
infoRaw := provider.CustomMetricInfo{
GroupResource: schema.GroupResource{
Group: groupRes.Group,
Resource: groupRes.Resource,
},
}
info, _, err := infoRaw.Normalized(mapper)
if err != nil {
return nil, fmt.Errorf("unable to normalize group-resource %v: %v", groupRes, err)
}
namer.labelToResource[pmodel.LabelName(lbl)] = info.GroupResource
namer.resourceToLabel[info.GroupResource] = pmodel.LabelName(lbl)
}
namers[i] = namer
}
return namers, nil
} }

View file

@ -19,9 +19,9 @@ package provider
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/golang/glog"
"time" "time"
"github.com/golang/glog"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
apierr "k8s.io/apimachinery/pkg/api/errors" apierr "k8s.io/apimachinery/pkg/api/errors"
@ -40,42 +40,40 @@ import (
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
) )
// Runnable represents something that can be run until told to stop.
type Runnable interface {
// Run runs the runnable forever.
Run()
// RunUntil runs the runnable until the given channel is closed.
RunUntil(stopChan <-chan struct{})
}
type prometheusProvider struct { type prometheusProvider struct {
mapper apimeta.RESTMapper mapper apimeta.RESTMapper
kubeClient dynamic.ClientPool kubeClient dynamic.Interface
promClient prom.Client promClient prom.Client
SeriesRegistry SeriesRegistry
rateInterval time.Duration
} }
func NewPrometheusProvider(mapper apimeta.RESTMapper, kubeClient dynamic.ClientPool, promClient prom.Client, labelPrefix string, updateInterval time.Duration, rateInterval time.Duration, stopChan <-chan struct{}) provider.CustomMetricsProvider { func NewPrometheusProvider(mapper apimeta.RESTMapper, kubeClient dynamic.Interface, promClient prom.Client, namers []MetricNamer, updateInterval time.Duration) (provider.CustomMetricsProvider, Runnable) {
lister := &cachingMetricsLister{ lister := &cachingMetricsLister{
updateInterval: updateInterval, updateInterval: updateInterval,
promClient: promClient, promClient: promClient,
namers: namers,
SeriesRegistry: &basicSeriesRegistry{ SeriesRegistry: &basicSeriesRegistry{
namer: metricNamer{ mapper: mapper,
// TODO: populate the overrides list
overrides: nil,
mapper: mapper,
labelPrefix: labelPrefix,
},
}, },
} }
lister.RunUntil(stopChan)
return &prometheusProvider{ return &prometheusProvider{
mapper: mapper, mapper: mapper,
kubeClient: kubeClient, kubeClient: kubeClient,
promClient: promClient, promClient: promClient,
SeriesRegistry: lister, SeriesRegistry: lister,
}, lister
rateInterval: rateInterval,
}
} }
func (p *prometheusProvider) metricFor(value pmodel.SampleValue, groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) { func (p *prometheusProvider) metricFor(value pmodel.SampleValue, groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) {
@ -97,7 +95,7 @@ func (p *prometheusProvider) metricFor(value pmodel.SampleValue, groupResource s
}, nil }, nil
} }
func (p *prometheusProvider) metricsFor(valueSet pmodel.Vector, info provider.MetricInfo, list runtime.Object) (*custom_metrics.MetricValueList, error) { func (p *prometheusProvider) metricsFor(valueSet pmodel.Vector, info provider.CustomMetricInfo, list runtime.Object) (*custom_metrics.MetricValueList, error) {
if !apimeta.IsListType(list) { if !apimeta.IsListType(list) {
return nil, apierr.NewInternalError(fmt.Errorf("result of label selector list operation was not a list")) return nil, apierr.NewInternalError(fmt.Errorf("result of label selector list operation was not a list"))
} }
@ -131,30 +129,14 @@ func (p *prometheusProvider) metricsFor(valueSet pmodel.Vector, info provider.Me
}, nil }, nil
} }
func (p *prometheusProvider) buildQuery(info provider.MetricInfo, namespace string, names ...string) (pmodel.Vector, error) { func (p *prometheusProvider) buildQuery(info provider.CustomMetricInfo, namespace string, names ...string) (pmodel.Vector, error) {
kind, baseQuery, groupBy, found := p.QueryForMetric(info, namespace, names...) query, found := p.QueryForMetric(info, namespace, names...)
if !found { if !found {
return nil, provider.NewMetricNotFoundError(info.GroupResource, info.Metric) return nil, provider.NewMetricNotFoundError(info.GroupResource, info.Metric)
} }
fullQuery := baseQuery
switch kind {
case CounterSeries:
fullQuery = prom.Selector(fmt.Sprintf("rate(%s[%s])", baseQuery, pmodel.Duration(p.rateInterval).String()))
case SecondsCounterSeries:
// TODO: futher modify for seconds?
fullQuery = prom.Selector(prom.Selector(fmt.Sprintf("rate(%s[%s])", baseQuery, pmodel.Duration(p.rateInterval).String())))
}
// NB: too small of a rate interval will return no results...
// sum over all other dimensions of this query (e.g. if we select on route, sum across all pods,
// but if we select on pods, sum across all routes), and split by the dimension of our resource
// TODO: return/populate the by list in SeriesForMetric
fullQuery = prom.Selector(fmt.Sprintf("sum(%s) by (%s)", fullQuery, groupBy))
// TODO: use an actual context // TODO: use an actual context
queryResults, err := p.promClient.Query(context.Background(), pmodel.Now(), fullQuery) queryResults, err := p.promClient.Query(context.TODO(), pmodel.Now(), query)
if err != nil { if err != nil {
glog.Errorf("unable to fetch metrics from prometheus: %v", err) glog.Errorf("unable to fetch metrics from prometheus: %v", err)
// don't leak implementation details to the user // don't leak implementation details to the user
@ -169,7 +151,7 @@ func (p *prometheusProvider) buildQuery(info provider.MetricInfo, namespace stri
return *queryResults.Vector, nil return *queryResults.Vector, nil
} }
func (p *prometheusProvider) getSingle(info provider.MetricInfo, namespace, name string) (*custom_metrics.MetricValue, error) { func (p *prometheusProvider) getSingle(info provider.CustomMetricInfo, namespace, name string) (*custom_metrics.MetricValue, error) {
queryResults, err := p.buildQuery(info, namespace, name) queryResults, err := p.buildQuery(info, namespace, name)
if err != nil { if err != nil {
return nil, err return nil, err
@ -197,24 +179,25 @@ func (p *prometheusProvider) getSingle(info provider.MetricInfo, namespace, name
return p.metricFor(resultValue, info.GroupResource, "", name, info.Metric) return p.metricFor(resultValue, info.GroupResource, "", name, info.Metric)
} }
func (p *prometheusProvider) getMultiple(info provider.MetricInfo, namespace string, selector labels.Selector) (*custom_metrics.MetricValueList, error) { func (p *prometheusProvider) getMultiple(info provider.CustomMetricInfo, namespace string, selector labels.Selector) (*custom_metrics.MetricValueList, error) {
// construct a client to list the names of objects matching the label selector fullResources, err := p.mapper.ResourcesFor(info.GroupResource.WithVersion(""))
client, err := p.kubeClient.ClientForGroupVersionResource(info.GroupResource.WithVersion("")) if err == nil && len(fullResources) == 0 {
err = fmt.Errorf("no fully versioned resources known for group-resource %v", info.GroupResource)
}
if err != nil { if err != nil {
glog.Errorf("unable to construct dynamic client to list matching resource names: %v", err) glog.Errorf("unable to find preferred version to list matching resource names: %v", err)
// don't leak implementation details to the user // don't leak implementation details to the user
return nil, apierr.NewInternalError(fmt.Errorf("unable to list matching resources")) return nil, apierr.NewInternalError(fmt.Errorf("unable to list matching resources"))
} }
var client dynamic.ResourceInterface
// we can construct a this APIResource ourself, since the dynamic client only uses Name and Namespaced if namespace != "" {
apiRes := &metav1.APIResource{ client = p.kubeClient.Resource(fullResources[0]).Namespace(namespace)
Name: info.GroupResource.Resource, } else {
Namespaced: info.Namespaced, client = p.kubeClient.Resource(fullResources[0])
} }
// actually list the objects matching the label selector // actually list the objects matching the label selector
matchingObjectsRaw, err := client.Resource(apiRes, namespace). matchingObjectsRaw, err := client.List(metav1.ListOptions{LabelSelector: selector.String()})
List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil { if err != nil {
glog.Errorf("unable to list matching resource names: %v", err) glog.Errorf("unable to list matching resource names: %v", err)
// don't leak implementation details to the user // don't leak implementation details to the user
@ -243,7 +226,7 @@ func (p *prometheusProvider) getMultiple(info provider.MetricInfo, namespace str
} }
func (p *prometheusProvider) GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error) { func (p *prometheusProvider) GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error) {
info := provider.MetricInfo{ info := provider.CustomMetricInfo{
GroupResource: groupResource, GroupResource: groupResource,
Metric: metricName, Metric: metricName,
Namespaced: false, Namespaced: false,
@ -253,7 +236,7 @@ func (p *prometheusProvider) GetRootScopedMetricByName(groupResource schema.Grou
} }
func (p *prometheusProvider) GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) { func (p *prometheusProvider) GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
info := provider.MetricInfo{ info := provider.CustomMetricInfo{
GroupResource: groupResource, GroupResource: groupResource,
Metric: metricName, Metric: metricName,
Namespaced: false, Namespaced: false,
@ -262,7 +245,7 @@ func (p *prometheusProvider) GetRootScopedMetricBySelector(groupResource schema.
} }
func (p *prometheusProvider) GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) { func (p *prometheusProvider) GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) {
info := provider.MetricInfo{ info := provider.CustomMetricInfo{
GroupResource: groupResource, GroupResource: groupResource,
Metric: metricName, Metric: metricName,
Namespaced: true, Namespaced: true,
@ -272,7 +255,7 @@ func (p *prometheusProvider) GetNamespacedMetricByName(groupResource schema.Grou
} }
func (p *prometheusProvider) GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) { func (p *prometheusProvider) GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
info := provider.MetricInfo{ info := provider.CustomMetricInfo{
GroupResource: groupResource, GroupResource: groupResource,
Metric: metricName, Metric: metricName,
Namespaced: true, Namespaced: true,
@ -285,6 +268,7 @@ type cachingMetricsLister struct {
promClient prom.Client promClient prom.Client
updateInterval time.Duration updateInterval time.Duration
namers []MetricNamer
} }
func (l *cachingMetricsLister) Run() { func (l *cachingMetricsLister) Run() {
@ -299,20 +283,65 @@ func (l *cachingMetricsLister) RunUntil(stopChan <-chan struct{}) {
}, l.updateInterval, stopChan) }, l.updateInterval, stopChan)
} }
type selectorSeries struct {
selector prom.Selector
series []prom.Series
}
func (l *cachingMetricsLister) updateMetrics() error { func (l *cachingMetricsLister) updateMetrics() error {
startTime := pmodel.Now().Add(-1 * l.updateInterval) startTime := pmodel.Now().Add(-1 * l.updateInterval)
sels := l.Selectors() // don't do duplicate queries when it's just the matchers that change
seriesCacheByQuery := make(map[prom.Selector][]prom.Series)
// TODO: use an actual context here // these can take a while on large clusters, so launch in parallel
series, err := l.promClient.Series(context.Background(), pmodel.Interval{startTime, 0}, sels...) // and don't duplicate
if err != nil { selectors := make(map[prom.Selector]struct{})
return fmt.Errorf("unable to update list of all available metrics: %v", err) selectorSeriesChan := make(chan selectorSeries, len(l.namers))
errs := make(chan error, len(l.namers))
for _, namer := range l.namers {
sel := namer.Selector()
if _, ok := selectors[sel]; ok {
errs <- nil
selectorSeriesChan <- selectorSeries{}
continue
}
selectors[sel] = struct{}{}
go func() {
series, err := l.promClient.Series(context.TODO(), pmodel.Interval{startTime, 0}, sel)
if err != nil {
errs <- fmt.Errorf("unable to fetch metrics for query %q: %v", sel, err)
return
}
errs <- nil
selectorSeriesChan <- selectorSeries{
selector: sel,
series: series,
}
}()
} }
glog.V(10).Infof("Set available metric list from Prometheus to: %v", series) // iterate through, blocking until we've got all results
for range l.namers {
if err := <-errs; err != nil {
return fmt.Errorf("unable to update list of all metrics: %v", err)
}
if ss := <-selectorSeriesChan; ss.series != nil {
seriesCacheByQuery[ss.selector] = ss.series
}
}
close(errs)
l.SetSeries(series) newSeries := make([][]prom.Series, len(l.namers))
for i, namer := range l.namers {
series, cached := seriesCacheByQuery[namer.Selector()]
if !cached {
return fmt.Errorf("unable to update list of all metrics: no metrics retrieved for query %q", namer.Selector())
}
newSeries[i] = namer.FilterSeries(series)
}
return nil glog.V(10).Infof("Set available metric list from Prometheus to: %v", newSeries)
return l.SetSeries(newSeries, l.namers)
} }

View file

@ -29,6 +29,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
fakedyn "k8s.io/client-go/dynamic/fake" fakedyn "k8s.io/client-go/dynamic/fake"
config "github.com/directxman12/k8s-prometheus-adapter/cmd/config-gen/utils"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
) )
@ -86,20 +87,20 @@ func (c *fakePromClient) QueryRange(_ context.Context, r prom.Range, query prom.
return prom.QueryResult{}, nil return prom.QueryResult{}, nil
} }
func setupPrometheusProvider(t *testing.T, stopCh <-chan struct{}) (provider.CustomMetricsProvider, *fakePromClient) { func setupPrometheusProvider(t *testing.T) (provider.CustomMetricsProvider, *fakePromClient) {
fakeProm := &fakePromClient{} fakeProm := &fakePromClient{}
fakeKubeClient := &fakedyn.FakeClientPool{} fakeKubeClient := &fakedyn.FakeDynamicClient{}
prov := NewPrometheusProvider(restMapper(), fakeKubeClient, fakeProm, "", fakeProviderUpdateInterval, 1*time.Minute, stopCh) cfg := config.DefaultConfig(1*time.Minute, "")
namers, err := NamersFromConfig(cfg, restMapper())
require.NoError(t, err)
prov, _ := NewPrometheusProvider(restMapper(), fakeKubeClient, fakeProm, namers, fakeProviderUpdateInterval)
containerSel := prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", "")) containerSel := prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", ""))
namespacedSel := prom.MatchSeries("", prom.LabelNeq("namespace", ""), prom.NameNotMatches("^container_.*")) namespacedSel := prom.MatchSeries("", prom.LabelNeq("namespace", ""), prom.NameNotMatches("^container_.*"))
fakeProm.series = map[prom.Selector][]prom.Series{ fakeProm.series = map[prom.Selector][]prom.Series{
containerSel: { containerSel: {
{
Name: "container_actually_gauge_seconds_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
{ {
Name: "container_some_usage", Name: "container_some_usage",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"}, Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
@ -130,28 +131,24 @@ func setupPrometheusProvider(t *testing.T, stopCh <-chan struct{}) (provider.Cus
func TestListAllMetrics(t *testing.T) { func TestListAllMetrics(t *testing.T) {
// setup // setup
stopCh := make(chan struct{}) prov, fakeProm := setupPrometheusProvider(t)
defer close(stopCh)
prov, fakeProm := setupPrometheusProvider(t, stopCh)
// assume we have no updates // assume we have no updates
require.Len(t, prov.ListAllMetrics(), 0, "assume: should have no metrics updates at the start") require.Len(t, prov.ListAllMetrics(), 0, "assume: should have no metrics updates at the start")
// set the acceptible interval (now until the next update, with a bit of wiggle room) // set the acceptible interval (now until the next update, with a bit of wiggle room)
startTime := pmodel.Now() startTime := pmodel.Now().Add(-1*fakeProviderUpdateInterval - fakeProviderUpdateInterval/10)
endTime := startTime.Add(fakeProviderUpdateInterval + fakeProviderUpdateInterval/10) fakeProm.acceptibleInterval = pmodel.Interval{Start: startTime, End: 0}
fakeProm.acceptibleInterval = pmodel.Interval{Start: startTime, End: endTime}
// wait one update interval (with a bit of wiggle room) // update the metrics (without actually calling RunUntil, so we can avoid timing issues)
time.Sleep(fakeProviderUpdateInterval + fakeProviderUpdateInterval/10) lister := prov.(*prometheusProvider).SeriesRegistry.(*cachingMetricsLister)
require.NoError(t, lister.updateMetrics())
// list/sort the metrics // list/sort the metrics
actualMetrics := prov.ListAllMetrics() actualMetrics := prov.ListAllMetrics()
sort.Sort(metricInfoSorter(actualMetrics)) sort.Sort(metricInfoSorter(actualMetrics))
expectedMetrics := []provider.MetricInfo{ expectedMetrics := []provider.CustomMetricInfo{
{schema.GroupResource{Resource: "pods"}, true, "actually_gauge"},
{schema.GroupResource{Resource: "pods"}, true, "some_usage"},
{schema.GroupResource{Resource: "services"}, true, "ingress_hits"}, {schema.GroupResource{Resource: "services"}, true, "ingress_hits"},
{schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"}, {schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"},
{schema.GroupResource{Resource: "pods"}, true, "ingress_hits"}, {schema.GroupResource{Resource: "pods"}, true, "ingress_hits"},
@ -160,6 +157,8 @@ func TestListAllMetrics(t *testing.T) {
{schema.GroupResource{Resource: "namespaces"}, false, "service_proxy_packets"}, {schema.GroupResource{Resource: "namespaces"}, false, "service_proxy_packets"},
{schema.GroupResource{Group: "extensions", Resource: "deployments"}, true, "work_queue_wait"}, {schema.GroupResource{Group: "extensions", Resource: "deployments"}, true, "work_queue_wait"},
{schema.GroupResource{Resource: "namespaces"}, false, "work_queue_wait"}, {schema.GroupResource{Resource: "namespaces"}, false, "work_queue_wait"},
{schema.GroupResource{Resource: "namespaces"}, false, "some_usage"},
{schema.GroupResource{Resource: "pods"}, true, "some_usage"},
} }
sort.Sort(metricInfoSorter(expectedMetrics)) sort.Sort(metricInfoSorter(expectedMetrics))

View file

@ -0,0 +1,198 @@
/*
Copyright 2017 The Kubernetes Authors.
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 provider
import (
"fmt"
"sync"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
apimeta "k8s.io/apimachinery/pkg/api/meta"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
"github.com/golang/glog"
pmodel "github.com/prometheus/common/model"
)
// NB: container metrics sourced from cAdvisor don't consistently follow naming conventions,
// so we need to whitelist them and handle them on a case-by-case basis. Metrics ending in `_total`
// *should* be counters, but may actually be guages in this case.
// SeriesType represents the kind of series backing a metric.
type SeriesType int
const (
CounterSeries SeriesType = iota
SecondsCounterSeries
GaugeSeries
)
// SeriesRegistry provides conversions between Prometheus series and MetricInfo
type SeriesRegistry interface {
// SetSeries replaces the known series in this registry.
// Each slice in series should correspond to a MetricNamer in namers.
SetSeries(series [][]prom.Series, namers []MetricNamer) error
// ListAllMetrics lists all metrics known to this registry
ListAllMetrics() []provider.CustomMetricInfo
// SeriesForMetric looks up the minimum required series information to make a query for the given metric
// against the given resource (namespace may be empty for non-namespaced resources)
QueryForMetric(info provider.CustomMetricInfo, namespace string, resourceNames ...string) (query prom.Selector, found bool)
// MatchValuesToNames matches result values to resource names for the given metric and value set
MatchValuesToNames(metricInfo provider.CustomMetricInfo, values pmodel.Vector) (matchedValues map[string]pmodel.SampleValue, found bool)
}
type seriesInfo struct {
// seriesName is the name of the corresponding Prometheus series
seriesName string
// namer is the MetricNamer used to name this series
namer MetricNamer
}
// overridableSeriesRegistry is a basic SeriesRegistry
type basicSeriesRegistry struct {
mu sync.RWMutex
// info maps metric info to information about the corresponding series
info map[provider.CustomMetricInfo]seriesInfo
// metrics is the list of all known metrics
metrics []provider.CustomMetricInfo
mapper apimeta.RESTMapper
}
func (r *basicSeriesRegistry) SetSeries(newSeriesSlices [][]prom.Series, namers []MetricNamer) error {
if len(newSeriesSlices) != len(namers) {
return fmt.Errorf("need one set of series per namer")
}
newInfo := make(map[provider.CustomMetricInfo]seriesInfo)
for i, newSeries := range newSeriesSlices {
namer := namers[i]
for _, series := range newSeries {
// TODO: warn if it doesn't match any resources
resources, namespaced := namer.ResourcesForSeries(series)
name, err := namer.MetricNameForSeries(series)
if err != nil {
glog.Errorf("unable to name series %q, skipping: %v", series.String(), err)
continue
}
for _, resource := range resources {
info := provider.CustomMetricInfo{
GroupResource: resource,
Namespaced: namespaced,
Metric: name,
}
// namespace metrics aren't counted as namespaced
if resource == nsGroupResource {
info.Namespaced = false
}
// we don't need to re-normalize, because the metric namer should have already normalized for us
newInfo[info] = seriesInfo{
seriesName: series.Name,
namer: namer,
}
}
}
}
// regenerate metrics
newMetrics := make([]provider.CustomMetricInfo, 0, len(newInfo))
for info := range newInfo {
newMetrics = append(newMetrics, info)
}
r.mu.Lock()
defer r.mu.Unlock()
r.info = newInfo
r.metrics = newMetrics
return nil
}
func (r *basicSeriesRegistry) ListAllMetrics() []provider.CustomMetricInfo {
r.mu.RLock()
defer r.mu.RUnlock()
return r.metrics
}
func (r *basicSeriesRegistry) QueryForMetric(metricInfo provider.CustomMetricInfo, namespace string, resourceNames ...string) (prom.Selector, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
if len(resourceNames) == 0 {
glog.Errorf("no resource names requested while producing a query for metric %s", metricInfo.String())
return "", false
}
metricInfo, _, err := metricInfo.Normalized(r.mapper)
if err != nil {
glog.Errorf("unable to normalize group resource while producing a query: %v", err)
return "", false
}
info, infoFound := r.info[metricInfo]
if !infoFound {
glog.V(10).Infof("metric %v not registered", metricInfo)
return "", false
}
query, err := info.namer.QueryForSeries(info.seriesName, metricInfo.GroupResource, namespace, resourceNames...)
if err != nil {
glog.Errorf("unable to construct query for metric %s: %v", metricInfo.String(), err)
return "", false
}
return query, true
}
func (r *basicSeriesRegistry) MatchValuesToNames(metricInfo provider.CustomMetricInfo, values pmodel.Vector) (matchedValues map[string]pmodel.SampleValue, found bool) {
r.mu.RLock()
defer r.mu.RUnlock()
metricInfo, _, err := metricInfo.Normalized(r.mapper)
if err != nil {
glog.Errorf("unable to normalize group resource while matching values to names: %v", err)
return nil, false
}
info, infoFound := r.info[metricInfo]
if !infoFound {
return nil, false
}
resourceLbl, err := info.namer.LabelForResource(metricInfo.GroupResource)
if err != nil {
glog.Errorf("unable to construct resource label for metric %s: %v", metricInfo.String(), err)
return nil, false
}
res := make(map[string]pmodel.SampleValue, len(values))
for _, val := range values {
if val == nil {
// skip empty values
continue
}
res[string(val.Metric[resourceLbl])] = val.Value
}
return res, true
}

View file

@ -19,6 +19,7 @@ package provider
import ( import (
"sort" "sort"
"testing" "testing"
"time"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
@ -29,13 +30,14 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
config "github.com/directxman12/k8s-prometheus-adapter/cmd/config-gen/utils"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
) )
// restMapper creates a RESTMapper with just the types we need for // restMapper creates a RESTMapper with just the types we need for
// these tests. // these tests.
func restMapper() apimeta.RESTMapper { func restMapper() apimeta.RESTMapper {
mapper := apimeta.NewDefaultRESTMapper([]schema.GroupVersion{coreapi.SchemeGroupVersion}, apimeta.InterfacesForUnstructured) mapper := apimeta.NewDefaultRESTMapper([]schema.GroupVersion{coreapi.SchemeGroupVersion})
mapper.Add(coreapi.SchemeGroupVersion.WithKind("Pod"), apimeta.RESTScopeNamespace) mapper.Add(coreapi.SchemeGroupVersion.WithKind("Pod"), apimeta.RESTScopeNamespace)
mapper.Add(coreapi.SchemeGroupVersion.WithKind("Service"), apimeta.RESTScopeNamespace) mapper.Add(coreapi.SchemeGroupVersion.WithKind("Service"), apimeta.RESTScopeNamespace)
@ -49,122 +51,46 @@ func restMapper() apimeta.RESTMapper {
return mapper return mapper
} }
func setupMetricNamer(t *testing.T) *metricNamer { func setupMetricNamer(t testing.TB) []MetricNamer {
return &metricNamer{ cfg := config.DefaultConfig(1*time.Minute, "kube_")
overrides: map[string]seriesSpec{ namers, err := NamersFromConfig(cfg, restMapper())
"container_actually_gauge_seconds_total": { require.NoError(t, err)
metricName: "actually_gauge", return namers
kind: GaugeSeries,
},
},
labelPrefix: "kube_",
mapper: restMapper(),
}
} }
func TestMetricNamerContainerSeries(t *testing.T) { var seriesRegistryTestSeries = [][]prom.Series{
testCases := []struct { // container series
input prom.Series {
outputMetricName string
outputInfo seriesInfo
}{
{
input: prom.Series{
Name: "container_actually_gauge_seconds_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
outputMetricName: "actually_gauge",
outputInfo: seriesInfo{
baseSeries: prom.Series{Name: "container_actually_gauge_seconds_total"},
kind: GaugeSeries,
isContainer: true,
},
},
{
input: prom.Series{
Name: "container_some_usage",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
outputMetricName: "some_usage",
outputInfo: seriesInfo{
baseSeries: prom.Series{Name: "container_some_usage"},
kind: GaugeSeries,
isContainer: true,
},
},
{
input: prom.Series{
Name: "container_some_count_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
outputMetricName: "some_count",
outputInfo: seriesInfo{
baseSeries: prom.Series{Name: "container_some_count_total"},
kind: CounterSeries,
isContainer: true,
},
},
{
input: prom.Series{
Name: "container_some_time_seconds_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
outputMetricName: "some_time",
outputInfo: seriesInfo{
baseSeries: prom.Series{Name: "container_some_time_seconds_total"},
kind: SecondsCounterSeries,
isContainer: true,
},
},
}
assert := assert.New(t)
namer := setupMetricNamer(t)
resMap := map[provider.MetricInfo]seriesInfo{}
for _, test := range testCases {
namer.processContainerSeries(test.input, resMap)
metric := provider.MetricInfo{
Metric: test.outputMetricName,
GroupResource: schema.GroupResource{Resource: "pods"},
Namespaced: true,
}
if assert.Contains(resMap, metric) {
assert.Equal(test.outputInfo, resMap[metric])
}
}
}
func TestSeriesRegistry(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
namer := setupMetricNamer(t)
registry := &basicSeriesRegistry{
namer: *namer,
}
inputSeries := []prom.Series{
// container series
{
Name: "container_actually_gauge_seconds_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
{
Name: "container_some_usage",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
{
Name: "container_some_count_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
{ {
Name: "container_some_time_seconds_total", Name: "container_some_time_seconds_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"}, Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
}, },
// namespaced series },
// a series that should turn into multiple metrics {
{
Name: "container_some_count_total",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
},
{
{
Name: "container_some_usage",
Labels: pmodel.LabelSet{"pod_name": "somepod", "namespace": "somens", "container_name": "somecont"},
},
},
{
// guage metrics
{
Name: "node_gigawatts",
Labels: pmodel.LabelSet{"kube_node": "somenode"},
},
{
Name: "service_proxy_packets",
Labels: pmodel.LabelSet{"kube_service": "somesvc", "kube_namespace": "somens"},
},
},
{
// cumulative --> rate metrics
{ {
Name: "ingress_hits_total", Name: "ingress_hits_total",
Labels: pmodel.LabelSet{"kube_ingress": "someingress", "kube_service": "somesvc", "kube_pod": "backend1", "kube_namespace": "somens"}, Labels: pmodel.LabelSet{"kube_ingress": "someingress", "kube_service": "somesvc", "kube_pod": "backend1", "kube_namespace": "somens"},
@ -174,191 +100,151 @@ func TestSeriesRegistry(t *testing.T) {
Labels: pmodel.LabelSet{"kube_ingress": "someingress", "kube_service": "somesvc", "kube_pod": "backend2", "kube_namespace": "somens"}, Labels: pmodel.LabelSet{"kube_ingress": "someingress", "kube_service": "somesvc", "kube_pod": "backend2", "kube_namespace": "somens"},
}, },
{ {
Name: "service_proxy_packets", Name: "volume_claims_total",
Labels: pmodel.LabelSet{"kube_service": "somesvc", "kube_namespace": "somens"}, Labels: pmodel.LabelSet{"kube_persistentvolume": "somepv"},
}, },
},
{
// cumulative seconds --> rate metrics
{ {
Name: "work_queue_wait_seconds_total", Name: "work_queue_wait_seconds_total",
Labels: pmodel.LabelSet{"kube_deployment": "somedep", "kube_namespace": "somens"}, Labels: pmodel.LabelSet{"kube_deployment": "somedep", "kube_namespace": "somens"},
}, },
// non-namespaced series
{
Name: "node_gigawatts",
Labels: pmodel.LabelSet{"kube_node": "somenode"},
},
{
Name: "volume_claims_total",
Labels: pmodel.LabelSet{"kube_persistentvolume": "somepv"},
},
{ {
Name: "node_fan_seconds_total", Name: "node_fan_seconds_total",
Labels: pmodel.LabelSet{"kube_node": "somenode"}, Labels: pmodel.LabelSet{"kube_node": "somenode"},
}, },
// unrelated series },
{ }
Name: "admin_coffee_liters_total",
Labels: pmodel.LabelSet{"admin": "some-admin"}, func TestSeriesRegistry(t *testing.T) {
}, assert := assert.New(t)
{ require := require.New(t)
Name: "admin_unread_emails",
Labels: pmodel.LabelSet{"admin": "some-admin"}, namers := setupMetricNamer(t)
}, registry := &basicSeriesRegistry{
{ mapper: restMapper(),
Name: "admin_reddit_seconds_total",
Labels: pmodel.LabelSet{"kube_admin": "some-admin"},
},
} }
// set up the registry // set up the registry
require.NoError(registry.SetSeries(inputSeries)) require.NoError(registry.SetSeries(seriesRegistryTestSeries, namers))
// make sure each metric got registered and can form queries // make sure each metric got registered and can form queries
testCases := []struct { testCases := []struct {
title string title string
info provider.MetricInfo info provider.CustomMetricInfo
namespace string namespace string
resourceNames []string resourceNames []string
expectedKind SeriesType expectedQuery string
expectedQuery string
expectedGroupBy string
}{ }{
// container metrics // container metrics
{
title: "container metrics overrides / single resource name",
info: provider.MetricInfo{schema.GroupResource{Resource: "pods"}, true, "actually_gauge"},
namespace: "somens",
resourceNames: []string{"somepod"},
expectedKind: GaugeSeries,
expectedQuery: "container_actually_gauge_seconds_total{pod_name=\"somepod\",container_name!=\"POD\",namespace=\"somens\"}",
expectedGroupBy: "pod_name",
},
{ {
title: "container metrics gauge / multiple resource names", title: "container metrics gauge / multiple resource names",
info: provider.MetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_usage"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_usage"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"somepod1", "somepod2"}, resourceNames: []string{"somepod1", "somepod2"},
expectedKind: GaugeSeries, expectedQuery: "sum(container_some_usage{namespace=\"somens\",pod_name=~\"somepod1|somepod2\",container_name!=\"POD\"}) by (pod_name)",
expectedQuery: "container_some_usage{pod_name=~\"somepod1|somepod2\",container_name!=\"POD\",namespace=\"somens\"}",
expectedGroupBy: "pod_name",
}, },
{ {
title: "container metrics counter", title: "container metrics counter",
info: provider.MetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_count"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_count"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"somepod1", "somepod2"}, resourceNames: []string{"somepod1", "somepod2"},
expectedKind: CounterSeries, expectedQuery: "sum(rate(container_some_count_total{namespace=\"somens\",pod_name=~\"somepod1|somepod2\",container_name!=\"POD\"}[1m])) by (pod_name)",
expectedQuery: "container_some_count_total{pod_name=~\"somepod1|somepod2\",container_name!=\"POD\",namespace=\"somens\"}",
expectedGroupBy: "pod_name",
}, },
{ {
title: "container metrics seconds counter", title: "container metrics seconds counter",
info: provider.MetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_time"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_time"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"somepod1", "somepod2"}, resourceNames: []string{"somepod1", "somepod2"},
expectedKind: SecondsCounterSeries, expectedQuery: "sum(rate(container_some_time_seconds_total{namespace=\"somens\",pod_name=~\"somepod1|somepod2\",container_name!=\"POD\"}[1m])) by (pod_name)",
expectedQuery: "container_some_time_seconds_total{pod_name=~\"somepod1|somepod2\",container_name!=\"POD\",namespace=\"somens\"}",
expectedGroupBy: "pod_name",
}, },
// namespaced metrics // namespaced metrics
{ {
title: "namespaced metrics counter / multidimensional (service)", title: "namespaced metrics counter / multidimensional (service)",
info: provider.MetricInfo{schema.GroupResource{Resource: "service"}, true, "ingress_hits"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "service"}, true, "ingress_hits"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"somesvc"}, resourceNames: []string{"somesvc"},
expectedKind: CounterSeries, expectedQuery: "sum(rate(ingress_hits_total{kube_namespace=\"somens\",kube_service=\"somesvc\"}[1m])) by (kube_service)",
expectedQuery: "ingress_hits_total{kube_service=\"somesvc\",kube_namespace=\"somens\"}",
}, },
{ {
title: "namespaced metrics counter / multidimensional (ingress)", title: "namespaced metrics counter / multidimensional (ingress)",
info: provider.MetricInfo{schema.GroupResource{Group: "extensions", Resource: "ingress"}, true, "ingress_hits"}, info: provider.CustomMetricInfo{schema.GroupResource{Group: "extensions", Resource: "ingress"}, true, "ingress_hits"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"someingress"}, resourceNames: []string{"someingress"},
expectedKind: CounterSeries, expectedQuery: "sum(rate(ingress_hits_total{kube_namespace=\"somens\",kube_ingress=\"someingress\"}[1m])) by (kube_ingress)",
expectedQuery: "ingress_hits_total{kube_ingress=\"someingress\",kube_namespace=\"somens\"}",
}, },
{ {
title: "namespaced metrics counter / multidimensional (pod)", title: "namespaced metrics counter / multidimensional (pod)",
info: provider.MetricInfo{schema.GroupResource{Resource: "pod"}, true, "ingress_hits"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "pod"}, true, "ingress_hits"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"somepod"}, resourceNames: []string{"somepod"},
expectedKind: CounterSeries, expectedQuery: "sum(rate(ingress_hits_total{kube_namespace=\"somens\",kube_pod=\"somepod\"}[1m])) by (kube_pod)",
expectedQuery: "ingress_hits_total{kube_pod=\"somepod\",kube_namespace=\"somens\"}",
}, },
{ {
title: "namespaced metrics gauge", title: "namespaced metrics gauge",
info: provider.MetricInfo{schema.GroupResource{Resource: "service"}, true, "service_proxy_packets"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "service"}, true, "service_proxy_packets"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"somesvc"}, resourceNames: []string{"somesvc"},
expectedKind: GaugeSeries, expectedQuery: "sum(service_proxy_packets{kube_namespace=\"somens\",kube_service=\"somesvc\"}) by (kube_service)",
expectedQuery: "service_proxy_packets{kube_service=\"somesvc\",kube_namespace=\"somens\"}",
}, },
{ {
title: "namespaced metrics seconds counter", title: "namespaced metrics seconds counter",
info: provider.MetricInfo{schema.GroupResource{Group: "extensions", Resource: "deployment"}, true, "work_queue_wait"}, info: provider.CustomMetricInfo{schema.GroupResource{Group: "extensions", Resource: "deployment"}, true, "work_queue_wait"},
namespace: "somens", namespace: "somens",
resourceNames: []string{"somedep"}, resourceNames: []string{"somedep"},
expectedKind: SecondsCounterSeries, expectedQuery: "sum(rate(work_queue_wait_seconds_total{kube_namespace=\"somens\",kube_deployment=\"somedep\"}[1m])) by (kube_deployment)",
expectedQuery: "work_queue_wait_seconds_total{kube_deployment=\"somedep\",kube_namespace=\"somens\"}",
}, },
// non-namespaced series // non-namespaced series
{ {
title: "root scoped metrics gauge", title: "root scoped metrics gauge",
info: provider.MetricInfo{schema.GroupResource{Resource: "node"}, false, "node_gigawatts"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "node"}, false, "node_gigawatts"},
resourceNames: []string{"somenode"}, resourceNames: []string{"somenode"},
expectedKind: GaugeSeries, expectedQuery: "sum(node_gigawatts{kube_node=\"somenode\"}) by (kube_node)",
expectedQuery: "node_gigawatts{kube_node=\"somenode\"}",
}, },
{ {
title: "root scoped metrics counter", title: "root scoped metrics counter",
info: provider.MetricInfo{schema.GroupResource{Resource: "persistentvolume"}, false, "volume_claims"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "persistentvolume"}, false, "volume_claims"},
resourceNames: []string{"somepv"}, resourceNames: []string{"somepv"},
expectedKind: CounterSeries, expectedQuery: "sum(rate(volume_claims_total{kube_persistentvolume=\"somepv\"}[1m])) by (kube_persistentvolume)",
expectedQuery: "volume_claims_total{kube_persistentvolume=\"somepv\"}",
}, },
{ {
title: "root scoped metrics seconds counter", title: "root scoped metrics seconds counter",
info: provider.MetricInfo{schema.GroupResource{Resource: "node"}, false, "node_fan"}, info: provider.CustomMetricInfo{schema.GroupResource{Resource: "node"}, false, "node_fan"},
resourceNames: []string{"somenode"}, resourceNames: []string{"somenode"},
expectedKind: SecondsCounterSeries, expectedQuery: "sum(rate(node_fan_seconds_total{kube_node=\"somenode\"}[1m])) by (kube_node)",
expectedQuery: "node_fan_seconds_total{kube_node=\"somenode\"}",
}, },
} }
for _, testCase := range testCases { for _, testCase := range testCases {
outputKind, outputQuery, groupBy, found := registry.QueryForMetric(testCase.info, testCase.namespace, testCase.resourceNames...) outputQuery, found := registry.QueryForMetric(testCase.info, testCase.namespace, testCase.resourceNames...)
if !assert.True(found, "%s: metric %v should available", testCase.title, testCase.info) { if !assert.True(found, "%s: metric %v should available", testCase.title, testCase.info) {
continue continue
} }
assert.Equal(testCase.expectedKind, outputKind, "%s: metric %v should have had the right series type", testCase.title, testCase.info)
assert.Equal(prom.Selector(testCase.expectedQuery), outputQuery, "%s: metric %v should have produced the correct query for %v in namespace %s", testCase.title, testCase.info, testCase.resourceNames, testCase.namespace) assert.Equal(prom.Selector(testCase.expectedQuery), outputQuery, "%s: metric %v should have produced the correct query for %v in namespace %s", testCase.title, testCase.info, testCase.resourceNames, testCase.namespace)
expectedGroupBy := testCase.expectedGroupBy
if expectedGroupBy == "" {
expectedGroupBy = registry.namer.labelPrefix + testCase.info.GroupResource.Resource
}
assert.Equal(expectedGroupBy, groupBy, "%s: metric %v should have produced the correct groupBy clause", testCase.title, testCase.info)
} }
allMetrics := registry.ListAllMetrics() allMetrics := registry.ListAllMetrics()
expectedMetrics := []provider.MetricInfo{ expectedMetrics := []provider.CustomMetricInfo{
{schema.GroupResource{Resource: "pods"}, true, "actually_gauge"},
{schema.GroupResource{Resource: "pods"}, true, "some_usage"},
{schema.GroupResource{Resource: "pods"}, true, "some_count"}, {schema.GroupResource{Resource: "pods"}, true, "some_count"},
{schema.GroupResource{Resource: "namespaces"}, false, "some_count"},
{schema.GroupResource{Resource: "pods"}, true, "some_time"}, {schema.GroupResource{Resource: "pods"}, true, "some_time"},
{schema.GroupResource{Resource: "namespaces"}, false, "some_time"},
{schema.GroupResource{Resource: "pods"}, true, "some_usage"},
{schema.GroupResource{Resource: "namespaces"}, false, "some_usage"},
{schema.GroupResource{Resource: "services"}, true, "ingress_hits"}, {schema.GroupResource{Resource: "services"}, true, "ingress_hits"},
{schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"}, {schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"},
{schema.GroupResource{Resource: "pods"}, true, "ingress_hits"}, {schema.GroupResource{Resource: "pods"}, true, "ingress_hits"},
@ -379,8 +265,32 @@ func TestSeriesRegistry(t *testing.T) {
assert.Equal(expectedMetrics, allMetrics, "should have listed all expected metrics") assert.Equal(expectedMetrics, allMetrics, "should have listed all expected metrics")
} }
// metricInfoSorter is a sort.Interface for sorting provider.MetricInfos func BenchmarkSetSeries(b *testing.B) {
type metricInfoSorter []provider.MetricInfo namers := setupMetricNamer(b)
registry := &basicSeriesRegistry{
mapper: restMapper(),
}
numDuplicates := 10000
newSeriesSlices := make([][]prom.Series, len(seriesRegistryTestSeries))
for i, seriesSlice := range seriesRegistryTestSeries {
newSlice := make([]prom.Series, len(seriesSlice)*numDuplicates)
for j, series := range seriesSlice {
for k := 0; k < numDuplicates; k++ {
newSlice[j*numDuplicates+k] = series
}
}
newSeriesSlices[i] = newSlice
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry.SetSeries(newSeriesSlices, namers)
}
}
// metricInfoSorter is a sort.Interface for sorting provider.CustomMetricInfos
type metricInfoSorter []provider.CustomMetricInfo
func (s metricInfoSorter) Len() int { func (s metricInfoSorter) Len() int {
return len(s) return len(s)