github.com/codingfuture/orig-energi3@v0.8.4/dashboard/dashboard.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package dashboard 18 19 //go:generate yarn --cwd ./assets install 20 //go:generate yarn --cwd ./assets build 21 //go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/index.html assets/bundle.js 22 //go:generate sh -c "sed 's#var _bundleJs#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go" 23 //go:generate sh -c "sed 's#var _indexHtml#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go" 24 //go:generate gofmt -w -s assets.go 25 26 import ( 27 "fmt" 28 "net" 29 "net/http" 30 "runtime" 31 "sync" 32 "sync/atomic" 33 "time" 34 35 "io" 36 37 "github.com/elastic/gosigar" 38 "github.com/ethereum/go-ethereum/log" 39 "github.com/ethereum/go-ethereum/metrics" 40 "github.com/ethereum/go-ethereum/p2p" 41 "github.com/ethereum/go-ethereum/params" 42 "github.com/ethereum/go-ethereum/rpc" 43 "github.com/mohae/deepcopy" 44 "golang.org/x/net/websocket" 45 ) 46 47 const ( 48 activeMemorySampleLimit = 200 // Maximum number of active memory data samples 49 virtualMemorySampleLimit = 200 // Maximum number of virtual memory data samples 50 networkIngressSampleLimit = 200 // Maximum number of network ingress data samples 51 networkEgressSampleLimit = 200 // Maximum number of network egress data samples 52 processCPUSampleLimit = 200 // Maximum number of process cpu data samples 53 systemCPUSampleLimit = 200 // Maximum number of system cpu data samples 54 diskReadSampleLimit = 200 // Maximum number of disk read data samples 55 diskWriteSampleLimit = 200 // Maximum number of disk write data samples 56 ) 57 58 var nextID uint32 // Next connection id 59 60 // Dashboard contains the dashboard internals. 61 type Dashboard struct { 62 config *Config 63 64 listener net.Listener 65 conns map[uint32]*client // Currently live websocket connections 66 history *Message 67 lock sync.RWMutex // Lock protecting the dashboard's internals 68 69 logdir string 70 71 quit chan chan error // Channel used for graceful exit 72 wg sync.WaitGroup 73 } 74 75 // client represents active websocket connection with a remote browser. 76 type client struct { 77 conn *websocket.Conn // Particular live websocket connection 78 msg chan *Message // Message queue for the update messages 79 logger log.Logger // Logger for the particular live websocket connection 80 } 81 82 // New creates a new dashboard instance with the given configuration. 83 func New(config *Config, commit string, logdir string) *Dashboard { 84 now := time.Now() 85 versionMeta := "" 86 if len(params.VersionMeta) > 0 { 87 versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta) 88 } 89 return &Dashboard{ 90 conns: make(map[uint32]*client), 91 config: config, 92 quit: make(chan chan error), 93 history: &Message{ 94 General: &GeneralMessage{ 95 Commit: commit, 96 Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta), 97 }, 98 System: &SystemMessage{ 99 ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh), 100 VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh), 101 NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh), 102 NetworkEgress: emptyChartEntries(now, networkEgressSampleLimit, config.Refresh), 103 ProcessCPU: emptyChartEntries(now, processCPUSampleLimit, config.Refresh), 104 SystemCPU: emptyChartEntries(now, systemCPUSampleLimit, config.Refresh), 105 DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh), 106 DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh), 107 }, 108 }, 109 logdir: logdir, 110 } 111 } 112 113 // emptyChartEntries returns a ChartEntry array containing limit number of empty samples. 114 func emptyChartEntries(t time.Time, limit int, refresh time.Duration) ChartEntries { 115 ce := make(ChartEntries, limit) 116 for i := 0; i < limit; i++ { 117 ce[i] = &ChartEntry{ 118 Time: t.Add(-time.Duration(i) * refresh), 119 } 120 } 121 return ce 122 } 123 124 // Protocols implements the node.Service interface. 125 func (db *Dashboard) Protocols() []p2p.Protocol { return nil } 126 127 // APIs implements the node.Service interface. 128 func (db *Dashboard) APIs() []rpc.API { return nil } 129 130 // Start starts the data collection thread and the listening server of the dashboard. 131 // Implements the node.Service interface. 132 func (db *Dashboard) Start(server *p2p.Server) error { 133 log.Info("Starting dashboard") 134 135 db.wg.Add(2) 136 go db.collectData() 137 go db.streamLogs() 138 139 http.HandleFunc("/", db.webHandler) 140 http.Handle("/api", websocket.Handler(db.apiHandler)) 141 142 listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port)) 143 if err != nil { 144 return err 145 } 146 db.listener = listener 147 148 go http.Serve(listener, nil) 149 150 return nil 151 } 152 153 // Stop stops the data collection thread and the connection listener of the dashboard. 154 // Implements the node.Service interface. 155 func (db *Dashboard) Stop() error { 156 // Close the connection listener. 157 var errs []error 158 if err := db.listener.Close(); err != nil { 159 errs = append(errs, err) 160 } 161 // Close the collectors. 162 errc := make(chan error, 1) 163 for i := 0; i < 2; i++ { 164 db.quit <- errc 165 if err := <-errc; err != nil { 166 errs = append(errs, err) 167 } 168 } 169 // Close the connections. 170 db.lock.Lock() 171 for _, c := range db.conns { 172 if err := c.conn.Close(); err != nil { 173 c.logger.Warn("Failed to close connection", "err", err) 174 } 175 } 176 db.lock.Unlock() 177 178 // Wait until every goroutine terminates. 179 db.wg.Wait() 180 log.Info("Dashboard stopped") 181 182 var err error 183 if len(errs) > 0 { 184 err = fmt.Errorf("%v", errs) 185 } 186 187 return err 188 } 189 190 // webHandler handles all non-api requests, simply flattening and returning the dashboard website. 191 func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) { 192 log.Debug("Request", "URL", r.URL) 193 194 path := r.URL.String() 195 if path == "/" { 196 path = "/index.html" 197 } 198 blob, err := Asset(path[1:]) 199 if err != nil { 200 log.Warn("Failed to load the asset", "path", path, "err", err) 201 http.Error(w, "not found", http.StatusNotFound) 202 return 203 } 204 w.Write(blob) 205 } 206 207 // apiHandler handles requests for the dashboard. 208 func (db *Dashboard) apiHandler(conn *websocket.Conn) { 209 id := atomic.AddUint32(&nextID, 1) 210 client := &client{ 211 conn: conn, 212 msg: make(chan *Message, 128), 213 logger: log.New("id", id), 214 } 215 done := make(chan struct{}) 216 217 // Start listening for messages to send. 218 db.wg.Add(1) 219 go func() { 220 defer db.wg.Done() 221 222 for { 223 select { 224 case <-done: 225 return 226 case msg := <-client.msg: 227 if err := websocket.JSON.Send(client.conn, msg); err != nil { 228 client.logger.Warn("Failed to send the message", "msg", msg, "err", err) 229 client.conn.Close() 230 return 231 } 232 } 233 } 234 }() 235 236 db.lock.Lock() 237 // Send the past data. 238 client.msg <- deepcopy.Copy(db.history).(*Message) 239 // Start tracking the connection and drop at connection loss. 240 db.conns[id] = client 241 db.lock.Unlock() 242 defer func() { 243 db.lock.Lock() 244 delete(db.conns, id) 245 db.lock.Unlock() 246 }() 247 for { 248 r := new(Request) 249 if err := websocket.JSON.Receive(conn, r); err != nil { 250 if err != io.EOF { 251 client.logger.Warn("Failed to receive request", "err", err) 252 } 253 close(done) 254 return 255 } 256 if r.Logs != nil { 257 db.handleLogRequest(r.Logs, client) 258 } 259 } 260 } 261 262 // meterCollector returns a function, which retrieves a specific meter. 263 func meterCollector(name string) func() int64 { 264 if metric := metrics.DefaultRegistry.Get(name); metric != nil { 265 m := metric.(metrics.Meter) 266 return func() int64 { 267 return m.Count() 268 } 269 } 270 return func() int64 { 271 return 0 272 } 273 } 274 275 // collectData collects the required data to plot on the dashboard. 276 func (db *Dashboard) collectData() { 277 defer db.wg.Done() 278 279 systemCPUUsage := gosigar.Cpu{} 280 systemCPUUsage.Get() 281 var ( 282 mem runtime.MemStats 283 284 collectNetworkIngress = meterCollector("p2p/InboundTraffic") 285 collectNetworkEgress = meterCollector("p2p/OutboundTraffic") 286 collectDiskRead = meterCollector("eth/db/chaindata/disk/read") 287 collectDiskWrite = meterCollector("eth/db/chaindata/disk/write") 288 289 prevNetworkIngress = collectNetworkIngress() 290 prevNetworkEgress = collectNetworkEgress() 291 prevProcessCPUTime = getProcessCPUTime() 292 prevSystemCPUUsage = systemCPUUsage 293 prevDiskRead = collectDiskRead() 294 prevDiskWrite = collectDiskWrite() 295 296 frequency = float64(db.config.Refresh / time.Second) 297 numCPU = float64(runtime.NumCPU()) 298 ) 299 300 for { 301 select { 302 case errc := <-db.quit: 303 errc <- nil 304 return 305 case <-time.After(db.config.Refresh): 306 systemCPUUsage.Get() 307 var ( 308 curNetworkIngress = collectNetworkIngress() 309 curNetworkEgress = collectNetworkEgress() 310 curProcessCPUTime = getProcessCPUTime() 311 curSystemCPUUsage = systemCPUUsage 312 curDiskRead = collectDiskRead() 313 curDiskWrite = collectDiskWrite() 314 315 deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress) 316 deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress) 317 deltaProcessCPUTime = curProcessCPUTime - prevProcessCPUTime 318 deltaSystemCPUUsage = curSystemCPUUsage.Delta(prevSystemCPUUsage) 319 deltaDiskRead = curDiskRead - prevDiskRead 320 deltaDiskWrite = curDiskWrite - prevDiskWrite 321 ) 322 prevNetworkIngress = curNetworkIngress 323 prevNetworkEgress = curNetworkEgress 324 prevProcessCPUTime = curProcessCPUTime 325 prevSystemCPUUsage = curSystemCPUUsage 326 prevDiskRead = curDiskRead 327 prevDiskWrite = curDiskWrite 328 329 now := time.Now() 330 331 runtime.ReadMemStats(&mem) 332 activeMemory := &ChartEntry{ 333 Time: now, 334 Value: float64(mem.Alloc) / frequency, 335 } 336 virtualMemory := &ChartEntry{ 337 Time: now, 338 Value: float64(mem.Sys) / frequency, 339 } 340 networkIngress := &ChartEntry{ 341 Time: now, 342 Value: deltaNetworkIngress / frequency, 343 } 344 networkEgress := &ChartEntry{ 345 Time: now, 346 Value: deltaNetworkEgress / frequency, 347 } 348 processCPU := &ChartEntry{ 349 Time: now, 350 Value: deltaProcessCPUTime / frequency / numCPU * 100, 351 } 352 systemCPU := &ChartEntry{ 353 Time: now, 354 Value: float64(deltaSystemCPUUsage.Sys+deltaSystemCPUUsage.User) / frequency / numCPU, 355 } 356 diskRead := &ChartEntry{ 357 Time: now, 358 Value: float64(deltaDiskRead) / frequency, 359 } 360 diskWrite := &ChartEntry{ 361 Time: now, 362 Value: float64(deltaDiskWrite) / frequency, 363 } 364 sys := db.history.System 365 db.lock.Lock() 366 sys.ActiveMemory = append(sys.ActiveMemory[1:], activeMemory) 367 sys.VirtualMemory = append(sys.VirtualMemory[1:], virtualMemory) 368 sys.NetworkIngress = append(sys.NetworkIngress[1:], networkIngress) 369 sys.NetworkEgress = append(sys.NetworkEgress[1:], networkEgress) 370 sys.ProcessCPU = append(sys.ProcessCPU[1:], processCPU) 371 sys.SystemCPU = append(sys.SystemCPU[1:], systemCPU) 372 sys.DiskRead = append(sys.DiskRead[1:], diskRead) 373 sys.DiskWrite = append(sys.DiskWrite[1:], diskWrite) 374 db.lock.Unlock() 375 376 db.sendToAll(&Message{ 377 System: &SystemMessage{ 378 ActiveMemory: ChartEntries{activeMemory}, 379 VirtualMemory: ChartEntries{virtualMemory}, 380 NetworkIngress: ChartEntries{networkIngress}, 381 NetworkEgress: ChartEntries{networkEgress}, 382 ProcessCPU: ChartEntries{processCPU}, 383 SystemCPU: ChartEntries{systemCPU}, 384 DiskRead: ChartEntries{diskRead}, 385 DiskWrite: ChartEntries{diskWrite}, 386 }, 387 }) 388 } 389 } 390 } 391 392 // sendToAll sends the given message to the active dashboards. 393 func (db *Dashboard) sendToAll(msg *Message) { 394 db.lock.Lock() 395 for _, c := range db.conns { 396 select { 397 case c.msg <- msg: 398 default: 399 c.conn.Close() 400 } 401 } 402 db.lock.Unlock() 403 }