You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/command/operator/usage/instances/usage_instances.go

292 lines
7.0 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package instances
import (
"bytes"
"flag"
"fmt"
"sort"
"strings"
"text/tabwriter"
"github.com/mitchellh/cli"
"golang.org/x/exp/maps"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
// flags
onlyBillable bool
onlyConnect bool
allDatacenters bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(&c.onlyBillable, "billable", false, "Display only billable service info. "+
"Cannot be used with -connect.")
c.flags.BoolVar(&c.onlyConnect, "connect", false, "Display only Connect service info."+
"Cannot be used with -billable.")
c.flags.BoolVar(&c.allDatacenters, "all-datacenters", false, "Display service counts from "+
"all datacenters.")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if l := len(c.flags.Args()); l > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", l))
return 1
}
if c.onlyBillable && c.onlyConnect {
c.UI.Error("Cannot specify both -billable and -connect flags")
return 1
}
// Create and test the HTTP client
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
billableTotal := 0
var datacenterBillableTotals []string
usage, _, err := client.Operator().Usage(&api.QueryOptions{Global: c.allDatacenters})
if err != nil {
c.UI.Error(fmt.Sprintf("Error fetching usage information: %s", err))
return 1
}
for dc, usage := range usage.Usage {
billableTotal += usage.BillableServiceInstances
datacenterBillableTotals = append(datacenterBillableTotals,
fmt.Sprintf("%s Billable Service Instances: %d", dc, usage.BillableServiceInstances))
}
// Output billable service counts
if !c.onlyConnect {
c.UI.Output(fmt.Sprintf("Billable Service Instances Total: %d", billableTotal))
sort.Strings(datacenterBillableTotals)
for _, datacenterTotal := range datacenterBillableTotals {
c.UI.Output(datacenterTotal)
}
c.UI.Output("\nBillable Services")
billableOutput, err := formatServiceCounts(usage.Usage, true, c.allDatacenters)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.UI.Output(billableOutput + "\n")
c.UI.Output("\nNodes")
nodesOutput, err := formatNodesCounts(usage.Usage)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.UI.Output(nodesOutput + "\n\n")
}
// Output Connect service counts
if !c.onlyBillable {
c.UI.Output("Connect Services")
connectOutput, err := formatServiceCounts(usage.Usage, false, c.allDatacenters)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.UI.Output(connectOutput)
}
return 0
}
func formatNodesCounts(usageStats map[string]api.ServiceUsage) (string, error) {
var output bytes.Buffer
tw := tabwriter.NewWriter(&output, 0, 2, 6, ' ', 0)
nodesTotal := 0
fmt.Fprintf(tw, "Datacenter\t")
fmt.Fprintf(tw, "Count\t")
fmt.Fprint(tw, "\t\n")
nodes := maps.Keys(usageStats)
sort.Strings(nodes)
for _, dc := range nodes {
nodesTotal += usageStats[dc].Nodes
fmt.Fprintf(tw, "%s\t%d\n", dc, usageStats[dc].Nodes)
}
fmt.Fprint(tw, "\t\n")
fmt.Fprintf(tw, "Total")
fmt.Fprintf(tw, "\t%d", nodesTotal)
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("Error flushing tabwriter: %s", err)
}
return strings.TrimSpace(output.String()), nil
}
func formatServiceCounts(usageStats map[string]api.ServiceUsage, billable, showDatacenter bool) (string, error) {
var output bytes.Buffer
tw := tabwriter.NewWriter(&output, 0, 2, 6, ' ', 0)
var serviceCounts []serviceCount
for datacenter, usage := range usageStats {
if billable {
serviceCounts = append(serviceCounts, getBillableInstanceCounts(usage, datacenter)...)
} else {
serviceCounts = append(serviceCounts, getConnectInstanceCounts(usage, datacenter)...)
}
}
sortServiceCounts(serviceCounts)
if showDatacenter {
fmt.Fprintf(tw, "Datacenter\t")
}
if showPartitionNamespace {
fmt.Fprintf(tw, "Partition\tNamespace\t")
}
if !billable {
fmt.Fprintf(tw, "Type\t")
} else {
fmt.Fprintf(tw, "Services\t")
}
fmt.Fprintf(tw, "Service instances\n")
serviceTotal := 0
instanceTotal := 0
for _, c := range serviceCounts {
if showDatacenter {
fmt.Fprintf(tw, "%s\t", c.datacenter)
}
if showPartitionNamespace {
fmt.Fprintf(tw, "%s\t%s\t", c.partition, c.namespace)
}
if !billable {
fmt.Fprintf(tw, "%s\t", c.serviceType)
} else {
fmt.Fprintf(tw, "%d\t", c.services)
}
fmt.Fprintf(tw, "%d\n", c.instanceCount)
serviceTotal += c.services
instanceTotal += c.instanceCount
}
// Show total counts if there's multiple rows because of datacenter or partition/ns view
if showDatacenter || showPartitionNamespace {
if showDatacenter {
fmt.Fprint(tw, "\t")
}
if showPartitionNamespace {
fmt.Fprint(tw, "\t\t")
}
fmt.Fprint(tw, "\t\n")
fmt.Fprintf(tw, "Total")
if showPartitionNamespace {
fmt.Fprint(tw, "\t")
if showDatacenter {
fmt.Fprint(tw, "\t")
}
}
if billable {
fmt.Fprintf(tw, "\t%d\t%d\n", serviceTotal, instanceTotal)
} else {
fmt.Fprintf(tw, "\t\t%d\n", instanceTotal)
}
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("Error flushing tabwriter: %s", err)
}
return strings.TrimSpace(output.String()), nil
}
type serviceCount struct {
datacenter string
partition string
namespace string
serviceType string
instanceCount int
services int
}
// Sort entries by datacenter > partition > namespace
func sortServiceCounts(counts []serviceCount) {
sort.Slice(counts, func(i, j int) bool {
if counts[i].datacenter != counts[j].datacenter {
return counts[i].datacenter < counts[j].datacenter
}
if counts[i].partition != counts[j].partition {
return counts[i].partition < counts[j].partition
}
if counts[i].namespace != counts[j].namespace {
return counts[i].namespace < counts[j].namespace
}
return counts[i].serviceType < counts[j].serviceType
})
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return c.help
}
const (
synopsis = "Display service instance usage information"
help = `
Usage: consul operator usage instances [options]
Retrieves usage information about the number of services registered in a given
datacenter. By default, the datacenter of the local agent is queried.
To retrieve the service usage data:
$ consul operator usage instances
To show only billable service instance counts:
$ consul operator usage instances -billable
To show only connect service instance counts:
$ consul operator usage instances -connect
For a full list of options and examples, please see the Consul documentation.
`
)