big_query_log_viewer 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []