// Package costs provides functions for calculating and // showing GCP costs package costs import ( "fmt" "math" "os" "strings" "text/tabwriter" "edge-infra.dev/pkg/edge/monitoring/billman/edgesql" ) // MetricData contains the number of metric samples and the // estimated GCP cost for those samples. type MetricData struct { Samples int64 Cost float64 } // LogNodeData contains the number of log bytes // and the estimated GCP cost for ingested bytes // from k8s-container type LogContainerData struct { Bytes int64 Cost float64 } // LogNodeData contains the number of log bytes // and the estimated GCP cost for ingested bytes // from k8s-node type LogNodeData struct { Bytes int64 Cost float64 } // LogData contains the number of log bytes from both node // & container and the total estimated GCP cost for those // ingested bytes. type LogData struct { Bytes int64 Cost float64 } // Options let you control the table output. i.e., showing // or hiding the table header, printing as tab or csv. type Options struct { Output string // csv, tab ShowDisclaimer bool ShowHeader bool } // Billing is used by PrintClusterCosts to control the // output of the CLI. All fields in the struct are required. type Billing struct { Cluster edgesql.EdgeCluster LogContainerData LogNodeData LogData MetricData Options DisplayType string // e.g.: "all", "metrics", "logs" } // LogCosts calculates the estimated GCP costs for logs // ingested. // ref: https://cloud.google.com/stackdriver/pricing#binary-units: // For pricing purposes, all units are treated as binary measures, // for example, as mebibytes (MiB, or 2^20 bytes) or gibibytes (GiB, or 2^30 bytes). func LogCosts(bytes int64, rate float64) float64 { gb := float64(bytes) / 1024 / 1024 / 1024 dollars := rate * gb cost := math.Ceil(dollars*100) / 100 return cost } // MetricCosts calculates the estimated GCP costs for metric // samples ingested. As noted on the // [pricing summary page](https://cloud.google.com/stackdriver/pricing#monitoring-pricing-summary) // metrics have a tiered billing rate but these rates are // tiered on a per-project basis, rather than per-cluster. // This function only provides the billing estimates for // metrics // using the non-tiered base rate of // $0.15/million samples. If a given cluster has over 50 // billion samples during the interval that this cli is // executed then the estimates with this function might be // slightly higher than what google would charge for during // a monthly billing cycle. func MetricCosts(samples int64, rate float64) float64 { millionSamples := float64(samples) / 1000000 dollars := rate * millionSamples cost := math.Ceil(dollars*100) / 100 return cost } func printDisclamer() { fmt.Fprintln(os.Stderr, ` ****************** * Note: The results shown below are only estimates. * Please read our billing disclaimer at https://docs.edge-infra.dev/edge/observability/Billing/billing/ ******************`) } // PrintClusterCosts prints to stdout a table or csv // showing log and or metric costs for a given cluster. // Output options can be controlled by setting the // options func PrintClusterCosts(b Billing) { if b.Options.ShowDisclaimer { printDisclamer() } w := tabwriter.NewWriter(os.Stdout, 5, 2, 2, ' ', 0) baseCols := []string{"CLUSTER_NAME", "CLUSTER_EDGE_ID", "BANNER_NAME", "PROJECT_ID"} baseFmt := []string{"%s", "%s", "%s", "%s"} logCols := []string{"LOG_CONTAINER_BYTES", "LOG_CONTAINER_COST", "LOG_NODE_BYTES", "LOG_NODE_COST", "LOG_BYTES", "LOG_COST"} logFmt := []string{"%d", "%.2f", "%d", "%.2f", "%d", "%.2f"} metricCols := []string{"METRIC_SAMPLES", "METRIC_COST"} metricFmt := []string{"%d", "%.2f"} totalCol := []string{"TOTAL"} totalFmt := []string{"%.2f\n"} var separator string switch b.Options.Output { case "csv": separator = "," default: separator = "\t" } switch b.DisplayType { case "all": c := append(append(append(baseCols, logCols...), metricCols...), totalCol...) cFmt := append(append(append(baseFmt, logFmt...), metricFmt...), totalFmt...) cols := fmt.Sprint(strings.Join(c, separator)) fmtStr := fmt.Sprint(strings.Join(cFmt, separator)) if b.Options.ShowHeader { fmt.Fprintln(w, cols) } fmt.Fprintf(w, fmtStr, b.Cluster.ClusterName, b.Cluster.ClusterEdgeID, b.Cluster.BannerName, b.Cluster.ProjectID, b.LogContainerData.Bytes, b.LogContainerData.Cost, b.LogNodeData.Bytes, b.LogNodeData.Cost, b.LogData.Bytes, b.LogData.Cost, b.MetricData.Samples, b.MetricData.Cost, b.LogData.Cost+b.MetricData.Cost, ) case "logs": c := append(baseCols, logCols...) cFmt := append(baseFmt, logFmt...) cols := fmt.Sprint(strings.Join(c, separator)) fmtStr := fmt.Sprint(strings.Join(cFmt, separator)) if b.ShowHeader { fmt.Fprintln(w, cols) } fmt.Fprintf(w, fmtStr+"\n", b.Cluster.ClusterName, b.Cluster.ClusterEdgeID, b.Cluster.BannerName, b.Cluster.ProjectID, b.LogContainerData.Bytes, b.LogContainerData.Cost, b.LogNodeData.Bytes, b.LogNodeData.Cost, b.LogData.Bytes, b.LogData.Cost, ) case "metrics": c := append(baseCols, metricCols...) cFmt := append(baseFmt, metricFmt...) cols := fmt.Sprint(strings.Join(c, separator)) fmtStr := fmt.Sprint(strings.Join(cFmt, separator)) if b.Options.ShowHeader { fmt.Fprintln(w, cols) } fmt.Fprintf(w, fmtStr+"\n", b.Cluster.ClusterName, b.Cluster.ClusterEdgeID, b.Cluster.BannerName, b.Cluster.ProjectID, b.MetricData.Samples, b.MetricData.Cost, ) } w.Flush() }