Add cost service for per-VIN cost estimation
- Estimates cloud vs on-prem costs per active vehicle - Queries feature_table_last_shard from ClickHouse (lightweight) - 85% savings estimate with on-prem (hardware only) - Deployed to cec-prd-cluster-1 (internal only) - Text report endpoint at /cost/report
This commit is contained in:
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -185,3 +186,82 @@ func respondJSON(w http.ResponseWriter, data interface{}) {
|
||||
logger.Error().Err(err).Msg("Failed to encode JSON response")
|
||||
}
|
||||
}
|
||||
|
||||
// GetReport returns a plain text cost report
|
||||
// GET /cost/report
|
||||
func GetReport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
to := time.Now()
|
||||
from := to.AddDate(0, -1, 0) // Last month
|
||||
|
||||
summary, err := services.GetFleetCostSummary(from, to, 10)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to get report data")
|
||||
http.Error(w, "Failed to get report data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
report := `
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ COST SERVICE REPORT ║
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ Period: %s to %s
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ FLEET OVERVIEW ║
|
||||
║ ─────────────────────────────────────────────────────────────── ║
|
||||
║ Active Vehicles: %d
|
||||
║ Cloud Cost: $%.2f
|
||||
║ On-Prem Cost: $%.2f
|
||||
║ Savings: $%.2f (%.1f%%)
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ COST RATES ║
|
||||
║ ─────────────────────────────────────────────────────────────── ║
|
||||
║ Cloud: CPU $%.3f/core-hr Memory $%.4f/GB-hr
|
||||
║ On-Prem: CPU $%.3f/core-hr Memory $%.4f/GB-hr
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ ANNUAL PROJECTION (based on current usage) ║
|
||||
║ ─────────────────────────────────────────────────────────────── ║
|
||||
║ Cloud Annual: $%.2f
|
||||
║ On-Prem Annual: $%.2f
|
||||
║ Annual Savings: $%.2f
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
`
|
||||
annualCloud := summary.TotalCloudCost * 12
|
||||
annualOnprem := summary.TotalOnpremCost * 12
|
||||
annualSavings := annualCloud - annualOnprem
|
||||
|
||||
fmt.Fprintf(w, report,
|
||||
from.Format("2006-01-02"), to.Format("2006-01-02"),
|
||||
summary.VehicleCount,
|
||||
summary.TotalCloudCost,
|
||||
summary.TotalOnpremCost,
|
||||
summary.TotalSavings, summary.SavingsPercent,
|
||||
services.CloudCPUPerCoreHour, services.CloudMemoryPerGBHour,
|
||||
services.OnpremCPUPerCoreHour, services.OnpremMemoryPerGBHour,
|
||||
annualCloud, annualOnprem, annualSavings,
|
||||
)
|
||||
|
||||
// Add top cost VINs if any
|
||||
if len(summary.TopCostVins) > 0 {
|
||||
fmt.Fprintf(w, "\nTOP COST VEHICLES:\n")
|
||||
fmt.Fprintf(w, "%-20s %12s %12s %10s\n", "VIN", "Cloud $", "On-Prem $", "Savings %")
|
||||
fmt.Fprintf(w, "%-20s %12s %12s %10s\n", "───────────────────", "──────────", "──────────", "────────")
|
||||
for _, v := range summary.TopCostVins {
|
||||
fmt.Fprintf(w, "%-20s %12.2f %12.2f %9.1f%%\n",
|
||||
truncateVIN(v.VIN), v.TotalCloudCost, v.TotalOnpremCost, v.SavingsPercent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func truncateVIN(vin string) string {
|
||||
if len(vin) > 20 {
|
||||
return vin[:17] + "..."
|
||||
}
|
||||
return vin
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user