diff --git a/changelog/unreleased/read-metrics-from-file.md b/changelog/unreleased/read-metrics-from-file.md new file mode 100644 index 0000000000..d6aabb1c7c --- /dev/null +++ b/changelog/unreleased/read-metrics-from-file.md @@ -0,0 +1,5 @@ +Enhancement: Metrics module can be configured to retrieve metrics data from file. + +- Export site metrics in Prometheus #698 + +https://github.com/cs3org/reva/pull/973 diff --git a/examples/metrics/metrics.toml b/examples/metrics/metrics.toml index caa41975f7..d5203f3b4c 100644 --- a/examples/metrics/metrics.toml +++ b/examples/metrics/metrics.toml @@ -2,6 +2,9 @@ jwt_secret = "Pive-Fumkiu4" [http.services.prometheus] +# metrics_data_driver_type, one of: dummy, json +metrics_data_driver_type = "dummy" +metrics_data_location = "" [http] address = "0.0.0.0:5550" \ No newline at end of file diff --git a/internal/http/services/prometheus/prometheus.go b/internal/http/services/prometheus/prometheus.go index d1392dc499..50536b5e2f 100644 --- a/internal/http/services/prometheus/prometheus.go +++ b/internal/http/services/prometheus/prometheus.go @@ -21,15 +21,16 @@ package prometheus import ( "net/http" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/metrics" + "github.com/cs3org/reva/pkg/metrics/config" + "contrib.go.opencensus.io/exporter/prometheus" "github.com/cs3org/reva/pkg/rhttp/global" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog" "go.opencensus.io/stats/view" - - // Initializes goroutines which periodically update stats - _ "github.com/cs3org/reva/pkg/metrics/reader/dummy" ) func init() { @@ -38,32 +39,57 @@ func init() { // New returns a new prometheus service func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { - conf := &config{} + conf := &config.Config{} if err := mapstructure.Decode(m, conf); err != nil { return nil, err } - conf.init() + conf.Init() + + metrics, err := metrics.New(conf) + if err != nil { + return nil, errors.Wrap(err, "prometheus: error creating metrics") + } + // prometheus handler pe, err := prometheus.NewExporter(prometheus.Options{ Namespace: "revad", }) if err != nil { return nil, errors.Wrap(err, "prometheus: error creating exporter") } + // metricsHandler wraps the prometheus handler + metricsHandler := &MetricsHandler{ + pe: pe, + metrics: metrics, + } + view.RegisterExporter(metricsHandler) - view.RegisterExporter(pe) - return &svc{prefix: conf.Prefix, h: pe}, nil + return &svc{prefix: conf.Prefix, h: metricsHandler}, nil } -type config struct { - Prefix string `mapstructure:"prefix"` +// MetricsHandler struct and methods (ServeHTTP, ExportView) is a wrapper for prometheus Exporter +// so we can override (execute our own logic) before forwarding to the prometheus Exporter: see overriding method MetricsHandler.ServeHTTP() +type MetricsHandler struct { + pe *prometheus.Exporter + metrics *metrics.Metrics } -func (c *config) init() { - if c.Prefix == "" { - c.Prefix = "metrics" +// ServeHTTP override and forward to prometheus.Exporter ServeHTTP() +func (h *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + // make sure the latest metrics data are recorded + if err := h.metrics.RecordMetrics(); err != nil { + log.Err(err).Msg("Unable to record metrics") } + // proceed with regular flow + h.pe.ServeHTTP(w, r) +} + +// ExportView must only be implemented to adhere to prometheus.Exporter signature; we simply forward to prometheus ExportView +func (h *MetricsHandler) ExportView(vd *view.Data) { + // just proceed with regular flow + h.pe.ExportView(vd) } type svc struct { diff --git a/pkg/metrics/config/config.go b/pkg/metrics/config/config.go new file mode 100644 index 0000000000..0cef040983 --- /dev/null +++ b/pkg/metrics/config/config.go @@ -0,0 +1,39 @@ +// Copyright 2018-2020 CERN +// +// 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. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package config + +// Config holds the config options that need to be passed down to the metrics reader +type Config struct { + Prefix string `mapstructure:"prefix"` + MetricsDataDriverType string `mapstructure:"metrics_data_driver_type"` + MetricsDataLocation string `mapstructure:"metrics_data_location"` +} + +// Init sets sane defaults +func (c *Config) Init() { + if c.Prefix == "" { + c.Prefix = "metrics" + } + if c.MetricsDataDriverType == "json" { + // default values + if c.MetricsDataLocation == "" { + c.MetricsDataLocation = "/var/tmp/reva/metrics/metricsdata.json" + } + } +} diff --git a/pkg/metrics/driver/dummy/dummy.go b/pkg/metrics/driver/dummy/dummy.go new file mode 100644 index 0000000000..010201610d --- /dev/null +++ b/pkg/metrics/driver/dummy/dummy.go @@ -0,0 +1,54 @@ +// Copyright 2018-2020 CERN +// +// 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. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package dummy + +import ( + "math/rand" + + "github.com/cs3org/reva/pkg/metrics/config" +) + +// New returns a new MetricsDummyDriver object. +func New(config *config.Config) (*MetricsDummyDriver, error) { + driver := &MetricsDummyDriver{ + config: config, + } + + return driver, nil +} + +// MetricsDummyDriver the MetricsDummyDriver struct +type MetricsDummyDriver struct { + config *config.Config +} + +// GetNumUsers returns the number of site users, it's a dummy number +func (d *MetricsDummyDriver) GetNumUsers() int64 { + return int64(rand.Intn(30000)) +} + +// GetNumGroups returns the number of site groups, it's a dummy number +func (d *MetricsDummyDriver) GetNumGroups() int64 { + return int64(rand.Intn(200)) +} + +// GetAmountStorage returns the amount of site storage used, it's a dummy amount +func (d *MetricsDummyDriver) GetAmountStorage() int64 { + return int64(rand.Intn(70000000000)) +} diff --git a/pkg/metrics/driver/json/json.go b/pkg/metrics/driver/json/json.go new file mode 100644 index 0000000000..29b880dfab --- /dev/null +++ b/pkg/metrics/driver/json/json.go @@ -0,0 +1,92 @@ +// Copyright 2018-2020 CERN +// +// 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. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package json + +import ( + "encoding/json" + "errors" + "io/ioutil" + + "github.com/cs3org/reva/pkg/metrics/config" +) + +// New returns a new MetricsJSONDriver object. +// It reads the data file from the specified config.MetricsDataLocation upon initializing. +// It does not reload the data file for each metric. +func New(config *config.Config) (*MetricsJSONDriver, error) { + // the json driver reads the data metrics file upon initializing + metricsData, err := readJSON(config) + if err != nil { + return nil, err + } + + driver := &MetricsJSONDriver{ + config: config, + data: metricsData, + } + + return driver, nil +} + +func readJSON(config *config.Config) (*data, error) { + if config.MetricsDataLocation == "" { + err := errors.New("Unable to initialize a metrics data driver, has the data location (metrics_data_location) been configured?") + return nil, err + } + + file, err := ioutil.ReadFile(config.MetricsDataLocation) + if err != nil { + return nil, err + } + + data := &data{} + err = json.Unmarshal(file, data) + if err != nil { + return nil, err + } + + return data, nil +} + +type data struct { + NumUsers int64 `json:"cs3_org_sciencemesh_site_total_num_users"` + NumGroups int64 `json:"cs3_org_sciencemesh_site_total_num_groups"` + AmountStorage int64 `json:"cs3_org_sciencemesh_site_total_amount_storage"` +} + +// MetricsJSONDriver the JsonDriver struct that also holds the data +type MetricsJSONDriver struct { + config *config.Config + data *data +} + +// GetNumUsers returns the number of site users +func (d *MetricsJSONDriver) GetNumUsers() int64 { + return d.data.NumUsers +} + +// GetNumGroups returns the number of site groups +func (d *MetricsJSONDriver) GetNumGroups() int64 { + return d.data.NumGroups +} + +// GetAmountStorage returns the amount of site storage used +func (d *MetricsJSONDriver) GetAmountStorage() int64 { + return d.data.AmountStorage +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 6aa00e15f2..af865f38a9 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -19,21 +19,171 @@ package metrics import ( + "context" + "errors" + + "github.com/cs3org/reva/pkg/metrics/config" + "github.com/cs3org/reva/pkg/metrics/driver/dummy" + "github.com/cs3org/reva/pkg/metrics/driver/json" + + "github.com/rs/zerolog/log" + "go.opencensus.io/stats" "go.opencensus.io/stats/view" ) -// Reader is the interface that defines how the metrics will be read. +// New returns a Metrics object +func New(c *config.Config) (*Metrics, error) { + m := &Metrics{ + dataDriverType: "", + dataLocation: "", + dataDriver: nil, + config: c, + NumUsersMeasure: stats.Int64("cs3_org_sciencemesh_site_total_num_users", "The total number of users within this site", stats.UnitDimensionless), + NumGroupsMeasure: stats.Int64("cs3_org_sciencemesh_site_total_num_groups", "The total number of groups within this site", stats.UnitDimensionless), + AmountStorageMeasure: stats.Int64("cs3_org_sciencemesh_site_total_amount_storage", "The total amount of storage used within this site", stats.UnitBytes), + } + + // register the desired measures' views + if err := view.Register( + m.getNumUsersView(), + m.getNumGroupsView(), + m.getAmountStorageView(), + ); err != nil { + log.Error().Err(err).Msg("error registering the driver's views with opencensus exporter") + return nil, err + } + + return m, nil +} + +// Metrics the metrics struct +type Metrics struct { + dataDriverType string + dataLocation string + dataDriver Reader // the metrics data driver is an implemention of Reader + config *config.Config + NumUsersMeasure *stats.Int64Measure + NumGroupsMeasure *stats.Int64Measure + AmountStorageMeasure *stats.Int64Measure +} + +// RecordMetrics records the latest metrics from the metrics data source as OpenCensus stats views. +func (m *Metrics) RecordMetrics() error { + if err := initDataDriver(m); err != nil { + log.Error().Err(err).Msg("Could not set a driver") + return err + } + // record all latest metrics + m.recordNumUsers() + m.recordNumGroups() + m.recordAmountStorage() + + return nil +} + +// initDataDriver initializes a data driver and sets it to be the Metrics.dataDriver +func initDataDriver(m *Metrics) error { + // find out what driver to use + if m.config.MetricsDataDriverType == "" { + err := errors.New("Unable to initialize a metrics data driver, has a driver type (metrics_data_driver_type) been configured?") + return err + } + m.dataLocation = m.config.MetricsDataLocation + + // create/init a driver depending on driver type + if m.config.MetricsDataDriverType == "json" { + // Because the json metrics data file is only read on json driver creation + // a json driver must be re-created to make sure we have the current/latest metrics data. + // Other drivers may need creation only once. + jsonDriver, err := json.New(m.config) + if err != nil { + log.Error().Err(err) + return err + } + m.dataDriver = jsonDriver + log.Debug().Msgf("Metrics uses json driver") + } + if m.config.MetricsDataDriverType == "dummy" && m.dataDriver == nil { + // the dummy driver does not need to be initialized every time + dummyDriver, err := dummy.New(m.config) + if err != nil { + log.Error().Err(err) + return err + } + m.dataDriver = dummyDriver + log.Debug().Msgf("Metrics uses dummy driver") + } + // no known driver configured, return error + if m.dataDriver == nil { + err := errors.New("Unable to initialize a metrics data driver. Has a correct driver type (one of: json, dummy) been configured?") + return err + } + + return nil +} + +// recordNumUsers records the latest number of site users figure +func (m *Metrics) recordNumUsers() { + ctx := context.Background() + stats.Record(ctx, m.NumUsersMeasure.M(m.dataDriver.GetNumUsers())) +} + +func (m *Metrics) getNumUsersView() *view.View { + return &view.View{ + Name: m.NumUsersMeasure.Name(), + Description: m.NumUsersMeasure.Description(), + Measure: m.NumUsersMeasure, + Aggregation: view.LastValue(), + } +} + +// recordNumGroups records the latest number of site groups figure +func (m *Metrics) recordNumGroups() { + ctx := context.Background() + stats.Record(ctx, m.NumGroupsMeasure.M(m.dataDriver.GetNumGroups())) +} + +func (m *Metrics) getNumGroupsView() *view.View { + return &view.View{ + Name: m.NumGroupsMeasure.Name(), + Description: m.NumGroupsMeasure.Description(), + Measure: m.NumGroupsMeasure, + Aggregation: view.LastValue(), + } +} + +// recordAmountStorage records the latest amount storage figure +func (m *Metrics) recordAmountStorage() { + ctx := context.Background() + stats.Record(ctx, m.AmountStorageMeasure.M(m.dataDriver.GetAmountStorage())) +} + +func (m *Metrics) getAmountStorageView() *view.View { + return &view.View{ + Name: m.AmountStorageMeasure.Name(), + Description: m.AmountStorageMeasure.Description(), + Measure: m.AmountStorageMeasure, + Aggregation: view.LastValue(), + } +} + +// Reader is the interface that defines the metrics to read. +// Any metrics data driver must implement this interface. +// Each function should return the current/latest available metrics figure relevant to that function. type Reader interface { // GetNumUsersView returns an OpenCensus stats view which records the // number of users registered in the mesh provider. - GetNumUsersView() *view.View + // Metric name: cs3_org_sciencemesh_site_total_num_users + GetNumUsers() int64 // GetNumGroupsView returns an OpenCensus stats view which records the // number of user groups registered in the mesh provider. - GetNumGroupsView() *view.View + // Metric name: cs3_org_sciencemesh_site_total_num_groups + GetNumGroups() int64 // GetAmountStorageView returns an OpenCensus stats view which records the // amount of storage in the system. - GetAmountStorageView() *view.View + // Metric name: cs3_org_sciencemesh_site_total_amount_storage + GetAmountStorage() int64 } diff --git a/pkg/metrics/reader/dummy/dummy.go b/pkg/metrics/reader/dummy/dummy.go deleted file mode 100644 index d12947c0fc..0000000000 --- a/pkg/metrics/reader/dummy/dummy.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2018-2020 CERN -// -// 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. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package dummy - -import ( - "context" - "math/rand" - "os" - "time" - - "github.com/cs3org/reva/pkg/logger" - "github.com/cs3org/reva/pkg/metrics" - "go.opencensus.io/stats" - "go.opencensus.io/stats/view" -) - -func init() { - - log := logger.New().With().Int("pid", os.Getpid()).Logger() - - m := &Metrics{ - NumUsersMeasure: stats.Int64("cs3_org_sciencemesh_site_total_num_users", "The total number of users within this site", stats.UnitDimensionless), - NumGroupsMeasure: stats.Int64("cs3_org_sciencemesh_site_total_num_groups", "The total number of groups within this site", stats.UnitDimensionless), - AmountStorageMeasure: stats.Int64("cs3_org_sciencemesh_site_total_amount_storage", "The total amount of storage used within this site", stats.UnitBytes), - } - - // Verify that the struct implements the metrics.Reader interface - _, ok := interface{}(m).(metrics.Reader) - if !ok { - log.Error().Msg("the driver does not implement the metrics.Reader interface") - return - } - - // register the desired measures' views - if err := view.Register( - m.GetNumUsersView(), - m.GetNumGroupsView(), - m.GetAmountStorageView(), - ); err != nil { - log.Error().Err(err).Msg("error registering views with opencensus exporter") - return - } - - // call the actual metric provider functions for the latest metrics every 4th second - go func() { - rand.Seed(time.Now().UnixNano()) - for { - m.getNumUsers() - m.getNumGroups() - m.getAmountStorage() - time.Sleep(4 * time.Second) - } - }() -} - -// Metrics returns randomly generated values for the defined metrics. -type Metrics struct { - numUsersCounter int64 - amountStorageCounter int64 - - NumUsersMeasure *stats.Int64Measure - NumGroupsMeasure *stats.Int64Measure - AmountStorageMeasure *stats.Int64Measure -} - -// getNumberUsers links to the underlying number of site users provider -func (m *Metrics) getNumUsers() { - ctx := context.Background() - m.numUsersCounter += int64(rand.Intn(100)) - stats.Record(ctx, m.NumUsersMeasure.M(m.numUsersCounter)) -} - -// GetNumUsersView returns the number of site users measure view -func (m *Metrics) GetNumUsersView() *view.View { - return &view.View{ - Name: m.NumUsersMeasure.Name(), - Description: m.NumUsersMeasure.Description(), - Measure: m.NumUsersMeasure, - Aggregation: view.LastValue(), - } -} - -// getNumberGroups links to the underlying number of site groups provider -func (m *Metrics) getNumGroups() { - ctx := context.Background() - var numGroupsCounter = int64(rand.Intn(100)) - stats.Record(ctx, m.NumGroupsMeasure.M(numGroupsCounter)) -} - -// GetNumGroupsView returns the number of site groups measure view -func (m *Metrics) GetNumGroupsView() *view.View { - return &view.View{ - Name: m.NumGroupsMeasure.Name(), - Description: m.NumGroupsMeasure.Description(), - Measure: m.NumGroupsMeasure, - Aggregation: view.LastValue(), - } -} - -// getAmountStorage links to the underlying amount of storage provider -func (m *Metrics) getAmountStorage() { - ctx := context.Background() - m.amountStorageCounter += int64(rand.Intn(12865000)) - stats.Record(ctx, m.AmountStorageMeasure.M(m.amountStorageCounter)) -} - -// GetAmountStorageView returns the amount of site storage measure view -func (m *Metrics) GetAmountStorageView() *view.View { - return &view.View{ - Name: m.AmountStorageMeasure.Name(), - Description: m.AmountStorageMeasure.Description(), - Measure: m.AmountStorageMeasure, - Aggregation: view.LastValue(), - } -} diff --git a/pkg/metrics/reader/script/script.go b/pkg/metrics/reader/script/script.go deleted file mode 100644 index 79617d5c00..0000000000 --- a/pkg/metrics/reader/script/script.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2018-2020 CERN -// -// 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. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package script - -import ( - "os" - - "github.com/cs3org/reva/pkg/logger" - "github.com/cs3org/reva/pkg/metrics" - "go.opencensus.io/stats" - "go.opencensus.io/stats/view" -) - -func init() { - - log := logger.New().With().Int("pid", os.Getpid()).Logger() - - m := &Metrics{ - NumUsersMeasure: stats.Int64("cs3_org_sciencemesh_site_total_num_users", "The total number of users within this site", stats.UnitDimensionless), - NumGroupsMeasure: stats.Int64("cs3_org_sciencemesh_site_total_num_groups", "The total number of groups within this site", stats.UnitDimensionless), - AmountStorageMeasure: stats.Int64("cs3_org_sciencemesh_site_total_amount_storage", "The total amount of storage used within this site", stats.UnitBytes), - } - - // Verify that the struct implements the metrics.Reader interface - _, ok := interface{}(m).(metrics.Reader) - if !ok { - log.Error().Msg("the driver does not implement the metrics.Reader interface") - return - } - - // register the desired measures' views - if err := view.Register( - m.GetNumUsersView(), - m.GetNumGroupsView(), - m.GetAmountStorageView(), - ); err != nil { - log.Error().Err(err).Msg("error registering views with opencensus exporter") - return - } -} - -// Metrics returns randomly generated values for the defined metrics. -type Metrics struct { - NumUsersMeasure *stats.Int64Measure - NumGroupsMeasure *stats.Int64Measure - AmountStorageMeasure *stats.Int64Measure -} - -// GetNumUsersView returns the number of site users measure view -func (m *Metrics) GetNumUsersView() *view.View { - return nil -} - -// GetNumGroupsView returns the number of site groups measure view -func (m *Metrics) GetNumGroupsView() *view.View { - return nil -} - -// GetAmountStorageView returns the amount of site storage measure view -func (m *Metrics) GetAmountStorageView() *view.View { - return nil -}