github.com/beyonderyue/gochain@v2.2.26+incompatible/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 npm --prefix ./assets install 20 //go:generate ./assets/node_modules/.bin/webpack --config ./assets/webpack.config.js --context ./assets 21 //go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/dashboard.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 _dashboardHtml#//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 "io/ioutil" 29 "net" 30 "net/http" 31 "path/filepath" 32 "runtime" 33 "sync" 34 "sync/atomic" 35 "time" 36 37 "github.com/elastic/gosigar" 38 "github.com/gochain-io/gochain/log" 39 "github.com/gochain-io/gochain/metrics" 40 "github.com/gochain-io/gochain/p2p" 41 "github.com/gochain-io/gochain/params" 42 "github.com/gochain-io/gochain/rpc" 43 "golang.org/x/net/websocket" 44 ) 45 46 const ( 47 activeMemorySampleLimit = 200 // Maximum number of active memory data samples 48 virtualMemorySampleLimit = 200 // Maximum number of virtual memory data samples 49 networkIngressSampleLimit = 200 // Maximum number of network ingress data samples 50 networkEgressSampleLimit = 200 // Maximum number of network egress data samples 51 processCPUSampleLimit = 200 // Maximum number of process cpu data samples 52 systemCPUSampleLimit = 200 // Maximum number of system cpu data samples 53 diskReadSampleLimit = 200 // Maximum number of disk read data samples 54 diskWriteSampleLimit = 200 // Maximum number of disk write data samples 55 ) 56 57 var nextID uint32 // Next connection id 58 59 // Dashboard contains the dashboard internals. 60 type Dashboard struct { 61 config *Config 62 63 listener net.Listener 64 conns map[uint32]*client // Currently live websocket connections 65 charts *HomeMessage 66 commit string 67 lock sync.RWMutex // Lock protecting the dashboard's internals 68 69 quit chan chan error // Channel used for graceful exit 70 wg sync.WaitGroup 71 } 72 73 // client represents active websocket connection with a remote browser. 74 type client struct { 75 conn *websocket.Conn // Particular live websocket connection 76 msg chan Message // Message queue for the update messages 77 logger log.Logger // Logger for the particular live websocket connection 78 } 79 80 // New creates a new dashboard instance with the given configuration. 81 func New(config *Config, commit string) (*Dashboard, error) { 82 now := time.Now() 83 db := &Dashboard{ 84 conns: make(map[uint32]*client), 85 config: config, 86 quit: make(chan chan error), 87 charts: &HomeMessage{ 88 ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh), 89 VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh), 90 NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh), 91 NetworkEgress: emptyChartEntries(now, networkEgressSampleLimit, config.Refresh), 92 ProcessCPU: emptyChartEntries(now, processCPUSampleLimit, config.Refresh), 93 SystemCPU: emptyChartEntries(now, systemCPUSampleLimit, config.Refresh), 94 DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh), 95 DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh), 96 }, 97 commit: commit, 98 } 99 return db, nil 100 } 101 102 // emptyChartEntries returns a ChartEntry array containing limit number of empty samples. 103 func emptyChartEntries(t time.Time, limit int, refresh time.Duration) ChartEntries { 104 ce := make(ChartEntries, limit) 105 for i := 0; i < limit; i++ { 106 ce[i] = &ChartEntry{ 107 Time: t.Add(-time.Duration(i) * refresh), 108 } 109 } 110 return ce 111 } 112 113 // Protocols is a meaningless implementation of node.Service. 114 func (db *Dashboard) Protocols() []p2p.Protocol { return nil } 115 116 // APIs is a meaningless implementation of node.Service. 117 func (db *Dashboard) APIs() []rpc.API { return nil } 118 119 // Start implements node.Service, starting the data collection thread and the listening server of the dashboard. 120 func (db *Dashboard) Start(server *p2p.Server) error { 121 log.Info("Starting dashboard") 122 123 db.wg.Add(2) 124 go db.collectData() 125 go db.collectLogs() // In case of removing this line change 2 back to 1 in wg.Add. 126 127 http.HandleFunc("/", db.webHandler) 128 http.Handle("/api", websocket.Handler(db.apiHandler)) 129 130 listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port)) 131 if err != nil { 132 return err 133 } 134 db.listener = listener 135 136 go http.Serve(listener, nil) 137 138 return nil 139 } 140 141 // Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard. 142 func (db *Dashboard) Stop() error { 143 // Close the connection listener. 144 var errs []error 145 if err := db.listener.Close(); err != nil { 146 errs = append(errs, err) 147 } 148 // Close the collectors. 149 errc := make(chan error, 1) 150 for i := 0; i < 2; i++ { 151 db.quit <- errc 152 if err := <-errc; err != nil { 153 errs = append(errs, err) 154 } 155 } 156 // Close the connections. 157 db.lock.Lock() 158 for _, c := range db.conns { 159 if err := c.conn.Close(); err != nil { 160 c.logger.Warn("Failed to close connection", "err", err) 161 } 162 } 163 db.lock.Unlock() 164 165 // Wait until every goroutine terminates. 166 db.wg.Wait() 167 log.Info("Dashboard stopped") 168 169 var err error 170 if len(errs) > 0 { 171 err = fmt.Errorf("%v", errs) 172 } 173 174 return err 175 } 176 177 // webHandler handles all non-api requests, simply flattening and returning the dashboard website. 178 func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) { 179 log.Debug("Request", "URL", r.URL) 180 181 path := r.URL.String() 182 if path == "/" { 183 path = "/dashboard.html" 184 } 185 // If the path of the assets is manually set 186 if db.config.Assets != "" { 187 blob, err := ioutil.ReadFile(filepath.Join(db.config.Assets, path)) 188 if err != nil { 189 log.Warn("Failed to read file", "path", path, "err", err) 190 http.Error(w, "not found", http.StatusNotFound) 191 return 192 } 193 w.Write(blob) 194 return 195 } 196 blob, err := Asset(path[1:]) 197 if err != nil { 198 log.Warn("Failed to load the asset", "path", path, "err", err) 199 http.Error(w, "not found", http.StatusNotFound) 200 return 201 } 202 w.Write(blob) 203 } 204 205 // apiHandler handles requests for the dashboard. 206 func (db *Dashboard) apiHandler(conn *websocket.Conn) { 207 id := atomic.AddUint32(&nextID, 1) 208 client := &client{ 209 conn: conn, 210 msg: make(chan Message, 128), 211 logger: log.New("id", id), 212 } 213 done := make(chan struct{}) 214 215 // Start listening for messages to send. 216 db.wg.Add(1) 217 go func() { 218 defer db.wg.Done() 219 220 for { 221 select { 222 case <-done: 223 return 224 case msg := <-client.msg: 225 if err := websocket.JSON.Send(client.conn, msg); err != nil { 226 client.logger.Warn("Failed to send the message", "msg", msg, "err", err) 227 client.conn.Close() 228 return 229 } 230 } 231 } 232 }() 233 234 // Send the past data. 235 client.msg <- Message{ 236 General: &GeneralMessage{ 237 Version: fmt.Sprintf("v%s", params.Version), 238 Commit: db.commit, 239 }, 240 Home: &HomeMessage{ 241 ActiveMemory: db.charts.ActiveMemory, 242 VirtualMemory: db.charts.VirtualMemory, 243 NetworkIngress: db.charts.NetworkIngress, 244 NetworkEgress: db.charts.NetworkEgress, 245 ProcessCPU: db.charts.ProcessCPU, 246 SystemCPU: db.charts.SystemCPU, 247 DiskRead: db.charts.DiskRead, 248 DiskWrite: db.charts.DiskWrite, 249 }, 250 } 251 // Start tracking the connection and drop at connection loss. 252 db.lock.Lock() 253 db.conns[id] = client 254 db.lock.Unlock() 255 defer func() { 256 db.lock.Lock() 257 delete(db.conns, id) 258 db.lock.Unlock() 259 }() 260 for { 261 fail := []byte{} 262 if _, err := conn.Read(fail); err != nil { 263 close(done) 264 return 265 } 266 // Ignore all messages 267 } 268 } 269 270 // collectData collects the required data to plot on the dashboard. 271 func (db *Dashboard) collectData() { 272 defer db.wg.Done() 273 systemCPUUsage := gosigar.Cpu{} 274 systemCPUUsage.Get() 275 var ( 276 prevNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count() 277 prevNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count() 278 prevProcessCPUTime = getProcessCPUTime() 279 prevSystemCPUUsage = systemCPUUsage 280 prevDiskRead = metrics.DefaultRegistry.Get("gochain/db/chaindata/compact/input").(metrics.Meter).Count() 281 prevDiskWrite = metrics.DefaultRegistry.Get("gochain/db/chaindata/compact/output").(metrics.Meter).Count() 282 283 frequency = float64(db.config.Refresh / time.Second) 284 numCPU = float64(runtime.NumCPU()) 285 ) 286 287 for { 288 select { 289 case errc := <-db.quit: 290 errc <- nil 291 return 292 case <-time.After(db.config.Refresh): 293 systemCPUUsage.Get() 294 var ( 295 curNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count() 296 curNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count() 297 curProcessCPUTime = getProcessCPUTime() 298 curSystemCPUUsage = systemCPUUsage 299 curDiskRead = metrics.DefaultRegistry.Get("gochain/db/chaindata/compact/input").(metrics.Meter).Count() 300 curDiskWrite = metrics.DefaultRegistry.Get("gochain/db/chaindata/compact/output").(metrics.Meter).Count() 301 302 deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress) 303 deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress) 304 deltaProcessCPUTime = curProcessCPUTime - prevProcessCPUTime 305 deltaSystemCPUUsage = systemCPUUsage.Delta(prevSystemCPUUsage) 306 deltaDiskRead = curDiskRead - prevDiskRead 307 deltaDiskWrite = curDiskWrite - prevDiskWrite 308 ) 309 prevNetworkIngress = curNetworkIngress 310 prevNetworkEgress = curNetworkEgress 311 prevProcessCPUTime = curProcessCPUTime 312 prevSystemCPUUsage = curSystemCPUUsage 313 prevDiskRead = curDiskRead 314 prevDiskWrite = curDiskWrite 315 316 now := time.Now() 317 318 var mem runtime.MemStats 319 runtime.ReadMemStats(&mem) 320 activeMemory := &ChartEntry{ 321 Time: now, 322 Value: float64(mem.Alloc) / frequency, 323 } 324 virtualMemory := &ChartEntry{ 325 Time: now, 326 Value: float64(mem.Sys) / frequency, 327 } 328 networkIngress := &ChartEntry{ 329 Time: now, 330 Value: deltaNetworkIngress / frequency, 331 } 332 networkEgress := &ChartEntry{ 333 Time: now, 334 Value: deltaNetworkEgress / frequency, 335 } 336 processCPU := &ChartEntry{ 337 Time: now, 338 Value: deltaProcessCPUTime / frequency / numCPU * 100, 339 } 340 systemCPU := &ChartEntry{ 341 Time: now, 342 Value: float64(deltaSystemCPUUsage.Sys+deltaSystemCPUUsage.User) / frequency / numCPU, 343 } 344 diskRead := &ChartEntry{ 345 Time: now, 346 Value: float64(deltaDiskRead) / frequency, 347 } 348 diskWrite := &ChartEntry{ 349 Time: now, 350 Value: float64(deltaDiskWrite) / frequency, 351 } 352 db.charts.ActiveMemory = append(db.charts.ActiveMemory[1:], activeMemory) 353 db.charts.VirtualMemory = append(db.charts.VirtualMemory[1:], virtualMemory) 354 db.charts.NetworkIngress = append(db.charts.NetworkIngress[1:], networkIngress) 355 db.charts.NetworkEgress = append(db.charts.NetworkEgress[1:], networkEgress) 356 db.charts.ProcessCPU = append(db.charts.ProcessCPU[1:], processCPU) 357 db.charts.SystemCPU = append(db.charts.SystemCPU[1:], systemCPU) 358 db.charts.DiskRead = append(db.charts.DiskRead[1:], diskRead) 359 db.charts.DiskWrite = append(db.charts.DiskRead[1:], diskWrite) 360 361 db.sendToAll(&Message{ 362 Home: &HomeMessage{ 363 ActiveMemory: ChartEntries{activeMemory}, 364 VirtualMemory: ChartEntries{virtualMemory}, 365 NetworkIngress: ChartEntries{networkIngress}, 366 NetworkEgress: ChartEntries{networkEgress}, 367 ProcessCPU: ChartEntries{processCPU}, 368 SystemCPU: ChartEntries{systemCPU}, 369 DiskRead: ChartEntries{diskRead}, 370 DiskWrite: ChartEntries{diskWrite}, 371 }, 372 }) 373 } 374 } 375 } 376 377 // collectLogs collects and sends the logs to the active dashboards. 378 func (db *Dashboard) collectLogs() { 379 defer db.wg.Done() 380 381 id := 1 382 // TODO (kurkomisi): log collection comes here. 383 for { 384 select { 385 case errc := <-db.quit: 386 errc <- nil 387 return 388 case <-time.After(db.config.Refresh / 2): 389 db.sendToAll(&Message{ 390 Logs: &LogsMessage{ 391 Log: []string{fmt.Sprintf("%-4d: This is a fake log.", id)}, 392 }, 393 }) 394 id++ 395 } 396 } 397 } 398 399 // sendToAll sends the given message to the active dashboards. 400 func (db *Dashboard) sendToAll(msg *Message) { 401 db.lock.Lock() 402 for _, c := range db.conns { 403 select { 404 case c.msg <- *msg: 405 default: 406 c.conn.Close() 407 } 408 } 409 db.lock.Unlock() 410 }