mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-31 02:52:01 +08:00

### Summary - update the checkout and portal endpoints to use `zeus` instead of license server
332 lines
8.4 KiB
Go
332 lines
8.4 KiB
Go
package license
|
|
|
|
import (
|
|
"context"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
|
|
"sync"
|
|
|
|
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
|
"go.signoz.io/signoz/pkg/types"
|
|
"go.signoz.io/signoz/pkg/types/authtypes"
|
|
|
|
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
|
|
"go.signoz.io/signoz/ee/query-service/model"
|
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
|
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var LM *Manager
|
|
|
|
// validate and update license every 24 hours
|
|
var validationFrequency = 24 * 60 * time.Minute
|
|
|
|
type Manager struct {
|
|
repo *Repo
|
|
mutex sync.Mutex
|
|
validatorRunning bool
|
|
// end the license validation, this is important to gracefully
|
|
// stopping validation and protect in-consistent updates
|
|
done chan struct{}
|
|
// terminated waits for the validate go routine to end
|
|
terminated chan struct{}
|
|
// last time the license was validated
|
|
lastValidated int64
|
|
// keep track of validation failure attempts
|
|
failedAttempts uint64
|
|
// keep track of active license and features
|
|
activeLicenseV3 *model.LicenseV3
|
|
activeFeatures basemodel.FeatureSet
|
|
}
|
|
|
|
func StartManager(db *sqlx.DB, bundb *bun.DB, features ...basemodel.Feature) (*Manager, error) {
|
|
if LM != nil {
|
|
return LM, nil
|
|
}
|
|
|
|
repo := NewLicenseRepo(db, bundb)
|
|
m := &Manager{
|
|
repo: &repo,
|
|
}
|
|
if err := m.start(features...); err != nil {
|
|
return m, err
|
|
}
|
|
|
|
LM = m
|
|
return m, nil
|
|
}
|
|
|
|
// start loads active license in memory and initiates validator
|
|
func (lm *Manager) start(features ...basemodel.Feature) error {
|
|
return lm.LoadActiveLicenseV3(features...)
|
|
}
|
|
|
|
func (lm *Manager) Stop() {
|
|
close(lm.done)
|
|
<-lm.terminated
|
|
}
|
|
|
|
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
|
|
lm.mutex.Lock()
|
|
defer lm.mutex.Unlock()
|
|
|
|
if l == nil {
|
|
return
|
|
}
|
|
|
|
lm.activeLicenseV3 = l
|
|
lm.activeFeatures = append(l.Features, features...)
|
|
// set default features
|
|
setDefaultFeatures(lm)
|
|
|
|
err := lm.InitFeatures(lm.activeFeatures)
|
|
if err != nil {
|
|
zap.L().Panic("Couldn't activate features", zap.Error(err))
|
|
}
|
|
if !lm.validatorRunning {
|
|
// we want to make sure only one validator runs,
|
|
// we already have lock() so good to go
|
|
lm.validatorRunning = true
|
|
go lm.ValidatorV3(context.Background())
|
|
}
|
|
|
|
}
|
|
|
|
func setDefaultFeatures(lm *Manager) {
|
|
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
|
|
}
|
|
|
|
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
|
active, err := lm.repo.GetActiveLicenseV3(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if active != nil {
|
|
lm.SetActiveV3(active, features...)
|
|
} else {
|
|
zap.L().Info("No active license found, defaulting to basic plan")
|
|
// if no active license is found, we default to basic(free) plan with all default features
|
|
lm.activeFeatures = model.BasicPlan
|
|
setDefaultFeatures(lm)
|
|
err := lm.InitFeatures(lm.activeFeatures)
|
|
if err != nil {
|
|
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
|
|
|
|
licenses, err := lm.repo.GetLicensesV3(ctx)
|
|
if err != nil {
|
|
return nil, model.InternalError(err)
|
|
}
|
|
|
|
for _, l := range licenses {
|
|
if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key {
|
|
l.IsCurrent = true
|
|
}
|
|
if l.ValidUntil == -1 {
|
|
// for subscriptions, there is no end-date as such
|
|
// but for showing user some validity we default one year timespan
|
|
l.ValidUntil = l.ValidFrom + 31556926
|
|
}
|
|
response = append(response, l)
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// Validator validates license after an epoch of time
|
|
func (lm *Manager) ValidatorV3(ctx context.Context) {
|
|
zap.L().Info("ValidatorV3 started!")
|
|
defer close(lm.terminated)
|
|
|
|
tick := time.NewTicker(validationFrequency)
|
|
defer tick.Stop()
|
|
|
|
lm.ValidateV3(ctx)
|
|
for {
|
|
select {
|
|
case <-lm.done:
|
|
return
|
|
default:
|
|
select {
|
|
case <-lm.done:
|
|
return
|
|
case <-tick.C:
|
|
lm.ValidateV3(ctx)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
|
|
|
|
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
|
|
if apiError != nil {
|
|
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
|
|
return apiError
|
|
}
|
|
|
|
err := lm.repo.UpdateLicenseV3(ctx, license)
|
|
if err != nil {
|
|
return model.BadRequest(errors.Wrap(err, "failed to update the new license"))
|
|
}
|
|
lm.SetActiveV3(license)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
|
zap.L().Info("License validation started")
|
|
if lm.activeLicenseV3 == nil {
|
|
return nil
|
|
}
|
|
|
|
defer func() {
|
|
lm.mutex.Lock()
|
|
|
|
lm.lastValidated = time.Now().Unix()
|
|
if reterr != nil {
|
|
zap.L().Error("License validation completed with error", zap.Error(reterr))
|
|
|
|
atomic.AddUint64(&lm.failedAttempts, 1)
|
|
// default to basic plan if validation fails for three consecutive times
|
|
if atomic.LoadUint64(&lm.failedAttempts) > 3 {
|
|
zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false))
|
|
lm.activeLicenseV3 = nil
|
|
lm.activeFeatures = model.BasicPlan
|
|
setDefaultFeatures(lm)
|
|
err := lm.InitFeatures(lm.activeFeatures)
|
|
if err != nil {
|
|
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
|
}
|
|
lm.done <- struct{}{}
|
|
lm.validatorRunning = false
|
|
}
|
|
|
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
|
map[string]interface{}{"err": reterr.Error()}, "", true, false)
|
|
} else {
|
|
// reset the failed attempts counter
|
|
atomic.StoreUint64(&lm.failedAttempts, 0)
|
|
zap.L().Info("License validation completed with no errors")
|
|
}
|
|
|
|
lm.mutex.Unlock()
|
|
}()
|
|
|
|
err := lm.RefreshLicense(ctx)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
|
|
defer func() {
|
|
if errResponse != nil {
|
|
claims, ok := authtypes.ClaimsFromContext(ctx)
|
|
if ok {
|
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
|
map[string]interface{}{"err": errResponse.Err.Error()}, claims.Email, true, false)
|
|
}
|
|
}
|
|
}()
|
|
|
|
license, apiError := validate.ValidateLicenseV3(licenseKey)
|
|
if apiError != nil {
|
|
zap.L().Error("failed to get the license", zap.Error(apiError.Err))
|
|
return nil, apiError
|
|
}
|
|
|
|
// insert the new license to the sqlite db
|
|
err := lm.repo.InsertLicenseV3(ctx, license)
|
|
if err != nil {
|
|
zap.L().Error("failed to activate license", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
// license is valid, activate it
|
|
lm.SetActiveV3(license)
|
|
return license, nil
|
|
}
|
|
|
|
func (lm *Manager) GetActiveLicense() *model.LicenseV3 {
|
|
return lm.activeLicenseV3
|
|
}
|
|
|
|
// CheckFeature will be internally used by backend routines
|
|
// for feature gating
|
|
func (lm *Manager) CheckFeature(featureKey string) error {
|
|
feature, err := lm.repo.GetFeature(featureKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if feature.Active {
|
|
return nil
|
|
}
|
|
return basemodel.ErrFeatureUnavailable{Key: featureKey}
|
|
}
|
|
|
|
// GetFeatureFlags returns current active features
|
|
func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
|
|
return lm.repo.GetAllFeatures()
|
|
}
|
|
|
|
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
|
|
featureStatus := make([]types.FeatureStatus, len(features))
|
|
for i, f := range features {
|
|
featureStatus[i] = types.FeatureStatus{
|
|
Name: f.Name,
|
|
Active: f.Active,
|
|
Usage: int(f.Usage),
|
|
UsageLimit: int(f.UsageLimit),
|
|
Route: f.Route,
|
|
}
|
|
}
|
|
return lm.repo.InitFeatures(featureStatus)
|
|
}
|
|
|
|
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
|
|
return lm.repo.UpdateFeature(types.FeatureStatus{
|
|
Name: feature.Name,
|
|
Active: feature.Active,
|
|
Usage: int(feature.Usage),
|
|
UsageLimit: int(feature.UsageLimit),
|
|
Route: feature.Route,
|
|
})
|
|
}
|
|
|
|
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
|
|
featureStatus, err := lm.repo.GetFeature(key)
|
|
if err != nil {
|
|
return basemodel.Feature{}, err
|
|
}
|
|
return basemodel.Feature{
|
|
Name: featureStatus.Name,
|
|
Active: featureStatus.Active,
|
|
Usage: int64(featureStatus.Usage),
|
|
UsageLimit: int64(featureStatus.UsageLimit),
|
|
Route: featureStatus.Route,
|
|
}, nil
|
|
}
|
|
|
|
// GetRepo return the license repo
|
|
func (lm *Manager) GetRepo() *Repo {
|
|
return lm.repo
|
|
}
|