diff --git a/.gitignore b/.gitignore index aac67cf..ef9fe64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .gobuild bin arangodb-exporter - +vendor diff --git a/Dockerfile.scratch b/Dockerfile.scratch index e7b506c..a8e8d63 100644 --- a/Dockerfile.scratch +++ b/Dockerfile.scratch @@ -1,4 +1,5 @@ -FROM scratch +ARG BASE_IMAGE=scratch +FROM ${BASE_IMAGE} ARG VERSION LABEL name="arangodb-exporter" \ diff --git a/Dockerfile.ubi b/Dockerfile.ubi new file mode 100644 index 0000000..5b4e5c2 --- /dev/null +++ b/Dockerfile.ubi @@ -0,0 +1,4 @@ +ARG IMAGE=registry.access.redhat.com/ubi8/ubi-minimal:8.0 +FROM ${IMAGE} + +RUN microdnf update && microdnf clean all \ No newline at end of file diff --git a/Makefile b/Makefile index 3ec4378..c99f45e 100644 --- a/Makefile +++ b/Makefile @@ -87,17 +87,24 @@ $(GOBUILDDIR): run-tests: go test $(REPOPATH) +docker-ubi-base: check-vars + docker build --no-cache -t $(DOCKERIMAGE)-base-image-ubi -f Dockerfile.ubi . + +docker-ubi: docker-ubi-base build + for arch in amd64; do \ + docker build --build-arg "GOARCH=$$arch" --build-arg "VERSION=$(VERSION_MAJOR_MINOR_PATCH)" -t $(DOCKERIMAGE)-ubi --build-arg "BASE_IMAGE=$(DOCKERIMAGE)-base-image-ubi" -f Dockerfile.scratch . ; \ + docker push $(DOCKERIMAGE)-ubi ; \ + done + +ifndef IGNORE_UBI +docker: docker-ubi +endif + docker: check-vars build for arch in $(ARCHS); do \ docker build --build-arg "GOARCH=$$arch" --build-arg "VERSION=$(VERSION_MAJOR_MINOR_PATCH)" -t $(DOCKERIMAGE)-$$arch -f Dockerfile.scratch . ; \ docker push $(DOCKERIMAGE)-$$arch ; \ done - for arch in amd64; do \ - sed -e 's|FROM scratch|FROM $(UBI)|' Dockerfile.scratch > Dockerfile.ubi ; \ - docker build --build-arg "GOARCH=$$arch" --build-arg "VERSION=$(VERSION_MAJOR_MINOR_PATCH)" -t $(DOCKERIMAGE)-ubi -f Dockerfile.ubi . ; \ - rm -f Dockerfile.ubi ; \ - docker push $(DOCKERIMAGE)-ubi ; \ - done DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create --amend $(DOCKERIMAGE) $(foreach arch,$(ARCHS),$(DOCKERIMAGE)-$(arch)) $(DOCKERIMAGE)-ubi DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push $(DOCKERIMAGE) diff --git a/README.md b/README.md index 1b9f8c0..4d40334 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,20 @@ This results in an ArangoDB Exporter exposing all statistics of the ArangoDB server (running at `http://:8529`) at `http://:9101/metrics`. +## Exporter modes + +### internal + +Use internal metrics exporter mode for ArangoDB < 3.6.0 + +In this mode metrics are calculated on ArangoDB Exporter side + +### passthru + +Expose ArangoDB metrics for ArangoDB >= 3.6.0 + +In this mode metrics provided by ArangoDB `_admin/metrics` are exposed on Exporter port. + ## Running in Docker To run the ArangoDB Exporter in docker, use an image such as diff --git a/main.go b/main.go index 4ceae7d..663b598 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,13 @@ import ( "github.com/spf13/cobra" ) +type ExporterMode string + +const ( + ModeInternal ExporterMode = "internal" + ModePassthru ExporterMode = "passthru" +) + var ( projectVersion = "dev" projectBuild = "dev" @@ -50,6 +57,7 @@ var ( serverOptions ServerConfig arangodbOptions struct { endpoint string + mode string jwtSecret string jwtFile string timeout time.Duration @@ -67,6 +75,8 @@ func init() { f.StringVar(&arangodbOptions.jwtFile, "arangodb.jwt-file", "", "File containing the JWT for authentication with ArangoDB server") f.DurationVar(&arangodbOptions.timeout, "arangodb.timeout", time.Second*15, "Timeout of statistics requests for ArangoDB") + f.StringVar(&arangodbOptions.mode, "mode", "internal", "Mode for ArangoDB exporter. Internal - use internal, old mode of metrics calculation (default). Passthru - expose ArangoD metrics directly, using proper authentication.") + f.MarkDeprecated("arangodb.jwtsecret", "please use --arangodb.jwt-file instead") } @@ -91,20 +101,28 @@ func cmdMainRun(cmd *cobra.Command, args []string) { log.Fatal(err) } } - - exporter, err := NewExporter(arangodbOptions.endpoint, token, false, arangodbOptions.timeout) - if err != nil { - log.Fatal(err) + mux := http.NewServeMux() + switch ExporterMode(arangodbOptions.mode) { + case ModePassthru: + passthru, err := NewPassthru(arangodbOptions.endpoint, token, false, arangodbOptions.timeout) + if err != nil { + log.Fatal(err) + } + mux.Handle("/metrics", passthru) + default: + exporter, err := NewExporter(arangodbOptions.endpoint, token, false, arangodbOptions.timeout) + if err != nil { + log.Fatal(err) + } + prometheus.MustRegister(exporter) + version.Version = projectVersion + version.Revision = projectBuild + prometheus.MustRegister(version.NewCollector("arangodb_exporter")) + mux.Handle("/metrics", prometheus.Handler()) } - prometheus.MustRegister(exporter) - version.Version = projectVersion - version.Revision = projectBuild - prometheus.MustRegister(version.NewCollector("arangodb_exporter")) log.Infoln("Listening on", serverOptions.Address) - mux := http.NewServeMux() - mux.Handle("/metrics", prometheus.Handler()) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(` ArangoDB Exporter diff --git a/passthru.go b/passthru.go new file mode 100644 index 0000000..dfed6a4 --- /dev/null +++ b/passthru.go @@ -0,0 +1,102 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Adam Janikowski +// + +package main + +import ( + "crypto/tls" + "fmt" + "io" + "net/http" + "time" +) + +var _ http.Handler = &passthru{} + +func NewPassthru(arangodbEndpoint, jwt string, sslVerify bool, timeout time.Duration) (http.Handler, error) { + transport := &http.Transport{} + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/_admin/metrics", arangodbEndpoint), nil) + if err != nil { + return nil, maskAny(err) + } + + if !sslVerify { + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + if jwt != "" { + hdr, err := CreateArangodJwtAuthorizationHeader(jwt) + if err != nil { + return nil, maskAny(err) + } + req.Header.Add("Authorization", hdr) + } + + client := &http.Client{ + Transport: transport, + Timeout: timeout, + } + + return &passthru{ + client: client, + request: req, + }, nil +} + +type passthru struct { + request *http.Request + client *http.Client +} + +func (p passthru) get() (*http.Response, error) { + return p.client.Do(p.request) +} + +func (p passthru) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + data, err := p.get() + + if err != nil { + // Ignore error + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte(err.Error())) + return + } + + if data.Body == nil { + // Ignore error + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte("Body is empty")) + return + } + + defer data.Body.Close() + + _, err = io.Copy(resp, data.Body) + + if err != nil { + // Ignore error + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte("Unable to write body")) + return + } +}