diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..957a893 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: go + +go_import_path: contrib.go.opencensus.io + +go: + - 1.11.x + +env: + global: + GO111MODULE=on + +before_script: + - make install-tools + +script: + - make travis-ci + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2e11d22 --- /dev/null +++ b/Makefile @@ -0,0 +1,95 @@ +# TODO: Fix this on windows. +ALL_SRC := $(shell find . -name '*.go' \ + -not -path './vendor/*' \ + -not -path '*/gen-go/*' \ + -type f | sort) +ALL_PKGS := $(shell go list $(sort $(dir $(ALL_SRC)))) + +GOTEST_OPT?=-v -race -timeout 30s +GOTEST_OPT_WITH_COVERAGE = $(GOTEST_OPT) -coverprofile=coverage.txt -covermode=atomic +GOTEST=go test +GOFMT=gofmt +GOLINT=golint +GOVET=go vet +EMBEDMD=embedmd +# TODO decide if we need to change these names. +README_FILES := $(shell find . -name '*README.md' | sort | tr '\n' ' ') + + +.DEFAULT_GOAL := fmt-lint-vet-embedmd-test + +.PHONY: fmt-lint-vet-embedmd-test +fmt-lint-vet-embedmd-test: fmt lint vet embedmd test + +# TODO enable test-with-coverage in tavis +.PHONY: travis-ci +travis-ci: fmt lint vet embedmd test test-386 + +all-pkgs: + @echo $(ALL_PKGS) | tr ' ' '\n' | sort + +all-srcs: + @echo $(ALL_SRC) | tr ' ' '\n' | sort + +.PHONY: test +test: + $(GOTEST) $(GOTEST_OPT) $(ALL_PKGS) + +.PHONY: test-386 +test-386: + GOARCH=386 $(GOTEST) -v -timeout 30s $(ALL_PKGS) + +.PHONY: test-with-coverage +test-with-coverage: + $(GOTEST) $(GOTEST_OPT_WITH_COVERAGE) $(ALL_PKGS) + +.PHONY: fmt +fmt: + @FMTOUT=`$(GOFMT) -s -l $(ALL_SRC) 2>&1`; \ + if [ "$$FMTOUT" ]; then \ + echo "$(GOFMT) FAILED => gofmt the following files:\n"; \ + echo "$$FMTOUT\n"; \ + exit 1; \ + else \ + echo "Fmt finished successfully"; \ + fi + +.PHONY: lint +lint: + @LINTOUT=`$(GOLINT) $(ALL_PKGS) 2>&1`; \ + if [ "$$LINTOUT" ]; then \ + echo "$(GOLINT) FAILED => clean the following lint errors:\n"; \ + echo "$$LINTOUT\n"; \ + exit 1; \ + else \ + echo "Lint finished successfully"; \ + fi + +.PHONY: vet +vet: + # TODO: Understand why go vet downloads "github.com/google/go-cmp v0.2.0" + @VETOUT=`$(GOVET) ./... | grep -v "go: downloading" 2>&1`; \ + if [ "$$VETOUT" ]; then \ + echo "$(GOVET) FAILED => go vet the following files:\n"; \ + echo "$$VETOUT\n"; \ + exit 1; \ + else \ + echo "Vet finished successfully"; \ + fi + +.PHONY: embedmd +embedmd: + @EMBEDMDOUT=`$(EMBEDMD) -d $(README_FILES) 2>&1`; \ + if [ "$$EMBEDMDOUT" ]; then \ + echo "$(EMBEDMD) FAILED => embedmd the following files:\n"; \ + echo "$$EMBEDMDOUT\n"; \ + exit 1; \ + else \ + echo "Embedmd finished successfully"; \ + fi + +.PHONY: install-tools +install-tools: + go get -u golang.org/x/tools/cmd/cover + go get -u golang.org/x/lint/golint + go get -u github.com/rakyll/embedmd diff --git a/README.md b/README.md index 3f37d1d..8c14977 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# opencensus-go-exporter-prometheus \ No newline at end of file +# opencensus-go-exporter-prometheus + diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..838cf36 --- /dev/null +++ b/example/main.go @@ -0,0 +1,83 @@ +// Copyright 2017, OpenCensus 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. + +// Command prometheus is an example program that collects data for +// video size. Collected data is exported to Prometheus. +package main + +import ( + "context" + "log" + "math/rand" + "net/http" + "time" + + "go.opencensus.io/exporter/prometheus" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" +) + +// Create measures. The program will record measures for the size of +// processed videos and the number of videos marked as spam. +var ( + videoCount = stats.Int64("example.com/measures/video_count", "number of processed videos", stats.UnitDimensionless) + videoSize = stats.Int64("example.com/measures/video_size", "size of processed video", stats.UnitBytes) +) + +func main() { + ctx := context.Background() + + exporter, err := prometheus.NewExporter(prometheus.Options{}) + if err != nil { + log.Fatal(err) + } + view.RegisterExporter(exporter) + + // Create view to see the number of processed videos cumulatively. + // Create view to see the amount of video processed + // Subscribe will allow view data to be exported. + // Once no longer needed, you can unsubscribe from the view. + if err = view.Register( + &view.View{ + Name: "video_count", + Description: "number of videos processed over time", + Measure: videoCount, + Aggregation: view.Count(), + }, + &view.View{ + Name: "video_size", + Description: "processed video size over time", + Measure: videoSize, + Aggregation: view.Distribution(0, 1<<16, 1<<32), + }, + ); err != nil { + log.Fatalf("Cannot register the view: %v", err) + } + + // Set reporting period to report data at every second. + view.SetReportingPeriod(1 * time.Second) + + // Record some data points... + go func() { + for { + stats.Record(ctx, videoCount.M(1), videoSize.M(rand.Int63())) + <-time.After(time.Millisecond * time.Duration(1+rand.Intn(400))) + } + }() + + addr := ":9999" + log.Printf("Serving at %s", addr) + http.Handle("/metrics", exporter) + log.Fatal(http.ListenAndServe(addr, nil)) +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..23af53e --- /dev/null +++ b/example_test.go @@ -0,0 +1,33 @@ +// Copyright 2018, OpenCensus 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 prometheus_test + +import ( + "log" + "net/http" + + "contrib.go.opencensus.io/exporter/prometheus" +) + +func Example() { + exporter, err := prometheus.NewExporter(prometheus.Options{}) + if err != nil { + log.Fatal(err) + } + + // Serve the scrape endpoint on port 9999. + http.Handle("/metrics", exporter) + log.Fatal(http.ListenAndServe(":9999", nil)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2132d72 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module contrib.go.opencensus.io/exporter/prometheus + +require ( + github.com/golang/mock v1.2.0 // indirect + github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 + go.opencensus.io v0.21.0-alpha // TODO [rghetia] remove after v0.21.0 is released. + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..515f936 --- /dev/null +++ b/go.sum @@ -0,0 +1,131 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= +go.opencensus.io v0.21.0-alpha h1:Z7SNQK6FRCVua3mHU/Lj0VnS32/BIluIYNSNQBLg9Jk= +go.opencensus.io v0.21.0-alpha/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/prometheus.go b/prometheus.go new file mode 100644 index 0000000..59ce1c0 --- /dev/null +++ b/prometheus.go @@ -0,0 +1,277 @@ +// Copyright 2017, OpenCensus 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 prometheus contains a Prometheus exporter that supports exporting +// OpenCensus views as Prometheus metrics. +package prometheus // import "contrib.go.opencensus.io/exporter/prometheus" + +import ( + "fmt" + "log" + "net/http" + "sync" + + "context" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opencensus.io/metric/metricdata" + "go.opencensus.io/metric/metricexport" + "go.opencensus.io/stats/view" +) + +// Exporter exports stats to Prometheus, users need +// to register the exporter as an http.Handler to be +// able to export. +type Exporter struct { + opts Options + g prometheus.Gatherer + c *collector + handler http.Handler +} + +// Options contains options for configuring the exporter. +type Options struct { + Namespace string + Registry *prometheus.Registry + OnError func(err error) + ConstLabels prometheus.Labels // ConstLabels will be set as labels on all views. +} + +// NewExporter returns an exporter that exports stats to Prometheus. +func NewExporter(o Options) (*Exporter, error) { + if o.Registry == nil { + o.Registry = prometheus.NewRegistry() + } + collector := newCollector(o, o.Registry) + e := &Exporter{ + opts: o, + g: o.Registry, + c: collector, + handler: promhttp.HandlerFor(o.Registry, promhttp.HandlerOpts{}), + } + collector.ensureRegisteredOnce() + + return e, nil +} + +var _ http.Handler = (*Exporter)(nil) + +// ensureRegisteredOnce invokes reg.Register on the collector itself +// exactly once to ensure that we don't get errors such as +// cannot register the collector: descriptor Desc{fqName: *} +// already exists with the same fully-qualified name and const label values +// which is documented by Prometheus at +// https://github.com/prometheus/client_golang/blob/fcc130e101e76c5d303513d0e28f4b6d732845c7/prometheus/registry.go#L89-L101 +func (c *collector) ensureRegisteredOnce() { + c.registerOnce.Do(func() { + if err := c.reg.Register(c); err != nil { + c.opts.onError(fmt.Errorf("cannot register the collector: %v", err)) + } + }) + +} + +func (o *Options) onError(err error) { + if o.OnError != nil { + o.OnError(err) + } else { + log.Printf("Failed to export to Prometheus: %v", err) + } +} + +// ExportView exports to the Prometheus if view data has one or more rows. +// Each OpenCensus AggregationData will be converted to +// corresponding Prometheus Metric: SumData will be converted +// to Untyped Metric, CountData will be a Counter Metric, +// DistributionData will be a Histogram Metric. +// Deprecated in lieu of metricexport.Reader interface. +func (e *Exporter) ExportView(vd *view.Data) { +} + +// ServeHTTP serves the Prometheus endpoint. +func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + e.handler.ServeHTTP(w, r) +} + +// collector implements prometheus.Collector +type collector struct { + opts Options + mu sync.Mutex // mu guards all the fields. + + registerOnce sync.Once + + // reg helps collector register views dynamically. + reg *prometheus.Registry + + // reader reads metrics from all registered producers. + reader *metricexport.Reader +} + +func (c *collector) Describe(ch chan<- *prometheus.Desc) { + de := &descExporter{c: c, descCh: ch} + c.reader.ReadAndExport(de) +} + +// Collect fetches the statistics from OpenCensus +// and delivers them as Prometheus Metrics. +// Collect is invoked every time a prometheus.Gatherer is run +// for example when the HTTP endpoint is invoked by Prometheus. +func (c *collector) Collect(ch chan<- prometheus.Metric) { + me := &metricExporter{c: c, metricCh: ch} + c.reader.ReadAndExport(me) +} + +func newCollector(opts Options, registrar *prometheus.Registry) *collector { + return &collector{ + reg: registrar, + opts: opts, + reader: metricexport.NewReader()} +} + +func (c *collector) toDesc(metric *metricdata.Metric) *prometheus.Desc { + return prometheus.NewDesc( + metricName(c.opts.Namespace, metric), + metric.Descriptor.Description, + toPromLabels(metric.Descriptor.LabelKeys), + c.opts.ConstLabels) +} + +type metricExporter struct { + c *collector + metricCh chan<- prometheus.Metric +} + +// ExportMetrics exports to the Prometheus. +// Each OpenCensus Metric will be converted to +// corresponding Prometheus Metric: +// TypeCumulativeInt64 and TypeCumulativeFloat64 will be a Counter Metric, +// TypeCumulativeDistribution will be a Histogram Metric. +// TypeGaugeFloat64 and TypeGaugeInt64 will be a Gauge Metric +func (me *metricExporter) ExportMetrics(ctx context.Context, metrics []*metricdata.Metric) error { + for _, metric := range metrics { + desc := me.c.toDesc(metric) + for _, ts := range metric.TimeSeries { + tvs := toLabelValues(ts.LabelValues) + for _, point := range ts.Points { + metric, err := toPromMetric(desc, metric, point, tvs) + if err != nil { + me.c.opts.onError(err) + } else if metric != nil { + me.metricCh <- metric + } + } + } + } + return nil +} + +type descExporter struct { + c *collector + descCh chan<- *prometheus.Desc +} + +// ExportMetrics exports descriptor to the Prometheus. +// It is invoked when request to scrape descriptors is received. +func (me *descExporter) ExportMetrics(ctx context.Context, metrics []*metricdata.Metric) error { + for _, metric := range metrics { + desc := me.c.toDesc(metric) + me.descCh <- desc + } + return nil +} + +func toPromLabels(mls []metricdata.LabelKey) (labels []string) { + for _, ml := range mls { + labels = append(labels, sanitize(ml.Key)) + } + return labels +} + +func metricName(namespace string, m *metricdata.Metric) string { + var name string + if namespace != "" { + name = namespace + "_" + } + return name + sanitize(m.Descriptor.Name) +} + +func toPromMetric( + desc *prometheus.Desc, + metric *metricdata.Metric, + point metricdata.Point, + labelValues []string) (prometheus.Metric, error) { + switch metric.Descriptor.Type { + case metricdata.TypeCumulativeFloat64, metricdata.TypeCumulativeInt64: + pv, err := toPromValue(point) + if err != nil { + return nil, err + } + return prometheus.NewConstMetric(desc, prometheus.CounterValue, pv, labelValues...) + + case metricdata.TypeGaugeFloat64, metricdata.TypeGaugeInt64: + pv, err := toPromValue(point) + if err != nil { + return nil, err + } + return prometheus.NewConstMetric(desc, prometheus.GaugeValue, pv, labelValues...) + + case metricdata.TypeCumulativeDistribution: + switch v := point.Value.(type) { + case *metricdata.Distribution: + points := make(map[float64]uint64) + // Histograms are cumulative in Prometheus. + // Get cumulative bucket counts. + cumCount := uint64(0) + for i, b := range v.BucketOptions.Bounds { + cumCount += uint64(v.Buckets[i].Count) + points[b] = cumCount + } + return prometheus.NewConstHistogram(desc, uint64(v.Count), v.Sum, points, labelValues...) + default: + return nil, typeMismatchError(point) + } + case metricdata.TypeSummary: + // TODO: [rghetia] add support for TypeSummary. + return nil, nil + default: + return nil, fmt.Errorf("aggregation %T is not yet supported", metric.Descriptor.Type) + } +} + +func toLabelValues(labelValues []metricdata.LabelValue) (values []string) { + for _, lv := range labelValues { + if lv.Present { + values = append(values, lv.Value) + } else { + values = append(values, "") + } + } + return values +} + +func typeMismatchError(point metricdata.Point) error { + return fmt.Errorf("point type %T does not match metric type", point) + +} + +func toPromValue(point metricdata.Point) (float64, error) { + switch v := point.Value.(type) { + case float64: + return v, nil + case int64: + return float64(v), nil + default: + return 0.0, typeMismatchError(point) + } +} diff --git a/prometheus_test.go b/prometheus_test.go new file mode 100644 index 0000000..83fc90a --- /dev/null +++ b/prometheus_test.go @@ -0,0 +1,450 @@ +// Copyright 2017, OpenCensus 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 prometheus + +import ( + "context" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + + "github.com/prometheus/client_golang/prometheus" +) + +type mSlice []*stats.Int64Measure + +func (measures *mSlice) createAndAppend(name, desc, unit string) { + m := stats.Int64(name, desc, unit) + *measures = append(*measures, m) +} + +type vCreator []*view.View + +func (vc *vCreator) createAndAppend(name, description string, keys []tag.Key, measure stats.Measure, agg *view.Aggregation) { + v := &view.View{ + Name: name, + Description: description, + TagKeys: keys, + Measure: measure, + Aggregation: agg, + } + *vc = append(*vc, v) +} + +func TestMetricsEndpointOutput(t *testing.T) { + exporter, err := NewExporter(Options{}) + if err != nil { + t.Fatalf("failed to create prometheus exporter: %v", err) + } + + names := []string{"foo", "bar", "baz"} + + var measures mSlice + for _, name := range names { + measures.createAndAppend("tests/"+name, name, "") + } + + var vc vCreator + for _, m := range measures { + vc.createAndAppend(m.Name(), m.Description(), nil, m, view.Count()) + } + + if err := view.Register(vc...); err != nil { + t.Fatalf("failed to create views: %v", err) + } + defer view.Unregister(vc...) + + view.SetReportingPeriod(time.Millisecond) + + for _, m := range measures { + stats.Record(context.Background(), m.M(1)) + } + + srv := httptest.NewServer(exporter) + defer srv.Close() + + var i int + var output string + for { + time.Sleep(10 * time.Millisecond) + if i == 1000 { + t.Fatal("no output at /metrics (10s wait)") + } + i++ + + resp, err := http.Get(srv.URL) + if err != nil { + t.Fatalf("failed to get /metrics: %v", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + resp.Body.Close() + + output = string(body) + if output != "" { + break + } + } + + if strings.Contains(output, "collected before with the same name and label values") { + t.Fatal("metric name and labels being duplicated but must be unique") + } + + if strings.Contains(output, "error(s) occurred") { + t.Fatal("error reported by prometheus registry") + } + + for _, name := range names { + if !strings.Contains(output, "tests_"+name+" 1") { + t.Fatalf("measurement missing in output: %v", name) + } + } +} + +func TestCumulativenessFromHistograms(t *testing.T) { + exporter, err := NewExporter(Options{}) + if err != nil { + t.Fatalf("failed to create prometheus exporter: %v", err) + } + + m := stats.Float64("tests/bills", "payments by denomination", stats.UnitDimensionless) + v := &view.View{ + Name: "cash/register", + Description: "this is a test", + Measure: m, + + // Intentionally used repeated elements in the ascending distribution. + // to ensure duplicate distribution items are handles. + Aggregation: view.Distribution(1, 5, 5, 5, 5, 10, 20, 50, 100, 250), + } + + if err := view.Register(v); err != nil { + t.Fatalf("Register error: %v", err) + } + defer view.Unregister(v) + + // Give the reporter ample time to process registration + //<-time.After(10 * reportPeriod) + + values := []float64{0.25, 245.67, 12, 1.45, 199.9, 7.69, 187.12} + // We want the results that look like this: + // 1: [0.25] | 1 + prev(i) = 1 + 0 = 1 + // 5: [1.45] | 1 + prev(i) = 1 + 1 = 2 + // 10: [7.69] | 1 + prev(i) = 1 + 2 = 3 + // 20: [12] | 1 + prev(i) = 1 + 3 = 4 + // 50: [] | 0 + prev(i) = 0 + 4 = 4 + // 100: [] | 0 + prev(i) = 0 + 4 = 4 + // 250: [187.12, 199.9, 245.67] | 3 + prev(i) = 3 + 4 = 7 + wantLines := []string{ + `cash_register_bucket{le="1"} 1`, + `cash_register_bucket{le="5"} 2`, + `cash_register_bucket{le="10"} 3`, + `cash_register_bucket{le="20"} 4`, + `cash_register_bucket{le="50"} 4`, + `cash_register_bucket{le="100"} 4`, + `cash_register_bucket{le="250"} 7`, + `cash_register_bucket{le="+Inf"} 7`, + `cash_register_sum 654.0799999999999`, // Summation of the input values + `cash_register_count 7`, + } + + ctx := context.Background() + ms := make([]stats.Measurement, 0, len(values)) + for _, value := range values { + mx := m.M(value) + ms = append(ms, mx) + } + stats.Record(ctx, ms...) + + // Give the recorder ample time to process recording + //<-time.After(10 * reportPeriod) + + cst := httptest.NewServer(exporter) + defer cst.Close() + res, err := http.Get(cst.URL) + if err != nil { + t.Fatalf("http.Get error: %v", err) + } + blob, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("Read body error: %v", err) + } + str := strings.Trim(string(blob), "\n") + lines := strings.Split(str, "\n") + nonComments := make([]string, 0, len(lines)) + for _, line := range lines { + if !strings.Contains(line, "#") { + nonComments = append(nonComments, line) + } + } + + got := strings.Join(nonComments, "\n") + want := strings.Join(wantLines, "\n") + if got != want { + t.Fatalf("\ngot:\n%s\n\nwant:\n%s\n", got, want) + } +} + +func TestHistogramUnorderedBucketBounds(t *testing.T) { + exporter, err := NewExporter(Options{}) + if err != nil { + t.Fatalf("failed to create prometheus exporter: %v", err) + } + + m := stats.Float64("tests/bills", "payments by denomination", stats.UnitDimensionless) + v := &view.View{ + Name: "cash/register", + Description: "this is a test", + Measure: m, + + // Intentionally used unordered and duplicated elements in the distribution + // to ensure unordered bucket bounds are handled. + Aggregation: view.Distribution(10, 5, 1, 1, 50, 5, 20, 100, 250), + } + + if err := view.Register(v); err != nil { + t.Fatalf("Register error: %v", err) + } + defer view.Unregister(v) + + // Give the reporter ample time to process registration + //<-time.After(10 * reportPeriod) + + values := []float64{0.25, 245.67, 12, 1.45, 199.9, 7.69, 187.12} + // We want the results that look like this: + // 1: [0.25] | 1 + prev(i) = 1 + 0 = 1 + // 5: [1.45] | 1 + prev(i) = 1 + 1 = 2 + // 10: [7.69] | 1 + prev(i) = 1 + 2 = 3 + // 20: [12] | 1 + prev(i) = 1 + 3 = 4 + // 50: [] | 0 + prev(i) = 0 + 4 = 4 + // 100: [] | 0 + prev(i) = 0 + 4 = 4 + // 250: [187.12, 199.9, 245.67] | 3 + prev(i) = 3 + 4 = 7 + wantLines := []string{ + `cash_register_bucket{le="1"} 1`, + `cash_register_bucket{le="5"} 2`, + `cash_register_bucket{le="10"} 3`, + `cash_register_bucket{le="20"} 4`, + `cash_register_bucket{le="50"} 4`, + `cash_register_bucket{le="100"} 4`, + `cash_register_bucket{le="250"} 7`, + `cash_register_bucket{le="+Inf"} 7`, + `cash_register_sum 654.0799999999999`, // Summation of the input values + `cash_register_count 7`, + } + + ctx := context.Background() + ms := make([]stats.Measurement, 0, len(values)) + for _, value := range values { + mx := m.M(value) + ms = append(ms, mx) + } + stats.Record(ctx, ms...) + + // Give the recorder ample time to process recording + //<-time.After(10 * reportPeriod) + + cst := httptest.NewServer(exporter) + defer cst.Close() + res, err := http.Get(cst.URL) + if err != nil { + t.Fatalf("http.Get error: %v", err) + } + blob, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("Read body error: %v", err) + } + str := strings.Trim(string(blob), "\n") + lines := strings.Split(str, "\n") + nonComments := make([]string, 0, len(lines)) + for _, line := range lines { + if !strings.Contains(line, "#") { + nonComments = append(nonComments, line) + } + } + + got := strings.Join(nonComments, "\n") + want := strings.Join(wantLines, "\n") + if got != want { + t.Fatalf("\ngot:\n%s\n\nwant:\n%s\n", got, want) + } +} + +func TestConstLabelsIncluded(t *testing.T) { + constLabels := prometheus.Labels{ + "service": "spanner", + } + measureLabel, _ := tag.NewKey("method") + + exporter, err := NewExporter(Options{ + ConstLabels: constLabels, + }) + if err != nil { + t.Fatalf("failed to create prometheus exporter: %v", err) + } + + names := []string{"foo", "bar", "baz"} + + var measures mSlice + for _, name := range names { + measures.createAndAppend("tests/"+name, name, "") + } + + var vc vCreator + for _, m := range measures { + vc.createAndAppend(m.Name(), m.Description(), []tag.Key{measureLabel}, m, view.Count()) + } + + if err := view.Register(vc...); err != nil { + t.Fatalf("failed to create views: %v", err) + } + defer view.Unregister(vc...) + + view.SetReportingPeriod(time.Millisecond) + + ctx, _ := tag.New(context.Background(), tag.Upsert(measureLabel, "issue961")) + for _, m := range measures { + stats.Record(ctx, m.M(1)) + } + + srv := httptest.NewServer(exporter) + defer srv.Close() + + var i int + var output string + for { + time.Sleep(10 * time.Millisecond) + if i == 1000 { + t.Fatal("no output at /metrics (10s wait)") + } + i++ + + resp, err := http.Get(srv.URL) + if err != nil { + t.Fatalf("failed to get /metrics: %v", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + resp.Body.Close() + + output = string(body) + if output != "" { + break + } + } + + if strings.Contains(output, "collected before with the same name and label values") { + t.Fatal("metric name and labels being duplicated but must be unique") + } + + if strings.Contains(output, "error(s) occurred") { + t.Fatal("error reported by prometheus registry") + } + + want := `# HELP tests_bar bar +# TYPE tests_bar counter +tests_bar{method="issue961",service="spanner"} 1 +# HELP tests_baz baz +# TYPE tests_baz counter +tests_baz{method="issue961",service="spanner"} 1 +# HELP tests_foo foo +# TYPE tests_foo counter +tests_foo{method="issue961",service="spanner"} 1 +` + if output != want { + t.Fatal("output differed from expected") + } +} + +func TestViewMeasureWithoutTag(t *testing.T) { + exporter, err := NewExporter(Options{}) + if err != nil { + t.Fatalf("failed to create prometheus exporter: %v", err) + } + m := stats.Int64("tests/foo", "foo", stats.UnitDimensionless) + k1, _ := tag.NewKey("key/1") + k2, _ := tag.NewKey("key/2") + k3, _ := tag.NewKey("key/3") + k4, _ := tag.NewKey("key/4") + k5, _ := tag.NewKey("key/5") + randomKey, _ := tag.NewKey("issue659") + v := &view.View{ + Name: m.Name(), + Description: m.Description(), + TagKeys: []tag.Key{k2, k5, k3, k1, k4}, // Ensure view has a tag + Measure: m, + Aggregation: view.Count(), + } + if err := view.Register(v); err != nil { + t.Fatalf("failed to create views: %v", err) + } + defer view.Unregister(v) + view.SetReportingPeriod(time.Millisecond) + // Make a measure without some tags in the view. + ctx1, _ := tag.New(context.Background(), tag.Upsert(k4, "issue659"), tag.Upsert(randomKey, "value"), tag.Upsert(k2, "issue659")) + stats.Record(ctx1, m.M(1)) + ctx2, _ := tag.New(context.Background(), tag.Upsert(k5, "issue659"), tag.Upsert(k3, "issue659"), tag.Upsert(k1, "issue659")) + stats.Record(ctx2, m.M(2)) + srv := httptest.NewServer(exporter) + defer srv.Close() + var i int + var output string + for { + time.Sleep(10 * time.Millisecond) + if i == 1000 { + t.Fatal("no output at /metrics (10s wait)") + } + i++ + resp, err := http.Get(srv.URL) + if err != nil { + t.Fatalf("failed to get /metrics: %v", err) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + resp.Body.Close() + output = string(body) + if output != "" { + break + } + } + if strings.Contains(output, "collected before with the same name and label values") { + t.Fatal("metric name and labels being duplicated but must be unique") + } + if strings.Contains(output, "error(s) occurred") { + t.Fatal("error reported by prometheus registry") + } + want := `# HELP tests_foo foo +# TYPE tests_foo counter +tests_foo{key_1="",key_2="issue659",key_3="",key_4="issue659",key_5=""} 1 +tests_foo{key_1="issue659",key_2="",key_3="issue659",key_4="",key_5="issue659"} 1 +` + if output != want { + t.Fatalf("output differed from expected output: %s want: %s", output, want) + } +} diff --git a/sanitize.go b/sanitize.go new file mode 100644 index 0000000..ed6d8a1 --- /dev/null +++ b/sanitize.go @@ -0,0 +1,50 @@ +// Copyright 2017, OpenCensus 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 prometheus + +import ( + "strings" + "unicode" +) + +const labelKeySizeLimit = 100 + +// sanitize returns a string that is trunacated to 100 characters if it's too +// long, and replaces non-alphanumeric characters to underscores. +func sanitize(s string) string { + if len(s) == 0 { + return s + } + if len(s) > labelKeySizeLimit { + s = s[:labelKeySizeLimit] + } + s = strings.Map(sanitizeRune, s) + if unicode.IsDigit(rune(s[0])) { + s = "key_" + s + } + if s[0] == '_' { + s = "key" + s + } + return s +} + +// converts anything that is not a letter or digit to an underscore +func sanitizeRune(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + return r + } + // Everything else turns into an underscore + return '_' +} diff --git a/sanitize_test.go b/sanitize_test.go new file mode 100644 index 0000000..5261e80 --- /dev/null +++ b/sanitize_test.go @@ -0,0 +1,67 @@ +// Copyright 2017, OpenCensus 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 prometheus + +import ( + "strings" + "testing" +) + +func TestSanitize(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "trunacate long string", + input: strings.Repeat("a", 101), + want: strings.Repeat("a", 100), + }, + { + name: "replace character", + input: "test/key-1", + want: "test_key_1", + }, + { + name: "add prefix if starting with digit", + input: "0123456789", + want: "key_0123456789", + }, + { + name: "add prefix if starting with _", + input: "_0123456789", + want: "key_0123456789", + }, + { + name: "starts with _ after sanitization", + input: "/0123456789", + want: "key_0123456789", + }, + { + name: "valid input", + input: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789", + want: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, want := sanitize(tt.input), tt.want; got != want { + t.Errorf("sanitize() = %q; want %q", got, want) + } + }) + } +}