Improve probe docker code quality & test coverage.

- Move docker probe code into it's own module
- Put PIDTree behind and interface for mocking
- Disaggregate dockerTagger into a registry, tagger and reporter
- Similarly disaggregate tests
- Add mocks for docker container and registry
- Add test for docker events & stats
This commit is contained in:
Tom Wilkie
2015-06-18 08:50:27 +00:00
parent a6ef295bd8
commit 314af5ca89
20 changed files with 1062 additions and 596 deletions

View File

@@ -1,156 +0,0 @@
package tag
import (
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"sync"
docker "github.com/fsouza/go-dockerclient"
)
// These constants are keys used in node metadata
// TODO: use these constants in report/{mapping.go, detailed_node.go} - pending some circular references
const (
NetworkRxDropped = "network_rx_dropped"
NetworkRxBytes = "network_rx_bytes"
NetworkRxErrors = "network_rx_errors"
NetworkTxPackets = "network_tx_packets"
NetworkTxDropped = "network_tx_dropped"
NetworkRxPackets = "network_rx_packets"
NetworkTxErrors = "network_tx_errors"
NetworkTxBytes = "network_tx_bytes"
MemoryMaxUsage = "memory_max_usage"
MemoryUsage = "memory_usage"
MemoryFailcnt = "memory_failcnt"
MemoryLimit = "memory_limit"
CPUPercpuUsage = "cpu_per_cpu_usage"
CPUUsageInUsermode = "cpu_usage_in_usermode"
CPUTotalUsage = "cpu_total_usage"
CPUUsageInKernelmode = "cpu_usage_in_kernelmode"
CPUSystemCPUUsage = "cpu_system_cpu_usage"
)
type dockerContainer struct {
sync.RWMutex
*docker.Container
statsConn *httputil.ClientConn
latestStats *docker.Stats
}
// called whilst holding t.Lock() for writes
func (c *dockerContainer) startGatheringStats(containerID string) error {
if c.statsConn != nil {
return fmt.Errorf("already gather stats for container %s", containerID)
}
log.Printf("docker mapper: collecting stats for %s", containerID)
req, err := http.NewRequest("GET", fmt.Sprintf("/containers/%s/stats", containerID), nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "weavescope")
url, err := url.Parse(endpoint)
if err != nil {
return err
}
dial, err := net.Dial(url.Scheme, url.Path)
if err != nil {
return err
}
conn := httputil.NewClientConn(dial, nil)
resp, err := conn.Do(req)
if err != nil {
return err
}
c.statsConn = conn
go func() {
defer func() {
c.Lock()
defer c.Unlock()
log.Printf("docker mapper: stopped collecting stats for %s", containerID)
c.statsConn = nil
c.latestStats = nil
}()
stats := &docker.Stats{}
decoder := json.NewDecoder(resp.Body)
for err := decoder.Decode(&stats); err != io.EOF; err = decoder.Decode(&stats) {
if err != nil {
log.Printf("docker mapper: error reading event %v", err)
return
}
c.Lock()
c.latestStats = stats
c.Unlock()
stats = &docker.Stats{}
}
}()
return nil
}
// called whilst holding t.Lock()
func (c *dockerContainer) stopGatheringStats(containerID string) {
c.Lock()
defer c.Unlock()
if c.statsConn == nil {
return
}
c.statsConn.Close()
c.statsConn = nil
c.latestStats = nil
return
}
// called whilst holding t.RLock()
func (c *dockerContainer) getStats() map[string]string {
c.RLock()
defer c.RUnlock()
if c.latestStats == nil {
return map[string]string{}
}
return map[string]string{
NetworkRxDropped: strconv.FormatUint(c.latestStats.Network.RxDropped, 10),
NetworkRxBytes: strconv.FormatUint(c.latestStats.Network.RxBytes, 10),
NetworkRxErrors: strconv.FormatUint(c.latestStats.Network.RxErrors, 10),
NetworkTxPackets: strconv.FormatUint(c.latestStats.Network.TxPackets, 10),
NetworkTxDropped: strconv.FormatUint(c.latestStats.Network.TxDropped, 10),
NetworkRxPackets: strconv.FormatUint(c.latestStats.Network.RxPackets, 10),
NetworkTxErrors: strconv.FormatUint(c.latestStats.Network.TxErrors, 10),
NetworkTxBytes: strconv.FormatUint(c.latestStats.Network.TxBytes, 10),
MemoryMaxUsage: strconv.FormatUint(c.latestStats.MemoryStats.MaxUsage, 10),
MemoryUsage: strconv.FormatUint(c.latestStats.MemoryStats.Usage, 10),
MemoryFailcnt: strconv.FormatUint(c.latestStats.MemoryStats.Failcnt, 10),
MemoryLimit: strconv.FormatUint(c.latestStats.MemoryStats.Limit, 10),
// CPUPercpuUsage: strconv.FormatUint(stats.CPUStats.CPUUsage.PercpuUsage, 10),
CPUUsageInUsermode: strconv.FormatUint(c.latestStats.CPUStats.CPUUsage.UsageInUsermode, 10),
CPUTotalUsage: strconv.FormatUint(c.latestStats.CPUStats.CPUUsage.TotalUsage, 10),
CPUUsageInKernelmode: strconv.FormatUint(c.latestStats.CPUStats.CPUUsage.UsageInKernelmode, 10),
CPUSystemCPUUsage: strconv.FormatUint(c.latestStats.CPUStats.SystemCPUUsage, 10),
}
}

View File

@@ -1,364 +0,0 @@
package tag
import (
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
docker "github.com/fsouza/go-dockerclient"
"github.com/weaveworks/scope/report"
)
const (
start = "start"
die = "die"
endpoint = "unix:///var/run/docker.sock"
)
// These constants are keys used in node metadata
// TODO: use these constants in report/{mapping.go, detailed_node.go} - pending some circular references
const (
ContainerID = "docker_container_id"
ContainerName = "docker_container_name"
ImageID = "docker_image_id"
ImageName = "docker_image_name"
)
var (
newDockerClientStub = newDockerClient
newPIDTreeStub = NewPIDTree
)
// DockerTagger is a tagger that tags Docker container information to process
// nodes that have a PID.
type DockerTagger struct {
sync.RWMutex
quit chan struct{}
interval time.Duration
client dockerClient
containers map[string]*dockerContainer
containersByPID map[int]*dockerContainer
images map[string]*docker.APIImages
procRoot string
pidTree *PIDTree
}
// Sub-interface for mocking.
type dockerClient interface {
ListContainers(docker.ListContainersOptions) ([]docker.APIContainers, error)
InspectContainer(string) (*docker.Container, error)
ListImages(docker.ListImagesOptions) ([]docker.APIImages, error)
AddEventListener(chan<- *docker.APIEvents) error
RemoveEventListener(chan *docker.APIEvents) error
}
func newDockerClient(endpoint string) (dockerClient, error) {
return docker.NewClient(endpoint)
}
// NewDockerTagger returns a usable DockerTagger. Don't forget to Stop it.
func NewDockerTagger(procRoot string, interval time.Duration) (*DockerTagger, error) {
pidTree, err := newPIDTreeStub(procRoot)
if err != nil {
return nil, err
}
t := DockerTagger{
containers: map[string]*dockerContainer{},
containersByPID: map[int]*dockerContainer{},
images: map[string]*docker.APIImages{},
procRoot: procRoot,
pidTree: pidTree,
interval: interval,
quit: make(chan struct{}),
}
go t.loop()
return &t, nil
}
// Stop stops the Docker tagger's event subscriber.
func (t *DockerTagger) Stop() {
close(t.quit)
}
func (t *DockerTagger) loop() {
if !t.update() {
return
}
ticker := time.Tick(t.interval)
for {
select {
case <-ticker:
if !t.update() {
return
}
case <-t.quit:
return
}
}
}
func (t *DockerTagger) update() bool {
client, err := newDockerClientStub(endpoint)
if err != nil {
log.Printf("docker mapper: %s", err)
return true
}
t.client = client
events := make(chan *docker.APIEvents)
if err := client.AddEventListener(events); err != nil {
log.Printf("docker mapper: %s", err)
return true
}
defer func() {
if err := client.RemoveEventListener(events); err != nil {
log.Printf("docker mapper: %s", err)
}
}()
if err := t.updateContainers(); err != nil {
log.Printf("docker mapper: %s", err)
return true
}
if err := t.updateImages(); err != nil {
log.Printf("docker mapper: %s", err)
return true
}
otherUpdates := time.Tick(t.interval)
for {
select {
case event := <-events:
t.handleEvent(event)
case <-otherUpdates:
if err := t.updatePIDTree(); err != nil {
log.Printf("docker mapper: %s", err)
continue
}
if err := t.updateImages(); err != nil {
log.Printf("docker mapper: %s", err)
continue
}
case <-t.quit:
return false
}
}
}
func (t *DockerTagger) updateContainers() error {
apiContainers, err := t.client.ListContainers(docker.ListContainersOptions{All: true})
if err != nil {
return err
}
for _, apiContainer := range apiContainers {
if err := t.addContainer(apiContainer.ID); err != nil {
log.Printf("docker mapper: %s", err)
}
}
return nil
}
func (t *DockerTagger) updateImages() error {
images, err := t.client.ListImages(docker.ListImagesOptions{})
if err != nil {
return err
}
t.Lock()
for i := range images {
image := &images[i]
t.images[image.ID] = image
}
t.Unlock()
return nil
}
func (t *DockerTagger) handleEvent(event *docker.APIEvents) {
switch event.Status {
case die:
containerID := event.ID
t.removeContainer(containerID)
case start:
containerID := event.ID
if err := t.addContainer(containerID); err != nil {
log.Printf("docker mapper: %s", err)
}
}
}
func (t *DockerTagger) updatePIDTree() error {
pidTree, err := newPIDTreeStub(t.procRoot)
if err != nil {
return err
}
t.Lock()
t.pidTree = pidTree
t.Unlock()
return nil
}
func (t *DockerTagger) addContainer(containerID string) error {
container, err := t.client.InspectContainer(containerID)
if err != nil {
// Don't spam the logs if the container was short lived
if _, ok := err.(*docker.NoSuchContainer); !ok {
return err
}
return nil
}
if !container.State.Running {
return fmt.Errorf("docker mapper: container %s not running", containerID)
}
t.Lock()
defer t.Unlock()
dockerContainer := &dockerContainer{Container: container}
t.containers[containerID] = dockerContainer
t.containersByPID[container.State.Pid] = dockerContainer
return dockerContainer.startGatheringStats(containerID)
}
func (t *DockerTagger) removeContainer(containerID string) {
t.Lock()
defer t.Unlock()
container, ok := t.containers[containerID]
if !ok {
return
}
delete(t.containers, containerID)
delete(t.containersByPID, container.State.Pid)
container.stopGatheringStats(containerID)
}
// Containers returns the Containers the DockerTagger knows about.
func (t *DockerTagger) Containers() []*docker.Container {
containers := []*docker.Container{}
t.RLock()
for _, container := range t.containers {
containers = append(containers, container.Container)
}
t.RUnlock()
return containers
}
// Tag implements Tagger.
func (t *DockerTagger) Tag(r report.Report) report.Report {
t.tag(&r.Process)
return r
}
func (t *DockerTagger) tag(topology *report.Topology) {
for nodeID, nodeMetadata := range topology.NodeMetadatas {
pidStr, ok := nodeMetadata["pid"]
if !ok {
//log.Printf("dockerTagger: %q: no process node ID", id)
continue
}
pid, err := strconv.ParseUint(pidStr, 10, 64)
if err != nil {
//log.Printf("dockerTagger: %q: bad process node PID (%v)", id, err)
continue
}
var (
container *dockerContainer
candidate = int(pid)
)
t.RLock()
for {
container, ok = t.containersByPID[candidate]
if ok {
break
}
candidate, err = t.pidTree.getParent(candidate)
if err != nil {
break
}
}
t.RUnlock()
if !ok {
continue
}
md := report.NodeMetadata{
ContainerID: container.ID,
}
topology.NodeMetadatas[nodeID].Merge(md)
}
}
// ContainerTopology produces a Toplogy of Containers
func (t *DockerTagger) ContainerTopology(scope string) report.Topology {
t.RLock()
defer t.RUnlock()
result := report.NewTopology()
for _, container := range t.containers {
nmd := report.NodeMetadata{
ContainerID: container.ID,
ContainerName: strings.TrimPrefix(container.Name, "/"),
ImageID: container.Image,
}
nmd.Merge(container.getStats())
nodeID := report.MakeContainerNodeID(scope, container.ID)
result.NodeMetadatas[nodeID] = nmd
}
return result
}
// ContainerImageTopology produces a Toplogy of Container Images
func (t *DockerTagger) ContainerImageTopology(scope string) report.Topology {
t.RLock()
defer t.RUnlock()
result := report.NewTopology()
// Loop over containers so we only emit images for running containers.
for _, container := range t.containers {
nmd := report.NodeMetadata{
ImageID: container.Image,
}
image, ok := t.images[container.Image]
if ok && len(image.RepoTags) > 0 {
nmd[ImageName] = image.RepoTags[0]
}
nodeID := report.MakeContainerNodeID(scope, container.Image)
result.NodeMetadatas[nodeID] = nmd
}
return result
}

View File

@@ -1,123 +0,0 @@
package tag
import (
"reflect"
"runtime"
"testing"
"time"
docker "github.com/fsouza/go-dockerclient"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/test"
)
type mockDockerClient struct {
apiContainers []docker.APIContainers
containers map[string]*docker.Container
apiImages []docker.APIImages
}
func (m mockDockerClient) ListContainers(docker.ListContainersOptions) ([]docker.APIContainers, error) {
return m.apiContainers, nil
}
func (m mockDockerClient) InspectContainer(id string) (*docker.Container, error) {
return m.containers[id], nil
}
func (m mockDockerClient) ListImages(docker.ListImagesOptions) ([]docker.APIImages, error) {
return m.apiImages, nil
}
func (m mockDockerClient) AddEventListener(events chan<- *docker.APIEvents) error {
return nil
}
func (m mockDockerClient) RemoveEventListener(events chan *docker.APIEvents) error {
return nil
}
func TestDockerTagger(t *testing.T) {
oldPIDTree, oldDockerClient := newPIDTreeStub, newDockerClientStub
defer func() { newPIDTreeStub, newDockerClientStub = oldPIDTree, oldDockerClient }()
newPIDTreeStub = func(procRoot string) (*PIDTree, error) {
pid1 := &Process{PID: 1}
pid2 := &Process{PID: 2, PPID: 1, parent: pid1}
pid1.children = []*Process{pid2}
return &PIDTree{
processes: map[int]*Process{
1: pid1, 2: pid2,
},
}, nil
}
newDockerClientStub = func(endpoint string) (dockerClient, error) {
return mockDockerClient{
apiContainers: []docker.APIContainers{{ID: "foo"}},
containers: map[string]*docker.Container{
"foo": {
ID: "foo",
Name: "bar",
Image: "baz",
State: docker.State{Pid: 1, Running: true},
},
},
apiImages: []docker.APIImages{{ID: "baz", RepoTags: []string{"bang", "not-chosen"}}},
}, nil
}
var (
pid1NodeID = report.MakeProcessNodeID("somehost.com", "1")
pid2NodeID = report.MakeProcessNodeID("somehost.com", "2")
processNodeMetadata = report.NodeMetadata{
ContainerID: "foo",
}
wantContainerTopology = report.Topology{
Adjacency: report.Adjacency{},
EdgeMetadatas: report.EdgeMetadatas{},
NodeMetadatas: report.NodeMetadatas{
report.MakeContainerNodeID("", "foo"): report.NodeMetadata{
ContainerID: "foo",
ContainerName: "bar",
ImageID: "baz",
},
},
}
wantContainerImageTopology = report.Topology{
Adjacency: report.Adjacency{},
EdgeMetadatas: report.EdgeMetadatas{},
NodeMetadatas: report.NodeMetadatas{
report.MakeContainerNodeID("", "baz"): report.NodeMetadata{
ImageID: "baz",
ImageName: "bang",
},
},
}
)
r := report.MakeReport()
r.Process.NodeMetadatas[pid1NodeID] = report.NodeMetadata{"pid": "1"}
r.Process.NodeMetadatas[pid2NodeID] = report.NodeMetadata{"pid": "2"}
dockerTagger, _ := NewDockerTagger("/irrelevant", 10*time.Second)
runtime.Gosched()
for _, nodeID := range []string{pid1NodeID, pid2NodeID} {
want := processNodeMetadata.Copy()
have := dockerTagger.Tag(r).Process.NodeMetadatas[nodeID].Copy()
delete(have, "pid")
if !reflect.DeepEqual(want, have) {
t.Errorf("%q: want %+v, have %+v", nodeID, want, have)
}
}
haveContainerTopology := dockerTagger.ContainerTopology("")
if !reflect.DeepEqual(wantContainerTopology, haveContainerTopology) {
t.Errorf("%s", test.Diff(wantContainerTopology, haveContainerTopology))
}
haveContainerImageTopology := dockerTagger.ContainerImageTopology("")
if !reflect.DeepEqual(wantContainerImageTopology, haveContainerImageTopology) {
t.Errorf("%s", test.Diff(wantContainerImageTopology, haveContainerImageTopology))
}
}

View File

@@ -12,12 +12,12 @@ func NewOriginHostTagger(hostID string) Tagger {
return &originHostTagger{hostNodeID: report.MakeHostNodeID(hostID)}
}
func (t originHostTagger) Tag(r report.Report) report.Report {
func (t originHostTagger) Tag(r report.Report) (report.Report, error) {
for _, topology := range r.Topologies() {
md := report.NodeMetadata{report.HostNodeID: t.hostNodeID}
for nodeID := range topology.NodeMetadatas {
topology.NodeMetadatas[nodeID].Merge(md)
}
}
return r
return r, nil
}

View File

@@ -18,7 +18,8 @@ func TestOriginHostTagger(t *testing.T) {
r := report.MakeReport()
r.Endpoint.NodeMetadatas[endpointNodeID] = nodeMetadata
want := nodeMetadata.Merge(report.NodeMetadata{report.HostNodeID: report.MakeHostNodeID(hostID)})
have := tag.NewOriginHostTagger(hostID).Tag(r).Endpoint.NodeMetadatas[endpointNodeID].Copy()
rpt, _ := tag.NewOriginHostTagger(hostID).Tag(r)
have := rpt.Endpoint.NodeMetadatas[endpointNodeID].Copy()
if !reflect.DeepEqual(want, have) {
t.Errorf("\nwant %+v\nhave %+v", want, have)
}

View File

@@ -11,16 +11,21 @@ import (
)
// PIDTree represents all processes on the machine.
type PIDTree struct {
processes map[int]*Process
type PIDTree interface {
GetParent(pid int) (int, error)
ProcessTopology(hostID string) report.Topology
}
type pidTree struct {
processes map[int]*process
}
// Process represents a single process.
type Process struct {
PID, PPID int
Comm string
parent *Process
children []*Process
type process struct {
pid, ppid int
comm string
parent *process
children []*process
}
// Hooks for mocking
@@ -30,13 +35,13 @@ var (
)
// NewPIDTree returns a new PIDTree that can be polled.
func NewPIDTree(procRoot string) (*PIDTree, error) {
func NewPIDTree(procRoot string) (PIDTree, error) {
dirEntries, err := readDir(procRoot)
if err != nil {
return nil, err
}
pt := PIDTree{processes: map[int]*Process{}}
pt := pidTree{processes: map[int]*process{}}
for _, dirEntry := range dirEntries {
filename := dirEntry.Name()
pid, err := strconv.Atoi(filename)
@@ -59,15 +64,15 @@ func NewPIDTree(procRoot string) (*PIDTree, error) {
comm = string(commBuf)
}
pt.processes[pid] = &Process{
PID: pid,
PPID: ppid,
Comm: comm,
pt.processes[pid] = &process{
pid: pid,
ppid: ppid,
comm: comm,
}
}
for _, child := range pt.processes {
parent, ok := pt.processes[child.PPID]
parent, ok := pt.processes[child.ppid]
if !ok {
// This can happen as listing proc is not a consistent snapshot
continue
@@ -79,48 +84,28 @@ func NewPIDTree(procRoot string) (*PIDTree, error) {
return &pt, nil
}
func (pt *PIDTree) getParent(pid int) (int, error) {
// GetParent returns the pid of the parent process for a given pid
func (pt *pidTree) GetParent(pid int) (int, error) {
proc, ok := pt.processes[pid]
if !ok {
return -1, fmt.Errorf("PID %d not found", pid)
}
return proc.PPID, nil
}
// allChildren returns a flattened list of child pids including the given pid
func (pt *PIDTree) allChildren(pid int) ([]int, error) {
proc, ok := pt.processes[pid]
if !ok {
return []int{}, fmt.Errorf("PID %d not found", pid)
}
var result []int
var f func(*Process)
f = func(p *Process) {
result = append(result, p.PID)
for _, child := range p.children {
f(child)
}
}
f(proc)
return result, nil
return proc.ppid, nil
}
// ProcessTopology returns a process topology based on the current state of the PIDTree.
func (pt *PIDTree) ProcessTopology(hostID string) report.Topology {
func (pt *pidTree) ProcessTopology(hostID string) report.Topology {
t := report.NewTopology()
for pid, proc := range pt.processes {
pidstr := strconv.Itoa(pid)
nodeID := report.MakeProcessNodeID(hostID, pidstr)
t.NodeMetadatas[nodeID] = report.NodeMetadata{
"pid": pidstr,
"comm": proc.Comm,
"comm": proc.comm,
}
if proc.PPID > 0 {
t.NodeMetadatas[nodeID]["ppid"] = strconv.Itoa(proc.PPID)
if proc.ppid > 0 {
t.NodeMetadatas[nodeID]["ppid"] = strconv.Itoa(proc.ppid)
}
}
return t

View File

@@ -48,16 +48,16 @@ func TestPIDTree(t *testing.T) {
return []byte(fmt.Sprintf("%d na R %d", pid, parent)), nil
}
pidtree, err := newPIDTreeStub("/proc")
pidtree, err := NewPIDTree("/proc")
if err != nil {
t.Fatalf("newPIDTree error: %v", err)
}
for pid, want := range map[int][]int{
1: {1, 2, 3, 4},
2: {2, 3, 4},
for pid, want := range map[int]int{
2: 1,
3: 2,
} {
have, err := pidtree.allChildren(pid)
have, err := pidtree.GetParent(pid)
if err != nil || !reflect.DeepEqual(want, have) {
t.Errorf("%d: want %#v, have %#v (%v)", pid, want, have, err)
}

View File

@@ -1,16 +1,29 @@
package tag
import "github.com/weaveworks/scope/report"
import (
"log"
"github.com/weaveworks/scope/report"
)
// Tagger tags nodes with value-add node metadata.
type Tagger interface {
Tag(r report.Report) report.Report
Tag(r report.Report) (report.Report, error)
}
// Reporter generates Reports.
type Reporter interface {
Report() report.Report
}
// Apply tags the report with all the taggers.
func Apply(r report.Report, taggers []Tagger) report.Report {
var err error
for _, tagger := range taggers {
r = tagger.Tag(r)
r, err = tagger.Tag(r)
if err != nil {
log.Printf("error applying tagger: %v", err)
}
}
return r
}

View File

@@ -11,7 +11,7 @@ func NewTopologyTagger() Tagger {
return &topologyTagger{}
}
func (topologyTagger) Tag(r report.Report) report.Report {
func (topologyTagger) Tag(r report.Report) (report.Report, error) {
for val, topology := range map[string]*report.Topology{
"endpoint": &(r.Endpoint),
"address": &(r.Address),
@@ -24,5 +24,5 @@ func (topologyTagger) Tag(r report.Report) report.Report {
(*topology).NodeMetadatas[nodeID].Merge(md)
}
}
return r
return r, nil
}

View File

@@ -12,7 +12,8 @@ func TestTagMissingID(t *testing.T) {
const nodeID = "not-found"
r := report.MakeReport()
want := report.NodeMetadata{}
have := tag.NewTopologyTagger().Tag(r).Endpoint.NodeMetadatas[nodeID].Copy()
rpt, _ := tag.NewTopologyTagger().Tag(r)
have := rpt.Endpoint.NodeMetadatas[nodeID].Copy()
if !reflect.DeepEqual(want, have) {
t.Error("TopologyTagger erroneously tagged a missing node ID")
}

View File

@@ -41,10 +41,10 @@ func NewWeaveTagger(weaveRouterAddress string) (*WeaveTagger, error) {
}
// Tag implements Tagger.
func (t WeaveTagger) Tag(r report.Report) report.Report {
func (t WeaveTagger) Tag(r report.Report) (report.Report, error) {
// The status-json endpoint doesn't return any link information, so
// there's nothing to tag, yet.
return r
return r, nil
}
// OverlayTopology produces an overlay topology from the Weave router.