github.com/oskarth/go-ethereum@v1.6.8-0.20191013093314-dac24a9d3494/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  }