This commit is contained in:
Kurkó Mihály 2017-09-13 10:30:31 +00:00 committed by GitHub
commit 8854f7012b
9 changed files with 991 additions and 7 deletions

View file

@ -30,6 +30,7 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/contracts/release"
"github.com/ethereum/go-ethereum/dashboard"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
@ -76,10 +77,11 @@ type ethstatsConfig struct {
}
type gethConfig struct {
Eth eth.Config
Shh whisper.Config
Node node.Config
Ethstats ethstatsConfig
Eth eth.Config
Shh whisper.Config
Node node.Config
Ethstats ethstatsConfig
Dashboard dashboard.Config
}
func loadConfig(file string, cfg *gethConfig) error {
@ -110,9 +112,10 @@ func defaultNodeConfig() node.Config {
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
// Load defaults.
cfg := gethConfig{
Eth: eth.DefaultConfig,
Shh: whisper.DefaultConfig,
Node: defaultNodeConfig(),
Eth: eth.DefaultConfig,
Shh: whisper.DefaultConfig,
Node: defaultNodeConfig(),
Dashboard: dashboard.DefaultConfig,
}
// Load config file.
@ -134,6 +137,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
}
utils.SetShhConfig(ctx, stack, &cfg.Shh)
utils.SetDashboardConfig(ctx, &cfg.Dashboard)
return stack, cfg
}
@ -153,6 +157,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
utils.RegisterEthService(stack, &cfg.Eth)
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
utils.RegisterDashboardService(stack, &cfg.Dashboard)
}
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
shhEnabled := enableWhisper(ctx)
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name)

View file

@ -61,6 +61,11 @@ var (
utils.DataDirFlag,
utils.KeyStoreDirFlag,
utils.NoUSBFlag,
utils.DashboardEnabledFlag,
utils.DashboardAddrFlag,
utils.DashboardPortFlag,
utils.DashboardRefreshFlag,
utils.DashboardAssetsFlag,
utils.EthashCacheDirFlag,
utils.EthashCachesInMemoryFlag,
utils.EthashCachesOnDiskFlag,

View file

@ -92,6 +92,16 @@ var AppHelpFlagGroups = []flagGroup{
utils.EthashDatasetsOnDiskFlag,
},
},
{
Name: "DASHBOARD",
Flags: []cli.Flag{
utils.DashboardEnabledFlag,
utils.DashboardAddrFlag,
utils.DashboardPortFlag,
utils.DashboardRefreshFlag,
utils.DashboardAssetsFlag,
},
},
{
Name: "TRANSACTION POOL",
Flags: []cli.Flag{

View file

@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/dashboard"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/gasprice"
@ -177,6 +178,31 @@ var (
Name: "lightkdf",
Usage: "Reduce key-derivation RAM & CPU usage at some expense of KDF strength",
}
// Dashboard settings
DashboardEnabledFlag = cli.BoolFlag{
Name: "dashboard",
Usage: "Enable the dashboard",
}
DashboardAddrFlag = cli.StringFlag{
Name: "dashboard.addr",
Usage: "Dashboard listening interface",
Value: dashboard.DefaultConfig.Host,
}
DashboardPortFlag = cli.IntFlag{
Name: "dashboard.host",
Usage: "Dashboard listening port",
Value: dashboard.DefaultConfig.Port,
}
DashboardRefreshFlag = cli.DurationFlag{
Name: "dashboard.refresh",
Usage: "Dashboard metrics collection refresh rate",
Value: dashboard.DefaultConfig.Refresh,
}
DashboardAssetsFlag = cli.StringFlag{
Name: "dashboard.assets",
Usage: "Developer flag to serve the dashboard from the local file system (default: \"\")",
Value: dashboard.DefaultConfig.Assets,
}
// Ethash settings
EthashCacheDirFlag = DirectoryFlag{
Name: "ethash.cachedir",
@ -997,6 +1023,14 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
}
}
// SetDashboardConfig applies dashboard related command line flags to the config.
func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) {
cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name)
cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name)
cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name)
cfg.Assets = ctx.GlobalString(DashboardAssetsFlag.Name)
}
// RegisterEthService adds an Ethereum client to the stack.
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
var err error
@ -1019,6 +1053,13 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
}
}
// RegisterDashboardService adds a dashboard to the stack.
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config) {
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return dashboard.New(cfg)
})
}
// RegisterShhService configures Whisper and adds it to the given node.
func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {

260
dashboard/assets.go Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,282 @@
Chart.defaults.global.legend = { enabled: false };
const Component = React.Component;
// const Component = Inferno.Component;
// isNullOrUndefined returns true if a variable is null or undefined.
let isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
// MenuItem renders an item for a Menu component and the belonging submenu items, if there is any.
class MenuItem extends Component {
render = () => <li>
<a>
<i className={`fa ${this.props.className}`}/>
{this.props.text}
<div className="fa fa-chevron-down"/>
</a>
{
// Render dropdown menu only if there are children.
isNullOrUndefined(this.props.children) || <ul className="nav child_menu">
{React.Children.map(this.props.children, child => <li>{child}</li>)}
</ul>
}
</li>;
}
// Menu renders a menu component.
let Menu = () => <ul className="nav side-menu">
<MenuItem className="fa-home" text="Home">
<a href="dashboard1.html">Dashboard1</a>
<a href="dashboard2.html">Dashboard2</a>
</MenuItem>
<MenuItem className="fa-edit" text="Networking">
<a href="networking.html">Networking</a>
</MenuItem>
<MenuItem className="fa-desktop" text="Txpool">
<a href="txpool.html">Txpool</a>
</MenuItem>
<MenuItem className="fa-table" text="Logs">
<a href="logs.html">Logs</a>
</MenuItem>
<MenuItem className="fa-clone" text="Blockchain">
<a href="blockchain1.html">Blockchain1</a>
<a href="blockchain2.html">Blockchain2</a>
<a href="blockchain3.html">Blockchain3</a>
<a href="blockchain4.html">Blockchain4</a>
<a href="blockchain5.html">Blockchain5</a>
</MenuItem>
<MenuItem className="fa-bar-chart-o" text="System"/>
</ul>;
let Clearfix = () => <div className="clearfix"/>;
// SideBar renders a sidebar component.
let SideBar = () => <div className="col-md-3 left_col">
<div className="left_col scroll-view">
<div className="navbar nav_title" style={{border: 0}}>
<a href="dashboard.html" className="site_title"><i className="fa fa-paw"/> <span>Go Ethereum Dashboard</span></a>
</div>
<Clearfix/>
<div id="sidebar-menu" className="main_menu_side hidden-print main_menu">
<div className="menu_section">
<Menu/>
</div>
</div>
</div>
</div>;
// TopNavigation renders a top navigation component.
let TopNavigation = () => <div className="top_nav">
<div className="nav_menu">
<nav className="" role="navigation">
<div className="nav toggle">
<a id="_______menu_toggle"> {/* TODO (kurkomisi): Resize the main container on toggle */}
<i className="fa fa-bars"/>
</a>
</div>
</nav>
</div>
</div>;
// Chart name is already in use.
// ChartComponent renders a chart component and updates it, when the related data changes.
class ChartComponent extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount = () => this.state.chart = new Chart(this.data, {
type: this.props.type,
data: this.props.data,
});
render = () => {
if (!isNullOrUndefined(this.state.chart)) {
this.state.chart.update();
}
return (
<div className={this.props.className}>
<div className="x_panel">
<div className="x_title">
<h2>{this.props.text}</h2>
<ul className="nav navbar-right panel_toolbox">
<li><a className="collapse-link"><i className="fa fa-chevron-up"/></a></li>
{
// Render dropdown menu only if there are children.
isNullOrUndefined(this.props.children) || <li className="dropdown">
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button"
aria-expanded="false"><i className="fa fa-wrench"/></a>
<ul className="dropdown-menu" role="menu">
{React.Children.map(this.props.children, child => <li>{child}</li>)}
</ul>
</li>
}
<li><a className="close-link"><i className="fa fa-close"/></a>
</li>
</ul>
<Clearfix/>
</div>
<div className="x_content">
{/* The chart will be generated here after the component is mounted (this.componentDidMount). */}
<canvas ref={data => {
this.data = data
}}/>
</div>
</div>
</div>
);
}
}
// Row renders a row component of charts only if there is any chart.
class Row extends Component {
render = () => isNullOrUndefined(this.props.children) || <div className="row"> {this.props.children} </div>;
}
// PageContent renders a component for the page content.
class PageContent extends Component {
render = () => <div className="right_col" role="main">
<div className="">
<div className="page-title">
<div className="title_left">
<h3>Go Ethereum Dashboard
<small>Statistics</small>
</h3>
</div>
</div>
<Clearfix/>
<Row>
<ChartComponent className="col-md-6 col-sm-6 col-xs-12" text="Memory usage system/memory/inuse" type="line" data={this.props.charts.memory}>
<a href="#">Settings 1</a>
<a href="#">Settings 2</a>
</ChartComponent>
<ChartComponent className="col-md-6 col-sm-6 col-xs-12" text="Inbound traffic p2p/InboundTraffic" type="line" data={this.props.charts.traffic}/>
</Row>
<Clearfix/>
</div>
</div>;
}
// Footer renders a footer.
let Footer = () => <footer>
<div className="pull-right">
Gentelella - Bootstrap Admin Template by <a href="https://colorlib.com">Colorlib</a>
</div>
<Clearfix/>
</footer>;
const MEMORY_SAMPLE_LIMIT = 200; // Maximum number of memory data samples.
const TRAFFIC_SAMPLE_LIMIT = 200; // Maximum number of traffic data samples.
// Dashboard renders a full dashboard component, which makes connection with the server and waits for messages.
// When there is a message, correspondingly updates the page's content.
class Dashboard extends Component {
constructor(props) {
super(props);
this.state = {
charts: {
memory: {
labels: [],
datasets: [{
label: "system/memory/inuse",
backgroundColor: "rgba(38, 185, 154, 0.31)",
borderColor: "rgba(38, 185, 154, 0.7)",
pointBorderColor: "rgba(38, 185, 154, 0.7)",
pointBackgroundColor: "rgba(38, 185, 154, 0.7)",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointBorderWidth: 1,
data: [],
}],
},
traffic: {
labels: [],
datasets: [{
label: "p2p/InboundTraffic",
backgroundColor: "rgba(3, 88, 106, 0.3)",
borderColor: "rgba(3, 88, 106, 0.70)",
pointBorderColor: "rgba(3, 88, 106, 0.70)",
pointBackgroundColor: "rgba(3, 88, 106, 0.70)",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(151,187,205,1)",
pointBorderWidth: 1,
data: [],
}],
},
},
};
}
updateCharts = msg => {
let memory = this.state.charts.memory;
let traffic = this.state.charts.traffic;
// Fill the dashboard with the past data. metrics is set only in the first msg, after the connection is established.
if (msg.metrics !== undefined) {
// Clear the arrays to prevent data confusion with the previous connection.
memory.labels = [];
traffic.labels = [];
memory.datasets[0].data = [];
traffic.datasets[0].data = [];
let mem = msg.metrics.memory;
let traff = msg.metrics.processor; // TODO (kurkomisi): !!!
// Put the past data to the beginning of the arrays. This prevents confusion with the next msg data, which goes to the end.
for (let i = mem.length - 1; i >= 0 && MEMORY_SAMPLE_LIMIT > memory.labels.length; --i) {
memory.labels.unshift(mem[i].time.substring(mem[i].time.length - 5));
traffic.labels.unshift(mem[i].time.substring(mem[i].time.length - 5));
memory.datasets[0].data.unshift(mem[i].value);
traffic.datasets[0].data.unshift(traff[i].value);
}
this.setState({charts: {memory: memory, traffic: traffic,}}); // Update the components.
return;
}
// Put the new data to the end of the arrays.
if (msg.memory !== undefined) {
// Remove the first elements in case the samples' amount exceeds the limit.
if (memory.labels.length === MEMORY_SAMPLE_LIMIT) {
memory.labels.shift();
traffic.labels.shift();
memory.datasets[0].data.shift();
traffic.datasets[0].data.shift();
}
memory.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
traffic.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
memory.datasets[0].data.push(msg.memory.value);
traffic.datasets[0].data.push(msg.processor.value);
this.setState({charts: {memory: memory, traffic: traffic,}}); // Update the components.
}
};
reconnect = () => {
let server = new WebSocket("ws://" + location.host + "/api");
let that = this;
server.onmessage = event => {
let msg = JSON.parse(event.data);
if (isNullOrUndefined(msg)) {
return;
}
that.updateCharts(msg);
};
server.onclose = () => setTimeout(that.reconnect, 3000);
};
componentDidMount = () => this.reconnect();
render = () => <div className="container body">
<div className="main_container">
<SideBar/>
<TopNavigation/>
<PageContent charts={this.state.charts}/>
<Footer/>
</div>
</div>;
}

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Go Ethereum Dashboard</title>
<link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/css/custom.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router/4.1.2/react-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-noty/2.4.1/packaged/jquery.noty.packaged.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.0.0/Chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.js"></script>
</head>
<body>
<!--<script src="Something replacing babel plugin"></script>-->
<!--<script src="https://unpkg.com/inferno@3.7.0/dist/inferno.js"></script>-->
<!--<script src="https://unpkg.com/inferno-component@3.7.0/dist/inferno-component.js"></script>-->
<!--<script src="https://unpkg.com/inferno-router@3.7.0/dist/inferno-router.min.js"></script>-->
<div id="dashboard" class="nav-md"></div>
<script type="text/babel" src="components/dashboard.js"></script>
<script type="text/babel">
ReactDOM.render(<Dashboard/>, document.getElementById('dashboard'));
</script>
<!--<script type="text/babel">-->
<!--Inferno.render(<Dashboard/>, document.getElementById('dashboard'));-->
<!--</script>-->
<script type="text/babel" src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
</body>
</html>

45
dashboard/config.go Normal file
View file

@ -0,0 +1,45 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dashboard
import "time"
// DefaultConfig contains default settings for the dashboard.
var DefaultConfig = Config{
Host: "localhost",
Port: 8080,
Refresh: time.Second,
}
// Config contains the configuration parameters of the dashboard.
type Config struct {
// Host is the host interface on which to start the dashboard server. If this
// field is empty, no dashboard will be started.
Host string `toml:",omitempty"`
// Port is the TCP port number on which to start the dashboard server. The
// default zero value is/ valid and will pick a port number randomly (useful
// for ephemeral nodes).
Port int `toml:",omitempty"`
// Refresh is the refresh rate of the data updates, the data will be collected this often.
Refresh time.Duration `toml:",omitempty"`
// Assets offers a possibility to manually set the dashboard website's location on the server side.
// It is useful for debugging, avoids the repeated generation of the binary.
Assets string `toml:",omitempty"`
}

286
dashboard/dashboard.go Normal file
View file

@ -0,0 +1,286 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dashboard
//go:generate go-bindata -nometadata -o assets.go -prefix assets -pkg dashboard assets/...
import (
"bytes"
"fmt"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/rcrowley/go-metrics"
"golang.org/x/net/websocket"
"io/ioutil"
"net"
"net/http"
"sync"
"sync/atomic"
"time"
)
const (
processorSampleLimit = 200
memorySampleLimit = 200
trafficSampleLimit = 200
)
var (
nextId uint32 // Next connection id
)
type dashboard struct {
config *Config
listener net.Listener
conns map[uint32]*client // Currently live websocket connections
Metrics *metricSamples `json:"metrics,omitempty"`
Stats *status `json:"stats,omitempty"`
lock sync.RWMutex // Lock protecting the dashboard's internals
closing chan chan error // Channel used for graceful exit
wg sync.WaitGroup
}
type client struct {
conn *websocket.Conn // Particular live websocket connection
msg chan *map[string]interface{} // Message queue for the update messages
logger log.Logger // Logger for the particular live websocket connection
}
type metricSamples struct {
Processor []*data `json:"processor,omitempty"`
Memory []*data `json:"memory,omitempty"`
}
type data struct {
Time time.Time `json:"time,omitempty"`
Value float64 `json:"value,omitempty"`
}
type status struct {
Peers int `json:"peers,omitempty"`
Block int `json:"block,omitempty"`
}
// New creates a new dashboard instance with the given configuration.
func New(config *Config) (*dashboard, error) {
return &dashboard{
conns: make(map[uint32]*client),
config: config,
Metrics: &metricSamples{},
closing: make(chan chan error),
}, nil
}
// Protocols is a meaningless implementation of node.Service.
func (db *dashboard) Protocols() []p2p.Protocol { return nil }
// APIs is a meaningless implementation of node.Service.
func (db *dashboard) APIs() []rpc.API { return nil }
// Start implements node.Service, starting the data collection thread and the listening server of the dashboard.
func (db *dashboard) Start(server *p2p.Server) error {
db.wg.Add(1)
go db.collectData()
http.HandleFunc("/", db.webHandler)
http.Handle("/api", websocket.Handler(db.apiHandler))
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port))
if err != nil {
return err
}
db.listener = listener
go http.Serve(listener, nil)
return nil
}
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
func (db *dashboard) Stop() error {
var err error
// Close the connection listener
if err = db.listener.Close(); err != nil {
log.Warn("Failed to close listener", "err", err)
}
errc := make(chan error)
db.closing <- errc
<-errc
db.lock.Lock()
for _, c := range db.conns {
if err := c.conn.Close(); err != nil {
c.logger.Warn("Failed to close connection", "err", err)
}
}
db.lock.Unlock()
db.wg.Wait()
log.Info("Dashboard stopped")
return err
}
// webHandler handles all non-api requests, simply flattening and returning the dashboard website.
func (db *dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
log.Info("Request", "URL", r.URL)
path := r.URL.String()
if path == "/" {
path = "/dashboard.html"
}
// If the path of the assets is manually set
if db.config.Assets != "" {
// Create the filename for ReadFile
var buffer bytes.Buffer
buffer.WriteString(db.config.Assets)
buffer.WriteString(path)
file, err := ioutil.ReadFile(buffer.String())
if err != nil {
log.Warn("Failed to read file", "err", err)
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write(file)
return
}
webapp, err := Asset(path[1:])
if err != nil {
log.Warn("Failed to load the asset", "path", path, "err", err)
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write(webapp)
}
// apiHandler handles requests for the dashboard.
func (db *dashboard) apiHandler(conn *websocket.Conn) {
id := atomic.AddUint32(&nextId, 1)
client := &client{
conn: conn,
msg: make(chan *map[string]interface{}, 128),
logger: log.New("id", id),
}
loss := make(chan bool, 1) // Buffered channel as sender may exit early
// Start listening for messages to send.
db.wg.Add(1)
go func() {
defer db.wg.Done()
for {
select {
case <-loss:
return
case msg := <-client.msg:
if err := websocket.JSON.Send(client.conn, msg); err != nil {
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
return
}
}
}
}()
// Send the past data.
client.msg <- &map[string]interface{}{
"metrics": db.Metrics,
}
// Start tracking the connection and drop at connection loss.
db.lock.Lock()
db.conns[id] = client
db.lock.Unlock()
defer func() {
db.lock.Lock()
delete(db.conns, id)
db.lock.Unlock()
}()
for {
fail := []byte{}
if _, err := conn.Read(fail); err != nil {
loss <- true
return
}
// Ignore all messages
}
}
// collectData collects the required data to plot on the dashboard.
func (db *dashboard) collectData() {
defer db.wg.Done()
for {
select {
case errc := <-db.closing:
errc <- nil
return
case <-time.After(db.config.Refresh):
now := time.Now()
traffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
traff := &data{
Time: now,
Value: traffic,
}
memory := &data{
Time: now,
Value: memoryInUse,
}
// TODO (kurkomisi): do not mix traffic with processor!
db.update(traff, memory)
}
}
}
// update updates the dashboards through the live websocket connections.
func (db *dashboard) update(processor *data, memory *data) {
// Remove the first elements in case the samples' amount exceeds the limit.
first := 0
if len(db.Metrics.Processor) == processorSampleLimit {
first = 1
}
db.Metrics.Processor = append(db.Metrics.Processor[first:], processor)
first = 0
if len(db.Metrics.Memory) == memorySampleLimit {
first = 1
}
db.Metrics.Memory = append(db.Metrics.Memory[first:], memory)
msg := &map[string]interface{}{
"processor": processor,
"memory": memory,
}
db.lock.Lock()
for _, c := range db.conns {
select {
case c.msg <- msg:
default:
c.logger.Warn("Client message queue is full")
}
}
db.lock.Unlock()
}