-
-
Notifications
You must be signed in to change notification settings - Fork 124
Expand file tree
/
Copy pathstatsviz.go
More file actions
273 lines (241 loc) · 7.23 KB
/
Copy pathstatsviz.go
File metadata and controls
273 lines (241 loc) · 7.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// Package statsviz allows visualizing Go [runtime/metrics] data in real time.
//
// Register Statsviz HTTP handlers with your server's [http.ServeMux]
// (preferred method):
//
// mux := http.NewServeMux()
// statsviz.Register(mux)
//
// Alternatively, you can register with [http.DefaultServeMux]
// though you shouldn't do that in production:
//
// ss := statsviz.NewServer()
// ss.Register(http.DefaultServeMux)
//
// By default, Statsviz is served at http://host:port/debug/statsviz/. This, and
// other settings, can be changed by passing some [Option] to [NewServer].
//
// If your application is not already running an HTTP server, you need to start
// one. Add "net/http" and "log" to your imports, and use the following code in
// your main function:
//
// go func() {
// log.Println(http.ListenAndServe("localhost:8080", nil))
// }()
//
// Then open your browser and visit http://localhost:8080/debug/statsviz/.
//
// # Advanced usage:
//
// If you want more control over Statsviz HTTP handlers, for examples if:
// - you're using some HTTP framework
// - you want to place Statsviz handler behind some middleware
//
// then use [NewServer] to obtain a [Server] instance. Both the [Server.Index] and
// [Server.Ws]() methods return [http.HandlerFunc].
//
// srv, err := statsviz.NewServer(); // Create server or handle error
// srv.Index() // UI (dashboard) http.HandlerFunc
// srv.Ws() // Websocket http.HandlerFunc
package statsviz
import (
"bytes"
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/arl/statsviz/internal/plot"
"github.com/arl/statsviz/internal/static"
)
const (
defaultRoot = "/debug/statsviz"
defaultSendInterval = time.Second
)
// RegisterDefault registers the Statsviz HTTP handlers on [http.DefaultServeMux].
//
// RegisterDefault should not be used in production.
func RegisterDefault(opts ...Option) error {
return Register(http.DefaultServeMux, opts...)
}
// Register registers the Statsviz HTTP handlers on the provided mux.
//
// Register must be called once per application.
func Register(mux *http.ServeMux, opts ...Option) error {
srv, err := NewServer(opts...)
if err != nil {
return err
}
srv.Register(mux)
return nil
}
// Server is the core component of Statsviz. It collects and periodically
// updates metrics data and provides two essential HTTP handlers:
// - the Index handler serves Statsviz user interface, allowing you to
// visualize runtime metrics on your browser.
// - The Ws handler establishes a WebSocket connection allowing the connected
// browser to receive metrics updates from the server.
//
// The zero value is a valid Server, with default options.
//
// NOTE: Having more than one Server in the same program is not supported (and
// is not useful anyway).
type Server struct {
cancel context.CancelFunc // terminate goroutines
clients *clients // connected websocket clients
interval time.Duration // interval between consecutive metrics emission
root string // HTTP path root
plots *plot.List // plots shown on the user interface
userPlots []plot.UserPlot
}
// NewServer constructs a new Statsviz Server with the provided options, or the
// default settings.
//
// Note that once the server is created, its HTTP handlers needs to be registered
// with some HTTP server. You can either use the Register method or register yourself
// the Index and Ws handlers.
func NewServer(opts ...Option) (*Server, error) {
var s Server
if err := s.init(opts...); err != nil {
return nil, err
}
return &s, nil
}
func (s *Server) init(opts ...Option) error {
*s = Server{
interval: defaultSendInterval,
root: defaultRoot,
}
for _, opt := range opts {
if err := opt(s); err != nil {
return err
}
}
pl, err := plot.NewList(s.userPlots)
if err != nil {
return err
}
s.plots = pl
ctx, cancel := context.WithCancel(context.Background())
s.cancel = cancel
s.clients = newClients(ctx, s.plots.Config())
// Collect metrics.
go func() {
tick := time.NewTicker(s.interval)
defer tick.Stop()
defer cancel()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
buf := bytes.Buffer{}
if _, err := s.plots.WriteTo(&buf); err != nil {
dbglog("failed to collect metrics: %v", err)
return
}
s.clients.broadcast(buf.Bytes())
}
}
}()
return nil
}
// Register registers the Statsviz HTTP handlers on the provided mux.
//
// Register must be called once per application.
func (s *Server) Register(mux *http.ServeMux) {
if s.plots == nil {
s.init()
}
mux.Handle(s.root+"/", s.Index())
mux.HandleFunc(s.root+"/ws", s.Ws())
}
// Close releases all resources used by the Server.
func (s *Server) Close() error {
s.cancel()
return nil
}
// Option is a configuration option for the Server.
type Option func(*Server) error
// SendFrequency changes the interval between successive acquisitions of metrics
// and their sending to the user interface. The default interval is one second.
func SendFrequency(intv time.Duration) Option {
return func(s *Server) error {
if intv <= 0 {
return fmt.Errorf("frequency must be a positive integer")
}
s.interval = intv
return nil
}
}
// Root changes the root path of the Statsviz user interface.
// The default is "/debug/statsviz".
func Root(path string) Option {
return func(s *Server) error {
s.root = strings.TrimSuffix(path, "/")
return nil
}
}
// TimeseriesPlot adds a new time series plot to Statsviz. This options can
// be added multiple times.
func TimeseriesPlot(tsp TimeSeriesPlot) Option {
return func(s *Server) error {
s.userPlots = append(s.userPlots, plot.UserPlot{Scatter: tsp.timeseries})
return nil
}
}
// Index returns the index handler, which responds with the Statsviz user
// interface HTML page. By default, the handler is served at the path specified
// by the root. Use [WithRoot] to change the path.
func (s *Server) Index() http.HandlerFunc {
prefix := s.root + "/"
dist := http.FileServerFS(static.Assets())
return http.StripPrefix(prefix, dist).ServeHTTP
}
func parseBoolEnv(name string) bool {
env := os.Getenv(name)
val, err := strconv.ParseBool(env)
if err != nil {
if env != "" {
fmt.Fprintf(os.Stderr, "statsviz: malformed %s %v\n", name, err)
}
}
return val
}
var debug = false
func dbglog(format string, args ...any) {
if debug {
fmt.Fprintf(os.Stderr, "statsviz: "+format+"\n", args...)
}
}
var wsUpgrader = sync.OnceValue(func() websocket.Upgrader {
var checkOrigin func(r *http.Request) bool
// Allow all origins for testing.
if debug = parseBoolEnv("STATSVIZ_DEBUG"); debug {
// passthrough
checkOrigin = func(r *http.Request) bool { return true }
}
return websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 2048,
CheckOrigin: checkOrigin,
}
})
// Ws returns the WebSocket handler used by Statsviz to send application
// metrics. The underlying net.Conn is used to upgrade the HTTP server
// connection to the WebSocket protocol.
func (s *Server) Ws() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
upgrader := wsUpgrader()
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
dbglog("failed to upgrade connection: %v", err)
return
}
s.clients.add(ws)
}
}