summaryrefslogtreecommitdiffstats
path: root/internal/formatter
diff options
context:
space:
mode:
authorsinner <[email protected]>2026-08-05 01:21:17 +0000
committersinner <[email protected]>2026-08-05 01:21:17 +0000
commit4c6c8e86565ac49d1bd1c64e17c1404f4517d200 (patch)
tree9f079e106a3f01f5d227c407e3d984a38bb2a8e9 /internal/formatter
parent2875e578f5e22ae0971d057819f1ab4362b4d115 (diff)
downloaddborg-4c6c8e86565ac49d1bd1c64e17c1404f4517d200.tar.gz
dborg-4c6c8e86565ac49d1bd1c64e17c1404f4517d200.zip
feat: add admin usage logging and reporting commandsHEADmaster
Diffstat (limited to 'internal/formatter')
-rw-r--r--internal/formatter/usage.go202
1 files changed, 202 insertions, 0 deletions
diff --git a/internal/formatter/usage.go b/internal/formatter/usage.go
new file mode 100644
index 0000000..6732a3b
--- /dev/null
+++ b/internal/formatter/usage.go
@@ -0,0 +1,202 @@
+package formatter
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "git.db.org.ai/dborg/internal/models"
+)
+
+// FormatCreditUsageLogs renders raw credit_usage_log rows.
+func FormatCreditUsageLogs(resp *models.CreditUsageLogsResponse, asJSON bool) (string, error) {
+ if asJSON {
+ if err := PrintColorizedJSON(resp); err != nil {
+ return "", err
+ }
+ return "", nil
+ }
+
+ if len(resp.Logs) == 0 {
+ return fmt.Sprintf("%s\n", Gray("No usage logs found")), nil
+ }
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("\n%s\n", Bold(Cyan("Credit Usage Logs"))))
+ sb.WriteString(fmt.Sprintf("%s\n\n", Gray(strings.Repeat("─", 90))))
+
+ table := NewTable([]string{"ID", "Account", "Endpoint", "Path", "Cost", "Charged", "Status", "Time"})
+
+ for _, row := range resp.Logs {
+ statusStr := Green(fmt.Sprintf("%d", row.StatusCode))
+ if row.StatusCode >= 400 {
+ statusStr = Red(fmt.Sprintf("%d", row.StatusCode))
+ } else if row.StatusCode >= 300 {
+ statusStr = Yellow(fmt.Sprintf("%d", row.StatusCode))
+ }
+
+ chargedStr := Gray("0")
+ if row.CreditsCharged > 0 {
+ chargedStr = Yellow(fmt.Sprintf("%d", row.CreditsCharged))
+ }
+
+ timeStr := row.CreatedAt
+ if t, err := time.Parse(time.RFC3339, row.CreatedAt); err == nil {
+ timeStr = t.Format("01-02 15:04:05")
+ } else if t, err := time.Parse("2006-01-02T15:04:05Z", row.CreatedAt); err == nil {
+ timeStr = t.Format("01-02 15:04:05")
+ }
+
+ table.AddRow(
+ Gray(fmt.Sprintf("%d", row.ID)),
+ Bold(row.AccountName),
+ Cyan(row.Endpoint),
+ Dim(truncate(row.Path, 30)),
+ Gray(fmt.Sprintf("%d", row.EndpointCost)),
+ chargedStr,
+ statusStr,
+ Gray(timeStr),
+ )
+ }
+
+ sb.WriteString(table.Render())
+ sb.WriteString(fmt.Sprintf("\n%s %d rows\n", Blue("Total:"), resp.Count))
+ return sb.String(), nil
+}
+
+// FormatCreditUsageByEndpoint renders per-endpoint aggregate stats.
+func FormatCreditUsageByEndpoint(resp *models.CreditUsageByEndpointResponse, asJSON bool) (string, error) {
+ if asJSON {
+ if err := PrintColorizedJSON(resp); err != nil {
+ return "", err
+ }
+ return "", nil
+ }
+
+ if len(resp.Endpoints) == 0 {
+ return fmt.Sprintf("%s\n", Gray("No endpoint usage data found")), nil
+ }
+
+ fromStr, toStr := formatTimeRange(resp.From, resp.To)
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("\n%s\n", Bold(Cyan("Usage by Endpoint"))))
+ sb.WriteString(fmt.Sprintf("%s %s → %s\n\n", Gray(strings.Repeat("─", 60)), Gray(fromStr), Gray(toStr)))
+
+ table := NewTable([]string{"Endpoint", "Total", "Success", "Cost", "Charged"})
+
+ var totalCalls, totalCharged int64
+ for _, row := range resp.Endpoints {
+ successRate := ""
+ if row.TotalCalls > 0 {
+ pct := float64(row.SuccessCalls) / float64(row.TotalCalls) * 100
+ color := Green
+ if pct < 50 {
+ color = Red
+ } else if pct < 80 {
+ color = Yellow
+ }
+ successRate = color(fmt.Sprintf("%d (%.0f%%)", row.SuccessCalls, pct))
+ }
+
+ chargedStr := Gray("0")
+ if row.TotalCharged > 0 {
+ chargedStr = Yellow(fmt.Sprintf("%d", row.TotalCharged))
+ }
+
+ table.AddRow(
+ Cyan(row.Endpoint),
+ Bold(fmt.Sprintf("%d", row.TotalCalls)),
+ successRate,
+ Gray(fmt.Sprintf("%d ea", row.EndpointCost)),
+ chargedStr,
+ )
+ totalCalls += row.TotalCalls
+ totalCharged += row.TotalCharged
+ }
+
+ sb.WriteString(table.Render())
+ sb.WriteString(fmt.Sprintf("\n%s %d calls %s %d credits charged\n",
+ Blue("Total calls:"), totalCalls,
+ Blue("Total charged:"), totalCharged,
+ ))
+ return sb.String(), nil
+}
+
+// FormatCreditUsageByAccount renders per-account per-endpoint aggregate stats.
+func FormatCreditUsageByAccount(resp *models.CreditUsageByAccountResponse, asJSON bool) (string, error) {
+ if asJSON {
+ if err := PrintColorizedJSON(resp); err != nil {
+ return "", err
+ }
+ return "", nil
+ }
+
+ if len(resp.Accounts) == 0 {
+ return fmt.Sprintf("%s\n", Gray("No account usage data found")), nil
+ }
+
+ fromStr, toStr := formatTimeRange(resp.From, resp.To)
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("\n%s\n", Bold(Cyan("Usage by Account"))))
+ sb.WriteString(fmt.Sprintf("%s %s → %s\n\n", Gray(strings.Repeat("─", 60)), Gray(fromStr), Gray(toStr)))
+
+ table := NewTable([]string{"Account", "Endpoint", "Calls", "Charged", "Last Call"})
+
+ // Group consecutive rows by account for visual separation
+ var prevAccount string
+ var totalCalls, totalCharged int64
+ for _, row := range resp.Accounts {
+ nameStr := Bold(row.AccountName)
+ if row.AccountName == prevAccount {
+ nameStr = Dim(" ↳")
+ }
+ prevAccount = row.AccountName
+
+ chargedStr := Gray("0")
+ if row.TotalCharged > 0 {
+ chargedStr = Yellow(fmt.Sprintf("%d", row.TotalCharged))
+ }
+
+ lastCall := row.LastCall
+ if t, err := time.Parse("2006-01-02 15:04:05", row.LastCall); err == nil {
+ lastCall = t.Format("2006-01-02 15:04")
+ }
+
+ table.AddRow(
+ nameStr,
+ Cyan(row.Endpoint),
+ fmt.Sprintf("%d", row.TotalCalls),
+ chargedStr,
+ Gray(lastCall),
+ )
+ totalCalls += row.TotalCalls
+ totalCharged += row.TotalCharged
+ }
+
+ sb.WriteString(table.Render())
+ sb.WriteString(fmt.Sprintf("\n%s %d %s %d credits charged\n",
+ Blue("Total calls:"), totalCalls,
+ Blue("Total charged:"), totalCharged,
+ ))
+ return sb.String(), nil
+}
+
+func formatTimeRange(from, to string) (string, string) {
+ fromStr, toStr := from, to
+ if t, err := time.Parse(time.RFC3339, from); err == nil {
+ fromStr = t.Format("2006-01-02 15:04")
+ }
+ if t, err := time.Parse(time.RFC3339, to); err == nil {
+ toStr = t.Format("2006-01-02 15:04")
+ }
+ return fromStr, toStr
+}
+
+func truncate(s string, n int) string {
+ if len(s) <= n {
+ return s
+ }
+ return s[:n-1] + "…"
+}