diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 5cdca8e204..1c0171efac 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -8,6 +8,7 @@ import ( "go.signoz.io/signoz/ee/query-service/dao" "go.signoz.io/signoz/ee/query-service/interfaces" "go.signoz.io/signoz/ee/query-service/license" + "go.signoz.io/signoz/ee/query-service/usage" baseapp "go.signoz.io/signoz/pkg/query-service/app" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/cache" @@ -27,6 +28,7 @@ type APIHandlerOptions struct { DialTimeout time.Duration AppDao dao.ModelDao RulesManager *rules.Manager + UsageManager *usage.Manager FeatureFlags baseint.FeatureLookup LicenseManager *license.Manager LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController @@ -82,6 +84,10 @@ func (ah *APIHandler) LM() *license.Manager { return ah.opts.LicenseManager } +func (ah *APIHandler) UM() *usage.Manager { + return ah.opts.UsageManager +} + func (ah *APIHandler) AppDao() dao.ModelDao { return ah.opts.AppDao } @@ -150,6 +156,13 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.getPATs)).Methods(http.MethodGet) router.HandleFunc("/api/v1/pat/{id}", am.OpenAccess(ah.deletePAT)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) + + router.HandleFunc("/api/v2/licenses", + am.ViewAccess(ah.listLicensesV2)). + Methods(http.MethodGet) + ah.APIHandler.RegisterRoutes(router, am) } diff --git a/ee/query-service/app/api/license.go b/ee/query-service/app/api/license.go index e5f5b0ca0a..a24ba122d2 100644 --- a/ee/query-service/app/api/license.go +++ b/ee/query-service/app/api/license.go @@ -4,10 +4,44 @@ import ( "context" "encoding/json" "fmt" - "go.signoz.io/signoz/ee/query-service/model" + "io" "net/http" + + "go.signoz.io/signoz/ee/query-service/constants" + "go.signoz.io/signoz/ee/query-service/model" + "go.uber.org/zap" ) +type tierBreakdown struct { + UnitPrice float64 `json:"unitPrice"` + Quantity int64 `json:"quantity"` + TierStart int64 `json:"tierStart"` + TierEnd int64 `json:"tierEnd"` + TierCost float64 `json:"tierCost"` +} + +type usageResponse struct { + Type string `json:"type"` + Unit string `json:"unit"` + Tiers []tierBreakdown `json:"tiers"` +} + +type details struct { + Total float64 `json:"total"` + Breakdown []usageResponse `json:"breakdown"` + BaseFee float64 `json:"baseFee"` +} + +type billingDetails struct { + Status string `json:"status"` + Data struct { + BillingPeriodStart int64 `json:"billingPeriodStart"` + BillingPeriodEnd int64 `json:"billingPeriodEnd"` + Details details `json:"details"` + Discount float64 `json:"discount"` + } `json:"data"` +} + func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) { licenses, apiError := ah.LM().GetLicenses(context.Background()) if apiError != nil { @@ -38,3 +72,150 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) { ah.Respond(w, license) } + +func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) { + + type checkoutResponse struct { + Status string `json:"status"` + Data struct { + RedirectURL string `json:"redirectURL"` + } `json:"data"` + } + + hClient := &http.Client{} + req, err := http.NewRequest("POST", constants.LicenseSignozIo+"/checkout", r.Body) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) + licenseResp, err := hClient.Do(req) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + // decode response body + var resp checkoutResponse + if err := json.NewDecoder(licenseResp.Body).Decode(&resp); err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + ah.Respond(w, resp.Data) +} + +func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) { + licenseKey := r.URL.Query().Get("licenseKey") + + if licenseKey == "" { + RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil) + return + } + + billingURL := fmt.Sprintf("%s/usage?licenseKey=%s", constants.LicenseSignozIo, licenseKey) + + hClient := &http.Client{} + req, err := http.NewRequest("GET", billingURL, nil) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) + billingResp, err := hClient.Do(req) + if err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + // decode response body + var billingResponse billingDetails + if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil { + RespondError(w, model.InternalError(err), nil) + return + } + + // TODO(srikanthccv):Fetch the current day usage and add it to the response + ah.Respond(w, billingResponse.Data) +} + +func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) { + + licenses, apiError := ah.LM().GetLicenses(context.Background()) + if apiError != nil { + RespondError(w, apiError, nil) + } + + resp := model.Licenses{ + TrialStart: -1, + TrialEnd: -1, + OnTrial: false, + WorkSpaceBlock: false, + Licenses: licenses, + } + + var currentActiveLicenseKey string + + for _, license := range licenses { + if license.IsCurrent { + currentActiveLicenseKey = license.Key + } + } + + // For the case when no license is applied i.e community edition + // There will be no trial details or license details + if currentActiveLicenseKey == "" { + ah.Respond(w, resp) + return + } + + // Fetch trial details + hClient := &http.Client{} + url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + zap.S().Error("Error while creating request for trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid blocking the UI + ah.Respond(w, resp) + return + } + req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) + trialResp, err := hClient.Do(req) + if err != nil { + zap.S().Error("Error while fetching trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid incorrectly blocking the UI + ah.Respond(w, resp) + return + } + defer trialResp.Body.Close() + + trialRespBody, err := io.ReadAll(trialResp.Body) + + if err != nil || trialResp.StatusCode != http.StatusOK { + zap.S().Error("Error while fetching trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid incorrectly blocking the UI + ah.Respond(w, resp) + return + } + + // decode response body + var trialRespData model.SubscriptionServerResp + + if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil { + zap.S().Error("Error while decoding trial details", err) + // If there is an error in fetching trial details, we will still return the license details + // to avoid incorrectly blocking the UI + ah.Respond(w, resp) + return + } + + resp.TrialStart = trialRespData.Data.TrialStart + resp.TrialEnd = trialRespData.Data.TrialEnd + resp.OnTrial = trialRespData.Data.OnTrial + resp.WorkSpaceBlock = trialRespData.Data.WorkSpaceBlock + + ah.Respond(w, resp) +} diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index e36d201ec3..834575643b 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -217,6 +217,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { DialTimeout: serverOptions.DialTimeout, AppDao: modelDao, RulesManager: rm, + UsageManager: usageManager, FeatureFlags: lm, LicenseManager: lm, LogsParsingPipelineController: logParsingPipelineController, diff --git a/ee/query-service/constants/constants.go b/ee/query-service/constants/constants.go index 45fad74da6..4953f4d3eb 100644 --- a/ee/query-service/constants/constants.go +++ b/ee/query-service/constants/constants.go @@ -9,6 +9,7 @@ const ( ) var LicenseSignozIo = "https://license.signoz.io/api/v1" +var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "") var SpanLimitStr = GetOrDefaultEnv("SPAN_LIMIT", "5000") diff --git a/ee/query-service/model/license.go b/ee/query-service/model/license.go index e1e6a997da..3ba89cf456 100644 --- a/ee/query-service/model/license.go +++ b/ee/query-service/model/license.go @@ -89,3 +89,16 @@ func (l *License) ParseFeatures() { l.FeatureSet = BasicPlan } } + +type Licenses struct { + TrialStart int64 `json:"trialStart"` + TrialEnd int64 `json:"trialEnd"` + OnTrial bool `json:"onTrial"` + WorkSpaceBlock bool `json:"workSpaceBlock"` + Licenses []License `json:"licenses"` +} + +type SubscriptionServerResp struct { + Status string `json:"status"` + Data Licenses `json:"data"` +}