Skip to content

Added API call to fetch usage data#12646

Merged
roboquat merged 1 commit into
mainfrom
se/ledger-usage-view
Sep 6, 2022
Merged

Added API call to fetch usage data#12646
roboquat merged 1 commit into
mainfrom
se/ledger-usage-view

Conversation

@svenefftinge

Copy link
Copy Markdown
Contributor

Description

Added API call to fetch usage data

Related Issue(s)

Fixes #

How to test

Release Notes

NONE

Documentation

Werft options:

  • /werft with-preview

@svenefftinge
svenefftinge requested a review from a team September 5, 2022 10:32
@github-actions github-actions Bot added team: webapp Issue belongs to the WebApp team and removed size/XXL labels Sep 5, 2022
@svenefftinge
svenefftinge marked this pull request as draft September 5, 2022 10:33
rpc ListBilledUsage(ListBilledUsageRequest) returns (ListBilledUsageResponse) {}

// ListBilledUsage retrieves all usage for the specified attributionId
rpc FindUsage(FindUsageRequest) returns (FindUsageResponse) {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we followed CRUD more closely then the RPC would generally be Get/List/Create/Delete/Update but I'm not too fussed here. It mostly stands out given the RPC above uses List


message FindUsageResponse {
repeated BilledSession sessions = 1;
double total_credits_used = 2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this correctly then this would return the total credits for the period in the request, correct? For scenarios like this, the proto is a good place to put comments with this as they are included in all of the code-generated clients so docs can be right there with the SDK

@svenefftinge
svenefftinge marked this pull request as ready for review September 5, 2022 12:59
Comment thread components/usage/pkg/apiv1/usage.go Outdated
Comment on lines +162 to +168
log.Log.
WithField("attribution_id", in.AttributionId).
WithField("perPage", limit).
WithField("page", page).
WithField("from", from).
WithField("to", to).
WithError(err).Error("Failed to fetch usage.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields are quite useful even for errors later on. Let's lift this higher like this:

Suggested change
log.Log.
WithField("attribution_id", in.AttributionId).
WithField("perPage", limit).
WithField("page", page).
WithField("from", from).
WithField("to", to).
WithError(err).Error("Failed to fetch usage.")
logger := log.Log.
WithField("attribution_id", in.AttributionId).
WithField("perPage", limit).
WithField("page", page).
WithField("from", from).
WithField("to", to)
if err != nil {
logger.WithError(err).Error("Failed to fetch usage.")
return ...
}

This then allows you to re-use the logger fields in other error messages across this method

Comment thread components/usage/pkg/db/usage.go Outdated
return usageRecords, nil
}

type UsageMetadata struct {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type UsageMetadata struct {
type UsageSummary struct {

The Metadata keyword gets confusign with the Metadata property on the Usage entry

Comment thread components/usage/pkg/apiv1/usage.go Outdated
return nil, status.Errorf(codes.InvalidArgument, "Maximum range exceeded. Range specified can be at most %s", maxQuerySize.String())
}

var order db.Order = db.DescendingOrder

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the type here is rendundant and could be removed

Suggested change
var order db.Order = db.DescendingOrder
order := db.DescendingOrder

Comment thread components/usage/pkg/apiv1/usage.go Outdated
}

usageSummary, err := db.GetUsageSummary(ctx, s.conn,
db.AttributionID(in.GetAttributionId()),

@easyCZ easyCZ Sep 5, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should first attempt to parse the attribution using db.ParseAttributionID() which does a sanity check that the ID is valid.

Comment thread components/usage/pkg/apiv1/usage.go Outdated
logger.WithError(err).Error("Failed to fetch usage metadata.")
return nil, status.Error(codes.Internal, "unable to retrieve usage")
}
var totalPages = int64(math.Ceil(float64(usageSummary.NumRecordsInRange) / float64(limit)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will panic if limit is 0. We don't validate the pagination input from the request.

Comment thread components/usage/pkg/apiv1/usage.go Outdated
var offset int64 = 0
if in.Pagination != nil {
limit = in.Pagination.PerPage
page = in.Pagination.Page

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we should also validate the numbers are positive integers

Comment on lines +82 to +121
func FindUsage(ctx context.Context, conn *gorm.DB, params *FindUsageParams) ([]Usage, error) {
var usageRecords []Usage
var usageRecordsBatch []Usage

result := conn.WithContext(ctx).
Where("attributionId = ?", attributionId).
Where("? <= effectiveTime AND effectiveTime < ?", from.String(), to.String()).
Order("effectiveTime DESC").
Offset(int(offset)).
Limit(int(limit)).
FindInBatches(&usageRecordsBatch, 1000, func(_ *gorm.DB, _ int) error {
usageRecords = append(usageRecords, usageRecordsBatch...)
return nil
})
db := conn.WithContext(ctx).
Where("attributionId = ?", params.AttributionId).
Where("? <= effectiveTime AND effectiveTime < ?", params.From, params.To)
if params.ExcludeDrafts {
db = db.Where("draft = ?", false)
}
db = db.Order(fmt.Sprintf("effectiveTime %s", params.Order.ToSQL()))
if params.Offset != 0 {
db = db.Offset(int(params.Offset))
}
if params.Limit != 0 {
db = db.Limit(int(params.Limit))
}

result := db.FindInBatches(&usageRecordsBatch, 1000, func(_ *gorm.DB, _ int) error {
usageRecords = append(usageRecords, usageRecordsBatch...)
return nil
})
if result.Error != nil {
return nil, fmt.Errorf("failed to get usage records: %s", result.Error)
}
return usageRecords, nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For implementing these, you may find gorm scopes very applicable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure how you would like them to be used in this context.

}
var creditCentsBalanceInPeriod sql.NullInt64
var numRecordsInRange sql.NullInt32
err = query2.Row().Scan(&creditCentsBalanceInPeriod, &numRecordsInRange)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we shouldn't need to use the Row().Scan() API here, and instead should be able to do this with https://gorm.io/docs/advanced_query.html#Count

Happy to try and pair on this, I haven't used it myself yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it only gives me the count not the sum, does it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention was to use deserialize into the UsageSummary object directly. If you were to add a gorm annotation to the UsageSummary, you can have those annotations match the returned column names (creditCentsBalanceAtStart, ...)

@svenefftinge
svenefftinge force-pushed the se/ledger-usage-view branch 4 times, most recently from 129ea87 to 99494b5 Compare September 6, 2022 06:21
Comment thread components/usage/pkg/apiv1/usage.go Outdated
var limit int64 = 1000
var page int64 = 0
var offset int64 = 0
if in.Pagination != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grpc guarantees it won't be nil. If its not specified in the request, it will contain default values. For ints that's 0

Comment thread components/usage/pkg/apiv1/usage.go Outdated
Comment on lines +188 to +192
attributionId, err := db.ParseAttributionID(string(usageRecord.AttributionID))
if err != nil {
logger.WithError(err).Errorf("Ignoring usage entry (ID: %s) with invalid attributionID: %s.", usageRecord.ID, usageRecord.AttributionID)
continue
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persoanlly, I'd return an error here from the RPC. Yes, it would cause it to fail but it would more acutely show up and give us a chance to fix. There's a tendency that these logs are missed and never properly fixed.

I think I may have confused you in the previous comment on this. I think when we take the attribution ID from the DB, we can trust it to be the right shape so doing just db.AttributionID(usageRecord.AttributionID) should be fine. But I think we can do better. We can actually type the UsageRecord as an AttributionID directly, this removes this step.

But we should try to parse the Attribution ID from the incoming request. We currently use it on line 208 without parsing (and therefore at least some validation).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I got indeed confused (somehow the comments show up on the wrong lines in VS Code sometimes). Makes much more sense now.

var offset int64 = perPage * page

listUsageResult, err := db.FindUsage(ctx, s.conn, &db.FindUsageParams{
AttributionId: db.AttributionID(in.GetAttributionId()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
AttributionId: db.AttributionID(in.GetAttributionId()),
AttributionId: attributionId,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, in Go it's a common style to capitalize shorthands. In this case, ID instead of Id

@easyCZ easyCZ left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, left a couple of last comments but these can happen in a follow-up PR

}
var creditCentsBalanceInPeriod sql.NullInt64
var numRecordsInRange sql.NullInt32
err = query2.Row().Scan(&creditCentsBalanceInPeriod, &numRecordsInRange)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention was to use deserialize into the UsageSummary object directly. If you were to add a gorm annotation to the UsageSummary, you can have those annotations match the returned column names (creditCentsBalanceAtStart, ...)

@roboquat
roboquat merged commit 29388c9 into main Sep 6, 2022
@roboquat
roboquat deleted the se/ledger-usage-view branch September 6, 2022 07:04
@roboquat roboquat added the deployed: webapp Meta team change is running in production label Sep 6, 2022
@roboquat roboquat added the deployed Change is completely running in production label Sep 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployed: webapp Meta team change is running in production deployed Change is completely running in production release-note-none size/XXL team: webapp Issue belongs to the WebApp team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants