big_query_log_viewer 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 630d53dfd36e95d3a96822530d80f5cb00de1761
4
+ data.tar.gz: bebe698a4d02b570204b425b7d6667d31d19780e
5
+ SHA512:
6
+ metadata.gz: 11a067c1f173f439bb56d35240cea08522b37a184b08af16b6bf07ce2f13afac14b33f5fb846704a9004897705ceaece23547c49d00d4fcc5e6bbf3db0a6f0de
7
+ data.tar.gz: bf5de5515ae6664f8c9f8770cf041309b8c9e9d736cebed026acd915b0bd2539c425ae470bcc04521012deef68e3df996782f67e95d1fc8f7fca2d14fc6622c8
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+
2
+ Copyright (c) 2015 Aha! Labs Inc
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # BigQueryLogViewer
2
+
3
+ A simple Rails engine and React app to search logs stored in Google BigQuery.
4
+
5
+ ![BigQuery Log Viewer](https://cloud.githubusercontent.com/assets/1896112/9646564/7877bcda-519a-11e5-8bfb-bc34dc93de9e.png)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'big_query_log_viewer'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install big_query_log_viewer
22
+
23
+ ## Usage
24
+
25
+ Run the generator to create your initialization file:
26
+
27
+ $ rails generate big_query_log_viewer:install
28
+
29
+ Open the newly created file, `config/initializers/big_query_log_viewer.rb`, and add your client ID, project number, and table prefix from the [Google Developer Console](https://console.developers.google.com).
30
+
31
+ Finally, mount the engine by adding the following to your application's `routes.rb`:
32
+
33
+ `mount BigQueryLogViewer::Engine, at: '/some_url'`
34
+
35
+ ## Linting
36
+
37
+ The project uses [coffeelint](http://www.coffeelint.org/) to maintain quality CoffeeScript syntax. It is configured to run as the default rake task.
38
+
39
+ ## Authorship
40
+
41
+ Written by Zach Schneider, based on prototype by Chris Waters, for [Aha!, the world's #1 product roadmap software](http://www.aha.io/)
42
+
43
+ ## Contributing
44
+
45
+ 1. Fork it ( https://github.com/aha-app/bigquery-log-viewer/fork )
46
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
47
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
48
+ 4. Push to the branch (`git push origin my-new-feature`)
49
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ load 'rails/tasks/statistics.rake'
8
+
9
+ Bundler::GemHelper.install_tasks
10
+
11
+ require 'coffeelint'
12
+
13
+ task :coffeelint do
14
+ Coffeelint.lint_dir(File.join('app', 'assets', 'javascripts', 'big_query_log_viewer')) do |filename, lint_report|
15
+ Coffeelint.display_test_results(filename, lint_report)
16
+ end
17
+ end
18
+
19
+ task default: :coffeelint
@@ -0,0 +1,40 @@
1
+ #= require react
2
+ #= require jquery
3
+
4
+ #= require ./components/tab_manager
5
+
6
+ window.BigQueryLogViewer ||= {}
7
+
8
+ TabManager = BigQueryLogViewer.TabManager
9
+
10
+ class BigQueryLogViewer.App
11
+ constructor: (@projectId, @clientId, @tablePrefix, @rowsPerPage) ->
12
+ config =
13
+ 'client_id': @clientId,
14
+ 'scope': 'https://www.googleapis.com/auth/bigquery'
15
+ immediate: true
16
+
17
+ # Perform authentication.
18
+ gapi.auth.authorize(config, (result) ->
19
+ if result.error
20
+ config.immediate = false
21
+ $('#authenticate-btn').show()
22
+ $('#authenticate-btn').on 'click', ->
23
+ gapi.auth.authorize(config, (result) ->
24
+ unless result.error
25
+ gapi.client.load('bigquery', 'v2')
26
+ $('#application').fadeIn()
27
+ $('#authenticate-btn').hide()
28
+ )
29
+ else
30
+ gapi.client.load('bigquery', 'v2')
31
+ $('#application').fadeIn()
32
+ $('#authenticate-btn').hide()
33
+ )
34
+
35
+ # Create React element.
36
+ props =
37
+ projectId: @projectId
38
+ tablePrefix: @tablePrefix
39
+ rowsPerPage: @rowsPerPage
40
+ React.render(React.createElement(TabManager, props), $('#application')[0])
@@ -0,0 +1,35 @@
1
+ ###* @jsx React.DOM ###
2
+
3
+ window.BigQueryLogViewer ||= {}
4
+
5
+ BigQueryLogViewer.Pagination = React.createClass
6
+ render: ->
7
+ removeNode =
8
+ if @props.type == 'top'
9
+ <span className={'remove-wrapper'} onClick={@props.handleTabDelete}>
10
+ <i className={'icon icon-remove'}></i>
11
+ </span>
12
+
13
+ tabs =
14
+ for tab in @props.tabs
15
+ <li key={tab.key} onClick={tab.handler || @props.handleTabSwitch} className={'active' if tab.active}>
16
+ <a href={'#'}>
17
+ <span dangerouslySetInnerHTML={__html: tab.title} />
18
+ {removeNode}
19
+ </a>
20
+ </li>
21
+
22
+ if @props.type == 'top'
23
+ return (
24
+ <ul className={'nav nav-tabs'}>
25
+ {tabs}
26
+ </ul>
27
+ )
28
+ else
29
+ return (
30
+ <div className={'pagination'}>
31
+ <ul>
32
+ {tabs}
33
+ </ul>
34
+ </div>
35
+ )
@@ -0,0 +1,55 @@
1
+ ###* @jsx React.DOM ###
2
+
3
+ window.BigQueryLogViewer ||= {}
4
+
5
+ BigQueryLogViewer.Row = React.createClass
6
+ getInitialState: ->
7
+ s = "000#{@props.row.timestamp.getMilliseconds()}"
8
+ y = @props.row.timestamp.getFullYear()
9
+ mo = ('0' + @props.row.timestamp.getMonth()).slice(-2)
10
+ d = ('0' + @props.row.timestamp.getDate()).slice(-2)
11
+ h = ('0' + @props.row.timestamp.getHours()).slice(-2)
12
+ mi = ('0' + @props.row.timestamp.getMinutes()).slice(-2)
13
+ se = ('0' + @props.row.timestamp.getSeconds()).slice(-2)
14
+ {
15
+ tss: "#{y}-#{mo}-#{d} #{h}:#{mi}:#{se}"
16
+ tsm: s.substr(s.length - 3)
17
+ }
18
+ handleShowMore: (e) ->
19
+ e.preventDefault()
20
+ $(@getDOMNode()).find('.collapsed-string').show()
21
+ $(@getDOMNode()).find('.expand-row').hide()
22
+
23
+ handleShowProximity: (e) ->
24
+ e.preventDefault()
25
+ @props.handleShowProximity(@props.row)
26
+
27
+ render: ->
28
+ row = @props.row
29
+
30
+ collapsedMsg =
31
+ if row.msg.length > 200
32
+ <span>{row.msg.substring(0,200)}...<a href='#' className='expand-row' onClick={@handleShowMore}>expand</a><span className='collapsed-string'>{row.msg.substring(200)}</span></span>
33
+ else
34
+ row.msg
35
+
36
+ if @props.type == 'results'
37
+ return (
38
+ <tr>
39
+ <td className='column-controls' onClick={@handleShowProximity}><i className='icon icon-external-link'></i></td>
40
+ <td className='column-ts'>{@state.tss}<span className='ts-milliseconds'>.{@state.tsm}</span></td>
41
+ <td className='column-host'>{row.host}</td>
42
+ <td className='column-pid'>{row.pid}</td>
43
+ <td className='column-severity' data-severity={row.severity}>{row.severity}</td>
44
+ <td className='column-msg'><pre>{collapsedMsg}</pre></td>
45
+ </tr>
46
+ )
47
+ else
48
+ return (
49
+ <tr className={'highlight-row' if @props.highlighted}>
50
+ <td className='column-ts'>{@state.tss}<span className='ts-milliseconds'>.{@state.tsm}</span></td>
51
+ <td className='column-rid'>{row.rid}</td>
52
+ <td className='column-severity' data-severity={row.severity}>{row.severity}</td>
53
+ <td className='column-msg'><pre>{collapsedMsg}</pre></td>
54
+ </tr>
55
+ )
@@ -0,0 +1,20 @@
1
+ ###* @jsx React.DOM ###
2
+
3
+ window.BigQueryLogViewer ||= {}
4
+
5
+ BigQueryLogViewer.SearchBox = React.createClass
6
+ handleSearch: (e) ->
7
+ e.preventDefault()
8
+ @props.handleSearch(@refs.searchInput.getDOMNode().value, @refs.startDate.getDOMNode().value, @refs.endDate.getDOMNode().value)
9
+
10
+ render: ->
11
+ return (
12
+ <form className='navbar-form search-box form-inline' onSubmit={@handleSearch}>
13
+ <div className='controls'>
14
+ <input ref='searchInput' placeholder={'Term'} />
15
+ Start Date: <input ref='startDate' type='date' />
16
+ End Date: <input ref='endDate' type='date' />
17
+ <input type='submit' value='Search' className='btn btn-primary' />
18
+ </div>
19
+ </form>
20
+ )
@@ -0,0 +1,18 @@
1
+ ###* @jsx React.DOM ###
2
+
3
+ window.BigQueryLogViewer ||= {}
4
+
5
+ BigQueryLogViewer.SearchStatus = React.createClass
6
+ render: ->
7
+ if @props.queryInProgress
8
+ progress = <span>Query running...</span>
9
+ else if @props.errorMessage
10
+ progress = <span className='error'>{@props.errorMessage}</span>
11
+ else if !@props.queryInProgress and @props.numReturnedResults?
12
+ results = <span>Found {@props.numReturnedResults} results</span>
13
+ return (
14
+ <div className='query-status'>
15
+ {progress}
16
+ {results}
17
+ </div>
18
+ )
@@ -0,0 +1,275 @@
1
+ ###* @jsx React.DOM ###
2
+
3
+ #= require ./pagination
4
+ #= require ./row
5
+
6
+ window.BigQueryLogViewer ||= {}
7
+
8
+ Pagination = BigQueryLogViewer.Pagination
9
+ Row = BigQueryLogViewer.Row
10
+
11
+ BigQueryLogViewer.Tab = React.createClass
12
+ getInitialState: ->
13
+ {
14
+ pages: [@props.tab.rowData]
15
+ activePageIndex: 0
16
+ pageToken: @props.tab.pageToken
17
+ showPrevLink: true
18
+ showNextLink: true
19
+ currentNextPage: 0
20
+ currentPrevPage: 0
21
+ }
22
+
23
+ resultsTab: ->
24
+ @props.tab.type is 'results'
25
+
26
+ expansionTab: ->
27
+ @props.tab.type is 'expansion'
28
+
29
+ highlighted: (row) ->
30
+ @expansionTab() && @props.tab.row.rid is row.rid
31
+
32
+ prevTitle: ->
33
+ if @resultsTab() then 'Prev' else 'More'
34
+
35
+ nextTitle: ->
36
+ if @resultsTab() then 'Next' else 'More'
37
+
38
+ handleNextPage: ->
39
+ if @state.activePageIndex + 1 < @state.pages.length
40
+ # Page has already been loaded, just activate it
41
+ @setState(activePageIndex: @state.activePageIndex + 1)
42
+ else if @resultsTab()
43
+ # Else load next page from Google.
44
+ @props.query.executeListQuery({pageToken: @state.pageToken, jobId: @props.tab.jobId}, (response) =>
45
+ rows =
46
+ for r in response.rows
47
+ {
48
+ timestamp: new Date(1000 * r.f[0].v)
49
+ host: r.f[4].v
50
+ pid: r.f[3].v
51
+ rid: parseInt(r.f[1].v)
52
+ severity: r.f[2].v
53
+ msg: r.f[5].v
54
+ }
55
+ @state.pages.push(rows)
56
+ @setState(
57
+ pageToken: response.pageToken
58
+ activePageIndex: @state.activePageIndex + 1
59
+ )
60
+
61
+ , (response) =>
62
+ console.log "ERROR: #{response.message}; entire response follows"
63
+ console.log response
64
+ alert 'Error loading more rows; check console for more information'
65
+ )
66
+ else if @expansionTab()
67
+ # Load additional context.
68
+ startRow = @props.tab.row.rid + @props.rowsPerPage / 2 + @state.currentNextPage * @props.rowsPerPage
69
+ endRow = @props.tab.row.rid + @props.rowsPerPage / 2 + (@state.currentNextPage + 1) * @props.rowsPerPage
70
+
71
+ # Construct query.
72
+ conds = [
73
+ {
74
+ field: 'host'
75
+ method: 'equals'
76
+ type: 'string'
77
+ value: @props.tab.row.host
78
+ }
79
+ {
80
+ field: 'pid'
81
+ method: 'equals'
82
+ type: 'int'
83
+ value: @props.tab.row.pid
84
+ }
85
+ {
86
+ field: 'rid'
87
+ method: 'between'
88
+ firstValue: startRow
89
+ secondValue: endRow
90
+ }
91
+ ]
92
+ query = @props.query.buildQuery(@props.tab.source.startDate, @props.tab.source.endDate, conds, 'ts, rid desc')
93
+
94
+ @props.query.executeQuery(query, {maxResults: @props.rowsPerPage}, (response) =>
95
+ # Mark no more next if there were no results.
96
+ if parseInt(response.totalRows) == 0
97
+ @setState(showNextLink: false)
98
+ return
99
+
100
+ # Create new page for the expansion.
101
+ rows =
102
+ for r in response.rows
103
+ {
104
+ timestamp: new Date(1000 * r.f[0].v)
105
+ host: r.f[4].v
106
+ pid: r.f[3].v
107
+ rid: parseInt(r.f[1].v)
108
+ severity: r.f[2].v
109
+ msg: r.f[5].v
110
+ }
111
+
112
+ @state.pages[0] = @state.pages[0].concat(rows)
113
+ @setState(currentNextPage: @state.currentNextPage + 1)
114
+
115
+ , (reponse) =>
116
+ console.log "ERROR: #{response.message}; entire response follows"
117
+ console.log response
118
+ alert 'Error finding more context; check console for more information'
119
+ )
120
+
121
+ handlePrevPage: ->
122
+ if @resultsTab()
123
+ @setState(activePageIndex: @state.activePageIndex - 1)
124
+ else if @expansionTab()
125
+ # Load additional context.
126
+ startRow = @props.tab.row.rid - @props.rowsPerPage / 2 - @state.currentPrevPage * @props.rowsPerPage
127
+ endRow = @props.tab.row.rid - @props.rowsPerPage / 2 - (@state.currentPrevPage + 1) * @props.rowsPerPage
128
+
129
+ # Construct query.
130
+ conds = [
131
+ {
132
+ field: 'host'
133
+ method: 'equals'
134
+ type: 'string'
135
+ value: @props.tab.row.host
136
+ }
137
+ {
138
+ field: 'pid'
139
+ method: 'equals'
140
+ type: 'int'
141
+ value: @props.tab.row.pid
142
+ }
143
+ {
144
+ field: 'rid'
145
+ method: 'between'
146
+ firstValue: startRow
147
+ secondValue: endRow
148
+ }
149
+ ]
150
+ query = @props.query.buildQuery(@props.tab.source.startDate, @props.tab.source.endDate, conds, 'ts, rid desc')
151
+
152
+ @props.query.executeQuery(query, {maxResults: @props.rowsPerPage}, (response) =>
153
+ # Mark no more prev if there were no results.
154
+ if parseInt(response.totalRows) == 0
155
+ @setState(showPrevLink: false)
156
+ return
157
+
158
+ # Create new page for the expansion.
159
+ rows =
160
+ for r in response.rows
161
+ {
162
+ timestamp: new Date(1000 * r.f[0].v)
163
+ host: r.f[4].v
164
+ pid: r.f[3].v
165
+ rid: parseInt(r.f[1].v)
166
+ severity: r.f[2].v
167
+ msg: r.f[5].v
168
+ }
169
+
170
+ @state.pages[0] = rows.concat(@state.pages[0])
171
+ @setState(currentPrevPage: @state.currentPrevPage + 1)
172
+
173
+ , (reponse) =>
174
+ console.log "ERROR: #{response.message}; entire response follows"
175
+ console.log response
176
+ alert 'Error finding more context; check console for more information'
177
+ )
178
+
179
+ handleShowPage: (event) ->
180
+ @setState(activePageIndex: parseInt(event.dispatchMarker.split('pagination-link-')[1]))
181
+
182
+ componentDidMount: ->
183
+ if @expansionTab()
184
+ window.requestAnimationFrame =>
185
+ node = @getDOMNode()
186
+ node.scrollTop = $(node).find('.highlight-row').offset().top
187
+
188
+ componentDidUpdate: ->
189
+ node = $(@getDOMNode())
190
+ maxHeight = $(window).height() - node.offset().top - 40
191
+ node.css('max-height', maxHeight)
192
+
193
+ render: ->
194
+ showProximity = @props.showProximity
195
+ tab = @props.tab
196
+
197
+ rows =
198
+ for row in @state.pages[@state.activePageIndex]
199
+ <Row key={"#{row.pid}-#{row.rid}"} row={row} type={@props.tab.type} highlighted={@highlighted(row)} handleShowProximity={@props.handleShowProximity} />
200
+
201
+ # Generate pagination.
202
+ if @resultsTab()
203
+ pagination = []
204
+
205
+ if @state.activePageIndex > 0 || (@expansionTab() && @state.showPrevLink)
206
+ pagination.push(
207
+ title: @prevTitle()
208
+ active: false
209
+ key: 'pagination-link-prev'
210
+ handler: @handlePrevPage
211
+ )
212
+
213
+ for page, index in @state.pages
214
+ pagination.push(
215
+ title: (index + 1)
216
+ active: index == @state.activePageIndex
217
+ key: "pagination-link-#{index}"
218
+ handler: @handleShowPage
219
+ )
220
+
221
+ if @state.activePageIndex + 1 < @state.pages.length || (@resultsTab() && @state.pageToken) || (@expansionTab() && @state.showNextLink)
222
+ pagination.push(
223
+ title: @nextTitle()
224
+ active: false
225
+ key: 'pagination-link-next'
226
+ handler: @handleNextPage
227
+ )
228
+
229
+ head =
230
+ if @resultsTab()
231
+ <thead>
232
+ <tr>
233
+ <th></th>
234
+ <th>Timestamp</th>
235
+ <th>Host</th>
236
+ <th>PID</th>
237
+ <th>Severity</th>
238
+ <th>Message</th>
239
+ </tr>
240
+ </thead>
241
+ else
242
+ <thead>
243
+ <tr>
244
+ <th>Timestamp</th>
245
+ <th>RID</th>
246
+ <th>Severity</th>
247
+ <th>Message</th>
248
+ </tr>
249
+ </thead>
250
+
251
+ pagination =
252
+ if @resultsTab()
253
+ <Pagination type={'inside'} tabs={pagination} handleTabSwitch={@handleTabSwitch} />
254
+
255
+ showMoreTop =
256
+ if @expansionTab() && @state.showPrevLink
257
+ <a href={'#'} onClick={@handlePrevPage}>More</a>
258
+
259
+ showMoreBottom =
260
+ if @expansionTab() && @state.showNextLink
261
+ <a href={'#'} onClick={@handleNextPage}>More</a>
262
+
263
+ return (
264
+ <div className={'hidden' unless @props.visible}>
265
+ {showMoreTop}
266
+ <table className={'table row-viewer'}>
267
+ {head}
268
+ <tbody>
269
+ {rows}
270
+ </tbody>
271
+ </table>
272
+ {pagination}
273
+ {showMoreBottom}
274
+ </div>
275
+ )
@@ -0,0 +1,216 @@
1
+ ###* @jsx React.DOM ###
2
+
3
+ #= require ../utils/query
4
+
5
+ #= require ./tab
6
+ #= require ./search_box
7
+ #= require ./search_status
8
+ #= require ./pagination
9
+
10
+ window.BigQueryLogViewer ||= {}
11
+
12
+ Tab = BigQueryLogViewer.Tab
13
+ SearchBox = BigQueryLogViewer.SearchBox
14
+ SearchStatus = BigQueryLogViewer.SearchStatus
15
+ Pagination = BigQueryLogViewer.Pagination
16
+
17
+ Query = BigQueryLogViewer.Query
18
+
19
+ BigQueryLogViewer.TabManager = React.createClass
20
+ getInitialState: ->
21
+ @query = new BigQueryLogViewer.Query(@props.projectId, @props.tablePrefix, @props.rowsPerPage)
22
+
23
+ {
24
+ tabs: []
25
+ activeTabIndex: null
26
+ queryInProgress: false
27
+ numReturnedResults: null
28
+ errorMessage: null
29
+ }
30
+
31
+ activeTab: ->
32
+ @state.tabs[@state.activeTabIndex]
33
+
34
+ findResultsTab: (term, startDate, endDate) ->
35
+ (index for tab, index in @state.tabs when tab.type == 'results' && tab.term is term && tab.startDate is startDate && tab.endDate == endDate)[0]
36
+
37
+ findExpansionTab: (rid) ->
38
+ (index for tab, index in @state.tabs when tab.type == 'expansion' && tab.source == @activeTab() && tab.rid is rid)[0]
39
+
40
+ handleSearch: (searchTerm, startDate, endDate) ->
41
+ return if searchTerm == ''
42
+ startDate = null if startDate == ''
43
+ endDate = null if endDate == ''
44
+
45
+ # Check that valid dates are entered.
46
+ if (startDate == null) && (endDate != null) || (startDate != null) && (endDate == null)
47
+ alert 'Must fill in both or neither of start and end dates'
48
+ return
49
+
50
+ # Check to see if we've already searched this term.
51
+ if (foundTab = @findResultsTab(searchTerm, startDate, endDate)) != undefined
52
+ @setState(activeTabIndex: foundTab)
53
+ return
54
+
55
+ @setState(queryInProgress: true)
56
+
57
+ # Construct query.
58
+ conds = [
59
+ {
60
+ field: 'msg'
61
+ method: 'contains'
62
+ value: searchTerm
63
+ }
64
+ ]
65
+ query = @query.buildQuery(startDate, endDate, conds, 'ts desc')
66
+
67
+ @query.executeQuery(query, {}, (response) =>
68
+ # Create new tab for the results.
69
+ unless parseInt(response.totalRows) == 0
70
+ rows =
71
+ for r in response.rows
72
+ {
73
+ timestamp: new Date(1000 * r.f[0].v)
74
+ host: r.f[4].v
75
+ pid: r.f[3].v
76
+ rid: parseInt(r.f[1].v)
77
+ severity: r.f[2].v
78
+ msg: r.f[5].v
79
+ }
80
+
81
+ tab =
82
+ type: 'results'
83
+ rowData: rows
84
+ pageToken: response.pageToken
85
+ jobId: response.jobReference.jobId
86
+ term: searchTerm
87
+ startDate: startDate
88
+ endDate: endDate
89
+
90
+ position = if @state.activeTabIndex != null then @state.activeTabIndex + 1 else 0
91
+ @state.tabs.splice(position, 0, tab)
92
+ @setState(activeTabIndex: position)
93
+
94
+ # Update view component.
95
+ @setState
96
+ queryInProgress: false
97
+ numReturnedResults: response.totalRows
98
+ errorMessage: null
99
+ , (response) =>
100
+ @setState
101
+ queryInProgress: false
102
+ numReturnedResults: 0
103
+ errorMessage: response.message
104
+ )
105
+
106
+ handleShowProximity: (row) ->
107
+ # Check to see if we've already queried this row proximity.
108
+ if (foundTab = @findExpansionTab(row.rid)) != undefined
109
+ @setState(activeTabIndex: foundTab)
110
+ return
111
+
112
+ @setState(queryInProgress: true)
113
+
114
+ # Construct query.
115
+ conds = [
116
+ {
117
+ field: 'host'
118
+ method: 'equals'
119
+ type: 'string'
120
+ value: row.host
121
+ }
122
+ {
123
+ field: 'pid'
124
+ method: 'equals'
125
+ type: 'int'
126
+ value: row.pid
127
+ }
128
+ {
129
+ field: 'rid'
130
+ method: 'between'
131
+ firstValue: row.rid - @props.rowsPerPage / 2
132
+ secondValue: row.rid + @props.rowsPerPage / 2
133
+ }
134
+ ]
135
+ query = @query.buildQuery(@activeTab().startDate, @activeTab().endDate, conds, 'ts, rid desc')
136
+
137
+ @query.executeQuery(query, {maxResults: 101}, (response) =>
138
+ # Create new tab for the expansion.
139
+ rows =
140
+ for r in response.rows
141
+ {
142
+ timestamp: new Date(1000 * r.f[0].v)
143
+ host: r.f[4].v
144
+ pid: r.f[3].v
145
+ rid: parseInt(r.f[1].v)
146
+ severity: r.f[2].v
147
+ msg: r.f[5].v
148
+ }
149
+ tab =
150
+ type: 'expansion'
151
+ rowData: rows
152
+ row: row
153
+ source: @activeTab()
154
+ term: @activeTab().term
155
+ position = if @state.activeTabIndex != null then @state.activeTabIndex + 1 else 0
156
+ @state.tabs.splice(position, 0, tab)
157
+ @setState(activeTabIndex: position)
158
+
159
+ # Update the view component.
160
+ @setState
161
+ queryInProgress: false
162
+ numReturnedResults: response.totalRows
163
+ errorMessage: null
164
+ , (reponse) =>
165
+ console.log "ERROR: #{response.message}; entire response follows"
166
+ console.log response
167
+ alert 'Error finding nearby rows; check console for more information'
168
+ )
169
+
170
+ handleTabSwitch: (event) ->
171
+ @setState(activeTabIndex: parseInt(event.dispatchMarker.split('tab-name-')[1]))
172
+
173
+ handleTabDelete: (event) ->
174
+ index = parseInt(event.dispatchMarker.split('tab-name-')[1])
175
+
176
+ newIndex =
177
+ if index <= @state.activeTabIndex
178
+ if @state.activeTabIndex > 0
179
+ @state.activeTabIndex - 1
180
+ else
181
+ null
182
+ else
183
+ @state.activeTabIndex
184
+
185
+ @state.tabs.splice(index, 1)
186
+ @setState(activeTabIndex: newIndex)
187
+
188
+ render: ->
189
+ # Create tab list for pagination.
190
+ pagination =
191
+ for tab, index in @state.tabs
192
+ title = tab.term
193
+ title = "<i class='icon icon-external-link'></i> #{title}" if tab.type == 'expansion'
194
+
195
+ {
196
+ title: title
197
+ active: index == @state.activeTabIndex
198
+ key: "tab-name-#{index}"
199
+ }
200
+
201
+ tabs =
202
+ for tab, index in @state.tabs
203
+ <Tab key={"tab-#{index}"} tab={tab} query={@query} visible={index == @state.activeTabIndex} handleShowProximity={@handleShowProximity} rowsPerPage={@props.rowsPerPage} />
204
+
205
+ return (
206
+ <div>
207
+ <div>
208
+ <SearchBox handleSearch={@handleSearch} />
209
+ <SearchStatus queryInProgress={@state.queryInProgress} numReturnedResults={@state.numReturnedResults} errorMessage={@state.errorMessage} />
210
+ <Pagination type={'top'} tabs={pagination} handleTabSwitch={@handleTabSwitch} handleTabDelete={@handleTabDelete} />
211
+ </div>
212
+ <div className={'tabs'}>
213
+ {tabs}
214
+ </div>
215
+ </div>
216
+ )
@@ -0,0 +1,61 @@
1
+ window.BigQueryLogViewer ||= {}
2
+
3
+ class BigQueryLogViewer.Query
4
+ constructor: (@projectId, @tablePrefix, @rowsPerPage) ->
5
+ #
6
+
7
+ tableRange: (startDate='CURRENT_TIMESTAMP()', endDate='CURRENT_TIMESTAMP()') ->
8
+ startDate = "TIMESTAMP('#{startDate} 00:00:00')" unless startDate == "CURRENT_TIMESTAMP()"
9
+ endDate = "TIMESTAMP('#{endDate} 23:59:59')" unless endDate == "CURRENT_TIMESTAMP()"
10
+
11
+ "(SELECT * FROM TABLE_DATE_RANGE(logs.#{@tablePrefix}_, #{startDate}, #{endDate}))"
12
+
13
+ buildQuery: (startDate, endDate, conds, order) ->
14
+ # Construct conditions.
15
+ conds =
16
+ for cond in conds
17
+ value = cond.value.replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0') if cond.value
18
+ switch cond.method
19
+ when 'contains' then "#{cond.field} contains '#{value}'"
20
+ when 'equals'
21
+ if cond.type == 'string'
22
+ "#{cond.field} = '#{value}'"
23
+ else
24
+ "#{cond.field} = #{value}"
25
+ when 'between' then "#{cond.field} between #{cond.firstValue} and #{cond.secondValue}"
26
+
27
+ q = "SELECT ts, rid, sev, pid, host, msg FROM #{@tableRange(startDate, endDate)}"
28
+ q = "#{q} WHERE #{conds.join(' AND ')}" if conds.length > 0
29
+ q = "#{q} ORDER BY #{order}" if order
30
+ q
31
+
32
+ executeQuery: (query, config, success, error) ->
33
+ console.log("Executing query: #{query}")
34
+ config.maxResults ?= @rowsPerPage
35
+ request = gapi.client.bigquery.jobs.query
36
+ projectId: @projectId
37
+ timeoutMs: 30000
38
+ maxResults: config.maxResults
39
+ query: query
40
+
41
+ request.execute (response) ->
42
+ if response.error
43
+ error(response) if error?
44
+ else
45
+ success(response) if success?
46
+
47
+ executeListQuery: (config, success, error) ->
48
+ console.log('Executing list query')
49
+ config.maxResults ?= @rowsPerPage
50
+ request = gapi.client.bigquery.jobs.getQueryResults
51
+ projectId: @projectId
52
+ jobId: config.jobId
53
+ timeoutMs: 30000
54
+ maxResults: config.maxResults
55
+ pageToken: config.pageToken
56
+
57
+ request.execute (response) ->
58
+ if response.error
59
+ error(response) if error?
60
+ else
61
+ success(response) if success?
@@ -0,0 +1,161 @@
1
+ /*
2
+ *= require twitter/bootstrap
3
+ *= require font-awesome
4
+ */
5
+
6
+ body {
7
+ font-size: 13px;
8
+ font-family: Helvetica, Arial, sans-serif;
9
+ padding: 25px;
10
+ padding-top: 5px;
11
+ margin: 0px;
12
+ }
13
+
14
+ .wrapper {
15
+ margin-left: auto;
16
+ margin-right: auto;
17
+ }
18
+
19
+ a {
20
+ color: #ccc;
21
+ }
22
+
23
+ .hidden {
24
+ display: none;
25
+ }
26
+
27
+ #authenticate-btn, #application {
28
+ display: none;
29
+ }
30
+
31
+ .controls {
32
+ input {
33
+ margin-right: 5px;
34
+ padding: 3px;
35
+ border-radius: 3px;
36
+
37
+ &[type=date] {
38
+ width: 150px;
39
+ }
40
+ }
41
+
42
+ span {
43
+ vertical-align: middle;
44
+ }
45
+ }
46
+
47
+ ul.nav {
48
+ margin-bottom: 0px;
49
+
50
+ li {
51
+ span.remove-wrapper {
52
+ display: inline-block;
53
+ width: 15px;
54
+
55
+ i.icon-remove {
56
+ margin-left: 5px;
57
+ cursor: pointer;
58
+ display: none;
59
+ }
60
+ }
61
+
62
+ &:hover span.remove-wrapper i.icon-remove {
63
+ display: inline-block;
64
+ }
65
+ }
66
+ }
67
+
68
+ .tabs {
69
+ padding: 10px;
70
+
71
+ >div {
72
+ overflow-y: scroll;
73
+ }
74
+ }
75
+
76
+ .query-status {
77
+ margin: 5px;
78
+ margin-top: 10px;
79
+ .error {
80
+ color: #C55050;
81
+ }
82
+ }
83
+
84
+ .row-viewer {
85
+ border-collapse: collapse;
86
+
87
+ td {
88
+ padding: 2px 4px;
89
+ vertical-align: top;
90
+ }
91
+
92
+ .column-controls {
93
+ i {
94
+ opacity: 0;
95
+ }
96
+ }
97
+ tr:hover {
98
+ .column-controls {
99
+ i {
100
+ opacity: 1;
101
+ }
102
+ cursor: pointer;
103
+ }
104
+ }
105
+ .column-ts {
106
+ white-space: nowrap;
107
+ .ts-milliseconds {
108
+ color: #777;
109
+ }
110
+ }
111
+ .column-host {
112
+ color: #333;
113
+ }
114
+ .column-pid, .column-rid {
115
+ color: rgb(163, 158, 56);
116
+ }
117
+ .column-severity {
118
+ &[data-severity=DEBUG] {
119
+ color: #999;
120
+ }
121
+ &[data-severity=INFO] {
122
+ color: rgb(135, 135, 176);
123
+ }
124
+ &[data-severity=WARN] {
125
+ color: yellow;
126
+ }
127
+ &[data-severity=ERROR] {
128
+ color: orange;
129
+ }
130
+ &[data-severity=FATAL] {
131
+ color: red;
132
+ }
133
+ }
134
+ .column-msg {
135
+ pre {
136
+ white-space: normal;
137
+ word-break: break-all;
138
+ margin: 0;
139
+ line-height: 16px;
140
+ }
141
+ }
142
+
143
+ .proximity-row {
144
+ display: none;
145
+ td {
146
+ background-color: #454545;
147
+ }
148
+ }
149
+
150
+ .collapsed-string {
151
+ display: none;
152
+ }
153
+ .expand-row {
154
+ padding-left: 10px;
155
+ color: #aaa;
156
+ }
157
+
158
+ .highlight-row {
159
+ background-color: #FFCCCC;
160
+ }
161
+ }
@@ -0,0 +1,3 @@
1
+ module BigQueryLogViewer
2
+ class ApplicationController < ActionController::Base; end
3
+ end
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
5
+ <title>Log viewer</title>
6
+
7
+ <%= stylesheet_link_tag 'big_query_log_viewer/application' %>
8
+ <%= javascript_include_tag 'big_query_log_viewer/application' %>
9
+
10
+ <script type='text/javascript'>
11
+ function init() {
12
+ var projectNumber = '<%= BigQueryLogViewer.project_number %>'
13
+ var clientId = '<%= BigQueryLogViewer.client_id %>'
14
+ var tablePrefix = '<%= BigQueryLogViewer.table_prefix %>'
15
+ var rowsPerPage = parseInt('<%= BigQueryLogViewer.rows_per_page %>')
16
+ BigQueryLogViewer.app = new BigQueryLogViewer.App(projectNumber, clientId, tablePrefix, rowsPerPage)
17
+ }
18
+ </script>
19
+
20
+ <%= javascript_include_tag 'https://apis.google.com/js/client.js?onload=init' %>
21
+ </head>
22
+
23
+ <body>
24
+ <div class='wrapper'>
25
+ <button id='authenticate-btn' class='btn btn-default'>Authenticate</button>
26
+ <div id='application'></div>
27
+ </div>
28
+ </body>
29
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "coffeescript_error": {
3
+ "level": "ignore"
4
+ },
5
+ "max_line_length": {
6
+ "value": 100,
7
+ "level": "error",
8
+ "limitComments": true
9
+ }
10
+ }
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ BigQueryLogViewer::Engine.routes.draw do
2
+ get '/', to: 'application#index'
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'big_query_log_viewer/engine'
2
+
3
+ module BigQueryLogViewer
4
+ mattr_accessor :project_number, :client_id, :table_prefix, :rows_per_page
5
+ end
@@ -0,0 +1,9 @@
1
+ module BigQueryLogViewer
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace BigQueryLogViewer
4
+
5
+ require 'less-rails'
6
+ require 'react-rails'
7
+ require 'jquery-rails'
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module BigQueryLogViewer
2
+ VERSION = '0.0.4'
3
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/base'
2
+
3
+ module BigQueryLogViewer
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../../templates', __FILE__)
7
+
8
+ desc 'Creates a BigQueryLogViewer initializer.'
9
+
10
+ def copy_initializer
11
+ template 'big_query_log_viewer.rb', 'config/initializers/big_query_log_viewer.rb'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # Configuration settings for BigQueryLogViewer.
2
+
3
+ # Your client ID from the Google developer console.
4
+ BigQueryLogViewer.client_id = ''
5
+
6
+ # Your project number from the Google developer console.
7
+ BigQueryLogViewer.project_number = ''
8
+
9
+ # The prefix of your table.
10
+ # Assumes the suffix is the log date - so if each table is named as 'app_logs_YYYY-MM-DD'
11
+ # then you would put 'app_logs' here
12
+ BigQueryLogViewer.table_prefix = ''
13
+
14
+ # The number of rows to display on each results page.
15
+ BigQueryLogViewer.rows_per_page = 100
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: big_query_log_viewer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Zach Schneider
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: therubyracer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: coffee-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: react-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: less-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: jquery-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: font-awesome-rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sprockets-coffee-react
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: less-rails-bootstrap
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: coffeelint
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: A simple Rails engine and React app to search logs stored in Google BigQuery.
154
+ email:
155
+ - zach@aha.io
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - LICENSE.txt
161
+ - README.md
162
+ - Rakefile
163
+ - app/assets/javascripts/big_query_log_viewer/application.coffee
164
+ - app/assets/javascripts/big_query_log_viewer/components/pagination.coffee
165
+ - app/assets/javascripts/big_query_log_viewer/components/row.coffee
166
+ - app/assets/javascripts/big_query_log_viewer/components/search_box.coffee
167
+ - app/assets/javascripts/big_query_log_viewer/components/search_status.coffee
168
+ - app/assets/javascripts/big_query_log_viewer/components/tab.coffee
169
+ - app/assets/javascripts/big_query_log_viewer/components/tab_manager.coffee
170
+ - app/assets/javascripts/big_query_log_viewer/utils/query.coffee
171
+ - app/assets/stylesheets/big_query_log_viewer/application.css.less
172
+ - app/controllers/big_query_log_viewer/application_controller.rb
173
+ - app/views/big_query_log_viewer/application/index.html.erb
174
+ - config/coffeelint.json
175
+ - config/routes.rb
176
+ - lib/big_query_log_viewer.rb
177
+ - lib/big_query_log_viewer/engine.rb
178
+ - lib/big_query_log_viewer/version.rb
179
+ - lib/generators/big_query_log_viewer/install_generator.rb
180
+ - lib/generators/templates/big_query_log_viewer.rb
181
+ homepage: https://github.com/aha-app/bigquery-log-viewer
182
+ licenses:
183
+ - MIT
184
+ metadata: {}
185
+ post_install_message:
186
+ rdoc_options: []
187
+ require_paths:
188
+ - lib
189
+ required_ruby_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ required_rubygems_version: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - ">="
197
+ - !ruby/object:Gem::Version
198
+ version: '0'
199
+ requirements: []
200
+ rubyforge_project:
201
+ rubygems_version: 2.4.6
202
+ signing_key:
203
+ specification_version: 4
204
+ summary: A simple Rails engine and React app to search logs stored in Google BigQuery.
205
+ test_files: []