Skip to content
This repository has been archived by the owner on Mar 4, 2024. It is now read-only.

Commit

Permalink
EVEREST-507 Report metrics (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
oksana-grishchenko authored Oct 20, 2023
1 parent 6cc5ffa commit ad9c7f7
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 3 deletions.
7 changes: 7 additions & 0 deletions api/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type storage interface {
backupStorageStorage
kubernetesClusterStorage
monitoringInstanceStorage
settingsStorage

Begin(ctx context.Context) *gorm.DB
Close() error
Expand Down Expand Up @@ -67,3 +68,9 @@ type monitoringInstanceStorage interface {
DeleteMonitoringInstance(name string, tx *gorm.DB) error
UpdateMonitoringInstance(name string, params model.UpdateMonitoringInstanceParams) error
}

type settingsStorage interface {
GetEverestID(ctx context.Context) (string, error)
GetSettingByKey(ctx context.Context, key string) (string, error)
InitSettings(ctx context.Context) error
}
10 changes: 10 additions & 0 deletions api/everest.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ func NewEverestServer(c *config.EverestConfig, l *zap.SugaredLogger) (*EverestSe
return e, err
}
err := e.initEverest()
if err != nil {
e.l.Error(err)
return e, err
}

err = e.storage.InitSettings(context.Background())
if err != nil {
e.l.Error(err)
return e, err
}

return e, err
}
Expand Down
151 changes: 151 additions & 0 deletions api/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package api

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"time"

"github.com/google/uuid"

"github.com/percona/percona-everest-backend/cmd/config"
)

const (
telemetryProductFamily = "PRODUCT_FAMILY_EVEREST"

// delay the initial metrics to prevent flooding in case of many restarts.
initialMetricsDelay = 5 * time.Minute
)

// Telemetry is the struct for telemetry reports.
type Telemetry struct {
Reports []Report `json:"reports"`
}

// Report is a struct for a single telemetry report.
type Report struct {
ID string `json:"id"`
CreateTime time.Time `json:"createTime"`
InstanceID string `json:"instanceId"`
ProductFamily string `json:"productFamily"`
Metrics []Metric `json:"metrics"`
}

// Metric represents key-value metrics.
type Metric struct {
Key string `json:"key"`
Value string `json:"value"`
}

func (e *EverestServer) report(ctx context.Context, baseURL string, data Telemetry) error {
b, err := json.Marshal(data)
if err != nil {
e.l.Error(errors.Join(err, errors.New("failed to marshal the telemetry report")))
return err
}

url := fmt.Sprintf("%s/v1/telemetry/GenericReport", baseURL)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
if err != nil {
e.l.Error(errors.Join(err, errors.New("failed to create http request")))
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
e.l.Error(errors.Join(err, errors.New("failed to send telemetry request")))
return err
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK {
e.l.Info("Telemetry service responded with http status ", resp.StatusCode)
}
return nil
}

// RunTelemetryJob runs background job for collecting telemetry.
func (e *EverestServer) RunTelemetryJob(ctx context.Context, c *config.EverestConfig) {
e.l.Debug("Starting background jobs runner.")

interval, err := time.ParseDuration(c.TelemetryInterval)
if err != nil {
e.l.Error(errors.Join(err, errors.New("could not parse telemetry interval")))
return
}

timer := time.NewTimer(initialMetricsDelay)
defer timer.Stop()

for {
select {
case <-ctx.Done():
return
case <-timer.C:
timer.Reset(interval)
err = e.collectMetrics(ctx, c.TelemetryURL)
if err != nil {
e.l.Error(errors.Join(err, errors.New("failed to collect telemetry data")))
}
}
}
}

func (e *EverestServer) collectMetrics(ctx context.Context, url string) error {
everestID, err := e.storage.GetEverestID(ctx)
if err != nil {
e.l.Error(errors.Join(err, errors.New("failed to get Everest settings")))
return err
}

ks, err := e.storage.ListKubernetesClusters(ctx)
if err != nil {
e.l.Error(errors.Join(err, errors.New("could not list Kubernetes clusters")))
return err
}
if len(ks) == 0 {
return nil
}
// FIXME: Revisit it once multi k8s support will be enabled
_, kubeClient, _, err := e.initKubeClient(ctx, ks[0].ID)
if err != nil {
e.l.Error(errors.Join(err, errors.New("could not init kube client for config")))
return err
}

clusters, err := kubeClient.ListDatabaseClusters(ctx)
if err != nil {
e.l.Error(errors.Join(err, errors.New("failed to list database clusters")))
return err
}

types := make(map[string]int, 3)
for _, cl := range clusters.Items {
types[string(cl.Spec.Engine.Type)]++
}

// key - the engine type, value - the amount of db clusters of that type
metrics := make([]Metric, 0, 3)
for key, val := range types {
metrics = append(metrics, Metric{key, strconv.Itoa(val)})
}

report := Telemetry{
[]Report{
{
ID: uuid.NewString(),
CreateTime: time.Now(),
InstanceID: everestID,
ProductFamily: telemetryProductFamily,
Metrics: metrics,
},
},
}

return e.report(ctx, url, report)
}
4 changes: 1 addition & 3 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
// Package config ...
package config

import (
"github.com/kelseyhightower/envconfig"
)
import "github.com/kelseyhightower/envconfig"

//nolint:gochecknoglobals
var (
Expand Down
13 changes: 13 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,23 @@ func main() {
}
}()

tCtx, tCancel := context.WithCancel(context.Background())
if !c.DisableTelemetry {
// To prevent leaking test data to prod,
// the prod TelemetryURL is set for the release builds during the build time.
// The dev TelemetryURL is set only when running `make run-debug`.
if c.TelemetryURL != "" {
go server.RunTelemetryJob(tCtx, c)
} else {
l.Info("Telemetry is not running, the TELEMETRY_URL is not set")
}
}

quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit

tCancel()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
Expand Down
1 change: 1 addition & 0 deletions migrations/000008_create_settings_table.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE settings;
6 changes: 6 additions & 0 deletions migrations/000008_create_settings_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE settings
(
id SERIAL PRIMARY KEY,
key TEXT UNIQUE NOT NULL,
value TEXT
);
66 changes: 66 additions & 0 deletions model/settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// percona-everest-backend
// Copyright (C) 2023 Percona LLC
//
// 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 model

import (
"context"
"errors"

"github.com/google/uuid"
"github.com/jinzhu/gorm"
)

// EverestIDSettingName name of the Everest ID setting.
const everestIDSettingName = "everest_id"

// Setting represents db model for Everest settings.
type Setting struct {
ID string
Key string
Value string
}

// InitSettings creates an Everest settings record.
func (db *Database) InitSettings(ctx context.Context) error {
everestID, err := db.GetEverestID(ctx)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// settings are already initialized
if everestID != "" {
return nil
}

setting := &Setting{Key: everestIDSettingName, Value: uuid.NewString()}
return db.gormDB.Create(setting).Error
}

// GetSettingByKey returns Everest settings.
func (db *Database) GetSettingByKey(_ context.Context, key string) (string, error) {
setting := &Setting{}

err := db.gormDB.First(&setting, "key = ?", key).Error
if err != nil {
return "", err
}

return setting.Value, nil
}

// GetEverestID returns Everest settings.
func (db *Database) GetEverestID(ctx context.Context) (string, error) {
return db.GetSettingByKey(ctx, everestIDSettingName)
}

0 comments on commit ad9c7f7

Please sign in to comment.