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  }