Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lago integration #4620

Merged
merged 16 commits into from
May 15, 2024
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
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
MauAraujo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading