Repository navigation
Real Time Charts #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Real Time Charts #93
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,7 +29,7 @@ func NewRealtimeCommand(parent common.Registerer, globals *config.Data) *Realtim | |
| c.CmdClause = parent.Command("realtime", "View realtime stats for a Fastly service") | ||
| c.CmdClause.Flag("service-id", "Service ID").Short('s').Required().StringVar(&c.manifest.Flag.ServiceID) | ||
|
|
||
| c.CmdClause.Flag("format", "Output format (json)").EnumVar(&c.formatFlag, "json") | ||
| c.CmdClause.Flag("format", "Output format (json, chart)").EnumVar(&c.formatFlag, "json", "chart") | ||
|
|
||
| return &c | ||
| } | ||
|
|
@@ -46,6 +46,10 @@ func (c *RealtimeCommand) Exec(in io.Reader, out io.Writer) error { | |
| if err := loopJSON(c.Globals.RTSClient, service, out); err != nil { | ||
| return err | ||
| } | ||
| case "chart": | ||
| if err := loopChart(c.Globals.RTSClient, service, out); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| default: | ||
| if err := loopText(c.Globals.RTSClient, service, out); err != nil { | ||
|
|
@@ -64,6 +68,7 @@ func loopJSON(client api.RealtimeStatsInterface, service string, out io.Writer) | |
| Data []json.RawMessage `json:"data"` | ||
| } | ||
|
|
||
| // this is annoyingly similar to the same loop in loopText | ||
| err := client.GetRealtimeStatsJSON(&fastly.GetRealtimeStatsInput{ | ||
| Service: service, | ||
| Timestamp: timestamp, | ||
|
|
@@ -82,6 +87,74 @@ func loopJSON(client api.RealtimeStatsInterface, service string, out io.Writer) | |
| } | ||
|
|
||
| func loopText(client api.RealtimeStatsInterface, service string, out io.Writer) error { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this already slipped in, but loopText/loopJSON never return an error so there shouldn't be one in the function signature. |
||
| responses := make(chan realtimeResponse) | ||
| errors := make(chan error) | ||
| go fetchData(client, service, responses, errors) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Splitting fetchData also doesn't ever return, aside from being implicitly killed at process exit. This is risky because it means loopText etc can't add an innocent looking Personally I wouldn't solve this with channels at all–make fetchData return one batch of data and move it into the for loop a few lines down. |
||
|
|
||
| for { | ||
| select { | ||
| case err := <-errors: | ||
| text.Error(out, "fetching stats: %w", err) | ||
| continue | ||
| case envelope := <-responses: | ||
| for _, block := range envelope.Data { | ||
| agg := block.Aggregated | ||
|
|
||
| // FIXME: These are heavy-handed compatibility | ||
| // fixes for stats vs realtime, so we can use | ||
| // fmtBlock for both. | ||
| agg["start_time"] = block.Recorded | ||
| delete(agg, "miss_histogram") | ||
|
|
||
| if err := fmtBlock(out, service, agg); err != nil { | ||
| text.Error(out, "formatting stats: %w", err) | ||
| continue | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func loopChart(client api.RealtimeStatsInterface, service string, out io.Writer) error { | ||
| responses := make(chan realtimeResponse) | ||
| errors := make(chan error) | ||
|
|
||
| view, err := NewView(service) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer view.Close() | ||
| view.Resize() | ||
| view.SetLayout() | ||
|
|
||
| go fetchData(client, service, responses, errors) | ||
|
|
||
| for { | ||
| select { | ||
| case err := <-errors: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, here it makes more sense to use a channel because you do want to So I might do a combination of what was above:
|
||
| text.Error(out, "fetching stats: %w", err) | ||
| continue | ||
| case envelope := <-responses: | ||
| for _, block := range envelope.Data { | ||
| if err := view.UpdateStats(block); err != nil { | ||
| text.Error(out, "charting stats: %w", err) | ||
| continue | ||
| } | ||
| view.Render() | ||
| } | ||
| case e := <-view.Events: | ||
| switch e.ID { // event string/identifier | ||
| case "q", "<C-c>": // press 'q' or 'C-c' to quit | ||
| return nil | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This return is one place the infinite looping fetchData will leak. |
||
| case "<Resize>": | ||
| view.Resize() | ||
| view.Render() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func fetchData(client api.RealtimeStatsInterface, service string, responses chan realtimeResponse, errors chan error) { | ||
| var timestamp uint64 | ||
| for { | ||
| var envelope realtimeResponse | ||
|
|
@@ -91,24 +164,10 @@ func loopText(client api.RealtimeStatsInterface, service string, out io.Writer) | |
| Timestamp: timestamp, | ||
| }, &envelope) | ||
| if err != nil { | ||
| text.Error(out, "fetching stats: %w", err) | ||
| errors <- err | ||
| continue | ||
| } | ||
| timestamp = envelope.Timestamp | ||
|
|
||
| for _, block := range envelope.Data { | ||
| agg := block.Aggregated | ||
|
|
||
| // FIXME: These are heavy-handed compatibility | ||
| // fixes for stats vs realtime, so we can use | ||
| // fmtBlock for both. | ||
| agg["start_time"] = block.Recorded | ||
| delete(agg, "miss_histogram") | ||
|
|
||
| if err := fmtBlock(out, service, agg); err != nil { | ||
| text.Error(out, "formatting stats: %w", err) | ||
| continue | ||
| } | ||
| } | ||
| responses <- envelope | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, something about the termui takeover rubs me the wrong way here–I thought of both the historical & realtime stats commands as stages in a shell pipeline, so format might expand to json/csv/text but this full takeover might be its own command. I guess that's more of a UI question than anything else.