diff --git a/app/api_topology.go b/app/api_topology.go
index e05b385b2..57796d90f 100644
--- a/app/api_topology.go
+++ b/app/api_topology.go
@@ -35,7 +35,7 @@ func handleTopology(ctx context.Context, rep Reporter, renderer render.Renderer,
return
}
respondWith(w, http.StatusOK, APITopology{
- Nodes: detailed.Summaries(renderer.Render(report)),
+ Nodes: detailed.Summaries(report, renderer.Render(report)),
})
}
@@ -119,7 +119,7 @@ func handleWebsocket(
log.Errorf("Error generating report: %v", err)
return
}
- newTopo := detailed.Summaries(renderer.Render(report))
+ newTopo := detailed.Summaries(report, renderer.Render(report))
diff := detailed.TopoDiff(previousTopo, newTopo)
previousTopo = newTopo
diff --git a/app/router.go b/app/router.go
index 0f9f19780..260a08a55 100644
--- a/app/router.go
+++ b/app/router.go
@@ -84,7 +84,7 @@ func gzipHandler(h http.HandlerFunc) http.HandlerFunc {
func RegisterTopologyRoutes(router *mux.Router, r Reporter) {
get := router.Methods("GET").Subrouter()
get.HandleFunc("/api",
- gzipHandler(requestContextDecorator(apiHandler)))
+ gzipHandler(requestContextDecorator(apiHandler(r))))
get.HandleFunc("/api/topology",
gzipHandler(requestContextDecorator(topologyRegistry.makeTopologyList(r))))
get.HandleFunc("/api/topology/{topology}",
@@ -130,10 +130,17 @@ func RegisterReportPostHandler(a Adder, router *mux.Router) {
}))
}
-func apiHandler(_ context.Context, w http.ResponseWriter, r *http.Request) {
- respondWith(w, http.StatusOK, xfer.Details{
- ID: UniqueID,
- Version: Version,
- Hostname: hostname.Get(),
- })
+func apiHandler(rep Reporter) CtxHandlerFunc {
+ return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
+ report, err := rep.Report(ctx)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ respondWith(w, http.StatusOK, xfer.Details{
+ ID: UniqueID,
+ Version: Version,
+ Hostname: hostname.Get(),
+ Plugins: report.Plugins,
+ })
+ }
}
diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js
index 61327d4bb..2094e257b 100644
--- a/client/app/scripts/actions/app-actions.js
+++ b/client/app/scripts/actions/app-actions.js
@@ -317,7 +317,8 @@ export function receiveApiDetails(apiDetails) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_API_DETAILS,
hostname: apiDetails.hostname,
- version: apiDetails.version
+ version: apiDetails.version,
+ plugins: apiDetails.plugins
});
}
diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js
index ab9babe27..198d76441 100644
--- a/client/app/scripts/components/app.js
+++ b/client/app/scripts/components/app.js
@@ -11,6 +11,7 @@ import HelpPanel from './help-panel';
import Status from './status.js';
import Topologies from './topologies.js';
import TopologyOptions from './topology-options.js';
+import Plugins from './plugins.js';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import { pinNextMetric, hitEsc, unpinMetric,
selectMetric, toggleHelp } from '../actions/app-actions';
@@ -53,6 +54,7 @@ function getStateFromStores() {
updatePaused: AppStore.isUpdatePaused(),
updatePausedAt: AppStore.getUpdatePausedAt(),
version: AppStore.getVersion(),
+ plugins: AppStore.getPlugins(),
websocketClosed: AppStore.isWebsocketClosed()
};
}
@@ -178,6 +180,7 @@ export default class App extends React.Component {
+
diff --git a/client/app/scripts/components/node-details/node-details-info.js b/client/app/scripts/components/node-details/node-details-info.js
index e04062f2f..1a076d694 100644
--- a/client/app/scripts/components/node-details/node-details-info.js
+++ b/client/app/scripts/components/node-details/node-details-info.js
@@ -19,7 +19,7 @@ export default class NodeDetailsInfo extends React.Component {
render() {
let rows = (this.props.rows || []);
- const prime = rows.filter(row => row.prime);
+ const prime = rows.filter(row => row.priority < 10);
let notShown = 0;
if (!this.state.expanded && prime.length < rows.length) {
notShown = rows.length - prime.length;
diff --git a/client/app/scripts/components/plugins.js b/client/app/scripts/components/plugins.js
new file mode 100644
index 000000000..c2438a3f1
--- /dev/null
+++ b/client/app/scripts/components/plugins.js
@@ -0,0 +1,22 @@
+import React from 'react';
+
+export default class Plugins extends React.Component {
+ renderPlugin(plugin) {
+ return (
+
+ {plugin.label || plugin.id}
+
+ );
+ }
+
+ render() {
+ if (!this.props.plugins || this.props.plugins.length === 0) {
+ return No plugins loaded
;
+ }
+ return (
+
+ Plugins: {this.props.plugins.map(plugin => this.renderPlugin(plugin))}
+
+ );
+ }
+}
diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js
index 646e11f10..0b00441fa 100644
--- a/client/app/scripts/stores/app-store.js
+++ b/client/app/scripts/stores/app-store.js
@@ -46,6 +46,7 @@ let highlightedEdgeIds = makeSet();
let highlightedNodeIds = makeSet();
let hostname = '...';
let version = '...';
+let plugins = null;
let mouseOverEdgeId = null;
let mouseOverNodeId = null;
let nodeDetails = makeOrderedMap(); // nodeId -> details
@@ -275,6 +276,11 @@ export class AppStore extends Store {
return version;
}
+ getPlugins() {
+ return plugins;
+ }
+
+
isForceRelayout() {
return forceRelayout;
}
@@ -679,6 +685,7 @@ export class AppStore extends Store {
errorUrl = null;
hostname = payload.hostname;
version = payload.version;
+ plugins = payload.plugins;
this.__emitChange();
break;
}
diff --git a/common/xfer/constants.go b/common/xfer/constants.go
index 6bbc0d7c6..2f25747ca 100644
--- a/common/xfer/constants.go
+++ b/common/xfer/constants.go
@@ -13,7 +13,8 @@ const (
// Details are some generic details that can be fetched from /api
type Details struct {
- ID string `json:"id"`
- Version string `json:"version"`
- Hostname string `json:"hostname"`
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Hostname string `json:"hostname"`
+ Plugins PluginSpecs `json:"plugins,omitempty"`
}
diff --git a/common/xfer/plugin_spec.go b/common/xfer/plugin_spec.go
new file mode 100644
index 000000000..41f203cf1
--- /dev/null
+++ b/common/xfer/plugin_spec.go
@@ -0,0 +1,223 @@
+package xfer
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "sort"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/mndrix/ps"
+ "github.com/ugorji/go/codec"
+
+ "github.com/weaveworks/scope/test/reflect"
+)
+
+// PluginSpec is shared between the Probe, App, and UI. It is the plugin's
+// self-proclaimed description.
+type PluginSpec struct {
+ ID string `json:"id"`
+
+ // Label is a human-readable name of the plugin
+ Label string `json:"label"`
+
+ Description string `json:"description,omitempty"`
+
+ // Interfaces is a list of things this plugin can be used for (e.g. "reporter")
+ Interfaces []string `json:"interfaces"`
+}
+
+// PluginSpecs is a set of plugin specs keyed on ID. Clients must use
+// the Add method to add plugin specs
+type PluginSpecs struct {
+ psMap ps.Map
+}
+
+// EmptyPluginSpecs is the empty set of plugin specs.
+var EmptyPluginSpecs = PluginSpecs{ps.NewMap()}
+
+// MakePluginSpecs makes a new PluginSpecs with the given plugin specs.
+func MakePluginSpecs(specs ...PluginSpec) PluginSpecs {
+ return EmptyPluginSpecs.Add(specs...)
+}
+
+// Add adds the specs to the PluginSpecs. Add is the only valid way to grow a
+// PluginSpecs. Add returns the PluginSpecs to enable chaining.
+func (n PluginSpecs) Add(specs ...PluginSpec) PluginSpecs {
+ result := n.psMap
+ if result == nil {
+ result = ps.NewMap()
+ }
+ for _, spec := range specs {
+ result = result.Set(spec.ID, spec)
+ }
+ return PluginSpecs{result}
+}
+
+// Merge combines the two PluginSpecss and returns a new result.
+func (n PluginSpecs) Merge(other PluginSpecs) PluginSpecs {
+ nSize, otherSize := n.Size(), other.Size()
+ if nSize == 0 {
+ return other
+ }
+ if otherSize == 0 {
+ return n
+ }
+ result, iter := n.psMap, other.psMap
+ if nSize < otherSize {
+ result, iter = iter, result
+ }
+ iter.ForEach(func(key string, otherVal interface{}) {
+ result = result.Set(key, otherVal)
+ })
+ return PluginSpecs{result}
+}
+
+// Lookup the spec by 'key'
+func (n PluginSpecs) Lookup(key string) (PluginSpec, bool) {
+ if n.psMap != nil {
+ value, ok := n.psMap.Lookup(key)
+ if ok {
+ return value.(PluginSpec), true
+ }
+ }
+ return PluginSpec{}, false
+}
+
+// Keys is a list of all the keys in this set.
+func (n PluginSpecs) Keys() []string {
+ if n.psMap == nil {
+ return nil
+ }
+ k := n.psMap.Keys()
+ sort.Strings(k)
+ return k
+}
+
+// Size is the number of specs in the set
+func (n PluginSpecs) Size() int {
+ if n.psMap == nil {
+ return 0
+ }
+ return n.psMap.Size()
+}
+
+// ForEach executes f for each spec in the set. Nodes are traversed in sorted
+// order.
+func (n PluginSpecs) ForEach(f func(PluginSpec)) {
+ for _, key := range n.Keys() {
+ if val, ok := n.psMap.Lookup(key); ok {
+ f(val.(PluginSpec))
+ }
+ }
+}
+
+// Copy is a noop
+func (n PluginSpecs) Copy() PluginSpecs {
+ return n
+}
+
+func (n PluginSpecs) String() string {
+ keys := []string{}
+ if n.psMap == nil {
+ n = EmptyPluginSpecs
+ }
+ psMap := n.psMap
+ if psMap == nil {
+ psMap = ps.NewMap()
+ }
+ for _, k := range psMap.Keys() {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ buf := bytes.NewBufferString("{")
+ for _, key := range keys {
+ val, _ := psMap.Lookup(key)
+ fmt.Fprintf(buf, "%s: %s, ", key, spew.Sdump(val))
+ }
+ fmt.Fprintf(buf, "}")
+ return buf.String()
+}
+
+// DeepEqual tests equality with other PluginSpecss
+func (n PluginSpecs) DeepEqual(i interface{}) bool {
+ d, ok := i.(PluginSpecs)
+ if !ok {
+ return false
+ }
+
+ if n.Size() != d.Size() {
+ return false
+ }
+ if n.Size() == 0 {
+ return true
+ }
+
+ equal := true
+ n.psMap.ForEach(func(k string, val interface{}) {
+ if otherValue, ok := d.psMap.Lookup(k); !ok {
+ equal = false
+ } else {
+ equal = equal && reflect.DeepEqual(val, otherValue)
+ }
+ })
+ return equal
+}
+
+func (n PluginSpecs) toIntermediate() []PluginSpec {
+ intermediate := make([]PluginSpec, 0, n.Size())
+ n.ForEach(func(spec PluginSpec) {
+ intermediate = append(intermediate, spec)
+ })
+ return intermediate
+}
+
+func (n PluginSpecs) fromIntermediate(specs []PluginSpec) PluginSpecs {
+ return MakePluginSpecs(specs...)
+}
+
+// CodecEncodeSelf implements codec.Selfer
+func (n *PluginSpecs) CodecEncodeSelf(encoder *codec.Encoder) {
+ if n.psMap != nil {
+ encoder.Encode(n.toIntermediate())
+ } else {
+ encoder.Encode(nil)
+ }
+}
+
+// CodecDecodeSelf implements codec.Selfer
+func (n *PluginSpecs) CodecDecodeSelf(decoder *codec.Decoder) {
+ in := []PluginSpec{}
+ if err := decoder.Decode(&in); err != nil {
+ return
+ }
+ *n = PluginSpecs{}.fromIntermediate(in)
+}
+
+// MarshalJSON shouldn't be used, use CodecEncodeSelf instead
+func (PluginSpecs) MarshalJSON() ([]byte, error) {
+ panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
+}
+
+// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead
+func (*PluginSpecs) UnmarshalJSON(b []byte) error {
+ panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
+}
+
+// GobEncode implements gob.Marshaller
+func (n PluginSpecs) GobEncode() ([]byte, error) {
+ buf := bytes.Buffer{}
+ err := gob.NewEncoder(&buf).Encode(n.toIntermediate())
+ return buf.Bytes(), err
+}
+
+// GobDecode implements gob.Unmarshaller
+func (n *PluginSpecs) GobDecode(input []byte) error {
+ in := []PluginSpec{}
+ if err := gob.NewDecoder(bytes.NewBuffer(input)).Decode(&in); err != nil {
+ return err
+ }
+ *n = PluginSpecs{}.fromIntermediate(in)
+ return nil
+}
diff --git a/common/xfer/plugin_spec_internal_test.go b/common/xfer/plugin_spec_internal_test.go
new file mode 100644
index 000000000..30e3a5adc
--- /dev/null
+++ b/common/xfer/plugin_spec_internal_test.go
@@ -0,0 +1,225 @@
+package xfer
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/weaveworks/scope/test/reflect"
+)
+
+var benchmarkResult PluginSpecs
+
+func TestMakePluginSpecs(t *testing.T) {
+ for _, testcase := range []struct {
+ inputs []string
+ wants []string
+ }{
+ {inputs: nil, wants: nil},
+ {
+ inputs: []string{"a"},
+ wants: []string{"a"},
+ },
+ {
+ inputs: []string{"a", "a"},
+ wants: []string{"a"},
+ },
+ {
+ inputs: []string{"b", "c", "a"},
+ wants: []string{"a", "b", "c"},
+ },
+ } {
+ var inputs []PluginSpec
+ for _, id := range testcase.inputs {
+ inputs = append(inputs, PluginSpec{ID: id})
+ }
+ have := MakePluginSpecs(inputs...)
+ var haveIDs []string
+ have.ForEach(func(p PluginSpec) {
+ haveIDs = append(haveIDs, p.ID)
+ })
+ if !reflect.DeepEqual(testcase.wants, haveIDs) {
+ t.Errorf("%#v: want %#v, have %#v", inputs, testcase.wants, haveIDs)
+ }
+ }
+}
+
+func BenchmarkMakePluginSpecs(b *testing.B) {
+ plugins := []PluginSpec{}
+ for i := 1000; i >= 0; i-- {
+ plugins = append(plugins, PluginSpec{ID: fmt.Sprint(i)})
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkResult = MakePluginSpecs(plugins...)
+ }
+}
+
+func TestPluginSpecsAdd(t *testing.T) {
+ for _, testcase := range []struct {
+ input PluginSpecs
+ plugins []PluginSpec
+ want PluginSpecs
+ }{
+ {
+ input: PluginSpecs{},
+ plugins: []PluginSpec{},
+ want: PluginSpecs{},
+ },
+ {
+ input: EmptyPluginSpecs,
+ plugins: []PluginSpec{},
+ want: EmptyPluginSpecs,
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "a"}),
+ plugins: []PluginSpec{},
+ want: MakePluginSpecs(PluginSpec{ID: "a"}),
+ },
+ {
+ input: EmptyPluginSpecs,
+ plugins: []PluginSpec{{ID: "a"}},
+ want: MakePluginSpecs(PluginSpec{ID: "a"}),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "a"}),
+ plugins: []PluginSpec{{ID: "a"}},
+ want: MakePluginSpecs(PluginSpec{ID: "a"}),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "b"}),
+ plugins: []PluginSpec{
+ {ID: "a"},
+ {ID: "b"},
+ },
+ want: MakePluginSpecs(
+ PluginSpec{ID: "a"},
+ PluginSpec{ID: "b"},
+ ),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "a"}),
+ plugins: []PluginSpec{
+ {ID: "c"},
+ {ID: "b"},
+ },
+ want: MakePluginSpecs(
+ PluginSpec{ID: "a"},
+ PluginSpec{ID: "b"},
+ PluginSpec{ID: "c"},
+ ),
+ },
+ {
+ input: MakePluginSpecs(
+ PluginSpec{ID: "a"},
+ PluginSpec{ID: "c"},
+ ),
+ plugins: []PluginSpec{
+ {ID: "b"},
+ {ID: "b"},
+ {ID: "b"},
+ },
+ want: MakePluginSpecs(
+ PluginSpec{ID: "a"},
+ PluginSpec{ID: "b"},
+ PluginSpec{ID: "c"},
+ ),
+ },
+ } {
+ originalLen := testcase.input.Size()
+ if want, have := testcase.want, testcase.input.Add(testcase.plugins...); !reflect.DeepEqual(want, have) {
+ t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.plugins, want, have)
+ }
+ if testcase.input.Size() != originalLen {
+ t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.plugins)
+ }
+ }
+}
+
+func BenchmarkPluginSpecsAdd(b *testing.B) {
+ n := EmptyPluginSpecs
+ for i := 0; i < 600; i++ {
+ n = n.Add(PluginSpec{ID: fmt.Sprint(i)})
+ }
+
+ plugin := PluginSpec{ID: "401.5"}
+
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkResult = n.Add(plugin)
+ }
+}
+
+func TestPluginSpecsMerge(t *testing.T) {
+ for _, testcase := range []struct {
+ input PluginSpecs
+ other PluginSpecs
+ want PluginSpecs
+ }{
+ {input: PluginSpecs{}, other: PluginSpecs{}, want: PluginSpecs{}},
+ {input: EmptyPluginSpecs, other: EmptyPluginSpecs, want: EmptyPluginSpecs},
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "a"}),
+ other: EmptyPluginSpecs,
+ want: MakePluginSpecs(PluginSpec{ID: "a"}),
+ },
+ {
+ input: EmptyPluginSpecs,
+ other: MakePluginSpecs(PluginSpec{ID: "a"}),
+ want: MakePluginSpecs(PluginSpec{ID: "a"}),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "a"}),
+ other: MakePluginSpecs(PluginSpec{ID: "b"}),
+ want: MakePluginSpecs(PluginSpec{ID: "a"}, PluginSpec{ID: "b"}),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "b"}),
+ other: MakePluginSpecs(PluginSpec{ID: "a"}),
+ want: MakePluginSpecs(PluginSpec{ID: "a"}, PluginSpec{ID: "b"}),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "a"}),
+ other: MakePluginSpecs(PluginSpec{ID: "a"}),
+ want: MakePluginSpecs(PluginSpec{ID: "a"}),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "a"}, PluginSpec{ID: "c"}),
+ other: MakePluginSpecs(PluginSpec{ID: "a"}, PluginSpec{ID: "b"}),
+ want: MakePluginSpecs(PluginSpec{ID: "a"}, PluginSpec{ID: "b"}, PluginSpec{ID: "c"}),
+ },
+ {
+ input: MakePluginSpecs(PluginSpec{ID: "b"}),
+ other: MakePluginSpecs(PluginSpec{ID: "a"}),
+ want: MakePluginSpecs(PluginSpec{ID: "a"}, PluginSpec{ID: "b"}),
+ },
+ } {
+ originalLen := testcase.input.Size()
+ if want, have := testcase.want, testcase.input.Merge(testcase.other); !reflect.DeepEqual(want, have) {
+ t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.other, want, have)
+ }
+ if testcase.input.Size() != originalLen {
+ t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.other)
+ }
+ }
+}
+
+func BenchmarkPluginSpecsMerge(b *testing.B) {
+ n, other := PluginSpecs{}, PluginSpecs{}
+ for i := 0; i < 600; i++ {
+ n = n.Add(PluginSpec{ID: fmt.Sprint(i)})
+ }
+
+ for i := 400; i < 1000; i++ {
+ other = other.Add(PluginSpec{ID: fmt.Sprint(i)})
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkResult = n.Merge(other)
+ }
+}
diff --git a/examples/plugins/README.md b/examples/plugins/README.md
new file mode 100644
index 000000000..79c023dfa
--- /dev/null
+++ b/examples/plugins/README.md
@@ -0,0 +1,57 @@
+# Scope Plugins
+
+## Protocol
+
+All plugins should listen for HTTP connections on a unix socket in the
+`/var/run/scope/plugins` directory. The scope probe will recursively scan that
+directory every 5 seconds, to look for sockets being added (or removed). It is
+also valid to put the plugin unix socket in a sub-directory, in case you want
+to apply some permissions, or store other information with the socket.
+
+When a new plugin is detected, the scope probe will conduct a basic
+[Handshake](#handshake) by requesting `GET /`.
+
+All plugin endpoints are expected to respond within 500ms, and respond in the JSON format.
+
+### Handshake
+
+When the scope probe discovers a new plugin unix socket it needs to know some
+information about the plugin. To learn this it will make a GET request for the
+`/` endpoint.
+
+An example response is:
+
+```json
+{
+ "name": "iowait",
+ "description": "Adds a graph of CPU IO Wait to hosts",
+ "interfaces": []string{"reporter"},
+ "api_version": "1",
+}
+```
+
+The fields are:
+
+* `name` is used to check for duplicate plugins, and displayed in the UI
+* `description` is displayed in the UI
+* `interfaces` tells the scope probe which endpoints this plugin supports
+* `api_version` is used to ensure both the plugin and the scope probe can speak to each other
+
+### Interfaces
+
+Currently the only interface a plugin can fulfill is `reporter`.
+
+#### Reporter
+
+The `reporter` interface allows a plugin to add information into the probe report. This could include more nodes, or new fields on existing nodes.
+
+Endpoints:
+
+* GET /report
+ This endpoint should return a scope probe-style report. For an example of the
+datastructure see `/api/report` on any scope instance. At the moment the plugin
+is limited to adding nodes or fields to existing topologies (Endpoint, Process,
+Container, etc), along with `metadata_templates` and `metric_templates` to
+display more information. For an example of adding a metric to the hosts, see
+[the example iowait
+plugin.](https://github.com/weaveworks/scope/tree/master/example/plugins/iowait)
diff --git a/examples/plugins/iowait/.gitignore b/examples/plugins/iowait/.gitignore
new file mode 100644
index 000000000..c01e9b159
--- /dev/null
+++ b/examples/plugins/iowait/.gitignore
@@ -0,0 +1 @@
+iowait
diff --git a/examples/plugins/iowait/Dockerfile b/examples/plugins/iowait/Dockerfile
new file mode 100644
index 000000000..036342b3e
--- /dev/null
+++ b/examples/plugins/iowait/Dockerfile
@@ -0,0 +1,6 @@
+FROM alpine:3.3
+MAINTAINER Weaveworks Inc
+LABEL works.weave.role=system
+COPY ./iowait /usr/bin/iowait
+RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
+ENTRYPOINT ["/usr/bin/iowait"]
diff --git a/examples/plugins/iowait/Makefile b/examples/plugins/iowait/Makefile
new file mode 100644
index 000000000..496249816
--- /dev/null
+++ b/examples/plugins/iowait/Makefile
@@ -0,0 +1,19 @@
+.PHONY: run clean
+
+EXE=iowait
+IMAGE=weavescope-iowait-plugin
+UPTODATE=.$(EXE).uptodate
+
+run: $(UPTODATE)
+ docker run --rm -it --privileged -v /var/run/scope/plugins:/var/run/scope/plugins --name $(IMAGE) $(IMAGE) -hostname=$(shell hostname)
+
+$(UPTODATE): $(EXE) Dockerfile
+ docker build -t $(IMAGE) .
+ touch $@
+
+$(EXE): main.go
+ docker run --rm -v "$$PWD":/usr/src/$(EXE) -w /usr/src/$(EXE) golang:1.6 go build -v
+
+clean:
+ - rm -rf $(UPTODATE) $(EXE)
+ - docker rmi $(IMAGE)
diff --git a/examples/plugins/iowait/main.go b/examples/plugins/iowait/main.go
new file mode 100644
index 000000000..bea3798d9
--- /dev/null
+++ b/examples/plugins/iowait/main.go
@@ -0,0 +1,138 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func main() {
+ hostname, _ := os.Hostname()
+ var (
+ addr = flag.String("addr", "/var/run/scope/plugins/iowait.sock", "unix socket to listen for connections on")
+ hostID = flag.String("hostname", hostname, "hostname of the host running this plugin")
+ )
+ flag.Parse()
+
+ log.Println("Starting...")
+
+ // Check we can get the iowait for the system
+ _, err := iowait()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ os.Remove(*addr)
+ listener, err := net.Listen("unix", *addr)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer func() {
+ listener.Close()
+ os.Remove(*addr)
+ }()
+
+ log.Printf("Listening on: unix://%s", *addr)
+
+ plugin := &Plugin{HostID: *hostID}
+ http.HandleFunc("/", plugin.Handshake)
+ http.HandleFunc("/report", plugin.Report)
+ if err := http.Serve(listener, nil); err != nil {
+ log.Printf("error: %v", err)
+ }
+}
+
+// Plugin groups the methods a plugin needs
+type Plugin struct {
+ HostID string
+}
+
+// Handshake is the first method that scope calls on this plugin. It is used
+// for the plugin to inform scope about the interfaces it fulfills, and to
+// ensure both scope and the plugin support the same api version.
+func (p *Plugin) Handshake(w http.ResponseWriter, r *http.Request) {
+ log.Printf("Probe %s handshake", r.FormValue("probe_id"))
+ err := json.NewEncoder(w).Encode(map[string]interface{}{
+ "name": "iowait",
+ "description": "Adds a graph of CPU IO Wait to hosts",
+ "interfaces": []string{"reporter"},
+ "api_version": "1",
+ })
+ if err != nil {
+ log.Printf("error: %v", err)
+ }
+}
+
+// Report is called by scope when a new report is needed. It is part of the
+// "reporter" interface, which this plugin implements.
+func (p *Plugin) Report(w http.ResponseWriter, r *http.Request) {
+ now := time.Now()
+ nowISO := now.Format(time.RFC3339)
+ value, err := iowait()
+ if err != nil {
+ log.Printf("error: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ err = json.NewEncoder(w).Encode(map[string]interface{}{
+ "Host": map[string]interface{}{
+ "nodes": map[string]interface{}{
+ p.HostID + ";": map[string]interface{}{
+ "metrics": map[string]interface{}{
+ "iowait": map[string]interface{}{
+ "samples": []interface{}{
+ map[string]interface{}{
+ "date": nowISO,
+ "value": value,
+ },
+ },
+ },
+ },
+ },
+ },
+ "metric_templates": map[string]interface{}{
+ "iowait": map[string]interface{}{
+ "id": "iowait",
+ "label": "IO Wait",
+ "format": "percent",
+ "priority": 0.1, // low number so it shows up first
+ },
+ },
+ },
+ })
+ if err != nil {
+ log.Printf("error: %v", err)
+ }
+}
+
+// Get the latest iowait value
+func iowait() (float64, error) {
+ out, err := exec.Command("iostat", "-c").Output()
+ if err != nil {
+ return 0, fmt.Errorf("iowait: %v", err)
+ }
+
+ // Linux 4.2.0-25-generic (a109563eab38) 04/01/16 _x86_64_(4 CPU)
+ //
+ // avg-cpu: %user %nice %system %iowait %steal %idle
+ // 2.37 0.00 1.58 0.01 0.00 96.04
+ lines := strings.Split(string(out), "\n")
+ if len(lines) < 4 {
+ return 0, fmt.Errorf("iowait: unexpected output: %q", out)
+ }
+
+ values := strings.Fields(lines[3])
+ if len(values) != 6 {
+ return 0, fmt.Errorf("iowait: unexpected output: %q", out)
+ }
+
+ return strconv.ParseFloat(values[3], 64)
+}
diff --git a/experimental/graphviz/render.go b/experimental/graphviz/render.go
index de452b89f..511184b6f 100644
--- a/experimental/graphviz/render.go
+++ b/experimental/graphviz/render.go
@@ -19,5 +19,5 @@ func renderTo(rpt report.Report, topology string) (detailed.NodeSummaries, error
if !ok {
return detailed.NodeSummaries{}, fmt.Errorf("unknown topology %v", topology)
}
- return detailed.Summaries(renderer.Render(rpt)), nil
+ return detailed.Summaries(rpt, renderer.Render(rpt)), nil
}
diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go
index 87b69a606..872e0f5e2 100644
--- a/probe/docker/reporter.go
+++ b/probe/docker/reporter.go
@@ -17,6 +17,31 @@ const (
ImageName = "docker_image_name"
)
+// Exposed for testing
+var (
+ ContainerMetadataTemplates = report.MetadataTemplates{
+ ContainerID: {ID: ContainerID, Label: "ID", From: report.FromLatest, Truncate: 12, Priority: 1},
+ ContainerStateHuman: {ID: ContainerStateHuman, Label: "State", From: report.FromLatest, Priority: 2},
+ ContainerCommand: {ID: ContainerCommand, Label: "Command", From: report.FromLatest, Priority: 3},
+ ImageID: {ID: ImageID, Label: "Image ID", From: report.FromLatest, Truncate: 12, Priority: 11},
+ ContainerUptime: {ID: ContainerUptime, Label: "Uptime", From: report.FromLatest, Priority: 12},
+ ContainerRestartCount: {ID: ContainerRestartCount, Label: "Restart #", From: report.FromLatest, Priority: 13},
+ ContainerIPs: {ID: ContainerIPs, Label: "IPs", From: report.FromSets, Priority: 14},
+ ContainerPorts: {ID: ContainerPorts, Label: "Ports", From: report.FromSets, Priority: 15},
+ ContainerCreated: {ID: ContainerCreated, Label: "Created", From: report.FromLatest, Priority: 16},
+ }
+
+ ContainerMetricTemplates = report.MetricTemplates{
+ CPUTotalUsage: {ID: CPUTotalUsage, Label: "CPU", Format: report.PercentFormat, Priority: 1},
+ MemoryUsage: {ID: MemoryUsage, Label: "Memory", Format: report.FilesizeFormat, Priority: 2},
+ }
+
+ ContainerImageMetadataTemplates = report.MetadataTemplates{
+ ImageID: {ID: ImageID, Label: "Image ID", From: report.FromLatest, Truncate: 12, Priority: 1},
+ report.Container: {ID: report.Container, Label: "# Containers", From: report.FromCounters, Datatype: "number", Priority: 2},
+ }
+)
+
// Reporter generate Reports containing Container and ContainerImage topologies
type Reporter struct {
registry Registry
@@ -69,7 +94,9 @@ func (r *Reporter) Report() (report.Report, error) {
}
func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology {
- result := report.MakeTopology()
+ result := report.MakeTopology().
+ WithMetadataTemplates(ContainerMetadataTemplates).
+ WithMetricTemplates(ContainerMetricTemplates)
result.Controls.AddControl(report.Control{
ID: StopContainer,
Human: "Stop",
@@ -117,7 +144,7 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology {
}
func (r *Reporter) containerImageTopology() report.Topology {
- result := report.MakeTopology()
+ result := report.MakeTopology().WithMetadataTemplates(ContainerImageMetadataTemplates)
r.registry.WalkImages(func(image *docker_client.APIImages) {
imageID := trimImageID(image.ID)
diff --git a/probe/host/reporter.go b/probe/host/reporter.go
index 587a50ff2..47413dfe4 100644
--- a/probe/host/reporter.go
+++ b/probe/host/reporter.go
@@ -31,6 +31,23 @@ const (
ProcMemInfo = "/proc/meminfo"
)
+// Exposed for testing.
+var (
+ MetadataTemplates = report.MetadataTemplates{
+ KernelVersion: {ID: KernelVersion, Label: "Kernel Version", From: report.FromLatest, Priority: 1},
+ Uptime: {ID: Uptime, Label: "Uptime", From: report.FromLatest, Priority: 2},
+ HostName: {ID: HostName, Label: "Hostname", From: report.FromLatest, Priority: 11},
+ OS: {ID: OS, Label: "OS", From: report.FromLatest, Priority: 12},
+ LocalNetworks: {ID: LocalNetworks, Label: "Local Networks", From: report.FromSets, Priority: 13},
+ }
+
+ MetricTemplates = report.MetricTemplates{
+ CPUUsage: {ID: CPUUsage, Label: "CPU", Format: report.PercentFormat, Priority: 1},
+ MemoryUsage: {ID: MemoryUsage, Label: "Memory", Format: report.FilesizeFormat, Priority: 2},
+ Load1: {ID: Load1, Label: "Load (1m)", Format: report.DefaultFormat, Group: "load", Priority: 11},
+ }
+)
+
// Reporter generates Reports containing the host topology.
type Reporter struct {
hostID string
@@ -98,6 +115,9 @@ func (r *Reporter) Report() (report.Report, error) {
return rep, err
}
+ rep.Host = rep.Host.WithMetadataTemplates(MetadataTemplates)
+ rep.Host = rep.Host.WithMetricTemplates(MetricTemplates)
+
now := mtime.Now()
metrics := GetLoad(now)
cpuUsage, max := GetCPUUsagePercent()
diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go
index 66eebaa18..e7da0c28a 100644
--- a/probe/kubernetes/reporter.go
+++ b/probe/kubernetes/reporter.go
@@ -7,6 +7,21 @@ import (
"github.com/weaveworks/scope/report"
)
+// Exposed for testing
+var (
+ PodMetadataTemplates = report.MetadataTemplates{
+ PodID: {ID: PodID, Label: "ID", From: report.FromLatest, Priority: 1},
+ Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 2},
+ PodCreated: {ID: PodCreated, Label: "Created", From: report.FromLatest, Priority: 3},
+ }
+
+ ServiceMetadataTemplates = report.MetadataTemplates{
+ ServiceID: {ID: ServiceID, Label: "ID", From: report.FromLatest, Priority: 1},
+ Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 2},
+ ServiceCreated: {ID: ServiceCreated, Label: "Created", From: report.FromLatest, Priority: 3},
+ }
+)
+
// Reporter generate Reports containing Container and ContainerImage topologies
type Reporter struct {
client Client
@@ -41,7 +56,7 @@ func (r *Reporter) Report() (report.Report, error) {
func (r *Reporter) serviceTopology() (report.Topology, []Service, error) {
var (
- result = report.MakeTopology()
+ result = report.MakeTopology().WithMetadataTemplates(ServiceMetadataTemplates)
services = []Service{}
)
err := r.client.WalkServices(func(s Service) error {
@@ -54,8 +69,11 @@ func (r *Reporter) serviceTopology() (report.Topology, []Service, error) {
}
func (r *Reporter) podTopology(services []Service) (report.Topology, report.Topology, error) {
- pods, containers := report.MakeTopology(), report.MakeTopology()
- selectors := map[string]labels.Selector{}
+ var (
+ pods = report.MakeTopology().WithMetadataTemplates(PodMetadataTemplates)
+ containers = report.MakeTopology()
+ selectors = map[string]labels.Selector{}
+ )
for _, service := range services {
selectors[service.ID()] = service.Selector()
}
diff --git a/probe/overlay/weave.go b/probe/overlay/weave.go
index 1a384daaa..9636b6213 100644
--- a/probe/overlay/weave.go
+++ b/probe/overlay/weave.go
@@ -168,6 +168,10 @@ func (w *Weave) Report() (report.Report, error) {
defer w.mtx.RUnlock()
r := report.MakeReport()
+ r.Container = r.Container.WithMetadataTemplates(report.MetadataTemplates{
+ WeaveMACAddress: {ID: WeaveMACAddress, Label: "Weave MAC", From: report.FromLatest, Priority: 17},
+ WeaveDNSHostname: {ID: WeaveDNSHostname, Label: "Weave DNS Name", From: report.FromLatest, Priority: 18},
+ })
for _, peer := range w.statusCache.Router.Peers {
r.Overlay.AddNode(report.MakeOverlayNodeID(peer.Name), report.MakeNodeWith(map[string]string{
WeavePeerName: peer.Name,
diff --git a/probe/plugins/registry.go b/probe/plugins/registry.go
new file mode 100644
index 000000000..ef55a0678
--- /dev/null
+++ b/probe/plugins/registry.go
@@ -0,0 +1,265 @@
+package plugins
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ log "github.com/Sirupsen/logrus"
+ "github.com/ugorji/go/codec"
+ "golang.org/x/net/context"
+ "golang.org/x/net/context/ctxhttp"
+
+ "github.com/weaveworks/scope/common/backoff"
+ "github.com/weaveworks/scope/common/fs"
+ "github.com/weaveworks/scope/common/xfer"
+ "github.com/weaveworks/scope/report"
+)
+
+// Exposed for testing
+var (
+ transport = makeUnixRoundTripper
+)
+
+const (
+ pluginTimeout = 500 * time.Millisecond
+ pluginRetry = 5 * time.Second
+ pollingInterval = 5 * time.Second
+)
+
+// Registry maintains a list of available plugins by name.
+type Registry struct {
+ rootPath string
+ apiVersion string
+ handshakeMetadata map[string]string
+ pluginsBySocket map[string]*Plugin
+ lock sync.RWMutex
+ context context.Context
+ cancel context.CancelFunc
+}
+
+// NewRegistry creates a new registry which watches the given dir root for new
+// plugins, and adds them.
+func NewRegistry(rootPath, apiVersion string, handshakeMetadata map[string]string) (*Registry, error) {
+ ctx, cancel := context.WithCancel(context.Background())
+ r := &Registry{
+ rootPath: rootPath,
+ apiVersion: apiVersion,
+ handshakeMetadata: handshakeMetadata,
+ pluginsBySocket: map[string]*Plugin{},
+ context: ctx,
+ cancel: cancel,
+ }
+ if err := r.scan(); err != nil {
+ r.Close()
+ return nil, err
+ }
+ go r.loop()
+ return r, nil
+}
+
+// loop periodically rescans for plugins
+func (r *Registry) loop() {
+ ticker := time.NewTicker(pollingInterval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-r.context.Done():
+ return
+ case <-ticker.C:
+ log.Debugf("plugins: scanning...")
+ if err := r.scan(); err != nil {
+ log.Warningf("plugins: error: %v", err)
+ }
+ }
+ }
+}
+
+// Rescan the plugins directory, load new plugins, and remove missing plugins
+func (r *Registry) scan() error {
+ sockets, err := r.sockets(r.rootPath)
+ if err != nil {
+ return err
+ }
+
+ r.lock.Lock()
+ plugins := map[string]*Plugin{}
+ // add (or keep) plugins which were found
+ for _, path := range sockets {
+ if plugin, ok := r.pluginsBySocket[path]; ok {
+ plugins[path] = plugin
+ continue
+ }
+ tr, err := transport(path, pluginTimeout)
+ if err != nil {
+ log.Warningf("plugins: error loading plugin %s: %v", path, err)
+ continue
+ }
+ client := &http.Client{Transport: tr, Timeout: pluginTimeout}
+ plugins[path] = NewPlugin(r.context, path, client, r.apiVersion, r.handshakeMetadata)
+ }
+ // remove plugins which weren't found
+ for path, plugin := range r.pluginsBySocket {
+ if _, ok := plugins[path]; !ok {
+ plugin.Close()
+ log.Infof("plugins: removed plugin %s", plugin.socket)
+ }
+ }
+ r.pluginsBySocket = plugins
+ r.lock.Unlock()
+ return nil
+}
+
+// sockets recursively finds all unix sockets under the path provided
+func (r *Registry) sockets(path string) ([]string, error) {
+ var (
+ result []string
+ statT syscall.Stat_t
+ )
+ // TODO: use of fs.Stat (which is syscall.Stat) here makes this linux specific.
+ if err := fs.Stat(path, &statT); err != nil {
+ return nil, err
+ }
+ switch statT.Mode & syscall.S_IFMT {
+ case syscall.S_IFDIR:
+ files, err := fs.ReadDir(path)
+ if err != nil {
+ return nil, err
+ }
+ for _, file := range files {
+ fpath := filepath.Join(path, file.Name())
+ s, err := r.sockets(fpath)
+ if err != nil {
+ log.Warningf("plugins: error loading path %s: %v", fpath, err)
+ }
+ result = append(result, s...)
+ }
+ case syscall.S_IFSOCK:
+ result = append(result, path)
+ }
+ return result, nil
+}
+
+// ForEach walks through all the plugins running f for each one.
+func (r *Registry) ForEach(f func(p *Plugin)) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ paths := []string{}
+ for path := range r.pluginsBySocket {
+ paths = append(paths, path)
+ }
+ sort.Strings(paths)
+ for _, path := range paths {
+ f(r.pluginsBySocket[path])
+ }
+}
+
+// Implementers walks the available plugins fulfilling the given interface
+func (r *Registry) Implementers(iface string, f func(p *Plugin)) {
+ r.ForEach(func(p *Plugin) {
+ for _, piface := range p.Interfaces {
+ if piface == iface {
+ f(p)
+ }
+ }
+ })
+}
+
+// Close shuts down the registry. It can still be used after this, but will be
+// out of date.
+func (r *Registry) Close() {
+ r.cancel()
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ for _, plugin := range r.pluginsBySocket {
+ plugin.Close()
+ }
+}
+
+// Plugin is the implementation of a plugin. It is responsible for doing the
+// plugin handshake, gathering reports, etc.
+type Plugin struct {
+ xfer.PluginSpec
+ context context.Context
+ socket string
+ client *http.Client
+ cancel context.CancelFunc
+ backoff backoff.Interface
+}
+
+// NewPlugin loads and initializes a new plugin. If client is nil,
+// http.DefaultClient will be used.
+func NewPlugin(ctx context.Context, socket string, client *http.Client, expectedAPIVersion string, handshakeMetadata map[string]string) *Plugin {
+ params := url.Values{}
+ for k, v := range handshakeMetadata {
+ params.Add(k, v)
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ p := &Plugin{context: ctx, socket: socket, client: client, cancel: cancel}
+ f := p.handshake(ctx, expectedAPIVersion, params)
+ f() // try the first time synchronously
+ p.backoff = backoff.New(f, "plugin handshake")
+ go p.backoff.Start()
+ return p
+}
+
+type handshakeResponse struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ Interfaces []string `json:"interfaces"`
+ APIVersion string `json:"api_version,omitempty"`
+}
+
+// handshake tries the handshake with this plugin.
+func (p *Plugin) handshake(ctx context.Context, expectedAPIVersion string, params url.Values) func() (bool, error) {
+ return func() (bool, error) {
+ var resp handshakeResponse
+ if err := p.get("/", params, &resp); err != nil {
+ return err == context.Canceled, fmt.Errorf("plugins: error loading plugin %s: %v", p.socket, err)
+ }
+
+ if resp.Name == "" {
+ return false, fmt.Errorf("plugins: error loading plugin %s: plugin did not provide a name", p.socket)
+ }
+ if resp.APIVersion != expectedAPIVersion {
+ return false, fmt.Errorf("plugins: error loading plugin %s: plugin did not provide correct API version: expected %q, got %q", p.socket, expectedAPIVersion, resp.APIVersion)
+ }
+ p.ID, p.Label = resp.Name, resp.Name
+ p.Description = resp.Description
+ p.Interfaces = resp.Interfaces
+ log.Infof("plugins: loaded plugin %s: %s", p.ID, strings.Join(p.Interfaces, ", "))
+ return true, nil
+ }
+}
+
+// Report gets the latest report from the plugin
+func (p *Plugin) Report() (report.Report, error) {
+ result := report.MakeReport()
+ err := p.get("/report", nil, &result)
+ return result, err
+}
+
+// TODO(paulbellamy): better error handling on wrong status codes
+func (p *Plugin) get(path string, params url.Values, result interface{}) error {
+ ctx, cancel := context.WithTimeout(p.context, pluginTimeout)
+ defer cancel()
+ resp, err := ctxhttp.Get(ctx, p.client, fmt.Sprintf("unix://%s?%s", path, params.Encode()))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ return codec.NewDecoder(resp.Body, &codec.JsonHandle{}).Decode(&result)
+}
+
+// Close closes the client
+func (p *Plugin) Close() {
+ p.backoff.Stop()
+ p.cancel()
+}
diff --git a/probe/plugins/registry_internal_test.go b/probe/plugins/registry_internal_test.go
new file mode 100644
index 000000000..590981dd0
--- /dev/null
+++ b/probe/plugins/registry_internal_test.go
@@ -0,0 +1,285 @@
+package plugins
+
+import (
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "net/http/httputil"
+ "path/filepath"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/paypal/ionet"
+
+ fs_hook "github.com/weaveworks/scope/common/fs"
+ "github.com/weaveworks/scope/test/fs"
+)
+
+func stubTransport(fn func(socket string, timeout time.Duration) (http.RoundTripper, error)) {
+ transport = fn
+}
+func restoreTransport() { transport = makeUnixRoundTripper }
+
+type readWriteCloseRoundTripper struct {
+ io.ReadWriteCloser
+}
+
+func (rwc readWriteCloseRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ conn := &closeableConn{
+ Conn: &ionet.Conn{R: rwc, W: rwc},
+ Closer: rwc,
+ }
+ client := httputil.NewClientConn(conn, nil)
+ defer client.Close()
+ return client.Do(req)
+}
+
+// closeableConn gives us an overrideable Close, where ionet.Conn does not.
+type closeableConn struct {
+ net.Conn
+ io.Closer
+}
+
+func (c *closeableConn) Close() error {
+ c.Conn.Close()
+ return c.Closer.Close()
+}
+
+type mockPlugin struct {
+ t *testing.T
+ Name string
+ Handler http.Handler
+ Requests chan *http.Request
+}
+
+func (p mockPlugin) dir() string {
+ return "/plugins"
+}
+
+func (p mockPlugin) path() string {
+ return filepath.Join(p.dir(), p.base())
+}
+
+func (p mockPlugin) base() string {
+ return p.Name + ".sock"
+}
+
+func (p mockPlugin) file() fs.File {
+ incomingR, incomingW := io.Pipe()
+ outgoingR, outgoingW := io.Pipe()
+ go func() {
+ conn := httputil.NewServerConn(&ionet.Conn{R: incomingR, W: outgoingW}, nil)
+ req, err := conn.Read()
+ if err != nil {
+ p.t.Fatal(err)
+ }
+ resp := httptest.NewRecorder()
+ p.Handler.ServeHTTP(resp, req)
+ fmt.Fprintf(outgoingW, "HTTP/1.1 200 OK\nContent-Length: %d\n\n%s", resp.Body.Len(), resp.Body.String())
+ if p.Requests != nil {
+ p.Requests <- req
+ }
+ }()
+ return fs.File{
+ FName: p.base(),
+ FWriter: incomingW,
+ FReader: outgoingR,
+ FStat: syscall.Stat_t{Mode: syscall.S_IFSOCK},
+ }
+}
+
+type chanWriter chan []byte
+
+func (w chanWriter) Write(p []byte) (int, error) {
+ w <- p
+ return len(p), nil
+}
+
+func (w chanWriter) Close() error {
+ close(w)
+ return nil
+}
+
+func setup(t *testing.T, sockets ...fs.Entry) fs.Entry {
+ mockFS := fs.Dir("", fs.Dir("plugins", sockets...))
+ fs_hook.Mock(
+ mockFS)
+
+ stubTransport(func(socket string, timeout time.Duration) (http.RoundTripper, error) {
+ f, err := mockFS.Open(socket)
+ return readWriteCloseRoundTripper{f}, err
+ })
+
+ return mockFS
+}
+
+func restore(t *testing.T) {
+ fs_hook.Restore()
+ restoreTransport()
+}
+
+type iterator func(func(*Plugin))
+
+func checkLoadedPlugins(t *testing.T, forEach iterator, expectedIDs []string) {
+ pluginIDs := []string{}
+ plugins := map[string]*Plugin{}
+ forEach(func(p *Plugin) {
+ pluginIDs = append(pluginIDs, p.ID)
+ plugins[p.ID] = p
+ })
+ if len(pluginIDs) != len(expectedIDs) {
+ t.Fatalf("Expected plugins %q, got: %q", expectedIDs, pluginIDs)
+ }
+ for i, id := range pluginIDs {
+ if id != expectedIDs[i] {
+ t.Fatalf("Expected plugins %q, got: %q", expectedIDs, pluginIDs)
+ }
+ }
+}
+
+// stringHandler returns an http.Handler which just prints the given string
+func stringHandler(j string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, j)
+ })
+}
+
+func TestRegistryLoadsExistingPlugins(t *testing.T) {
+ setup(t, mockPlugin{t: t, Name: "testPlugin", Handler: stringHandler(`{"name":"testPlugin","interfaces":["reporter"],"api_version":"1"}`)}.file())
+ defer restore(t)
+
+ root := "/plugins"
+ r, err := NewRegistry(root, "1", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ checkLoadedPlugins(t, r.ForEach, []string{"testPlugin"})
+}
+
+func TestRegistryLoadsExistingPluginsEvenWhenOneFails(t *testing.T) {
+ setup(
+ t,
+ // TODO: This first one needs to fail
+ fs.Dir("fail",
+ mockPlugin{t: t, Name: "aFailure", Handler: stringHandler(`{"name":"aFailure","interfaces":["reporter"],"api_version":"2"}`)}.file(),
+ ),
+ mockPlugin{t: t, Name: "testPlugin", Handler: stringHandler(`{"name":"testPlugin","interfaces":["reporter"],"api_version":"1"}`)}.file(),
+ )
+ defer restore(t)
+
+ root := "/plugins"
+ r, err := NewRegistry(root, "1", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ checkLoadedPlugins(t, r.ForEach, []string{"", "testPlugin"})
+}
+
+func TestRegistryDiscoversNewPlugins(t *testing.T) {
+ mockFS := setup(t)
+ defer restore(t)
+
+ root := "/plugins"
+ r, err := NewRegistry(root, "", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ checkLoadedPlugins(t, r.ForEach, []string{})
+
+ // Add the new plugin
+ plugin := mockPlugin{t: t, Name: "testPlugin", Requests: make(chan *http.Request), Handler: stringHandler(`{"name":"testPlugin","interfaces":["reporter"]}`)}
+ mockFS.Add(plugin.dir(), plugin.file())
+ if err := r.scan(); err != nil {
+ t.Fatal(err)
+ }
+
+ checkLoadedPlugins(t, r.ForEach, []string{"testPlugin"})
+}
+
+func TestRegistryRemovesPlugins(t *testing.T) {
+ plugin := mockPlugin{t: t, Name: "testPlugin", Requests: make(chan *http.Request), Handler: stringHandler(`{"name":"testPlugin","interfaces":["reporter"]}`)}
+ mockFS := setup(t, plugin.file())
+ defer restore(t)
+
+ root := "/plugins"
+ r, err := NewRegistry(root, "", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ checkLoadedPlugins(t, r.ForEach, []string{"testPlugin"})
+
+ // Remove the plugin
+ mockFS.Remove(plugin.path())
+ if err := r.scan(); err != nil {
+ t.Fatal(err)
+ }
+
+ checkLoadedPlugins(t, r.ForEach, []string{})
+}
+
+func TestRegistryReturnsPluginsByInterface(t *testing.T) {
+ setup(
+ t,
+ mockPlugin{
+ t: t,
+ Name: "plugin1",
+ Handler: stringHandler(`{"name":"plugin1","interfaces":["reporter"]}`),
+ }.file(),
+ mockPlugin{
+ t: t,
+ Name: "plugin2",
+ Handler: stringHandler(`{"name":"plugin2","interfaces":["other"]}`),
+ }.file(),
+ )
+ defer restore(t)
+
+ root := "/plugins"
+ r, err := NewRegistry(root, "", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ checkLoadedPlugins(t, r.ForEach, []string{"plugin1", "plugin2"})
+ checkLoadedPlugins(t, func(fn func(*Plugin)) { r.Implementers("reporter", fn) }, []string{"plugin1"})
+ checkLoadedPlugins(t, func(fn func(*Plugin)) { r.Implementers("other", fn) }, []string{"plugin2"})
+}
+
+func TestRegistryHandlesConflictingPlugins(t *testing.T) {
+ setup(
+ t,
+ mockPlugin{
+ t: t,
+ Name: "plugin1",
+ Handler: stringHandler(`{"name":"plugin1","interfaces":["reporter"]}`),
+ }.file(),
+ mockPlugin{
+ t: t,
+ Name: "plugin1",
+ Handler: stringHandler(`{"name":"plugin2","interfaces":["other"]}`),
+ }.file(),
+ )
+ defer restore(t)
+
+ root := "/plugins"
+ r, err := NewRegistry(root, "", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ // Should just have the second one (we just log conflicts)
+ checkLoadedPlugins(t, r.ForEach, []string{"plugin2"})
+ checkLoadedPlugins(t, func(fn func(*Plugin)) { r.Implementers("other", fn) }, []string{"plugin2"})
+}
diff --git a/probe/plugins/reporter.go b/probe/plugins/reporter.go
new file mode 100644
index 000000000..a85f16b42
--- /dev/null
+++ b/probe/plugins/reporter.go
@@ -0,0 +1,25 @@
+package plugins
+
+import (
+ log "github.com/Sirupsen/logrus"
+
+ "github.com/weaveworks/scope/probe"
+ "github.com/weaveworks/scope/report"
+)
+
+// Reporter implements the Reporter interface for a plugin registry.
+func Reporter(pluginRegistry *Registry) probe.Reporter {
+ return probe.ReporterFunc("plugins", func() (report.Report, error) {
+ rpt := report.MakeReport()
+ pluginRegistry.Implementers("reporter", func(plugin *Plugin) {
+ pluginReport, err := plugin.Report()
+ if err != nil {
+ log.Errorf("plugins: error getting report from %s: %v", plugin.ID, err)
+ return
+ }
+ pluginReport.Plugins = pluginReport.Plugins.Add(plugin.PluginSpec)
+ rpt = rpt.Merge(pluginReport)
+ })
+ return rpt, nil
+ })
+}
diff --git a/probe/plugins/unix_round_tripper.go b/probe/plugins/unix_round_tripper.go
new file mode 100644
index 000000000..0bbd49b51
--- /dev/null
+++ b/probe/plugins/unix_round_tripper.go
@@ -0,0 +1,27 @@
+package plugins
+
+import (
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "time"
+)
+
+type unixRoundTripper struct {
+ address string
+ timeout time.Duration
+}
+
+func makeUnixRoundTripper(address string, timeout time.Duration) (http.RoundTripper, error) {
+ return unixRoundTripper{address: address, timeout: timeout}, nil
+}
+
+func (t unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ conn, err := net.DialTimeout("unix", t.address, t.timeout)
+ if err != nil {
+ return nil, err
+ }
+ client := httputil.NewClientConn(conn, nil)
+ defer client.Close()
+ return client.Do(req)
+}
diff --git a/probe/probe.go b/probe/probe.go
index b8321c6dc..2f485be6c 100644
--- a/probe/probe.go
+++ b/probe/probe.go
@@ -43,6 +43,19 @@ type Reporter interface {
Report() (report.Report, error)
}
+// ReporterFunc uses a function to implement a Reporter
+func ReporterFunc(name string, f func() (report.Report, error)) Reporter {
+ return reporterFunc{name, f}
+}
+
+type reporterFunc struct {
+ name string
+ f func() (report.Report, error)
+}
+
+func (r reporterFunc) Name() string { return r.name }
+func (r reporterFunc) Report() (report.Report, error) { return r.f() }
+
// Ticker is something which will be invoked every spyDuration.
// It's useful for things that should be updated on that interval.
// For example, cached shared state between Taggers and Reporters.
diff --git a/probe/process/reporter.go b/probe/process/reporter.go
index e5721a28f..f86da068f 100644
--- a/probe/process/reporter.go
+++ b/probe/process/reporter.go
@@ -19,6 +19,22 @@ const (
OpenFilesCount = "open_files_count"
)
+// Exposed for testing
+var (
+ MetadataTemplates = report.MetadataTemplates{
+ PID: {ID: PID, Label: "PID", From: report.FromLatest, Datatype: "number", Priority: 1},
+ Cmdline: {ID: Cmdline, Label: "Command", From: report.FromLatest, Priority: 2},
+ PPID: {ID: PPID, Label: "Parent PID", From: report.FromLatest, Priority: 3},
+ Threads: {ID: Threads, Label: "# Threads", From: report.FromLatest, Priority: 4},
+ }
+
+ MetricTemplates = report.MetricTemplates{
+ CPUUsage: {ID: CPUUsage, Label: "CPU", Format: report.PercentFormat, Priority: 1},
+ MemoryUsage: {ID: MemoryUsage, Label: "Memory", Format: report.FilesizeFormat, Priority: 2},
+ OpenFilesCount: {ID: OpenFilesCount, Label: "Open Files", Format: report.IntegerFormat, Priority: 3},
+ }
+)
+
// Reporter generates Reports containing the Process topology.
type Reporter struct {
scope string
@@ -53,7 +69,9 @@ func (r *Reporter) Report() (report.Report, error) {
}
func (r *Reporter) processTopology() (report.Topology, error) {
- t := report.MakeTopology()
+ t := report.MakeTopology().
+ WithMetadataTemplates(MetadataTemplates).
+ WithMetricTemplates(MetricTemplates)
now := mtime.Now()
deltaTotal, maxCPU, err := r.jiffies()
if err != nil {
diff --git a/prog/main.go b/prog/main.go
index 36e48467c..e4b66c479 100644
--- a/prog/main.go
+++ b/prog/main.go
@@ -6,7 +6,8 @@ import (
"strings"
log "github.com/Sirupsen/logrus"
- weavecommon "github.com/weaveworks/weave/common"
+
+ "github.com/weaveworks/weave/common"
)
var version = "dev" // set at build time
@@ -31,7 +32,7 @@ func setLogFormatter(prefix string) {
f := prefixFormatter{
prefix: []byte(prefix),
// reuse weave's log format
- next: weavecommon.Log.Formatter,
+ next: common.Log.Formatter,
}
log.SetFormatter(&f)
}
diff --git a/prog/probe.go b/prog/probe.go
index 7ae166747..2d56fd04c 100644
--- a/prog/probe.go
+++ b/prog/probe.go
@@ -31,6 +31,7 @@ import (
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/overlay"
+ "github.com/weaveworks/scope/probe/plugins"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
@@ -39,6 +40,8 @@ const (
versionCheckPeriod = 6 * time.Hour
)
+var pluginAPIVersion = "1"
+
func check() {
handleResponse := func(r *checkpoint.CheckResponse, err error) {
if err != nil {
@@ -69,6 +72,7 @@ func probeMain() {
spyInterval = flag.Duration("spy.interval", time.Second, "spy (scan) interval")
spyProcs = flag.Bool("processes", true, "report processes (needs root)")
procRoot = flag.String("proc.root", "/proc", "location of the proc filesystem")
+ pluginsRoot = flag.String("plugins.root", "/var/run/scope/plugins", "Root directory to search for plugins")
useConntrack = flag.Bool("conntrack", true, "also use conntrack to track connections")
insecure = flag.Bool("insecure", false, "(SSL) explicitly allow \"insecure\" SSL connections and transfers")
logPrefix = flag.String("log.prefix", "", "prefix for each log line")
@@ -111,6 +115,19 @@ func probeMain() {
log.Infof("probe starting, version %s, ID %s", version, probeID)
go check()
+ pluginRegistry, err := plugins.NewRegistry(
+ *pluginsRoot,
+ pluginAPIVersion,
+ map[string]string{
+ "probe_id": probeID,
+ "api_version": pluginAPIVersion,
+ },
+ )
+ if err != nil {
+ log.Errorf("plugins: problem loading: %v", err)
+ }
+ defer pluginRegistry.Close()
+
if len(flag.Args()) > 0 {
targets = flag.Args()
}
@@ -189,6 +206,8 @@ func probeMain() {
}
}
+ p.AddReporter(plugins.Reporter(pluginRegistry))
+
if *httpListen != "" {
go func() {
log.Infof("Profiling data being exported to %s", *httpListen)
diff --git a/render/detailed/connections.go b/render/detailed/connections.go
index b7bc7d6c1..8100a4a70 100644
--- a/render/detailed/connections.go
+++ b/render/detailed/connections.go
@@ -65,7 +65,7 @@ func (row connection) ID() string {
return fmt.Sprintf("%s:%s-%s:%s-%s", row.remoteNode.ID, row.remoteAddr, row.localNode.ID, row.localAddr, row.port)
}
-func incomingConnectionsSummary(topologyID string, n report.Node, ns report.Nodes) ConnectionsSummary {
+func incomingConnectionsSummary(topologyID string, r report.Report, n report.Node, ns report.Nodes) ConnectionsSummary {
localEndpointIDs := endpointChildIDsOf(n)
// For each node which has an edge TO me
@@ -110,11 +110,11 @@ func incomingConnectionsSummary(topologyID string, n report.Node, ns report.Node
TopologyID: topologyID,
Label: "Inbound",
Columns: columnHeaders,
- Connections: connectionRows(counts, isInternetNode(n)),
+ Connections: connectionRows(r, counts, isInternetNode(n)),
}
}
-func outgoingConnectionsSummary(topologyID string, n report.Node, ns report.Nodes) ConnectionsSummary {
+func outgoingConnectionsSummary(topologyID string, r report.Report, n report.Node, ns report.Nodes) ConnectionsSummary {
localEndpoints := endpointChildrenOf(n)
// For each node which has an edge FROM me
@@ -160,7 +160,7 @@ func outgoingConnectionsSummary(topologyID string, n report.Node, ns report.Node
TopologyID: topologyID,
Label: "Outbound",
Columns: columnHeaders,
- Connections: connectionRows(counts, isInternetNode(n)),
+ Connections: connectionRows(r, counts, isInternetNode(n)),
}
}
@@ -188,13 +188,13 @@ func isInternetNode(n report.Node) bool {
return n.ID == render.IncomingInternetID || n.ID == render.OutgoingInternetID
}
-func connectionRows(in map[connection]int, includeLocal bool) []Connection {
+func connectionRows(r report.Report, in map[connection]int, includeLocal bool) []Connection {
output := []Connection{}
for row, count := range in {
// Use MakeNodeSummary to render the id and label of this node
// TODO(paulbellamy): Would be cleaner if we hade just a
// MakeNodeID(*row.remoteode). As we don't need the whole summary.
- summary, ok := MakeNodeSummary(*row.remoteNode)
+ summary, ok := MakeNodeSummary(r, *row.remoteNode)
connection := Connection{
ID: row.ID(),
NodeID: summary.ID,
@@ -207,19 +207,19 @@ func connectionRows(in map[connection]int, includeLocal bool) []Connection {
}
if includeLocal {
connection.Metadata = append(connection.Metadata,
- MetadataRow{
+ report.MetadataRow{
ID: "foo",
Value: row.localAddr,
Datatype: number,
})
}
connection.Metadata = append(connection.Metadata,
- MetadataRow{
+ report.MetadataRow{
ID: portKey,
Value: row.port,
Datatype: number,
},
- MetadataRow{
+ report.MetadataRow{
ID: countKey,
Value: strconv.Itoa(count),
Datatype: number,
diff --git a/render/detailed/docker_labels.go b/render/detailed/docker_labels.go
index 0e086e1c8..a1cb46056 100644
--- a/render/detailed/docker_labels.go
+++ b/render/detailed/docker_labels.go
@@ -10,7 +10,7 @@ import (
// NodeDockerLabels produces a table (to be consumed directly by the UI) based
// on an origin ID, which is (optimistically) a node ID in one of our
// topologies.
-func NodeDockerLabels(nmd report.Node) []MetadataRow {
+func NodeDockerLabels(nmd report.Node) []report.MetadataRow {
if _, ok := nmd.Counters.Lookup(nmd.Topology); ok {
// This is a group of nodes, so no docker labels!
return nil
@@ -20,7 +20,7 @@ func NodeDockerLabels(nmd report.Node) []MetadataRow {
return nil
}
- var rows []MetadataRow
+ var rows []report.MetadataRow
// Add labels in alphabetical order
labels := docker.ExtractLabels(nmd)
labelKeys := make([]string, 0, len(labels))
@@ -29,7 +29,7 @@ func NodeDockerLabels(nmd report.Node) []MetadataRow {
}
sort.Strings(labelKeys)
for _, labelKey := range labelKeys {
- rows = append(rows, MetadataRow{ID: "label_" + labelKey, Value: labels[labelKey]})
+ rows = append(rows, report.MetadataRow{ID: "label_" + labelKey, Value: labels[labelKey]})
}
return rows
}
diff --git a/render/detailed/docker_labels_test.go b/render/detailed/docker_labels_test.go
index 0eefa342b..2d9d99170 100644
--- a/render/detailed/docker_labels_test.go
+++ b/render/detailed/docker_labels_test.go
@@ -15,7 +15,7 @@ func TestNodeDockerLabels(t *testing.T) {
inputs := []struct {
name string
node report.Node
- want []detailed.MetadataRow
+ want []report.MetadataRow
}{
{
name: "container",
@@ -26,7 +26,7 @@ func TestNodeDockerLabels(t *testing.T) {
}).WithTopology(report.Container).WithSets(report.EmptySets.
Add(docker.ContainerIPs, report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24")),
),
- want: []detailed.MetadataRow{
+ want: []report.MetadataRow{
{
ID: "label_label1",
Value: "label1value",
diff --git a/render/detailed/metadata.go b/render/detailed/metadata.go
index f032f02c3..c6a3d71a0 100644
--- a/render/detailed/metadata.go
+++ b/render/detailed/metadata.go
@@ -1,190 +1,19 @@
package detailed
import (
- "strconv"
- "strings"
-
- "github.com/ugorji/go/codec"
-
- "github.com/weaveworks/scope/probe/docker"
- "github.com/weaveworks/scope/probe/host"
- "github.com/weaveworks/scope/probe/kubernetes"
- "github.com/weaveworks/scope/probe/overlay"
- "github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
-var (
- processNodeMetadata = []MetadataRowTemplate{
- Latest{ID: process.PID, Prime: true, Datatype: number},
- Latest{ID: process.Cmdline, Prime: true},
- Latest{ID: process.PPID, Prime: true},
- Latest{ID: process.Threads, Prime: true},
- }
- containerNodeMetadata = []MetadataRowTemplate{
- Latest{ID: docker.ContainerID, Truncate: 12, Prime: true},
- Latest{ID: docker.ContainerStateHuman, Prime: true},
- Latest{ID: docker.ContainerCommand, Prime: true},
- Latest{ID: docker.ImageID, Truncate: 12},
- Latest{ID: docker.ContainerUptime},
- Latest{ID: docker.ContainerRestartCount},
- Set{ID: docker.ContainerIPs},
- Set{ID: docker.ContainerPorts},
- Latest{ID: docker.ContainerCreated},
- Latest{ID: overlay.WeaveMACAddress},
- Latest{ID: overlay.WeaveDNSHostname},
- }
- containerImageNodeMetadata = []MetadataRowTemplate{
- Latest{ID: docker.ImageID, Truncate: 12, Prime: true},
- Counter{ID: report.Container, Prime: true},
- }
- podNodeMetadata = []MetadataRowTemplate{
- Latest{ID: kubernetes.PodID, Prime: true},
- Latest{ID: kubernetes.Namespace, Prime: true},
- Latest{ID: kubernetes.PodCreated, Prime: true},
- }
- hostNodeMetadata = []MetadataRowTemplate{
- Latest{ID: host.KernelVersion, Prime: true},
- Latest{ID: host.Uptime, Prime: true},
- Latest{ID: host.HostName},
- Latest{ID: host.OS},
- Set{ID: host.LocalNetworks},
- }
-)
-
-// MetadataRowTemplate extracts some metadata rows from a node
-type MetadataRowTemplate interface {
- MetadataRows(report.Node) []MetadataRow
-}
-
-// Latest extracts some metadata rows from a node's Latest
-type Latest struct {
- ID string
- Truncate int // If > 0, truncate the value to this length.
- Prime bool // Whether the row should be shown by default
- Datatype string
-}
-
-// MetadataRows implements MetadataRowTemplate
-func (l Latest) MetadataRows(n report.Node) []MetadataRow {
- if val, ok := n.Latest.Lookup(l.ID); ok {
- if l.Truncate > 0 && len(val) > l.Truncate {
- val = val[:l.Truncate]
- }
- return []MetadataRow{{ID: l.ID, Value: val, Prime: l.Prime, Datatype: l.Datatype}}
- }
- return nil
-}
-
-// Set extracts some metadata rows from a node's Sets
-type Set struct {
- ID string
-}
-
-// MetadataRows implements MetadataRowTemplate
-func (s Set) MetadataRows(n report.Node) []MetadataRow {
- if val, ok := n.Sets.Lookup(s.ID); ok && len(val) > 0 {
- return []MetadataRow{{ID: s.ID, Value: strings.Join(val, ", ")}}
- }
- return nil
-}
-
-// Counter extracts some metadata rows from a node's Counters
-type Counter struct {
- ID string
- Prime bool
-}
-
-// MetadataRows implements MetadataRowTemplate
-func (c Counter) MetadataRows(n report.Node) []MetadataRow {
- if val, ok := n.Counters.Lookup(c.ID); ok {
- return []MetadataRow{{
- ID: c.ID,
- Value: strconv.Itoa(val),
- Prime: c.Prime,
- Datatype: number,
- }}
- }
- return nil
-}
-
-// MetadataRow is a row for the metadata table.
-type MetadataRow struct {
- ID string
- Value string
- Prime bool
- Datatype string
-}
-
-// Copy returns a value copy of a metadata row.
-func (m MetadataRow) Copy() MetadataRow {
- return m
-}
-
-// MarshalJSON shouldn't be used, use CodecEncodeSelf instead
-func (MetadataRow) MarshalJSON() ([]byte, error) {
- panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
-}
-
-// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead
-func (*MetadataRow) UnmarshalJSON(b []byte) error {
- panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
-}
-
-type labelledMetadataRow struct {
- ID string `json:"id"`
- Label string `json:"label"`
- Value string `json:"value"`
- Prime bool `json:"prime,omitempty"`
- Datatype string `json:"dataType,omitempty"`
-}
-
-// CodecEncodeSelf marshals this MetadataRow. It adds a label before
-// rendering.
-func (m *MetadataRow) CodecEncodeSelf(encoder *codec.Encoder) {
- in := labelledMetadataRow{
- ID: m.ID,
- Label: Label(m.ID),
- Value: m.Value,
- Prime: m.Prime,
- Datatype: m.Datatype,
- }
- encoder.Encode(in)
-}
-
-// CodecDecodeSelf implements codec.Selfer
-func (m *MetadataRow) CodecDecodeSelf(decoder *codec.Decoder) {
- var in labelledMetadataRow
- decoder.Decode(&in)
- *m = MetadataRow{
- ID: in.ID,
- Value: in.Value,
- Prime: in.Prime,
- Datatype: in.Datatype,
- }
-}
-
// NodeMetadata produces a table (to be consumed directly by the UI) based on
-// an origin ID, which is (optimistically) a node ID in one of our topologies.
-func NodeMetadata(n report.Node) []MetadataRow {
+// an a report.Node, which is (hopefully) a node in one of our topologies.
+func NodeMetadata(r report.Report, n report.Node) []report.MetadataRow {
if _, ok := n.Counters.Lookup(n.Topology); ok {
// This is a group of nodes, so no metadata!
return nil
}
- renderers := map[string][]MetadataRowTemplate{
- report.Process: processNodeMetadata,
- report.Container: containerNodeMetadata,
- report.ContainerImage: containerImageNodeMetadata,
- report.Pod: podNodeMetadata,
- report.Host: hostNodeMetadata,
- }
- if templates, ok := renderers[n.Topology]; ok {
- rows := []MetadataRow{}
- for _, template := range templates {
- rows = append(rows, template.MetadataRows(n)...)
- }
- return rows
+ if topology, ok := r.Topology(n.Topology); ok {
+ return topology.MetadataTemplates.MetadataRows(n)
}
return nil
}
diff --git a/render/detailed/metadata_test.go b/render/detailed/metadata_test.go
index 66511f269..d70a746e2 100644
--- a/render/detailed/metadata_test.go
+++ b/render/detailed/metadata_test.go
@@ -15,7 +15,7 @@ func TestNodeMetadata(t *testing.T) {
inputs := []struct {
name string
node report.Node
- want []detailed.MetadataRow
+ want []report.MetadataRow
}{
{
name: "container",
@@ -26,10 +26,10 @@ func TestNodeMetadata(t *testing.T) {
}).WithTopology(report.Container).WithSets(report.EmptySets.
Add(docker.ContainerIPs, report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24")),
),
- want: []detailed.MetadataRow{
- {ID: docker.ContainerID, Value: fixture.ClientContainerID, Prime: true},
- {ID: docker.ContainerStateHuman, Value: "running", Prime: true},
- {ID: docker.ContainerIPs, Value: "10.10.10.0/24, 10.10.10.1/24"},
+ want: []report.MetadataRow{
+ {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 1},
+ {ID: docker.ContainerStateHuman, Label: "State", Value: "running", Priority: 2},
+ {ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24", Priority: 14},
},
},
{
@@ -41,7 +41,7 @@ func TestNodeMetadata(t *testing.T) {
},
}
for _, input := range inputs {
- have := detailed.NodeMetadata(input.node)
+ have := detailed.NodeMetadata(fixture.Report, input.node)
if !reflect.DeepEqual(input.want, have) {
t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
}
@@ -50,10 +50,10 @@ func TestNodeMetadata(t *testing.T) {
func TestMetadataRowCopy(t *testing.T) {
var (
- row = detailed.MetadataRow{
+ row = report.MetadataRow{
ID: "id",
Value: "value",
- Prime: true,
+ Priority: 1,
Datatype: "datatype",
}
cp = row.Copy()
@@ -67,9 +67,9 @@ func TestMetadataRowCopy(t *testing.T) {
// changing the copy should not change the original
cp.ID = ""
cp.Value = ""
- cp.Prime = false
+ cp.Priority = 2
cp.Datatype = ""
- if row.ID != "id" || row.Value != "value" || row.Prime != true || row.Datatype != "datatype" {
+ if row.ID != "id" || row.Value != "value" || row.Priority != 1 || row.Datatype != "datatype" {
t.Errorf("Expected changing the copy not to modify the original")
}
}
diff --git a/render/detailed/metrics.go b/render/detailed/metrics.go
index b44b7f3ab..ad5935e59 100644
--- a/render/detailed/metrics.go
+++ b/render/detailed/metrics.go
@@ -1,177 +1,20 @@
package detailed
import (
- "math"
-
- "github.com/ugorji/go/codec"
-
- "github.com/weaveworks/scope/probe/docker"
- "github.com/weaveworks/scope/probe/host"
- "github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
-const (
- defaultFormat = ""
- filesizeFormat = "filesize"
- integerFormat = "integer"
- percentFormat = "percent"
-)
-
-var (
- processNodeMetrics = []MetricRow{
- {ID: process.CPUUsage, Format: percentFormat},
- {ID: process.MemoryUsage, Format: filesizeFormat},
- {ID: process.OpenFilesCount, Format: integerFormat},
- }
- containerNodeMetrics = []MetricRow{
- {ID: docker.CPUTotalUsage, Format: percentFormat},
- {ID: docker.MemoryUsage, Format: filesizeFormat},
- }
- hostNodeMetrics = []MetricRow{
- {ID: host.CPUUsage, Format: percentFormat},
- {ID: host.MemoryUsage, Format: filesizeFormat},
- {ID: host.Load1, Format: defaultFormat, Group: "load"},
- }
-)
-
-// MetricRow is a tuple of data used to render a metric as a sparkline and
-// accoutrements.
-type MetricRow struct {
- ID string
- Format string
- Group string
- Value float64
- Metric *report.Metric
-}
-
-// Summary returns a copy of the MetricRow, without the samples, just the value if there is one.
-func (m MetricRow) Summary() MetricRow {
- row := MetricRow{
- ID: m.ID,
- Format: m.Format,
- Group: m.Group,
- Value: m.Value,
- }
- if m.Metric != nil {
- var metric = m.Metric.Copy()
- metric.Samples = nil
- row.Metric = &metric
- }
- return row
-}
-
-// Copy returns a value copy of the MetricRow
-func (m MetricRow) Copy() MetricRow {
- row := MetricRow{
- ID: m.ID,
- Format: m.Format,
- Group: m.Group,
- Value: m.Value,
- }
- if m.Metric != nil {
- var metric = m.Metric.Copy()
- row.Metric = &metric
- }
- return row
-}
-
-// MarshalJSON shouldn't be used, use CodecEncodeSelf instead
-func (MetricRow) MarshalJSON() ([]byte, error) {
- panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
-}
-
-// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead
-func (*MetricRow) UnmarshalJSON(b []byte) error {
- panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
-}
-
-type wiredMetricRow struct {
- ID string `json:"id"`
- Label string `json:"label"`
- Format string `json:"format,omitempty"`
- Group string `json:"group,omitempty"`
- Value float64 `json:"value"`
- Samples []report.Sample `json:"samples"`
- Min float64 `json:"min"`
- Max float64 `json:"max"`
- First string `json:"first,omitempty"`
- Last string `json:"last,omitempty"`
-}
-
-// CodecEncodeSelf marshals this MetricRow. It takes the basic Metric
-// rendering, then adds some row-specific fields.
-func (m *MetricRow) CodecEncodeSelf(encoder *codec.Encoder) {
- in := m.Metric.ToIntermediate()
- encoder.Encode(wiredMetricRow{
- ID: m.ID,
- Label: Label(m.ID),
- Format: m.Format,
- Group: m.Group,
- Value: m.Value,
- Samples: in.Samples,
- Min: in.Min,
- Max: in.Max,
- First: in.First,
- Last: in.Last,
- })
-}
-
-// CodecDecodeSelf implements codec.Selfer
-func (m *MetricRow) CodecDecodeSelf(decoder *codec.Decoder) {
- var in wiredMetricRow
- decoder.Decode(&in)
- w := report.WireMetrics{
- Samples: in.Samples,
- Min: in.Min,
- Max: in.Max,
- First: in.First,
- Last: in.Last,
- }
- metric := w.FromIntermediate()
- *m = MetricRow{
- ID: in.ID,
- Format: in.Format,
- Group: in.Group,
- Value: in.Value,
- Metric: &metric,
- }
-}
-
// NodeMetrics produces a table (to be consumed directly by the UI) based on
// an a report.Node, which is (hopefully) a node in one of our topologies.
-func NodeMetrics(n report.Node) []MetricRow {
+func NodeMetrics(r report.Report, n report.Node) []report.MetricRow {
if _, ok := n.Counters.Lookup(n.Topology); ok {
// This is a group of nodes, so no metrics!
return nil
}
- renderers := map[string][]MetricRow{
- report.Process: processNodeMetrics,
- report.Container: containerNodeMetrics,
- report.Host: hostNodeMetrics,
+ topology, ok := r.Topology(n.Topology)
+ if !ok {
+ return nil
}
- if templates, ok := renderers[n.Topology]; ok {
- rows := []MetricRow{}
- for _, template := range templates {
- metric, ok := n.Metrics[template.ID]
- if !ok {
- continue
- }
- t := template.Copy()
- if s := metric.LastSample(); s != nil {
- t.Value = toFixed(s.Value, 2)
- }
- t.Metric = &metric
- rows = append(rows, t)
- }
- return rows
- }
- return nil
-}
-
-// toFixed truncates decimals of float64 down to specified precision
-func toFixed(num float64, precision int) float64 {
- output := math.Pow(10, float64(precision))
- return float64(int64(num*output)) / output
+ return topology.MetricTemplates.MetricRows(n)
}
diff --git a/render/detailed/metrics_test.go b/render/detailed/metrics_test.go
index 94160af61..75b00a88a 100644
--- a/render/detailed/metrics_test.go
+++ b/render/detailed/metrics_test.go
@@ -18,71 +18,85 @@ func TestNodeMetrics(t *testing.T) {
inputs := []struct {
name string
node report.Node
- want []detailed.MetricRow
+ want []report.MetricRow
}{
{
name: "process",
node: fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
- want: []detailed.MetricRow{
+ want: []report.MetricRow{
{
- ID: process.CPUUsage,
- Format: "percent",
- Group: "",
- Value: 0.01,
- Metric: &fixture.ClientProcess1CPUMetric,
+ ID: process.CPUUsage,
+ Label: "CPU",
+ Format: "percent",
+ Group: "",
+ Value: 0.01,
+ Priority: 1,
+ Metric: &fixture.ClientProcess1CPUMetric,
},
{
- ID: process.MemoryUsage,
- Format: "filesize",
- Group: "",
- Value: 0.02,
- Metric: &fixture.ClientProcess1MemoryMetric,
+ ID: process.MemoryUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Group: "",
+ Value: 0.02,
+ Priority: 2,
+ Metric: &fixture.ClientProcess1MemoryMetric,
},
},
},
{
name: "container",
node: fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
- want: []detailed.MetricRow{
+ want: []report.MetricRow{
{
- ID: docker.CPUTotalUsage,
- Format: "percent",
- Group: "",
- Value: 0.03,
- Metric: &fixture.ClientContainerCPUMetric,
+ ID: docker.CPUTotalUsage,
+ Label: "CPU",
+ Format: "percent",
+ Group: "",
+ Value: 0.03,
+ Priority: 1,
+ Metric: &fixture.ClientContainerCPUMetric,
},
{
- ID: docker.MemoryUsage,
- Format: "filesize",
- Group: "",
- Value: 0.04,
- Metric: &fixture.ClientContainerMemoryMetric,
+ ID: docker.MemoryUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Group: "",
+ Value: 0.04,
+ Priority: 2,
+ Metric: &fixture.ClientContainerMemoryMetric,
},
},
},
{
name: "host",
node: fixture.Report.Host.Nodes[fixture.ClientHostNodeID],
- want: []detailed.MetricRow{
+ want: []report.MetricRow{
{
- ID: host.CPUUsage,
- Format: "percent",
- Group: "",
- Value: 0.07,
- Metric: &fixture.ClientHostCPUMetric,
+ ID: host.CPUUsage,
+ Label: "CPU",
+ Format: "percent",
+ Group: "",
+ Value: 0.07,
+ Priority: 1,
+ Metric: &fixture.ClientHostCPUMetric,
},
{
- ID: host.MemoryUsage,
- Format: "filesize",
- Group: "",
- Value: 0.08,
- Metric: &fixture.ClientHostMemoryMetric,
+ ID: host.MemoryUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Group: "",
+ Value: 0.08,
+ Priority: 2,
+ Metric: &fixture.ClientHostMemoryMetric,
},
{
- ID: host.Load1,
- Group: "load",
- Value: 0.09,
- Metric: &fixture.ClientHostLoad1Metric,
+ ID: host.Load1,
+ Label: "Load (1m)",
+ Group: "load",
+ Value: 0.09,
+ Priority: 11,
+ Metric: &fixture.ClientHostLoad1Metric,
},
},
},
@@ -93,7 +107,7 @@ func TestNodeMetrics(t *testing.T) {
},
}
for _, input := range inputs {
- have := detailed.NodeMetrics(input.node)
+ have := detailed.NodeMetrics(fixture.Report, input.node)
if !reflect.DeepEqual(input.want, have) {
t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
}
@@ -104,19 +118,16 @@ func TestMetricRowSummary(t *testing.T) {
var (
now = time.Now()
metric = report.MakeMetric().Add(now, 1.234)
- row = detailed.MetricRow{
- ID: "id",
- Format: "format",
- Group: "group",
- Value: 1.234,
- Metric: &metric,
+ row = report.MetricRow{
+ ID: "id",
+ Format: "format",
+ Group: "group",
+ Value: 1.234,
+ Priority: 1,
+ Metric: &metric,
}
summary = row.Summary()
)
- // summary should have all the same fields
- if row.ID != summary.ID || row.Format != summary.Format || row.Group != summary.Group || row.Value != summary.Value {
- t.Errorf("Expected summary to have same fields as original: %#v, but had %#v", row, summary)
- }
// summary should not have any samples
if summary.Metric.Len() != 0 {
t.Errorf("Expected summary to have no samples, but had %d", summary.Metric.Len())
@@ -125,4 +136,10 @@ func TestMetricRowSummary(t *testing.T) {
if metric.Len() != 1 {
t.Errorf("Expected original metric to still have it's samples, but had %d", metric.Len())
}
+ // summary should have all the same fields (minus the metric)
+ summary.Metric = nil
+ row.Metric = nil
+ if !reflect.DeepEqual(summary, row) {
+ t.Errorf("Expected summary to have same fields as original: %s", test.Diff(summary, row))
+ }
}
diff --git a/render/detailed/node.go b/render/detailed/node.go
index 19f8f3249..f93674368 100644
--- a/render/detailed/node.go
+++ b/render/detailed/node.go
@@ -47,7 +47,7 @@ type wiredControlInstance struct {
Icon string `json:"icon"`
}
-// CodecEncodeSelf marshals this MetricRow. It takes the basic Metric
+// CodecEncodeSelf marshals this ControlInstance. It takes the basic Metric
// rendering, then adds some row-specific fields.
func (c *ControlInstance) CodecEncodeSelf(encoder *codec.Encoder) {
encoder.Encode(wiredControlInstance{
@@ -77,15 +77,15 @@ func (c *ControlInstance) CodecDecodeSelf(decoder *codec.Decoder) {
// MakeNode transforms a renderable node to a detailed node. It uses
// aggregate metadata, plus the set of origin node IDs, to produce tables.
func MakeNode(topologyID string, r report.Report, ns report.Nodes, n report.Node) Node {
- summary, _ := MakeNodeSummary(n)
+ summary, _ := MakeNodeSummary(r, n)
return Node{
NodeSummary: summary,
Controls: controls(r, n),
- Children: children(n),
+ Children: children(r, n),
Parents: Parents(r, n),
Connections: []ConnectionsSummary{
- incomingConnectionsSummary(topologyID, n, ns),
- outgoingConnectionsSummary(topologyID, n, ns),
+ incomingConnectionsSummary(topologyID, r, n, ns),
+ outgoingConnectionsSummary(topologyID, r, n, ns),
},
}
}
@@ -114,7 +114,6 @@ func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
}
func controls(r report.Report, n report.Node) []ControlInstance {
- // TODO(paulbellamy): this ID will have been munged in rendering, so we should stop doing that, so that this matches up.
if t, ok := r.Topology(n.Topology); ok {
return controlsFor(t, n.ID)
}
@@ -178,13 +177,13 @@ var (
}
)
-func children(n report.Node) []NodeSummaryGroup {
+func children(r report.Report, n report.Node) []NodeSummaryGroup {
summaries := map[string][]NodeSummary{}
n.Children.ForEach(func(child report.Node) {
if child.ID == n.ID {
return
}
- summary, ok := MakeNodeSummary(child)
+ summary, ok := MakeNodeSummary(r, child)
if !ok {
return
}
diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go
index dd782b8de..e75552c8b 100644
--- a/render/detailed/node_test.go
+++ b/render/detailed/node_test.go
@@ -16,7 +16,7 @@ import (
)
func child(t *testing.T, r render.Renderer, id string) detailed.NodeSummary {
- s, ok := detailed.MakeNodeSummary(r.Render(fixture.Report)[id])
+ s, ok := detailed.MakeNodeSummary(fixture.Report, r.Render(fixture.Report)[id])
if !ok {
t.Fatalf("Expected node %s to be summarizable, but wasn't", id)
}
@@ -44,38 +44,50 @@ func TestMakeDetailedHostNode(t *testing.T) {
Shape: "circle",
Linkable: true,
Adjacency: report.MakeIDList(fixture.ServerHostNodeID),
- Metadata: []detailed.MetadataRow{
+ Metadata: []report.MetadataRow{
{
- ID: "host_name",
- Value: "client.hostname.com",
+ ID: "host_name",
+ Label: "Hostname",
+ Value: "client.hostname.com",
+ Priority: 11,
},
{
- ID: "os",
- Value: "Linux",
+ ID: "os",
+ Label: "OS",
+ Value: "Linux",
+ Priority: 12,
},
{
- ID: "local_networks",
- Value: "10.10.10.0/24",
+ ID: "local_networks",
+ Label: "Local Networks",
+ Value: "10.10.10.0/24",
+ Priority: 13,
},
},
- Metrics: []detailed.MetricRow{
+ Metrics: []report.MetricRow{
{
- ID: host.CPUUsage,
- Format: "percent",
- Value: 0.07,
- Metric: &fixture.ClientHostCPUMetric,
+ ID: host.CPUUsage,
+ Label: "CPU",
+ Format: "percent",
+ Value: 0.07,
+ Priority: 1,
+ Metric: &fixture.ClientHostCPUMetric,
},
{
- ID: host.MemoryUsage,
- Format: "filesize",
- Value: 0.08,
- Metric: &fixture.ClientHostMemoryMetric,
+ ID: host.MemoryUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Value: 0.08,
+ Priority: 2,
+ Metric: &fixture.ClientHostMemoryMetric,
},
{
- ID: host.Load1,
- Group: "load",
- Value: 0.09,
- Metric: &fixture.ClientHostLoad1Metric,
+ ID: host.Load1,
+ Label: "Load (1m)",
+ Group: "load",
+ Value: 0.09,
+ Priority: 11,
+ Metric: &fixture.ClientHostLoad1Metric,
},
},
},
@@ -121,7 +133,7 @@ func TestMakeDetailedHostNode(t *testing.T) {
NodeID: fixture.ServerHostNodeID,
Label: "server",
Linkable: true,
- Metadata: []detailed.MetadataRow{
+ Metadata: []report.MetadataRow{
{
ID: "port",
Value: "80",
@@ -162,29 +174,33 @@ func TestMakeDetailedContainerNode(t *testing.T) {
Shape: "hexagon",
Linkable: true,
Pseudo: false,
- Metadata: []detailed.MetadataRow{
- {ID: "docker_container_id", Value: fixture.ServerContainerID, Prime: true},
- {ID: "docker_container_state_human", Value: "running", Prime: true},
- {ID: "docker_image_id", Value: fixture.ServerContainerImageID},
+ Metadata: []report.MetadataRow{
+ {ID: "docker_container_id", Label: "ID", Value: fixture.ServerContainerID, Priority: 1},
+ {ID: "docker_container_state_human", Label: "State", Value: "running", Priority: 2},
+ {ID: "docker_image_id", Label: "Image ID", Value: fixture.ServerContainerImageID, Priority: 11},
},
- DockerLabels: []detailed.MetadataRow{
+ DockerLabels: []report.MetadataRow{
{ID: "label_" + detailed.AmazonECSContainerNameLabel, Value: `server`},
{ID: "label_foo1", Value: `bar1`},
{ID: "label_foo2", Value: `bar2`},
{ID: "label_io.kubernetes.pod.name", Value: "ping/pong-b"},
},
- Metrics: []detailed.MetricRow{
+ Metrics: []report.MetricRow{
{
- ID: docker.CPUTotalUsage,
- Format: "percent",
- Value: 0.05,
- Metric: &fixture.ServerContainerCPUMetric,
+ ID: docker.CPUTotalUsage,
+ Label: "CPU",
+ Format: "percent",
+ Value: 0.05,
+ Priority: 1,
+ Metric: &fixture.ServerContainerCPUMetric,
},
{
- ID: docker.MemoryUsage,
- Format: "filesize",
- Value: 0.06,
- Metric: &fixture.ServerContainerMemoryMetric,
+ ID: docker.MemoryUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Value: 0.06,
+ Priority: 2,
+ Metric: &fixture.ServerContainerMemoryMetric,
},
},
},
@@ -221,7 +237,7 @@ func TestMakeDetailedContainerNode(t *testing.T) {
NodeID: fixture.ClientContainerNodeID,
Label: "client",
Linkable: true,
- Metadata: []detailed.MetadataRow{
+ Metadata: []report.MetadataRow{
{
ID: "port",
Value: "80",
@@ -239,7 +255,7 @@ func TestMakeDetailedContainerNode(t *testing.T) {
NodeID: render.IncomingInternetID,
Label: render.InboundMajor,
Linkable: true,
- Metadata: []detailed.MetadataRow{
+ Metadata: []report.MetadataRow{
{
ID: "port",
Value: "80",
diff --git a/render/detailed/summary.go b/render/detailed/summary.go
index a61506dbb..d6385d7af 100644
--- a/render/detailed/summary.go
+++ b/render/detailed/summary.go
@@ -68,22 +68,22 @@ func MakeColumn(id string) Column {
// NodeSummary is summary information about a child for a Node.
type NodeSummary struct {
- ID string `json:"id"`
- Label string `json:"label"`
- LabelMinor string `json:"label_minor"`
- Rank string `json:"rank"`
- Shape string `json:"shape,omitempty"`
- Stack bool `json:"stack,omitempty"`
- Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to
- Pseudo bool `json:"pseudo,omitempty"`
- Metadata []MetadataRow `json:"metadata,omitempty"`
- DockerLabels []MetadataRow `json:"docker_labels,omitempty"`
- Metrics []MetricRow `json:"metrics,omitempty"`
- Adjacency report.IDList `json:"adjacency,omitempty"`
+ ID string `json:"id"`
+ Label string `json:"label"`
+ LabelMinor string `json:"label_minor"`
+ Rank string `json:"rank"`
+ Shape string `json:"shape,omitempty"`
+ Stack bool `json:"stack,omitempty"`
+ Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to
+ Pseudo bool `json:"pseudo,omitempty"`
+ Metadata []report.MetadataRow `json:"metadata,omitempty"`
+ DockerLabels []report.MetadataRow `json:"docker_labels,omitempty"`
+ Metrics []report.MetricRow `json:"metrics,omitempty"`
+ Adjacency report.IDList `json:"adjacency,omitempty"`
}
// MakeNodeSummary summarizes a node, if possible.
-func MakeNodeSummary(n report.Node) (NodeSummary, bool) {
+func MakeNodeSummary(r report.Report, n report.Node) (NodeSummary, bool) {
renderers := map[string]func(NodeSummary, report.Node) (NodeSummary, bool){
render.Pseudo: pseudoNodeSummary,
report.Process: processNodeSummary,
@@ -94,7 +94,7 @@ func MakeNodeSummary(n report.Node) (NodeSummary, bool) {
report.Host: hostNodeSummary,
}
if renderer, ok := renderers[n.Topology]; ok {
- return renderer(baseNodeSummary(n), n)
+ return renderer(baseNodeSummary(r, n), n)
}
return NodeSummary{}, false
}
@@ -133,14 +133,14 @@ func (n NodeSummary) Copy() NodeSummary {
return result
}
-func baseNodeSummary(n report.Node) NodeSummary {
+func baseNodeSummary(r report.Report, n report.Node) NodeSummary {
return NodeSummary{
ID: n.ID,
Shape: Circle,
Linkable: true,
- Metadata: NodeMetadata(n),
+ Metadata: NodeMetadata(r, n),
DockerLabels: NodeDockerLabels(n),
- Metrics: NodeMetrics(n),
+ Metrics: NodeMetrics(r, n),
Adjacency: n.Adjacency.Copy(),
}
}
@@ -341,10 +341,10 @@ func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }
type NodeSummaries map[string]NodeSummary
// Summaries converts RenderableNodes into a set of NodeSummaries
-func Summaries(rns report.Nodes) NodeSummaries {
+func Summaries(r report.Report, rns report.Nodes) NodeSummaries {
result := NodeSummaries{}
for id, node := range rns {
- if summary, ok := MakeNodeSummary(node); ok {
+ if summary, ok := MakeNodeSummary(r, node); ok {
for i, m := range summary.Metrics {
summary.Metrics[i] = m.Summary()
}
diff --git a/render/detailed/summary_test.go b/render/detailed/summary_test.go
index db1c362a4..329157b1a 100644
--- a/render/detailed/summary_test.go
+++ b/render/detailed/summary_test.go
@@ -21,7 +21,7 @@ import (
func TestSummaries(t *testing.T) {
{
// Just a convenient source of some rendered nodes
- have := detailed.Summaries(render.ProcessRenderer.Render(fixture.Report))
+ have := detailed.Summaries(fixture.Report, render.ProcessRenderer.Render(fixture.Report))
// The ids of the processes rendered above
expectedIDs := []string{
fixture.ClientProcess1NodeID,
@@ -53,14 +53,14 @@ func TestSummaries(t *testing.T) {
input := fixture.Report.Copy()
input.Process.Nodes[fixture.ClientProcess1NodeID] = input.Process.Nodes[fixture.ClientProcess1NodeID].WithMetrics(report.Metrics{process.CPUUsage: metric})
- have := detailed.Summaries(render.ProcessRenderer.Render(input))
+ have := detailed.Summaries(input, render.ProcessRenderer.Render(input))
node, ok := have[fixture.ClientProcess1NodeID]
if !ok {
t.Fatalf("Expected output to have the node we added the metric to")
}
- var row detailed.MetricRow
+ var row report.MetricRow
ok = false
for _, metric := range node.Metrics {
if metric.ID == process.CPUUsage {
@@ -74,10 +74,12 @@ func TestSummaries(t *testing.T) {
}
// Our summarized MetricRow
- want := detailed.MetricRow{
- ID: process.CPUUsage,
- Format: "percent",
- Value: 2,
+ want := report.MetricRow{
+ ID: process.CPUUsage,
+ Label: "CPU",
+ Format: "percent",
+ Value: 2,
+ Priority: 1,
Metric: &report.Metric{
Samples: nil,
Min: metric.Min,
@@ -109,10 +111,9 @@ func TestMakeNodeSummary(t *testing.T) {
LabelMinor: "client.hostname.com (10001)",
Rank: fixture.Client1Name,
Shape: "square",
- Metadata: []detailed.MetadataRow{
- {ID: process.PID, Value: fixture.Client1PID, Prime: true, Datatype: "number"},
+ Metadata: []report.MetadataRow{
+ {ID: process.PID, Label: "PID", Value: fixture.Client1PID, Priority: 1, Datatype: "number"},
},
- Metrics: []detailed.MetricRow{},
Adjacency: report.MakeIDList(fixture.ServerProcessNodeID),
},
},
@@ -127,10 +128,9 @@ func TestMakeNodeSummary(t *testing.T) {
Rank: fixture.ClientContainerImageName,
Shape: "hexagon",
Linkable: true,
- Metadata: []detailed.MetadataRow{
- {ID: docker.ContainerID, Value: fixture.ClientContainerID, Prime: true},
+ Metadata: []report.MetadataRow{
+ {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 1},
},
- Metrics: []detailed.MetricRow{},
Adjacency: report.MakeIDList(fixture.ServerContainerNodeID),
},
},
@@ -146,9 +146,9 @@ func TestMakeNodeSummary(t *testing.T) {
Shape: "hexagon",
Linkable: true,
Stack: true,
- Metadata: []detailed.MetadataRow{
- {ID: docker.ImageID, Value: fixture.ClientContainerImageID, Prime: true},
- {ID: report.Container, Value: "1", Prime: true, Datatype: "number"},
+ Metadata: []report.MetadataRow{
+ {ID: docker.ImageID, Label: "Image ID", Value: fixture.ClientContainerImageID, Priority: 1},
+ {ID: report.Container, Label: "# Containers", Value: "1", Priority: 2, Datatype: "number"},
},
Adjacency: report.MakeIDList(fixture.ServerContainerImageNodeID),
},
@@ -164,16 +164,15 @@ func TestMakeNodeSummary(t *testing.T) {
Rank: "hostname.com",
Shape: "circle",
Linkable: true,
- Metadata: []detailed.MetadataRow{
- {ID: host.HostName, Value: fixture.ClientHostName, Prime: false},
+ Metadata: []report.MetadataRow{
+ {ID: host.HostName, Label: "Hostname", Value: fixture.ClientHostName, Priority: 11},
},
- Metrics: []detailed.MetricRow{},
Adjacency: report.MakeIDList(fixture.ServerHostNodeID),
},
},
}
for _, testcase := range testcases {
- have, ok := detailed.MakeNodeSummary(testcase.input)
+ have, ok := detailed.MakeNodeSummary(fixture.Report, testcase.input)
if ok != testcase.ok {
t.Errorf("%s: MakeNodeSummary failed: expected ok value to be: %v", testcase.name, testcase.ok)
continue
diff --git a/render/mapping.go b/render/mapping.go
index ea1605bfe..b10fee3d7 100644
--- a/render/mapping.go
+++ b/render/mapping.go
@@ -26,8 +26,6 @@ const (
InboundMinor = "Inbound connections"
OutboundMinor = "Outbound connections"
- ipsKey = "ips"
-
// Topology for pseudo-nodes and IPs so we can differentiate them at the end
Pseudo = "pseudo"
IP = "IP"
@@ -46,7 +44,7 @@ func NewDerivedNode(id string, node report.Node) report.Node {
// NewDerivedPseudoNode makes a new pseudo node with the node as a child
func NewDerivedPseudoNode(id string, node report.Node) report.Node {
- return node.WithID(id).WithTopology(Pseudo).WithChildren(report.MakeNodeSet(node)).PruneParents()
+ return NewDerivedNode(id, node).WithTopology(Pseudo)
}
func theInternetNode(m report.Node) report.Node {
@@ -110,7 +108,7 @@ func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes {
result[id] = NewDerivedNode(id, m).
WithTopology(IP).
WithLatests(map[string]string{docker.ContainerID: containerID}).
- WithCounters(map[string]int{ipsKey: 1})
+ WithCounters(map[string]int{IP: 1})
}
}
@@ -125,7 +123,7 @@ func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes {
result[id] = NewDerivedNode(id, m).
WithTopology(IP).
WithLatests(map[string]string{docker.ContainerID: containerID}).
- WithCounters(map[string]int{ipsKey: 1})
+ WithCounters(map[string]int{IP: 1})
}
}
@@ -139,7 +137,7 @@ func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes {
func MapIP2Container(n report.Node, _ report.Networks) report.Nodes {
// If an IP is shared between multiple containers, we can't
// reliably attribute an connection based on its IP
- if count, _ := n.Counters.Lookup(ipsKey); count > 1 {
+ if count, _ := n.Counters.Lookup(IP); count > 1 {
return report.Nodes{}
}
diff --git a/report/counters.go b/report/counters.go
index f7c5b7f08..35f2c228e 100644
--- a/report/counters.go
+++ b/report/counters.go
@@ -139,8 +139,8 @@ func (c Counters) DeepEqual(d Counters) bool {
func (c Counters) toIntermediate() map[string]int {
intermediate := map[string]int{}
- c.psMap.ForEach(func(key string, val interface{}) {
- intermediate[key] = val.(int)
+ c.ForEach(func(key string, val int) {
+ intermediate[key] = val
})
return intermediate
}
diff --git a/report/metadata_template.go b/report/metadata_template.go
new file mode 100644
index 000000000..3091ba730
--- /dev/null
+++ b/report/metadata_template.go
@@ -0,0 +1,162 @@
+package report
+
+import (
+ "sort"
+ "strconv"
+ "strings"
+)
+
+const (
+ number = "number"
+)
+
+// FromLatest and friends denote the different fields where metadata can be
+// gathered from.
+const (
+ FromLatest = "latest"
+ FromSets = "sets"
+ FromCounters = "counters"
+)
+
+// MetadataTemplate extracts some metadata rows from a node
+type MetadataTemplate struct {
+ ID string `json:"id"`
+ Label string `json:"label,omitempty"` // Human-readable descriptor for this row
+ Truncate int `json:"truncate,omitempty"` // If > 0, truncate the value to this length.
+ Datatype string `json:"dataType,omitempty"`
+ Priority float64 `json:"priority,omitempty"`
+ From string `json:"from,omitempty"` // Defines how to get the value from a report node
+}
+
+// Copy returns a value-copy of the template
+func (t MetadataTemplate) Copy() MetadataTemplate {
+ return MetadataTemplate{
+ ID: t.ID,
+ Label: t.Label,
+ Truncate: t.Truncate,
+ Priority: t.Priority,
+ From: t.From,
+ }
+}
+
+// MetadataRows returns the rows for a node
+func (t MetadataTemplate) MetadataRows(n Node) []MetadataRow {
+ from := fromDefault
+ switch t.From {
+ case FromLatest:
+ from = fromLatest
+ case FromSets:
+ from = fromSets
+ case FromCounters:
+ from = fromCounters
+ }
+ if val, ok := from(n, t.ID); ok {
+ if t.Truncate > 0 && len(val) > t.Truncate {
+ val = val[:t.Truncate]
+ }
+ return []MetadataRow{{
+ ID: t.ID,
+ Label: t.Label,
+ Value: val,
+ Datatype: t.Datatype,
+ Priority: t.Priority,
+ }}
+ }
+ return nil
+}
+
+func fromDefault(n Node, key string) (string, bool) {
+ for _, from := range []func(n Node, key string) (string, bool){fromLatest, fromSets, fromCounters} {
+ if val, ok := from(n, key); ok {
+ return val, ok
+ }
+ }
+ return "", false
+}
+
+func fromLatest(n Node, key string) (string, bool) {
+ return n.Latest.Lookup(key)
+}
+
+func fromSets(n Node, key string) (string, bool) {
+ val, ok := n.Sets.Lookup(key)
+ return strings.Join(val, ", "), ok
+}
+
+func fromCounters(n Node, key string) (string, bool) {
+ val, ok := n.Counters.Lookup(key)
+ return strconv.Itoa(val), ok
+}
+
+// MetadataRow is a row for the metadata table.
+type MetadataRow struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ Value string `json:"value"`
+ Priority float64 `json:"priority,omitempty"`
+ Datatype string `json:"dataType,omitempty"`
+}
+
+// Copy returns a value copy of a metadata row.
+func (m MetadataRow) Copy() MetadataRow {
+ return m
+}
+
+// MetadataTemplates is a mergeable set of metadata templates
+type MetadataTemplates map[string]MetadataTemplate
+
+// MetadataRows returns the rows for a node
+func (e MetadataTemplates) MetadataRows(n Node) []MetadataRow {
+ var rows []MetadataRow
+ for _, template := range e {
+ rows = append(rows, template.MetadataRows(n)...)
+ }
+ sort.Sort(MetadataRowsByPriority(rows))
+ return rows
+}
+
+// Copy returns a value copy of the metadata templates
+func (e MetadataTemplates) Copy() MetadataTemplates {
+ if e == nil {
+ return nil
+ }
+ result := MetadataTemplates{}
+ for k, v := range e {
+ result[k] = v.Copy()
+ }
+ return result
+}
+
+// Merge merges two sets of MetadataTemplates so far just ignores based
+// on duplicate id key
+func (e MetadataTemplates) Merge(other MetadataTemplates) MetadataTemplates {
+ result := e.Copy()
+ for k, v := range other {
+ if result == nil {
+ result = MetadataTemplates{}
+ }
+ if existing, ok := result[k]; !ok || existing.Priority < v.Priority {
+ result[k] = v
+ }
+ }
+ return result
+}
+
+// MetadataRowsByPriority implements sort.Interface, so we can sort the rows by
+// priority before rendering them to the UI.
+type MetadataRowsByPriority []MetadataRow
+
+// Len is part of sort.Interface.
+func (m MetadataRowsByPriority) Len() int {
+ return len(m)
+}
+
+// Swap is part of sort.Interface.
+func (m MetadataRowsByPriority) Swap(i, j int) {
+ m[i], m[j] = m[j], m[i]
+}
+
+// Less is part of sort.Interface.
+func (m MetadataRowsByPriority) Less(i, j int) bool {
+ return m[i].Priority < m[j].Priority
+}
diff --git a/report/metric_row.go b/report/metric_row.go
new file mode 100644
index 000000000..ad965175e
--- /dev/null
+++ b/report/metric_row.go
@@ -0,0 +1,130 @@
+package report
+
+import (
+ "github.com/ugorji/go/codec"
+)
+
+// DefaultFormat and friends tell the UI how to render the "Value" of this
+// metric.
+const (
+ DefaultFormat = ""
+ FilesizeFormat = "filesize"
+ IntegerFormat = "integer"
+ PercentFormat = "percent"
+)
+
+// MetricRow is a tuple of data used to render a metric as a sparkline and
+// accoutrements.
+type MetricRow struct {
+ ID string
+ Label string
+ Format string
+ Group string
+ Value float64
+ Priority float64
+ Metric *Metric
+}
+
+// Summary returns a copy of the MetricRow, without the samples, just the value if there is one.
+func (m MetricRow) Summary() MetricRow {
+ row := m.Copy()
+ if m.Metric != nil {
+ row.Metric.Samples = nil
+ }
+ return row
+}
+
+// Copy returns a value copy of the MetricRow
+func (m MetricRow) Copy() MetricRow {
+ row := m
+ if m.Metric != nil {
+ var metric = m.Metric.Copy()
+ row.Metric = &metric
+ }
+ return row
+}
+
+// MarshalJSON shouldn't be used, use CodecEncodeSelf instead
+func (MetricRow) MarshalJSON() ([]byte, error) {
+ panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
+}
+
+// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead
+func (*MetricRow) UnmarshalJSON(b []byte) error {
+ panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
+}
+
+type wiredMetricRow struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ Format string `json:"format,omitempty"`
+ Group string `json:"group,omitempty"`
+ Value float64 `json:"value"`
+ Priority float64 `json:"priority,omitempty"`
+ Samples []Sample `json:"samples"`
+ Min float64 `json:"min"`
+ Max float64 `json:"max"`
+ First string `json:"first,omitempty"`
+ Last string `json:"last,omitempty"`
+}
+
+// CodecEncodeSelf marshals this MetricRow. It takes the basic Metric
+// rendering, then adds some row-specific fields.
+func (m *MetricRow) CodecEncodeSelf(encoder *codec.Encoder) {
+ in := m.Metric.ToIntermediate()
+ encoder.Encode(wiredMetricRow{
+ ID: m.ID,
+ Label: m.Label,
+ Format: m.Format,
+ Group: m.Group,
+ Value: m.Value,
+ Priority: m.Priority,
+ Samples: in.Samples,
+ Min: in.Min,
+ Max: in.Max,
+ First: in.First,
+ Last: in.Last,
+ })
+}
+
+// CodecDecodeSelf implements codec.Selfer
+func (m *MetricRow) CodecDecodeSelf(decoder *codec.Decoder) {
+ var in wiredMetricRow
+ decoder.Decode(&in)
+ w := WireMetrics{
+ Samples: in.Samples,
+ Min: in.Min,
+ Max: in.Max,
+ First: in.First,
+ Last: in.Last,
+ }
+ metric := w.FromIntermediate()
+ *m = MetricRow{
+ ID: in.ID,
+ Label: in.Label,
+ Format: in.Format,
+ Group: in.Group,
+ Value: in.Value,
+ Priority: in.Priority,
+ Metric: &metric,
+ }
+}
+
+// MetricRowsByPriority implements sort.Interface, so we can sort the rows by
+// priority before rendering them to the UI.
+type MetricRowsByPriority []MetricRow
+
+// Len is part of sort.Interface.
+func (m MetricRowsByPriority) Len() int {
+ return len(m)
+}
+
+// Swap is part of sort.Interface.
+func (m MetricRowsByPriority) Swap(i, j int) {
+ m[i], m[j] = m[j], m[i]
+}
+
+// Less is part of sort.Interface.
+func (m MetricRowsByPriority) Less(i, j int) bool {
+ return m[i].Priority < m[j].Priority
+}
diff --git a/report/metric_template.go b/report/metric_template.go
new file mode 100644
index 000000000..8cd46f244
--- /dev/null
+++ b/report/metric_template.go
@@ -0,0 +1,86 @@
+package report
+
+import (
+ "math"
+ "sort"
+)
+
+// MetricTemplate extracts a metric row from a node
+type MetricTemplate struct {
+ ID string `json:"id"`
+ Label string `json:"label,omitempty"`
+ Format string `json:"format,omitempty"`
+ Group string `json:"group,omitempty"`
+ Priority float64 `json:"priority,omitempty"`
+}
+
+// MetricRows returns the rows for a node
+func (t MetricTemplate) MetricRows(n Node) []MetricRow {
+ metric, ok := n.Metrics.Lookup(t.ID)
+ if !ok {
+ return nil
+ }
+ row := MetricRow{
+ ID: t.ID,
+ Label: t.Label,
+ Format: t.Format,
+ Group: t.Group,
+ Priority: t.Priority,
+ Metric: &metric,
+ }
+ if s := metric.LastSample(); s != nil {
+ row.Value = toFixed(s.Value, 2)
+ }
+ return []MetricRow{row}
+}
+
+// Copy returns a value-copy of the metric template
+func (t MetricTemplate) Copy() MetricTemplate {
+ return t
+}
+
+// MetricTemplates is a mergeable set of metric templates
+type MetricTemplates map[string]MetricTemplate
+
+// MetricRows returns the rows for a node
+func (e MetricTemplates) MetricRows(n Node) []MetricRow {
+ var rows []MetricRow
+ for _, template := range e {
+ rows = append(rows, template.MetricRows(n)...)
+ }
+ sort.Sort(MetricRowsByPriority(rows))
+ return rows
+}
+
+// Copy returns a value copy of the metadata templates
+func (e MetricTemplates) Copy() MetricTemplates {
+ if e == nil {
+ return nil
+ }
+ result := MetricTemplates{}
+ for k, v := range e {
+ result[k] = v.Copy()
+ }
+ return result
+}
+
+// Merge merges two sets of MetricTemplates so far just ignores based
+// on duplicate id key
+func (e MetricTemplates) Merge(other MetricTemplates) MetricTemplates {
+ result := e.Copy()
+ for k, v := range other {
+ if result == nil {
+ result = MetricTemplates{}
+ }
+ if existing, ok := result[k]; !ok || existing.Priority < v.Priority {
+ result[k] = v
+ }
+ }
+ return result
+}
+
+// toFixed truncates decimals of float64 down to specified precision
+func toFixed(num float64, precision int) float64 {
+ output := math.Pow(10, float64(precision))
+ return float64(int64(num*output)) / output
+}
diff --git a/report/metrics.go b/report/metrics.go
index c911b41c8..af1227dbc 100644
--- a/report/metrics.go
+++ b/report/metrics.go
@@ -13,6 +13,12 @@ import (
// Metrics is a string->metric map.
type Metrics map[string]Metric
+// Lookup the metric for the given key
+func (m Metrics) Lookup(key string) (Metric, bool) {
+ v, ok := m[key]
+ return v, ok
+}
+
// Merge merges two sets maps into a fresh set, performing set-union merges as
// appropriate.
func (m Metrics) Merge(other Metrics) Metrics {
diff --git a/report/report.go b/report/report.go
index 7be47d92a..202f68e29 100644
--- a/report/report.go
+++ b/report/report.go
@@ -5,6 +5,8 @@ import (
"math/rand"
"strings"
"time"
+
+ "github.com/weaveworks/scope/common/xfer"
)
// Names of the various topologies.
@@ -17,6 +19,9 @@ const (
ContainerImage = "container_image"
Host = "host"
Overlay = "overlay"
+
+ // Used when counting the number of containers
+ ContainersKey = "containers"
)
// Report is the core data type. It's produced by probes, and consumed and
@@ -76,6 +81,8 @@ type Report struct {
// bypassing the usual spy interval, publish interval and app ws interval.
Shortcut bool
+ Plugins xfer.PluginSpecs
+
// ID a random identifier for this report, used when caching
// rendered views of the report. Reports with the same id
// must be equal, but we don't require that equal reports have
@@ -96,6 +103,7 @@ func MakeReport() Report {
Overlay: MakeTopology(),
Sampling: Sampling{},
Window: 0,
+ Plugins: xfer.MakePluginSpecs(),
ID: fmt.Sprintf("%d", rand.Int63()),
}
}
@@ -113,6 +121,7 @@ func (r Report) Copy() Report {
Overlay: r.Overlay.Copy(),
Sampling: r.Sampling,
Window: r.Window,
+ Plugins: r.Plugins.Copy(),
ID: fmt.Sprintf("%d", rand.Int63()),
}
}
@@ -131,6 +140,7 @@ func (r Report) Merge(other Report) Report {
cp.Overlay = r.Overlay.Merge(other.Overlay)
cp.Sampling = r.Sampling.Merge(other.Sampling)
cp.Window += other.Window
+ cp.Plugins = r.Plugins.Merge(other.Plugins)
return cp
}
diff --git a/report/topology.go b/report/topology.go
index 46af52b43..efbb52667 100644
--- a/report/topology.go
+++ b/report/topology.go
@@ -10,8 +10,10 @@ import (
// EdgeMetadatas and Nodes respectively. Edges are directional, and embedded
// in the Node struct.
type Topology struct {
- Nodes `json:"nodes"`
- Controls `json:"controls,omitempty"`
+ Nodes `json:"nodes"`
+ Controls `json:"controls,omitempty"`
+ MetadataTemplates `json:"metadata_templates,omitempty"`
+ MetricTemplates `json:"metric_templates,omitempty"`
}
// MakeTopology gives you a Topology.
@@ -22,6 +24,28 @@ func MakeTopology() Topology {
}
}
+// WithMetadataTemplates merges some metadata templates into this topology,
+// returning a new topology.
+func (t Topology) WithMetadataTemplates(other MetadataTemplates) Topology {
+ return Topology{
+ Nodes: t.Nodes.Copy(),
+ Controls: t.Controls.Copy(),
+ MetadataTemplates: t.MetadataTemplates.Merge(other),
+ MetricTemplates: t.MetricTemplates.Copy(),
+ }
+}
+
+// WithMetricTemplates merges some metadata templates into this topology,
+// returning a new topology.
+func (t Topology) WithMetricTemplates(other MetricTemplates) Topology {
+ return Topology{
+ Nodes: t.Nodes.Copy(),
+ Controls: t.Controls.Copy(),
+ MetadataTemplates: t.MetadataTemplates.Copy(),
+ MetricTemplates: t.MetricTemplates.Merge(other),
+ }
+}
+
// AddNode adds node to the topology under key nodeID; if a
// node already exists for this key, nmd is merged with that node.
// The same topology is returned to enable chaining.
@@ -38,8 +62,10 @@ func (t Topology) AddNode(nodeID string, node Node) Topology {
// Copy returns a value copy of the Topology.
func (t Topology) Copy() Topology {
return Topology{
- Nodes: t.Nodes.Copy(),
- Controls: t.Controls.Copy(),
+ Nodes: t.Nodes.Copy(),
+ Controls: t.Controls.Copy(),
+ MetadataTemplates: t.MetadataTemplates.Copy(),
+ MetricTemplates: t.MetricTemplates.Copy(),
}
}
@@ -47,8 +73,10 @@ func (t Topology) Copy() Topology {
// The original is not modified.
func (t Topology) Merge(other Topology) Topology {
return Topology{
- Nodes: t.Nodes.Merge(other.Nodes),
- Controls: t.Controls.Merge(other.Controls),
+ Nodes: t.Nodes.Merge(other.Nodes),
+ Controls: t.Controls.Merge(other.Controls),
+ MetadataTemplates: t.MetadataTemplates.Merge(other.MetadataTemplates),
+ MetricTemplates: t.MetricTemplates.Merge(other.MetricTemplates),
}
}
diff --git a/scope b/scope
index 4233d2d15..4e3e92af2 100755
--- a/scope
+++ b/scope
@@ -92,6 +92,7 @@ check_not_running() {
launch_command() {
echo docker run --privileged -d --name=$SCOPE_CONTAINER_NAME --net=host --pid=host \
-v /var/run/docker.sock:/var/run/docker.sock \
+ -v /var/run/scope/plugins:/var/run/scope/plugins \
-e CHECKPOINT_DISABLE \
$WEAVESCOPE_DOCKER_ARGS $SCOPE_IMAGE --probe.docker true "$@"
}
diff --git a/test/fixture/report_fixture.go b/test/fixture/report_fixture.go
index fa5a5bd77..8faf9a147 100644
--- a/test/fixture/report_fixture.go
+++ b/test/fixture/report_fixture.go
@@ -242,6 +242,8 @@ var (
Add("host", report.MakeStringSet(ServerHostNodeID)),
),
},
+ MetadataTemplates: process.MetadataTemplates,
+ MetricTemplates: process.MetricTemplates,
},
Container: report.Topology{
Nodes: report.Nodes{
@@ -285,6 +287,8 @@ var (
docker.MemoryUsage: ServerContainerMemoryMetric,
}),
},
+ MetadataTemplates: docker.ContainerMetadataTemplates,
+ MetricTemplates: docker.ContainerMetricTemplates,
},
ContainerImage: report.Topology{
Nodes: report.Nodes{
@@ -305,6 +309,7 @@ var (
Add("host", report.MakeStringSet(ServerHostNodeID)),
).WithID(ServerContainerImageNodeID).WithTopology(report.ContainerImage),
},
+ MetadataTemplates: docker.ContainerImageMetadataTemplates,
},
Host: report.Topology{
Nodes: report.Nodes{
@@ -331,6 +336,8 @@ var (
host.Load1: ServerHostLoad1Metric,
}),
},
+ MetadataTemplates: host.MetadataTemplates,
+ MetricTemplates: host.MetricTemplates,
},
Pod: report.Topology{
Nodes: report.Nodes{
@@ -355,6 +362,7 @@ var (
Add("service", report.MakeStringSet(ServiceID)),
),
},
+ MetadataTemplates: kubernetes.PodMetadataTemplates,
},
Service: report.Topology{
Nodes: report.Nodes{
diff --git a/test/fs/fs.go b/test/fs/fs.go
index b59f37dc8..080895bd0 100644
--- a/test/fs/fs.go
+++ b/test/fs/fs.go
@@ -27,6 +27,9 @@ type File struct {
mockInode
FName string
FContents string
+ FReader io.Reader
+ FWriter io.Writer
+ FCloser io.Closer
FStat syscall.Stat_t
}
@@ -34,6 +37,8 @@ type File struct {
type Entry interface {
os.FileInfo
fs.Interface
+ Add(path string, e Entry) error
+ Remove(path string) error
}
// Dir creates a new directory with the given entries.
@@ -123,6 +128,7 @@ func (p dir) ReadFile(path string) ([]byte, error) {
func (p dir) Lstat(path string, stat *syscall.Stat_t) error {
if path == "/" {
+ *stat = syscall.Stat_t{Mode: syscall.S_IFDIR}
return nil
}
@@ -137,6 +143,7 @@ func (p dir) Lstat(path string, stat *syscall.Stat_t) error {
func (p dir) Stat(path string, stat *syscall.Stat_t) error {
if path == "/" {
+ *stat = syscall.Stat_t{Mode: syscall.S_IFDIR}
return nil
}
@@ -163,6 +170,36 @@ func (p dir) Open(path string) (io.ReadWriteCloser, error) {
return fs.Open(tail)
}
+func (p dir) Add(path string, e Entry) error {
+ if path == "/" {
+ p.entries[e.Name()] = e
+ return nil
+ }
+
+ head, tail := split(path)
+ fs, ok := p.entries[head]
+ if !ok {
+ fs = Dir(head)
+ p.entries[head] = fs
+ }
+
+ return fs.Add(tail, e)
+}
+
+func (p dir) Remove(path string) error {
+ if _, ok := p.entries[strings.TrimPrefix(path, "/")]; ok {
+ delete(p.entries, strings.TrimPrefix(path, "/"))
+ return nil
+ }
+
+ head, tail := split(path)
+ fs, ok := p.entries[head]
+ if !ok {
+ return nil
+ }
+ return fs.Remove(tail)
+}
+
// Name implements os.FileInfo
func (p File) Name() string { return p.FName }
@@ -184,6 +221,9 @@ func (p File) ReadFile(path string) ([]byte, error) {
if path != "/" {
return nil, fmt.Errorf("I'm a file!")
}
+ if p.FReader != nil {
+ return ioutil.ReadAll(p.FReader)
+ }
return []byte(p.FContents), nil
}
@@ -210,11 +250,38 @@ func (p File) Open(path string) (io.ReadWriteCloser, error) {
if path != "/" {
return nil, fmt.Errorf("I'm a file!")
}
- return struct {
- io.ReadWriter
+ buf := bytes.NewBuffer([]byte(p.FContents))
+ s := struct {
+ io.Reader
+ io.Writer
io.Closer
}{
- bytes.NewBuffer([]byte(p.FContents)),
- ioutil.NopCloser(nil),
- }, nil
+ buf, buf, ioutil.NopCloser(nil),
+ }
+ if p.FReader != nil {
+ s.Reader = p.FReader
+ }
+ if p.FWriter != nil {
+ s.Writer = p.FWriter
+ }
+ if p.FCloser != nil {
+ s.Closer = p.FCloser
+ }
+ return s, nil
+}
+
+// Add adds a new node to the fs
+func (p File) Add(path string, e Entry) error {
+ if path != "/" {
+ return fmt.Errorf("I'm a file!")
+ }
+ return nil
+}
+
+// Remove removes a node from the fs
+func (p File) Remove(path string) error {
+ if path != "/" {
+ return fmt.Errorf("I'm a file!")
+ }
+ return nil
}