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

EVEREST-507 Report metrics #252

Merged
merged 6 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}