feat(analytics): dashboards

This commit is contained in:
Shekar Siri 2024-11-27 16:13:26 +01:00
parent f5df3fb5b5
commit 99085a95a1
8 changed files with 490 additions and 0 deletions

View file

@ -0,0 +1,43 @@
package main
import (
"context"
analyticsConfig "openreplay/backend/internal/config/analytics"
"openreplay/backend/pkg/analytics"
"openreplay/backend/pkg/db/postgres/pool"
"openreplay/backend/pkg/logger"
"openreplay/backend/pkg/metrics"
analyticsMetrics "openreplay/backend/pkg/metrics/analytics"
databaseMetrics "openreplay/backend/pkg/metrics/database"
"openreplay/backend/pkg/metrics/web"
"openreplay/backend/pkg/server"
"openreplay/backend/pkg/server/api"
)
func main() {
ctx := context.Background()
log := logger.New()
cfg := analyticsConfig.New(log)
webMetrics := web.New("analytics")
metrics.New(log, append(webMetrics.List(), append(analyticsMetrics.List(), databaseMetrics.List()...)...))
pgConn, err := pool.New(cfg.Postgres.String())
if err != nil {
log.Fatal(ctx, "can't init postgres connection: %s", err)
}
defer pgConn.Close()
builder, err := analytics.NewServiceBuilder(log, cfg, webMetrics, pgConn)
if err != nil {
log.Fatal(ctx, "can't init services: %s", err)
}
router, err := api.NewRouter(&cfg.HTTP, log)
if err != nil {
log.Fatal(ctx, "failed while creating router: %s", err)
}
router.AddHandlers(api.NoPrefix, builder.AnalyticsAPI)
router.AddMiddlewares(builder.Auth.Middleware, builder.RateLimiter.Middleware, builder.AuditTrail.Middleware)
server.Run(ctx, log, &cfg.HTTP, router)
}

View file

@ -0,0 +1,29 @@
package analytics
import (
"time"
"openreplay/backend/internal/config/common"
"openreplay/backend/internal/config/configurator"
"openreplay/backend/internal/config/objectstorage"
"openreplay/backend/internal/config/redis"
"openreplay/backend/pkg/env"
"openreplay/backend/pkg/logger"
)
type Config struct {
common.Config
common.Postgres
redis.Redis
objectstorage.ObjectsConfig
common.HTTP
FSDir string `env:"FS_DIR,required"`
ProjectExpiration time.Duration `env:"PROJECT_EXPIRATION,default=10m"`
WorkerID uint16
}
func New(log logger.Logger) *Config {
cfg := &Config{WorkerID: env.WorkerID()}
configurator.Process(log, cfg)
return cfg
}

View file

@ -0,0 +1,205 @@
package api
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"net/http"
"openreplay/backend/pkg/server/api"
"openreplay/backend/pkg/server/user"
"strconv"
"time"
)
func getId(r *http.Request) (int, error) {
vars := mux.Vars(r)
idStr := vars["id"]
if idStr == "" {
return 0, fmt.Errorf("invalid dashboard ID")
}
id, err := strconv.Atoi(idStr)
if err != nil {
return 0, fmt.Errorf("invalid dashboard ID")
}
return id, nil
}
func (e *handlersImpl) createDashboard(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
bodyBytes, err := api.ReadBody(e.log, w, r, e.jsonSizeLimit)
if err != nil {
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize)
return
}
bodySize = len(bodyBytes)
req := &CreateDashboardRequest{}
if err := json.Unmarshal(bodyBytes, req); err != nil {
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
return
}
resp := &GetDashboardResponse{
Dashboard: Dashboard{
DashboardID: 1,
Name: req.Name,
Description: req.Description,
IsPublic: req.IsPublic,
IsPinned: req.IsPinned,
},
}
currentUser := r.Context().Value("userData").(*user.User)
e.log.Info(r.Context(), "User ID: ", currentUser.ID)
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
}
// getDashboards
func (e *handlersImpl) getDashboards(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
//id, err := getId(r)
//if err != nil {
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
// return
//}
resp := &GetDashboardsResponse{
Dashboards: []Dashboard{
{
DashboardID: 1,
Name: "Dashboard",
Description: "Description",
IsPublic: true,
IsPinned: false,
},
},
Total: 1,
}
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
}
func (e *handlersImpl) getDashboard(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
id, err := getId(r)
if err != nil {
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
return
}
resp := &GetDashboardResponse{
Dashboard: Dashboard{
DashboardID: id,
Name: "Dashboard",
Description: "Description",
IsPublic: true,
IsPinned: false,
},
}
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
}
func (e *handlersImpl) updateDashboard(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
//id, err := getId(r)
//if err != nil {
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
// return
//}
bodyBytes, err := api.ReadBody(e.log, w, r, e.jsonSizeLimit)
if err != nil {
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize)
return
}
bodySize = len(bodyBytes)
req := &UpdateDashboardRequest{}
if err := json.Unmarshal(bodyBytes, req); err != nil {
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
return
}
resp := &GetDashboardResponse{
Dashboard: Dashboard{
DashboardID: 1,
Name: req.Name,
Description: req.Description,
IsPublic: req.IsPublic,
IsPinned: req.IsPinned,
},
}
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
}
func (e *handlersImpl) deleteDashboard(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
//id, err := getId(r)
//if err != nil {
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
// return
//}
e.log.Info(r.Context(), "Dashboard deleted")
e.responser.ResponseOK(e.log, r.Context(), w, startTime, r.URL.Path, bodySize)
}
func (e *handlersImpl) pinDashboard(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
//id, err := getId(r)
//if err != nil {
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
// return
//}
e.log.Info(r.Context(), "Dashboard pinned")
e.responser.ResponseOK(e.log, r.Context(), w, startTime, r.URL.Path, bodySize)
}
// add card to dashboard
func (e *handlersImpl) addCardToDashboard(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
//id, err := getId(r)
//if err != nil {
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
// return
//}
e.log.Info(r.Context(), "Card added to dashboard")
e.responser.ResponseOK(e.log, r.Context(), w, startTime, r.URL.Path, bodySize)
}
// remove card from dashboard
func (e *handlersImpl) removeCardFromDashboard(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
bodySize := 0
//id, err := getId(r)
//if err != nil {
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
// return
//}
e.responser.ResponseOK(e.log, r.Context(), w, startTime, r.URL.Path, bodySize)
}

View file

@ -0,0 +1,40 @@
package api
import (
config "openreplay/backend/internal/config/analytics"
"openreplay/backend/pkg/analytics/service"
"openreplay/backend/pkg/logger"
"openreplay/backend/pkg/objectstorage"
"openreplay/backend/pkg/server/api"
"openreplay/backend/pkg/server/keys"
)
type handlersImpl struct {
log logger.Logger
responser *api.Responser
objStorage objectstorage.ObjectStorage
jsonSizeLimit int64
keys keys.Keys
service service.Service
}
func (e *handlersImpl) GetAll() []*api.Description {
return []*api.Description{
{"/v1/analytics/{projectId}/dashboards", e.createDashboard, "POST"},
{"/v1/analytics/{projectId}/dashboards", e.getDashboards, "GET"},
{"/v1/analytics/{projectId}/dashboards/{id}", e.getDashboard, "GET"},
{"/v1/analytics/{projectId}/dashboards/{id}", e.updateDashboard, "PUT"},
{"/v1/analytics/{projectId}/dashboards/{id}", e.deleteDashboard, "DELETE"},
}
}
func NewHandlers(log logger.Logger, cfg *config.Config, responser *api.Responser, objStore objectstorage.ObjectStorage, keys keys.Keys, service service.Service) (api.Handlers, error) {
return &handlersImpl{
log: log,
responser: responser,
objStorage: objStore,
jsonSizeLimit: cfg.JsonSizeLimit,
keys: keys,
service: service,
}, nil
}

View file

@ -0,0 +1,60 @@
package api
type Dashboard struct {
DashboardID int `json:"dashboard_id"`
Name string `json:"name"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
IsPinned bool `json:"is_pinned"`
}
type CreateDashboardResponse struct {
DashboardID int `json:"dashboard_id"`
}
type GetDashboardResponse struct {
Dashboard
}
type GetDashboardsResponse struct {
Dashboards []Dashboard `json:"dashboards"`
Total uint64 `json:"total"`
}
// REQUESTS
type CreateDashboardRequest struct {
Name string `json:"name"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
IsPinned bool `json:"is_pinned"`
Metrics []int `json:"metrics"`
}
type GetDashboardsRequest struct {
Page uint64 `json:"page"`
Limit uint64 `json:"limit"`
Order string `json:"order"`
Query string `json:"query"`
FilterBy string `json:"filterBy"`
}
type UpdateDashboardRequest struct {
Name string `json:"name"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
IsPinned bool `json:"is_pinned"`
Metrics []int `json:"metrics"`
}
type PinDashboardRequest struct {
IsPinned bool `json:"is_pinned"`
}
type AddCardToDashboardRequest struct {
CardIDs []int `json:"card_ids"`
}
type DeleteCardFromDashboardRequest struct {
CardIDs []int `json:"card_ids"`
}

View file

@ -0,0 +1,57 @@
package analytics
import (
"openreplay/backend/pkg/metrics/web"
"openreplay/backend/pkg/server/tracer"
"time"
"openreplay/backend/internal/config/analytics"
analyticsAPI "openreplay/backend/pkg/analytics/api"
"openreplay/backend/pkg/analytics/service"
"openreplay/backend/pkg/db/postgres/pool"
"openreplay/backend/pkg/logger"
"openreplay/backend/pkg/objectstorage/store"
"openreplay/backend/pkg/server/api"
"openreplay/backend/pkg/server/auth"
"openreplay/backend/pkg/server/keys"
"openreplay/backend/pkg/server/limiter"
)
type ServicesBuilder struct {
Auth auth.Auth
RateLimiter *limiter.UserRateLimiter
AuditTrail tracer.Tracer
AnalyticsAPI api.Handlers
}
func NewServiceBuilder(log logger.Logger, cfg *analytics.Config, webMetrics web.Web, pgconn pool.Pool) (*ServicesBuilder, error) {
objStore, err := store.NewStore(&cfg.ObjectsConfig)
if err != nil {
return nil, err
}
newKeys := keys.NewKeys(log, pgconn)
responser := api.NewResponser(webMetrics)
audiTrail, err := tracer.NewTracer(log, pgconn)
if err != nil {
return nil, err
}
analyticsService, err := service.NewService(log, pgconn, objStore)
if err != nil {
return nil, err
}
handlers, err := analyticsAPI.NewHandlers(log, cfg, responser, objStore, keys.NewKeys(log, pgconn), analyticsService)
if err != nil {
return nil, err
}
return &ServicesBuilder{
Auth: auth.NewAuth(log, cfg.JWTSecret, cfg.JWTSpotSecret, pgconn, newKeys),
RateLimiter: limiter.NewUserRateLimiter(10, 30, 1*time.Minute, 5*time.Minute),
AuditTrail: audiTrail,
AnalyticsAPI: handlers,
}, nil
}

View file

@ -0,0 +1,34 @@
package service
import (
"errors"
"openreplay/backend/pkg/db/postgres/pool"
"openreplay/backend/pkg/logger"
"openreplay/backend/pkg/objectstorage"
)
type Service interface {
}
type serviceImpl struct {
log logger.Logger
conn pool.Pool
storage objectstorage.ObjectStorage
}
func NewService(log logger.Logger, conn pool.Pool, storage objectstorage.ObjectStorage) (Service, error) {
switch {
case log == nil:
return nil, errors.New("logger is empty")
case conn == nil:
return nil, errors.New("connection pool is empty")
case storage == nil:
return nil, errors.New("object storage is empty")
}
return &serviceImpl{
log: log,
conn: conn,
storage: storage,
}, nil
}

View file

@ -0,0 +1,22 @@
package analytics
import (
"github.com/prometheus/client_golang/prometheus"
"openreplay/backend/pkg/metrics/common"
)
var cardCreated = prometheus.NewHistogram(
prometheus.HistogramOpts{
Namespace: "card",
Name: "created",
Help: "Histogram for tracking card creation",
Buckets: common.DefaultBuckets,
},
)
func List() []prometheus.Collector {
return []prometheus.Collector{
cardCreated,
}
}