github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/commands/stats/realtime.go (about) 1 package stats 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 8 "github.com/fastly/go-fastly/v9/fastly" 9 10 "github.com/fastly/cli/pkg/api" 11 "github.com/fastly/cli/pkg/argparser" 12 "github.com/fastly/cli/pkg/global" 13 "github.com/fastly/cli/pkg/text" 14 ) 15 16 // RealtimeCommand exposes the Realtime Metrics API. 17 type RealtimeCommand struct { 18 argparser.Base 19 20 formatFlag string 21 serviceName argparser.OptionalServiceNameID 22 } 23 24 // NewRealtimeCommand is the "stats realtime" subcommand. 25 func NewRealtimeCommand(parent argparser.Registerer, g *global.Data) *RealtimeCommand { 26 var c RealtimeCommand 27 c.Globals = g 28 29 c.CmdClause = parent.Command("realtime", "View realtime stats for a Fastly service") 30 c.RegisterFlag(argparser.StringFlagOpts{ 31 Name: argparser.FlagServiceIDName, 32 Description: argparser.FlagServiceIDDesc, 33 Dst: &g.Manifest.Flag.ServiceID, 34 Short: 's', 35 }) 36 c.RegisterFlag(argparser.StringFlagOpts{ 37 Action: c.serviceName.Set, 38 Name: argparser.FlagServiceName, 39 Description: argparser.FlagServiceDesc, 40 Dst: &c.serviceName.Value, 41 }) 42 43 c.CmdClause.Flag("format", "Output format (json)").EnumVar(&c.formatFlag, "json") 44 45 return &c 46 } 47 48 // Exec implements the command interface. 49 func (c *RealtimeCommand) Exec(_ io.Reader, out io.Writer) error { 50 serviceID, source, flag, err := argparser.ServiceID(c.serviceName, *c.Globals.Manifest, c.Globals.APIClient, c.Globals.ErrLog) 51 if err != nil { 52 return err 53 } 54 if c.Globals.Verbose() { 55 argparser.DisplayServiceID(serviceID, flag, source, out) 56 } 57 58 switch c.formatFlag { 59 case "json": 60 if err := loopJSON(c.Globals.RTSClient, serviceID, out); err != nil { 61 c.Globals.ErrLog.AddWithContext(err, map[string]any{ 62 "Service ID": serviceID, 63 }) 64 return err 65 } 66 67 default: 68 if err := loopText(c.Globals.RTSClient, serviceID, out); err != nil { 69 c.Globals.ErrLog.AddWithContext(err, map[string]any{ 70 "Service ID": serviceID, 71 }) 72 return err 73 } 74 } 75 76 return nil 77 } 78 79 func loopJSON(client api.RealtimeStatsInterface, service string, out io.Writer) error { 80 var timestamp uint64 81 for { 82 var envelope struct { 83 Timestamp uint64 `json:"timestamp"` 84 Data []json.RawMessage `json:"data"` 85 } 86 87 err := client.GetRealtimeStatsJSON(&fastly.GetRealtimeStatsInput{ 88 ServiceID: service, 89 Timestamp: timestamp, 90 }, &envelope) 91 if err != nil { 92 text.Error(out, "fetching stats: %w", err) 93 continue 94 } 95 timestamp = envelope.Timestamp 96 97 for _, data := range envelope.Data { 98 _, err = out.Write(data) 99 if err != nil { 100 return fmt.Errorf("error: unable to write data to stdout: %w", err) 101 } 102 text.Break(out) 103 } 104 } 105 } 106 107 func loopText(client api.RealtimeStatsInterface, service string, out io.Writer) error { 108 var timestamp uint64 109 for { 110 var envelope realtimeResponse 111 112 err := client.GetRealtimeStatsJSON(&fastly.GetRealtimeStatsInput{ 113 ServiceID: service, 114 Timestamp: timestamp, 115 }, &envelope) 116 if err != nil { 117 text.Error(out, "fetching stats: %w", err) 118 continue 119 } 120 timestamp = envelope.Timestamp 121 122 for _, block := range envelope.Data { 123 agg := block.Aggregated 124 125 // FIXME: These are heavy-handed compatibility 126 // fixes for stats vs realtime, so we can use 127 // fmtBlock for both. 128 agg["start_time"] = block.Recorded 129 delete(agg, "miss_histogram") 130 131 if err := fmtBlock(out, service, agg); err != nil { 132 text.Error(out, "formatting stats: %w", err) 133 continue 134 } 135 } 136 } 137 }