Skip to content

Added API call to fetch usage data #12646

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

Merged
merged 1 commit into from
Sep 6, 2022
Merged

Added API call to fetch usage data #12646

merged 1 commit into from
Sep 6, 2022

Conversation

svenefftinge
Copy link
Member

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
@@ -10,6 +10,9 @@ service UsageService {
// ListBilledUsage retrieves all usage for the specified attributionId
rpc ListBilledUsage(ListBilledUsageRequest) returns (ListBilledUsageResponse) {}

// ListBilledUsage retrieves all usage for the specified attributionId
rpc FindUsage(FindUsageRequest) returns (FindUsageResponse) {}
Copy link
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
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 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
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

if result.Error != nil {
return nil, fmt.Errorf("failed to get usage records: %s", result.Error)
}
return usageRecords, nil
}

type UsageMetadata struct {
Copy link
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

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
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

}

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

@easyCZ easyCZ Sep 5, 2022

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.

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
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.

var offset int64 = 0
if in.Pagination != nil {
limit = in.Pagination.PerPage
page = in.Pagination.Page
Copy link
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
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
Member 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
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
Member 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
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
var limit int64 = 1000
var page int64 = 0
var offset int64 = 0
if in.Pagination != nil {
Copy link
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 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
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
Member 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
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
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

Copy link
Member

@easyCZ easyCZ left a comment

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
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