mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-20 22:10:30 +00:00
Intial commit
This commit is contained in:
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
.vagrant
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
profile.cov
|
||||
coverage.html
|
||||
|
||||
# Emacs backup files
|
||||
*~
|
||||
|
||||
# Project specific
|
||||
weaver/weaver
|
||||
weavedns/weavedns
|
||||
sigproxy/sigproxy
|
||||
tools/bin
|
||||
tools/build
|
||||
releases
|
||||
weave.tar
|
||||
weavedns.tar
|
||||
weaveexec.tar
|
||||
weaveexec/weave
|
||||
weaveexec/sigproxy
|
||||
weaveexec/docker*.tgz
|
||||
Vagrantfile.local
|
||||
scope.tar
|
||||
scope/probe/probe
|
||||
scope/bridge/bridge
|
||||
scope/app/app
|
||||
scope/demoprobe/demoprobe
|
||||
scope/fixprobe/fixprobe
|
||||
scope/genreport/genreport
|
||||
scope/graphviz/graphviz
|
||||
scope/oneshot/oneshot
|
||||
scope/web/assets/videos/*.mp4
|
||||
scope/release/*.tar.gz
|
||||
scope/web/docs/*.html
|
||||
scope/*sublime-project
|
||||
scope/*sublime-workspace
|
||||
scope/*npm-debug.log
|
||||
|
||||
29
.travis.yml
Normal file
29
.travis.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.4
|
||||
|
||||
env:
|
||||
- PATH="${PATH}:${HOME}/.local/bin"
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libpcap-dev
|
||||
|
||||
install:
|
||||
- go clean -i net
|
||||
- go install -tags netgo std
|
||||
- pip install --user --upgrade gcloud gsutil
|
||||
- bin/setup-ci-secrets $encrypted_5ba036e89377_key $encrypted_5ba036e89377_iv
|
||||
- cd ..; git clone ssh://git@github.com/weaveworks/procspy.git; cd scope
|
||||
- make travis
|
||||
|
||||
script:
|
||||
- bin/lint .
|
||||
- make tests
|
||||
|
||||
after_success:
|
||||
- go get github.com/mattn/goveralls
|
||||
- $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
- test "${TRAVIS_PULL_REQUEST}" = "false" && site/build-and-publish.sh
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM gliderlabs/alpine
|
||||
MAINTAINER Weaveworks Inc <help@weave.works>
|
||||
WORKDIR /home/weave
|
||||
RUN apk add --update supervisor
|
||||
RUN ["sh", "-c", "rm -rf /var/cache/apk/*"]
|
||||
ADD supervisord.conf /etc/
|
||||
ADD ./app/app /home/weave/
|
||||
ADD ./probe/probe /home/weave/
|
||||
ADD ./entrypoint.sh /home/weave/
|
||||
EXPOSE 4040
|
||||
ENTRYPOINT ["/home/weave/entrypoint.sh"]
|
||||
137
ENHANCE.md
Normal file
137
ENHANCE.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Enhance
|
||||
|
||||
## 2014 10 05
|
||||
|
||||
Informed by a new context, we began sketching out a simplified data model.
|
||||
Initial requirements:
|
||||
|
||||
- Must support order-of-thousands nodes in the topology.
|
||||
- Must support a scrapable API.
|
||||
- Packet counts, etc. are a value-add and should be composed in to the topology as needed.
|
||||
|
||||
Desires based on experience so far:
|
||||
|
||||
- Too much effort was spent wrangling deeply nested hierarchies in the report structs. They should be flattened.
|
||||
- It would be nice if the probe API were directly consumable by the app.
|
||||
- This enables much simpler composition of "bridge" components, including none at all.
|
||||
- This composes nicely with the scrapable API requirement.
|
||||
- The simplest use case of no metadata at all could (should) be satisfied by scraping the /proc FS, with no packet sniffing.
|
||||
|
||||
Peter's initial thoughts was to key everything based on the atom of host + socket_descriptor.
|
||||
This represents the most specific (application-level) topology.
|
||||
Other more general topologies (network, transport) could be derived from that.
|
||||
|
||||
Harmen noted that we want to capture data that may not be associated with a socket_descriptor.
|
||||
Therefore we should build multiple, independent topologies: application, network, and transport.
|
||||
Each one is a complete topology for that layer, with lower (OSI) layers always containing the higher ones.
|
||||
|
||||
We think we can get away with a report structure that looks like this
|
||||
|
||||
```json
|
||||
{
|
||||
"application": {
|
||||
"adjacency": {
|
||||
"host:src": ["dst1", "dst2", "dst3"]
|
||||
},
|
||||
"metadata": {
|
||||
"host:src:dst1": {}
|
||||
}
|
||||
}
|
||||
|
||||
"network": {
|
||||
"adjacency": {},
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
"transport": {
|
||||
"adjacency": {},
|
||||
"metadata": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where the meaning of the keys and key-parts depends on the section of the report.
|
||||
Specifically, in the application section, src is an encoding of the socket_descriptor.
|
||||
|
||||
Note that we still need another section to map application keys to processes, either physical (PIDs) or logical (process names, service names, etc.).
|
||||
That mapping can take the form of a function:
|
||||
|
||||
```go
|
||||
type mapper func(socket_descriptor) string
|
||||
```
|
||||
|
||||
Where the returned string will be the aggregation key of the logical (application) topology.
|
||||
At the lowest level, it could be the PID that bound the socket_descriptor.
|
||||
At a middle layer, it could be the name of the process associated with the PID.
|
||||
At a higher layer, the function could query a service discovery component, and return a service name.
|
||||
It would probably be good to support multiple simultaneous mappers, so the application-layer topology could have multiple levels of detail.
|
||||
|
||||
### Example implementation
|
||||
|
||||
Two probes, A and B, with a single TCP connection, both agree there is a connection.
|
||||
|
||||
```json
|
||||
{
|
||||
"proc": {
|
||||
"mapping": {
|
||||
"hostA:192.168.1.1:12345": "curl",
|
||||
"hostB:192.168.1.2:80": "apache"
|
||||
},
|
||||
"adjacency": {
|
||||
"hostA:192.168.1.1:12345": ["192.168.1.2:80"],
|
||||
"hostB:192.168.1.2:80": ["192.168.1.1:12345"]
|
||||
},
|
||||
"metadata": {
|
||||
"hostA:192.168.1.1:12345:192.168.1.2:80": {"egress": "12bytes", "ingress": "0", "d": "15s"},
|
||||
"hostB:192.168.1.2:80:192.168.1.1:12345": {"egress": "0", "ingress": "12bytes", "d": "15s"}
|
||||
}
|
||||
},
|
||||
|
||||
"transport": {
|
||||
"adjacency": {
|
||||
"hostA:192.168.1.1:12345": ["192.168.1.2:80"],
|
||||
"hostB:192.168.1.2:80": ["192.168.1.1:12345"]
|
||||
},
|
||||
"metadata": {
|
||||
"hostA:192.168.1.1:12345:192.168.1.2:80": {"egress": "12bytes", "ingress": "0", "d": "15s"},
|
||||
"hostB:192.168.1.2:80:192.168.1.1:12345": {"egress": "0", "ingress": "12bytes", "d": "15s"}
|
||||
}
|
||||
},
|
||||
|
||||
"network": {
|
||||
"adjacency": {
|
||||
"hostA:192.168.1.1": ["192.168.1.2"],
|
||||
"hostB:192.168.1.2": ["192.168.1.1"]
|
||||
},
|
||||
"metadata": {
|
||||
"hostA:192.168.1.1:12345:192.168.1.2:80": {"egress": "12bytes", "ingress": "0", "d": "15s"},
|
||||
"hostB:192.168.1.2:80:192.168.1.1:12345": {"egress": "0", "ingress": "12bytes", "d": "15s"}
|
||||
}
|
||||
},
|
||||
|
||||
"nodes": {
|
||||
"hostA": {
|
||||
"addresses": ["192.168.1.1"],
|
||||
"hostname": "..."
|
||||
},
|
||||
"hostB": {
|
||||
"addresses": ["192.168.1.2"],
|
||||
"hostname": "...",
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the proc.mapping can only be filled by the procfs-probe.
|
||||
But {proc,transport}.{adjacency,metadata} could in theory be filled both procfs-probe and sniffing-probe.
|
||||
That's a consequence of the fact that, in the case of UDP and TCP, the socket_descriptor is fully reconstructable from a network packet's source and destination fields.
|
||||
For simplicity, and for now, we'd like to restrict that freedom, and state that the proc section should be filled only by the procfs-probe.
|
||||
This draws a clear line of ownership and demarcation.
|
||||
We can revisit this decision in the future, if it proves too restrictive.
|
||||
|
||||
For now we will assert that the metadata type is the same structure for each section.
|
||||
Adjacency list values don't necessarily need to exist as nodes. Cases are non-probe machines and broadcast addresses.
|
||||
|
||||
Merging reports erquires a time component because rates require a window, and we can't make assumptions about fixed window sizes (either because the edge isn't present for the entire interval, or the whole probe isn't).
|
||||
Reports are by definition non-overlapping for a single e.g. socket_descriptor, so that's a safe assumption to make. Therefore, all we need are durations, which may be semantically merged with simple addition.
|
||||
|
||||
191
LICENSE
Normal file
191
LICENSE
Normal file
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2014-2015 Weaveworks Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
45
Makefile
Normal file
45
Makefile
Normal file
@@ -0,0 +1,45 @@
|
||||
.PHONY: default get all build release clean
|
||||
|
||||
default: all
|
||||
|
||||
get:
|
||||
cd app && go get
|
||||
|
||||
all:
|
||||
$(MAKE) -C report
|
||||
$(MAKE) -C xfer
|
||||
$(MAKE) -C app
|
||||
$(MAKE) -C bridge
|
||||
$(MAKE) -C demoprobe
|
||||
$(MAKE) -C oneshot
|
||||
$(MAKE) -C fixprobe
|
||||
$(MAKE) -C genreport
|
||||
$(MAKE) -C probe
|
||||
$(MAKE) -C integration
|
||||
|
||||
build:
|
||||
$(MAKE) -C report build
|
||||
$(MAKE) -C xfer build
|
||||
$(MAKE) -C app build
|
||||
$(MAKE) -C bridge build
|
||||
$(MAKE) -C demoprobe build
|
||||
$(MAKE) -C oneshot build
|
||||
$(MAKE) -C fixprobe build
|
||||
$(MAKE) -C probe build
|
||||
$(MAKE) -C genreport build
|
||||
$(MAKE) -C integration build
|
||||
|
||||
release:
|
||||
$(MAKE) -C release
|
||||
|
||||
clean:
|
||||
$(MAKE) -C report clean
|
||||
$(MAKE) -C xfer clean
|
||||
$(MAKE) -C app clean
|
||||
$(MAKE) -C bridge clean
|
||||
$(MAKE) -C demoprobe clean
|
||||
$(MAKE) -C oneshot clean
|
||||
$(MAKE) -C fixprobe clean
|
||||
$(MAKE) -C genreport clean
|
||||
$(MAKE) -C probe clean
|
||||
$(MAKE) -C integration clean
|
||||
3
NOTICE
Normal file
3
NOTICE
Normal file
@@ -0,0 +1,3 @@
|
||||
Weave
|
||||
Copyright 2014-2015 Weaveworks Ltd.
|
||||
This product includes software developed at Weaveworks Ltd.
|
||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Cello
|
||||
|
||||
Network harmony for the 21st century.
|
||||
|
||||
## Component diagram
|
||||
|
||||
```
|
||||
+-----------------------------------+
|
||||
| client |
|
||||
+-----------------------------------+
|
||||
^
|
||||
| 4040, report.RenderableNode
|
||||
|
|
||||
+---------------------------------------------------+
|
||||
| app |
|
||||
+---------------------------------------------------+
|
||||
^ ^ ^ ^
|
||||
| | | | 4030, report.Report
|
||||
| | | |
|
||||
+--------+ +-------+ +----------+ +-----------+
|
||||
| bridge | .-| probe | .-| fixprobe | .-| demoprobe |
|
||||
+--------+ | +-------+ | +----------+ | +-----------+
|
||||
^^^ | | |
|
||||
||| | | |
|
||||
||'-----' | |
|
||||
|'------------------' |
|
||||
'----------------------------------'
|
||||
```
|
||||
158
REST.md
Normal file
158
REST.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Rest structure
|
||||
|
||||
The various REST calls to retrieve topology information.
|
||||
|
||||
# Topologies list
|
||||
|
||||
```
|
||||
GET /api/topology
|
||||
```
|
||||
|
||||
Returns a list of topology descriptions. Each description has the fields:
|
||||
|
||||
- `name` - string; friendly name of the topology
|
||||
- `url` - string; path of the topology
|
||||
- `grouped_url` - string or not existing; path to the 'grouped' version on the
|
||||
topology, if there is one for this topology
|
||||
- `stats` - object; basic statistics about the topology, with these fields:
|
||||
- node_count - int
|
||||
- nonpseudo_node_count - int
|
||||
- edge_count - int
|
||||
|
||||
|
||||
# Full topologies
|
||||
|
||||
```
|
||||
GET /api/topology/:kind
|
||||
```
|
||||
|
||||
Where `:kind` is one of
|
||||
|
||||
- `processpid` - processes by PID (most granular)
|
||||
- `processname` - processes by name
|
||||
- `networkip` - hosts (interfaces) by IP
|
||||
- `networkhost` - hosts by hostname (least granular)
|
||||
|
||||
A GET on a topology URL gives:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": {
|
||||
"nodeID": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`nodeID` is an opaque string. The node object is a map with the fields:
|
||||
|
||||
- `id` - string; same as nodeID
|
||||
- `adjacency` - array of strings; list of node IDs that this node is connected to
|
||||
- `label_major` - string; primary label for this node
|
||||
- `label_minor` - string; extra label information for this node
|
||||
- `pseudo` - bool; true if the node is "deduced" from conditions and has no actual probe; false if this node comes from a probe
|
||||
- `origin` - optional array of strings; origin IDs that the node information comes from
|
||||
|
||||
All node IDs listed in `adjacency` have a corresponding node entry. In other
|
||||
words, the graph is complete.
|
||||
|
||||
If A has B in its `adjacency` list, that means A communicates to B. B may or
|
||||
may not have A in its adjacency list. In other words, the graph is directed.
|
||||
|
||||
Pseudonodes are nodes for which we don't have direct measurements, but we know
|
||||
that other nodes communicate with them. These nodes will always have empty
|
||||
adjacency and origin lists.
|
||||
|
||||
|
||||
# Websocket
|
||||
|
||||
There is also a websocket version of the full topology, which provides
|
||||
continuous updates.
|
||||
|
||||
```
|
||||
GET ws://hostname/api/topology/:kind/ws?t=5s
|
||||
```
|
||||
|
||||
The URL is the same as for an HTTP topology, but with `/ws` appended. The
|
||||
argument `t` can be used to specify the update interval.
|
||||
|
||||
The topology is send as a stream of updates, starting with the difference
|
||||
between the empty topology and the current state.
|
||||
|
||||
```json
|
||||
{
|
||||
"add": [],
|
||||
"update": [],
|
||||
"remove": []
|
||||
}
|
||||
```
|
||||
|
||||
The object has these fields:
|
||||
|
||||
- add - array of objects; a list of node objects which are new since the last update
|
||||
- update - array of objects; a list of node objects which have changed
|
||||
- remove - array of strings; a list of node IDs which are gone
|
||||
|
||||
|
||||
# Nodes
|
||||
|
||||
Given any base topology URL, there is also:
|
||||
|
||||
```
|
||||
GET /api/topology/:kind/:id
|
||||
```
|
||||
|
||||
This returns a single node object. It has fields in addition to those from the
|
||||
corresponding entry from the full topology.
|
||||
|
||||
```json
|
||||
{
|
||||
"node": {}
|
||||
}
|
||||
```
|
||||
|
||||
Extra fields:
|
||||
- aggregate - object - sum of all edge metadata, see 'metadata' below for
|
||||
the fields.
|
||||
|
||||
# Edges
|
||||
|
||||
Details for an edge:
|
||||
|
||||
```
|
||||
GET /api/topology/:kind/:id/:adjacentid
|
||||
```
|
||||
|
||||
This returns the metadata for the undirected edge between :id and :adjacentid,
|
||||
from the point of view of :id. If the edge is not found it'll still return a
|
||||
statuscode 200, but without values.
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
metadata can have these keys:
|
||||
|
||||
- max_conn_count_tcp - int; maximum number of concurrent TCP connections.
|
||||
- egress_bytes - int; total number of bytes.
|
||||
- ingress_bytes - int; total number of bytes.
|
||||
|
||||
Keys will only be set if the statistic is measured by the probes used. If a
|
||||
statistic has the value 0 the key will still be present.
|
||||
|
||||
# Origin
|
||||
|
||||
Origin calls give information about the machine on which the measurements are
|
||||
made.
|
||||
|
||||
```
|
||||
GET /api/origin/:id
|
||||
```
|
||||
|
||||
Origin is a reference to a physical machine that runs a probe. The origin ID
|
||||
comes from the `origin` field in a node object. The returned object is a map
|
||||
with the fields:
|
||||
|
||||
- `hostname` - string; fully qualified domain name of the host
|
||||
- `os` - string; operating system of the host, e.g. "linux"
|
||||
24
app/Makefile
Normal file
24
app/Makefile
Normal file
@@ -0,0 +1,24 @@
|
||||
.PHONY: all vet lint build test clean static
|
||||
|
||||
all: build test vet lint
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
lint:
|
||||
golint .
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
static:
|
||||
go get github.com/mjibson/esc
|
||||
cd ../client && make build && rm -f dist/.htaccess
|
||||
esc -o static.go -prefix ../client/dist ../client/dist
|
||||
${MAKE} build
|
||||
74
app/api_listtopology.go
Normal file
74
app/api_listtopology.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
// APITopologyDesc is returned in a list by the /api/topology handler.
|
||||
type APITopologyDesc struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
GroupedURL string `json:"grouped_url,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Stats topologyStats `json:"stats"`
|
||||
}
|
||||
|
||||
type topologyStats struct {
|
||||
NodeCount int `json:"node_count"`
|
||||
NonpseudoNodeCount int `json:"nonpseudo_node_count"`
|
||||
EdgeCount int `json:"edge_count"`
|
||||
}
|
||||
|
||||
// makeTopologyList returns a handler that yields an APITopologyList.
|
||||
func makeTopologyList(rep Reporter) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
rpt := rep.Report()
|
||||
|
||||
var a []APITopologyDesc
|
||||
for name, def := range topologyRegistry {
|
||||
if strings.HasSuffix(name, "grouped") {
|
||||
continue
|
||||
}
|
||||
|
||||
url := "/api/topology/" + name
|
||||
var groupedURL string
|
||||
if def.hasGrouped {
|
||||
groupedURL = url + "grouped"
|
||||
}
|
||||
|
||||
a = append(a, APITopologyDesc{
|
||||
Name: def.human,
|
||||
URL: url,
|
||||
GroupedURL: groupedURL,
|
||||
Type: def.typ,
|
||||
Stats: stats(def.topologySelecter(rpt).RenderBy(def.MapFunc, false)),
|
||||
})
|
||||
}
|
||||
respondWith(w, http.StatusOK, a)
|
||||
}
|
||||
}
|
||||
|
||||
func stats(r map[string]report.RenderableNode) topologyStats {
|
||||
var (
|
||||
nodes int
|
||||
realNodes int
|
||||
edges int
|
||||
)
|
||||
|
||||
for _, n := range r {
|
||||
nodes++
|
||||
if !n.Pseudo {
|
||||
realNodes++
|
||||
}
|
||||
edges += len(n.Adjacency)
|
||||
}
|
||||
|
||||
return topologyStats{
|
||||
NodeCount: nodes,
|
||||
NonpseudoNodeCount: realNodes,
|
||||
EdgeCount: edges,
|
||||
}
|
||||
}
|
||||
37
app/api_listtopology_test.go
Normal file
37
app/api_listtopology_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAPITopology(t *testing.T) {
|
||||
ts := httptest.NewServer(Router(StaticReport{}))
|
||||
defer ts.Close()
|
||||
|
||||
body := getRawJSON(t, ts, "/api/topology")
|
||||
var topos []APITopologyDesc
|
||||
if err := json.Unmarshal(body, &topos); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
equals(t, 2, len(topos))
|
||||
for _, topo := range topos {
|
||||
is200(t, ts, topo.URL)
|
||||
if topo.GroupedURL != "" {
|
||||
is200(t, ts, topo.GroupedURL)
|
||||
}
|
||||
if have := topo.Type; have == "" {
|
||||
t.Errorf("Type isn't empty: %q", have)
|
||||
}
|
||||
if have := topo.Stats.EdgeCount; have <= 0 {
|
||||
t.Errorf("EdgeCount isn't positive: %d", have)
|
||||
}
|
||||
if have := topo.Stats.NodeCount; have <= 0 {
|
||||
t.Errorf("NodeCount isn't positive: %d", have)
|
||||
}
|
||||
if have := topo.Stats.NonpseudoNodeCount; have <= 0 {
|
||||
t.Errorf("NonpseudoNodeCount isn't positive: %d", have)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
app/api_origin.go
Normal file
55
app/api_origin.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Origin is returned by the /api/origin/* handler. It represents a machine
|
||||
// that runs a probe, i.e. the origin of some data in the system.
|
||||
type Origin struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OS string `json:"os"`
|
||||
Addresses []string `json:"addresses"`
|
||||
LoadOne float64 `json:"load_one"`
|
||||
LoadFive float64 `json:"load_five"`
|
||||
LoadFifteen float64 `json:"load_fifteen"`
|
||||
}
|
||||
|
||||
// makeOriginHandler makes the /api/origin/* handler.
|
||||
func makeOriginHandler(rep Reporter) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vars = mux.Vars(r)
|
||||
nodeID = vars["id"]
|
||||
)
|
||||
origin, ok := getOrigin(rep, nodeID)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
respondWith(w, http.StatusOK, origin)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrigin(rep Reporter, nodeID string) (Origin, bool) {
|
||||
host, ok := rep.Report().HostMetadatas[nodeID]
|
||||
if !ok {
|
||||
return Origin{}, false
|
||||
}
|
||||
|
||||
var addrs []string
|
||||
for _, l := range host.LocalNets {
|
||||
addrs = append(addrs, l.String())
|
||||
}
|
||||
|
||||
return Origin{
|
||||
Hostname: host.Hostname,
|
||||
OS: host.OS,
|
||||
Addresses: addrs,
|
||||
LoadOne: host.LoadOne,
|
||||
LoadFive: host.LoadFive,
|
||||
LoadFifteen: host.LoadFifteen,
|
||||
}, true
|
||||
}
|
||||
35
app/api_origin_test.go
Normal file
35
app/api_origin_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAPIOrigin(t *testing.T) {
|
||||
ts := httptest.NewServer(Router(StaticReport{}))
|
||||
defer ts.Close()
|
||||
|
||||
is404(t, ts, "/api/origin/foobar")
|
||||
|
||||
{
|
||||
// Origin
|
||||
body := getRawJSON(t, ts, "/api/origin/hostA")
|
||||
var o Origin
|
||||
if err := json.Unmarshal(body, &o); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
if want, have := "Linux", o.OS; want != have {
|
||||
t.Errorf("Origin error. Want %v, have %v", want, have)
|
||||
}
|
||||
if want, have := 3.1415, o.LoadOne; want != have {
|
||||
t.Errorf("Origin error. Want %v, have %v", want, have)
|
||||
}
|
||||
if want, have := 2.7182, o.LoadFive; want != have {
|
||||
t.Errorf("Origin error. Want %v, have %v", want, have)
|
||||
}
|
||||
if want, have := 1.6180, o.LoadFifteen; want != have {
|
||||
t.Errorf("Origin error. Want %v, have %v", want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
app/api_report.go
Normal file
13
app/api_report.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Raw report handler
|
||||
func makeRawReportHandler(rep Reporter) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// r.ParseForm()
|
||||
respondWith(w, http.StatusOK, rep.Report())
|
||||
}
|
||||
}
|
||||
24
app/api_report_test.go
Normal file
24
app/api_report_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
func TestAPIReport(t *testing.T) {
|
||||
ts := httptest.NewServer(Router(StaticReport{}))
|
||||
defer ts.Close()
|
||||
|
||||
is404(t, ts, "/api/report/foobar")
|
||||
|
||||
var body = getRawJSON(t, ts, "/api/report")
|
||||
// fmt.Printf("Body: %v\n", string(body))
|
||||
var r report.Report
|
||||
err := json.Unmarshal(body, &r)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
}
|
||||
207
app/api_topology.go
Normal file
207
app/api_topology.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
const (
|
||||
websocketLoop = 1 * time.Second
|
||||
websocketTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// APITopology is returned by the /api/topology/{name} handler.
|
||||
type APITopology struct {
|
||||
Nodes map[string]report.RenderableNode `json:"nodes"`
|
||||
}
|
||||
|
||||
// APINode is returned by the /api/topology/{name}/{id} handler.
|
||||
type APINode struct {
|
||||
Node report.DetailedNode `json:"node"`
|
||||
}
|
||||
|
||||
// APIEdge is returned by the /api/topology/*/*/* handlers.
|
||||
type APIEdge struct {
|
||||
Metadata report.AggregateMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// topologySelecter selects a single topology from a report.
|
||||
type topologySelecter func(r report.Report) report.Topology
|
||||
|
||||
func selectProcess(r report.Report) report.Topology {
|
||||
return r.Process
|
||||
}
|
||||
|
||||
func selectNetwork(r report.Report) report.Topology {
|
||||
return r.Network
|
||||
}
|
||||
|
||||
// makeTopologyHandlers make /api/topology/* handlers.
|
||||
func makeTopologyHandlers(
|
||||
rep Reporter,
|
||||
topo topologySelecter,
|
||||
mapping report.MapFunc,
|
||||
grouped bool,
|
||||
get *mux.Router,
|
||||
base string,
|
||||
) {
|
||||
// Full topology.
|
||||
get.HandleFunc(base, func(w http.ResponseWriter, r *http.Request) {
|
||||
rpt := rep.Report()
|
||||
rendered := topo(rpt).RenderBy(mapping, grouped)
|
||||
t := APITopology{
|
||||
Nodes: rendered,
|
||||
}
|
||||
respondWith(w, http.StatusOK, t)
|
||||
})
|
||||
|
||||
// Websocket for the full topology. This route overlaps with the next.
|
||||
get.HandleFunc(base+"/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
loop := websocketLoop
|
||||
if t := r.Form.Get("t"); t != "" {
|
||||
var err error
|
||||
if loop, err = time.ParseDuration(t); err != nil {
|
||||
respondWith(w, http.StatusBadRequest, t)
|
||||
return
|
||||
}
|
||||
}
|
||||
handleWebsocket(w, r, rep, topo, mapping, grouped, loop)
|
||||
})
|
||||
|
||||
// Individual nodes.
|
||||
get.HandleFunc(base+"/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vars = mux.Vars(r)
|
||||
nodeID = vars["id"]
|
||||
rpt = rep.Report()
|
||||
node, ok = topo(rpt).RenderBy(mapping, grouped)[nodeID]
|
||||
)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
of := func(nodeID string) (Origin, bool) { return getOrigin(rep, nodeID) }
|
||||
respondWith(w, http.StatusOK, APINode{Node: makeDetailed(node, of)})
|
||||
})
|
||||
|
||||
// Individual edges.
|
||||
get.HandleFunc(base+"/{local}/{remote}", func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vars = mux.Vars(r)
|
||||
localID = vars["local"]
|
||||
remoteID = vars["remote"]
|
||||
rpt = rep.Report()
|
||||
metadata = topo(rpt).EdgeMetadata(mapping, grouped, localID, remoteID).Transform()
|
||||
)
|
||||
respondWith(w, http.StatusOK, APIEdge{Metadata: metadata})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(pb): temporary hack
|
||||
func makeDetailed(n report.RenderableNode, of func(string) (Origin, bool)) report.DetailedNode {
|
||||
// A RenderableNode may be the result of merge operation(s), and so may
|
||||
// have multiple origins.
|
||||
origins := []report.Table{}
|
||||
for _, originID := range n.Origin {
|
||||
origin, ok := of(originID)
|
||||
if !ok {
|
||||
origin = unknownOrigin
|
||||
}
|
||||
origins = append(origins, report.Table{
|
||||
Title: "Origin",
|
||||
Numeric: false,
|
||||
Rows: []report.Row{
|
||||
{"Hostname", origin.Hostname, ""},
|
||||
{"Load", fmt.Sprintf("%.2f %.2f %.2f", origin.LoadOne, origin.LoadFive, origin.LoadFifteen), ""},
|
||||
{"OS", origin.OS, ""},
|
||||
//{"Addresses", strings.Join(origin.Addresses, ", "), ""},
|
||||
{"ID", originID, ""},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
tables := []report.Table{}
|
||||
tables = append(tables, report.Table{
|
||||
Title: "Connections",
|
||||
Numeric: true,
|
||||
Rows: []report.Row{
|
||||
{"TCP (max)", strconv.FormatInt(int64(n.Metadata[report.KeyMaxConnCountTCP]), 10), ""},
|
||||
},
|
||||
})
|
||||
tables = append(tables, origins...)
|
||||
|
||||
return report.DetailedNode{
|
||||
ID: n.ID,
|
||||
LabelMajor: n.LabelMajor,
|
||||
LabelMinor: n.LabelMinor,
|
||||
Pseudo: n.Pseudo,
|
||||
Tables: tables,
|
||||
}
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
func handleWebsocket(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
rep Reporter,
|
||||
topo topologySelecter,
|
||||
mapping report.MapFunc,
|
||||
grouped bool,
|
||||
loop time.Duration,
|
||||
) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
// log.Println("Upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
quit := make(chan struct{})
|
||||
go func(c *websocket.Conn) {
|
||||
// Discard all the browser sends us.
|
||||
for {
|
||||
if _, _, err := c.NextReader(); err != nil {
|
||||
close(quit)
|
||||
break
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
|
||||
var previousTopo map[string]report.RenderableNode
|
||||
for {
|
||||
newTopo := topo(rep.Report()).RenderBy(mapping, grouped)
|
||||
diff := report.TopoDiff(previousTopo, newTopo)
|
||||
previousTopo = newTopo
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(websocketTimeout))
|
||||
if err := conn.WriteJSON(diff); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
case <-time.After(loop):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unknownOrigin = Origin{
|
||||
Hostname: "unknown",
|
||||
OS: "unknown",
|
||||
Addresses: []string{},
|
||||
LoadOne: 0.0,
|
||||
LoadFive: 0.0,
|
||||
LoadFifteen: 0.0,
|
||||
}
|
||||
154
app/api_topology_test.go
Normal file
154
app/api_topology_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
func TestAPITopologyApplications(t *testing.T) {
|
||||
ts := httptest.NewServer(Router(StaticReport{}))
|
||||
defer ts.Close()
|
||||
|
||||
is404(t, ts, "/api/topology/applications/foobar")
|
||||
|
||||
{
|
||||
body := getRawJSON(t, ts, "/api/topology/applications")
|
||||
var topo APITopology
|
||||
if err := json.Unmarshal(body, &topo); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
equals(t, 4, len(topo.Nodes))
|
||||
node, ok := topo.Nodes["proc:node-a.local:curl"]
|
||||
if !ok {
|
||||
t.Errorf("missing curl node")
|
||||
}
|
||||
equals(t, report.NewIDList("proc:node-b.local:apache"), node.Adjacency)
|
||||
equals(t, report.NewIDList("hostA"), node.Origin)
|
||||
equals(t, "curl", node.LabelMajor)
|
||||
equals(t, "node-a.local", node.LabelMinor)
|
||||
equals(t, "curl", node.Rank)
|
||||
equals(t, false, node.Pseudo)
|
||||
}
|
||||
|
||||
{
|
||||
// Node detail
|
||||
body := getRawJSON(t, ts, "/api/topology/applications/proc:node-a.local:curl")
|
||||
var node APINode
|
||||
if err := json.Unmarshal(body, &node); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
// TODO(pb): replace
|
||||
}
|
||||
|
||||
{
|
||||
// Edge detail
|
||||
body := getRawJSON(t, ts, "/api/topology/applications/proc:node-a.local:curl/proc:node-b.local:apache")
|
||||
var edge APIEdge
|
||||
if err := json.Unmarshal(body, &edge); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
want := report.AggregateMetadata{
|
||||
"egress_bytes": 24,
|
||||
"ingress_bytes": 0,
|
||||
"max_conn_count_tcp": 401,
|
||||
}
|
||||
if !reflect.DeepEqual(want, edge.Metadata) {
|
||||
t.Errorf("Edge metadata error. Want %v, have %v", want, edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPITopologyHosts(t *testing.T) {
|
||||
ts := httptest.NewServer(Router(StaticReport{}))
|
||||
defer ts.Close()
|
||||
|
||||
is404(t, ts, "/api/topology/hosts/foobar")
|
||||
|
||||
{
|
||||
body := getRawJSON(t, ts, "/api/topology/hosts")
|
||||
var topo APITopology
|
||||
if err := json.Unmarshal(body, &topo); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
|
||||
equals(t, 3, len(topo.Nodes))
|
||||
node, ok := topo.Nodes["host:host-b"]
|
||||
if !ok {
|
||||
t.Errorf("missing host:host-b node")
|
||||
}
|
||||
equals(t, report.NewIDList("host:host-a"), node.Adjacency)
|
||||
equals(t, report.NewIDList("hostB"), node.Origin)
|
||||
equals(t, "host-b", node.LabelMajor)
|
||||
equals(t, "", node.LabelMinor)
|
||||
equals(t, "host-b", node.Rank)
|
||||
equals(t, false, node.Pseudo)
|
||||
}
|
||||
|
||||
{
|
||||
// Node detail
|
||||
body := getRawJSON(t, ts, "/api/topology/hosts/host:host-b")
|
||||
var node APINode
|
||||
if err := json.Unmarshal(body, &node); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
// TODO(pb): replace
|
||||
}
|
||||
|
||||
{
|
||||
// Edge detail
|
||||
body := getRawJSON(t, ts, "/api/topology/hosts/host:host-b/host:host-a")
|
||||
var edge APIEdge
|
||||
if err := json.Unmarshal(body, &edge); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
want := report.AggregateMetadata{
|
||||
"egress_bytes": 0,
|
||||
"ingress_bytes": 12,
|
||||
"max_conn_count_tcp": 16,
|
||||
}
|
||||
if !reflect.DeepEqual(want, edge.Metadata) {
|
||||
t.Errorf("Edge metadata error. Want %v, have %v", want, edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic websocket test
|
||||
func TestAPITopologyWebsocket(t *testing.T) {
|
||||
ts := httptest.NewServer(Router(StaticReport{}))
|
||||
defer ts.Close()
|
||||
|
||||
url := "/api/topology/applications/ws"
|
||||
|
||||
// Not a websocket request:
|
||||
res, _ := checkGet(t, ts, url)
|
||||
if have := res.StatusCode; have != 400 {
|
||||
t.Fatalf("Expected status %d, got %d.", 400, have)
|
||||
}
|
||||
|
||||
// Proper websocket request:
|
||||
ts.URL = "ws" + ts.URL[len("http"):]
|
||||
dialer := &websocket.Dialer{}
|
||||
ws, res, err := dialer.Dial(ts.URL+url, nil)
|
||||
ok(t, err)
|
||||
defer ws.Close()
|
||||
|
||||
if have := res.StatusCode; have != 101 {
|
||||
t.Fatalf("Expected status %d, got %d.", 101, have)
|
||||
}
|
||||
|
||||
_, p, err := ws.ReadMessage()
|
||||
ok(t, err)
|
||||
var d report.Diff
|
||||
if err := json.Unmarshal(p, &d); err != nil {
|
||||
t.Fatalf("JSON parse error: %s", err)
|
||||
}
|
||||
equals(t, 4, len(d.Add))
|
||||
equals(t, 0, len(d.Update))
|
||||
equals(t, 0, len(d.Remove))
|
||||
}
|
||||
94
app/main.go
Normal file
94
app/main.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"log/syslog"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/xfer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
defaultProbes = []string{fmt.Sprintf("localhost:%d", xfer.ProbePort), fmt.Sprintf("scope.weave.local:%d", xfer.ProbePort)}
|
||||
logfile = flag.String("log", "stderr", "stderr, syslog, or filename")
|
||||
probes = flag.String("probes", strings.Join(defaultProbes, ","), "list of probe endpoints, comma separated")
|
||||
batch = flag.Duration("batch", 1*time.Second, "batch interval")
|
||||
window = flag.Duration("window", 15*time.Second, "window")
|
||||
pidfile = flag.String("pidfile", "", "write PID file")
|
||||
listen = flag.String("http.address", ":"+strconv.Itoa(xfer.AppPort), "webserver listen address")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
switch *logfile {
|
||||
case "stderr":
|
||||
break // by default
|
||||
|
||||
case "syslog":
|
||||
w, err := syslog.New(syslog.LOG_INFO, "cello-app")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer w.Close()
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(w)
|
||||
|
||||
default: // file
|
||||
f, err := os.OpenFile(*logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
log.SetOutput(f)
|
||||
}
|
||||
|
||||
if *pidfile != "" {
|
||||
err := ioutil.WriteFile(*pidfile, []byte(fmt.Sprint(os.Getpid())), 0644)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(*pidfile)
|
||||
}
|
||||
|
||||
log.Printf("starting")
|
||||
|
||||
// Collector deals with the probes, and generates merged reports.
|
||||
xfer.MaxBackoff = 10 * time.Second
|
||||
c := xfer.NewCollector(*batch)
|
||||
defer c.Stop()
|
||||
|
||||
r := NewResolver(strings.Split(*probes, ","), c.AddAddress)
|
||||
defer r.Stop()
|
||||
|
||||
lifo := NewReportLIFO(c, *window)
|
||||
defer lifo.Stop()
|
||||
|
||||
http.Handle("/", Router(lifo))
|
||||
irq := interrupt()
|
||||
go func() {
|
||||
log.Printf("listening on %s", *listen)
|
||||
log.Print(http.ListenAndServe(*listen, nil))
|
||||
irq <- syscall.SIGINT
|
||||
}()
|
||||
<-irq
|
||||
log.Printf("shutting down")
|
||||
}
|
||||
|
||||
func interrupt() chan os.Signal {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
return c
|
||||
}
|
||||
135
app/mock_reporter_test.go
Normal file
135
app/mock_reporter_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
// StaticReport is used as know test data in api tests.
|
||||
type StaticReport struct{}
|
||||
|
||||
func (s StaticReport) Report() report.Report {
|
||||
_, localNet, err := net.ParseCIDR("192.168.1.1/24")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
var testReport = report.Report{
|
||||
Process: report.Topology{
|
||||
Adjacency: report.Adjacency{
|
||||
"hostA|;192.168.1.1;12345": []string{";192.168.1.2;80"},
|
||||
"hostA|;192.168.1.1;12346": []string{";192.168.1.2;80"},
|
||||
"hostA|;192.168.1.1;8888": []string{";1.2.3.4;22"},
|
||||
"hostB|;192.168.1.2;80": []string{";192.168.1.1;12345"},
|
||||
},
|
||||
EdgeMetadatas: report.EdgeMetadatas{
|
||||
";192.168.1.1;12345|;192.168.1.2;80": report.EdgeMetadata{
|
||||
WithBytes: true,
|
||||
BytesEgress: 12,
|
||||
BytesIngress: 0,
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: 200,
|
||||
},
|
||||
";192.168.1.1;12346|;192.168.1.2;80": report.EdgeMetadata{
|
||||
WithBytes: true,
|
||||
BytesEgress: 12,
|
||||
BytesIngress: 0,
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: 201,
|
||||
},
|
||||
";192.168.1.1;8888|;1.2.3.4;80": report.EdgeMetadata{
|
||||
WithBytes: true,
|
||||
BytesEgress: 200,
|
||||
BytesIngress: 0,
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: 202,
|
||||
},
|
||||
";192.168.1.2;80|;192.168.1.1;12345": report.EdgeMetadata{
|
||||
WithBytes: true,
|
||||
BytesEgress: 0,
|
||||
BytesIngress: 12,
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: 203,
|
||||
},
|
||||
},
|
||||
NodeMetadatas: report.NodeMetadatas{
|
||||
";192.168.1.1;12345": report.NodeMetadata{
|
||||
"pid": "23128",
|
||||
"name": "curl",
|
||||
"domain": "node-a.local",
|
||||
},
|
||||
";192.168.1.1;12346": report.NodeMetadata{ // <-- same as :12345
|
||||
"pid": "23128",
|
||||
"name": "curl",
|
||||
"domain": "node-a.local",
|
||||
},
|
||||
";192.168.1.1;8888": report.NodeMetadata{
|
||||
"pid": "55100",
|
||||
"name": "ssh",
|
||||
"domain": "node-a.local",
|
||||
},
|
||||
";192.168.1.2;80": report.NodeMetadata{
|
||||
"pid": "215",
|
||||
"name": "apache",
|
||||
"domain": "node-b.local",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Network: report.Topology{
|
||||
Adjacency: report.Adjacency{
|
||||
"hostA|;192.168.1.1": []string{";192.168.1.2", ";1.2.3.4"},
|
||||
"hostB|;192.168.1.2": []string{";192.168.1.1"},
|
||||
},
|
||||
EdgeMetadatas: report.EdgeMetadatas{
|
||||
";192.168.1.1|;192.168.1.2": report.EdgeMetadata{
|
||||
WithBytes: true,
|
||||
BytesEgress: 12,
|
||||
BytesIngress: 0,
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: 14,
|
||||
},
|
||||
";192.168.1.1|;1.2.3.4": report.EdgeMetadata{
|
||||
WithBytes: true,
|
||||
BytesEgress: 200,
|
||||
BytesIngress: 0,
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: 15,
|
||||
},
|
||||
";192.168.1.2|;192.168.1.1": report.EdgeMetadata{
|
||||
WithBytes: true,
|
||||
BytesEgress: 0,
|
||||
BytesIngress: 12,
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: 16,
|
||||
},
|
||||
},
|
||||
NodeMetadatas: report.NodeMetadatas{
|
||||
";192.168.1.1": report.NodeMetadata{
|
||||
"name": "host-a",
|
||||
},
|
||||
";192.168.1.2": report.NodeMetadata{
|
||||
"name": "host-b",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
HostMetadatas: report.HostMetadatas{
|
||||
"hostA": report.HostMetadata{
|
||||
Hostname: "node-a.local",
|
||||
LocalNets: []*net.IPNet{localNet},
|
||||
OS: "Linux",
|
||||
LoadOne: 3.1415,
|
||||
LoadFive: 2.7182,
|
||||
LoadFifteen: 1.6180,
|
||||
},
|
||||
"hostB": report.HostMetadata{
|
||||
Hostname: "node-b.local",
|
||||
LocalNets: []*net.IPNet{localNet},
|
||||
OS: "Linux",
|
||||
},
|
||||
},
|
||||
}
|
||||
return testReport.SquashRemote()
|
||||
}
|
||||
92
app/report_lifo.go
Normal file
92
app/report_lifo.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
// Reporter is something which generates a single 'current' report over a
|
||||
// stream of incoming reports.
|
||||
type Reporter interface {
|
||||
Report() report.Report
|
||||
}
|
||||
|
||||
type timedReport struct {
|
||||
report.Report
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// ReportLIFO keeps a short-term history of reports.
|
||||
type ReportLIFO struct {
|
||||
reports []timedReport
|
||||
requests chan chan report.Report
|
||||
quit chan chan struct{}
|
||||
}
|
||||
|
||||
type reporter interface {
|
||||
Reports() <-chan report.Report
|
||||
}
|
||||
|
||||
// NewReportLIFO collects reports up to a certain age.
|
||||
func NewReportLIFO(r reporter, maxAge time.Duration) *ReportLIFO {
|
||||
l := ReportLIFO{
|
||||
reports: []timedReport{},
|
||||
requests: make(chan chan report.Report),
|
||||
quit: make(chan chan struct{}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case report := <-r.Reports():
|
||||
// Incoming report from the collecter.
|
||||
report = report.SquashRemote() // TODO?: make this a CLI argument.
|
||||
tr := timedReport{
|
||||
Timestamp: time.Now(),
|
||||
Report: report,
|
||||
}
|
||||
l.reports = append(l.reports, tr)
|
||||
l.reports = cleanOld(l.reports, time.Now().Add(-maxAge))
|
||||
|
||||
case req := <-l.requests:
|
||||
// Request for the current report.
|
||||
report := report.NewReport()
|
||||
for _, r := range l.reports {
|
||||
report.Merge(r.Report)
|
||||
}
|
||||
req <- report
|
||||
|
||||
case q := <-l.quit:
|
||||
close(q)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return &l
|
||||
}
|
||||
|
||||
// Stop shuts down the monitor.
|
||||
func (r *ReportLIFO) Stop() {
|
||||
q := make(chan struct{})
|
||||
r.quit <- q
|
||||
<-q
|
||||
}
|
||||
|
||||
// Report returns the latest report.
|
||||
func (r *ReportLIFO) Report() report.Report {
|
||||
req := make(chan report.Report)
|
||||
r.requests <- req
|
||||
return <-req
|
||||
}
|
||||
|
||||
func cleanOld(reports []timedReport, threshold time.Time) []timedReport {
|
||||
res := make([]timedReport, 0, len(reports))
|
||||
for _, tr := range reports {
|
||||
if tr.Timestamp.Before(threshold) {
|
||||
continue
|
||||
}
|
||||
res = append(res, tr)
|
||||
}
|
||||
return res
|
||||
}
|
||||
86
app/resolver.go
Normal file
86
app/resolver.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Resolver periodically tries to resolve the IP addresses for a given
|
||||
// set of hostnames.
|
||||
type Resolver struct {
|
||||
quit chan struct{}
|
||||
add func(string)
|
||||
peers []peer
|
||||
}
|
||||
|
||||
type peer struct {
|
||||
hostname string
|
||||
port string
|
||||
}
|
||||
|
||||
// NewResolver starts a new resolver that periodically
|
||||
// tries to resolve peers and the calls add() with all the
|
||||
// resolved IPs. It explictiy supports hostnames which
|
||||
// resolve to multiple IPs; it will repeatedly call
|
||||
// add with the same IP, expecting the target to dedupe.
|
||||
func NewResolver(peers []string, add func(string)) Resolver {
|
||||
resolver := Resolver{
|
||||
quit: make(chan struct{}),
|
||||
add: add,
|
||||
peers: prepareNames(peers),
|
||||
}
|
||||
|
||||
go resolver.loop()
|
||||
return resolver
|
||||
}
|
||||
|
||||
func prepareNames(peers []string) []peer {
|
||||
var results []peer
|
||||
for _, p := range peers {
|
||||
hostname, port, err := net.SplitHostPort(p)
|
||||
if err != nil {
|
||||
log.Printf("invalid address %s: %v", p, err)
|
||||
continue
|
||||
}
|
||||
results = append(results, peer{hostname, port})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (r Resolver) loop() {
|
||||
r.resolveHosts()
|
||||
for {
|
||||
tick := time.Tick(1 * time.Minute)
|
||||
select {
|
||||
case <-tick:
|
||||
r.resolveHosts()
|
||||
case <-r.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r Resolver) resolveHosts() {
|
||||
for _, peer := range r.peers {
|
||||
addrs, err := net.LookupIP(peer.hostname)
|
||||
if err != nil {
|
||||
log.Printf("lookup %s: %v", peer.hostname, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
// For now, ignore IPv6
|
||||
if addr.To4() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.add(net.JoinHostPort(addr.String(), peer.port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop this resolver.
|
||||
func (r Resolver) Stop() {
|
||||
r.quit <- struct{}{}
|
||||
}
|
||||
52
app/router.go
Normal file
52
app/router.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// HTTP server routing table
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
// Router gives of the HTTP dispatcher. It will always use the embedded HTML
|
||||
// resources.
|
||||
func Router(c Reporter) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
get := router.Methods("GET").Subrouter()
|
||||
get.HandleFunc("/api/topology", makeTopologyList(c))
|
||||
for name, def := range topologyRegistry {
|
||||
makeTopologyHandlers(
|
||||
c,
|
||||
def.topologySelecter,
|
||||
def.MapFunc,
|
||||
false, // not grouped
|
||||
get,
|
||||
"/api/topology/"+name,
|
||||
)
|
||||
if def.hasGrouped {
|
||||
makeTopologyHandlers(
|
||||
c,
|
||||
def.topologySelecter,
|
||||
def.MapFunc,
|
||||
true, // grouped
|
||||
get,
|
||||
"/api/topology/"+name+"grouped",
|
||||
)
|
||||
}
|
||||
}
|
||||
get.HandleFunc("/api/origin/{id}", makeOriginHandler(c))
|
||||
get.HandleFunc("/api/report", makeRawReportHandler(c))
|
||||
get.PathPrefix("/").Handler(http.FileServer(FS(false))) // everything else is static
|
||||
return router
|
||||
}
|
||||
|
||||
var topologyRegistry = map[string]struct {
|
||||
human string
|
||||
topologySelecter
|
||||
report.MapFunc
|
||||
hasGrouped bool
|
||||
typ string
|
||||
}{
|
||||
"applications": {"Applications", selectProcess, report.ProcessName, true, "Process"},
|
||||
"hosts": {"Hosts", selectNetwork, report.NetworkHostname, false, "Network"},
|
||||
}
|
||||
121
app/scope_test.go
Normal file
121
app/scope_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// assert fails the test if the condition is false.
|
||||
func assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
|
||||
if !condition {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
fmt.Printf("%s:%d: "+msg+"\n", append([]interface{}{filepath.Base(file), line}, v...)...)
|
||||
tb.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// ok fails the test if an err is not nil.
|
||||
func ok(tb testing.TB, err error) {
|
||||
if err != nil {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
fmt.Printf("%s:%d: unexpected error: %s\n", filepath.Base(file), line, err.Error())
|
||||
tb.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// equals fails the test if exp is not equal to act.
|
||||
func equals(tb testing.TB, exp, act interface{}) {
|
||||
if !reflect.DeepEqual(exp, act) {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
fmt.Printf("%s:%d: expected: %#v got: %#v\n", filepath.Base(file), line, exp, act)
|
||||
tb.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// checkGet does a GET and returns the response and the body
|
||||
func checkGet(t *testing.T, ts *httptest.Server, path string) (*http.Response, []byte) {
|
||||
return checkRequest(t, ts, "GET", path, nil)
|
||||
}
|
||||
|
||||
// checkRequest does a 'method'-request (e.g. 'GET') and returns the response and the body
|
||||
func checkRequest(t *testing.T, ts *httptest.Server, method, path string, body []byte) (*http.Response, []byte) {
|
||||
fullPath := ts.URL + path
|
||||
var bodyReader io.Reader
|
||||
if len(body) > 0 {
|
||||
bodyReader = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, fullPath, bodyReader)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting %s: %s", path, err)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting %s: %s", path, err)
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("%s body read error: %s", path, err)
|
||||
}
|
||||
return res, body
|
||||
}
|
||||
|
||||
// getRawJSON GETs a file, checks it is JSON, and returns the non-parsed body
|
||||
func getRawJSON(t *testing.T, ts *httptest.Server, path string) []byte {
|
||||
|
||||
res, body := checkGet(t, ts, path)
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("Expected status %d, got %d. Path: %s", 200, res.StatusCode, path)
|
||||
}
|
||||
|
||||
foundCtype := res.Header.Get("content-type")
|
||||
if foundCtype != "application/json" {
|
||||
t.Errorf("Wrong Content-type for JSON: %s", foundCtype)
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
t.Errorf("No response body")
|
||||
}
|
||||
// fmt.Printf("Body: %s", body)
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
// is200 GETs path and verifies the status code. Returns the body
|
||||
func is200(t *testing.T, ts *httptest.Server, path string) []byte {
|
||||
res, body := checkGet(t, ts, path)
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("Expected status %d, got %d. Path: %s", 200, res.StatusCode, path)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// is404 GETs path and verifies it returns a 404 status code. Returns the body
|
||||
func is404(t *testing.T, ts *httptest.Server, path string) []byte {
|
||||
res, body := checkGet(t, ts, path)
|
||||
if res.StatusCode != 404 {
|
||||
t.Fatalf("Expected status %d, got %d", 404, res.StatusCode)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// is400 GETs path and verifies it returns a 400 status code. Returns the body
|
||||
func is400(t *testing.T, ts *httptest.Server, path string) []byte {
|
||||
res, body := checkGet(t, ts, path)
|
||||
if res.StatusCode != 400 {
|
||||
t.Fatalf("Expected status %d, got %d", 400, res.StatusCode)
|
||||
}
|
||||
return body
|
||||
}
|
||||
13
app/server_helpers.go
Normal file
13
app/server_helpers.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func respondWith(w http.ResponseWriter, code int, response interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
17
app/site_test.go
Normal file
17
app/site_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Basic site layout tests.
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test site
|
||||
func TestSite(t *testing.T) {
|
||||
ts := httptest.NewServer(Router(StaticReport{}))
|
||||
defer ts.Close()
|
||||
|
||||
is200(t, ts, "/")
|
||||
is200(t, ts, "/index.html")
|
||||
is404(t, ts, "/index.html/foobar")
|
||||
}
|
||||
6113
app/static.go
Normal file
6113
app/static.go
Normal file
File diff suppressed because it is too large
Load Diff
19
bridge/Makefile
Normal file
19
bridge/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
.PHONY: all vet lint build test clean
|
||||
|
||||
all: build test vet lint
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
lint:
|
||||
golint .
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
204
bridge/main.go
Normal file
204
bridge/main.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
"github.com/weaveworks/scope/scope/xfer"
|
||||
)
|
||||
|
||||
const (
|
||||
trafficTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
listen = flag.String("listen", ":"+strconv.Itoa(xfer.ProbePort), "listen address")
|
||||
probes = flag.String("probes", "", "list of all initial probes, comma separated")
|
||||
batch = flag.Duration("batch", 1*time.Second, "batch interval")
|
||||
version = flag.Bool("version", false, "print version number and exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) != 0 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *version {
|
||||
//fmt.Printf("%s\n", probe.Version)
|
||||
return
|
||||
}
|
||||
|
||||
if *probes == "" {
|
||||
log.Fatal("no probes given via -probes")
|
||||
}
|
||||
|
||||
log.Printf("starting")
|
||||
|
||||
fixedAddresses := strings.Split(*probes, ",")
|
||||
|
||||
// Collector deals with the probes, and generates a single merged report
|
||||
// every second.
|
||||
c := xfer.NewCollector(*batch)
|
||||
c.AddAddresses(fixedAddresses)
|
||||
defer c.Stop()
|
||||
|
||||
publisher, err := xfer.NewTCPPublisher(*listen)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
log.Printf("listening on %s\n", *listen)
|
||||
|
||||
var fixedIPs []string
|
||||
for _, a := range fixedAddresses {
|
||||
if addr, _, err := net.SplitHostPort(a); err == nil {
|
||||
fixedIPs = append(fixedIPs, addr)
|
||||
}
|
||||
}
|
||||
go discover(c, publisher, fixedIPs)
|
||||
|
||||
<-interrupt()
|
||||
|
||||
log.Printf("shutting down")
|
||||
}
|
||||
|
||||
func interrupt() chan os.Signal {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
return c
|
||||
}
|
||||
|
||||
type collector interface {
|
||||
Reports() <-chan report.Report
|
||||
RemoveAddress(string)
|
||||
AddAddress(string)
|
||||
}
|
||||
|
||||
type publisher xfer.Publisher
|
||||
|
||||
// makeAvoid makes a map with IPs we don't want to consider in discover(). It
|
||||
// is the set of IPs which the bridge is configured to connect to, and the all
|
||||
// the IPs from the local interfaces.
|
||||
func makeAvoid(fixed []string) map[string]struct{} {
|
||||
avoid := map[string]struct{}{}
|
||||
|
||||
// Don't discover fixed probes. This way we'll never remove them.
|
||||
for _, a := range fixed {
|
||||
avoid[a] = struct{}{}
|
||||
}
|
||||
|
||||
// Don't go Ouroboros.
|
||||
if localNets, err := net.InterfaceAddrs(); err == nil {
|
||||
for _, n := range localNets {
|
||||
if net, ok := n.(*net.IPNet); ok {
|
||||
avoid[net.IP.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return avoid
|
||||
}
|
||||
|
||||
// discover reads reports from a collector and republishes them on the
|
||||
// publisher, while scanning the reports for IPs to connect to. Only addresses
|
||||
// in the network topology of the report are considered. IPs listed in fixed
|
||||
// are always skipped.
|
||||
func discover(c collector, p publisher, fixed []string) {
|
||||
lastSeen := map[string]time.Time{}
|
||||
|
||||
avoid := makeAvoid(fixed)
|
||||
|
||||
for r := range c.Reports() {
|
||||
// log.Printf("got a report")
|
||||
p.Publish(r)
|
||||
|
||||
var (
|
||||
now = time.Now()
|
||||
localNets = r.LocalNets()
|
||||
)
|
||||
|
||||
for _, adjacent := range r.Network.Adjacency {
|
||||
for _, a := range adjacent {
|
||||
ip := report.AddressIP(a) // address id -> IP
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
addr := ip.String()
|
||||
if _, ok := avoid[addr]; ok {
|
||||
continue
|
||||
}
|
||||
// log.Printf("potential address: %v (via %s)", addr, src)
|
||||
if _, ok := lastSeen[addr]; !ok {
|
||||
if interestingAddress(localNets, addr) {
|
||||
log.Printf("discovery %v: potential probe address", addr)
|
||||
c.AddAddress(addressToDial(addr))
|
||||
} else {
|
||||
log.Printf("discovery %v: non-probe address", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// We always add addr to lastSeen[], even if it's a non-local
|
||||
// address. This way they are part of the normal timeout logic,
|
||||
// and we won't analyze the address again until it re-appears
|
||||
// after a timeout.
|
||||
lastSeen[addr] = now
|
||||
}
|
||||
}
|
||||
|
||||
for addr, last := range lastSeen {
|
||||
if now.Sub(last) > trafficTimeout {
|
||||
// Timeout can be for a non-local address, which we didn't
|
||||
// connect to. In that case the RemoveAddress() call won't do
|
||||
// anything.
|
||||
log.Printf("discovery %v: traffic timeout", addr)
|
||||
delete(lastSeen, addr)
|
||||
c.RemoveAddress(addressToDial(addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// interestingAddress tells whether the address is a local and normal address,
|
||||
// which we want to try to connect to.
|
||||
func interestingAddress(localNets []*net.IPNet, addr string) bool {
|
||||
if addr == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// The address is expected to be an IPv{4,6} address.
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out localhost, broadcast, and other non-connectable addresses.
|
||||
if !validateRemoteAddr(ip) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only connect to addresses we know are localnet.
|
||||
for _, n := range localNets {
|
||||
if n.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// addressToDial formats an IP address so we can pass it on to Dial().
|
||||
func addressToDial(address string) string {
|
||||
// return fmt.Sprintf("[%s]:%d", addr, probePort)
|
||||
return net.JoinHostPort(address, strconv.Itoa(xfer.ProbePort))
|
||||
}
|
||||
71
bridge/net.go
Normal file
71
bridge/net.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func validateRemoteAddr(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
if ip.IsInterfaceLocalMulticast() {
|
||||
return false
|
||||
}
|
||||
if ip.IsLinkLocalMulticast() {
|
||||
return false
|
||||
}
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
return false
|
||||
}
|
||||
if ip.IsLoopback() {
|
||||
return false
|
||||
}
|
||||
if ip.IsMulticast() {
|
||||
return false
|
||||
}
|
||||
if ip.IsUnspecified() {
|
||||
return false
|
||||
}
|
||||
if isBroadcasty(ip) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isBroadcasty(ip net.IP) bool {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
if ip4.Equal(net.IPv4bcast) {
|
||||
return true
|
||||
}
|
||||
if ip4.Equal(net.IPv4allsys) {
|
||||
return true
|
||||
}
|
||||
if ip4.Equal(net.IPv4allrouter) {
|
||||
return true
|
||||
}
|
||||
if ip4.Equal(net.IPv4zero) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if ip.Equal(net.IPv6zero) {
|
||||
return true
|
||||
}
|
||||
if ip.Equal(net.IPv6unspecified) {
|
||||
return true
|
||||
}
|
||||
if ip.Equal(net.IPv6loopback) {
|
||||
return true
|
||||
}
|
||||
if ip.Equal(net.IPv6interfacelocalallnodes) {
|
||||
return true
|
||||
}
|
||||
if ip.Equal(net.IPv6linklocalallnodes) {
|
||||
return true
|
||||
}
|
||||
if ip.Equal(net.IPv6linklocalallrouters) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
41
bridge/net_test.go
Normal file
41
bridge/net_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidRemoteAddr(t *testing.T) {
|
||||
for input, expected := range map[string]bool{
|
||||
"localhost": false,
|
||||
|
||||
"127.0.0.1": false, // should be same as loopback
|
||||
"127.1.2.3": false, // 127.0.0.0/8 is all loopback
|
||||
"0.0.0.0": false,
|
||||
"224.0.0.1": false, // all systems
|
||||
"224.0.0.2": false, // all routers
|
||||
"224.0.0.22": false,
|
||||
"255.255.255.255": false, // broadcast
|
||||
"1.2.3.4": true,
|
||||
|
||||
"::": false, // unspecified
|
||||
"0:0:0:0:0:0:0:0": false, // unspecified (alternate form)
|
||||
"b8:27:eb:a4:bf:6e": false,
|
||||
"fe80::1240:f3ff:fe80:5474": false, // loopback
|
||||
"fe80::1": false, // loopback
|
||||
"::1": false, // loopback
|
||||
"0:0:0:0:0:0:0:1": false, // loopback (alternate form)
|
||||
"2001:db8::1:0:0:1": true, // valid
|
||||
|
||||
// http://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml
|
||||
"FF01:0:0:0:0:0:0:1": false, // Node-local all-nodes
|
||||
// "FF01:0:0:0:0:0:0:2": false, // Node-local all-routers, isn't spec'd
|
||||
// in package net/ip
|
||||
"FF02:0:0:0:0:0:0:1": false, // Link-local all-nodes
|
||||
"FF02:0:0:0:0:0:0:2": false, // Link-local all-routers
|
||||
} {
|
||||
if got := validateRemoteAddr(net.ParseIP(input)); expected != got {
|
||||
t.Errorf("%s: expected %v, got %v", input, expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
29
client/.editorconfig
Normal file
29
client/.editorconfig
Normal file
@@ -0,0 +1,29 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# Change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# We recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[package.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[bower.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
1
client/.gitattributes
vendored
Normal file
1
client/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
6
client/.gitignore
vendored
Normal file
6
client/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.tmp
|
||||
.sass-cache
|
||||
bower_components
|
||||
test/bower_components
|
||||
21
client/.jshintrc
Normal file
21
client/.jshintrc
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"esnext": true,
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 4,
|
||||
"latedef": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"strict": true,
|
||||
"trailing": true,
|
||||
"smarttabs": true,
|
||||
"jquery": true
|
||||
}
|
||||
3
client/.yo-rc.json
Normal file
3
client/.yo-rc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"generator-mocha": {}
|
||||
}
|
||||
11
client/Makefile
Normal file
11
client/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
.PHONY: run builddep build
|
||||
run:
|
||||
npm start
|
||||
|
||||
builddep:
|
||||
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-pngquant
|
||||
|
||||
build:
|
||||
rm -rf ./dist/
|
||||
npm install
|
||||
gulp build
|
||||
32
client/README.md
Normal file
32
client/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Web app template
|
||||
|
||||
> Template based on [Yeoman](http://yeoman.io) generator [generator-gulp-webapp](https://github.com/yeoman/generator-gulp-webapp) using [gulp](http://gulpjs.com/) for the build process
|
||||
|
||||
## Features
|
||||
|
||||
Please see our [gulpfile.js](gulpfile.js) for up to date information on what we support.
|
||||
|
||||
* CSS Autoprefixing
|
||||
* Built-in preview server with livereload
|
||||
* Automagically compile Sass
|
||||
* Automagically lint your scripts
|
||||
* Awesome image optimization
|
||||
* Automagically wire-up dependencies installed with [Bower](http://bower.io) *(when `gulp watch` or `gulp wiredep`)*
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Install: `npm install`
|
||||
- Run `gulp` for building to the `dist` directory and `gulp serve` for preview
|
||||
|
||||
|
||||
#### Third-Party Dependencies
|
||||
|
||||
*(HTML/CSS/JS/Images/etc)*
|
||||
|
||||
To install dependencies, run `bower install --save package-name` to get the files, then add a `script` or `style` tag to your `index.html` or another appropriate place.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[BSD license](http://opensource.org/licenses/bsd-license.php)
|
||||
663
client/app/.htaccess
Normal file
663
client/app/.htaccess
Normal file
@@ -0,0 +1,663 @@
|
||||
# Apache Server Configs v2.2.0 | MIT License
|
||||
# https://github.com/h5bp/server-configs-apache
|
||||
|
||||
# (!) Using `.htaccess` files slows down Apache, therefore, if you have access
|
||||
# to the main server config file (usually called `httpd.conf`), you should add
|
||||
# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html.
|
||||
|
||||
# ##############################################################################
|
||||
# # CROSS-ORIGIN RESOURCE SHARING (CORS) #
|
||||
# ##############################################################################
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Cross-domain AJAX requests |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Allow cross-origin AJAX requests.
|
||||
# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
|
||||
# http://enable-cors.org/
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# Header set Access-Control-Allow-Origin "*"
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | CORS-enabled images |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Send the CORS header for images when browsers request it.
|
||||
# https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image
|
||||
# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
|
||||
# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
|
||||
|
||||
<IfModule mod_setenvif.c>
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(cur|gif|ico|jpe?g|png|svgz?|webp)$">
|
||||
SetEnvIf Origin ":" IS_CORS
|
||||
Header set Access-Control-Allow-Origin "*" env=IS_CORS
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Web fonts access |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Allow access to web fonts from all domains.
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(eot|otf|tt[cf]|woff)$">
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
|
||||
# ##############################################################################
|
||||
# # ERRORS #
|
||||
# ##############################################################################
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | 404 error prevention for non-existing redirected folders |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Prevent Apache from returning a 404 error as the result of a rewrite
|
||||
# when the directory with the same name does not exist.
|
||||
# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews
|
||||
# http://www.webmasterworld.com/apache/3808792.htm
|
||||
|
||||
Options -MultiViews
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Custom error messages / pages |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Customize what Apache returns to the client in case of an error.
|
||||
# http://httpd.apache.org/docs/current/mod/core.html#errordocument
|
||||
|
||||
ErrorDocument 404 /404.html
|
||||
|
||||
|
||||
# ##############################################################################
|
||||
# # INTERNET EXPLORER #
|
||||
# ##############################################################################
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Better website experience |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Force Internet Explorer to render pages in the highest available mode
|
||||
# in the various cases when it may not.
|
||||
# http://hsivonen.iki.fi/doctype/ie-mode.pdf
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
Header set X-UA-Compatible "IE=edge"
|
||||
# `mod_headers` cannot match based on the content-type, however, this
|
||||
# header should be send only for HTML pages and not for the other resources
|
||||
<FilesMatch "\.(appcache|atom|crx|css|cur|eot|f4[abpv]|flv|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|tt[cf]|vcf|vtt|webapp|web[mp]|woff|xml|xpi)$">
|
||||
Header unset X-UA-Compatible
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Cookie setting from iframes |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Allow cookies to be set from iframes in Internet Explorer.
|
||||
# http://msdn.microsoft.com/en-us/library/ms537343.aspx
|
||||
# http://www.w3.org/TR/2000/CR-P3P-20001215/
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\""
|
||||
# </IfModule>
|
||||
|
||||
|
||||
# ##############################################################################
|
||||
# # MIME TYPES AND ENCODING #
|
||||
# ##############################################################################
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Proper MIME types for all files |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
<IfModule mod_mime.c>
|
||||
|
||||
# Audio
|
||||
AddType audio/mp4 m4a f4a f4b
|
||||
AddType audio/ogg oga ogg opus
|
||||
|
||||
# Data interchange
|
||||
AddType application/json json map
|
||||
AddType application/ld+json jsonld
|
||||
|
||||
# JavaScript
|
||||
# Normalize to standard type.
|
||||
# http://tools.ietf.org/html/rfc4329#section-7.2
|
||||
AddType application/javascript js
|
||||
|
||||
# Video
|
||||
AddType video/mp4 f4v f4p m4v mp4
|
||||
AddType video/ogg ogv
|
||||
AddType video/webm webm
|
||||
AddType video/x-flv flv
|
||||
|
||||
# Web fonts
|
||||
AddType application/font-woff woff
|
||||
AddType application/vnd.ms-fontobject eot
|
||||
|
||||
# Browsers usually ignore the font MIME types and simply sniff the bytes
|
||||
# to figure out the font type.
|
||||
# http://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern
|
||||
|
||||
# Chrome however, shows a warning if any other MIME types are used for
|
||||
# the following fonts.
|
||||
|
||||
AddType application/x-font-ttf ttc ttf
|
||||
AddType font/opentype otf
|
||||
|
||||
# Make SVGZ fonts work on the iPad.
|
||||
# https://twitter.com/FontSquirrel/status/14855840545
|
||||
AddType image/svg+xml svgz
|
||||
AddEncoding gzip svgz
|
||||
|
||||
# Other
|
||||
AddType application/octet-stream safariextz
|
||||
AddType application/x-chrome-extension crx
|
||||
AddType application/x-opera-extension oex
|
||||
AddType application/x-web-app-manifest+json webapp
|
||||
AddType application/x-xpinstall xpi
|
||||
AddType application/xml atom rdf rss xml
|
||||
AddType image/webp webp
|
||||
AddType image/x-icon cur
|
||||
AddType text/cache-manifest appcache manifest
|
||||
AddType text/vtt vtt
|
||||
AddType text/x-component htc
|
||||
AddType text/x-vcard vcf
|
||||
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | UTF-8 encoding |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Use UTF-8 encoding for anything served as `text/html` or `text/plain`.
|
||||
AddDefaultCharset utf-8
|
||||
|
||||
# Force UTF-8 for certain file formats.
|
||||
<IfModule mod_mime.c>
|
||||
AddCharset utf-8 .atom .css .js .json .jsonld .rss .vtt .webapp .xml
|
||||
</IfModule>
|
||||
|
||||
|
||||
# ##############################################################################
|
||||
# # URL REWRITES #
|
||||
# ##############################################################################
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Rewrite engine |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Turn on the rewrite engine and enable the `FollowSymLinks` option (this is
|
||||
# necessary in order for the following directives to work).
|
||||
|
||||
# If your web host doesn't allow the `FollowSymlinks` option, you may need to
|
||||
# comment it out and use `Options +SymLinksIfOwnerMatch`, but be aware of the
|
||||
# performance impact.
|
||||
# http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks
|
||||
|
||||
# Also, some cloud hosting services require `RewriteBase` to be set.
|
||||
# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
Options +FollowSymlinks
|
||||
# Options +SymLinksIfOwnerMatch
|
||||
RewriteEngine On
|
||||
# RewriteBase /
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Suppressing / Forcing the `www.` at the beginning of URLs |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# The same content should never be available under two different URLs,
|
||||
# especially not with and without `www.` at the beginning. This can cause
|
||||
# SEO problems (duplicate content), and therefore, you should choose one
|
||||
# of the alternatives and redirect the other one.
|
||||
|
||||
# By default `Option 1` (no `www.`) is activated.
|
||||
# http://no-www.org/faq.php?q=class_b
|
||||
|
||||
# If you would prefer to use `Option 2`, just comment out all the lines
|
||||
# from `Option 1` and uncomment the ones from `Option 2`.
|
||||
|
||||
# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME!
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# Option 1: rewrite www.example.com → example.com
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteCond %{HTTPS} !=on
|
||||
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||
RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
|
||||
</IfModule>
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# Option 2: rewrite example.com → www.example.com
|
||||
|
||||
# Be aware that the following might not be a good idea if you use "real"
|
||||
# subdomains for certain parts of your website.
|
||||
|
||||
# <IfModule mod_rewrite.c>
|
||||
# RewriteCond %{HTTPS} !=on
|
||||
# RewriteCond %{HTTP_HOST} !^www\. [NC]
|
||||
# RewriteCond %{SERVER_ADDR} !=127.0.0.1
|
||||
# RewriteCond %{SERVER_ADDR} !=::1
|
||||
# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
|
||||
# </IfModule>
|
||||
|
||||
|
||||
# ##############################################################################
|
||||
# # SECURITY #
|
||||
# ##############################################################################
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Clickjacking |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Protect website against clickjacking.
|
||||
|
||||
# The example below sends the `X-Frame-Options` response header with the value
|
||||
# `DENY`, informing browsers not to display the web page content in any frame.
|
||||
|
||||
# This might not be the best setting for everyone. You should read about the
|
||||
# other two possible values for `X-Frame-Options`: `SAMEORIGIN` & `ALLOW-FROM`.
|
||||
# http://tools.ietf.org/html/rfc7034#section-2.1
|
||||
|
||||
# Keep in mind that while you could send the `X-Frame-Options` header for all
|
||||
# of your site’s pages, this has the potential downside that it forbids even
|
||||
# non-malicious framing of your content (e.g.: when users visit your site using
|
||||
# a Google Image Search results page).
|
||||
|
||||
# Nonetheless, you should ensure that you send the `X-Frame-Options` header for
|
||||
# all pages that allow a user to make a state changing operation (e.g: pages
|
||||
# that contain one-click purchase links, checkout or bank-transfer confirmation
|
||||
# pages, pages that make permanent configuration changes, etc.).
|
||||
|
||||
# Sending the `X-Frame-Options` header can also protect your website against
|
||||
# more than just clickjacking attacks: https://cure53.de/xfo-clickjacking.pdf.
|
||||
|
||||
# http://tools.ietf.org/html/rfc7034
|
||||
# http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx
|
||||
# https://www.owasp.org/index.php/Clickjacking
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# Header set X-Frame-Options "DENY"
|
||||
# <FilesMatch "\.(appcache|atom|crx|css|cur|eot|f4[abpv]|flv|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|tt[cf]|vcf|vtt|webapp|web[mp]|woff|xml|xpi)$">
|
||||
# Header unset X-Frame-Options
|
||||
# </FilesMatch>
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Content Security Policy (CSP) |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Mitigate the risk of cross-site scripting and other content-injection attacks.
|
||||
|
||||
# This can be done by setting a `Content Security Policy` which whitelists
|
||||
# trusted sources of content for your website.
|
||||
|
||||
# The example header below allows ONLY scripts that are loaded from the current
|
||||
# site's origin (no inline scripts, no CDN, etc). This almost certainly won't
|
||||
# work as-is for your site!
|
||||
|
||||
# For more details on how to craft a reasonable policy for your site, read:
|
||||
# http://html5rocks.com/en/tutorials/security/content-security-policy (or the
|
||||
# specification: http://w3.org/TR/CSP). Also, to make things easier, you can
|
||||
# use an online CSP header generator such as: http://cspisawesome.com/.
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# Header set Content-Security-Policy "script-src 'self'; object-src 'self'"
|
||||
# <FilesMatch "\.(appcache|atom|crx|css|cur|eot|f4[abpv]|flv|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|tt[cf]|vcf|vtt|webapp|web[mp]|woff|xml|xpi)$">
|
||||
# Header unset Content-Security-Policy
|
||||
# </FilesMatch>
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | File access |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Block access to directories without a default document.
|
||||
# You should leave the following uncommented, as you shouldn't allow anyone to
|
||||
# surf through every directory on your server (which may includes rather private
|
||||
# places such as the CMS's directories).
|
||||
|
||||
<IfModule mod_autoindex.c>
|
||||
Options -Indexes
|
||||
</IfModule>
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# Block access to hidden files and directories.
|
||||
# This includes directories used by version control systems such as Git and SVN.
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteCond %{SCRIPT_FILENAME} -d [OR]
|
||||
RewriteCond %{SCRIPT_FILENAME} -f
|
||||
RewriteRule "(^|/)\." - [F]
|
||||
</IfModule>
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
# Block access to files that can expose sensitive information.
|
||||
|
||||
# By default, block access to backup and source files that may be left by some
|
||||
# text editors and can pose a security risk when anyone has access to them.
|
||||
# http://feross.org/cmsploit/
|
||||
|
||||
# IMPORTANT: Update the `<FilesMatch>` regular expression from below to include
|
||||
# any files that might end up on your production server and can expose sensitive
|
||||
# information about your website. These files may include: configuration files,
|
||||
# files that contain metadata about the project (e.g.: project dependencies),
|
||||
# build scripts, etc..
|
||||
|
||||
<FilesMatch "(^#.*#|\.(bak|config|dist|fla|in[ci]|log|psd|sh|sql|sw[op])|~)$">
|
||||
|
||||
# Apache < 2.3
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
Satisfy All
|
||||
</IfModule>
|
||||
|
||||
# Apache ≥ 2.3
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
|
||||
</FilesMatch>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Reducing MIME-type security risks |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Prevent some browsers from MIME-sniffing the response.
|
||||
|
||||
# This reduces exposure to drive-by download attacks and should be enable
|
||||
# especially if the web server is serving user uploaded content, content
|
||||
# that could potentially be treated by the browser as executable.
|
||||
|
||||
# http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx
|
||||
# http://msdn.microsoft.com/en-us/library/ie/gg622941.aspx
|
||||
# http://mimesniff.spec.whatwg.org/
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# Header set X-Content-Type-Options "nosniff"
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Reflected Cross-Site Scripting (XSS) attacks |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# (1) Try to re-enable the Cross-Site Scripting (XSS) filter built into the
|
||||
# most recent web browsers.
|
||||
#
|
||||
# The filter is usually enabled by default, but in some cases it may be
|
||||
# disabled by the user. However, in Internet Explorer for example, it can
|
||||
# be re-enabled just by sending the `X-XSS-Protection` header with the
|
||||
# value of `1`.
|
||||
#
|
||||
# (2) Prevent web browsers from rendering the web page if a potential reflected
|
||||
# (a.k.a non-persistent) XSS attack is detected by the filter.
|
||||
#
|
||||
# By default, if the filter is enabled and browsers detect a reflected
|
||||
# XSS attack, they will attempt to block the attack by making the smallest
|
||||
# possible modifications to the returned web page.
|
||||
#
|
||||
# Unfortunately, in some browsers (e.g.: Internet Explorer), this default
|
||||
# behavior may allow the XSS filter to be exploited, thereby, it's better
|
||||
# to tell browsers to prevent the rendering of the page altogether, instead
|
||||
# of attempting to modify it.
|
||||
#
|
||||
# http://hackademix.net/2009/11/21/ies-xss-filter-creates-xss-vulnerabilities
|
||||
#
|
||||
# IMPORTANT: Do not rely on the XSS filter to prevent XSS attacks! Ensure that
|
||||
# you are taking all possible measures to prevent XSS attacks, the most obvious
|
||||
# being: validating and sanitizing your site's inputs.
|
||||
#
|
||||
# http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx
|
||||
# http://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx
|
||||
# https://www.owasp.org/index.php/Cross-site_Scripting_%28XSS%29
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# # (1) (2)
|
||||
# Header set X-XSS-Protection "1; mode=block"
|
||||
# <FilesMatch "\.(appcache|atom|crx|css|cur|eot|f4[abpv]|flv|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|tt[cf]|vcf|vtt|webapp|web[mp]|woff|xml|xpi)$">
|
||||
# Header unset X-XSS-Protection
|
||||
# </FilesMatch>
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Secure Sockets Layer (SSL) |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Rewrite secure requests properly in order to prevent SSL certificate warnings.
|
||||
# E.g.: prevent `https://www.example.com` when your certificate only allows
|
||||
# `https://secure.example.com`.
|
||||
|
||||
# <IfModule mod_rewrite.c>
|
||||
# RewriteCond %{SERVER_PORT} !^443
|
||||
# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L]
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | HTTP Strict Transport Security (HSTS) |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Force client-side SSL redirection.
|
||||
|
||||
# If a user types `example.com` in his browser, the above rule will redirect
|
||||
# him to the secure version of the site. That still leaves a window of
|
||||
# opportunity (the initial HTTP connection) for an attacker to downgrade or
|
||||
# redirect the request.
|
||||
|
||||
# The following header ensures that browser will ONLY connect to your server
|
||||
# via HTTPS, regardless of what the users type in the address bar.
|
||||
|
||||
# http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14#section-6.1
|
||||
# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/
|
||||
|
||||
# IMPORTANT: Remove the `includeSubDomains` optional directive if the subdomains
|
||||
# are not using HTTPS.
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# Header set Strict-Transport-Security "max-age=16070400; includeSubDomains"
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Server software information |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Avoid displaying the exact Apache version number, the description of the
|
||||
# generic OS-type and the information about Apache's compiled-in modules.
|
||||
|
||||
# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`!
|
||||
|
||||
# ServerTokens Prod
|
||||
|
||||
|
||||
# ##############################################################################
|
||||
# # WEB PERFORMANCE #
|
||||
# ##############################################################################
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Compression |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
|
||||
# Force compression for mangled headers.
|
||||
# http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping
|
||||
<IfModule mod_setenvif.c>
|
||||
<IfModule mod_headers.c>
|
||||
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
|
||||
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
# Compress all output labeled with one of the following MIME-types
|
||||
# (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
|
||||
# and can remove the `<IfModule mod_filter.c>` and `</IfModule>` lines
|
||||
# as `AddOutputFilterByType` is still in the core directives).
|
||||
<IfModule mod_filter.c>
|
||||
AddOutputFilterByType DEFLATE application/atom+xml \
|
||||
application/javascript \
|
||||
application/json \
|
||||
application/ld+json \
|
||||
application/rss+xml \
|
||||
application/vnd.ms-fontobject \
|
||||
application/x-font-ttf \
|
||||
application/x-web-app-manifest+json \
|
||||
application/xhtml+xml \
|
||||
application/xml \
|
||||
font/opentype \
|
||||
image/svg+xml \
|
||||
image/x-icon \
|
||||
text/css \
|
||||
text/html \
|
||||
text/plain \
|
||||
text/x-component \
|
||||
text/xml
|
||||
</IfModule>
|
||||
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Content transformations |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Prevent mobile network providers from modifying the website's content.
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5.
|
||||
|
||||
# <IfModule mod_headers.c>
|
||||
# Header set Cache-Control "no-transform"
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | ETags |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Remove `ETags` as resources are sent with far-future expires headers.
|
||||
# http://developer.yahoo.com/performance/rules.html#etags.
|
||||
|
||||
# `FileETag None` doesn't work in all cases.
|
||||
<IfModule mod_headers.c>
|
||||
Header unset ETag
|
||||
</IfModule>
|
||||
|
||||
FileETag None
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Expires headers |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# The following expires headers are set pretty far in the future. If you
|
||||
# don't control versioning with filename-based cache busting, consider
|
||||
# lowering the cache time for resources such as style sheets and JavaScript
|
||||
# files to something like one week.
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
|
||||
ExpiresActive on
|
||||
ExpiresDefault "access plus 1 month"
|
||||
|
||||
# CSS
|
||||
ExpiresByType text/css "access plus 1 year"
|
||||
|
||||
# Data interchange
|
||||
ExpiresByType application/json "access plus 0 seconds"
|
||||
ExpiresByType application/ld+json "access plus 0 seconds"
|
||||
ExpiresByType application/xml "access plus 0 seconds"
|
||||
ExpiresByType text/xml "access plus 0 seconds"
|
||||
|
||||
# Favicon (cannot be renamed!) and cursor images
|
||||
ExpiresByType image/x-icon "access plus 1 week"
|
||||
|
||||
# HTML components (HTCs)
|
||||
ExpiresByType text/x-component "access plus 1 month"
|
||||
|
||||
# HTML
|
||||
ExpiresByType text/html "access plus 0 seconds"
|
||||
|
||||
# JavaScript
|
||||
ExpiresByType application/javascript "access plus 1 year"
|
||||
|
||||
# Manifest files
|
||||
ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
|
||||
ExpiresByType text/cache-manifest "access plus 0 seconds"
|
||||
|
||||
# Media
|
||||
ExpiresByType audio/ogg "access plus 1 month"
|
||||
ExpiresByType image/gif "access plus 1 month"
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType video/mp4 "access plus 1 month"
|
||||
ExpiresByType video/ogg "access plus 1 month"
|
||||
ExpiresByType video/webm "access plus 1 month"
|
||||
|
||||
# Web feeds
|
||||
ExpiresByType application/atom+xml "access plus 1 hour"
|
||||
ExpiresByType application/rss+xml "access plus 1 hour"
|
||||
|
||||
# Web fonts
|
||||
ExpiresByType application/font-woff "access plus 1 month"
|
||||
ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
|
||||
ExpiresByType application/x-font-ttf "access plus 1 month"
|
||||
ExpiresByType font/opentype "access plus 1 month"
|
||||
ExpiresByType image/svg+xml "access plus 1 month"
|
||||
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | Filename-based cache busting |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# If you're not using a build process to manage your filename version revving,
|
||||
# you might want to consider enabling the following directives to route all
|
||||
# requests such as `/css/style.12345.css` to `/css/style.css`.
|
||||
|
||||
# To understand why this is important and a better idea than `*.css?v231`, read:
|
||||
# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring
|
||||
|
||||
# <IfModule mod_rewrite.c>
|
||||
# RewriteCond %{REQUEST_FILENAME} !-f
|
||||
# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpe?g|gif)$ $1.$3 [L]
|
||||
# </IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# | File concatenation |
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Allow concatenation from within specific style sheets and JavaScript files.
|
||||
|
||||
# e.g.:
|
||||
#
|
||||
# If you have the following content in a file
|
||||
#
|
||||
# <!--#include file="libs/jquery.js" -->
|
||||
# <!--#include file="plugins/jquery.timer.js" -->
|
||||
#
|
||||
# Apache will replace it with the content from the specified files.
|
||||
|
||||
# <IfModule mod_include.c>
|
||||
# <FilesMatch "\.combined\.js$">
|
||||
# Options +Includes
|
||||
# AddOutputFilterByType INCLUDES application/javascript application/json
|
||||
# SetOutputFilter INCLUDES
|
||||
# </FilesMatch>
|
||||
# <FilesMatch "\.combined\.css$">
|
||||
# Options +Includes
|
||||
# AddOutputFilterByType INCLUDES text/css
|
||||
# SetOutputFilter INCLUDES
|
||||
# </FilesMatch>
|
||||
# </IfModule>
|
||||
157
client/app/404.html
Normal file
157
client/app/404.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Page Not Found :(</title>
|
||||
<style>
|
||||
::-moz-selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html {
|
||||
padding: 30px 10px;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
color: #737373;
|
||||
background: #f0f0f0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
input {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 500px;
|
||||
_width: 500px;
|
||||
padding: 30px 20px 50px;
|
||||
border: 1px solid #b3b3b3;
|
||||
border-radius: 4px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 10px;
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 1.5em 0 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0 0 0 40px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 380px;
|
||||
_width: 380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* google search */
|
||||
|
||||
#goog-fixurl ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#goog-fixurl form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#goog-wm-qt,
|
||||
#goog-wm-sb {
|
||||
border: 1px solid #bbb;
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
vertical-align: top;
|
||||
color: #444;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#goog-wm-qt {
|
||||
width: 220px;
|
||||
height: 20px;
|
||||
padding: 5px;
|
||||
margin: 5px 10px 0 0;
|
||||
box-shadow: inset 0 1px 1px #ccc;
|
||||
}
|
||||
|
||||
#goog-wm-sb {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
margin: 5px 0 0;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
background-color: #f5f5f5;
|
||||
background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
*overflow: visible;
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
#goog-wm-sb:hover,
|
||||
#goog-wm-sb:focus {
|
||||
border-color: #aaa;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
#goog-wm-qt:hover,
|
||||
#goog-wm-qt:focus {
|
||||
border-color: #105cb6;
|
||||
outline: 0;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
input::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Not found <span>:(</span></h1>
|
||||
<p>Sorry, but the page you were trying to view does not exist.</p>
|
||||
<p>It looks like this was the result of either:</p>
|
||||
<ul>
|
||||
<li>a mistyped address</li>
|
||||
<li>an out-of-date link</li>
|
||||
</ul>
|
||||
<script>
|
||||
var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
|
||||
</script>
|
||||
<script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
client/app/favicon.ico
Normal file
BIN
client/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 741 B |
24
client/app/images/logo.svg
Normal file
24
client/app/images/logo.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="100%" height="100%" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<path fill="#D65906" d="M128,32c52.935,0,96,43.065,96,96s-43.065,96-96,96s-96-43.065-96-96S75.065,32,128,32 M128,8
|
||||
C61.726,8,8,61.726,8,128s53.726,120,120,120s120-53.726,120-120S194.274,8,128,8L128,8z"/>
|
||||
<path fill="#D65906" d="M128,53c-41.422,0-75,33.579-75,75s33.578,75,75,75c41.421,0,75-33.579,75-75S169.421,53,128,53z
|
||||
M156.932,163.617c-1.012,1.29-2.353,2.451-4.02,3.483c-1.668,1.032-3.584,1.936-5.748,2.71s-4.398,1.42-6.7,1.936
|
||||
c-2.304,0.517-4.597,0.903-6.88,1.162c-2.283,0.258-4.397,0.387-6.343,0.387c-5.876,0-11.355-0.982-16.438-2.948
|
||||
c-5.083-1.966-9.491-4.845-13.222-8.636c-3.732-3.793-6.671-8.458-8.815-13.997s-3.216-11.882-3.216-19.028
|
||||
c0-7.227,1.062-13.688,3.187-19.388c2.124-5.697,5.092-10.512,8.904-14.442c3.812-3.932,8.348-6.939,13.609-9.023
|
||||
c5.261-2.084,11.028-3.127,17.302-3.127c4.964,0,9.28,0.308,12.954,0.924c3.672,0.615,6.721,1.459,9.143,2.531
|
||||
s4.229,2.342,5.42,3.811c1.191,1.47,1.787,3.059,1.787,4.766c0,1.151-0.238,2.154-0.715,3.008s-1.102,1.559-1.877,2.113
|
||||
c-0.773,0.557-1.637,0.964-2.59,1.222c-0.953,0.259-1.906,0.388-2.859,0.388c-0.557,0-1.111-0.011-1.668-0.03
|
||||
c-0.557-0.021-1.093-0.109-1.607-0.269c-1.51-0.396-2.949-0.783-4.318-1.161c-1.37-0.377-2.72-0.704-4.05-0.982
|
||||
c-1.331-0.278-2.661-0.507-3.991-0.685c-1.33-0.179-2.709-0.269-4.139-0.269c-2.979,0-5.787,0.446-8.428,1.341
|
||||
c-2.641,0.893-4.935,2.471-6.879,4.734c-1.945,2.264-3.484,5.33-4.616,9.202c-1.132,3.871-1.698,8.805-1.698,14.8
|
||||
c0,4.606,0.456,8.746,1.37,12.419c0.913,3.673,2.272,6.789,4.08,9.351s4.07,4.526,6.79,5.896c2.719,1.369,5.887,2.055,9.5,2.055
|
||||
c1.469,0,2.998-0.158,4.586-0.477c1.587-0.318,3.217-0.725,4.884-1.222c1.667-0.495,3.364-1.062,5.092-1.696
|
||||
c1.728-0.636,3.444-1.271,5.151-1.906c0.517-0.159,1.152-0.238,1.906-0.238c0.992,0,1.896,0.188,2.711,0.565
|
||||
c0.812,0.377,1.518,0.884,2.114,1.52c0.595,0.635,1.052,1.379,1.37,2.233c0.317,0.854,0.476,1.757,0.476,2.71
|
||||
C158.45,160.907,157.943,162.326,156.932,163.617z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
33
client/app/index.html
Normal file
33
client/app/index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Weave Scope</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- build:css styles/vendor.css -->
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- build:css styles/main.css -->
|
||||
<link rel="stylesheet" href="styles/main.css">
|
||||
<!-- endbuild -->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 10]>
|
||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
<div class="wrap">
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
|
||||
<!-- @ifdef DEBUG -->
|
||||
<script>window.WS_URL = 'ws://localhost:4040';</script>
|
||||
<!-- @endif -->
|
||||
|
||||
<!-- build:js scripts/bundle.js -->
|
||||
<script src="scripts/bundle.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
||||
3
client/app/robots.txt
Normal file
3
client/app/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# robotstxt.org/
|
||||
|
||||
User-agent: *
|
||||
76
client/app/scripts/actions/app-actions.js
Normal file
76
client/app/scripts/actions/app-actions.js
Normal file
@@ -0,0 +1,76 @@
|
||||
var AppDispatcher = require('../dispatcher/app-dispatcher');
|
||||
var ActionTypes = require('../constants/action-types');
|
||||
|
||||
module.exports = {
|
||||
clickCloseDetails: function() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_DETAILS
|
||||
});
|
||||
RouterUtils.updateRoute();
|
||||
},
|
||||
|
||||
clickNode: function(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
nodeId: nodeId
|
||||
});
|
||||
RouterUtils.updateRoute();
|
||||
WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId());
|
||||
},
|
||||
|
||||
clickTopology: function(topologyId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId: topologyId
|
||||
});
|
||||
RouterUtils.updateRoute();
|
||||
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
|
||||
},
|
||||
|
||||
clickTopologyMode: function(mode) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_TOPOLOGY_MODE,
|
||||
mode: mode
|
||||
});
|
||||
RouterUtils.updateRoute();
|
||||
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
|
||||
},
|
||||
|
||||
hitEsc: function() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.HIT_ESC_KEY
|
||||
});
|
||||
RouterUtils.updateRoute();
|
||||
},
|
||||
|
||||
receiveNodeDetails: function(details) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_NODE_DETAILS,
|
||||
details: details
|
||||
});
|
||||
},
|
||||
|
||||
receiveTopologies: function(topologies) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
topologies: topologies
|
||||
});
|
||||
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
|
||||
WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId());
|
||||
},
|
||||
|
||||
route: function(state) {
|
||||
AppDispatcher.dispatch({
|
||||
state: state,
|
||||
type: ActionTypes.ROUTE_TOPOLOGY
|
||||
});
|
||||
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
|
||||
WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId());
|
||||
}
|
||||
};
|
||||
|
||||
// breaking circular deps
|
||||
|
||||
var RouterUtils = require('../utils/router-utils');
|
||||
var WebapiUtils = require('../utils/web-api-utils');
|
||||
var AppStore = require('../stores/app-store');
|
||||
25
client/app/scripts/actions/topology-actions.js
Normal file
25
client/app/scripts/actions/topology-actions.js
Normal file
@@ -0,0 +1,25 @@
|
||||
var AppDispatcher = require('../dispatcher/app-dispatcher');
|
||||
var ActionTypes = require('../constants/action-types');
|
||||
|
||||
module.exports = {
|
||||
enterNode: function(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.ENTER_NODE,
|
||||
nodeId: nodeId
|
||||
});
|
||||
},
|
||||
|
||||
leaveNode: function(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.LEAVE_NODE,
|
||||
nodeId: nodeId
|
||||
});
|
||||
},
|
||||
|
||||
receiveNodesDelta: function(delta) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta: delta
|
||||
});
|
||||
}
|
||||
};
|
||||
302
client/app/scripts/charts/node-explorer.js
Normal file
302
client/app/scripts/charts/node-explorer.js
Normal file
@@ -0,0 +1,302 @@
|
||||
var d3 = require('d3');
|
||||
var _ = require('lodash');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var Node = require('./node');
|
||||
|
||||
|
||||
function determineAllEdges(nodes, allNodes) {
|
||||
var edges = [],
|
||||
edgeIds = {},
|
||||
nodeIds = _.pluck(nodes, 'id');
|
||||
|
||||
_.each(nodes, function(node) {
|
||||
var nodeId = node.id;
|
||||
|
||||
_.each(node.adjacency, function(adjacent) {
|
||||
var edge = [nodeId, adjacent],
|
||||
edgeId = edge.join('-');
|
||||
|
||||
if (!edgeIds[edgeId] && _.contains(nodeIds, nodeId) && _.contains(nodeIds, adjacent)) {
|
||||
edges.push({
|
||||
id: edgeId,
|
||||
value: 5,
|
||||
source: allNodes[edge[0]],
|
||||
target: allNodes[edge[1]]
|
||||
});
|
||||
edgeIds[edgeId] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
function getAdjacentNodes(nodes, rootNodeIds) {
|
||||
var rootNodes = _.compact(_.map(rootNodeIds, function(nodeId) {
|
||||
return nodes[nodeId];
|
||||
}));
|
||||
|
||||
var allAdjacentNodeIds = _.union(_.flatten(_.map(rootNodes, function(node) {
|
||||
return node.adjacency;
|
||||
})), rootNodeIds);
|
||||
|
||||
return _.compact(_.map(allAdjacentNodeIds, function(nodeId) {
|
||||
return nodes[nodeId];
|
||||
}));
|
||||
}
|
||||
|
||||
function getAdjacentEdges(nodes, root) {
|
||||
return _.map(root.adjacency, function(nodeId) {
|
||||
var edge = [root.id, nodeId],
|
||||
edgeId = edge.join('-');
|
||||
|
||||
return {
|
||||
id: edgeId,
|
||||
value: 10,
|
||||
source: nodes[edge[0]],
|
||||
target: nodes[edge[1]]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function id(d) {
|
||||
return d.id;
|
||||
}
|
||||
|
||||
function degree(d) {
|
||||
return d.adjacency ? d.adjacency.length : 1;
|
||||
}
|
||||
|
||||
function getChildren(node, allNodes) {
|
||||
return _.map(node.adjacency, function(nodeId) {
|
||||
return allNodes[nodeId];
|
||||
});
|
||||
}
|
||||
|
||||
function dblclick(d) {
|
||||
d.fixed = false;
|
||||
}
|
||||
|
||||
function nodeExplorer() {
|
||||
|
||||
var color = d3.scale.category20();
|
||||
|
||||
var pie = d3.layout.pie()
|
||||
.value(degree);
|
||||
|
||||
var radius = d3.scale.sqrt()
|
||||
.range([1, 8]);
|
||||
|
||||
var drag = d3.behavior.drag();
|
||||
|
||||
var dispatcher = new EventEmitter();
|
||||
|
||||
var nodeLocations = {};
|
||||
|
||||
var width, height;
|
||||
|
||||
function circleRadius() {
|
||||
return width / 4;
|
||||
}
|
||||
|
||||
function radialLayout(centerNode, nodes, radius) {
|
||||
var slices = pie(_.sortBy(_.filter(nodes, function(node) {
|
||||
return !node.layedout && _.contains(centerNode.adjacency, node.id);
|
||||
}), 'id'));
|
||||
|
||||
_.each(slices, function(slice) {
|
||||
var previousXY = nodeLocations[slice.data.id];
|
||||
|
||||
slice.data.x = centerNode.x + circleRadius() * Math.sin((slice.startAngle + slice.endAngle) / 2);
|
||||
slice.data.y = centerNode.y + circleRadius() * Math.cos((slice.startAngle + slice.endAngle) / 2);
|
||||
slice.data.layedout = true;
|
||||
// nodeLocations[slice.data.id] = [slice.data.forceX, slice.data.forceY];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function chart(selection) {
|
||||
selection.each(function(data) {
|
||||
var allNodes = data.nodes;
|
||||
var root = allNodes[data.root];
|
||||
var expandedNodeIds = data.expandedNodes;
|
||||
|
||||
var centerX = width / 2;
|
||||
var centerY = height / 2;
|
||||
|
||||
var nodes = getAdjacentNodes(allNodes, expandedNodeIds);
|
||||
|
||||
if (root) {
|
||||
var previousXY = nodeLocations[root.id];
|
||||
root.x = previousXY ? previousXY[0] : centerX;
|
||||
root.y = previousXY ? previousXY[1] : centerY;
|
||||
root.layedout = true;
|
||||
|
||||
_.each(nodes, function(node) {
|
||||
if (node.id !== root.id) {
|
||||
node.layedout = false;
|
||||
}
|
||||
});
|
||||
|
||||
_.each(expandedNodeIds, function(nodeId, i) {
|
||||
var centerNode = allNodes[nodeId];
|
||||
if (centerNode) {
|
||||
radialLayout(centerNode, nodes, i ? circleRadius() / 4 : circleRadius());
|
||||
}
|
||||
});
|
||||
|
||||
if (nodes.length == 0) {
|
||||
nodes.push(root);
|
||||
}
|
||||
}
|
||||
|
||||
var edges = determineAllEdges(nodes, allNodes);
|
||||
|
||||
// Select the svg element, if it exists.
|
||||
var svg = d3.select(this).selectAll("svg").data([data]);
|
||||
|
||||
// Otherwise, create the skeletal chart.
|
||||
var gEnter = svg.enter().append("svg")
|
||||
.attr('width', "100%")
|
||||
.attr('height', "100%");
|
||||
|
||||
gEnter.append('g')
|
||||
.classed('links', true);
|
||||
gEnter.append('g')
|
||||
.classed('nodes', true);
|
||||
|
||||
var link = svg.select('.links').selectAll(".link")
|
||||
.data(edges, id);
|
||||
link.exit()
|
||||
.remove()
|
||||
link.enter().append("line")
|
||||
.attr("class", "link")
|
||||
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
|
||||
|
||||
var node = svg.select('.nodes').selectAll(".node")
|
||||
.data(nodes, id);
|
||||
|
||||
node.exit()
|
||||
.remove();
|
||||
|
||||
gEnter = node.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.on("dblclick", dblclick)
|
||||
.on('click', function(d) {
|
||||
if (d3.event.defaultPrevented) {
|
||||
return; // click suppressed
|
||||
}
|
||||
dispatcher.emit('node.click', d);
|
||||
})
|
||||
.call(drag);
|
||||
|
||||
gEnter.append("circle")
|
||||
.attr('class', 'outer')
|
||||
.style("fill", function(d) {
|
||||
return color(d.label_major);
|
||||
})
|
||||
.attr("r", function(d) {
|
||||
return radius(degree(d)) + 4;
|
||||
});
|
||||
|
||||
gEnter.append("circle")
|
||||
.style("fill", function(d) {
|
||||
return color(d.label_major);
|
||||
})
|
||||
.attr("r", function(d) {
|
||||
return radius(degree(d));
|
||||
});
|
||||
|
||||
gEnter.append("text")
|
||||
.attr('class', 'label-major')
|
||||
.attr("dy", "-.25em")
|
||||
.text(function(d) { return d.label_major; });
|
||||
|
||||
gEnter.append("text")
|
||||
.attr('class', 'label-minor')
|
||||
.attr("dy", ".75em")
|
||||
.text(function(d) { return d.label_minor; });
|
||||
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
node
|
||||
.classed('node-root', function(d) {
|
||||
return d.id === root.id;
|
||||
})
|
||||
.classed('node-expanded', function(d) {
|
||||
return _.contains(expandedNodeIds, d.id);
|
||||
})
|
||||
.classed('node-leaf', function(d) {
|
||||
return _.size(d.adjacency) === 0;
|
||||
});
|
||||
|
||||
function updatePositions() {
|
||||
link.attr("x1", function(d) { return d.source.x; })
|
||||
.attr("y1", function(d) { return d.source.y; })
|
||||
.attr("x2", function(d) { return d.target.x; })
|
||||
.attr("y2", function(d) { return d.target.y; });
|
||||
|
||||
node.attr("transform", function(d) {
|
||||
return "translate(" + d.x + "," + d.y + ")";
|
||||
});
|
||||
|
||||
node.selectAll('text')
|
||||
.attr("dx", function(d) { return d.x > centerX ? radius(degree(d)) + 6 : - radius(degree(d)) - 6})
|
||||
.attr("text-anchor", function(d) { return d.x > centerX ? "start" : "end"; });
|
||||
}
|
||||
|
||||
var density = Math.sqrt(nodes.length / (width * height));
|
||||
|
||||
drag.on('drag', function(d) {
|
||||
d.x = d3.event.x;
|
||||
d.y = d3.event.y;
|
||||
updatePositions();
|
||||
});
|
||||
|
||||
updatePositions();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
chart.create = function(el, state) {
|
||||
d3.select(el)
|
||||
.datum(state)
|
||||
.call(chart);
|
||||
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.update = _.throttle(function(el, state) {
|
||||
d3.select(el)
|
||||
.datum(state)
|
||||
.call(chart);
|
||||
|
||||
return chart;
|
||||
}, 500);
|
||||
|
||||
chart.on = function(event, callback) {
|
||||
dispatcher.on(event, callback);
|
||||
};
|
||||
|
||||
chart.teardown = function() {
|
||||
force.on('tick', null);
|
||||
};
|
||||
|
||||
chart.width = function(_) {
|
||||
if (!arguments.length) return width;
|
||||
width = _;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.height = function(_) {
|
||||
if (!arguments.length) return height;
|
||||
height = _;
|
||||
return chart;
|
||||
};
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
module.exports = nodeExplorer;
|
||||
63
client/app/scripts/charts/node.js
Normal file
63
client/app/scripts/charts/node.js
Normal file
@@ -0,0 +1,63 @@
|
||||
var _ = require('lodash');
|
||||
var React = require('react');
|
||||
var tweenState = require('react-tween-state');
|
||||
|
||||
var NodeColorMixin = require('../mixins/node-color-mixin');
|
||||
|
||||
var Node = React.createClass({
|
||||
mixins: [
|
||||
NodeColorMixin,
|
||||
tweenState.Mixin
|
||||
],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// initial node position when rendered the first time
|
||||
this.setState({
|
||||
x: this.props.dx,
|
||||
y: this.props.dy
|
||||
});
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// animate node transition to next position
|
||||
this.tweenState('x', {
|
||||
easing: tweenState.easingTypes.easeInOutQuad,
|
||||
duration: 500,
|
||||
endValue: nextProps.dx
|
||||
});
|
||||
this.tweenState('y', {
|
||||
easing: tweenState.easingTypes.easeInOutQuad,
|
||||
duration: 500,
|
||||
endValue: nextProps.dy
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var transform = "translate(" + this.getTweeningValue('x') + "," + this.getTweeningValue('y') + ")";
|
||||
var scale = this.props.scale;
|
||||
var textOffsetX = 0;
|
||||
var textOffsetY = scale(0.5) + 18;
|
||||
var textAngle = _.isUndefined(this.props.angle) ? 0 : -1 * (this.props.angle * 180 / Math.PI - 90);
|
||||
var color = this.getNodeColor(this.props.label);
|
||||
var className = this.props.highlighted ? "node highlighted" : "node";
|
||||
|
||||
return (
|
||||
<g className={className} transform={transform} onClick={this.props.onClick} id={this.props.id}>
|
||||
<circle r={scale(0.5)} className="border" stroke={color}></circle>
|
||||
<circle r={scale(0.45)} className="shadow"></circle>
|
||||
<circle r={Math.max(2, scale(0.125))} className="node"></circle>
|
||||
<text className="node-label" textAnchor="middle" x={textOffsetX} y={textOffsetY}>{this.props.label}</text>
|
||||
<text className="node-sublabel" textAnchor="middle" x={textOffsetX} y={textOffsetY + 17}>{this.props.subLabel}</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Node;
|
||||
234
client/app/scripts/charts/nodes-chart.js
Normal file
234
client/app/scripts/charts/nodes-chart.js
Normal file
@@ -0,0 +1,234 @@
|
||||
var _ = require('lodash');
|
||||
var d3 = require('d3');
|
||||
var React = require('react');
|
||||
|
||||
var NodesLayout = require('./nodes-layout');
|
||||
var Node = require('./node');
|
||||
|
||||
var MAX_NODES = 100;
|
||||
var MARGINS = {
|
||||
top: 120,
|
||||
left: 40,
|
||||
right: 40,
|
||||
bottom: 0
|
||||
};
|
||||
|
||||
var line = d3.svg.line()
|
||||
.interpolate("basis")
|
||||
.x(function(d) { return d.x; })
|
||||
.y(function(d) { return d.y; });
|
||||
|
||||
var NodesChart = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
nodes: {},
|
||||
edges: {},
|
||||
nodeScale: 1,
|
||||
translate: "0,0",
|
||||
scale: 1,
|
||||
hasZoomed: false
|
||||
};
|
||||
},
|
||||
|
||||
initNodes: function(topology, prevNodes) {
|
||||
var centerX = this.props.width / 2;
|
||||
var centerY = this.props.height / 2;
|
||||
var nodes = {};
|
||||
|
||||
_.each(topology, function(node, id) {
|
||||
nodes[id] = prevNodes[id] || {};
|
||||
_.defaults(nodes[id], {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
textAnchor: 'start'
|
||||
});
|
||||
_.assign(nodes[id], {
|
||||
id: id,
|
||||
label: node.label_major,
|
||||
subLabel: node.label_minor,
|
||||
degree: _.size(node.adjacency)
|
||||
});
|
||||
}, this);
|
||||
|
||||
return nodes;
|
||||
},
|
||||
|
||||
initEdges: function(topology, nodes) {
|
||||
var edges = {};
|
||||
|
||||
_.each(topology, function(node) {
|
||||
_.each(node.adjacency, function(adjacent) {
|
||||
var edge = [node.id, adjacent],
|
||||
edgeId = edge.join('-');
|
||||
|
||||
if (!edges[edgeId]) {
|
||||
var source = nodes[edge[0]];
|
||||
var target = nodes[edge[1]];
|
||||
|
||||
if(!source || !target) {
|
||||
console.error("Missing edge node", edge[0], source, edge[1], target);
|
||||
}
|
||||
|
||||
edges[edgeId] = {
|
||||
id: edgeId,
|
||||
value: 1,
|
||||
source: source,
|
||||
target: target
|
||||
};
|
||||
}
|
||||
});
|
||||
}, this);
|
||||
|
||||
return edges;
|
||||
},
|
||||
|
||||
getNodes: function(nodes, scale) {
|
||||
return _.map(nodes, function (node) {
|
||||
var highlighted = _.includes(this.props.highlightedNodes, node.id);
|
||||
return (
|
||||
<Node
|
||||
highlighted={highlighted}
|
||||
onClick={this.props.onNodeClick}
|
||||
key={node.id}
|
||||
id={node.id}
|
||||
label={node.label}
|
||||
subLabel={node.subLabel}
|
||||
scale={scale}
|
||||
dx={node.x}
|
||||
dy={node.y}
|
||||
/>
|
||||
);
|
||||
}, this);
|
||||
},
|
||||
|
||||
getEdges: function(edges, scale) {
|
||||
return _.map(edges, function(edge) {
|
||||
return (
|
||||
<path className="link" d={line(edge.points)} key={edge.id} />
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
getTopologyFingerprint: function(topology) {
|
||||
var nodes = _.keys(topology).sort();
|
||||
var fingerprint = [];
|
||||
|
||||
_.each(topology, function(node) {
|
||||
fingerprint.push(node.id);
|
||||
if (node.adjacency) {
|
||||
fingerprint.push(node.adjacency.join(','));
|
||||
}
|
||||
});
|
||||
return fingerprint.join(';');
|
||||
},
|
||||
|
||||
updateGraphState: function(props) {
|
||||
var nodes = this.initNodes(props.nodes, this.state.nodes);
|
||||
var edges = this.initEdges(props.nodes, nodes);
|
||||
|
||||
var expanse = Math.min(props.height, props.width);
|
||||
var nodeSize = expanse / 2;
|
||||
var n = _.size(props.nodes);
|
||||
var nodeScale = d3.scale.linear().range([0, nodeSize/Math.pow(n, 0.7)]);
|
||||
|
||||
var layoutId = 'layered node chart';
|
||||
console.time(layoutId);
|
||||
var graph = NodesLayout.doLayout(
|
||||
nodes,
|
||||
edges,
|
||||
props.width,
|
||||
props.height,
|
||||
nodeScale,
|
||||
MARGINS
|
||||
);
|
||||
console.timeEnd(layoutId);
|
||||
|
||||
// adjust layout based on viewport
|
||||
|
||||
var xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
|
||||
var yFactor = props.height / graph.height;
|
||||
var zoomFactor = Math.min(xFactor, yFactor);
|
||||
var zoomScale = this.state.scale;
|
||||
|
||||
if(this.zoom && !this.state.hasZoomed && zoomFactor < 1) {
|
||||
zoomScale = zoomFactor;
|
||||
// saving in d3's behavior cache
|
||||
this.zoom.scale(zoomFactor);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
nodeScale: nodeScale,
|
||||
scale: zoomScale
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.updateGraphState(this.props);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.zoom = d3.behavior.zoom()
|
||||
.scaleExtent([0.1, 2])
|
||||
.on('zoom', this.zoomed);
|
||||
|
||||
d3.select('.nodes-chart')
|
||||
.call(this.zoom);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
||||
// undoing .call(zoom)
|
||||
|
||||
d3.select('.nodes-chart')
|
||||
.on("mousedown.zoom", null)
|
||||
.on("onwheel", null)
|
||||
.on("onmousewheel", null)
|
||||
.on("dblclick.zoom", null)
|
||||
.on("touchstart.zoom", null);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (this.getTopologyFingerprint(nextProps.nodes) !== this.getTopologyFingerprint(this.props.nodes)) {
|
||||
this.setState({
|
||||
nodes: {},
|
||||
edges: {}
|
||||
});
|
||||
}
|
||||
|
||||
this.updateGraphState(nextProps);
|
||||
},
|
||||
|
||||
zoomed: function() {
|
||||
this.setState({
|
||||
hasZoomed: true,
|
||||
translate: d3.event.translate,
|
||||
scale: d3.event.scale
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var nodeElements = this.getNodes(this.state.nodes, this.state.nodeScale);
|
||||
var edgeElements = this.getEdges(this.state.edges, this.state.nodeScale);
|
||||
var transform = 'translate(' + this.state.translate + ')' +
|
||||
' scale(' + this.state.scale + ')';
|
||||
|
||||
return (
|
||||
<svg width="100%" height="100%" className="nodes-chart">
|
||||
<g className="canvas" transform={transform}>
|
||||
<g className="edges">
|
||||
{edgeElements}
|
||||
</g>
|
||||
<g className="nodes">
|
||||
{nodeElements}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = NodesChart;
|
||||
73
client/app/scripts/charts/nodes-layout.js
Normal file
73
client/app/scripts/charts/nodes-layout.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var dagre = require('dagre');
|
||||
var _ = require('lodash');
|
||||
|
||||
var MAX_NODES = 100;
|
||||
|
||||
var doLayout = function(nodes, edges, width, height, scale, margins) {
|
||||
var offsetX = 0 + margins.left;
|
||||
var offsetY = 0 + margins.top;
|
||||
var g = new dagre.graphlib.Graph({});
|
||||
|
||||
if (_.size(nodes) > MAX_NODES) {
|
||||
console.error('Too many nodes for graph layout engine. Limit: ' + MAX_NODES);
|
||||
return;
|
||||
}
|
||||
|
||||
// configure node margins
|
||||
|
||||
g.setGraph({
|
||||
nodesep: scale(2.5),
|
||||
ranksep: scale(2.5)
|
||||
});
|
||||
|
||||
// add nodes and edges to layout engine
|
||||
|
||||
_.each(nodes, function(node) {
|
||||
g.setNode(node.id, {id: node.id, width: scale(0.75), height: scale(0.75)});
|
||||
});
|
||||
|
||||
_.each(edges, function(edge) {
|
||||
var virtualNodes = edge.source.id === edge.target.id ? 1 : 0;
|
||||
g.setEdge(edge.source.id, edge.target.id, {id: edge.id, minlen: virtualNodes});
|
||||
});
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
var graph = g.graph();
|
||||
|
||||
// shifting graph coordinates to center
|
||||
|
||||
if (graph.width < width) {
|
||||
offsetX = (width - graph.width) / 2 + margins.left;
|
||||
}
|
||||
if (graph.height < height) {
|
||||
offsetY = (height - graph.height) / 2 + margins.top;
|
||||
}
|
||||
|
||||
// apply coordinates to nodes and edges
|
||||
|
||||
g.nodes().forEach(function(id) {
|
||||
var node = nodes[id];
|
||||
var graphNode = g.node(id);
|
||||
node.x = graphNode.x + offsetX;
|
||||
node.y = graphNode.y + offsetY;
|
||||
});
|
||||
|
||||
g.edges().forEach(function(id) {
|
||||
var graphEdge = g.edge(id);
|
||||
var edge = edges[graphEdge.id];
|
||||
_.each(graphEdge.points, function(point) {
|
||||
point.x += offsetX;
|
||||
point.y += offsetY;
|
||||
});
|
||||
edge.points = graphEdge.points;
|
||||
});
|
||||
|
||||
// return object with width and height of layout
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
doLayout: doLayout
|
||||
};
|
||||
82
client/app/scripts/components/app.js
Normal file
82
client/app/scripts/components/app.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var Logo = require('./logo');
|
||||
var SearchBar = require('./search-bar.js');
|
||||
var AppStore = require('../stores/app-store');
|
||||
var Topologies = require('./topologies.js');
|
||||
var TopologyStore = require('../stores/topology-store');
|
||||
var WebapiUtils = require('../utils/web-api-utils');
|
||||
var AppActions = require('../actions/app-actions');
|
||||
var Details = require('./details');
|
||||
var Nodes = require('./nodes');
|
||||
var ViewOptions = require('./view-options');
|
||||
var RouterUtils = require('../utils/router-utils');
|
||||
|
||||
|
||||
var ESC_KEY_CODE = 27;
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
selectedNodeId: AppStore.getSelectedNodeId(),
|
||||
nodeDetails: AppStore.getNodeDetails(),
|
||||
nodes: TopologyStore.getNodes(),
|
||||
topologies: AppStore.getTopologies(),
|
||||
activeTopology: AppStore.getCurrentTopology(),
|
||||
activeTopologyMode: AppStore.getCurrentTopologyMode()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var App = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return getStateFromStores();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
TopologyStore.on(TopologyStore.CHANGE_EVENT, this.onChange);
|
||||
AppStore.on(AppStore.CHANGE_EVENT, this.onChange);
|
||||
window.addEventListener('keyup', this.onKeyPress);
|
||||
|
||||
RouterUtils.getRouter().start({hashbang: true});
|
||||
WebapiUtils.getTopologies();
|
||||
},
|
||||
|
||||
onChange: function() {
|
||||
this.setState(getStateFromStores());
|
||||
},
|
||||
|
||||
onKeyPress: function(ev) {
|
||||
if (ev.keyCode === ESC_KEY_CODE) {
|
||||
AppActions.hitEsc();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var showingDetails = this.state.selectedNodeId;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showingDetails && <Details nodes={this.state.nodes}
|
||||
nodeId={this.state.selectedNodeId}
|
||||
details={this.state.nodeDetails}
|
||||
topology={this.state.activeTopology} /> }
|
||||
|
||||
<div className="header">
|
||||
<div id="logo">
|
||||
<Logo />
|
||||
</div>
|
||||
<Topologies topologies={this.state.topologies} active={this.state.activeTopology} />
|
||||
</div>
|
||||
|
||||
<Nodes nodes={this.state.nodes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = App;
|
||||
35
client/app/scripts/components/details.js
Normal file
35
client/app/scripts/components/details.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var mui = require('material-ui');
|
||||
var Paper = mui.Paper;
|
||||
var IconButton = mui.IconButton;
|
||||
|
||||
var AppActions = require('../actions/app-actions');
|
||||
var NodeDetails = require('./node-details');
|
||||
var WebapiUtils = require('../utils/web-api-utils');
|
||||
|
||||
var Details = React.createClass({
|
||||
|
||||
handleClickClose: function(ev) {
|
||||
ev.preventDefault();
|
||||
AppActions.clickCloseDetails();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div id="details">
|
||||
<Paper zDepth={3}>
|
||||
<div className="details-tools">
|
||||
<span className="fa fa-close" onClick={this.handleClickClose} />
|
||||
</div>
|
||||
<NodeDetails details={this.props.details} />
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Details;
|
||||
95
client/app/scripts/components/explorer.js
Normal file
95
client/app/scripts/components/explorer.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var NodesChart = require('../charts/nodes-chart');
|
||||
var NodeDetails = require('./node-details');
|
||||
|
||||
var marginBottom = 64;
|
||||
var marginTop = 64;
|
||||
var marginLeft = 36;
|
||||
var marginRight = 36;
|
||||
|
||||
var Explorer = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
layout: 'solar',
|
||||
width: window.innerWidth - marginLeft - marginRight,
|
||||
height: window.innerHeight - marginBottom - marginTop
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
setDimensions: function() {
|
||||
this.setState({
|
||||
height: window.innerHeight - marginBottom - marginTop,
|
||||
width: window.innerWidth - marginLeft - marginRight
|
||||
});
|
||||
},
|
||||
|
||||
handleResize: function() {
|
||||
this.setDimensions();
|
||||
},
|
||||
|
||||
getSubTopology: function(topology) {
|
||||
var subTopology = {};
|
||||
var nodeSet = [];
|
||||
|
||||
_.each(this.props.expandedNodes, function(nodeId) {
|
||||
if (topology[nodeId]) {
|
||||
subTopology[nodeId] = topology[nodeId];
|
||||
nodeSet = _.union(subTopology[nodeId].adjacency, nodeSet);
|
||||
_.each(subTopology[nodeId].adjacency, function(adjacentId) {
|
||||
var node = _.assign({}, topology[adjacentId]);
|
||||
|
||||
subTopology[adjacentId] = node;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// weed out edges
|
||||
_.each(subTopology, function(node) {
|
||||
node.adjacency = _.intersection(node.adjacency, nodeSet);
|
||||
});
|
||||
|
||||
return subTopology;
|
||||
},
|
||||
|
||||
onNodeClick: function(ev) {
|
||||
var nodeId = ev.currentTarget.id;
|
||||
AppActions.clickNode(nodeId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var subTopology = this.getSubTopology(this.props.nodes);
|
||||
|
||||
return (
|
||||
<div id="explorer">
|
||||
<NodeDetails details={this.props.details} />
|
||||
<div className="graph">
|
||||
<NodesChart
|
||||
onNodeClick={this.onNodeClick}
|
||||
layout={this.state.layout}
|
||||
nodes={subTopology}
|
||||
highlightedNodes={this.props.expandedNodes}
|
||||
width={this.state.width}
|
||||
height={this.state.height}
|
||||
context="explorer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Explorer;
|
||||
24
client/app/scripts/components/logo.js
Normal file
24
client/app/scripts/components/logo.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var Logo = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="logo">
|
||||
<svg width="100%" height="100%" viewBox="0 0 145.85 148">
|
||||
<path d="M145.142,63.868l-62.7,56.006V98.518l58.14-51.933c-2.77-6.938-6.551-13.359-11.175-19.076L82.442,69.46
|
||||
V47.132L60.771,66.49v22.327l-18.668,16.676V83.165l75.418-67.367c-5.989-4.706-12.71-8.519-19.981-11.211L82.442,18.074V0.767
|
||||
C78.981,0.27,75.448,0,71.85,0c-3.766,0-7.465,0.286-11.079,0.828v36.604L42.103,54.107V6.242
|
||||
c-8.086,3.555-15.409,8.513-21.672,14.568v52.656L0,91.715c1.86,7.57,4.88,14.683,8.87,21.135l11.561-10.326v24.667
|
||||
c4.885,4.724,10.409,8.787,16.444,12.03l23.896-21.345v29.296C64.385,147.715,68.084,148,71.85,148c4.41,0,8.722-0.407,12.921-1.147
|
||||
l58.033-51.838c1.971-6.664,3.046-13.712,3.046-21.015C145.85,70.561,145.596,67.183,145.142,63.868z"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Logo;
|
||||
34
client/app/scripts/components/node-details-table.js
Normal file
34
client/app/scripts/components/node-details-table.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var NodeDetailsTable = React.createClass({
|
||||
|
||||
render: function() {
|
||||
var isNumeric = this.props.isNumeric;
|
||||
|
||||
return (
|
||||
<div className="node-details-table">
|
||||
<h4 className="node-details-table-title">
|
||||
{this.props.title}
|
||||
</h4>
|
||||
|
||||
{this.props.rows.map(function(row) {
|
||||
return (
|
||||
<div className="node-details-table-row">
|
||||
<div className="node-details-table-row-key">{row.key}</div>
|
||||
{isNumeric && <div className="node-details-table-row-value-scalar">{row.value_major}</div>}
|
||||
{isNumeric && <div className="node-details-table-row-value-unit">{row.value_minor}</div>}
|
||||
{!isNumeric && <div className="node-details-table-row-value-major">{row.value_major}</div>}
|
||||
{!isNumeric && row.value_minor && <div className="node-details-table-row-value-minor">{row.value_minor}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = NodeDetailsTable;
|
||||
46
client/app/scripts/components/node-details.js
Normal file
46
client/app/scripts/components/node-details.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var NodeDetailsTable = require('./node-details-table');
|
||||
var NodeColorMixin = require('../mixins/node-color-mixin');
|
||||
|
||||
var NodeDetails = React.createClass({
|
||||
|
||||
mixins: [
|
||||
NodeColorMixin
|
||||
],
|
||||
|
||||
render: function() {
|
||||
var node = this.props.details;
|
||||
|
||||
if (!node) {
|
||||
return <div className="node-details" />;
|
||||
}
|
||||
|
||||
var style = {
|
||||
"background-color": this.getNodeColorDark(node.label_major)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="node-details">
|
||||
<div className="node-details-header" style={style}>
|
||||
<h2 className="node-details-header-label">
|
||||
{node.label_major}
|
||||
</h2>
|
||||
<div className="node-details-header-label-minor">{node.label_minor}</div>
|
||||
</div>
|
||||
|
||||
<div className="node-details-content">
|
||||
{this.props.details.tables.map(function(table) {
|
||||
return <NodeDetailsTable title={table.title} rows={table.rows} isNumeric={table.numeric} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = NodeDetails;
|
||||
60
client/app/scripts/components/nodes.js
Normal file
60
client/app/scripts/components/nodes.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var NodesChart = require('../charts/nodes-chart');
|
||||
var AppActions = require('../actions/app-actions');
|
||||
|
||||
var navbarHeight = 160;
|
||||
var marginTop = 0;
|
||||
var marginLeft = 0;
|
||||
|
||||
var Nodes = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight - navbarHeight - marginTop
|
||||
};
|
||||
},
|
||||
|
||||
onNodeClick: function(ev) {
|
||||
AppActions.clickNode(ev.currentTarget.id);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
setDimensions: function() {
|
||||
this.setState({
|
||||
height: window.innerHeight - navbarHeight - marginTop,
|
||||
width: window.innerWidth
|
||||
});
|
||||
},
|
||||
|
||||
handleResize: function() {
|
||||
this.setDimensions();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div id="nodes">
|
||||
<NodesChart
|
||||
onNodeClick={this.onNodeClick}
|
||||
nodes={this.props.nodes}
|
||||
width={this.state.width}
|
||||
height={this.state.height}
|
||||
context="view"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Nodes;
|
||||
89
client/app/scripts/components/search-bar.js
Normal file
89
client/app/scripts/components/search-bar.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var Stats = require('./stats.js');
|
||||
var TopologyActions = require('../actions/topology-actions');
|
||||
|
||||
var SearchBar = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
initialFilterText: ''
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
filterAdjacent: false,
|
||||
filterText: this.props.filterText
|
||||
};
|
||||
},
|
||||
|
||||
handleChange: function() {
|
||||
TopologyActions.inputFilterText(this.state.filterText);
|
||||
},
|
||||
|
||||
onVolatileChange: function(event) {
|
||||
this.setState({
|
||||
filterText: event.target.value
|
||||
});
|
||||
|
||||
this.scheduleChange();
|
||||
},
|
||||
|
||||
scheduleChange: _.debounce(function() {
|
||||
this.handleChange();
|
||||
}, 300),
|
||||
|
||||
onFilterAjacent: function(event) {
|
||||
this.setState({
|
||||
filterAdjacent: event.target.checked
|
||||
});
|
||||
TopologyActions.checkFilterAdjacent(event.target.checked);
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.setState({
|
||||
filterText: this.props.filterText
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.refs.filterTextInput.getDOMNode().focus();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="row" id="search-bar">
|
||||
<div className="form-group col-md-9">
|
||||
<div className="form-control-wrapper">
|
||||
<input className="form-control input-lg" type="text"
|
||||
id="filterTextInput"
|
||||
placeholder="Filter for nodes"
|
||||
value={this.state.filterText}
|
||||
ref="filterTextInput"
|
||||
onChange={this.onVolatileChange} />
|
||||
<span className="material-input"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group col-md-3">
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
ref="filterAdjacent"
|
||||
value={this.state.filterAdjacent}
|
||||
onChange={this.onFilterAjacent} />
|
||||
<span className="check"></span>
|
||||
Include connected
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = SearchBar;
|
||||
18
client/app/scripts/components/stat-value.js
Normal file
18
client/app/scripts/components/stat-value.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var StatValue = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="stat-value">
|
||||
<span className="value">{this.props.value}</span>
|
||||
<span className="stat-label">{this.props.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = StatValue;
|
||||
30
client/app/scripts/components/stats.js
Normal file
30
client/app/scripts/components/stats.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var StatValue = require('./stat-value.js');
|
||||
|
||||
var Stats = React.createClass({
|
||||
|
||||
render: function() {
|
||||
var nodeCount = _.size(this.props.nodes),
|
||||
edgeCount = _.reduce(this.props.nodes, function(result, node) {
|
||||
return result + _.size(node.adjacency);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div id="stats">
|
||||
<div className="col-xs-6">
|
||||
<StatValue value={nodeCount} label="Nodes" />
|
||||
</div>
|
||||
<div className="col-xs-6">
|
||||
<StatValue value={edgeCount} label="Connections" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Stats;
|
||||
52
client/app/scripts/components/topologies.js
Normal file
52
client/app/scripts/components/topologies.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var AppActions = require('../actions/app-actions');
|
||||
var AppStore = require('../stores/app-store');
|
||||
|
||||
var Topologies = React.createClass({
|
||||
|
||||
onTopologyClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
AppActions.clickTopology(ev.currentTarget.getAttribute('rel'));
|
||||
},
|
||||
|
||||
renderTopology: function(topology, active) {
|
||||
var className = AppStore.isUrlForTopology(topology.url, active) ? "topologies-item topologies-item-active" : "topologies-item",
|
||||
topologyId = AppStore.getTopologyForUrl(topology.url),
|
||||
title = ['Topology: ' + topology.name,
|
||||
'Nodes: ' + topology.stats.node_count,
|
||||
'Connections: ' + topology.stats.node_count].join('\n');
|
||||
|
||||
return (
|
||||
<div className={className} key={topologyId} rel={topologyId} onClick={this.onTopologyClick}>
|
||||
<div title={title}>
|
||||
<div className="topologies-item-label">
|
||||
{topology.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var activeTopologyId = this.props.active,
|
||||
topologies = _.sortBy(this.props.topologies, function(topology) {
|
||||
return topology.name;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="topologies">
|
||||
<span className="topologies-icon fa fa-sitemap" />
|
||||
{topologies.map(function(topology) {
|
||||
return this.renderTopology(topology, activeTopologyId);
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = Topologies;
|
||||
93
client/app/scripts/components/view-options.js
Normal file
93
client/app/scripts/components/view-options.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var AppActions = require('../actions/app-actions');
|
||||
|
||||
var ViewOptions = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
menuVisible: false
|
||||
};
|
||||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
AppActions.clickTopologyMode(ev.currentTarget.rel);
|
||||
},
|
||||
|
||||
onMouseOver: function(ev) {
|
||||
this.handleMouseDebounced(true);
|
||||
},
|
||||
|
||||
onMouseOut: function(ev) {
|
||||
this.handleMouseDebounced(false);
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.handleMouseDebounced = _.debounce(function(isOver) {
|
||||
this.setState({
|
||||
menuVisible: isOver
|
||||
});
|
||||
}, 200);
|
||||
},
|
||||
|
||||
getActiveViewMode: function() {
|
||||
var className = this.props.active === 'class' ? "glyphicon glyphicon-th-large" : "glyphicon glyphicon-th";
|
||||
|
||||
return (
|
||||
<div className="nav-preview" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<div rel={this.props.active} onClick={this.props.onClick}>
|
||||
<div className="nav-icon">
|
||||
<span className={className}></span>
|
||||
</div>
|
||||
<div className="nav-label">
|
||||
View Options
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var activeMode = this.props.active,
|
||||
activeOptions = this.getActiveViewMode(),
|
||||
baseClass = "",
|
||||
individualClass = activeMode == 'individual' ? baseClass + ' active' : baseClass,
|
||||
classClass = activeMode == 'class' ? baseClass + ' active' : baseClass,
|
||||
navClassName = "nav navbar-nav";
|
||||
|
||||
return (
|
||||
<div className="navbar-view-options">
|
||||
{activeOptions}
|
||||
{this.state.menuVisible && <ul className={navClassName} onMouseOut={this.onMouseOut} onMouseOver={this.onMouseOver}>
|
||||
<li className={individualClass}>
|
||||
<a href="#" className="row" rel="individual" onClick={this.onClick}>
|
||||
<div className="col-xs-5 nav-item-preview">
|
||||
<span className="glyphicon glyphicon-th"></span>
|
||||
</div>
|
||||
<div className="col-xs-7 nav-item-label">
|
||||
Standard View
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li className={classClass}>
|
||||
<a href="#" className="row" rel="class" onClick={this.onClick}>
|
||||
<div className="col-xs-5 nav-item-preview">
|
||||
<span className="glyphicon glyphicon-th-large"></span>
|
||||
</div>
|
||||
<div className="col-xs-7 nav-item-label">
|
||||
Group View
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = ViewOptions;
|
||||
16
client/app/scripts/constants/action-types.js
Normal file
16
client/app/scripts/constants/action-types.js
Normal file
@@ -0,0 +1,16 @@
|
||||
var keymirror = require('keymirror');
|
||||
|
||||
module.exports = keymirror({
|
||||
CLICK_CLOSE_DETAILS: null,
|
||||
CLICK_NODE: null,
|
||||
CLICK_TOPOLOGY: null,
|
||||
CLICK_TOPOLOGY_MODE: null,
|
||||
ENTER_NODE: null,
|
||||
HIT_ESC_KEY: null,
|
||||
LEAVE_NODE: null,
|
||||
RECEIVE_NODE_DETAILS: null,
|
||||
RECEIVE_NODES: null,
|
||||
RECEIVE_NODES_DELTA: null,
|
||||
RECEIVE_TOPOLOGIES: null,
|
||||
ROUTE_TOPOLOGY: null
|
||||
});
|
||||
13
client/app/scripts/constants/topologies.js
Normal file
13
client/app/scripts/constants/topologies.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
url: '/api/topology/applications',
|
||||
name: 'Applications',
|
||||
stats: {}
|
||||
},
|
||||
{
|
||||
url: '/api/topology/hosts',
|
||||
name: 'Hosts',
|
||||
stats: {}
|
||||
}
|
||||
];
|
||||
12
client/app/scripts/dispatcher/app-dispatcher.js
Normal file
12
client/app/scripts/dispatcher/app-dispatcher.js
Normal file
@@ -0,0 +1,12 @@
|
||||
var flux = require('flux');
|
||||
var _ = require('lodash');
|
||||
|
||||
var AppDispatcher = new flux.Dispatcher();
|
||||
|
||||
AppDispatcher.dispatch = _.wrap(flux.Dispatcher.prototype.dispatch, function(func) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
// console.log(args[0]);
|
||||
func.apply(this, args);
|
||||
});
|
||||
|
||||
module.exports = AppDispatcher;
|
||||
11
client/app/scripts/main.js
Normal file
11
client/app/scripts/main.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @jsx React.DOM
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var App = require('./components/app.js');
|
||||
|
||||
React.render(
|
||||
<App/>,
|
||||
document.getElementById('app'));
|
||||
27
client/app/scripts/mixins/node-color-mixin.js
Normal file
27
client/app/scripts/mixins/node-color-mixin.js
Normal file
@@ -0,0 +1,27 @@
|
||||
var d3 = require('d3');
|
||||
|
||||
var colors = d3.scale.category20();
|
||||
|
||||
// make sure the internet always gets the same color
|
||||
var internetLabel = "the Internet";
|
||||
colors(internetLabel);
|
||||
|
||||
|
||||
var NodeColorMixin = {
|
||||
getNodeColor: function(text) {
|
||||
return colors(text);
|
||||
},
|
||||
getNodeColorDark: function(text) {
|
||||
var color = d3.rgb(colors(text));
|
||||
var hsl = color.hsl();
|
||||
|
||||
// ensure darkness
|
||||
// if (hsl.l > 0.5) {
|
||||
hsl = hsl.darker();
|
||||
// }
|
||||
|
||||
return hsl.toString();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = NodeColorMixin;
|
||||
131
client/app/scripts/stores/app-store.js
Normal file
131
client/app/scripts/stores/app-store.js
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var _ = require('lodash');
|
||||
var assign = require('object-assign');
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app-dispatcher');
|
||||
var ActionTypes = require('../constants/action-types');
|
||||
var TopologyStore = require('./topology-store');
|
||||
// var topologies = require('../constants/topologies');
|
||||
|
||||
|
||||
// Initial values
|
||||
|
||||
var currentTopology = 'applications';
|
||||
var currentTopologyMode = 'individual';
|
||||
var nodeDetails = null;
|
||||
var selectedNodeId = null;
|
||||
var topologies = [];
|
||||
|
||||
// Store API
|
||||
|
||||
var AppStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
CHANGE_EVENT: 'change',
|
||||
|
||||
getAppState: function() {
|
||||
return {
|
||||
currentTopology: this.getCurrentTopology(),
|
||||
currentTopologyMode: this.getCurrentTopologyMode(),
|
||||
selectedNodeId: this.getSelectedNodeId()
|
||||
};
|
||||
},
|
||||
|
||||
getCurrentTopology: function() {
|
||||
return currentTopology;
|
||||
},
|
||||
|
||||
getCurrentTopologyMode: function() {
|
||||
return currentTopologyMode;
|
||||
},
|
||||
|
||||
getNodeDetails: function() {
|
||||
return nodeDetails;
|
||||
},
|
||||
|
||||
getSelectedNodeId: function() {
|
||||
return selectedNodeId;
|
||||
},
|
||||
|
||||
getTopologies: function() {
|
||||
return topologies;
|
||||
},
|
||||
|
||||
getTopologyForUrl: function(url) {
|
||||
return url.split('/').pop();
|
||||
},
|
||||
|
||||
getUrlForTopology: function(topologyId) {
|
||||
var topology = _.find(topologies, function(topology) {
|
||||
return this.isUrlForTopology(topology.url, topologyId);
|
||||
}, this);
|
||||
|
||||
if (topology) {
|
||||
return topology.grouped_url && currentTopologyMode == 'class' ? topology.grouped_url : topology.url;
|
||||
}
|
||||
},
|
||||
|
||||
isUrlForTopology: function(url, topologyId) {
|
||||
return _.endsWith(url, topologyId);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Store Dispatch Hooks
|
||||
|
||||
AppStore.dispatchToken = AppDispatcher.register(function(payload) {
|
||||
switch (payload.type) {
|
||||
case ActionTypes.CLICK_CLOSE_DETAILS:
|
||||
selectedNodeId = null;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.CLICK_NODE:
|
||||
selectedNodeId = payload.nodeId;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.CLICK_TOPOLOGY:
|
||||
currentTopology = payload.topologyId;
|
||||
AppDispatcher.waitFor([TopologyStore.dispatchToken]);
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.CLICK_TOPOLOGY_MODE:
|
||||
currentTopologyMode = payload.mode;
|
||||
AppDispatcher.waitFor([TopologyStore.dispatchToken]);
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.HIT_ESC_KEY:
|
||||
nodeDetails = null;
|
||||
selectedNodeId = null;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.RECEIVE_NODE_DETAILS:
|
||||
nodeDetails = payload.details;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.RECEIVE_TOPOLOGIES:
|
||||
topologies = payload.topologies;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.ROUTE_TOPOLOGY:
|
||||
currentTopology = payload.state.currentTopology;
|
||||
currentTopologyMode = payload.state.currentTopologyMode;
|
||||
selectedNodeId = payload.state.selectedNodeId;
|
||||
AppDispatcher.waitFor([TopologyStore.dispatchToken]);
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AppStore;
|
||||
86
client/app/scripts/stores/topology-store.js
Normal file
86
client/app/scripts/stores/topology-store.js
Normal file
@@ -0,0 +1,86 @@
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var _ = require('lodash');
|
||||
var assign = require('object-assign');
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app-dispatcher');
|
||||
var ActionTypes = require('../constants/action-types');
|
||||
|
||||
|
||||
|
||||
// Initial values
|
||||
|
||||
var nodes = {};
|
||||
var mouseOverNode = null;
|
||||
|
||||
// Store API
|
||||
|
||||
var TopologyStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
CHANGE_EVENT: 'change',
|
||||
|
||||
getNodes: function() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Store Dispatch Hooks
|
||||
|
||||
TopologyStore.dispatchToken = AppDispatcher.register(function(payload) {
|
||||
switch (payload.type) {
|
||||
case ActionTypes.CLICK_TOPOLOGY:
|
||||
nodes = {};
|
||||
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.CLICK_TOPOLOGY_MODE:
|
||||
nodes = {};
|
||||
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.ENTER_NODE:
|
||||
mouseOverNode = payload.nodeId;
|
||||
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.LEAVE_NODE:
|
||||
mouseOverNode = null;
|
||||
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.RECEIVE_NODES_DELTA:
|
||||
// nodes that no longer exist
|
||||
_.each(payload.delta.remove, function(nodeId) {
|
||||
// in case node disappears before mouseleave event
|
||||
if (mouseOverNode === nodeId) {
|
||||
mouseOverNode = null;
|
||||
}
|
||||
delete nodes[nodeId];
|
||||
});
|
||||
|
||||
// update existing nodes
|
||||
_.each(payload.delta.update, function(node) {
|
||||
nodes[node.id] = node;
|
||||
});
|
||||
|
||||
// add new nodes
|
||||
_.each(payload.delta.add, function(node) {
|
||||
nodes[node.id] = node;
|
||||
});
|
||||
|
||||
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.ROUTE_TOPOLOGY:
|
||||
nodes = {};
|
||||
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TopologyStore;
|
||||
31
client/app/scripts/utils/router-utils.js
Normal file
31
client/app/scripts/utils/router-utils.js
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
var page = require('page');
|
||||
|
||||
var AppActions = require('../actions/app-actions');
|
||||
var AppStore = require('../stores/app-store');
|
||||
|
||||
page('/', function(ctx) {
|
||||
updateRoute();
|
||||
});
|
||||
|
||||
page('/state/:state', function(ctx) {
|
||||
var state = JSON.parse(ctx.params.state);
|
||||
AppActions.route(state);
|
||||
});
|
||||
|
||||
function updateRoute() {
|
||||
var state = AppStore.getAppState();
|
||||
var stateUrl = JSON.stringify(state);
|
||||
var dispatch = false;
|
||||
|
||||
page.show('/state/' + stateUrl, state, dispatch);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
getRouter: function() {
|
||||
return page;
|
||||
},
|
||||
|
||||
updateRoute: updateRoute
|
||||
};
|
||||
68
client/app/scripts/utils/web-api-utils.js
Normal file
68
client/app/scripts/utils/web-api-utils.js
Normal file
@@ -0,0 +1,68 @@
|
||||
var reqwest = require('reqwest');
|
||||
|
||||
var TopologyActions = require('../actions/topology-actions');
|
||||
var AppActions = require('../actions/app-actions');
|
||||
var AppStore = require('../stores/app-store');
|
||||
|
||||
var WS_URL = window.WS_URL || 'ws://' + location.host;
|
||||
|
||||
|
||||
var socket;
|
||||
var reconnectTimer = 0;
|
||||
var currentUrl = null;
|
||||
var updateFrequency = '5s';
|
||||
var topologyTimer = 0;
|
||||
|
||||
function createWebsocket(topologyUrl) {
|
||||
if (socket) {
|
||||
socket.onclose = null;
|
||||
socket.close();
|
||||
}
|
||||
|
||||
socket = new WebSocket(WS_URL + topologyUrl + '/ws?t=' + updateFrequency);
|
||||
|
||||
socket.onclose = function() {
|
||||
clearTimeout(reconnectTimer);
|
||||
socket = null;
|
||||
|
||||
reconnectTimer = setTimeout(function() {
|
||||
createWebsocket(topologyUrl);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
var msg = JSON.parse(event.data);
|
||||
if (msg.add || msg.remove || msg.update) {
|
||||
TopologyActions.receiveNodesDelta(msg);
|
||||
}
|
||||
};
|
||||
|
||||
currentUrl = topologyUrl;
|
||||
}
|
||||
|
||||
function getTopologies() {
|
||||
clearTimeout(topologyTimer);
|
||||
reqwest('/api/topology', function(res) {
|
||||
AppActions.receiveTopologies(res);
|
||||
topologyTimer = setTimeout(getTopologies, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeDetails(topology, nodeId) {
|
||||
var url = [AppStore.getUrlForTopology(topology), nodeId].join('/');
|
||||
reqwest(url, function(res) {
|
||||
AppActions.receiveNodeDetails(res.node);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNodeDetails: getNodeDetails,
|
||||
|
||||
getTopologies: getTopologies,
|
||||
|
||||
getNodesDelta: function(topologyUrl) {
|
||||
if (topologyUrl) {
|
||||
createWebsocket(topologyUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
289
client/app/styles/main.less
Normal file
289
client/app/styles/main.less
Normal file
@@ -0,0 +1,289 @@
|
||||
@import "../../node_modules/material-ui/src/less/scaffolding";
|
||||
@import "../../node_modules/material-ui/src/less/components";
|
||||
@import "../../node_modules/font-awesome/less/font-awesome.less";
|
||||
|
||||
@import url(http://fonts.googleapis.com/css?family=Roboto:300,400);
|
||||
|
||||
.browsehappy {
|
||||
margin: 0.2em 0;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
@primary-color: #445566;
|
||||
@background-color: white;
|
||||
@background-secondary-color: lighten(@primary-color, 60%);
|
||||
@text-color: @primary-color;
|
||||
@text-secondary-color: lighten(@primary-color, 33%);
|
||||
@text-pale-color: lighten(@primary-color, 50%);
|
||||
@text-darker-color: darken(@primary-color, 33%);
|
||||
@lighter-color: lighten(@primary-color, 10%);
|
||||
@cello-orange: #D65906;
|
||||
@white: #E7E7E7;
|
||||
|
||||
html, body {
|
||||
}
|
||||
|
||||
.wrap {
|
||||
}
|
||||
|
||||
|
||||
/* Space out content a bit */
|
||||
body {
|
||||
background: linear-gradient(30deg, #DDD 0%, #F2F2F2 100%);
|
||||
color: @text-color;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
#logo {
|
||||
margin: 0 64px 0 64px;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
float: left;
|
||||
|
||||
.logo {
|
||||
path {
|
||||
stroke-width: 2px;
|
||||
stroke: @text-secondary-color;
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#app {
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
// border-bottom: 1px solid @text-pale-color;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.topologies {
|
||||
float: left;
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: @text-secondary-color;
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.topologies-item {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
|
||||
&-frame {
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&-nodes,
|
||||
&-edges,
|
||||
&-divider {
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
font-size: 24px;
|
||||
color: @text-secondary-color;
|
||||
}
|
||||
|
||||
&-label {
|
||||
color: @text-secondary-color;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&-active, &:hover {
|
||||
.topologies-item-label {
|
||||
color: @text-color;
|
||||
//border-bottom: 2px solid @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#stats {
|
||||
|
||||
.stat-value {
|
||||
text-align: center;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 36px;
|
||||
display: block;
|
||||
}
|
||||
.stat-label {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
color: @text-secondary-color;
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
#nodes {
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 14px;
|
||||
font-family: Roboto;
|
||||
fill: @text-secondary-color;
|
||||
text-shadow: 0 1px 0 @white, 1px 0 0 @white, 0 -1px 0 @white, -1px 0 0 @white;
|
||||
|
||||
&.node-label {
|
||||
fill: @text-color;
|
||||
}
|
||||
|
||||
&.node-sublabel {
|
||||
font-size: 12px;
|
||||
fill: @text-secondary-color;
|
||||
}
|
||||
}
|
||||
|
||||
g.node {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: @text-secondary-color;
|
||||
stroke-width: 1.5px;
|
||||
fill: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
circle.border {
|
||||
stroke-width: 3px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
circle.shadow {
|
||||
stroke: none;
|
||||
fill: @background-secondary-color;
|
||||
}
|
||||
|
||||
circle.node {
|
||||
fill: @text-color;
|
||||
}
|
||||
|
||||
g.node.highlighted {
|
||||
circle.border {
|
||||
stroke: @cello-orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#details {
|
||||
position: absolute;
|
||||
z-index: 1024;
|
||||
display: block;
|
||||
right: 36px;
|
||||
top: 24px;
|
||||
bottom: 48px;
|
||||
width: 32em;
|
||||
|
||||
.details-tools {
|
||||
float: right;
|
||||
padding: 16px 24px;
|
||||
color: @white;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mui-paper, .mui-paper-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.node-details {
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.86);
|
||||
|
||||
&-header {
|
||||
|
||||
padding: 24px 36px 24px 36px;
|
||||
|
||||
&-label {
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
|
||||
&-minor {
|
||||
font-size: 120%;
|
||||
color: @white;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-content {
|
||||
padding: 0 36px 0 36px;
|
||||
}
|
||||
|
||||
&-table {
|
||||
|
||||
margin-bottom: 8px;
|
||||
|
||||
&-title {
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0;
|
||||
color: @text-secondary-color;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
&-row {
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&-key {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
&-value-major {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
&-value-scalar {
|
||||
width: 2em;
|
||||
text-align: right;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
&-value-minor,
|
||||
&-value-unit {
|
||||
font-size: 95%;
|
||||
color: @text-secondary-color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
119
client/gulpfile.js
Normal file
119
client/gulpfile.js
Normal file
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var connect = require('gulp-connect');
|
||||
var livereload = require('gulp-livereload');
|
||||
var browserify = require('browserify');
|
||||
var del = require('del');
|
||||
var source = require('vinyl-source-stream');
|
||||
var buffer = require('vinyl-buffer');
|
||||
var reactify = require('reactify');
|
||||
|
||||
// load plugins
|
||||
var $ = require('gulp-load-plugins')();
|
||||
|
||||
var isDev = true;
|
||||
var isProd = false;
|
||||
|
||||
gulp.task('styles', function () {
|
||||
return gulp.src('app/styles/main.less')
|
||||
.pipe($.if(isDev, $.sourcemaps.init()))
|
||||
.pipe($.less())
|
||||
.pipe($.autoprefixer('last 1 version'))
|
||||
.pipe($.if(isDev, $.sourcemaps.write()))
|
||||
.pipe($.if(isDev, gulp.dest('.tmp/styles')))
|
||||
.pipe($.if(isProd, $.csso()))
|
||||
.pipe($.if(isProd, gulp.dest('dist/styles')))
|
||||
.pipe($.size())
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('scripts', function() {
|
||||
var bundler = browserify('./app/scripts/main.js', {debug: isDev});
|
||||
bundler.transform(reactify);
|
||||
|
||||
var stream = bundler.bundle();
|
||||
|
||||
return stream
|
||||
.pipe(source('bundle.js'))
|
||||
.pipe($.if(isDev, gulp.dest('.tmp/scripts')))
|
||||
.pipe($.if(isProd, buffer()))
|
||||
.pipe($.if(isProd, $.uglify()))
|
||||
.pipe($.if(isProd, gulp.dest('dist/scripts')))
|
||||
.pipe(livereload())
|
||||
.on('error', $.util.log);
|
||||
});
|
||||
|
||||
gulp.task('html', ['styles', 'scripts'], function () {
|
||||
return gulp.src('app/*.html')
|
||||
.pipe($.preprocess())
|
||||
//.pipe($.useref.assets({searchPath: 'dist'}))
|
||||
//.pipe($.useref())
|
||||
.pipe(gulp.dest('dist'))
|
||||
.pipe($.size())
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('images', function () {
|
||||
return gulp.src('app/images/**/*')
|
||||
.pipe(gulp.dest('dist/images'))
|
||||
.pipe($.size());
|
||||
});
|
||||
|
||||
gulp.task('fonts', function () {
|
||||
return gulp.src('node_modules/font-awesome/fonts/*')
|
||||
.pipe($.filter('**/*.{eot,svg,ttf,woff}'))
|
||||
.pipe($.flatten())
|
||||
.pipe($.if(isDev, gulp.dest('.tmp/fonts')))
|
||||
.pipe($.if(isProd, gulp.dest('dist/fonts')))
|
||||
.pipe($.size());
|
||||
});
|
||||
|
||||
gulp.task('extras', function () {
|
||||
return gulp.src(['app/*.*', '!app/*.html'], { dot: true })
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('clean', function () {
|
||||
return del(['.tmp', 'dist']);
|
||||
});
|
||||
|
||||
gulp.task('production', ['html', 'images', 'fonts', 'extras']);
|
||||
|
||||
gulp.task('build', function () {
|
||||
isDev = false;
|
||||
isProd = true;
|
||||
gulp.start('production');
|
||||
});
|
||||
|
||||
gulp.task('default', ['clean'], function () {
|
||||
gulp.start('build');
|
||||
});
|
||||
|
||||
gulp.task('connect', function () {
|
||||
connect.server({
|
||||
root: ['.tmp', 'app'],
|
||||
port: 4041,
|
||||
middleware: function(connect, o) {
|
||||
return [(function() {
|
||||
var url = require('url');
|
||||
var proxy = require('proxy-middleware');
|
||||
var options = url.parse('http://localhost:4040/api');
|
||||
options.route = '/api';
|
||||
return proxy(options);
|
||||
})()];
|
||||
},
|
||||
livereload: false
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('serve', ['connect', 'styles', 'scripts', 'fonts'], function () {
|
||||
//require('opn')('http://localhost:9000');
|
||||
});
|
||||
|
||||
gulp.task('watch', ['serve'], function () {
|
||||
livereload.listen();
|
||||
gulp.watch('app/styles/**/*.less', ['styles']);
|
||||
gulp.watch('app/scripts/**/*.js', ['scripts']);
|
||||
gulp.watch('app/images/**/*', ['images']);
|
||||
});
|
||||
55
client/package.json
Normal file
55
client/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "scope-webapp",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.3",
|
||||
"dagre": "^0.7.1",
|
||||
"flux": "^2.0.1",
|
||||
"font-awesome": "^4.3.0",
|
||||
"keymirror": "^0.1.1",
|
||||
"lodash": "~3.0.1",
|
||||
"material-ui": "^0.7.5",
|
||||
"object-assign": "^2.0.0",
|
||||
"page": "^1.6.0",
|
||||
"react": "^0.13.2",
|
||||
"react-tween-state": "0.0.5",
|
||||
"reqwest": "~1.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^8.1.3",
|
||||
"connect": "^3.3.4",
|
||||
"del": "^1.1.1",
|
||||
"gulp": "^3.8.10",
|
||||
"gulp-autoprefixer": "^2.1.0",
|
||||
"gulp-cache": "^0.2.4",
|
||||
"gulp-clean": "^0.3.1",
|
||||
"gulp-connect": "^2.2.0",
|
||||
"gulp-csso": "^1.0.0",
|
||||
"gulp-filter": "^2.0.0",
|
||||
"gulp-flatten": "^0.0.4",
|
||||
"gulp-if": "^1.2.5",
|
||||
"gulp-imagemin": "^2.1.0",
|
||||
"gulp-jshint": "^1.9.2",
|
||||
"gulp-less": "^3.0.3",
|
||||
"gulp-livereload": "^3.8.0",
|
||||
"gulp-load-plugins": "^0.8.0",
|
||||
"gulp-preprocess": "^1.2.0",
|
||||
"gulp-size": "^1.2.0",
|
||||
"gulp-sourcemaps": "^1.3.0",
|
||||
"gulp-uglify": "^1.1.0",
|
||||
"gulp-useref": "^1.1.1",
|
||||
"gulp-util": "^3.0.3",
|
||||
"jshint-stylish": "^1.0.0",
|
||||
"opn": "^1.0.1",
|
||||
"proxy-middleware": "^0.11.0",
|
||||
"reactify": "^1.1.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "npm install && gulp serve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
}
|
||||
3
client/test/.bowerrc
Normal file
3
client/test/.bowerrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"directory": "bower_components"
|
||||
}
|
||||
9
client/test/bower.json
Normal file
9
client/test/bower.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "gulp-webapp",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"chai": "~1.8.0",
|
||||
"mocha": "~1.14.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
26
client/test/index.html
Normal file
26
client/test/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mocha Spec Runner</title>
|
||||
<link rel="stylesheet" href="bower_components/mocha/mocha.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
<script src="bower_components/mocha/mocha.js"></script>
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="bower_components/chai/chai.js"></script>
|
||||
<script>
|
||||
var assert = chai.assert;
|
||||
var expect = chai.expect;
|
||||
var should = chai.should();
|
||||
</script>
|
||||
|
||||
<!-- include source files here... -->
|
||||
|
||||
<!-- include spec files here... -->
|
||||
<script src="spec/test.js"></script>
|
||||
|
||||
<script>mocha.run()</script>
|
||||
</body>
|
||||
</html>
|
||||
13
client/test/spec/test.js
Normal file
13
client/test/spec/test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* global describe, it */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('Give it some context', function () {
|
||||
describe('maybe a bit more context here', function () {
|
||||
it('should run here few assertions', function () {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
19
demoprobe/Makefile
Normal file
19
demoprobe/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
.PHONY: all vet lint build test clean
|
||||
|
||||
all: build test vet lint
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
lint:
|
||||
golint .
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
123
demoprobe/generate.go
Normal file
123
demoprobe/generate.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// DemoReport makes up a report.
|
||||
func DemoReport(nodeCount int) report.Report {
|
||||
r := report.NewReport()
|
||||
|
||||
// Make up some plausible IPv4 numbers
|
||||
hosts := []string{}
|
||||
ip := [4]int{192, 168, 1, 1}
|
||||
for _ = range make([]struct{}, nodeCount) {
|
||||
hosts = append(hosts, fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]))
|
||||
ip[3]++
|
||||
if ip[3] > 200 {
|
||||
ip[2]++
|
||||
ip[3] = 1
|
||||
}
|
||||
}
|
||||
// Some non-local ones.
|
||||
hosts = append(hosts, []string{"1.2.3.4", "2.3.4.5"}...)
|
||||
|
||||
_, localNet, err := net.ParseCIDR("192.168.0.0/16")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
srcProc, dstProc string
|
||||
dstPort int
|
||||
}
|
||||
procPool := []conn{
|
||||
{srcProc: "curl", dstPort: 80, dstProc: "apache"},
|
||||
{srcProc: "wget", dstPort: 80, dstProc: "apache"},
|
||||
{srcProc: "curl", dstPort: 80, dstProc: "nginx"},
|
||||
{srcProc: "curl", dstPort: 8080, dstProc: "app1"},
|
||||
{srcProc: "nginx", dstPort: 8080, dstProc: "app1"},
|
||||
{srcProc: "nginx", dstPort: 8080, dstProc: "app2"},
|
||||
{srcProc: "nginx", dstPort: 8080, dstProc: "app3"},
|
||||
}
|
||||
connectionCount := nodeCount * 2
|
||||
for i := 0; i < connectionCount; i++ {
|
||||
var (
|
||||
c = procPool[rand.Intn(len(procPool))]
|
||||
src = hosts[rand.Intn(len(hosts))]
|
||||
dst = hosts[rand.Intn(len(hosts))]
|
||||
srcPort = rand.Intn(50000) + 10000
|
||||
srcPortID = fmt.Sprintf("%s%s%s%d", report.ScopeDelim, src, report.ScopeDelim, srcPort)
|
||||
dstPortID = fmt.Sprintf("%s%s%s%d", report.ScopeDelim, dst, report.ScopeDelim, c.dstPort)
|
||||
srcID = "hostX" + report.IDDelim + srcPortID
|
||||
dstID = "hostX" + report.IDDelim + dstPortID
|
||||
srcAddressID = fmt.Sprintf("%s%s", report.ScopeDelim, src)
|
||||
dstAddressID = fmt.Sprintf("%s%s", report.ScopeDelim, dst)
|
||||
nodeSrcAddressID = "hostX" + report.IDDelim + srcAddressID
|
||||
nodeDstAddressID = "hostX" + report.IDDelim + dstAddressID
|
||||
)
|
||||
|
||||
// Process topology
|
||||
if _, ok := r.Process.NodeMetadatas[srcPortID]; !ok {
|
||||
r.Process.NodeMetadatas[srcPortID] = report.NodeMetadata{
|
||||
"pid": "4000",
|
||||
"name": c.srcProc,
|
||||
"domain": "node-" + src,
|
||||
}
|
||||
}
|
||||
r.Process.Adjacency[srcID] = r.Process.Adjacency[srcID].Add(dstPortID)
|
||||
if _, ok := r.Process.NodeMetadatas[dstPortID]; !ok {
|
||||
r.Process.NodeMetadatas[dstPortID] = report.NodeMetadata{
|
||||
"pid": "4000",
|
||||
"name": c.dstProc,
|
||||
"domain": "node-" + dst,
|
||||
}
|
||||
}
|
||||
r.Process.Adjacency[dstID] = r.Process.Adjacency[dstID].Add(srcPortID)
|
||||
var (
|
||||
edgeKeyEgress = srcPortID + report.IDDelim + dstPortID
|
||||
edgeKeyIngress = dstPortID + report.IDDelim + srcPortID
|
||||
)
|
||||
r.Process.EdgeMetadatas[edgeKeyEgress] = report.EdgeMetadata{
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: uint(rand.Intn(100) + 10),
|
||||
}
|
||||
r.Process.EdgeMetadatas[edgeKeyIngress] = report.EdgeMetadata{
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: uint(rand.Intn(100) + 10),
|
||||
}
|
||||
|
||||
// Network topology
|
||||
if _, ok := r.Network.NodeMetadatas[srcAddressID]; !ok {
|
||||
r.Network.NodeMetadatas[srcAddressID] = report.NodeMetadata{
|
||||
"name": src,
|
||||
}
|
||||
}
|
||||
r.Network.Adjacency[nodeSrcAddressID] = r.Network.Adjacency[nodeSrcAddressID].Add(dstAddressID)
|
||||
if _, ok := r.Network.NodeMetadatas[dstAddressID]; !ok {
|
||||
r.Network.NodeMetadatas[dstAddressID] = report.NodeMetadata{
|
||||
"name": dst,
|
||||
}
|
||||
}
|
||||
r.Network.Adjacency[nodeDstAddressID] = r.Network.Adjacency[nodeDstAddressID].Add(srcAddressID)
|
||||
|
||||
// Host data
|
||||
r.HostMetadatas["hostX"] = report.HostMetadata{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Hostname: "host-x",
|
||||
LocalNets: []*net.IPNet{localNet},
|
||||
OS: "linux",
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
56
demoprobe/main.go
Normal file
56
demoprobe/main.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/xfer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
version = flag.Bool("version", false, "print version number and exit")
|
||||
publishInterval = flag.Duration("publish.interval", 1*time.Second, "publish (output) interval")
|
||||
listen = flag.String("listen", ":"+strconv.Itoa(xfer.ProbePort), "listen address")
|
||||
hostCount = flag.Int("hostcount", 10, "Number of demo hosts to generate")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) != 0 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// -version flag:
|
||||
if *version {
|
||||
fmt.Printf("unstable\n")
|
||||
return
|
||||
}
|
||||
|
||||
publisher, err := xfer.NewTCPPublisher(*listen)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
go func() {
|
||||
for {
|
||||
publisher.Publish(DemoReport(*hostCount))
|
||||
time.Sleep(*publishInterval)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("%s", <-interrupt())
|
||||
log.Printf("Shutting down...")
|
||||
}
|
||||
|
||||
func interrupt() chan os.Signal {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
return c
|
||||
}
|
||||
44
entrypoint.sh
Executable file
44
entrypoint.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
|
||||
usage() {
|
||||
echo "$0 --dns <IP> --hostname <NAME> --searchpath <SEARCHPATH>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# This script exists to modify the network settings in the scope containers
|
||||
# as docker doesn't allow it when started with --net=host
|
||||
while true; do
|
||||
case "$1" in
|
||||
--dns)
|
||||
[ $# -gt 1 ] || usage
|
||||
DNS_SERVER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--hostname)
|
||||
[ $# -gt 1 ] || usage
|
||||
HOSTNAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--searchpath)
|
||||
[ $# -gt 1 ] || usage
|
||||
SEARCHPATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$DNS_SERVER" -a -n "$SEARCHPATH" ]; then
|
||||
echo "domain $SEARCHPATH" >/etc/resolv.conf
|
||||
echo "search $SEARCHPATH" >>/etc/resolv.conf
|
||||
echo "nameserver $DNS_SERVER" >>/etc/resolv.conf
|
||||
fi
|
||||
|
||||
if [ -n "$HOSTNAME" ]; then
|
||||
echo "$HOSTNAME" >/etc/hostname
|
||||
hostname -F /etc/hostname
|
||||
fi
|
||||
|
||||
/usr/bin/supervisord -c /etc/supervisord.conf
|
||||
19
fixprobe/Makefile
Normal file
19
fixprobe/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
.PHONY: all vet lint build test clean
|
||||
|
||||
all: build test vet lint
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
lint:
|
||||
golint .
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
52
fixprobe/main.go
Normal file
52
fixprobe/main.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Publish a fixed report.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
"github.com/weaveworks/scope/scope/xfer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
publishInterval = flag.Duration("publish.interval", 1*time.Second, "publish (output) interval")
|
||||
listenAddress = flag.String("listen", ":"+strconv.Itoa(xfer.ProbePort), "listen address")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) != 1 {
|
||||
fmt.Printf("usage: fixprobe [--args] report.json\n")
|
||||
return
|
||||
}
|
||||
fixture := flag.Arg(0)
|
||||
|
||||
f, err := os.Open(fixture)
|
||||
if err != nil {
|
||||
fmt.Printf("json error: %v\n", err)
|
||||
return
|
||||
}
|
||||
var fixedReport report.Report
|
||||
if err := json.NewDecoder(f).Decode(&fixedReport); err != nil {
|
||||
fmt.Printf("json error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
publisher, err := xfer.NewTCPPublisher(*listenAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer publisher.Close()
|
||||
|
||||
log.Printf("listening on %s", *listenAddress)
|
||||
|
||||
for _ = range time.Tick(*publishInterval) {
|
||||
publisher.Publish(fixedReport)
|
||||
}
|
||||
}
|
||||
19
genreport/Makefile
Normal file
19
genreport/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
.PHONY: all vet lint build test clean
|
||||
|
||||
all: build test vet lint
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
lint:
|
||||
golint .
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
123
genreport/generate.go
Normal file
123
genreport/generate.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// DemoReport makes up a report.
|
||||
func DemoReport(nodeCount int) report.Report {
|
||||
r := report.NewReport()
|
||||
|
||||
// Make up some plausible IPv4 numbers
|
||||
hosts := []string{}
|
||||
ip := [4]int{192, 168, 1, 1}
|
||||
for _ = range make([]struct{}, nodeCount) {
|
||||
hosts = append(hosts, fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]))
|
||||
ip[3]++
|
||||
if ip[3] > 200 {
|
||||
ip[2]++
|
||||
ip[3] = 1
|
||||
}
|
||||
}
|
||||
// Some non-local ones.
|
||||
hosts = append(hosts, []string{"1.2.3.4", "2.3.4.5"}...)
|
||||
|
||||
_, localNet, err := net.ParseCIDR("192.168.0.0/16")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
srcProc, dstProc string
|
||||
dstPort int
|
||||
}
|
||||
procPool := []conn{
|
||||
{srcProc: "curl", dstPort: 80, dstProc: "apache"},
|
||||
{srcProc: "wget", dstPort: 80, dstProc: "apache"},
|
||||
{srcProc: "curl", dstPort: 80, dstProc: "nginx"},
|
||||
{srcProc: "curl", dstPort: 8080, dstProc: "app1"},
|
||||
{srcProc: "nginx", dstPort: 8080, dstProc: "app1"},
|
||||
{srcProc: "nginx", dstPort: 8080, dstProc: "app2"},
|
||||
{srcProc: "nginx", dstPort: 8080, dstProc: "app3"},
|
||||
}
|
||||
connectionCount := nodeCount * 8
|
||||
for i := 0; i < connectionCount; i++ {
|
||||
var (
|
||||
c = procPool[rand.Intn(len(procPool))]
|
||||
src = hosts[rand.Intn(len(hosts))]
|
||||
dst = hosts[rand.Intn(len(hosts))]
|
||||
srcPort = rand.Intn(50000) + 10000
|
||||
srcPortID = fmt.Sprintf("%s%s%s%d", report.ScopeDelim, src, report.ScopeDelim, srcPort)
|
||||
dstPortID = fmt.Sprintf("%s%s%s%d", report.ScopeDelim, dst, report.ScopeDelim, c.dstPort)
|
||||
srcID = "hostX" + report.IDDelim + srcPortID
|
||||
dstID = "hostX" + report.IDDelim + dstPortID
|
||||
srcAddressID = fmt.Sprintf("%s%s", report.ScopeDelim, src)
|
||||
dstAddressID = fmt.Sprintf("%s%s", report.ScopeDelim, dst)
|
||||
nodeSrcAddressID = "hostX" + report.IDDelim + srcAddressID
|
||||
nodeDstAddressID = "hostX" + report.IDDelim + dstAddressID
|
||||
)
|
||||
|
||||
// Process topology
|
||||
if _, ok := r.Process.NodeMetadatas[srcPortID]; !ok {
|
||||
r.Process.NodeMetadatas[srcPortID] = report.NodeMetadata{
|
||||
"pid": "4000",
|
||||
"name": c.srcProc,
|
||||
"domain": "node-" + src,
|
||||
}
|
||||
}
|
||||
r.Process.Adjacency[srcID] = r.Process.Adjacency[srcID].Add(dstPortID)
|
||||
if _, ok := r.Process.NodeMetadatas[dstPortID]; !ok {
|
||||
r.Process.NodeMetadatas[dstPortID] = report.NodeMetadata{
|
||||
"pid": "4000",
|
||||
"name": c.dstProc,
|
||||
"domain": "node-" + dst,
|
||||
}
|
||||
}
|
||||
r.Process.Adjacency[dstID] = r.Process.Adjacency[dstID].Add(srcPortID)
|
||||
var (
|
||||
edgeKeyEgress = srcPortID + report.IDDelim + dstPortID
|
||||
edgeKeyIngress = dstPortID + report.IDDelim + srcPortID
|
||||
)
|
||||
r.Process.EdgeMetadatas[edgeKeyEgress] = report.EdgeMetadata{
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: uint(rand.Intn(100) + 10),
|
||||
}
|
||||
r.Process.EdgeMetadatas[edgeKeyIngress] = report.EdgeMetadata{
|
||||
WithConnCountTCP: true,
|
||||
MaxConnCountTCP: uint(rand.Intn(100) + 10),
|
||||
}
|
||||
|
||||
// Network topology
|
||||
if _, ok := r.Network.NodeMetadatas[srcAddressID]; !ok {
|
||||
r.Network.NodeMetadatas[srcAddressID] = report.NodeMetadata{
|
||||
"name": src,
|
||||
}
|
||||
}
|
||||
r.Network.Adjacency[nodeSrcAddressID] = r.Network.Adjacency[nodeSrcAddressID].Add(dstAddressID)
|
||||
if _, ok := r.Network.NodeMetadatas[dstAddressID]; !ok {
|
||||
r.Network.NodeMetadatas[dstAddressID] = report.NodeMetadata{
|
||||
"name": dst,
|
||||
}
|
||||
}
|
||||
r.Network.Adjacency[nodeDstAddressID] = r.Network.Adjacency[nodeDstAddressID].Add(srcAddressID)
|
||||
|
||||
// Host data
|
||||
r.HostMetadatas["hostX"] = report.HostMetadata{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Hostname: "host-x",
|
||||
LocalNets: []*net.IPNet{localNet},
|
||||
OS: "linux",
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
13
genreport/main.go
Normal file
13
genreport/main.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
nodes := flag.Int("nodes", 10, "node count")
|
||||
flag.Parse()
|
||||
json.NewEncoder(os.Stdout).Encode(DemoReport(*nodes))
|
||||
}
|
||||
109
graphviz/handle.go
Normal file
109
graphviz/handle.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
func handleTXT(r Reporter) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
dot(w, r.Report().Process.RenderBy(mapFunc(req), classView(req)))
|
||||
}
|
||||
}
|
||||
|
||||
func handleSVG(r Reporter) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
cmd := exec.Command(engine(req), "-Tsvg")
|
||||
|
||||
wc, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Stdout = w
|
||||
|
||||
dot(wc, r.Report().Process.RenderBy(mapFunc(req), classView(req)))
|
||||
wc.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
if err := cmd.Run(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleHTML(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "<html><head>\n")
|
||||
fmt.Fprintf(w, `<meta http-equiv="refresh" content="3">`+"\n")
|
||||
fmt.Fprintf(w, "</head><body>\n")
|
||||
fmt.Fprintf(w, `<center><img src="/svg?%s" width="100%%" height="95%%"></center>`+"\n", r.URL.Query().Encode())
|
||||
fmt.Fprintf(w, "</body></html>\n")
|
||||
}
|
||||
|
||||
func dot(w io.Writer, m map[string]report.RenderableNode) {
|
||||
fmt.Fprintf(w, "digraph G {\n")
|
||||
fmt.Fprintf(w, "\tgraph [ overlap=false ];\n")
|
||||
fmt.Fprintf(w, "\tnode [ shape=circle, style=filled ];\n")
|
||||
fmt.Fprintf(w, "\toutputorder=edgesfirst;\n")
|
||||
fmt.Fprintf(w, "\n")
|
||||
|
||||
// Sorting the nodes seems to stop jumpiness.
|
||||
nodes := make(sort.StringSlice, 0, len(m))
|
||||
for _, node := range m {
|
||||
nodes = append(nodes, fmt.Sprintf("\t\"%s\" [label=\"%s\n%s\"];\n", node.ID, node.LabelMajor, node.LabelMinor))
|
||||
}
|
||||
sort.Sort(nodes)
|
||||
for _, s := range nodes {
|
||||
fmt.Fprint(w, s)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
|
||||
// Add ranking information by default.
|
||||
// Non-dot engines don't seem to be harmed by it.
|
||||
same := map[string][]string{}
|
||||
for _, node := range m {
|
||||
k, v := node.LabelMajor, fmt.Sprintf(`"%s"`, node.ID)
|
||||
same[k] = append(same[k], v)
|
||||
}
|
||||
for _, ids := range same {
|
||||
fmt.Fprintf(w, "\t{ rank=same; %s }\n", strings.Join(ids, " "))
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
|
||||
for _, src := range m {
|
||||
for _, dstID := range src.Adjacency {
|
||||
fmt.Fprintf(w, "\t\"%s\" -> \"%s\";\n", src.ID, dstID)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
}
|
||||
|
||||
func engine(r *http.Request) string {
|
||||
engine := r.FormValue("engine")
|
||||
if engine == "" {
|
||||
engine = "dot"
|
||||
}
|
||||
return engine
|
||||
}
|
||||
|
||||
func mapFunc(r *http.Request) report.MapFunc {
|
||||
f, ok := report.MapFuncRegistry[strings.ToLower(r.FormValue("map_func"))]
|
||||
if !ok {
|
||||
f = report.ProcessName
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func classView(r *http.Request) bool {
|
||||
return r.FormValue("class_view") == "true"
|
||||
}
|
||||
53
graphviz/main.go
Normal file
53
graphviz/main.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/xfer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
defaultProbes = fmt.Sprintf("localhost:%d", xfer.ProbePort)
|
||||
probes = flag.String("probes", defaultProbes, "list of probe endpoints, comma separated")
|
||||
batch = flag.Duration("batch", 1*time.Second, "batch interval")
|
||||
window = flag.Duration("window", 15*time.Second, "window")
|
||||
listen = flag.String("http.address", ":"+strconv.Itoa(xfer.AppPort), "webserver listen address")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
xfer.MaxBackoff = 10 * time.Second
|
||||
c := xfer.NewCollector(*batch)
|
||||
c.AddAddresses(strings.Split(*probes, ","))
|
||||
defer c.Stop()
|
||||
lifo := NewReportLIFO(c, *window)
|
||||
defer lifo.Stop()
|
||||
|
||||
http.Handle("/svg", handleSVG(lifo))
|
||||
http.Handle("/txt", handleTXT(lifo))
|
||||
http.Handle("/", http.HandlerFunc(handleHTML))
|
||||
|
||||
irq := interrupt()
|
||||
go func() {
|
||||
log.Printf("listening on %s", *listen)
|
||||
log.Print(http.ListenAndServe(*listen, nil))
|
||||
irq <- syscall.SIGINT
|
||||
}()
|
||||
<-irq
|
||||
log.Printf("shutting down")
|
||||
}
|
||||
|
||||
func interrupt() chan os.Signal {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
return c
|
||||
}
|
||||
91
graphviz/report_lifo.go
Normal file
91
graphviz/report_lifo.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
// Copy/paste from app/report_lifo.go
|
||||
|
||||
// Reporter XXX
|
||||
type Reporter interface {
|
||||
Report() report.Report
|
||||
}
|
||||
|
||||
type timedReport struct {
|
||||
report.Report
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// ReportLIFO XXX
|
||||
type ReportLIFO struct {
|
||||
reports []timedReport
|
||||
requests chan chan report.Report
|
||||
quit chan chan struct{}
|
||||
}
|
||||
|
||||
type reporter interface {
|
||||
Reports() <-chan report.Report
|
||||
}
|
||||
|
||||
// NewReportLIFO XXX
|
||||
func NewReportLIFO(r reporter, maxAge time.Duration) *ReportLIFO {
|
||||
l := ReportLIFO{
|
||||
reports: []timedReport{},
|
||||
requests: make(chan chan report.Report),
|
||||
quit: make(chan chan struct{}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case report := <-r.Reports():
|
||||
report = report.SquashRemote()
|
||||
tr := timedReport{
|
||||
Timestamp: time.Now(),
|
||||
Report: report,
|
||||
}
|
||||
l.reports = append(l.reports, tr)
|
||||
l.reports = cleanOld(l.reports, time.Now().Add(-maxAge))
|
||||
|
||||
case req := <-l.requests:
|
||||
report := report.NewReport()
|
||||
for _, r := range l.reports {
|
||||
report.Merge(r.Report)
|
||||
}
|
||||
req <- report
|
||||
|
||||
case q := <-l.quit:
|
||||
close(q)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return &l
|
||||
}
|
||||
|
||||
// Stop XXX
|
||||
func (r *ReportLIFO) Stop() {
|
||||
q := make(chan struct{})
|
||||
r.quit <- q
|
||||
<-q
|
||||
}
|
||||
|
||||
// Report XXX
|
||||
func (r *ReportLIFO) Report() report.Report {
|
||||
req := make(chan report.Report)
|
||||
r.requests <- req
|
||||
return <-req
|
||||
}
|
||||
|
||||
func cleanOld(reports []timedReport, threshold time.Time) []timedReport {
|
||||
res := make([]timedReport, 0, len(reports))
|
||||
for _, tr := range reports {
|
||||
if tr.Timestamp.Before(threshold) {
|
||||
continue
|
||||
}
|
||||
res = append(res, tr)
|
||||
}
|
||||
return res
|
||||
}
|
||||
19
integration/Makefile
Normal file
19
integration/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
.PHONY: all vet lint build test clean
|
||||
|
||||
all: build test vet lint
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
lint:
|
||||
golint .
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
81
integration/cmd.go
Normal file
81
integration/cmd.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// cmdline is e.g. `fixprobe -publish.interval=10ms fixture.json`
|
||||
func start(t *testing.T, cmdline string) *exec.Cmd {
|
||||
toks := strings.Split(cmdline, " ")
|
||||
if len(toks) <= 0 {
|
||||
t.Fatalf("bad cmdline %q", cmdline)
|
||||
}
|
||||
|
||||
component, args := toks[0], toks[1:]
|
||||
|
||||
relpath := fmt.Sprintf("../%s/%s", component, component)
|
||||
|
||||
if _, err := os.Stat(relpath); err != nil {
|
||||
t.Fatalf("%s: %s", component, err)
|
||||
}
|
||||
|
||||
cmd := &exec.Cmd{
|
||||
Dir: filepath.Dir(relpath),
|
||||
Path: filepath.Base(relpath),
|
||||
Args: append([]string{filepath.Base(relpath)}, args...),
|
||||
Stdout: testWriter{t, component},
|
||||
Stderr: testWriter{t, component},
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("%s: Start: %s", component, err)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func stop(t *testing.T, c *exec.Cmd) {
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
if err := c.Process.Kill(); err != nil {
|
||||
t.Fatalf("%s: Kill: %s", filepath.Base(c.Path), err)
|
||||
}
|
||||
|
||||
if _, err := c.Process.Wait(); err != nil {
|
||||
t.Fatalf("%s: Wait: %s", filepath.Base(c.Path), err)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(250 * time.Millisecond):
|
||||
t.Fatalf("timeout when trying to stop %s", filepath.Base(c.Path))
|
||||
}
|
||||
}
|
||||
|
||||
type testWriter struct {
|
||||
*testing.T
|
||||
component string
|
||||
}
|
||||
|
||||
func (w testWriter) Write(p []byte) (int, error) {
|
||||
w.T.Logf("<%10s> %s", w.component, p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func cwd() string {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
79
integration/contexts.go
Normal file
79
integration/contexts.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type context int
|
||||
|
||||
const (
|
||||
oneProbe context = iota
|
||||
twoProbes
|
||||
)
|
||||
|
||||
var (
|
||||
appPort = 14030
|
||||
bridgePort = 14020
|
||||
probePort1 = 14010
|
||||
probePort2 = 14011
|
||||
)
|
||||
|
||||
func withContext(t *testing.T, c context, tests ...func()) {
|
||||
var (
|
||||
publish = 10 * time.Millisecond
|
||||
batch = 10 * publish
|
||||
wait = 2 * batch
|
||||
)
|
||||
|
||||
switch c {
|
||||
case oneProbe:
|
||||
probe := start(t, fmt.Sprintf(`fixprobe -listen=:%d -publish.interval=%s %s/test_single_report.json`, probePort1, publish, cwd()))
|
||||
defer stop(t, probe)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
app := start(t, fmt.Sprintf(`app -http.address=:%d -probes=localhost:%d -batch=%s`, appPort, probePort1, batch))
|
||||
defer stop(t, app)
|
||||
|
||||
case twoProbes:
|
||||
probe1 := start(t, fmt.Sprintf(`fixprobe -listen=:%d -publish.interval=%s %s/test_single_report.json`, probePort1, publish, cwd()))
|
||||
defer stop(t, probe1)
|
||||
|
||||
probe2 := start(t, fmt.Sprintf(`fixprobe -listen=:%d -publish.interval=%s %s/test_extra_report.json`, probePort2, publish, cwd()))
|
||||
defer stop(t, probe2)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
app := start(t, fmt.Sprintf(`app -http.address=:%d -probes=localhost:%d,localhost:%d -batch=%s`, appPort, probePort1, probePort2, batch))
|
||||
defer stop(t, app)
|
||||
|
||||
default:
|
||||
t.Fatalf("bad context %v", c)
|
||||
}
|
||||
|
||||
time.Sleep(wait)
|
||||
|
||||
for _, f := range tests {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
func httpGet(t *testing.T, url string) []byte {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Fatalf("httpGet: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if status := resp.StatusCode; status != 200 {
|
||||
t.Fatalf("httpGet got status %d, expected 200", status)
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("httpGet: %s", err)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
2
integration/doc.go
Normal file
2
integration/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package integration implements integration tests between components.
|
||||
package integration
|
||||
98
integration/easy_test.go
Normal file
98
integration/easy_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
)
|
||||
|
||||
func TestComponentsAreAvailable(t *testing.T) {
|
||||
var pause = 1 * time.Millisecond
|
||||
|
||||
for _, c := range []string{
|
||||
fmt.Sprintf(`app -http.address=:%d`, appPort),
|
||||
fmt.Sprintf(`bridge -listen=:%d`, bridgePort),
|
||||
fmt.Sprintf(`fixprobe -listen=:%d`, probePort1),
|
||||
fmt.Sprintf(`demoprobe -listen=:%d`, probePort1),
|
||||
} {
|
||||
cmd := start(t, c)
|
||||
time.Sleep(pause)
|
||||
stop(t, cmd)
|
||||
t.Logf("%s: OK", filepath.Base(cmd.Path))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplications(t *testing.T) {
|
||||
withContext(t, oneProbe, func() {
|
||||
topo := parseTopology(t, httpGet(t, fmt.Sprintf("http://localhost:%d/api/topology/applications", appPort)))
|
||||
assertAdjacent(t, topo["proc:node-1.2.3.4:apache"], "theinternet", "proc:node-192.168.1.1:wget")
|
||||
|
||||
have := parseEdge(t, httpGet(t, fmt.Sprintf("http://localhost:%d/api/topology/applications/%s/%s", appPort, "proc:node-192.168.1.1:wget", "theinternet")))
|
||||
want := map[string]interface{}{
|
||||
"max_conn_count_tcp": float64(19),
|
||||
}
|
||||
if !reflect.DeepEqual(have, want) {
|
||||
t.Errorf("have: %#v, want %#v", have, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHosts(t *testing.T) {
|
||||
withContext(t, oneProbe, func() {
|
||||
topo := parseTopology(t, httpGet(t, fmt.Sprintf("http://localhost:%d/api/topology/hosts", appPort)))
|
||||
assertAdjacent(t, topo["host:1_2_3_4"], "theinternet", "host:192_168_1_1")
|
||||
|
||||
have := parseEdge(t, httpGet(t, fmt.Sprintf("http://localhost:%d/api/topology/hosts/%s/%s", appPort, "host:192_168_1_1", "theinternet")))
|
||||
want := map[string]interface{}{
|
||||
// "window": "15s",
|
||||
"max_conn_count_tcp": float64(12),
|
||||
}
|
||||
if !reflect.DeepEqual(have, want) {
|
||||
t.Errorf("have: %#v, want %#v", have, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleProbes(t *testing.T) {
|
||||
withContext(t, twoProbes, func() {
|
||||
topo := parseTopology(t, httpGet(t, fmt.Sprintf("http://localhost:%d/api/topology/applications", appPort)))
|
||||
assertAdjacent(t, topo["proc:node-1.2.3.4:apache"], "theinternet", "proc:node-192.168.1.1:wget", "proc:node-192.168.1.1:curl")
|
||||
})
|
||||
}
|
||||
|
||||
func parseTopology(t *testing.T, p []byte) map[string]report.RenderableNode {
|
||||
var r struct {
|
||||
Nodes map[string]report.RenderableNode `json:"nodes"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(p, &r); err != nil {
|
||||
t.Fatalf("parseTopology: %s", err)
|
||||
}
|
||||
|
||||
return r.Nodes
|
||||
}
|
||||
|
||||
func parseEdge(t *testing.T, p []byte) map[string]interface{} {
|
||||
var edge struct {
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(p, &edge); err != nil {
|
||||
t.Fatalf("Err: %v", err)
|
||||
}
|
||||
|
||||
return edge.Metadata
|
||||
}
|
||||
|
||||
func assertAdjacent(t *testing.T, n report.RenderableNode, ids ...string) {
|
||||
want := report.NewIDList(ids...)
|
||||
|
||||
if have := n.Adjacency; !reflect.DeepEqual(want, have) {
|
||||
t.Fatalf("want adjacency list %v, have %v", want, have)
|
||||
}
|
||||
}
|
||||
0
integration/ignore_tests
Normal file
0
integration/ignore_tests
Normal file
63
integration/test_extra_report.json
Normal file
63
integration/test_extra_report.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"Process": {
|
||||
"Adjacency": {
|
||||
"hostX|;1.2.3.4;80": [
|
||||
";192.168.1.1;10747",
|
||||
"theinternet"
|
||||
],
|
||||
"hostX|;192.168.1.1;10747": [
|
||||
"theinternet"
|
||||
]
|
||||
},
|
||||
"EdgeMetadatas": {},
|
||||
"NodeMetadatas": {
|
||||
";1.2.3.4;80": {
|
||||
"domain": "node-1.2.3.4",
|
||||
"name": "apache",
|
||||
"pid": "4000"
|
||||
},
|
||||
";192.168.1.1;10747": {
|
||||
"domain": "node-192.168.1.1",
|
||||
"name": "curl",
|
||||
"pid": "4001"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"Network": {
|
||||
"Adjacency": {
|
||||
"hostX|;1.2.3.4": [
|
||||
";192.168.1.1",
|
||||
"theinternet"
|
||||
],
|
||||
"hostX|;192.168.1.1": [
|
||||
"theinternet",
|
||||
";192.168.1.1"
|
||||
]
|
||||
},
|
||||
"EdgeMetadatas": {},
|
||||
"NodeMetadatas": {
|
||||
";1.2.3.4": {
|
||||
"name": "1.2.3.4"
|
||||
},
|
||||
";192.168.1.1": {
|
||||
"name": "192.168.1.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"HostMetadatas": {
|
||||
"hostX": {
|
||||
"Timestamp": "2014-10-28T23:12:23.451728464Z",
|
||||
"Hostname": "host-x",
|
||||
"Addresses": null,
|
||||
"LocalNets": [
|
||||
{
|
||||
"IP": "192.168.0.0",
|
||||
"Mask": "//8AAA=="
|
||||
}
|
||||
],
|
||||
"OS": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
73
integration/test_single_report.json
Normal file
73
integration/test_single_report.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"Process": {
|
||||
"Adjacency": {
|
||||
"hostX|;1.2.3.4;80": [
|
||||
";192.168.1.1;10746",
|
||||
"theinternet"
|
||||
],
|
||||
"hostX|;192.168.1.1;10746": [
|
||||
"theinternet"
|
||||
]
|
||||
},
|
||||
"EdgeMetadatas": {
|
||||
";192.168.1.1;10746|theinternet": {
|
||||
"WithConnCountTCP": true,
|
||||
"MaxConnCountTCP": 19
|
||||
}
|
||||
},
|
||||
"NodeMetadatas": {
|
||||
";1.2.3.4;80": {
|
||||
"domain": "node-1.2.3.4",
|
||||
"name": "apache",
|
||||
"pid": "4000"
|
||||
},
|
||||
";192.168.1.1;10746": {
|
||||
"domain": "node-192.168.1.1",
|
||||
"name": "wget",
|
||||
"pid": "4000"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"Network": {
|
||||
"Adjacency": {
|
||||
"hostX|;1.2.3.4": [
|
||||
";192.168.1.1",
|
||||
"theinternet"
|
||||
],
|
||||
"hostX|;192.168.1.1": [
|
||||
"theinternet",
|
||||
";192.168.1.1"
|
||||
]
|
||||
},
|
||||
"EdgeMetadatas": {
|
||||
";192.168.1.1|theinternet": {
|
||||
"WithConnCountTCP": true,
|
||||
"MaxConnCountTCP": 12
|
||||
}
|
||||
},
|
||||
"NodeMetadatas": {
|
||||
";1.2.3.4": {
|
||||
"name": "1_2_3_4"
|
||||
},
|
||||
";192.168.1.1": {
|
||||
"name": "192_168_1_1"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"HostMetadatas": {
|
||||
"hostX": {
|
||||
"Timestamp": "2014-10-28T23:12:23.451728464Z",
|
||||
"Hostname": "host-x",
|
||||
"Addresses": null,
|
||||
"LocalNets": [
|
||||
{
|
||||
"IP": "192.168.0.0",
|
||||
"Mask": "//8AAA=="
|
||||
}
|
||||
],
|
||||
"OS": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
oneshot/Makefile
Normal file
19
oneshot/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
.PHONY: all vet lint build test clean
|
||||
|
||||
all: build test vet lint
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
lint:
|
||||
golint .
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
58
oneshot/main.go
Normal file
58
oneshot/main.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/scope/scope/report"
|
||||
"github.com/weaveworks/scope/scope/xfer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
defaultProbes = fmt.Sprintf("localhost:%d", xfer.ProbePort)
|
||||
probes = flag.String("probes", defaultProbes, "list of probe endpoints, comma separated")
|
||||
)
|
||||
flag.Parse()
|
||||
if len(flag.Args()) != 0 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Collector deals with the probes, and generates merged reports.
|
||||
xfer.MaxBackoff = 1 * time.Second
|
||||
c := xfer.NewCollector(1 * time.Second)
|
||||
c.AddAddresses(strings.Split(*probes, ","))
|
||||
defer c.Stop()
|
||||
|
||||
report := report.NewReport()
|
||||
irq := interrupt()
|
||||
OUTER:
|
||||
for {
|
||||
select {
|
||||
case r := <-c.Reports():
|
||||
report.Merge(r)
|
||||
case <-irq:
|
||||
break OUTER
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Print(string(b))
|
||||
}
|
||||
|
||||
func interrupt() chan os.Signal {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
return c
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user