diff --git a/client/env/reports.go b/client/env/reports.go new file mode 100644 index 000000000..aeae29ba4 --- /dev/null +++ b/client/env/reports.go @@ -0,0 +1,137 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package env + +import ( + "encoding/json" + "fmt" + "net/url" + "os" + "path" + "strconv" + "sync" + "text/tabwriter" + "time" + + "github.com/apigee/apigeecli/apiclient" + "github.com/apigee/apigeecli/clilog" +) + +const proxy_dimension = "apiproxy" +const selection = "sum(message_count)" + +type report struct { + Environments []environmentreport `json:"environments,omitempty"` +} + +type environmentreport struct { + Name string `json:"name,omitempty"` + Dimensions []dimension `json:"dimensions,omitempty"` +} + +type dimension struct { + Name string `json:"name,omitempty"` + Metrics []metric `json:"metrics,omitempty"` +} + +type metric struct { + Name string `json:"name,omitempty"` + Values []string `json:"values,omitempty"` +} + +var envAPICalls int +var mu sync.Mutex + +func TotalAPICallsInMonthAsync(environment string, month int, year int, envDetails bool, wg *sync.WaitGroup) { + defer wg.Done() + var total int + var err error + + if total, err = TotalAPICallsInMonth(environment, month, year); err != nil { + clilog.Error.Println(err) + return + } + syncCount(total) + if envDetails { + w := tabwriter.NewWriter(os.Stdout, 26, 4, 0, ' ', 0) + fmt.Fprintf(w, "%s\t%d/%d\t%d", environment, month, year, total) + fmt.Fprintln(w) + w.Flush() + } + return +} + +func TotalAPICallsInMonth(environment string, month int, year int) (total int, err error) { + + var apiCalls int + var respBody []byte + + u, _ := url.Parse(apiclient.BaseURL) + + timeRange := fmt.Sprintf("%d/01/%d 00:00~%d/%d/%d 23:59", month, year, month, daysIn(time.Month(month), year), year) + + q := u.Query() + q.Set("select", selection) + q.Set("timeRange", timeRange) + + u.RawQuery = q.Encode() + u.Path = path.Join(u.Path, apiclient.GetApigeeOrg(), "environments", environment, "stats", proxy_dimension) + + if respBody, err = apiclient.HttpClient(apiclient.GetPrintOutput(), u.String()); err != nil { + return -1, err + } + + environmentReport := report{} + + if err = json.Unmarshal(respBody, &environmentReport); err != nil { + return -1, err + } + + for _, e := range environmentReport.Environments { + for _, d := range e.Dimensions { + for _, m := range d.Metrics { + calls, _ := strconv.Atoi(m.Values[0]) + apiCalls = apiCalls + calls + } + } + } + + return apiCalls, nil +} + +//GetEnvAPICalls +func GetEnvAPICalls() int { + return envAPICalls +} + +func ResetEnvAPICalls() { + mu.Lock() + defer mu.Unlock() + envAPICalls = 0 +} + +// daysIn returns the number of days in a month for a given year. +//source https://groups.google.com/g/golang-nuts/c/W-ezk71hioo +func daysIn(m time.Month, year int) int { + // This is equivalent to time.daysIn(m, year). + return time.Date(year, m+1, 0, 0, 0, 0, 0, time.UTC).Day() +} + +//syncCount synchronizes counting +func syncCount(total int) { + mu.Lock() + defer mu.Unlock() + envAPICalls = envAPICalls + total +} diff --git a/client/orgs/reports.go b/client/orgs/reports.go new file mode 100644 index 000000000..1db96e158 --- /dev/null +++ b/client/orgs/reports.go @@ -0,0 +1,124 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package orgs + +import ( + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/apigee/apigeecli/apiclient" + "github.com/apigee/apigeecli/client/env" + "github.com/apigee/apigeecli/clilog" +) + +func TotalAPICallsInMonth(month int, year int, envDetails bool, conn int) (total int, err error) { + + var pwg sync.WaitGroup + var envListBytes []byte + var envList []string + + //ensure the count is reset to zero before calculating the next set + defer env.ResetEnvAPICalls() + + apiclient.SetPrintOutput(false) + + if envListBytes, err = env.List(); err != nil { + return -1, err + } + + if err = json.Unmarshal(envListBytes, &envList); err != nil { + return -1, err + } + + numEntities := len(envList) + clilog.Info.Printf("Found %d environments\n", numEntities) + clilog.Info.Printf("Generate report with %d connections\n", conn) + + numOfLoops, remaining := numEntities/conn, numEntities%conn + + //ensure connections aren't greater than entities + if conn > numEntities { + conn = numEntities + } + + start := 0 + + for i, end := 0, 0; i < numOfLoops; i++ { + pwg.Add(1) + end = (i * conn) + conn + clilog.Info.Printf("Creating reports for a batch %d of environments\n", (i + 1)) + go batchReport(envList[start:end], month, year, envDetails, &pwg) + start = end + pwg.Wait() + } + + if remaining > 0 { + pwg.Add(1) + clilog.Info.Printf("Creating reports for remaining %d environments\n", remaining) + go batchReport(envList[start:numEntities], month, year, envDetails, &pwg) + pwg.Wait() + } + + apiclient.SetPrintOutput(true) + + return env.GetEnvAPICalls(), nil +} + +func batchReport(envList []string, month int, year int, envDetails bool, pwg *sync.WaitGroup) { + defer pwg.Done() + //batch workgroup + var bwg sync.WaitGroup + + bwg.Add(len(envList)) + + for _, environment := range envList { + go env.TotalAPICallsInMonthAsync(environment, month, year, envDetails, &bwg) + } + + bwg.Wait() +} + +func TotalAPICallsInYear(year int, envDetails bool, conn int) (total int, err error) { + + var monthlyTotal int + + t := time.Now() + currentYear := t.Year() + + if year > currentYear { + return -1, fmt.Errorf("Invalid year. Year cannot be greater than current year") + } + + if currentYear == year { + currentMonth := t.Month() + for i := 1; i <= int(currentMonth); i++ { //run the loop only till the current month + if monthlyTotal, err = TotalAPICallsInMonth(i, year, envDetails, conn); err != nil { + return -1, err + } + total = total + monthlyTotal + } + } else { + for i := 1; i <= 12; i++ { //run the loop for each month + if monthlyTotal, err = TotalAPICallsInMonth(i, year, envDetails, conn); err != nil { + return -1, err + } + total = total + monthlyTotal + } + } + + return total, nil +} diff --git a/cmd/org/org.go b/cmd/org/org.go index 6b174e4a8..77b50f842 100644 --- a/cmd/org/org.go +++ b/cmd/org/org.go @@ -41,4 +41,5 @@ func init() { Cmd.AddCommand(ImportCmd) Cmd.AddCommand(UpdateCmd) Cmd.AddCommand(SetAddonCmd) + Cmd.AddCommand(ReportCmd) } diff --git a/cmd/org/reportmonthly.go b/cmd/org/reportmonthly.go new file mode 100644 index 000000000..ad484cfe3 --- /dev/null +++ b/cmd/org/reportmonthly.go @@ -0,0 +1,86 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org + +import ( + "fmt" + "os" + "text/tabwriter" + "time" + + "github.com/apigee/apigeecli/apiclient" + "github.com/apigee/apigeecli/client/orgs" + "github.com/spf13/cobra" +) + +//MonthlyCmd to get monthly usage +var MonthlyCmd = &cobra.Command{ + Use: "monthly", + Short: "Report monthly usage for an Apigee Org", + Long: "Report monthly usage for an Apigee Org", + Args: func(cmd *cobra.Command, args []string) (err error) { + return apiclient.SetApigeeOrg(org) + }, + RunE: func(cmd *cobra.Command, args []string) (err error) { + var apiCalls int + + if _, err = time.Parse("1/2006", fmt.Sprintf("%d/%d", month, year)); err != nil { + return + } + + if envDetails { + w := tabwriter.NewWriter(os.Stdout, 26, 4, 0, ' ', 0) + fmt.Fprintln(w, "ENVIRONMENT\tMONTH\tAPI CALLS") + fmt.Fprintln(w) + w.Flush() + } + + if apiCalls, err = orgs.TotalAPICallsInMonth(month, year, envDetails, conn); err != nil { + return + } + + if envDetails { + fmt.Printf("\nSummary\n\n") + } + + w := tabwriter.NewWriter(os.Stdout, 26, 4, 0, ' ', 0) + fmt.Fprintln(w, "ORGANIATION\tMONTH\tAPI CALLS") + fmt.Fprintf(w, "%s\t%d/%d\t%d\n", apiclient.GetApigeeOrg(), month, year, apiCalls) + fmt.Fprintln(w) + w.Flush() + + return + }, +} + +var month, year int +var envDetails bool + +func init() { + + MonthlyCmd.Flags().IntVarP(&month, "month", "m", + -1, "Month") + MonthlyCmd.Flags().IntVarP(&year, "year", "y", + -1, "Year") + MonthlyCmd.Flags().BoolVarP(&envDetails, "env-details", "", + false, "Print details of each environment") + MonthlyCmd.Flags().IntVarP(&conn, "conn", "c", + 4, "Number of connections") + MonthlyCmd.Flags().StringVarP(&org, "org", "o", + "", "Apigee organization name") + + _ = MonthlyCmd.MarkFlagRequired("month") + _ = MonthlyCmd.MarkFlagRequired("year") +} diff --git a/cmd/org/reports.go b/cmd/org/reports.go new file mode 100644 index 000000000..6c6690357 --- /dev/null +++ b/cmd/org/reports.go @@ -0,0 +1,32 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org + +import ( + "github.com/spf13/cobra" +) + +//ReportCmd to manage org reprots +var ReportCmd = &cobra.Command{ + Use: "reports", + Aliases: []string{"orgs"}, + Short: "Report Apigee Org Usage", + Long: "Report Apigee Org Usage", +} + +func init() { + ReportCmd.AddCommand(MonthlyCmd) + ReportCmd.AddCommand(YearlyCmd) +} diff --git a/cmd/org/reportyearly.go b/cmd/org/reportyearly.go new file mode 100644 index 000000000..56869fd93 --- /dev/null +++ b/cmd/org/reportyearly.go @@ -0,0 +1,79 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org + +import ( + "fmt" + "os" + "text/tabwriter" + "time" + + "github.com/apigee/apigeecli/apiclient" + "github.com/apigee/apigeecli/client/orgs" + "github.com/spf13/cobra" +) + +//YearlyCmd to get monthly usage +var YearlyCmd = &cobra.Command{ + Use: "yearly", + Short: "Report yearly usage for an Apigee Org", + Long: "Report yearly usage for an Apigee Org", + Args: func(cmd *cobra.Command, args []string) (err error) { + return apiclient.SetApigeeOrg(org) + }, + RunE: func(cmd *cobra.Command, args []string) (err error) { + var apiCalls int + + if _, err = time.Parse("2006", fmt.Sprintf("%d", year)); err != nil { + return + } + + if envDetails { + w := tabwriter.NewWriter(os.Stdout, 26, 4, 0, ' ', 0) + fmt.Fprintln(w, "ENVIRONMENT\tMONTH\tAPI CALLS") + w.Flush() + } + + if apiCalls, err = orgs.TotalAPICallsInYear(year, envDetails, conn); err != nil { + return + } + + if envDetails { + fmt.Printf("\nSummary\n\n") + } + + w := tabwriter.NewWriter(os.Stdout, 26, 4, 0, ' ', 0) + fmt.Fprintln(w, "ORGANIZATION\tYEAR\tAPI CALLS") + fmt.Fprintf(w, "%s\t%d\t%d\n", apiclient.GetApigeeOrg(), year, apiCalls) + fmt.Fprintln(w) + w.Flush() + + return + }, +} + +func init() { + + YearlyCmd.Flags().IntVarP(&year, "year", "y", + -1, "Year") + YearlyCmd.Flags().BoolVarP(&envDetails, "env-details", "", + false, "Print details of each environment") + YearlyCmd.Flags().IntVarP(&conn, "conn", "c", + 4, "Number of connections") + YearlyCmd.Flags().StringVarP(&org, "org", "o", + "", "Apigee organization name") + + _ = YearlyCmd.MarkFlagRequired("year") +}