From 813a5f0f0db1dd000e7b7127f315fda738e355a4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 7 Jan 2021 12:35:21 +0100 Subject: [PATCH] Add metrics module --- go.mod | 14 +- go.sum | 58 +++++++ metrics/api.go | 138 ++++++++++++++++ metrics/metric.go | 152 ++++++++++++++++++ metrics/metric_counter.go | 44 +++++ metrics/metric_gauge.go | 41 +++++ metrics/metric_histogram.go | 41 +++++ metrics/metric_info.go | 40 +++++ metrics/metric_runtime.go | 45 ++++++ metrics/module.go | 131 +++++++++++++++ metrics/persistence.go | 146 +++++++++++++++++ metrics/test/.gitignore | 1 + metrics/test/README.md | 4 + metrics/test/docker-compose.yml | 36 +++++ metrics/test/grafana/config.ini | 10 ++ .../test/grafana/dashboards/portmaster.yml | 11 ++ .../test/grafana/datasources/datasource.yml | 8 + utils/atomic.go | 4 + 18 files changed, 921 insertions(+), 3 deletions(-) create mode 100644 metrics/api.go create mode 100644 metrics/metric.go create mode 100644 metrics/metric_counter.go create mode 100644 metrics/metric_gauge.go create mode 100644 metrics/metric_histogram.go create mode 100644 metrics/metric_info.go create mode 100644 metrics/metric_runtime.go create mode 100644 metrics/module.go create mode 100644 metrics/persistence.go create mode 100644 metrics/test/.gitignore create mode 100644 metrics/test/README.md create mode 100644 metrics/test/docker-compose.yml create mode 100644 metrics/test/grafana/config.ini create mode 100644 metrics/test/grafana/dashboards/portmaster.yml create mode 100644 metrics/test/grafana/datasources/datasource.yml diff --git a/go.mod b/go.mod index 0ac09ac..1deaaf5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.15 require ( github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect + github.com/VictoriaMetrics/metrics v1.12.3 github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 github.com/armon/go-radix v1.0.0 github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 @@ -13,7 +14,6 @@ require ( github.com/go-ole/go-ole v1.2.4 // indirect github.com/gofrs/uuid v3.3.0+incompatible github.com/golang/protobuf v1.4.2 // indirect - github.com/google/renameio v0.1.0 github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.2 github.com/hashicorp/errwrap v1.1.0 // indirect @@ -29,8 +29,16 @@ require ( github.com/tidwall/gjson v1.6.0 github.com/tidwall/sjson v1.1.1 go.etcd.io/bbolt v1.3.4 - golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 // indirect + golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 - golang.org/x/sys v0.0.0-20200523222454-059865788121 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f + golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 // indirect gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect ) + +require ( + // The follow-up commit removes Windows support. + // TOOD: Check how we want to handle this in the future, possibly ingest + // needed functionality into here. + github.com/google/renameio v0.1.1-0.20200217212219-353f81969824 +) diff --git a/go.sum b/go.sum index d0c5fac..0a64eb5 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,24 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VictoriaMetrics/metrics v1.12.3 h1:Fe6JHC6MSEKa+BtLhPN8WIvS+HKPzMc2evEpNeCGy7I= +github.com/VictoriaMetrics/metrics v1.12.3/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE= +github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs= github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= 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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 h1:yCfXxYaelOyqnia8F/Yng47qhmfC9nKTRIbYRrRueq4= github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -27,19 +34,24 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.1 h1:w9pSFNSdq/JPM1N12Fz/F/bzo993Is1W+Q7HjPzi7yg= github.com/dgraph-io/badger v1.6.1/go.mod h1:FRmFw3uxvcpa8zG3Rxs0th+hCLIuaQg8HlNV5bjgnuU= +github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -53,6 +65,7 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -61,6 +74,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/renameio v0.1.1-0.20200217212219-353f81969824 h1:9q700G0beHecUuiZOuKgNqNsGQixTeDLnzVZ5nsW3lc= +github.com/google/renameio v0.1.1-0.20200217212219-353f81969824/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -73,6 +89,7 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -114,8 +131,11 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0= github.com/seehuhn/fortuna v1.0.1/go.mod h1:LX8ubejCnUoT/hX+1aKUtbKls2H6DRkqzkc7TdR3iis= +github.com/seehuhn/sha256d v1.0.0 h1:TXTsAuEWr02QjRm153Fnvvb6fXXDo7Bmy1FizxarGYw= github.com/seehuhn/sha256d v1.0.0/go.mod h1:PEuxg9faClSveVuFXacQmi+NtDI/PX8bpKjtNzf2+s4= +github.com/shirou/gopsutil v2.20.4+incompatible h1:cMT4rxS55zx9NVUnCkrmXCsEB/RNfG9SwHY9evtX8Ng= github.com/shirou/gopsutil v2.20.4+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -125,6 +145,7 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -140,17 +161,27 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tevino/abool v1.0.0 h1:5hlcsW0yartQp609pbLLrE/s3ZNm2k/F7YSGuqJxpbM= github.com/tevino/abool v1.0.0/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0= +github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U= github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/valyala/fastrand v1.0.0 h1:LUKT9aKer2dVQNUi3waewTbKV+7H17kvWFNKs2ObdkI= +github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/valyala/histogram v1.1.2 h1:vOk5VrGjMBIoPR5k6wA8vBaC8toeJ8XO0yfRjFEc1h8= +github.com/valyala/histogram v1.1.2/go.mod h1:CZAr6gK9dbD7hYx2s8WSPh0p5x5wETjC+2b3PJVtEdg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -158,19 +189,31 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -179,17 +222,31 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 h1:BTs2GMGSMWpgtCpv1CE7vkJTv7XcHdcLLnAMu7UbgTY= +golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -199,6 +256,7 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 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= diff --git a/metrics/api.go b/metrics/api.go new file mode 100644 index 0000000..b012447 --- /dev/null +++ b/metrics/api.go @@ -0,0 +1,138 @@ +package metrics + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "time" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/config" + "github.com/safing/portbase/log" +) + +func registerAPI() error { + api.RegisterHandler("/metrics", &metricsAPI{}) + + return api.RegisterEndpoint(api.Endpoint{ + Path: "metrics/list", + Read: api.PermitAnyone, + MimeType: api.MimeTypeJSON, + DataFunc: func(*api.Request) ([]byte, error) { + registryLock.RLock() + defer registryLock.RUnlock() + + return json.Marshal(registry) + }, + Name: "Export Registered Metrics", + Description: "List all registered metrics with their metadata.", + }) +} + +type metricsAPI struct{} + +func (m *metricsAPI) ReadPermission(*http.Request) api.Permission { return api.Dynamic } + +func (m *metricsAPI) WritePermission(*http.Request) api.Permission { return api.NotSupported } + +func (m *metricsAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Get API Request for permission and query. + ar := api.GetAPIRequest(r) + if ar == nil { + http.Error(w, "Missing API Request.", http.StatusInternalServerError) + return + } + + // Get expertise level from query. + expertiseLevel := config.ExpertiseLevelDeveloper + switch ar.Request.URL.Query().Get("level") { + case config.ExpertiseLevelNameUser: + expertiseLevel = config.ExpertiseLevelUser + case config.ExpertiseLevelNameExpert: + expertiseLevel = config.ExpertiseLevelExpert + case config.ExpertiseLevelNameDeveloper: + expertiseLevel = config.ExpertiseLevelDeveloper + } + + w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8") + w.WriteHeader(http.StatusOK) + WriteMetrics(w, ar.AuthToken.Read, expertiseLevel) +} + +// WriteMetrics writes all metrics that match the given permission and +// expertiseLevel to the given writer. +func WriteMetrics(w io.Writer, permission api.Permission, expertiseLevel config.ExpertiseLevel) { + registryLock.RLock() + defer registryLock.RUnlock() + + // Check if metric ID is already registered. + for _, metric := range registry { + if permission >= metric.Opts().Permission && + expertiseLevel >= metric.Opts().ExpertiseLevel { + metric.WritePrometheus(w) + } + } +} + +func writeMetricsTo(ctx context.Context, url string) error { + // First, collect metrics into buffer. + buf := &bytes.Buffer{} + WriteMetrics(buf, api.PermitSelf, config.ExpertiseLevelDeveloper) + + // Check if there is something to send. + if buf.Len() == 0 { + log.Debugf("metrics: not pushing metrics, nothing to send") + return nil + } + + // Create request + req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, buf) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Send. + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Check return status. + switch resp.StatusCode { + case http.StatusOK, + http.StatusAccepted, + http.StatusNoContent: + return nil + default: + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf( + "got %s while writing metrics to %s: %s", + resp.Status, + url, + body, + ) + } +} + +func metricsWriter(ctx context.Context) error { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + err := writeMetricsTo(ctx, pushURL) + if err != nil { + return err + } + } + } +} diff --git a/metrics/metric.go b/metrics/metric.go new file mode 100644 index 0000000..d2fc839 --- /dev/null +++ b/metrics/metric.go @@ -0,0 +1,152 @@ +package metrics + +import ( + "fmt" + "io" + "regexp" + "sort" + "strings" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/config" + + vm "github.com/VictoriaMetrics/metrics" +) + +// PrometheusFormatRequirement is required format defined by prometheus for +// metric and label names. +const PrometheusFormatRequirement = "[a-zA-Z_][a-zA-Z0-9_]*" + +var prometheusFormat = regexp.MustCompile(PrometheusFormatRequirement) + +// Metric represents one or more metrics. +type Metric interface { + ID() string + LabeledID() string + Opts() *Options + WritePrometheus(w io.Writer) +} + +type metricBase struct { + Identifier string + Labels map[string]string + LabeledIdentifier string + Options *Options + set *vm.Set +} + +// Options can be used to set advanced metric settings. +type Options struct { + // Name defines an optional human readable name for the metric. + Name string + + // AlertLimit defines an upper limit that triggers an alert. + AlertLimit float64 + + // AlertTimeframe defines an optional timeframe in seconds for which the + // AlertLimit should be interpreted in. + AlertTimeframe float64 + + // Permission defines the permission that is required to read the metric. + Permission api.Permission + + // ExpertiseLevel defines the expertise level that the metric is meant for. + ExpertiseLevel config.ExpertiseLevel + + // Persist enabled persisting the metric on shutdown and loading the previous + // value at start. This is only supported for counters. + Persist bool +} + +func newMetricBase(id string, labels map[string]string, opts Options) (*metricBase, error) { + // Check formats. + if !prometheusFormat.MatchString(id) { + return nil, fmt.Errorf("metric name %q must match %s", id, PrometheusFormatRequirement) + } + for labelName := range labels { + if !prometheusFormat.MatchString(labelName) { + return nil, fmt.Errorf("metric label name %q must match %s", labelName, PrometheusFormatRequirement) + } + } + + // Check permission. + if opts.Permission < api.PermitAnyone { + // Default to PermitUser. + opts.Permission = api.PermitUser + } + + // Create metric base. + base := &metricBase{ + Identifier: id, + Labels: labels, + Options: &opts, + set: vm.NewSet(), + } + base.LabeledIdentifier = base.buildLabeledID() + return base, nil +} + +// ID returns the given ID of the metric. +func (m *metricBase) ID() string { + return m.Identifier +} + +// LabeledID returns the Prometheus-compatible labeled ID of the metric. +func (m *metricBase) LabeledID() string { + return m.LabeledIdentifier +} + +// Opts returns the metric options. They may not be modified. +func (m *metricBase) Opts() *Options { + return m.Options +} + +// WritePrometheus writes the metric in the prometheus format to the given writer. +func (m *metricBase) WritePrometheus(w io.Writer) { + m.set.WritePrometheus(w) +} + +func (m *metricBase) buildLabeledID() string { + // Because we use the namespace and the global flags here, we need to flag + // them as immutable. + registryLock.Lock() + defer registryLock.Unlock() + firstMetricRegistered = true + + // Build ID from Identifier. + metricID := strings.TrimSpace(strings.ReplaceAll(m.Identifier, "/", "_")) + + // Add namespace to ID. + if metricNamespace != "" { + metricID = metricNamespace + "_" + metricID + } + + // Return now if no labels are defined. + if len(globalLabels) == 0 && len(m.Labels) == 0 { + return metricID + } + + // Add global labels to the custom ones. + // This overrides conflicts. + for labelName, labelValue := range globalLabels { + m.Labels[labelName] = labelValue + } + + // Render labels into a slice and sort them in order to make the labeled ID + // reproducible. + labels := make([]string, 0, len(m.Labels)) + for labelName, labelValue := range m.Labels { + labels = append(labels, fmt.Sprintf("%s=%q", labelName, labelValue)) + } + sort.Strings(labels) + + // Return fully labaled ID. + return fmt.Sprintf("%s{%s}", metricID, strings.Join(labels, ",")) +} + +// Split metrics into sets, according to the API Auth Levels, which will also correspond to the UI Mode levels. SPN // nodes will also allow public access to metrics with the permission "PermitAnyone". +// Save "life-long" metrics on shutdown and load them at start. +// Generate the correct metric name and labels. +// Expose metrics via http, but also via the runtime DB in order to push metrics to the UI. +// The UI will have to parse the prometheus metrics format and will not be able to immediately present historical data, // but data will have to be built. +// Provide the option to push metrics to a prometheus push gateway, this is especially helpful when gathering data from // loads of SPN nodes. diff --git a/metrics/metric_counter.go b/metrics/metric_counter.go new file mode 100644 index 0000000..d20ad6c --- /dev/null +++ b/metrics/metric_counter.go @@ -0,0 +1,44 @@ +package metrics + +import ( + vm "github.com/VictoriaMetrics/metrics" +) + +// Counter is a counter metric. +type Counter struct { + *metricBase + *vm.Counter +} + +// NewCounter registers a new counter metric. +func NewCounter(id string, labels map[string]string, opts *Options) (*Counter, error) { + // Ensure that there are options. + if opts == nil { + opts = &Options{} + } + + // Make base. + base, err := newMetricBase(id, labels, *opts) + if err != nil { + return nil, err + } + + // Create metric struct. + m := &Counter{ + metricBase: base, + } + + // Create metric in set + m.Counter = m.set.NewCounter(m.LabeledID()) + + // Register metric. + err = register(m) + if err != nil { + return nil, err + } + + // Load state. + m.loadState() + + return m, nil +} diff --git a/metrics/metric_gauge.go b/metrics/metric_gauge.go new file mode 100644 index 0000000..06628a8 --- /dev/null +++ b/metrics/metric_gauge.go @@ -0,0 +1,41 @@ +package metrics + +import ( + vm "github.com/VictoriaMetrics/metrics" +) + +// Gauge is a gauge metric. +type Gauge struct { + *metricBase + *vm.Gauge +} + +// NewGauge registers a new gauge metric. +func NewGauge(id string, labels map[string]string, fn func() float64, opts *Options) (*Gauge, error) { + // Ensure that there are options. + if opts == nil { + opts = &Options{} + } + + // Make base. + base, err := newMetricBase(id, labels, *opts) + if err != nil { + return nil, err + } + + // Create metric struct. + m := &Gauge{ + metricBase: base, + } + + // Create metric in set + m.Gauge = m.set.NewGauge(m.LabeledID(), fn) + + // Register metric. + err = register(m) + if err != nil { + return nil, err + } + + return m, nil +} diff --git a/metrics/metric_histogram.go b/metrics/metric_histogram.go new file mode 100644 index 0000000..92c0210 --- /dev/null +++ b/metrics/metric_histogram.go @@ -0,0 +1,41 @@ +package metrics + +import ( + vm "github.com/VictoriaMetrics/metrics" +) + +// Histogram is a histogram metric. +type Histogram struct { + *metricBase + *vm.Histogram +} + +// NewHistogram registers a new histogram metric. +func NewHistogram(id string, labels map[string]string, opts *Options) (*Histogram, error) { + // Ensure that there are options. + if opts == nil { + opts = &Options{} + } + + // Make base. + base, err := newMetricBase(id, labels, *opts) + if err != nil { + return nil, err + } + + // Create metric struct. + m := &Histogram{ + metricBase: base, + } + + // Create metric in set + m.Histogram = m.set.NewHistogram(m.LabeledID()) + + // Register metric. + err = register(m) + if err != nil { + return nil, err + } + + return m, nil +} diff --git a/metrics/metric_info.go b/metrics/metric_info.go new file mode 100644 index 0000000..9c82a9a --- /dev/null +++ b/metrics/metric_info.go @@ -0,0 +1,40 @@ +package metrics + +import ( + "runtime" + "strings" + + "github.com/safing/portbase/info" +) + +func registerInfoMetric() error { + meta := info.GetInfo() + _, err := NewGauge( + "info", + map[string]string{ + "version": checkUnknown(meta.Version), + "commit": checkUnknown(meta.Commit), + "build_options": checkUnknown(meta.BuildOptions), + "build_user": checkUnknown(meta.BuildUser), + "build_host": checkUnknown(meta.BuildHost), + "build_date": checkUnknown(meta.BuildDate), + "build_source": checkUnknown(meta.BuildSource), + "go_os": runtime.GOOS, + "go_arch": runtime.GOARCH, + "go_version": runtime.Version(), + "go_compiler": runtime.Compiler, + }, + func() float64 { + return 1 + }, + nil, + ) + return err +} + +func checkUnknown(s string) string { + if strings.Contains(s, "unknown") { + return "unknown" + } + return s +} diff --git a/metrics/metric_runtime.go b/metrics/metric_runtime.go new file mode 100644 index 0000000..9668583 --- /dev/null +++ b/metrics/metric_runtime.go @@ -0,0 +1,45 @@ +package metrics + +import ( + "io" + + vm "github.com/VictoriaMetrics/metrics" + "github.com/safing/portbase/api" + "github.com/safing/portbase/config" +) + +func init() { + registry = append(registry, &runtimeMetrics{}) +} + +var runtimeOpts = &Options{ + Name: "Golang Runtime", + Permission: api.PermitAdmin, + ExpertiseLevel: config.ExpertiseLevelDeveloper, +} + +type runtimeMetrics struct{} + +func (r *runtimeMetrics) ID() string { + return "_runtime" +} + +func (r *runtimeMetrics) LabeledID() string { + return "_runtime" +} + +func (r *runtimeMetrics) Opts() *Options { + return runtimeOpts +} + +func (r *runtimeMetrics) Permission() api.Permission { + return runtimeOpts.Permission +} + +func (r *runtimeMetrics) ExpertiseLevel() config.ExpertiseLevel { + return runtimeOpts.ExpertiseLevel +} + +func (r *runtimeMetrics) WritePrometheus(w io.Writer) { + vm.WriteProcessMetrics(w) +} diff --git a/metrics/module.go b/metrics/module.go new file mode 100644 index 0000000..c0c71b9 --- /dev/null +++ b/metrics/module.go @@ -0,0 +1,131 @@ +package metrics + +import ( + "errors" + "flag" + "fmt" + "sort" + "sync" + + "github.com/safing/portbase/modules" +) + +var ( + module *modules.Module + + registry []Metric + registryLock sync.RWMutex + + firstMetricRegistered bool + metricNamespace string + globalLabels = make(map[string]string) + + pushURL string + + // ErrAlreadyStarted is returned when an operation is only valid before the + // first metric is registered, and is called after. + ErrAlreadyStarted = errors.New("can only be changed before first metric is registered") + + // ErrAlreadyRegistered is returned when a metric with the same ID is + // registered again. + ErrAlreadyRegistered = errors.New("metric already registered") + + // ErrAlreadySet is returned when a value is already set and cannot be changed. + ErrAlreadySet = errors.New("already set") +) + +func init() { + flag.StringVar(&pushURL, "push-metrics", "", "URL to push prometheus metrics to") + + module = modules.Register("metrics", prep, start, stop, "database", "api") +} + +func prep() error { + return registerInfoMetric() +} + +func start() error { + if err := registerAPI(); err != nil { + return err + } + + if pushURL != "" { + module.StartServiceWorker("metric pusher", 0, metricsWriter) + } + + return nil +} + +func stop() error { + storePersistentMetrics() + + return nil +} + +func register(m Metric) error { + registryLock.Lock() + defer registryLock.Unlock() + + // Check if metric ID is already registered. + for _, registeredMetric := range registry { + if m.LabeledID() == registeredMetric.LabeledID() { + return ErrAlreadyRegistered + } + } + + // Add new metric to registry and sort it. + registry = append(registry, m) + sort.Sort(metricRegistry(registry)) + + // Set flag that first metric is now registered. + firstMetricRegistered = true + + return nil +} + +// SetNamespace sets the namespace for all metrics. It is prefixed to all +// metric IDs. +// It must be set before any metric is registered. +// Does not affect golang runtime metrics. +func SetNamespace(namespace string) error { + // Lock registry and check if a first metric is already registered. + registryLock.Lock() + defer registryLock.Unlock() + if firstMetricRegistered { + return ErrAlreadyStarted + } + + // Check if the namespace is already set. + if metricNamespace != "" { + return ErrAlreadySet + } + + metricNamespace = namespace + return nil +} + +// AddGlobalLabel adds a global label to all metrics. +// Global labels must be added before any metric is registered. +// Does not affect golang runtime metrics. +func AddGlobalLabel(name, value string) error { + // Lock registry and check if a first metric is already registered. + registryLock.Lock() + defer registryLock.Unlock() + if firstMetricRegistered { + return ErrAlreadyStarted + } + + // Check format. + if !prometheusFormat.MatchString(name) { + return fmt.Errorf("metric label name %q must match %s", name, PrometheusFormatRequirement) + } + + globalLabels[name] = value + return nil +} + +type metricRegistry []Metric + +func (r metricRegistry) Len() int { return len(r) } +func (r metricRegistry) Less(i, j int) bool { return r[i].LabeledID() < r[j].LabeledID() } +func (r metricRegistry) Swap(i, j int) { r[i], r[j] = r[j], r[i] } diff --git a/metrics/persistence.go b/metrics/persistence.go new file mode 100644 index 0000000..f4fd31b --- /dev/null +++ b/metrics/persistence.go @@ -0,0 +1,146 @@ +package metrics + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/log" + "github.com/tevino/abool" +) + +var ( + storage *metricsStorage + storageLoaded = abool.New() + storageKey string + + db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, + }) +) + +type metricsStorage struct { + sync.Mutex + record.Base + + Start time.Time + Counters map[string]uint64 +} + +// EnableMetricPersistence enables metric persistence for metrics that opted +// for it. They given key is the database key where the metric data will be +// persisted. +// This call also directly loads the stored data from the database. +// The returned error is only about loading the metrics, not about enabling +// persistence. +// May only be called once. +func EnableMetricPersistence(key string) error { + // Check if already loaded. + if storageLoaded.IsSet() { + return nil + } + + // Set storage key. + storageKey = key + + // Load metrics from storage. + var err error + storage, err = getMetricsStorage(key) + switch { + case err == nil: + // Continue. + case errors.Is(err, database.ErrNotFound): + return nil + default: + return err + } + storageLoaded.Set() + + // Load saved state for all counter metrics. + registryLock.RLock() + defer registryLock.RUnlock() + + for _, m := range registry { + counter, ok := m.(*Counter) + if ok { + counter.loadState() + } + } + + return nil +} + +func (c *Counter) loadState() { + // Check if we can and should load the state. + if !storageLoaded.IsSet() || !c.Opts().Persist { + return + } + + c.Set(storage.Counters[c.LabeledID()]) +} + +func storePersistentMetrics() { + // Check if persistence is enabled. + if storageKey == "" { + return + } + + // Create new storage. + newStorage := &metricsStorage{ + Start: time.Now(), + Counters: make(map[string]uint64), + } + newStorage.SetKey(storageKey) + // Copy values from previous version. + if storageLoaded.IsSet() { + newStorage.Start = storage.Start + } + + registryLock.RLock() + defer registryLock.RUnlock() + + // Export all counter metrics. + for _, m := range registry { + if m.Opts().Persist { + counter, ok := m.(*Counter) + if ok { + newStorage.Counters[m.LabeledID()] = counter.Get() + } + } + } + + // Save to database. + err := db.Put(newStorage) + if err != nil { + log.Warningf("metrics: failed to save metrics storage to db: %s", err) + } +} + +func getMetricsStorage(key string) (*metricsStorage, error) { + r, err := db.Get(key) + if err != nil { + return nil, err + } + + // unwrap + if r.IsWrapped() { + // only allocate a new struct, if we need it + new := &metricsStorage{} + err = record.Unwrap(r, new) + if err != nil { + return nil, err + } + return new, nil + } + + // or adjust type + new, ok := r.(*metricsStorage) + if !ok { + return nil, fmt.Errorf("record not of type *metricsStorage, but %T", r) + } + return new, nil +} diff --git a/metrics/test/.gitignore b/metrics/test/.gitignore new file mode 100644 index 0000000..6320cd2 --- /dev/null +++ b/metrics/test/.gitignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/metrics/test/README.md b/metrics/test/README.md new file mode 100644 index 0000000..e3239f6 --- /dev/null +++ b/metrics/test/README.md @@ -0,0 +1,4 @@ +# Testing metrics + +You can spin up a test setup for pushing and viewing metrics with `docker-compose up`. +Then use the flag `--push-metrics http://127.0.0.1:8428/api/v1/import/prometheus` to push metrics. diff --git a/metrics/test/docker-compose.yml b/metrics/test/docker-compose.yml new file mode 100644 index 0000000..e7d7bdc --- /dev/null +++ b/metrics/test/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3.8' + +networks: + pm-metrics-test-net: + +services: + + victoriametrics: + container_name: pm-metrics-test-victoriametrics + image: victoriametrics/victoria-metrics + command: + - '--storageDataPath=/storage' + ports: + - 8428:8428 + volumes: + - ./data/victoriametrics:/storage + networks: + - pm-metrics-test-net + restart: always + + grafana: + container_name: pm-metrics-test-grafana + image: grafana/grafana + command: + - '--config=/etc/grafana/provisioning/config.ini' + depends_on: + - "victoriametrics" + ports: + - 3000:3000 + volumes: + - ./data/grafana:/var/lib/grafana + - ./grafana:/etc/grafana/provisioning + - ./dashboards:/dashboards + networks: + - pm-metrics-test-net + restart: always diff --git a/metrics/test/grafana/config.ini b/metrics/test/grafana/config.ini new file mode 100644 index 0000000..341a31a --- /dev/null +++ b/metrics/test/grafana/config.ini @@ -0,0 +1,10 @@ +[auth] +disable_login_form = true +disable_signout_menu = true + +[auth.basic] +enabled = false + +[auth.anonymous] +enabled = true +org_role = Admin diff --git a/metrics/test/grafana/dashboards/portmaster.yml b/metrics/test/grafana/dashboards/portmaster.yml new file mode 100644 index 0000000..42813eb --- /dev/null +++ b/metrics/test/grafana/dashboards/portmaster.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'Portmaster' + folder: 'Portmaster' + disableDeletion: true + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /dashboards + foldersFromFilesStructure: true diff --git a/metrics/test/grafana/datasources/datasource.yml b/metrics/test/grafana/datasources/datasource.yml new file mode 100644 index 0000000..a833165 --- /dev/null +++ b/metrics/test/grafana/datasources/datasource.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: VictoriaMetrics + type: prometheus + access: proxy + url: http://pm-metrics-test-victoriametrics:8428 + isDefault: true diff --git a/utils/atomic.go b/utils/atomic.go index 45fd66d..d3d8287 100644 --- a/utils/atomic.go +++ b/utils/atomic.go @@ -5,6 +5,10 @@ import ( "io" "os" + // Version is fixed to commit 353f8196982447d8b12c64f69530e657331e3dbc. + // The follow-up commit removes Windows support. + // TOOD: Check how we want to handle this in the future, possibly ingest + // needed functionality into here. "github.com/google/renameio" )