mirror of
https://github.com/longsleep/realtimetraffic.git
synced 2024-11-16 08:48:33 +01:00
Rewrite of server code with Go.
This commit is contained in:
parent
ec371f324e
commit
54db38bac6
11 changed files with 573 additions and 247 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/vendor
|
||||||
|
/bin
|
60
Makefile
Normal file
60
Makefile
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
|
||||||
|
PKG := realtimetraffic
|
||||||
|
EXENAME := realtimetraffic
|
||||||
|
|
||||||
|
GOPATH = "$(CURDIR)/vendor:$(CURDIR)"
|
||||||
|
SYSTEM_GOPATH := /usr/share/gocode/src/
|
||||||
|
OUTPUT := $(CURDIR)/bin
|
||||||
|
|
||||||
|
DESTDIR ?= /
|
||||||
|
BIN := $(DESTDIR)/usr/sbin
|
||||||
|
CONF := $(DESTDIR)/$(CONFIG_PATH)
|
||||||
|
|
||||||
|
BUILD_ARCH := $(shell go env GOARCH)
|
||||||
|
DIST := $(CURDIR)/dist_$(BUILD_ARCH)
|
||||||
|
DIST_SRC := $(DIST)/src
|
||||||
|
DIST_BIN := $(DIST)/bin
|
||||||
|
|
||||||
|
build: get binary
|
||||||
|
|
||||||
|
gopath:
|
||||||
|
@echo GOPATH=$(GOPATH)
|
||||||
|
|
||||||
|
get:
|
||||||
|
GOPATH=$(GOPATH) go get $(PKG)
|
||||||
|
|
||||||
|
binary:
|
||||||
|
GOPATH=$(GOPATH) go build -o $(OUTPUT)/$(EXENAME) -ldflags '$(LDFLAGS)' $(PKG)
|
||||||
|
|
||||||
|
binaryrace:
|
||||||
|
GOPATH=$(GOPATH) go build -race -o $(OUTPUT)/$(EXENAME) -ldflags '$(LDFLAGS)' $(PKG)
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
GOPATH=$(GOPATH) go fmt $(PKG)/...
|
||||||
|
|
||||||
|
test: TESTDEPS = $(shell GOPATH=$(GOPATH) go list -f '{{.ImportPath}}{{"\n"}}{{join .Deps "\n"}}' $(PKG) |grep $(PKG))
|
||||||
|
test: get
|
||||||
|
GOPATH=$(GOPATH) go test -i $(TESTDEPS)
|
||||||
|
GOPATH=$(GOPATH) go test -v $(TESTDEPS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
GOPATH=$(GOPATH) go clean -i $(PKG)
|
||||||
|
rm -rf $(CURDIR)/pkg
|
||||||
|
|
||||||
|
distclean: clean
|
||||||
|
rm -rf $(DIST)
|
||||||
|
|
||||||
|
pristine: distclean
|
||||||
|
rm -rf vendor/*
|
||||||
|
|
||||||
|
$(DIST_SRC):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
$(DIST_BIN):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
dist_gopath: $(DIST_SRC)
|
||||||
|
find $(SYSTEM_GOPATH) -mindepth 1 -maxdepth 1 -type d \
|
||||||
|
-exec ln -sf {} $(DIST_SRC) \;
|
||||||
|
|
||||||
|
.PHONY: clean distclean pristine get build gopath binary
|
34
README.md
34
README.md
|
@ -8,46 +8,28 @@ Realtime Traffic is a Linux realtime trafic monitoring tool, graphing rx and tx
|
||||||
|
|
||||||
Dowload the software, either using Git, or grab a [ZIP](https://github.com/longsleep/realtimetraffic/archive/master.zip) and extract it somewhere.
|
Dowload the software, either using Git, or grab a [ZIP](https://github.com/longsleep/realtimetraffic/archive/master.zip) and extract it somewhere.
|
||||||
|
|
||||||
You can use the client right away without installation. Just open the file client/realtimetraffic.html in any modern browser, type in a WebSocket address of a server you started somewhere and press start.
|
Getting started:
|
||||||
|
|
||||||
To install the server, make sure you have [Python](http://www.python.org) (2.5, 2.6, 2.7 tested) and [tornado](http://pypi.python.org/pypi/tornado). For Python << 2.6 you also need [simplejson](http://pypi.python.org/pypi/simplejson). Then just startup the server.
|
|
||||||
|
|
||||||
On Ubuntu this is simple like this:
|
|
||||||
|
|
||||||
$ wget -O rtt.zip https://github.com/longsleep/realtimetraffic/archive/master.zip
|
$ wget -O rtt.zip https://github.com/longsleep/realtimetraffic/archive/master.zip
|
||||||
$ unzip rtt.zip
|
$ unzip rtt.zip
|
||||||
$ cd realtimetraffic-master
|
$ cd realtimetraffic-master
|
||||||
$ sudo apt-get install python-tornado
|
$ make
|
||||||
$ python trafficserver/trafficserver.py
|
$ ./bin/realtimetraffic
|
||||||
Server running on 127.0.0.1:8088 (ssl:False) ...
|
|
||||||
|
|
||||||
Now just open up your browser:
|
Now just open up your browser:
|
||||||
|
|
||||||
$ firefox http://127.0.0.1:8088/?autostart=1
|
$ firefox http://127.0.0.1:8088/?autostart=1
|
||||||
|
|
||||||
## Getting Started
|
See the usage information (-h) for options.
|
||||||
|
|
||||||
Startup the traffice server on a Linux machine of your choice.
|
|
||||||
|
|
||||||
$ python trafficserver/trafficserver.py
|
|
||||||
|
|
||||||
And open up the server's web page (http://yourserver:8088/).
|
|
||||||
|
|
||||||
See the usage information (--help) for options.
|
|
||||||
|
|
||||||
## Server usage options
|
## Server usage options
|
||||||
|
|
||||||
The trafficserver is basically a Websocket server pushing traffic data to any connected client.
|
The trafficserver is basically a Websocket server pushing traffic data to any connected client.
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage: trafficserver.py [options]
|
Usage of ./bin/realtimetraffic:
|
||||||
|
-client="./client": Path to client directory.
|
||||||
Options:
|
-listen="127.0.0.1:8088": Listen address.
|
||||||
-h, --help show this help message and exit
|
|
||||||
-l LISTEN, --listen=LISTEN
|
|
||||||
listen address (default: [127.0.0.1:8088])
|
|
||||||
--ssl_keyfile=FILE SSL key file
|
|
||||||
--ssl_certfile=FILE SSL certificate file
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Client parameters
|
## Client parameters
|
||||||
|
@ -66,7 +48,7 @@ This library was developed by Simon Eisenmann at [struktur AG](http://www.strukt
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2012 struktur AG
|
Copyright (C) 2012-2014 struktur AG
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -17,10 +17,18 @@
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
|
||||||
|
var add = function(a, b) {
|
||||||
|
return a+b;
|
||||||
|
}
|
||||||
|
|
||||||
var RealtimeTraffic = function() {
|
var RealtimeTraffic = function() {
|
||||||
|
|
||||||
this.tv = 500;
|
this.tv = 1000;
|
||||||
this.last_data = null;
|
this.last_data = null;
|
||||||
|
this.history = {
|
||||||
|
rx_bytes: [],
|
||||||
|
tx_bytes: []
|
||||||
|
};
|
||||||
this.connection = null;
|
this.connection = null;
|
||||||
this.initialize();
|
this.initialize();
|
||||||
|
|
||||||
|
@ -33,10 +41,10 @@
|
||||||
height: 300,
|
height: 300,
|
||||||
width: 690,
|
width: 690,
|
||||||
renderer: 'line',
|
renderer: 'line',
|
||||||
interpolation: 'linear',
|
interpolation: 'basis',
|
||||||
series: new Rickshaw.Series.FixedDuration([ { name: 'rx_tx_abs_diff', color: "palegreen" }, { name: 'rx_kbits', color: "mediumslateblue" }, { name: 'tx_kbits', color: "lightcoral" }], undefined, {
|
series: new Rickshaw.Series.FixedDuration([ { name: 'rx_tx_abs_diff', color: "palegreen" }, { name: 'rx_kbits', color: "mediumslateblue" }, { name: 'tx_kbits', color: "lightcoral" }], undefined, {
|
||||||
timeInterval: this.tv,
|
timeInterval: this.tv,
|
||||||
maxDataPoints: 200,
|
maxDataPoints: 100,
|
||||||
timeBase: new Date().getTime() / 1000
|
timeBase: new Date().getTime() / 1000
|
||||||
})
|
})
|
||||||
} );
|
} );
|
||||||
|
@ -126,14 +134,22 @@
|
||||||
|
|
||||||
connection.onmessage = function(e) {
|
connection.onmessage = function(e) {
|
||||||
var data = JSON.parse(e.data);
|
var data = JSON.parse(e.data);
|
||||||
var interface_data = data[interf];
|
var interface_data = data[data.name];
|
||||||
if (typeof(interface_data) !== "undefined") {
|
if (typeof(interface_data) !== "undefined") {
|
||||||
if (that.last_data !== null) {
|
if (that.last_data !== null) {
|
||||||
|
that.history.rx_bytes.push(interface_data.rx_bytes - that.last_data.rx_bytes);
|
||||||
|
that.history.tx_bytes.push(interface_data.tx_bytes - that.last_data.tx_bytes);
|
||||||
var d = {
|
var d = {
|
||||||
rx_kbits: ((interface_data.rx_bytes - that.last_data.rx_bytes) * 8 / 1024)*2,
|
rx_kbits: (that.history.rx_bytes.reduce(add, 0) / that.history.rx_bytes.length) * 8 / 1024,
|
||||||
tx_kbits: ((interface_data.tx_bytes - that.last_data.tx_bytes) * 8 / 1024)*2
|
tx_kbits: (that.history.tx_bytes.reduce(add, 0) / that.history.tx_bytes.length) * 8 / 1024
|
||||||
};
|
};
|
||||||
d.rx_tx_abs_diff = Math.abs(d.rx_kbits - d.tx_kbits);
|
d.rx_tx_abs_diff = Math.abs(d.rx_kbits - d.tx_kbits);
|
||||||
|
if (that.history.rx_bytes.length > 1) {
|
||||||
|
that.history.rx_bytes.shift();
|
||||||
|
}
|
||||||
|
if (that.history.tx_bytes.length > 1) {
|
||||||
|
that.history.tx_bytes.shift();
|
||||||
|
}
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
that.graph.series.addData(d);
|
that.graph.series.addData(d);
|
||||||
that.graph.render();
|
that.graph.render();
|
||||||
|
|
136
src/realtimetraffic/conn.go
Normal file
136
src/realtimetraffic/conn.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 struktur AG
|
||||||
|
* http://www.strukturag.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is baeed on ideas from the chat example of gorilla/websocket.
|
||||||
|
* Copyright 2013 Gary Burd. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
* are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
* other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
writeWait = 10 * time.Second
|
||||||
|
pongWait = 60 * time.Second
|
||||||
|
pingPeriod = (pongWait * 9) / 10
|
||||||
|
maxMessageSize = 512
|
||||||
|
)
|
||||||
|
|
||||||
|
type connection struct {
|
||||||
|
ws *websocket.Conn
|
||||||
|
send chan []byte
|
||||||
|
iface string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) readPump() {
|
||||||
|
defer func() {
|
||||||
|
h.unregister <- c
|
||||||
|
c.ws.Close()
|
||||||
|
}()
|
||||||
|
c.ws.SetReadLimit(maxMessageSize)
|
||||||
|
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
c.ws.SetPongHandler(func(string) error {
|
||||||
|
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
for {
|
||||||
|
_, _, err := c.ws.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) write(mt int, payload []byte) error {
|
||||||
|
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
return c.ws.WriteMessage(mt, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) writePump() {
|
||||||
|
ticker := time.NewTicker(pingPeriod)
|
||||||
|
defer func() {
|
||||||
|
ticker.Stop()
|
||||||
|
c.ws.Close()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message, ok := <-c.send:
|
||||||
|
if !ok {
|
||||||
|
c.write(websocket.CloseMessage, []byte{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.write(websocket.TextMessage, message); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "Method not allowed", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read request details.
|
||||||
|
r.ParseForm()
|
||||||
|
iface := r.FormValue("if")
|
||||||
|
if iface == "" {
|
||||||
|
iface = "eth0"
|
||||||
|
}
|
||||||
|
|
||||||
|
ws, err := websocket.Upgrade(w, r, nil, 1024, 1024)
|
||||||
|
if _, ok := err.(websocket.HandshakeError); ok {
|
||||||
|
http.Error(w, "Not a websocket handshake", 400)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c := &connection{send: make(chan []byte, 256), ws: ws, iface: iface}
|
||||||
|
h.register <- c
|
||||||
|
go c.writePump()
|
||||||
|
c.readPump()
|
||||||
|
}
|
38
src/realtimetraffic/data.go
Normal file
38
src/realtimetraffic/data.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 struktur AG
|
||||||
|
* http://www.strukturag.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type interfacedata struct {
|
||||||
|
data interface{}
|
||||||
|
iface string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *interfacedata) set(iface string, data interface{}) {
|
||||||
|
i.data = map[string]interface{}{
|
||||||
|
iface: data,
|
||||||
|
"name": iface,
|
||||||
|
}
|
||||||
|
i.iface = iface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *interfacedata) encode() ([]byte, error) {
|
||||||
|
return json.Marshal(i.data)
|
||||||
|
}
|
140
src/realtimetraffic/grabber.go
Normal file
140
src/realtimetraffic/grabber.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 struktur AG
|
||||||
|
* http://www.strukturag.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var grabkeys = []string{
|
||||||
|
"collisions",
|
||||||
|
"multicast",
|
||||||
|
"rx_bytes",
|
||||||
|
"rx_compressed",
|
||||||
|
"rx_crc_errors",
|
||||||
|
"rx_dropped",
|
||||||
|
"rx_errors",
|
||||||
|
"rx_fifo_errors",
|
||||||
|
"rx_frame_errors",
|
||||||
|
"rx_length_errors",
|
||||||
|
"rx_missed_errors",
|
||||||
|
"rx_over_errors",
|
||||||
|
"rx_packets",
|
||||||
|
"tx_aborted_errors",
|
||||||
|
"tx_bytes",
|
||||||
|
"tx_carrier_errors",
|
||||||
|
"tx_compressed",
|
||||||
|
"tx_dropped",
|
||||||
|
"tx_errors",
|
||||||
|
"tx_fifo_errors",
|
||||||
|
"tx_heartbeat_errors",
|
||||||
|
"tx_packets",
|
||||||
|
"tx_window_errors",
|
||||||
|
}
|
||||||
|
|
||||||
|
type grabber struct {
|
||||||
|
path string
|
||||||
|
iface string
|
||||||
|
quit chan bool
|
||||||
|
running bool
|
||||||
|
count uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrabber(iface string) *grabber {
|
||||||
|
return &grabber{
|
||||||
|
iface: iface,
|
||||||
|
path: fmt.Sprintf("/sys/class/net/%s/statistics", iface),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *grabber) grab() {
|
||||||
|
|
||||||
|
data := &interfacedata{}
|
||||||
|
statistics := map[string]interface{}{}
|
||||||
|
value := make([]byte, 100)
|
||||||
|
|
||||||
|
var file *os.File
|
||||||
|
var err error
|
||||||
|
for _, key := range grabkeys {
|
||||||
|
file, err = os.Open(path.Join(g.path, key))
|
||||||
|
if err == nil {
|
||||||
|
count, err := file.Read(value)
|
||||||
|
if err == nil {
|
||||||
|
statistics[key], err = strconv.ParseInt(strings.TrimSpace(string(value[:count])), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to process data", err, value[:count], count)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Println("Failed to read data", err)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
} else {
|
||||||
|
log.Println("Failed to load data", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.set(g.iface, statistics)
|
||||||
|
h.broadcast <- data
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *grabber) start() {
|
||||||
|
|
||||||
|
g.count++
|
||||||
|
if g.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g.quit = make(chan bool)
|
||||||
|
g.running = true
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("Grabbing started: %s\n", g.iface)
|
||||||
|
ticker := time.NewTicker(1000 * time.Millisecond)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
g.grab()
|
||||||
|
case <-g.quit:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *grabber) stop() {
|
||||||
|
|
||||||
|
if !g.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g.count--
|
||||||
|
if g.count == 0 {
|
||||||
|
g.running = false
|
||||||
|
close(g.quit)
|
||||||
|
fmt.Printf("Grabbing stopped: %s\n", g.iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
94
src/realtimetraffic/hub.go
Normal file
94
src/realtimetraffic/hub.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 struktur AG
|
||||||
|
* http://www.strukturag.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is baeed on ideas from the chat example of gorilla/websocket.
|
||||||
|
* Copyright 2013 Gary Burd. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
* are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
* other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
type hub struct {
|
||||||
|
grabbers map[string]*grabber
|
||||||
|
connections map[*connection]bool
|
||||||
|
broadcast chan *interfacedata
|
||||||
|
register chan *connection
|
||||||
|
unregister chan *connection
|
||||||
|
}
|
||||||
|
|
||||||
|
var h = hub{
|
||||||
|
broadcast: make(chan *interfacedata),
|
||||||
|
register: make(chan *connection),
|
||||||
|
unregister: make(chan *connection),
|
||||||
|
connections: make(map[*connection]bool),
|
||||||
|
grabbers: make(map[string]*grabber),
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hub) run() {
|
||||||
|
|
||||||
|
var eg *grabber
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case c := <-h.register:
|
||||||
|
h.connections[c] = true
|
||||||
|
if eg, ok = h.grabbers[c.iface]; !ok {
|
||||||
|
eg = newGrabber(c.iface)
|
||||||
|
h.grabbers[c.iface] = eg
|
||||||
|
}
|
||||||
|
eg.start()
|
||||||
|
case c := <-h.unregister:
|
||||||
|
delete(h.connections, c)
|
||||||
|
close(c.send)
|
||||||
|
if eg, ok = h.grabbers[c.iface]; ok {
|
||||||
|
eg.stop()
|
||||||
|
}
|
||||||
|
case d := <-h.broadcast:
|
||||||
|
for c := range h.connections {
|
||||||
|
if c.iface == d.iface {
|
||||||
|
if m, err := d.encode(); err == nil {
|
||||||
|
select {
|
||||||
|
case c.send <- m:
|
||||||
|
default:
|
||||||
|
close(c.send)
|
||||||
|
delete(h.connections, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
src/realtimetraffic/main.go
Normal file
73
src/realtimetraffic/main.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 struktur AG
|
||||||
|
* http://www.strukturag.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var templates *template.Template
|
||||||
|
|
||||||
|
func serveClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.Error(w, "Not found", 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "Method nod allowed", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
err := templates.ExecuteTemplate(w, "realtimetraffic.html", nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
client := flag.String("client", path.Join(cwd, "client"), "Full path to client directory.")
|
||||||
|
addr := flag.String("listen", "127.0.0.1:8088", "Listen address.")
|
||||||
|
|
||||||
|
templates, err = template.ParseGlob(path.Join(*client, "*.html"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to load templates: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
go h.run()
|
||||||
|
http.HandleFunc("/", serveClient)
|
||||||
|
http.HandleFunc("/realtimetraffic", serveWs)
|
||||||
|
http.Handle("/css/", http.FileServer(http.Dir(*client)))
|
||||||
|
http.Handle("/scripts/", http.FileServer(http.Dir(*client)))
|
||||||
|
http.Handle("/img/", http.FileServer(http.Dir(*client)))
|
||||||
|
http.Handle("/favicon.ico", http.FileServer(http.Dir(*client)))
|
||||||
|
|
||||||
|
err = http.ListenAndServe(*addr, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("ListenAndServe: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
import trafficserver
|
|
|
@ -1,214 +0,0 @@
|
||||||
#!/usr/bin/python
|
|
||||||
"""
|
|
||||||
Copyright (C) 2012 struktur AG
|
|
||||||
http://www.strukturag.com
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import tornado.httpserver
|
|
||||||
import tornado.websocket
|
|
||||||
import tornado.ioloop
|
|
||||||
import tornado.web
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
except ImportError:
|
|
||||||
import simplejson as json
|
|
||||||
|
|
||||||
# Globals
|
|
||||||
dataGrabbers = {}
|
|
||||||
dataGrabbed = {}
|
|
||||||
CLIENT_ROOT = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "client")))
|
|
||||||
|
|
||||||
|
|
||||||
class Grabber(object):
|
|
||||||
|
|
||||||
keys = [
|
|
||||||
"collisions",
|
|
||||||
"multicast",
|
|
||||||
"rx_bytes",
|
|
||||||
"rx_compressed",
|
|
||||||
"rx_crc_errors",
|
|
||||||
"rx_dropped",
|
|
||||||
"rx_errors",
|
|
||||||
"rx_fifo_errors",
|
|
||||||
"rx_frame_errors",
|
|
||||||
"rx_length_errors",
|
|
||||||
"rx_missed_errors",
|
|
||||||
"rx_over_errors",
|
|
||||||
"rx_packets",
|
|
||||||
"tx_aborted_errors",
|
|
||||||
"tx_bytes",
|
|
||||||
"tx_carrier_errors",
|
|
||||||
"tx_compressed",
|
|
||||||
"tx_dropped",
|
|
||||||
"tx_errors",
|
|
||||||
"tx_fifo_errors",
|
|
||||||
"tx_heartbeat_errors",
|
|
||||||
"tx_packets",
|
|
||||||
"tx_window_errors"
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, interface):
|
|
||||||
self.interface = interface
|
|
||||||
self.base = "/sys/class/net/%s/statistics" % interface
|
|
||||||
|
|
||||||
def grab(self):
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
for k in self.keys:
|
|
||||||
data[k] = self.read(k)
|
|
||||||
|
|
||||||
dataGrabbed[self.interface]=data
|
|
||||||
|
|
||||||
def read(self, key):
|
|
||||||
data = 0
|
|
||||||
fn = os.path.join(self.base, key)
|
|
||||||
if os.path.exists(fn):
|
|
||||||
fp = file(fn, "rb")
|
|
||||||
try:
|
|
||||||
data = fp.read()
|
|
||||||
finally:
|
|
||||||
fp.close()
|
|
||||||
data = int(data.strip())
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class WSHandler(tornado.websocket.WebSocketHandler):
|
|
||||||
|
|
||||||
dataSender = None
|
|
||||||
dataInterface = None
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
|
|
||||||
interface = self.dataInterface = self.get_argument("if", "eth0").lower()
|
|
||||||
|
|
||||||
grabber = dataGrabbers.get(interface, None)
|
|
||||||
if grabber is None:
|
|
||||||
# create new grabber
|
|
||||||
print >>sys.stdout, "Starting grabber for %s." % interface
|
|
||||||
callback = Grabber(interface)
|
|
||||||
grabber = tornado.ioloop.PeriodicCallback(callback.grab, 240)
|
|
||||||
grabber.start()
|
|
||||||
dataGrabbers[interface]=[1, grabber]
|
|
||||||
else:
|
|
||||||
dataGrabbers[interface][0] = grabber[0]+1
|
|
||||||
|
|
||||||
sender = self.dataSender = tornado.ioloop.PeriodicCallback(self.doSendData, 500)
|
|
||||||
sender.start()
|
|
||||||
|
|
||||||
def on_message(self, message):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_close(self):
|
|
||||||
|
|
||||||
self.dataSender.stop()
|
|
||||||
self.dataSender = None
|
|
||||||
|
|
||||||
grabber = dataGrabbers.get(self.dataInterface, None)
|
|
||||||
if grabber is not None:
|
|
||||||
dataGrabbers[self.dataInterface][0] = grabber[0]-1
|
|
||||||
if grabber[0] <= 0:
|
|
||||||
print >>sys.stdout, "Stopping grabber for %s." % self.dataInterface
|
|
||||||
grabber[1].stop()
|
|
||||||
del dataGrabbers[self.dataInterface]
|
|
||||||
|
|
||||||
def doSendData(self):
|
|
||||||
|
|
||||||
data = {
|
|
||||||
self.dataInterface: dataGrabbed.get(self.dataInterface, {})
|
|
||||||
}
|
|
||||||
self.write_message(json.dumps(data))
|
|
||||||
|
|
||||||
|
|
||||||
class ClientHandler(tornado.web.RequestHandler):
|
|
||||||
|
|
||||||
def get(self, filename):
|
|
||||||
|
|
||||||
if not filename or filename == "index.html":
|
|
||||||
filename = "realtimetraffic.html"
|
|
||||||
|
|
||||||
fn = os.path.abspath(os.path.join(CLIENT_ROOT, os.path.normpath(filename)))
|
|
||||||
if not fn.startswith(CLIENT_ROOT):
|
|
||||||
raise tornado.web.HTTPError(404)
|
|
||||||
if not os.path.isfile(fn):
|
|
||||||
raise tornado.web.HTTPError(404)
|
|
||||||
fp = file(fn, "rb")
|
|
||||||
try:
|
|
||||||
self.write(fp.read())
|
|
||||||
finally:
|
|
||||||
fp.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main(listen="127.0.0.1:8088"):
|
|
||||||
|
|
||||||
from optparse import OptionParser
|
|
||||||
|
|
||||||
parser = OptionParser()
|
|
||||||
parser.add_option("-l", "--listen", dest="listen", help="listen address (default: [%s])" % listen, default=listen)
|
|
||||||
parser.add_option("--ssl_keyfile", dest="ssl_keyfile", help="SSL key file", metavar="FILE")
|
|
||||||
parser.add_option("--ssl_certfile", dest="ssl_certfile", help="SSL certificate file", metavar="FILE")
|
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
|
||||||
|
|
||||||
if ":" in options.listen:
|
|
||||||
address, port = options.listen.split(":", 1)
|
|
||||||
port = int(port)
|
|
||||||
listen = options.listen
|
|
||||||
else:
|
|
||||||
address = options.listen
|
|
||||||
port = 8088
|
|
||||||
listen = "%s:%s" % (address, port)
|
|
||||||
|
|
||||||
application = tornado.web.Application([
|
|
||||||
(r'^/realtimetraffic$', WSHandler),
|
|
||||||
(r'^/css/(.*)$', tornado.web.StaticFileHandler, {'path': os.path.join(CLIENT_ROOT, 'css')}),
|
|
||||||
(r'^/scripts/(.*)$', tornado.web.StaticFileHandler, {'path': os.path.join(CLIENT_ROOT, 'scripts')}),
|
|
||||||
(r'^/img/(.*)$', tornado.web.StaticFileHandler, {'path': os.path.join(CLIENT_ROOT, 'img')}),
|
|
||||||
(r'^/(.*)$', ClientHandler)
|
|
||||||
], debug=False, static_path=CLIENT_ROOT)
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
ssl = False
|
|
||||||
if options.ssl_keyfile and options.ssl_certfile:
|
|
||||||
if not os.path.isfile(options.ssl_keyfile):
|
|
||||||
print >>sys.stderr, "SSL key file not found: %s" % options.ssl_keyfile
|
|
||||||
return 1
|
|
||||||
if not os.path.isfile(options.ssl_certfile):
|
|
||||||
print >>sys.stderr, "SSL certificate file not found: %s" % options.ssl_certfile
|
|
||||||
return 1
|
|
||||||
params["ssl_options"] = {
|
|
||||||
"keyfile": options.ssl_keyfile,
|
|
||||||
"certfile": options.ssl_certfile
|
|
||||||
}
|
|
||||||
ssl = True
|
|
||||||
|
|
||||||
http_server = tornado.httpserver.HTTPServer(application, **params)
|
|
||||||
http_server.listen(port=port, address=address)
|
|
||||||
print >>sys.stdout, "Server running on %s (ssl:%r) ..." % (listen, ssl)
|
|
||||||
try:
|
|
||||||
tornado.ioloop.IOLoop.instance().start()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
print >>sys.stdout, "Server stopped."
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
status = main()
|
|
||||||
sys.exit(status)
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue