log.io-ng
Version:
Realtime log monitoring in the browser
671 lines (523 loc) • 18.6 kB
text/coffeescript
###
Log.io Web Client
Listens to server for new log messages, renders them to screen "widgets".
# Usage:
wclient = new WebClient io, host: 'http://localhost:28778'
screen = wclient.createScreen
stream = wclient.logStreams.at 0
node = wclient.logNodes.at 0
screen.addPair stream, node
screen.on 'new_log', (stream, node, level, message) ->
###
if process.browser
$ = require 'jquery-browserify'
else
$ = eval "require('jquery')"
backbone = require 'backbone'
backbone.$ = $
io = require 'socket.io-client'
_ = require 'underscore'
templates = require './templates'
# Cap LogMessages collection size
MESSAGE_CAP = 1
###
ColorManager acts as a circular queue for color values.
Every new Stream or Node is assigned a color value on instantiation.
###
class ColorManager
_max: 20
constructor: (=1) ->
next: ->
= 1 if is
++
colors = new ColorManager
###
Backbone models are used to represent nodes and streams. When nodes
go offline, their LogNode model is destroyed, along with their
stream assocations.
###
class _LogObject extends backbone.Model
idAttribute: 'name'
_pclass: -> new _LogObjects
sync: (args...) ->
constructor: (args...) ->
super args...
= new LogScreens
=
= colors.next()
class _LogObjects extends backbone.Collection
model: _LogObject
comparator: (obj) ->
obj.get 'name'
class LogStream extends _LogObject
_pclass: -> new LogNodes
class LogStreams extends _LogObjects
model: LogStream
class LogNode extends _LogObject
_pclass: -> new LogStreams
class LogNodes extends _LogObjects
model: LogNode
class LogMessage extends backbone.Model
ROPEN = new RegExp '<','ig'
RCLOSE = new RegExp '>','ig'
render_message: ->
.replace(ROPEN, '<').replace(RCLOSE, '>')
class LogMessages extends backbone.Collection
model: LogMessage
constructor: (args...) ->
super args...
'add',
_capped: =>
( - MESSAGE_CAP) if > MESSAGE_CAP
###
LogScreen models maintain state for screen widgets in the UI.
When (Stream, Node) pairs are associated with a screen, the pair ID
is stored on the model. It uses pair ID instead of models themselves
in case a node goes offline, and a new LogNode model is created.
###
class LogScreen extends backbone.Model
idAttribute: null
defaults: ->
pairIds: []
constructor: (args...) ->
super args...
= new LogMessages
addPair: (stream, node) ->
pairIds = 'pairIds'
pid = stream, node
pairIds.push pid if pid not in pairIds
stream.trigger 'lwatch', node, @
node.trigger 'lwatch', stream, @
.trigger 'addPair'
removePair: (stream, node) ->
pairIds = 'pairIds'
pid = stream, node
'pairIds', (p for p in pairIds when p isnt pid)
stream.trigger 'lunwatch', node, @
node.trigger 'lunwatch', stream, @
stream.screens.remove @
node.screens.remove @
.trigger 'removePair'
hasPair: (stream, node) ->
pid = stream, node
pid in 'pairIds'
_pid: (stream, node) -> "#{stream.id}:#{node.id}"
isActive: (object, getPair) ->
# Returns true if all object pairs are activated on screen
return false if not object.pairs.length
object.pairs.every (item) =>
[stream, node] = getPair object, item
stream, node
class LogScreens extends backbone.Collection
model: LogScreen
###
WebClient listens for log messages and stream/node announcements
from the server via socket.io. It manipulates state in LogNodes &
LogStreams collections, which triggers view events.
###
class WebClient
constructor: (opts={host: '', secure: false}, ={}) ->
=
nodes: 0
streams: 0
messages: 0
start: new Date().getTime()
= new LogNodes
= new LogStreams
= new LogScreens
= new ClientApplication
logNodes:
logStreams:
logScreens:
webClient: this
.render()
= io.connect opts.host, secure: opts.secure
_on = (args...) => .on args...
# Bind to socket events from server
_on 'add_node',
_on 'add_stream',
_on 'remove_node',
_on 'remove_stream',
_on 'add_pair',
_on 'new_log',
_on 'ping',
_on 'disconnect',
_initScreens: =>
.on 'add remove addPair removePair', =>
['logScreens'] = JSON.stringify .toJSON()
screenCache = ['logScreens']
screens = if screenCache then JSON.parse(screenCache) else [{name: 'Screen1'}]
.add new .model screen for screen in screens
_addNode: (node) =>
node = .add node
.nodes++
node
_addStream: (stream) =>
.add stream
.streams++
stream = .get stream.name
stream.on 'lwatch', (node, screen) =>
.emit 'watch', screen._pid stream, node
stream.on 'lunwatch', (node, screen) =>
.emit 'unwatch', screen._pid stream, node
stream
_removeNode: (node) =>
.get(node.name)?.destroy()
.nodes--
_removeStream: (stream) =>
.get(stream.name)?.destroy()
.streams--
_addPair: (p) =>
stream = 'stream', p.stream
node = 'node', p.node
stream.pairs.add node
node.pairs.add stream
.each (screen) ->
screen.addPair stream, node if screen.hasPair stream, node
_newLog: (msg) =>
{stream, node, level, message} = msg
stream = 'stream', stream
node = 'node', node
.each (screen) ->
if screen.hasPair stream, node
screen.trigger 'new_log', new LogMessage
stream: stream
node: node
level: level
message: message
_getOrAdd: (type, name) =>
if type is 'stream'
stream = .get name
stream = name unless stream?
return stream
if type is 'node'
node = .get node
node = name unless node?
return node
_ping: (msg) =>
{stream, node} = msg
stream = 'stream', stream
node = 'node', node
stream.trigger 'ping', node if stream
node.trigger 'ping', stream if node
.messages++
_disconnect: =>
.reset()
.reset()
.nodes = 0
.streams = 0
createScreen: (sname) ->
screen = new LogScreen name: sname
.add screen
screen
###
Backbone views are used to manage the UI components,
including the list of log nodes and screen panels.
# View heirarchy:
ClientApplication
LogControlPanel
ObjectControls
ObjectGroupControls
ObjectItemControls
LogScreenPanel
LogScreenView
LogStatsView
TODO(msmathers): Build templates, fill out render() methods
###
class ClientApplication extends backbone.View
el: '#web_client'
template: _.template templates.clientApplication
initialize: (opts) ->
{, , , } = opts
= new LogControlPanel
logNodes:
logStreams:
logScreens:
= new LogScreensPanel
logScreens:
webClient:
$(window).resize if window?
, 'add remove',
_resize: =>
return if not window?
width = $(window).width() - @$el.find("#log_controls").width()
@$el.find("#log_screens").width width
render: ->
@$el.html
@$el.append .render().el
@$el.append .render().el
this
class LogControlPanel extends backbone.View
id: 'log_controls'
template: _.template templates.logControlPanel
initialize: (opts) ->
{, , } = opts
= new ObjectControls
objects:
logScreens:
getPair: (object, item) -> [object, item]
id: 'log_control_streams'
= new ObjectControls
objects:
logScreens:
getPair: (object, item) -> [item, object]
id: 'log_control_nodes'
attributes:
style: 'display: none'
events:
"click a.select_mode": "_toggleMode"
_toggleMode: (e) =>
target = $ e.currentTarget
target.addClass('active').siblings().removeClass 'active'
tid = target.attr 'href'
@$el.find(tid).show().siblings('.object_controls').hide()
false
render: ->
@$el.html
@$el.append .render().el
@$el.append .render().el
this
class ObjectControls extends backbone.View
className: 'object_controls'
template: _.template templates.objectControls
initialize: (opts) ->
{, , } = opts
, 'add',
, 'reset', =>
$(window).resize if window?
= null
_addObject: (obj) =>
new ObjectGroupControls
object: obj
getPair:
logScreens:
_insertObject: (view) ->
view._filter if
view.render()
index = .indexOf view.object
if index > 0
view.$el.insertAfter @$el.find "div.groups div.group:eq(#{index - 1})"
else
@$el.find("div.groups").prepend view.el
_filter: (e) =>
input = $ e.currentTarget
filter = input.val()
= if filter then new RegExp "(#{filter})", 'ig' else null
.trigger 'ui_filter',
_resize: =>
return if not window?
height = $(window).height()
@$el.find(".groups").height height - 80;
render: ->
@$el.html
title:
@$el.find('.filter').keyup
@
class ObjectGroupControls extends backbone.View
className: 'group'
template: _.template templates.objectGroupControls
initialize: (opts) ->
{, , } = opts
.pairs.each
.pairs, 'add',
, 'destroy', =>
.collection, 'ui_filter',
= new ObjectGroupHeader
object:
getPair:
logScreens:
.render()
_filter: (filter) =>
if filter and not .get('name').match filter
@$el.hide()
else
@$el.show()
_addItem: (pair) =>
new ObjectItemControls
item: pair
getPair:
object:
logScreens:
_insertItem: (view) ->
view.render()
index = .pairs.indexOf view.item
if index > 0
view.$el.insertAfter @$el.find "div.items div.item:eq(#{index - 1})"
else
@$el.find("div.items").prepend view.el
render: ->
@$el.html
@$el.prepend .el
@
class ObjectGroupHeader extends backbone.View
className: 'header'
template: _.template templates.objectGroupHeader
initialize: (opts) ->
{, , } = opts
, 'add remove', =>
, 'destroy', =>
, 'lwatch lunwatch', =>
.collection, 'add', =>
, 'ping',
events:
"click input": "_toggleScreen"
_toggleScreen: (e) =>
checkbox = $ e.currentTarget
screen_id = checkbox.attr('title').replace /screen-/ig, ''
screen = .get screen_id
.pairs.forEach (item) =>
[stream, node] = , item
if checkbox.is ':checked'
screen.addPair stream, node
else
screen.removePair stream, node
_ping: =>
.addClass 'ping'
setTimeout (=> .removeClass 'ping'), 20
render: =>
@$el.html
getPair:
object:
logScreens:
= @$el.find '.diode'
@
class ObjectItemControls extends backbone.View
className: 'item'
template: _.template templates.objectItemControls
initialize: (opts) ->
{, , } = opts
[, ] = opts.getPair ,
, 'add remove', =>
, 'destroy', =>
, 'lwatch lunwatch', =>
, 'ping',
events:
"click input": "_toggleScreen"
_toggleScreen: (e) =>
checkbox = $ e.currentTarget
screen_id = checkbox.attr('title').replace /screen-/ig, ''
screen = .get screen_id
if checkbox.is ':checked'
screen.addPair ,
else
screen.removePair ,
_ping: (object) =>
if object is
.addClass 'ping'
setTimeout (=> .removeClass 'ping'), 20
render: ->
@$el.html
item:
stream:
node:
logScreens:
= @$el.find '.diode'
@
class LogScreensPanel extends backbone.View
template: _.template templates.logScreensPanel
id: 'log_screens'
initialize: (opts) ->
{, } = opts
, 'add',
, 'add remove',
$(window).resize if window?
= new LogStatsView stats: .stats
events:
"click #new_screen_button": "_newScreen"
_newScreen: (e) ->
.add new .model name: 'Screen1'
false
_addLogScreen: (screen) =>
view = new LogScreenView
logScreens:
logScreen: screen
@$el.find("div.log_screens").append view.render().el
false
_resize: =>
return if not window?
lscreens =
if lscreens.length
height = $(window).height() - @$el.find("div.status_bar").height() - 10
@$el.find(".log_screen .messages").each ->
$(@).height (height/lscreens.length) - 12
render: ->
@$el.html
@$el.find('.stats').append .render().el
@
class LogScreenView extends backbone.View
className: 'log_screen'
template: _.template templates.logScreenView
logTemplate: _.template templates.logMessage
initialize: ({, }) ->
, 'destroy', =>
, 'new_log',
= true
= null
events:
"click .controls .close": "_close"
"click .controls .clear": "_clear"
_close: =>
.logMessages.reset()
.destroy()
false
_clear: =>
.logMessages.reset()
false
__filter: (e) =>
input = $ e.currentTarget
_filter_buffer = input.val()
wait = =>
_filter_buffer if _filter_buffer is input.val()
setTimeout wait, 350
_filter: (filter) =>
= if filter then new RegExp "(#{filter})", 'ig' else null
_addNewLogMessage: (lmessage) =>
.logMessages.add lmessage
lmessage
_recordScroll: (e) =>
msgs = @$el.find '.messages'
= (msgs.height() + msgs[0].scrollTop) is msgs[0].scrollHeight
_renderNewLog: (lmessage) =>
_msg = lmessage.get 'message'
msg = lmessage.render_message()
if
msg = if _msg.match then msg.replace , '<span class="highlight">$1</span>' else null
if msg
.append
lmessage: lmessage
msg: msg
@$el.find('.messages')[0].scrollTop = @$el.find('.messages')[0].scrollHeight if
_renderMessages: =>
.html ''
.logMessages.forEach
render: ->
@$el.html
logScreens:
@$el.find('.messages').scroll
@$el.find('.controls .filter input').keyup
= @$el.find '.msg'
this
class LogStatsView extends backbone.View
template: _.template templates.logStatsView
className: 'stats'
initialize: (opts) ->
{} = opts
= false
setInterval (=> if ), 1000
render: ->
@$el.html
stats:
= true
this
exports.WebClient = WebClient
do ->
client = new WebClient
secure: location?.protocol is 'https:'
, (localStorage? and localStorage) or (sessionStorage? and sessionStorage) or {}