{"action":"created","comment":{"url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/comments/170135158","pull_request_review_id":98777743,"id":170135158,"diff_hunk":"@@ -5,114 +5,237 @@ es = require 'event-stream'\n Lock = require 'rwlock'\n { checkTruthy } = require './lib/validation'\n \n-LOG_PUBLISH_INTERVAL = 110\n+class NativeLoggerBackend\n+\tconstructor: ->\n+\t\t@logPublishInterval = 1000\n+\t\t@maxLogLinesPerBatch = 10\n+\t\t@maxQueueSize = 100\n+\t\t@maxLineLength = 300\n+\t\t@publishQueue = []\n+\t\t@intervalHandle = null\n+\t\t@publishEnabled = false\n+\t\t@offlineMode = false\n+\t\t@shouldStop = false\n+\n+\tpublishLoop: =>\n+\t\tstartTime = process.hrtime()\n+\t\tPromise.try =>\n+\t\t\treturn if @offlineMode or !@publishEnabled or @publishQueue.length is 0\n+\t\t\tcurrentBatch = _.slice(@publishQueue, 0, @maxLogLinesPerBatch)\n+\t\t\t@publishQueue = _.slice(@publishQueue, @maxLogLinesPerBatch)\n+\t\t\t# Silently ignore errors sending logs (just like with pubnub)\n+\t\t\t@apiBinder.logBatch(currentBatch)\n+\t\t.catchReturn()\n+\t\t.then =>\n+\t\t\telapsedTime = process.hrtime(startTime)\n+\t\t\telapsedTimeMs = elapsedTime[0] * 1000 + elapsedTime[1] / 1e6\n+\t\t\tnextDelay = Math.min(@logPublishInterval - elapsedTimeMs, 0)\n+\t\t\tPromise.delay(nextDelay)\n+\t\t.then =>\n+\t\t\tif @shouldStop\n+\t\t\t\treturn\n+\t\t\t@publishLoop()\n \n-# Pubnub's message size limit is 32KB (unclear on whether it's KB or actually KiB,\n-# but we'll be conservative). So we limit a log message to 2 bytes less to account\n-# for the [ and ] in the array.\n-MAX_LOG_BYTE_SIZE = 30000\n-MAX_MESSAGE_INDEX = 9\n+\t_truncateToMaxLength: (message) =>\n+\t\tif message.message.length > @maxLineLength\n+\t\t\tmessage = _.clone(message)\n+\t\t\tmessage.message = _.truncate(message.message, { length: @maxLineLength, omission: '[...]' })\n+\t\treturn message\n \n-module.exports = class Logger\n-\tconstructor: ({ @eventTracker }) ->\n+\tlogDependent: (msg, { uuid }) =>\n+\t\tmsg = @_truncateToMaxLength(msg)\n+\t\t@apiBinder.logDependent(uuid, msg)\n+\n+\tlog: (message) =>\n+\t\treturn if @offlineMode or !@publishEnabled or @publishQueue.length >= @maxQueueSize\n+\t\tif _.isString(message)\n+\t\t\tmessage = { message: message }\n+\n+\t\tmessage = _.defaults({}, message, {\n+\t\t\ttimestamp: Date.now()\n+\t\t\tmessage: ''\n+\t\t})\n+\n+\t\tmessage = @_truncateToMaxLength(message)\n+\n+\t\tif @publishQueue.length < @maxQueueSize - 1\n+\t\t\t@publishQueue.push(_.pick(message, [ 'message', 'timestamp', 'isSystem', 'isStderr', 'imageId' ]))\n+\t\telse\n+\t\t\t@publishQueue.push({\n+\t\t\t\tmessage: 'Warning! Some logs dropped due to high load'\n+\t\t\t\ttimestamp: Date.now()\n+\t\t\t\tisSystem: true\n+\t\t\t\tisStderr: true\n+\t\t\t})\n+\n+\tstart: (opts) =>\n+\t\tconsole.log('Starting native logger')\n+\t\t@apiBinder = opts.apiBinder\n+\t\t@publishEnabled = checkTruthy(opts.enable)\n+\t\t@offlineMode = checkTruthy(opts.offlineMode)\n+\t\t@publishLoop()\n+\t\treturn null\n+\n+\tstop: =>\n+\t\tconsole.log('Stopping native logger')\n+\t\t@shouldStop = true\n+\n+class PubnubLoggerBackend\n+\tconstructor: ->\n \t\t@publishQueue = [[]]\n \t\t@messageIndex = 0\n-\t\t@publishQueueRemainingBytes = MAX_LOG_BYTE_SIZE\n+\t\t@maxMessageIndex = 9\n+\t\t# Pubnub's message size limit is 32KB (unclear on whether it's KB or actually KiB,\n+\t\t# but we'll be conservative). So we limit a log message to 2 bytes less to account\n+\t\t# for the [ and ] in the array.\n+\t\t@maxLogByteSize = 30000\n+\t\t@publishQueueRemainingBytes = @maxLogByteSize\n \t\t@logsOverflow = false\n-\t\t_lock = new Lock()\n-\t\t@_writeLock = Promise.promisify(_lock.async.writeLock)\n-\t\t@attached = {}\n-\t\t@offlineMode = false\n-\t\t@publishEnabled = false\n-\n-\tinit: ({ pubnub, channel, offlineMode, enable }) =>\n-\t\tPromise.try =>\n-\t\t\t@pubnub = PUBNUB.init(pubnub)\n-\t\t\t@channel = channel\n-\t\t\t@publishEnabled = checkTruthy(enable)\n-\t\t\t@offlineMode = checkTruthy(offlineMode)\n-\t\t\tsetInterval(@doPublish, LOG_PUBLISH_INTERVAL)\n-\n-\tenable: (val) =>\n-\t\t@publishEnabled = checkTruthy(val) ? true\n+\t\t@logPublishInterval = 110\n \n \tdoPublish: =>\n \t\treturn if @offlineMode or !@publishEnabled or @publishQueue[0].length is 0\n \t\tmessage = @publishQueue.shift()\n \t\t@pubnub.publish({ @channel, message })\n \t\tif @publishQueue.length is 0\n \t\t\t@publishQueue = [[]]\n-\t\t\t@publishQueueRemainingBytes = MAX_LOG_BYTE_SIZE\n+\t\t\t@publishQueueRemainingBytes = @maxLogByteSize\n \t\t@messageIndex = Math.max(@messageIndex - 1, 0)\n-\t\t@logsOverflow = false if @messageIndex < MAX_MESSAGE_INDEX\n+\t\t@logsOverflow = false if @messageIndex < @maxMessageIndex\n \n-\tpublish: (message) =>\n-\t\t# Disable sending logs for bandwidth control\n-\t\treturn if @offlineMode or !@publishEnabled or (@messageIndex >= MAX_MESSAGE_INDEX and @publishQueueRemainingBytes <= 0)\n+\tlogDependent: (message, { channel }) ->\n+\t\t@pubnub.publish({ channel, message })\n+\n+\tlog: (msg) =>\n+\t\treturn if @offlineMode or !@publishEnabled or (@messageIndex >= @maxMessageIndex and @publishQueueRemainingBytes <= 0)\n \t\tif _.isString(message)\n-\t\t\tmessage = { m: message }\n+\t\t\tmessage = { m: msg }\n+\t\telse\n+\t\t\tmessage = {\n+\t\t\t\tm: msg.message\n+\t\t\t\tt: msg.timestamp\n+\t\t\t}\n+\t\t\tif msg.isSystem\n+\t\t\t\tmessage.s = 1\n+\t\t\tif msg.serviceId?\n+\t\t\t\tmessage.c = msg.serviceId\n+\t\t\tif msg.isStderr\n+\t\t\t\tmessage.e = 1\n \n \t\t_.defaults message,\n \t\t\tt: Date.now()\n \t\t\tm: ''\n+\n \t\tmsgLength = Buffer.byteLength(encodeURIComponent(JSON.stringify(message)), 'utf8')\n-\t\treturn if msgLength > MAX_LOG_BYTE_SIZE # Unlikely, but we can't allow this\n+\t\treturn if msgLength > @maxLogByteSize # Unlikely, but we can't allow this\n \t\tremaining = @publishQueueRemainingBytes - msgLength\n \t\tif remaining >= 0\n \t\t\t@publishQueue[@messageIndex].push(message)\n \t\t\t@publishQueueRemainingBytes = remaining\n-\t\telse if @messageIndex < MAX_MESSAGE_INDEX\n+\t\telse if @messageIndex < @maxMessageIndex\n \t\t\t@messageIndex += 1\n \t\t\t@publishQueue[@messageIndex] = [ message ]\n-\t\t\t@publishQueueRemainingBytes = MAX_LOG_BYTE_SIZE - msgLength\n+\t\t\t@publishQueueRemainingBytes = @maxLogByteSize - msgLength\n \t\telse if !@logsOverflow\n \t\t\t@logsOverflow = true\n \t\t\t@messageIndex += 1\n \t\t\t@publishQueue[@messageIndex] = [ { m: 'Warning! Some logs dropped due to high load', t: Date.now(), s: 1 } ]\n \t\t\t@publishQueueRemainingBytes = 0\n \n-\t# log allows publishing logs through the regular way which includes a queue and has a specific channel\n-\t# or, when a channel is specified, publishing directly to that channel\n-\tlog: (msg, opts = {}) =>\n-\t\tif opts.channel?\n-\t\t\t@pubnub.publish({ channel: opts.channel, message: msg })\n+\tstart: (opts) =>\n+\t\tconsole.log('Starting pubnub logger')\n+\t\t@pubnub = PUBNUB.init(opts.pubnub)\n+\t\t@channel = opts.channel\n+\t\t@publishEnabled = checkTruthy(opts.enable)\n+\t\t@offlineMode = checkTruthy(opts.offlineMode)\n+\t\t@intervalHandle = setInterval(@doPublish, @logPublishInterval)\n+\n+\tstop: =>\n+\t\tconsole.log('Stopping pubnub logger')\n+\t\tclearInterval(@intervalHandle)\n+\n+module.exports = class Logger\n+\tconstructor: ({ @eventTracker }) ->\n+\t\t_lock = new Lock()\n+\t\t@_writeLock = Promise.promisify(_lock.async.writeLock)\n+\t\t@attached = { stdout: {}, stderr: {} }\n+\t\t@opts = {}\n+\t\t@backend = null\n+\n+\tinit: (opts) =>\n+\t\tPromise.try =>\n+\t\t\t@opts = opts\n+\t\t\t@_startBackend()\n+\n+\t_startBackend: =>\n+\t\tif checkTruthy(@opts.nativeLogger)\n+\t\t\t@backend = new NativeLoggerBackend()\n \t\telse\n-\t\t\t@publish(msg)\n+\t\t\t@backend = new PubnubLoggerBackend()\n+\t\t@backend.start(@opts)\n+\n+\tswitchBackend: (nativeLogger) =>\n+\t\tif checkTruthy(nativeLogger) != checkTruthy(@opts.nativeLogger)\n+\t\t\t@opts.nativeLogger = nativeLogger\n+\t\t\t@backend.stop()\n+\t\t\t@_startBackend()\n+\n+\tenable: (val) =>\n+\t\t@opts.enable = val\n+\t\t@backend.publishEnabled = checkTruthy(val) ? true\n+\n+\tlogDependent: (msg, device) =>\n+\t\t@backend.logDependent(msg, device)\n+\n+\tlog: (msg) =>\n+\t\t@backend.log(msg)\n \n-\tlogSystemMessage: (message, obj, eventName) =>\n-\t\tmessageObj = { m: message, s: 1 }\n-\t\tif obj?.serviceId?\n-\t\t\tmessageObj.c = obj.serviceId\n+\tlogSystemMessage: (msg, obj, eventName) =>\n+\t\tmessageObj = { message: msg, isSystem: true }\n+\t\tif obj?.error?\n+\t\t\tmessageObj.isStderr = true\n \t\t@log(messageObj)\n-\t\t@eventTracker.track(eventName ? message, obj)\n+\t\t@eventTracker.track(eventName ? msg, obj)\n \n \tlock: (containerId) =>\n \t\t@_writeLock(containerId)\n \t\t.disposer (release) ->\n \t\t\trelease()\n \n-\tattach: (docker, containerId, serviceId) =>\n-\t\tPromise.using @lock(containerId), =>\n-\t\t\tif !@attached[containerId]\n+\t_attachStream: (docker, stdoutOrStderr, containerId, { serviceId, imageId }) =>\n+\t\tPromise.try =>\n+\t\t\tif stdoutOrStderr not in [ 'stdout', 'stderr' ]\n+\t\t\t\tthrow new Error(\"Invalid log selection #{stdoutOrStderr}\")\n+\t\t\tif !@attached[stdoutOrStderr][containerId]\n+\t\t\t\tlogsOpts = { follow: true, stdout: stdoutOrStderr == 'stdout', stderr: stdoutOrStderr == 'stderr', timestamps: true }\n \t\t\t\tdocker.getContainer(containerId)\n-\t\t\t\t.logs({ follow: true, stdout: true, stderr: true, timestamps: true })\n+\t\t\t\t.logs(logsOpts)\n \t\t\t\t.then (stream) =>\n-\t\t\t\t\t@attached[containerId] = true\n+\t\t\t\t\t@attached[stdoutOrStderr][containerId] = true\n \t\t\t\t\tstream\n \t\t\t\t\t.on 'error', (err) =>\n \t\t\t\t\t\tconsole.error('Error on container logs', err, err.stack)\n-\t\t\t\t\t\t@attached[containerId] = false\n+\t\t\t\t\t\t@attached[stdoutOrStderr][containerId] = false\n \t\t\t\t\t.pipe(es.split())\n \t\t\t\t\t.on 'data', (logLine) =>\n \t\t\t\t\t\tspace = logLine.indexOf(' ')\n \t\t\t\t\t\tif space > 0\n-\t\t\t\t\t\t\tmsg = { t: logLine.substr(0, space), m: logLine.substr(space + 1), c: serviceId }\n-\t\t\t\t\t\t\t@publish(msg)\n+\t\t\t\t\t\t\tmsg = { timestamp: logLine.substr(0, space), message: logLine.substr(space + 1), serviceId, imageId }\n+\t\t\t\t\t\t\tif stdoutOrStderr == 'stderr'\n+\t\t\t\t\t\t\t\tmsg.isStderr = true\n+\t\t\t\t\t\t\t@log(msg)\n \t\t\t\t\t.on 'error', (err) =>\n \t\t\t\t\t\tconsole.error('Error on container logs', err, err.stack)\n-\t\t\t\t\t\t@attached[containerId] = false\n+\t\t\t\t\t\t@attached[stdoutOrStderr][containerId] = false\n \t\t\t\t\t.on 'end', =>\n-\t\t\t\t\t\t@attached[containerId] = false\n+\t\t\t\t\t\t@attached[stdoutOrStderr][containerId] = false\n+\n+\tattach: (docker, containerId, serviceInfo) =>\n+\t\tPromise.using @lock(containerId), =>\n+\t\t\t@_attachStream(docker, 'stdout', containerId, serviceInfo)\n+\t\t\t.then =>","path":"src/logger.coffee","position":null,"original_position":287,"commit_id":"7d0555f7c98d989bc217d37bb5a10be96aa2ac20","original_commit_id":"e5ff5b3f5b125d111233354ebb710d06c0340501","user":{"login":"pcarranzav","id":2223590,"avatar_url":"https://avatars1.githubusercontent.com/u/2223590?v=4","gravatar_id":"","url":"https://api.github.com/users/pcarranzav","html_url":"https://github.com/pcarranzav","followers_url":"https://api.github.com/users/pcarranzav/followers","following_url":"https://api.github.com/users/pcarranzav/following{/other_user}","gists_url":"https://api.github.com/users/pcarranzav/gists{/gist_id}","starred_url":"https://api.github.com/users/pcarranzav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pcarranzav/subscriptions","organizations_url":"https://api.github.com/users/pcarranzav/orgs","repos_url":"https://api.github.com/users/pcarranzav/repos","events_url":"https://api.github.com/users/pcarranzav/events{/privacy}","received_events_url":"https://api.github.com/users/pcarranzav/received_events","type":"User","site_admin":false},"body":"Done","created_at":"2018-02-23T00:24:15Z","updated_at":"2018-02-23T00:24:15Z","html_url":"https://github.com/resin-io/resin-supervisor/pull/545#discussion_r170135158","pull_request_url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/comments/170135158"},"html":{"href":"https://github.com/resin-io/resin-supervisor/pull/545#discussion_r170135158"},"pull_request":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545"}},"in_reply_to_id":170089328},"pull_request":{"url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545","id":162787406,"html_url":"https://github.com/resin-io/resin-supervisor/pull/545","diff_url":"https://github.com/resin-io/resin-supervisor/pull/545.diff","patch_url":"https://github.com/resin-io/resin-supervisor/pull/545.patch","issue_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues/545","number":545,"state":"open","locked":false,"title":"Multicontainer logger","user":{"login":"pcarranzav","id":2223590,"avatar_url":"https://avatars1.githubusercontent.com/u/2223590?v=4","gravatar_id":"","url":"https://api.github.com/users/pcarranzav","html_url":"https://github.com/pcarranzav","followers_url":"https://api.github.com/users/pcarranzav/followers","following_url":"https://api.github.com/users/pcarranzav/following{/other_user}","gists_url":"https://api.github.com/users/pcarranzav/gists{/gist_id}","starred_url":"https://api.github.com/users/pcarranzav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pcarranzav/subscriptions","organizations_url":"https://api.github.com/users/pcarranzav/orgs","repos_url":"https://api.github.com/users/pcarranzav/repos","events_url":"https://api.github.com/users/pcarranzav/events{/privacy}","received_events_url":"https://api.github.com/users/pcarranzav/received_events","type":"User","site_admin":false},"body":"Work in progress to implement logging to the Resin API on the multicontainer supervisor.\r\n\r\nDuring an experimental phase, logs can be switched between Pubnub and Resin with a config variable.\r\n\r\nLogs can have an imageId attached, which the resin API uses to infer the image_install that each log relates to. Logs sent to Pubnub, instead, just have a `c` to identify the serviceId that they relate to.\r\n\r\n(Still untested)","created_at":"2018-01-13T16:53:25Z","updated_at":"2018-02-23T00:24:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c7f3da6394ef8faeb462de50085f8303e63df67f","assignee":{"login":"pcarranzav","id":2223590,"avatar_url":"https://avatars1.githubusercontent.com/u/2223590?v=4","gravatar_id":"","url":"https://api.github.com/users/pcarranzav","html_url":"https://github.com/pcarranzav","followers_url":"https://api.github.com/users/pcarranzav/followers","following_url":"https://api.github.com/users/pcarranzav/following{/other_user}","gists_url":"https://api.github.com/users/pcarranzav/gists{/gist_id}","starred_url":"https://api.github.com/users/pcarranzav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pcarranzav/subscriptions","organizations_url":"https://api.github.com/users/pcarranzav/orgs","repos_url":"https://api.github.com/users/pcarranzav/repos","events_url":"https://api.github.com/users/pcarranzav/events{/privacy}","received_events_url":"https://api.github.com/users/pcarranzav/received_events","type":"User","site_admin":false},"assignees":[{"login":"pcarranzav","id":2223590,"avatar_url":"https://avatars1.githubusercontent.com/u/2223590?v=4","gravatar_id":"","url":"https://api.github.com/users/pcarranzav","html_url":"https://github.com/pcarranzav","followers_url":"https://api.github.com/users/pcarranzav/followers","following_url":"https://api.github.com/users/pcarranzav/following{/other_user}","gists_url":"https://api.github.com/users/pcarranzav/gists{/gist_id}","starred_url":"https://api.github.com/users/pcarranzav/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pcarranzav/subscriptions","organizations_url":"https://api.github.com/users/pcarranzav/orgs","repos_url":"https://api.github.com/users/pcarranzav/repos","events_url":"https://api.github.com/users/pcarranzav/events{/privacy}","received_events_url":"https://api.github.com/users/pcarranzav/received_events","type":"User","site_admin":false}],"requested_reviewers":[{"login":"Page-","id":360115,"avatar_url":"https://avatars3.githubusercontent.com/u/360115?v=4","gravatar_id":"","url":"https://api.github.com/users/Page-","html_url":"https://github.com/Page-","followers_url":"https://api.github.com/users/Page-/followers","following_url":"https://api.github.com/users/Page-/following{/other_user}","gists_url":"https://api.github.com/users/Page-/gists{/gist_id}","starred_url":"https://api.github.com/users/Page-/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Page-/subscriptions","organizations_url":"https://api.github.com/users/Page-/orgs","repos_url":"https://api.github.com/users/Page-/repos","events_url":"https://api.github.com/users/Page-/events{/privacy}","received_events_url":"https://api.github.com/users/Page-/received_events","type":"User","site_admin":false},{"login":"petrosagg","id":939420,"avatar_url":"https://avatars3.githubusercontent.com/u/939420?v=4","gravatar_id":"","url":"https://api.github.com/users/petrosagg","html_url":"https://github.com/petrosagg","followers_url":"https://api.github.com/users/petrosagg/followers","following_url":"https://api.github.com/users/petrosagg/following{/other_user}","gists_url":"https://api.github.com/users/petrosagg/gists{/gist_id}","starred_url":"https://api.github.com/users/petrosagg/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/petrosagg/subscriptions","organizations_url":"https://api.github.com/users/petrosagg/orgs","repos_url":"https://api.github.com/users/petrosagg/repos","events_url":"https://api.github.com/users/petrosagg/events{/privacy}","received_events_url":"https://api.github.com/users/petrosagg/received_events","type":"User","site_admin":false},{"login":"CameronDiver","id":7694128,"avatar_url":"https://avatars0.githubusercontent.com/u/7694128?v=4","gravatar_id":"","url":"https://api.github.com/users/CameronDiver","html_url":"https://github.com/CameronDiver","followers_url":"https://api.github.com/users/CameronDiver/followers","following_url":"https://api.github.com/users/CameronDiver/following{/other_user}","gists_url":"https://api.github.com/users/CameronDiver/gists{/gist_id}","starred_url":"https://api.github.com/users/CameronDiver/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CameronDiver/subscriptions","organizations_url":"https://api.github.com/users/CameronDiver/orgs","repos_url":"https://api.github.com/users/CameronDiver/repos","events_url":"https://api.github.com/users/CameronDiver/events{/privacy}","received_events_url":"https://api.github.com/users/CameronDiver/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[{"id":401685105,"url":"https://api.github.com/repos/resin-io/resin-supervisor/labels/flow/in-review","name":"flow/in-review","color":"e180ff","default":false},{"id":598832805,"url":"https://api.github.com/repos/resin-io/resin-supervisor/labels/procbots/versionbot/no-checks","name":"procbots/versionbot/no-checks","color":"0052cc","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545/commits","review_comments_url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545/comments","review_comment_url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/comments{/number}","comments_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues/545/comments","statuses_url":"https://api.github.com/repos/resin-io/resin-supervisor/statuses/7d0555f7c98d989bc217d37bb5a10be96aa2ac20","head":{"label":"resin-io:multicontainer-logger","ref":"multicontainer-logger","sha":"7d0555f7c98d989bc217d37bb5a10be96aa2ac20","user":{"login":"resin-io","id":6157842,"avatar_url":"https://avatars1.githubusercontent.com/u/6157842?v=4","gravatar_id":"","url":"https://api.github.com/users/resin-io","html_url":"https://github.com/resin-io","followers_url":"https://api.github.com/users/resin-io/followers","following_url":"https://api.github.com/users/resin-io/following{/other_user}","gists_url":"https://api.github.com/users/resin-io/gists{/gist_id}","starred_url":"https://api.github.com/users/resin-io/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/resin-io/subscriptions","organizations_url":"https://api.github.com/users/resin-io/orgs","repos_url":"https://api.github.com/users/resin-io/repos","events_url":"https://api.github.com/users/resin-io/events{/privacy}","received_events_url":"https://api.github.com/users/resin-io/received_events","type":"Organization","site_admin":false},"repo":{"id":44161944,"name":"resin-supervisor","full_name":"resin-io/resin-supervisor","owner":{"login":"resin-io","id":6157842,"avatar_url":"https://avatars1.githubusercontent.com/u/6157842?v=4","gravatar_id":"","url":"https://api.github.com/users/resin-io","html_url":"https://github.com/resin-io","followers_url":"https://api.github.com/users/resin-io/followers","following_url":"https://api.github.com/users/resin-io/following{/other_user}","gists_url":"https://api.github.com/users/resin-io/gists{/gist_id}","starred_url":"https://api.github.com/users/resin-io/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/resin-io/subscriptions","organizations_url":"https://api.github.com/users/resin-io/orgs","repos_url":"https://api.github.com/users/resin-io/repos","events_url":"https://api.github.com/users/resin-io/events{/privacy}","received_events_url":"https://api.github.com/users/resin-io/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/resin-io/resin-supervisor","description":"Resin Supervisor: resin.io's agent on devices.","fork":false,"url":"https://api.github.com/repos/resin-io/resin-supervisor","forks_url":"https://api.github.com/repos/resin-io/resin-supervisor/forks","keys_url":"https://api.github.com/repos/resin-io/resin-supervisor/keys{/key_id}","collaborators_url":"https://api.github.com/repos/resin-io/resin-supervisor/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/resin-io/resin-supervisor/teams","hooks_url":"https://api.github.com/repos/resin-io/resin-supervisor/hooks","issue_events_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues/events{/number}","events_url":"https://api.github.com/repos/resin-io/resin-supervisor/events","assignees_url":"https://api.github.com/repos/resin-io/resin-supervisor/assignees{/user}","branches_url":"https://api.github.com/repos/resin-io/resin-supervisor/branches{/branch}","tags_url":"https://api.github.com/repos/resin-io/resin-supervisor/tags","blobs_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/refs{/sha}","trees_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/trees{/sha}","statuses_url":"https://api.github.com/repos/resin-io/resin-supervisor/statuses/{sha}","languages_url":"https://api.github.com/repos/resin-io/resin-supervisor/languages","stargazers_url":"https://api.github.com/repos/resin-io/resin-supervisor/stargazers","contributors_url":"https://api.github.com/repos/resin-io/resin-supervisor/contributors","subscribers_url":"https://api.github.com/repos/resin-io/resin-supervisor/subscribers","subscription_url":"https://api.github.com/repos/resin-io/resin-supervisor/subscription","commits_url":"https://api.github.com/repos/resin-io/resin-supervisor/commits{/sha}","git_commits_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/commits{/sha}","comments_url":"https://api.github.com/repos/resin-io/resin-supervisor/comments{/number}","issue_comment_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues/comments{/number}","contents_url":"https://api.github.com/repos/resin-io/resin-supervisor/contents/{+path}","compare_url":"https://api.github.com/repos/resin-io/resin-supervisor/compare/{base}...{head}","merges_url":"https://api.github.com/repos/resin-io/resin-supervisor/merges","archive_url":"https://api.github.com/repos/resin-io/resin-supervisor/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/resin-io/resin-supervisor/downloads","issues_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues{/number}","pulls_url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls{/number}","milestones_url":"https://api.github.com/repos/resin-io/resin-supervisor/milestones{/number}","notifications_url":"https://api.github.com/repos/resin-io/resin-supervisor/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/resin-io/resin-supervisor/labels{/name}","releases_url":"https://api.github.com/repos/resin-io/resin-supervisor/releases{/id}","deployments_url":"https://api.github.com/repos/resin-io/resin-supervisor/deployments","created_at":"2015-10-13T08:15:58Z","updated_at":"2017-12-17T10:25:14Z","pushed_at":"2018-02-23T00:23:37Z","git_url":"git://github.com/resin-io/resin-supervisor.git","ssh_url":"git@github.com:resin-io/resin-supervisor.git","clone_url":"https://github.com/resin-io/resin-supervisor.git","svn_url":"https://github.com/resin-io/resin-supervisor","homepage":"https://resin.io","size":3028,"stargazers_count":53,"watchers_count":53,"language":"CoffeeScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":26,"mirror_url":null,"archived":false,"open_issues_count":68,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":26,"open_issues":68,"watchers":53,"default_branch":"master"}},"base":{"label":"resin-io:multicontainer","ref":"multicontainer","sha":"820b3d08d8811d5d1830e5d3b85afa4f50120d66","user":{"login":"resin-io","id":6157842,"avatar_url":"https://avatars1.githubusercontent.com/u/6157842?v=4","gravatar_id":"","url":"https://api.github.com/users/resin-io","html_url":"https://github.com/resin-io","followers_url":"https://api.github.com/users/resin-io/followers","following_url":"https://api.github.com/users/resin-io/following{/other_user}","gists_url":"https://api.github.com/users/resin-io/gists{/gist_id}","starred_url":"https://api.github.com/users/resin-io/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/resin-io/subscriptions","organizations_url":"https://api.github.com/users/resin-io/orgs","repos_url":"https://api.github.com/users/resin-io/repos","events_url":"https://api.github.com/users/resin-io/events{/privacy}","received_events_url":"https://api.github.com/users/resin-io/received_events","type":"Organization","site_admin":false},"repo":{"id":44161944,"name":"resin-supervisor","full_name":"resin-io/resin-supervisor","owner":{"login":"resin-io","id":6157842,"avatar_url":"https://avatars1.githubusercontent.com/u/6157842?v=4","gravatar_id":"","url":"https://api.github.com/users/resin-io","html_url":"https://github.com/resin-io","followers_url":"https://api.github.com/users/resin-io/followers","following_url":"https://api.github.com/users/resin-io/following{/other_user}","gists_url":"https://api.github.com/users/resin-io/gists{/gist_id}","starred_url":"https://api.github.com/users/resin-io/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/resin-io/subscriptions","organizations_url":"https://api.github.com/users/resin-io/orgs","repos_url":"https://api.github.com/users/resin-io/repos","events_url":"https://api.github.com/users/resin-io/events{/privacy}","received_events_url":"https://api.github.com/users/resin-io/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/resin-io/resin-supervisor","description":"Resin Supervisor: resin.io's agent on devices.","fork":false,"url":"https://api.github.com/repos/resin-io/resin-supervisor","forks_url":"https://api.github.com/repos/resin-io/resin-supervisor/forks","keys_url":"https://api.github.com/repos/resin-io/resin-supervisor/keys{/key_id}","collaborators_url":"https://api.github.com/repos/resin-io/resin-supervisor/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/resin-io/resin-supervisor/teams","hooks_url":"https://api.github.com/repos/resin-io/resin-supervisor/hooks","issue_events_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues/events{/number}","events_url":"https://api.github.com/repos/resin-io/resin-supervisor/events","assignees_url":"https://api.github.com/repos/resin-io/resin-supervisor/assignees{/user}","branches_url":"https://api.github.com/repos/resin-io/resin-supervisor/branches{/branch}","tags_url":"https://api.github.com/repos/resin-io/resin-supervisor/tags","blobs_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/refs{/sha}","trees_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/trees{/sha}","statuses_url":"https://api.github.com/repos/resin-io/resin-supervisor/statuses/{sha}","languages_url":"https://api.github.com/repos/resin-io/resin-supervisor/languages","stargazers_url":"https://api.github.com/repos/resin-io/resin-supervisor/stargazers","contributors_url":"https://api.github.com/repos/resin-io/resin-supervisor/contributors","subscribers_url":"https://api.github.com/repos/resin-io/resin-supervisor/subscribers","subscription_url":"https://api.github.com/repos/resin-io/resin-supervisor/subscription","commits_url":"https://api.github.com/repos/resin-io/resin-supervisor/commits{/sha}","git_commits_url":"https://api.github.com/repos/resin-io/resin-supervisor/git/commits{/sha}","comments_url":"https://api.github.com/repos/resin-io/resin-supervisor/comments{/number}","issue_comment_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues/comments{/number}","contents_url":"https://api.github.com/repos/resin-io/resin-supervisor/contents/{+path}","compare_url":"https://api.github.com/repos/resin-io/resin-supervisor/compare/{base}...{head}","merges_url":"https://api.github.com/repos/resin-io/resin-supervisor/merges","archive_url":"https://api.github.com/repos/resin-io/resin-supervisor/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/resin-io/resin-supervisor/downloads","issues_url":"https://api.github.com/repos/resin-io/resin-supervisor/issues{/number}","pulls_url":"https://api.github.com/repos/resin-io/resin-supervisor/pulls{/number}","milestones_url":"https://api.github.com/repos/resin-io/resin-supervisor/milestones{/number}","notifications_url":"https://api.github.com/repos/resin-io/resin-supervisor/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/resin-io/resin-supervisor/labels{/name}","releases_url":"https://api.github.com/repos/resin-io/resin-supervisor/releases{/id}","deployments_url":"https://api.github.com/repos/resin-io/resin-supervisor/deployments","created_at":"2015-10-13T08:15:58Z","updated_at":"2017-12-17T10:25:14Z","pushed_at":"2018-02-23T00:23:37Z","git_url":"git://github.com/resin-io/resin-supervisor.git","ssh_url":"git@github.com:resin-io/resin-supervisor.git","clone_url":"https://github.com/resin-io/resin-supervisor.git","svn_url":"https://github.com/resin-io/resin-supervisor","homepage":"https://resin.io","size":3028,"stargazers_count":53,"watchers_count":53,"language":"CoffeeScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":26,"mirror_url":null,"archived":false,"open_issues_count":68,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":26,"open_issues":68,"watchers":53,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545"},"html":{"href":"https://github.com/resin-io/resin-supervisor/pull/545"},"issue":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/issues/545"},"comments":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/issues/545/comments"},"review_comments":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545/comments"},"review_comment":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/pulls/545/commits"},"statuses":{"href":"https://api.github.com/repos/resin-io/resin-supervisor/statuses/7d0555f7c98d989bc217d37bb5a10be96aa2ac20"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"pcarranzav"}}