Skip to content
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
49 changes: 30 additions & 19 deletions pkg/common/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,28 +288,39 @@ func NewAuditRecord(runID string, rec Recommendation, result PurchaseResult, sta
errMsg = result.Error.Error()
}
return AuditRecord{
RunID: runID,
Provider: rec.Provider,
AccountID: rec.Account,
AccountName: rec.AccountName,
Region: rec.Region,
Service: string(rec.Service),
ResourceType: rec.ResourceType,
CommitmentType: rec.CommitmentType,
Term: termMonths(rec.Term),
Count: rec.Count,
EstimatedCost: rec.CommitmentCost,
EstimatedSavings: rec.EstimatedSavings,
CommitmentID: result.CommitmentID,
Status: status,
ErrorMessage: errMsg,
Timestamp: time.Now().UTC(),
DryRun: dryRun,
RawRecommendation: rec.RawRecommendation,
Source: source,
RunID: runID,
Provider: rec.Provider,
AccountID: rec.Account,
AccountName: rec.AccountName,
Region: rec.Region,
Service: string(rec.Service),
ResourceType: rec.ResourceType,
CommitmentType: rec.CommitmentType,
Term: termMonths(rec.Term),
Count: rec.Count,
EstimatedCost: rec.CommitmentCost,
RecurringMonthlyCost: copyFloatPtr(rec.RecurringMonthlyCost),
EstimatedSavings: rec.EstimatedSavings,
CommitmentID: result.CommitmentID,
Status: status,
ErrorMessage: errMsg,
Timestamp: time.Now().UTC(),
DryRun: dryRun,
RawRecommendation: rec.RawRecommendation,
Source: source,
}
}

// copyFloatPtr returns a pointer to a copy of *p, or nil, so the audit record
// does not alias the recommendation's value.
func copyFloatPtr(p *float64) *float64 {
if p == nil {
return nil
}
v := *p
return &v
}

// termMonths converts a term string ("1yr", "3yr") to months.
// Returns 0 for unrecognized strings.
func termMonths(t string) int {
Expand Down
30 changes: 30 additions & 0 deletions pkg/common/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,33 @@ func TestTermMonths(t *testing.T) {
})
}
}

func TestNewAuditRecord_RecurringMonthlyCost(t *testing.T) {
recurring := 1968.81
rec := Recommendation{CommitmentCost: 0, RecurringMonthlyCost: &recurring}
ar := NewAuditRecord("run", rec, PurchaseResult{}, "skipped", true, PurchaseSourceCLI)
if assert.NotNil(t, ar.RecurringMonthlyCost) {
assert.Equal(t, 1968.81, *ar.RecurringMonthlyCost)
}
assert.NotSame(t, rec.RecurringMonthlyCost, ar.RecurringMonthlyCost)
line, err := json.Marshal(ar)
assert.NoError(t, err)
assert.Contains(t, string(line), `"recurring_monthly_cost":1968.81`)
assert.Contains(t, string(line), `"estimated_cost":0`)

unknown, err := json.Marshal(NewAuditRecord("run", Recommendation{}, PurchaseResult{}, "skipped", true, PurchaseSourceCLI))
assert.NoError(t, err)
assert.NotContains(t, string(unknown), "recurring_monthly_cost")
}

func TestNewAuditRecord_RecurringMonthlyCostKnownZero(t *testing.T) {
zero := 0.0
rec := Recommendation{RecurringMonthlyCost: &zero}
ar := NewAuditRecord("run", rec, PurchaseResult{}, "skipped", true, PurchaseSourceCLI)
if assert.NotNil(t, ar.RecurringMonthlyCost, "a known 0 must stay distinguishable from unknown") {
assert.Equal(t, 0.0, *ar.RecurringMonthlyCost)
}
line, err := json.Marshal(ar)
assert.NoError(t, err)
assert.Contains(t, string(line), `"recurring_monthly_cost":0`)
}
43 changes: 24 additions & 19 deletions pkg/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,25 +748,30 @@ type RIUtilization struct {
// AuditRecord is one line in the JSON-lines audit log.
// Status values: "success", "error", "skipped" (dry-run), "skipped_covered" (idempotency).
type AuditRecord struct {
RunID string `json:"run_id"`
Provider ProviderType `json:"provider"`
AccountID string `json:"account_id"`
AccountName string `json:"account_name"`
CredentialScope string `json:"credential_scope,omitempty"` // Routing identifier, not a verified account ID.
Region string `json:"region"`
Service string `json:"service"`
ResourceType string `json:"resource_type"`
CommitmentType CommitmentType `json:"commitment_type"`
Term int `json:"term_months"`
Count int `json:"count"`
EstimatedCost float64 `json:"estimated_cost"`
EstimatedSavings float64 `json:"estimated_savings"`
CommitmentID string `json:"commitment_id"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
Timestamp time.Time `json:"timestamp"`
DryRun bool `json:"dry_run"`
RawRecommendation json.RawMessage `json:"raw_recommendation,omitempty"`
RunID string `json:"run_id"`
Provider ProviderType `json:"provider"`
AccountID string `json:"account_id"`
AccountName string `json:"account_name"`
CredentialScope string `json:"credential_scope,omitempty"` // Routing identifier, not a verified account ID.
Region string `json:"region"`
Service string `json:"service"`
ResourceType string `json:"resource_type"`
CommitmentType CommitmentType `json:"commitment_type"`
Term int `json:"term_months"`
Count int `json:"count"`
EstimatedCost float64 `json:"estimated_cost"` // Upfront payment (Recommendation.CommitmentCost); 0 for no-upfront commitments.
// RecurringMonthlyCost is the recurring charge in USD per month (the part
// paid every month after any upfront payment), copied from
// Recommendation.RecurringMonthlyCost. nil (omitted) means the provider did
// not report it, which is not the same as a known 0 (all-upfront).
RecurringMonthlyCost *float64 `json:"recurring_monthly_cost,omitempty"`
EstimatedSavings float64 `json:"estimated_savings"`
CommitmentID string `json:"commitment_id"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
Timestamp time.Time `json:"timestamp"`
DryRun bool `json:"dry_run"`
RawRecommendation json.RawMessage `json:"raw_recommendation,omitempty"`
// Source identifies the CUDly surface (cudly-cli / cudly-web) that
// triggered the purchase. Mirrors the value stamped on the commitment
// itself via purchase-automation tag/label.
Expand Down
13 changes: 9 additions & 4 deletions pkg/reporter/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,28 @@ const (
)

// RenderTable returns a formatted table of recommendations that passed the scorer.
// Columns: Cloud, Account, Region, Service, Type, Term, Count, Est. Cost, Est. Savings, Savings%, Break-even, Commitment.
// Columns: Cloud, Account, Region, Service, Type, Term, Count, Est. Cost (upfront), Recurring$/mo (USD per month, - when unknown), Est. Savings, Savings%, Break-even, Commitment.
func RenderTable(result scorer.ScoredResult) string {
if len(result.Passed) == 0 {
return "No recommendations passed the filters.\n"
}
var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, tabMinWidth, 0, tabPadding, ' ', 0)

fmt.Fprintln(w, "Cloud\tAccount\tRegion\tService\tType\tTerm\tCount\tEst.Cost\tEst.Savings\tSavings%\tBreak-even\tCommitment")
fmt.Fprintln(w, "-----\t-------\t------\t-------\t----\t----\t-----\t--------\t-----------\t---------\t----------\t----------")
fmt.Fprintln(w, "Cloud\tAccount\tRegion\tService\tType\tTerm\tCount\tEst.Cost\tRecurring$/mo\tEst.Savings\tSavings%\tBreak-even\tCommitment")
fmt.Fprintln(w, "-----\t-------\t------\t-------\t----\t----\t-----\t--------\t-------------\t-----------\t---------\t----------\t----------")

for _rvc := range result.Passed {
rec := result.Passed[_rvc]
breakEven := "-"
if rec.BreakEvenMonths > 0 {
breakEven = fmt.Sprintf("%.1f mo", rec.BreakEvenMonths)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%d\t$%.2f\t$%.2f\t%.1f%%\t%s\t%s\n",
recurring := "-"
if rec.RecurringMonthlyCost != nil {
recurring = fmt.Sprintf("$%.2f", *rec.RecurringMonthlyCost)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%d\t$%.2f\t%s\t$%.2f\t%.1f%%\t%s\t%s\n",
rec.Provider,
rec.AccountName,
rec.Region,
Expand All @@ -42,6 +46,7 @@ func RenderTable(result scorer.ScoredResult) string {
rec.Term,
rec.Count,
rec.CommitmentCost,
recurring,
rec.EstimatedSavings,
rec.SavingsPercentage,
breakEven,
Expand Down
26 changes: 25 additions & 1 deletion pkg/reporter/reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestRenderTable_ColumnCount(t *testing.T) {
header := lines[0]
// Split on 2+ spaces (tabwriter aligns with spaces, not tabs after flush)
cols := strings.Fields(header)
assert.Equal(t, 12, len(cols), "expected 12 columns in header, got: %v", cols)
assert.Equal(t, 13, len(cols), "expected 13 columns in header, got: %v", cols)
}

func TestRenderTable_Empty(t *testing.T) {
Expand Down Expand Up @@ -86,3 +86,27 @@ func TestRenderSummary_WithFiltered(t *testing.T) {
output := RenderSummary(result)
assert.Contains(t, output, "Filtered: 2")
}

func TestRenderTable_RecurringMonthlyCost(t *testing.T) {
recurring := 1968.81
zero := 0.0
rows := []common.Recommendation{
{Provider: "aws", Service: "savings-plans-ec2instance", Term: "1yr", Count: 1, RecurringMonthlyCost: &recurring, CommitmentType: common.CommitmentSavingsPlan},
{Provider: "aws", Service: "savings-plans-compute", Term: "1yr", Count: 1, RecurringMonthlyCost: &zero, CommitmentType: common.CommitmentSavingsPlan},
{Provider: "aws", Service: "rds", Term: "1yr", Count: 1, CommitmentType: common.CommitmentReservedInstance},
}
output := RenderTable(makeResult(rows, nil))
lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
assert.Contains(t, lines[0], "Recurring$/mo")

// Est.Cost and Est.Savings are also $0.00 on these rows, so look at the
// Recurring$/mo cell itself: fifth field from the end
// (Recurring, Est.Savings, Savings%, Break-even, Commitment).
recurringCell := func(line string) string {
fields := strings.Fields(line)
return fields[len(fields)-5]
}
assert.Equal(t, "$1968.81", recurringCell(lines[2]))
assert.Equal(t, "$0.00", recurringCell(lines[3]), "a known zero recurring cost (all-upfront) renders as $0.00")
assert.Equal(t, "-", recurringCell(lines[4]), "an unknown recurring cost renders as -, not a known $0.00")
}
Loading