Skip to content

Commit

Permalink
Lago integration (#4620)
Browse files Browse the repository at this point in the history
Co-authored-by: jusrhee <[email protected]>
  • Loading branch information
MauAraujo and jusrhee authored May 15, 2024
1 parent 0a1f90f commit a29f0b1
Show file tree
Hide file tree
Showing 32 changed files with 943 additions and 1,367 deletions.
13 changes: 6 additions & 7 deletions api/server/handlers/billing/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (c *CreateBillingHandler) grantRewardIfReferral(ctx context.Context, referr
return telemetry.Error(ctx, span, err, "failed to get referral count by referrer id")
}

maxReferralRewards := c.Config().BillingManager.MetronomeClient.MaxReferralRewards
maxReferralRewards := c.Config().BillingManager.LagoClient.MaxReferralRewards
if referralCount >= maxReferralRewards {
return nil
}
Expand All @@ -147,13 +147,12 @@ func (c *CreateBillingHandler) grantRewardIfReferral(ctx context.Context, referr
}

if referral != nil && referral.Status != models.ReferralStatusCompleted {
// Metronome requires an expiration to be passed in, so we set it to 5 years which in
// Lago requires an expiration to be passed in, so we set it to 5 years which in
// practice will mean the credits will most likely run out before expiring
expiresAt := time.Now().AddDate(5, 0, 0).Format(time.RFC3339)
reason := "Referral reward"
rewardAmount := c.Config().BillingManager.MetronomeClient.DefaultRewardAmountCents
paidAmount := c.Config().BillingManager.MetronomeClient.DefaultPaidAmountCents
err := c.Config().BillingManager.MetronomeClient.CreateCreditsGrant(ctx, referrerProject.UsageID, reason, rewardAmount, paidAmount, expiresAt)
expiresAt := time.Now().AddDate(5, 0, 0)
name := "Referral reward"
rewardAmount := c.Config().BillingManager.LagoClient.DefaultRewardAmountCents
err := c.Config().BillingManager.LagoClient.CreateCreditsGrant(ctx, referrerProject.ID, name, rewardAmount, &expiresAt, referrerProject.EnableSandbox)
if err != nil {
return telemetry.Error(ctx, span, err, "failed to grand credits reward")
}
Expand Down
34 changes: 20 additions & 14 deletions api/server/handlers/billing/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,17 @@ func (c *IngestEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
telemetry.AttributeKV{Key: "porter-cloud-enabled", Value: proj.EnableSandbox},
)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
telemetry.AttributeKV{Key: "porter-cloud-enabled", Value: proj.EnableSandbox},
)
if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
return
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
)

ingestEventsRequest := struct {
Events []types.BillingEvent `json:"billing_events"`
}{}
Expand All @@ -69,14 +64,25 @@ func (c *IngestEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
telemetry.AttributeKV{Key: "usage-events-count", Value: len(ingestEventsRequest.Events)},
)

// For Porter Cloud events, we apend a prefix to avoid collisions before sending to Metronome
// For Porter Cloud events, we apend a prefix to avoid collisions before sending to Lago
if proj.EnableSandbox {
for i := range ingestEventsRequest.Events {
ingestEventsRequest.Events[i].CustomerID = fmt.Sprintf("porter-cloud-%s", ingestEventsRequest.Events[i].CustomerID)
}
}

err := c.Config().BillingManager.MetronomeClient.IngestEvents(ctx, ingestEventsRequest.Events)
plan, err := c.Config().BillingManager.LagoClient.GetCustomeActivePlan(ctx, proj.ID, proj.EnableSandbox)
if err != nil {
err := telemetry.Error(ctx, span, err, "error getting active subscription")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID},
)

err = c.Config().BillingManager.LagoClient.IngestEvents(ctx, plan.ID, ingestEventsRequest.Events, proj.EnableSandbox)
if err != nil {
err := telemetry.Error(ctx, span, err, "error ingesting events")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
Expand Down
24 changes: 7 additions & 17 deletions api/server/handlers/billing/invoices.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package billing

import (
"fmt"
"net/http"

"github.com/porter-dev/porter/api/server/handlers"
Expand All @@ -25,7 +24,7 @@ func NewListCustomerInvoicesHandler(
writer shared.ResultWriter,
) *ListCustomerInvoicesHandler {
return &ListCustomerInvoicesHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
}
}

Expand All @@ -36,31 +35,22 @@ func (c *ListCustomerInvoicesHandler) ServeHTTP(w http.ResponseWriter, r *http.R
proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "billing-config-exists", Value: c.Config().BillingManager.StripeConfigLoaded},
telemetry.AttributeKV{Key: "billing-enabled", Value: proj.GetFeatureFlag(models.BillingEnabled, c.Config().LaunchDarklyClient)},
telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
telemetry.AttributeKV{Key: "porter-cloud-enabled", Value: proj.EnableSandbox},
)

if !c.Config().BillingManager.StripeConfigLoaded || !proj.GetFeatureFlag(models.BillingEnabled, c.Config().LaunchDarklyClient) {
if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
return
}

req := &types.ListCustomerInvoicesRequest{}

if ok := c.DecodeAndValidate(w, r, req); !ok {
err := telemetry.Error(ctx, span, nil, "error decoding list customer usage request")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

invoices, err := c.Config().BillingManager.StripeClient.ListCustomerInvoices(ctx, proj.BillingID, req.Status)
invoices, err := c.Config().BillingManager.LagoClient.ListCustomerFinalizedInvoices(ctx, proj.ID, proj.EnableSandbox)
if err != nil {
err = telemetry.Error(ctx, span, err, fmt.Sprintf("error listing invoices for customer %s", proj.BillingID))
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
err = telemetry.Error(ctx, span, err, "error listing invoices")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

// Write the response to the frontend
c.WriteResult(w, r, invoices)
}
39 changes: 23 additions & 16 deletions api/server/handlers/billing/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"net/http"

"github.com/google/uuid"
"github.com/porter-dev/porter/api/server/handlers"
"github.com/porter-dev/porter/api/server/shared"
"github.com/porter-dev/porter/api/server/shared/apierrors"
Expand Down Expand Up @@ -100,10 +99,9 @@ func (c *CheckPaymentEnabledHandler) ensureBillingSetup(ctx context.Context, pro

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "billing-id", Value: proj.BillingID},
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
)

if proj.BillingID == "" || proj.UsageID == uuid.Nil {
if proj.BillingID == "" {
adminUser, err := c.getAdminUser(ctx, proj.ID)
if err != nil {
return telemetry.Error(ctx, span, err, "error getting admin user")
Expand All @@ -119,11 +117,19 @@ func (c *CheckPaymentEnabledHandler) ensureBillingSetup(ctx context.Context, pro
if err != nil {
return telemetry.Error(ctx, span, err, "error ensuring Stripe customer exists")
}
}

lagoCustomerExists := false
if !lagoCustomerExists {
adminUser, err := c.getAdminUser(ctx, proj.ID)
if err != nil {
return telemetry.Error(ctx, span, err, "error getting admin user")
}

// Create usage customer for project and set the usage ID if it doesn't exist
err = c.ensureMetronomeCustomerExists(ctx, adminUser.Email, proj)
err = c.ensureLagoCustomerExists(ctx, adminUser.Email, proj)
if err != nil {
return telemetry.Error(ctx, span, err, "error ensuring Metronome customer exists")
return telemetry.Error(ctx, span, err, "error ensuring Lago customer exists")
}
}

Expand Down Expand Up @@ -189,30 +195,31 @@ func (c *CheckPaymentEnabledHandler) ensureStripeCustomerExists(ctx context.Cont
return nil
}

func (c *CheckPaymentEnabledHandler) ensureMetronomeCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
ctx, span := telemetry.NewSpan(ctx, "ensure-metronome-customer-exists")
func (c *CheckPaymentEnabledHandler) ensureLagoCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
ctx, span := telemetry.NewSpan(ctx, "ensure-lago-customer-exists")
defer span.End()

if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) || proj.UsageID != uuid.Nil {
if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
return nil
}

customerID, customerPlanID, err := c.Config().BillingManager.MetronomeClient.CreateCustomerWithPlan(ctx, adminUserEmail, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
// Check if the customer already exists
exists, err := c.Config().BillingManager.LagoClient.CheckIfCustomerExists(ctx, proj.ID, proj.EnableSandbox)
if err != nil {
return telemetry.Error(ctx, span, err, "error creating Metronome customer")
return telemetry.Error(ctx, span, err, "error while checking if customer exists")
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
telemetry.AttributeKV{Key: "usage-plan-id", Value: proj.UsagePlanID},
telemetry.AttributeKV{Key: "customer-exists", Value: exists},
)

proj.UsageID = customerID
proj.UsagePlanID = customerPlanID
if exists {
return nil
}

_, err = c.Repo().Project().UpdateProject(proj)
err = c.Config().BillingManager.LagoClient.CreateCustomerWithPlan(ctx, adminUserEmail, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
if err != nil {
return telemetry.Error(ctx, span, err, "error updating project")
return telemetry.Error(ctx, span, err, "error creating Lago customer")
}

return nil
Expand Down
100 changes: 27 additions & 73 deletions api/server/handlers/billing/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,27 @@ func (c *ListPlansHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
)
if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
return
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
)

plan, err := c.Config().BillingManager.MetronomeClient.ListCustomerPlan(ctx, proj.UsageID)
plan, err := c.Config().BillingManager.LagoClient.GetCustomeActivePlan(ctx, proj.ID, proj.EnableSandbox)
if err != nil {
err := telemetry.Error(ctx, span, err, "error listing plans")
err := telemetry.Error(ctx, span, err, "error getting active subscription")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID},
)

c.WriteResult(w, r, plan)
}

Expand All @@ -79,28 +78,23 @@ func (c *ListCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
)
if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
return
}

credits, err := c.Config().BillingManager.MetronomeClient.ListCustomerCredits(ctx, proj.UsageID)
credits, err := c.Config().BillingManager.LagoClient.ListCustomerCredits(ctx, proj.ID, proj.EnableSandbox)
if err != nil {
err := telemetry.Error(ctx, span, err, "error listing credits")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
)

c.WriteResult(w, r, credits)
}

Expand All @@ -127,12 +121,11 @@ func (c *ListCustomerUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
)

if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
return
}
Expand All @@ -145,59 +138,20 @@ func (c *ListCustomerUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
return
}

usage, err := c.Config().BillingManager.MetronomeClient.ListCustomerUsage(ctx, proj.UsageID, req.StartingOn, req.EndingBefore, req.WindowSize, req.CurrentPeriod)
plan, err := c.Config().BillingManager.LagoClient.GetCustomeActivePlan(ctx, proj.ID, proj.EnableSandbox)
if err != nil {
err := telemetry.Error(ctx, span, err, "error listing customer usage")
err := telemetry.Error(ctx, span, err, "error getting active subscription")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}
c.WriteResult(w, r, usage)
}

// ListCustomerCostsHandler returns customer usage aggregations like CPU and RAM hours.
type ListCustomerCostsHandler struct {
handlers.PorterHandlerReadWriter
}

// NewListCustomerCostsHandler returns a new ListCustomerCostsHandler
func NewListCustomerCostsHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *ListCustomerCostsHandler {
return &ListCustomerCostsHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
}
}

func (c *ListCustomerCostsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-list-customer-costs")
defer span.End()

proj, _ := ctx.Value(types.ProjectScope).(*models.Project)

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID},
)

if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
c.WriteResult(w, r, "")
return
}

req := &types.ListCustomerCostsRequest{}

if ok := c.DecodeAndValidate(w, r, req); !ok {
err := telemetry.Error(ctx, span, nil, "error decoding list customer costs request")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}

usage, err := c.Config().BillingManager.MetronomeClient.ListCustomerCosts(ctx, proj.UsageID, req.StartingOn, req.EndingBefore, req.Limit)
usage, err := c.Config().BillingManager.LagoClient.ListCustomerUsage(ctx, plan.CustomerID, plan.ID, req.CurrentPeriod, req.PreviousPeriods)
if err != nil {
err := telemetry.Error(ctx, span, err, "error listing customer costs")
err := telemetry.Error(ctx, span, err, "error listing customer usage")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}
Expand Down
1 change: 0 additions & 1 deletion api/server/handlers/cluster/install_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ func (c *InstallAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
"clusterID": fmt.Sprintf("%d", cluster.ID),
"projectID": fmt.Sprintf("%d", proj.ID),
"prometheusURL": c.Config().ServerConf.PrometheusUrl,
"metronomeKey": c.Config().ServerConf.MetronomeAPIKey,
},
"loki": map[string]interface{}{},
}
Expand Down
Loading

0 comments on commit a29f0b1

Please sign in to comment.