Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
### Breaking:

### Bug Fixes:
- fix(stats): `stats historical` now returns write errors instead of silently swallowing them. [#1678](https://github.com/fastly/cli/pull/1678)

### Enhancements:
- feat(stats): add `--field` flag to `stats historical` to filter to a single stats field. [#1678](https://github.com/fastly/cli/pull/1678)
- feat(stats): add `stats aggregate` subcommand for cross-service aggregated stats. [#1678](https://github.com/fastly/cli/pull/1678)
- feat(stats): add `stats usage` subcommand for bandwidth/request usage, with `--by-service` breakdown. [#1678](https://github.com/fastly/cli/pull/1678)
- feat(stats): add `stats domain-inspector` subcommand for domain-level metrics. [#1678](https://github.com/fastly/cli/pull/1678)
- feat(stats): add `stats origin-inspector` subcommand for origin-level metrics. [#1678](https://github.com/fastly/cli/pull/1678)

### Dependencies:
- build(deps): `golang.org/x/net` from 0.50.0 to 0.51.0 ([#1674](https://github.com/fastly/cli/pull/1674))
Expand Down
7 changes: 7 additions & 0 deletions pkg/api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,13 @@ type Interface interface {

GetRegions(context.Context) (*fastly.RegionsResponse, error)
GetStatsJSON(context.Context, *fastly.GetStatsInput, any) error
GetAggregateJSON(context.Context, *fastly.GetAggregateInput, any) error
Comment thread
anthony-gomez-fastly marked this conversation as resolved.
GetUsage(context.Context, *fastly.GetUsageInput) (*fastly.UsageResponse, error)
GetUsageByService(context.Context, *fastly.GetUsageInput) (*fastly.UsageByServiceResponse, error)
GetDomainMetricsForService(context.Context, *fastly.GetDomainMetricsInput) (*fastly.DomainInspector, error)
GetDomainMetricsForServiceJSON(context.Context, *fastly.GetDomainMetricsInput, any) error
GetOriginMetricsForService(context.Context, *fastly.GetOriginMetricsInput) (*fastly.OriginInspector, error)
GetOriginMetricsForServiceJSON(context.Context, *fastly.GetOriginMetricsInput, any) error

CreateManagedLogging(context.Context, *fastly.CreateManagedLoggingInput) (*fastly.ManagedLogging, error)

Expand Down
8 changes: 8 additions & 0 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -704,9 +704,13 @@ func Define( // nolint:revive // function-length
serviceresourcelinkList := serviceresourcelink.NewListCommand(serviceresourcelinkCmdRoot.CmdClause, data)
serviceresourcelinkUpdate := serviceresourcelink.NewUpdateCommand(serviceresourcelinkCmdRoot.CmdClause, data)
statsCmdRoot := stats.NewRootCommand(app, data)
statsAggregate := stats.NewAggregateCommand(statsCmdRoot.CmdClause, data)
statsDomainInspector := stats.NewDomainInspectorCommand(statsCmdRoot.CmdClause, data)
statsHistorical := stats.NewHistoricalCommand(statsCmdRoot.CmdClause, data)
statsOriginInspector := stats.NewOriginInspectorCommand(statsCmdRoot.CmdClause, data)
statsRealtime := stats.NewRealtimeCommand(statsCmdRoot.CmdClause, data)
statsRegions := stats.NewRegionsCommand(statsCmdRoot.CmdClause, data)
statsUsage := stats.NewUsageCommand(statsCmdRoot.CmdClause, data)
tlsConfigCmdRoot := tlsconfig.NewRootCommand(app, data)
tlsConfigDescribe := tlsconfig.NewDescribeCommand(tlsConfigCmdRoot.CmdClause, data)
tlsConfigList := tlsconfig.NewListCommand(tlsConfigCmdRoot.CmdClause, data)
Expand Down Expand Up @@ -1536,9 +1540,13 @@ func Define( // nolint:revive // function-length
serviceVersionUpdate,
ssoCmdRoot,
statsCmdRoot,
statsAggregate,
statsDomainInspector,
statsHistorical,
statsOriginInspector,
statsRealtime,
statsRegions,
statsUsage,
tlsConfigCmdRoot,
tlsConfigDescribe,
tlsConfigList,
Expand Down
89 changes: 89 additions & 0 deletions pkg/commands/stats/aggregate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package stats

import (
"context"
"encoding/json"
"fmt"
"io"

"github.com/fastly/go-fastly/v13/fastly"

"github.com/fastly/cli/pkg/argparser"
"github.com/fastly/cli/pkg/global"
)

// AggregateCommand exposes the Aggregate Stats API.
type AggregateCommand struct {
argparser.Base

by string
Comment thread
jedisct1 marked this conversation as resolved.
formatFlag string
from string
region string
to string
}

// NewAggregateCommand is the "stats aggregate" subcommand.
func NewAggregateCommand(parent argparser.Registerer, g *global.Data) *AggregateCommand {
var c AggregateCommand
c.Globals = g

c.CmdClause = parent.Command("aggregate", "View aggregated stats across all services")

// Optional.
c.CmdClause.Flag("from", "Start time").StringVar(&c.from)
c.CmdClause.Flag("to", "End time").StringVar(&c.to)
c.CmdClause.Flag("by", "Aggregation period (minute/hour/day)").EnumVar(&c.by, "minute", "hour", "day")
c.CmdClause.Flag("region", "Filter by region ('stats regions' to list)").StringVar(&c.region)
c.CmdClause.Flag("format", "Output format (json)").EnumVar(&c.formatFlag, "json")

return &c
}

// Exec implements the command interface.
func (c *AggregateCommand) Exec(_ io.Reader, out io.Writer) error {
input := fastly.GetAggregateInput{}
if c.by != "" {
input.By = &c.by
}
if c.from != "" {
input.From = &c.from
}
if c.region != "" {
input.Region = &c.region
}
if c.to != "" {
input.To = &c.to
}

var envelope statsResponse
Comment thread
anthony-gomez-fastly marked this conversation as resolved.
err := c.Globals.APIClient.GetAggregateJSON(context.TODO(), &input, &envelope)
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

if envelope.Status != statusSuccess {
Comment thread
anthony-gomez-fastly marked this conversation as resolved.
return fmt.Errorf("non-success response: %s", envelope.Msg)
}

switch c.formatFlag {
case "json":
for _, block := range envelope.Data {
if err := json.NewEncoder(out).Encode(block); err != nil {
c.Globals.ErrLog.Add(err)
return err
}
}
default:
writeHeader(out, envelope.Meta)
for _, block := range envelope.Data {
if err := fmtBlock(out, "aggregate", block); err != nil {
c.Globals.ErrLog.Add(err)
return err
}
}
}

return nil
}
84 changes: 84 additions & 0 deletions pkg/commands/stats/aggregate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package stats_test

import (
"bytes"
"context"
"encoding/json"
"io"
"testing"

"github.com/fastly/go-fastly/v13/fastly"

"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/mock"
"github.com/fastly/cli/pkg/testutil"
)

func TestAggregate(t *testing.T) {
args := testutil.SplitArgs
scenarios := []struct {
name string
args []string
api mock.API
wantError string
wantOutput string
}{
{
name: "success table",
args: args("stats aggregate"),
api: mock.API{GetAggregateJSONFn: getAggregateJSONOK},
wantOutput: "From:",
},
{
name: "success json",
args: args("stats aggregate --format=json"),
api: mock.API{GetAggregateJSONFn: getAggregateJSONOK},
wantOutput: `"start_time":0`,
},
{
name: "non-success status",
args: args("stats aggregate"),
api: mock.API{GetAggregateJSONFn: getAggregateJSONNonSuccess},
wantError: "non-success response",
},
{
name: "api error",
args: args("stats aggregate"),
api: mock.API{GetAggregateJSONFn: getAggregateJSONError},
wantError: errTest.Error(),
},
}
for _, tc := range scenarios {
t.Run(tc.name, func(t *testing.T) {
var stdout bytes.Buffer
app.Init = func(_ []string, _ io.Reader) (*global.Data, error) {
opts := testutil.MockGlobalData(tc.args, &stdout)
opts.APIClientFactory = mock.APIClient(tc.api)
return opts, nil
}
err := app.Run(tc.args, nil)
testutil.AssertErrorContains(t, err, tc.wantError)
testutil.AssertStringContains(t, stdout.String(), tc.wantOutput)
})
}
}

func getAggregateJSONOK(_ context.Context, _ *fastly.GetAggregateInput, o any) error {
msg := []byte(`{
"status": "success",
"meta": {"to": "Thu May 16 20:08:35 UTC 2013", "from": "Wed May 15 20:08:35 UTC 2013", "by": "day", "region": "all"},
"msg": null,
"data": [{"start_time": 0}]
}`)
return json.Unmarshal(msg, o)
}

func getAggregateJSONNonSuccess(_ context.Context, _ *fastly.GetAggregateInput, o any) error {
msg := []byte(`{"status": "error", "msg": "bad request", "meta": {}, "data": []}`)
return json.Unmarshal(msg, o)
}

func getAggregateJSONError(_ context.Context, _ *fastly.GetAggregateInput, _ any) error {
return errTest
}
Loading