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  }