{"action":"created","comment":{"url":"https://api.github.com/repos/weaveworks/scope/pulls/comments/59239581","id":59239581,"diff_hunk":"@@ -0,0 +1,293 @@\n+package plugins\n+\n+import (\n+\t\"fmt\"\n+\t\"net/http\"\n+\t\"net/url\"\n+\t\"path/filepath\"\n+\t\"sort\"\n+\t\"strings\"\n+\t\"sync\"\n+\t\"syscall\"\n+\t\"time\"\n+\n+\tlog \"github.com/Sirupsen/logrus\"\n+\t\"github.com/ugorji/go/codec\"\n+\t\"golang.org/x/net/context\"\n+\t\"golang.org/x/net/context/ctxhttp\"\n+\n+\t\"github.com/weaveworks/scope/common/backoff\"\n+\t\"github.com/weaveworks/scope/common/fs\"\n+\t\"github.com/weaveworks/scope/common/xfer\"\n+\t\"github.com/weaveworks/scope/report\"\n+)\n+\n+// Exposed for testing\n+var (\n+\ttransport = makeUnixRoundTripper\n+)\n+\n+const (\n+\tpluginTimeout   = 500 * time.Millisecond\n+\tpluginRetry     = 5 * time.Second\n+\tpollingInterval = 5 * time.Second\n+)\n+\n+// Registry maintains a list of available plugins by name.\n+type Registry struct {\n+\trootPath          string\n+\tapiVersion        string\n+\thandshakeMetadata map[string]string\n+\tpluginsBySocket   map[string]*Plugin\n+\tlock              sync.RWMutex\n+\tcontext           context.Context\n+\tcancel            context.CancelFunc\n+}\n+\n+// NewRegistry creates a new registry which watches the given dir root for new\n+// plugins, and adds them.\n+func NewRegistry(rootPath, apiVersion string, handshakeMetadata map[string]string) (*Registry, error) {\n+\tctx, cancel := context.WithCancel(context.Background())\n+\tr := &Registry{\n+\t\trootPath:          rootPath,\n+\t\tapiVersion:        apiVersion,\n+\t\thandshakeMetadata: handshakeMetadata,\n+\t\tpluginsBySocket:   map[string]*Plugin{},\n+\t\tcontext:           ctx,\n+\t\tcancel:            cancel,\n+\t}\n+\tif err := r.scan(); err != nil {\n+\t\tr.Close()\n+\t\treturn nil, err\n+\t}\n+\tgo r.loop()\n+\treturn r, nil\n+}\n+\n+// loop periodically rescans for plugins\n+func (r *Registry) loop() {\n+\tticker := time.NewTicker(pollingInterval)\n+\tdefer ticker.Stop()\n+\tfor {\n+\t\tselect {\n+\t\tcase <-r.context.Done():\n+\t\t\treturn\n+\t\tcase <-ticker.C:\n+\t\t\tlog.Debugf(\"plugins: scanning...\")\n+\t\t\tif err := r.scan(); err != nil {\n+\t\t\t\tlog.Warningf(\"plugins: error: %v\", err)\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n+// Rescan the plugins directory, load new plugins, and remove missing plugins\n+func (r *Registry) scan() error {\n+\tsockets, err := r.sockets(r.rootPath)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tr.lock.Lock()\n+\tplugins := map[string]*Plugin{}\n+\t// add (or keep) plugins which were found\n+\tfor _, path := range sockets {\n+\t\tif plugin, ok := r.pluginsBySocket[path]; ok {\n+\t\t\tplugins[path] = plugin\n+\t\t\tcontinue\n+\t\t}\n+\t\ttr, err := transport(path, pluginTimeout)\n+\t\tif err != nil {\n+\t\t\tlog.Warningf(\"plugins: error loading plugin %s: %v\", path, err)\n+\t\t\tcontinue\n+\t\t}\n+\t\tclient := &http.Client{Transport: tr, Timeout: pluginTimeout}\n+\t\tplugins[path] = NewPlugin(r.context, path, client, r.apiVersion, r.handshakeMetadata)\n+\t}\n+\t// remove plugins which weren't found\n+\tfor path, plugin := range r.pluginsBySocket {\n+\t\tif _, ok := plugins[path]; !ok {\n+\t\t\tplugin.Close()\n+\t\t\tlog.Infof(\"plugins: removed plugin %s\", plugin.socket)\n+\t\t}\n+\t}\n+\tr.pluginsBySocket = plugins\n+\tr.lock.Unlock()\n+\treturn nil\n+}\n+\n+// sockets recursively finds all unix sockets under the path provided\n+func (r *Registry) sockets(path string) ([]string, error) {\n+\tvar (\n+\t\tresult []string\n+\t\tstatT  syscall.Stat_t\n+\t)\n+\t// TODO: use of fs.Stat (which is syscall.Stat) here makes this linux specific.\n+\tif err := fs.Stat(path, &statT); err != nil {\n+\t\treturn nil, err\n+\t}\n+\tswitch statT.Mode & syscall.S_IFMT {\n+\tcase syscall.S_IFDIR:\n+\t\tfiles, err := fs.ReadDir(path)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tfor _, file := range files {\n+\t\t\tfpath := filepath.Join(path, file.Name())\n+\t\t\ts, err := r.sockets(fpath)\n+\t\t\tif err != nil {\n+\t\t\t\tlog.Warningf(\"plugins: error loading path %s: %v\", fpath, err)\n+\t\t\t}\n+\t\t\tresult = append(result, s...)\n+\t\t}\n+\tcase syscall.S_IFSOCK:\n+\t\tresult = append(result, path)\n+\t}\n+\treturn result, nil\n+}\n+\n+// ForEach walks through all the plugins running f for each one.\n+func (r *Registry) ForEach(f func(p *Plugin)) {\n+\tr.lock.RLock()\n+\tdefer r.lock.RUnlock()\n+\tpaths := []string{}\n+\tfor path := range r.pluginsBySocket {\n+\t\tpaths = append(paths, path)\n+\t}\n+\tsort.Strings(paths)\n+\tfor _, path := range paths {\n+\t\tf(r.pluginsBySocket[path])\n+\t}\n+}\n+\n+// Implementers walks the available plugins fulfilling the given interface\n+func (r *Registry) Implementers(iface string, f func(p *Plugin)) {\n+\tr.ForEach(func(p *Plugin) {\n+\t\tfor _, piface := range p.Interfaces {\n+\t\t\tif piface == iface {\n+\t\t\t\tf(p)\n+\t\t\t}\n+\t\t}\n+\t})\n+}\n+\n+// Name implements the Reporter interface\n+func (r *Registry) Name() string { return \"plugins\" }\n+\n+// Report implements the Reporter interface\n+func (r *Registry) Report() (report.Report, error) {\n+\trpt := report.MakeReport()\n+\tr.Implementers(\"reporter\", func(plugin *Plugin) {\n+\t\tpluginReport, err := plugin.Report()\n+\t\tif err != nil {\n+\t\t\tlog.Errorf(\"plugins: error getting report from %s: %v\", plugin.ID, err)\n+\t\t\tpluginReport = report.MakeReport()\n+\t\t}\n+\t\tpluginReport.Plugins = pluginReport.Plugins.Add(plugin.PluginSpec)\n+\t\trpt = rpt.Merge(pluginReport)\n+\t})\n+\treturn rpt, nil\n+}\n+\n+// Close shuts down the registry. It can still be used after this, but will be\n+// out of date.\n+func (r *Registry) Close() {\n+\tr.cancel()\n+\tr.lock.Lock()\n+\tdefer r.lock.Unlock()\n+\tfor _, plugin := range r.pluginsBySocket {\n+\t\tplugin.Close()\n+\t}\n+}\n+\n+// Plugin is the implementation of a plugin. It is responsible for doing the\n+// plugin handshake, gathering reports, etc.\n+type Plugin struct {\n+\txfer.PluginSpec\n+\tcontext context.Context\n+\tsocket  string\n+\tclient  *http.Client\n+\tcancel  context.CancelFunc\n+\tbackoff backoff.Interface\n+}\n+\n+// NewPlugin loads and initializes a new plugin. If client is nil,\n+// http.DefaultClient will be used.\n+func NewPlugin(ctx context.Context, socket string, client *http.Client, expectedAPIVersion string, handshakeMetadata map[string]string) *Plugin {\n+\tparams := url.Values{}\n+\tfor k, v := range handshakeMetadata {\n+\t\tparams.Add(k, v)\n+\t}\n+\n+\tctx, cancel := context.WithCancel(ctx)\n+\tp := &Plugin{context: ctx, socket: socket, client: client, cancel: cancel}\n+\tf := p.handshake(ctx, expectedAPIVersion, params)\n+\tf() // try the first time synchronously\n+\tp.backoff = backoff.New(f, \"plugin handshake\")\n+\tgo p.backoff.Start()\n+\treturn p\n+}\n+\n+type handshakeResponse struct {\n+\tName        string   `json:\"name\"`\n+\tDescription string   `json:\"description,omitempty\"`\n+\tInterfaces  []string `json:\"interfaces\"`\n+\tAPIVersion  string   `json:\"api_version,omitempty\"`\n+}\n+\n+// handshake tries the handshake with this plugin.\n+func (p *Plugin) handshake(ctx context.Context, expectedAPIVersion string, params url.Values) func() (bool, error) {\n+\treturn func() (ok bool, err error) {\n+\t\tdefer func() { p.setStatus(err) }()\n+\t\tvar resp handshakeResponse\n+\t\tif err := p.get(\"/\", params, &resp); err != nil {\n+\t\t\treturn err == context.Canceled, fmt.Errorf(\"plugins: error loading plugin %s: %v\", p.socket, err)\n+\t\t}\n+\n+\t\tif resp.Name == \"\" {\n+\t\t\treturn false, fmt.Errorf(\"plugins: error loading plugin %s: plugin did not provide a name\", p.socket)\n+\t\t}\n+\t\tif resp.APIVersion != expectedAPIVersion {\n+\t\t\treturn false, fmt.Errorf(\"plugins: error loading plugin %s: plugin did not provide correct API version: expected %q, got %q\", p.socket, expectedAPIVersion, resp.APIVersion)\n+\t\t}\n+\t\tp.ID, p.Label = resp.Name, resp.Name\n+\t\tp.Description = resp.Description\n+\t\tp.Interfaces = resp.Interfaces\n+\t\tlog.Infof(\"plugins: loaded plugin %s: %s\", p.ID, strings.Join(p.Interfaces, \", \"))\n+\t\treturn true, nil\n+\t}\n+}\n+\n+// Report gets the latest report from the plugin\n+func (p *Plugin) Report() (report.Report, error) {\n+\tresult := report.MakeReport()\n+\terr := p.get(\"/report\", nil, &result)\n+\tp.setStatus(err)\n+\treturn result, err\n+}\n+\n+func (p *Plugin) setStatus(err error) {\n+\tif err == nil {\n+\t\tp.Status = \"ok\"\n+\t} else {\n+\t\tp.Status = fmt.Sprintf(\"error: %v\", err)\n+\t}\n+}\n+\n+// TODO(paulbellamy): better error handling on wrong status codes","path":"probe/plugins/registry.go","position":283,"original_position":277,"commit_id":"4a0b42b23273eef1828b1cab99d71cf71bd234f1","original_commit_id":"6b10a726bf9b93dc2bba5144574b499a48e2a218","user":{"login":"2opremio","id":2362916,"avatar_url":"https://avatars.githubusercontent.com/u/2362916?v=3","gravatar_id":"","url":"https://api.github.com/users/2opremio","html_url":"https://github.com/2opremio","followers_url":"https://api.github.com/users/2opremio/followers","following_url":"https://api.github.com/users/2opremio/following{/other_user}","gists_url":"https://api.github.com/users/2opremio/gists{/gist_id}","starred_url":"https://api.github.com/users/2opremio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/2opremio/subscriptions","organizations_url":"https://api.github.com/users/2opremio/orgs","repos_url":"https://api.github.com/users/2opremio/repos","events_url":"https://api.github.com/users/2opremio/events{/privacy}","received_events_url":"https://api.github.com/users/2opremio/received_events","type":"User","site_admin":false},"body":"This comment doesn't apply anymore, right? (You are now handling error)","created_at":"2016-04-11T16:53:43Z","updated_at":"2016-04-11T16:53:43Z","html_url":"https://github.com/weaveworks/scope/pull/1126#discussion_r59239581","pull_request_url":"https://api.github.com/repos/weaveworks/scope/pulls/1126","_links":{"self":{"href":"https://api.github.com/repos/weaveworks/scope/pulls/comments/59239581"},"html":{"href":"https://github.com/weaveworks/scope/pull/1126#discussion_r59239581"},"pull_request":{"href":"https://api.github.com/repos/weaveworks/scope/pulls/1126"}}},"pull_request":{"url":"https://api.github.com/repos/weaveworks/scope/pulls/1126","id":61744756,"html_url":"https://github.com/weaveworks/scope/pull/1126","diff_url":"https://github.com/weaveworks/scope/pull/1126.diff","patch_url":"https://github.com/weaveworks/scope/pull/1126.patch","issue_url":"https://api.github.com/repos/weaveworks/scope/issues/1126","number":1126,"state":"open","locked":false,"title":"Plugins","user":{"login":"paulbellamy","id":250199,"avatar_url":"https://avatars.githubusercontent.com/u/250199?v=3","gravatar_id":"","url":"https://api.github.com/users/paulbellamy","html_url":"https://github.com/paulbellamy","followers_url":"https://api.github.com/users/paulbellamy/followers","following_url":"https://api.github.com/users/paulbellamy/following{/other_user}","gists_url":"https://api.github.com/users/paulbellamy/gists{/gist_id}","starred_url":"https://api.github.com/users/paulbellamy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/paulbellamy/subscriptions","organizations_url":"https://api.github.com/users/paulbellamy/orgs","repos_url":"https://api.github.com/users/paulbellamy/repos","events_url":"https://api.github.com/users/paulbellamy/events{/privacy}","received_events_url":"https://api.github.com/users/paulbellamy/received_events","type":"User","site_admin":false},"body":"Fixes #554\r\n\r\nCommits are still super-messy, and tests are earmarked, as this is both a work-in-progress and a proof-of-concept.\r\n\r\nHTTP Request latency is shown on the weavescope node (we can correlate it due to weavescope being in the host network).\r\n\r\nPlugin registry turned out very similar to #809.\r\n\r\nTODO:\r\n- [x] figure out what the sysdig querying looks like and how flexible it is\r\n    - [x] do some plugin data from the raw sysdig stream\r\n      - bandwidth usage per-process, maybe\r\n      - can we parse stuff fast enough? or filter it better?\r\n- [ ] ~~get better data from sysdig plugin~~\r\n    - Either need to:\r\n      - implement the plugin in lua (possible?)\r\n      - make the lua chisel more flexible and add an interface for the go plugin to control it\r\n      - just add more aggregations for each type of data\r\n- [x] show plugin list in UI\r\n    - [x] styling\r\n    - [ ] maybe adjust timers? or include it with report/node updates?\r\n- [x] clean up and make plugins code sane/tested\r\n- [x] add safe zero-values for everything in a report so json deserialization is safer on malformed inputs\r\n- [x] all templates (not just extra, and including metrics) need to come from the probe\r\n- [x] add deadline/timeout on plugin calls\r\n- [x] add api versioning to plugin handshake\r\n- [x] re-organize code to be consistent with controls\r\n- [x] clean up todos\r\n- [x] consider rest api instead of jsonrpc\r\n- [x] add periodic retry on dead plugin sockets (for if they restart)\r\n- [ ] ~~replace example plugin with something else~~ Pushed eBPF plugin into a separate PR.\r\n  - [ ] add check to ebpf plugin for if kernel is new enough\r\n  - [ ] port ebpf plugin to python, and do counting in kernel\r\n  - [ ] figure out how to get pids from ebpf, so we can get more accurate counts per/process\r\n- [ ] ~~consider an async \"plugin-push\" interface instead of probe polling.~~ Considered it. Delaying to a future \"api version\"\r\n- [x] see about removing render/detailed/labels.go\r\n- [x] final test fixup\r\n- [x] documentation\r\n- [ ] update plugins when they change\r\n  - [ ] lets ditch handshake, and just assume all plugins respond to `/report`, then parse the pluginspec from that.","created_at":"2016-03-04T17:34:33Z","updated_at":"2016-04-11T16:53:43Z","closed_at":null,"merged_at":null,"merge_commit_sha":"a97f42abc3aea9a4c1131842efbdae229e2323a6","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/weaveworks/scope/pulls/1126/commits","review_comments_url":"https://api.github.com/repos/weaveworks/scope/pulls/1126/comments","review_comment_url":"https://api.github.com/repos/weaveworks/scope/pulls/comments{/number}","comments_url":"https://api.github.com/repos/weaveworks/scope/issues/1126/comments","statuses_url":"https://api.github.com/repos/weaveworks/scope/statuses/4a0b42b23273eef1828b1cab99d71cf71bd234f1","head":{"label":"weaveworks:plugins","ref":"plugins","sha":"4a0b42b23273eef1828b1cab99d71cf71bd234f1","user":{"login":"weaveworks","id":9976052,"avatar_url":"https://avatars.githubusercontent.com/u/9976052?v=3","gravatar_id":"","url":"https://api.github.com/users/weaveworks","html_url":"https://github.com/weaveworks","followers_url":"https://api.github.com/users/weaveworks/followers","following_url":"https://api.github.com/users/weaveworks/following{/other_user}","gists_url":"https://api.github.com/users/weaveworks/gists{/gist_id}","starred_url":"https://api.github.com/users/weaveworks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/weaveworks/subscriptions","organizations_url":"https://api.github.com/users/weaveworks/orgs","repos_url":"https://api.github.com/users/weaveworks/repos","events_url":"https://api.github.com/users/weaveworks/events{/privacy}","received_events_url":"https://api.github.com/users/weaveworks/received_events","type":"Organization","site_admin":false},"repo":{"id":34659664,"name":"scope","full_name":"weaveworks/scope","owner":{"login":"weaveworks","id":9976052,"avatar_url":"https://avatars.githubusercontent.com/u/9976052?v=3","gravatar_id":"","url":"https://api.github.com/users/weaveworks","html_url":"https://github.com/weaveworks","followers_url":"https://api.github.com/users/weaveworks/followers","following_url":"https://api.github.com/users/weaveworks/following{/other_user}","gists_url":"https://api.github.com/users/weaveworks/gists{/gist_id}","starred_url":"https://api.github.com/users/weaveworks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/weaveworks/subscriptions","organizations_url":"https://api.github.com/users/weaveworks/orgs","repos_url":"https://api.github.com/users/weaveworks/repos","events_url":"https://api.github.com/users/weaveworks/events{/privacy}","received_events_url":"https://api.github.com/users/weaveworks/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/weaveworks/scope","description":"Monitoring, visualisation & management for Docker","fork":false,"url":"https://api.github.com/repos/weaveworks/scope","forks_url":"https://api.github.com/repos/weaveworks/scope/forks","keys_url":"https://api.github.com/repos/weaveworks/scope/keys{/key_id}","collaborators_url":"https://api.github.com/repos/weaveworks/scope/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/weaveworks/scope/teams","hooks_url":"https://api.github.com/repos/weaveworks/scope/hooks","issue_events_url":"https://api.github.com/repos/weaveworks/scope/issues/events{/number}","events_url":"https://api.github.com/repos/weaveworks/scope/events","assignees_url":"https://api.github.com/repos/weaveworks/scope/assignees{/user}","branches_url":"https://api.github.com/repos/weaveworks/scope/branches{/branch}","tags_url":"https://api.github.com/repos/weaveworks/scope/tags","blobs_url":"https://api.github.com/repos/weaveworks/scope/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/weaveworks/scope/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/weaveworks/scope/git/refs{/sha}","trees_url":"https://api.github.com/repos/weaveworks/scope/git/trees{/sha}","statuses_url":"https://api.github.com/repos/weaveworks/scope/statuses/{sha}","languages_url":"https://api.github.com/repos/weaveworks/scope/languages","stargazers_url":"https://api.github.com/repos/weaveworks/scope/stargazers","contributors_url":"https://api.github.com/repos/weaveworks/scope/contributors","subscribers_url":"https://api.github.com/repos/weaveworks/scope/subscribers","subscription_url":"https://api.github.com/repos/weaveworks/scope/subscription","commits_url":"https://api.github.com/repos/weaveworks/scope/commits{/sha}","git_commits_url":"https://api.github.com/repos/weaveworks/scope/git/commits{/sha}","comments_url":"https://api.github.com/repos/weaveworks/scope/comments{/number}","issue_comment_url":"https://api.github.com/repos/weaveworks/scope/issues/comments{/number}","contents_url":"https://api.github.com/repos/weaveworks/scope/contents/{+path}","compare_url":"https://api.github.com/repos/weaveworks/scope/compare/{base}...{head}","merges_url":"https://api.github.com/repos/weaveworks/scope/merges","archive_url":"https://api.github.com/repos/weaveworks/scope/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/weaveworks/scope/downloads","issues_url":"https://api.github.com/repos/weaveworks/scope/issues{/number}","pulls_url":"https://api.github.com/repos/weaveworks/scope/pulls{/number}","milestones_url":"https://api.github.com/repos/weaveworks/scope/milestones{/number}","notifications_url":"https://api.github.com/repos/weaveworks/scope/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/weaveworks/scope/labels{/name}","releases_url":"https://api.github.com/repos/weaveworks/scope/releases{/id}","deployments_url":"https://api.github.com/repos/weaveworks/scope/deployments","created_at":"2015-04-27T10:12:35Z","updated_at":"2016-04-11T14:19:34Z","pushed_at":"2016-04-11T16:53:08Z","git_url":"git://github.com/weaveworks/scope.git","ssh_url":"git@github.com:weaveworks/scope.git","clone_url":"https://github.com/weaveworks/scope.git","svn_url":"https://github.com/weaveworks/scope","homepage":"http://weave.works/product/scope/","size":55218,"stargazers_count":574,"watchers_count":574,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":36,"mirror_url":null,"open_issues_count":188,"forks":36,"open_issues":188,"watchers":574,"default_branch":"master"}},"base":{"label":"weaveworks:master","ref":"master","sha":"d237f52a905525b43d2554de6462b58ae716a468","user":{"login":"weaveworks","id":9976052,"avatar_url":"https://avatars.githubusercontent.com/u/9976052?v=3","gravatar_id":"","url":"https://api.github.com/users/weaveworks","html_url":"https://github.com/weaveworks","followers_url":"https://api.github.com/users/weaveworks/followers","following_url":"https://api.github.com/users/weaveworks/following{/other_user}","gists_url":"https://api.github.com/users/weaveworks/gists{/gist_id}","starred_url":"https://api.github.com/users/weaveworks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/weaveworks/subscriptions","organizations_url":"https://api.github.com/users/weaveworks/orgs","repos_url":"https://api.github.com/users/weaveworks/repos","events_url":"https://api.github.com/users/weaveworks/events{/privacy}","received_events_url":"https://api.github.com/users/weaveworks/received_events","type":"Organization","site_admin":false},"repo":{"id":34659664,"name":"scope","full_name":"weaveworks/scope","owner":{"login":"weaveworks","id":9976052,"avatar_url":"https://avatars.githubusercontent.com/u/9976052?v=3","gravatar_id":"","url":"https://api.github.com/users/weaveworks","html_url":"https://github.com/weaveworks","followers_url":"https://api.github.com/users/weaveworks/followers","following_url":"https://api.github.com/users/weaveworks/following{/other_user}","gists_url":"https://api.github.com/users/weaveworks/gists{/gist_id}","starred_url":"https://api.github.com/users/weaveworks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/weaveworks/subscriptions","organizations_url":"https://api.github.com/users/weaveworks/orgs","repos_url":"https://api.github.com/users/weaveworks/repos","events_url":"https://api.github.com/users/weaveworks/events{/privacy}","received_events_url":"https://api.github.com/users/weaveworks/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/weaveworks/scope","description":"Monitoring, visualisation & management for Docker","fork":false,"url":"https://api.github.com/repos/weaveworks/scope","forks_url":"https://api.github.com/repos/weaveworks/scope/forks","keys_url":"https://api.github.com/repos/weaveworks/scope/keys{/key_id}","collaborators_url":"https://api.github.com/repos/weaveworks/scope/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/weaveworks/scope/teams","hooks_url":"https://api.github.com/repos/weaveworks/scope/hooks","issue_events_url":"https://api.github.com/repos/weaveworks/scope/issues/events{/number}","events_url":"https://api.github.com/repos/weaveworks/scope/events","assignees_url":"https://api.github.com/repos/weaveworks/scope/assignees{/user}","branches_url":"https://api.github.com/repos/weaveworks/scope/branches{/branch}","tags_url":"https://api.github.com/repos/weaveworks/scope/tags","blobs_url":"https://api.github.com/repos/weaveworks/scope/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/weaveworks/scope/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/weaveworks/scope/git/refs{/sha}","trees_url":"https://api.github.com/repos/weaveworks/scope/git/trees{/sha}","statuses_url":"https://api.github.com/repos/weaveworks/scope/statuses/{sha}","languages_url":"https://api.github.com/repos/weaveworks/scope/languages","stargazers_url":"https://api.github.com/repos/weaveworks/scope/stargazers","contributors_url":"https://api.github.com/repos/weaveworks/scope/contributors","subscribers_url":"https://api.github.com/repos/weaveworks/scope/subscribers","subscription_url":"https://api.github.com/repos/weaveworks/scope/subscription","commits_url":"https://api.github.com/repos/weaveworks/scope/commits{/sha}","git_commits_url":"https://api.github.com/repos/weaveworks/scope/git/commits{/sha}","comments_url":"https://api.github.com/repos/weaveworks/scope/comments{/number}","issue_comment_url":"https://api.github.com/repos/weaveworks/scope/issues/comments{/number}","contents_url":"https://api.github.com/repos/weaveworks/scope/contents/{+path}","compare_url":"https://api.github.com/repos/weaveworks/scope/compare/{base}...{head}","merges_url":"https://api.github.com/repos/weaveworks/scope/merges","archive_url":"https://api.github.com/repos/weaveworks/scope/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/weaveworks/scope/downloads","issues_url":"https://api.github.com/repos/weaveworks/scope/issues{/number}","pulls_url":"https://api.github.com/repos/weaveworks/scope/pulls{/number}","milestones_url":"https://api.github.com/repos/weaveworks/scope/milestones{/number}","notifications_url":"https://api.github.com/repos/weaveworks/scope/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/weaveworks/scope/labels{/name}","releases_url":"https://api.github.com/repos/weaveworks/scope/releases{/id}","deployments_url":"https://api.github.com/repos/weaveworks/scope/deployments","created_at":"2015-04-27T10:12:35Z","updated_at":"2016-04-11T14:19:34Z","pushed_at":"2016-04-11T16:53:08Z","git_url":"git://github.com/weaveworks/scope.git","ssh_url":"git@github.com:weaveworks/scope.git","clone_url":"https://github.com/weaveworks/scope.git","svn_url":"https://github.com/weaveworks/scope","homepage":"http://weave.works/product/scope/","size":55218,"stargazers_count":574,"watchers_count":574,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":36,"mirror_url":null,"open_issues_count":188,"forks":36,"open_issues":188,"watchers":574,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/weaveworks/scope/pulls/1126"},"html":{"href":"https://github.com/weaveworks/scope/pull/1126"},"issue":{"href":"https://api.github.com/repos/weaveworks/scope/issues/1126"},"comments":{"href":"https://api.github.com/repos/weaveworks/scope/issues/1126/comments"},"review_comments":{"href":"https://api.github.com/repos/weaveworks/scope/pulls/1126/comments"},"review_comment":{"href":"https://api.github.com/repos/weaveworks/scope/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/weaveworks/scope/pulls/1126/commits"},"statuses":{"href":"https://api.github.com/repos/weaveworks/scope/statuses/4a0b42b23273eef1828b1cab99d71cf71bd234f1"}}}}