logstash-input-trello 0.1.0

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: 684e5b1bce85298dd5ed548c0cc0d805e2ec0e99
4
+ data.tar.gz: 24e1c5f8dec30eba8219f02810a2e4f724cd5ba2
5
+ SHA512:
6
+ metadata.gz: 31113e6f783ad8b6bee444eaa904e94d0fa6541b3fc48df8a25cd81391c5b4c51fcd1c1be9f6e31cedd35b025c4b6bd7bcf98ef63c3ecc87573b51e011f390a4
7
+ data.tar.gz: 8fb406be9ddba4fd2fc478c837f3aa00aea99ff21c58c4f1b42322ac8be504569ca7c9342b7213bd68b298f3204a3d2d165793c52c2cf925af8026bdb8eb91e4
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.DS_Store/
11
+ /vendor/
data/CONTRIBUTORS ADDED
@@ -0,0 +1,12 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * Alex Braun (ABraunCCS@gmail.com)
6
+ * Chad Dombrova (ChadD@luma-pictures.com)
7
+ * Chris Lyon (ChrisL@luma-pictures.com)
8
+
9
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
10
+ Logstash, and you aren't on the list above and want to be, please let us know
11
+ and we'll make sure you're here. Contributions from folks like you are what make
12
+ open source awesome.
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'http://rubygems.org'
2
+ # gem 'treetop'
3
+ # gem 'jrjackson'
4
+ # gem 'stud'
5
+ # gem 'clamp'
6
+ # gem 'i18n'
7
+ # gem 'filesize'
8
+ # gem 'cabin'
9
+ gem 'activesupport'
10
+ gem "logstash", :github => "elasticsearch/logstash", :branch => "1.5"
11
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012-2015 Elasticsearch <http://www.elasticsearch.org>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Logstash Plugin
2
+
3
+ This is a plugin for [Logstash](https://github.com/elasticsearch/logstash).
4
+
5
+ It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
6
+
7
+ ## Documentation
8
+
9
+ Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elasticsearch.org/guide/en/logstash/current/).
10
+
11
+ - For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
12
+ - For more asciidoc formatting tips, see the excellent reference here https://github.com/elasticsearch/docs#asciidoc-guide
13
+
14
+ ## Need Help?
15
+
16
+ Need help? Try #logstash on freenode IRC or the logstash-users@googlegroups.com mailing list.
17
+
18
+ ## Developing
19
+
20
+ ### 1. Plugin Developement and Testing
21
+
22
+ #### Code
23
+ - To get started, you'll need JRuby with the Bundler gem installed.
24
+
25
+ - Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
26
+
27
+ - Install dependencies
28
+ ```sh
29
+ bundle install
30
+ ```
31
+
32
+ #### Test
33
+
34
+ - Update your dependencies
35
+
36
+ ```sh
37
+ bundle install
38
+ ```
39
+
40
+ - Run tests
41
+
42
+ ```sh
43
+ bundle exec rspec
44
+ ```
45
+
46
+ ### 2. Running your unpublished Plugin in Logstash
47
+
48
+ #### 2.1 Run in a local Logstash clone
49
+
50
+ - Edit Logstash `Gemfile` and add the local plugin path, for example:
51
+ ```ruby
52
+ gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
53
+ ```
54
+ - Install plugin
55
+ ```sh
56
+ bin/plugin install --no-verify
57
+ ```
58
+ - Run Logstash with your plugin
59
+ ```sh
60
+ bin/logstash -e 'filter {awesome {}}'
61
+ ```
62
+ At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
63
+
64
+ #### 2.2 Run in an installed Logstash
65
+
66
+ You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
67
+
68
+ - Build your plugin gem
69
+ ```sh
70
+ gem build logstash-filter-awesome.gemspec
71
+ ```
72
+ - Install the plugin from the Logstash home
73
+ ```sh
74
+ bin/plugin install /your/local/plugin/logstash-filter-awesome.gem
75
+ ```
76
+ - Start Logstash and proceed to test the plugin
77
+
78
+ ## Contributing
79
+
80
+ All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
81
+
82
+ Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
83
+
84
+ It is more important to the community that you are able to contribute.
85
+
86
+ For more information about contributing, see the [CONTRIBUTING](https://github.com/elasticsearch/logstash/blob/master/CONTRIBUTING.md) file.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "logstash/devutils/rake"
@@ -0,0 +1,765 @@
1
+ # encoding: utf-8
2
+
3
+ require "trello_utils"
4
+ require "logstash/inputs/base"
5
+ require "logstash/namespace"
6
+ require "stud/interval"
7
+ require "socket"
8
+ require "json"
9
+ require "time"
10
+ require "set"
11
+ require "active_support/core_ext/string/inflections"
12
+ # ------------------------------------------------------------------------------
13
+
14
+ # A Logstash input plugin used for querying Trello at set intervals.
15
+ # It return events of varying types assosciated with Trello entities
16
+ # (ie cards, lists, etc).
17
+ class LogStash::Inputs::Trello < LogStash::Inputs::Base
18
+ config_name "trello"
19
+ milestone 1
20
+
21
+ @@plural_entities = [
22
+ "actions",
23
+ "boards",
24
+ "cards",
25
+ "checklists",
26
+ "invitations",
27
+ "labels",
28
+ "lists",
29
+ "members",
30
+ "memberships",
31
+ "organizations",
32
+ "powerups",
33
+ "checkItemStates",
34
+ "entities",
35
+ "checkItems"
36
+ ]
37
+
38
+ @@singular_entities = [
39
+ "action",
40
+ "board",
41
+ "card",
42
+ "checklist",
43
+ "invitation",
44
+ "label",
45
+ "list",
46
+ "member",
47
+ "membership",
48
+ "organization",
49
+ "powerup",
50
+ "checkItem"
51
+ ]
52
+
53
+ @@all_entities = @@singular_entities + @@plural_entities
54
+
55
+ @@plural_ids = [
56
+ "idChecklists",
57
+ "idLabels",
58
+ "idMembers",
59
+ "idMembersVoted"
60
+ ]
61
+
62
+ @@singular_ids = [
63
+ "idBoard",
64
+ "idList",
65
+ "idCard",
66
+ "idMemberCreator",
67
+ "idMember",
68
+ "idOrganization",
69
+ "idCheckItem",
70
+ "idShort"
71
+ ]
72
+
73
+ @@all_ids = @@singular_ids + @@plural_ids
74
+ @all_entities_and_ids = @@all_entities + @@all_ids
75
+ # --------------------------------------------------------------------------
76
+
77
+ # An array of all the fields configurable via the trello api.
78
+ # Unincluded fields will not show up in event data.
79
+ # Options are:
80
+ # - ["default"] = a custom array of fields defined within the plugin
81
+ # - ["all"] = all field
82
+ # - An array containg any of these fields:
83
+ # - - actions_entities
84
+ # - - action_member
85
+ # - - action_memberCreator
86
+ # - - card_stickers
87
+ # - - memberships_member
88
+ # - - organization
89
+ # - - myPrefs
90
+ # - - card_attachments
91
+ # - - cards
92
+ # - - card_checklists
93
+ # - - boardStars
94
+ # - - labels
95
+ # - - lists
96
+ # - - members
97
+ # - - membersInvited
98
+ # - - checklist
99
+ #
100
+ # - - active
101
+ # - - addAttachmentToCard
102
+ # - - addChecklistToCard
103
+ # - - addMemberToBoard
104
+ # - - addMemberToCard
105
+ # - - addMemberToOrganization
106
+ # - - addToOrganizationBoard
107
+ # - - admin
108
+ # - - admins
109
+ # - - avatarHash
110
+ # - - badges
111
+ # - - billableMemberCount
112
+ # - - bio
113
+ # - - bioData
114
+ # - - bytes
115
+ # - - checkItemStates
116
+ # - - closed
117
+ # - - color
118
+ # - - commentCard
119
+ # - - confirmed
120
+ # - - convertToCardFromCheckItem
121
+ # - - copyBoard
122
+ # - - copyCard
123
+ # - - copyCommentCard
124
+ # - - count
125
+ # - - createBoard
126
+ # - - createCard
127
+ # - - createList
128
+ # - - createOrganization
129
+ # - - data
130
+ # - - date
131
+ # - - dateLastActivity
132
+ # - - dateLastView
133
+ # - - deactivated
134
+ # - - deleteAttachmentFromCard
135
+ # - - deleteBoardInvitation
136
+ # - - deleteCard
137
+ # - - deleteOrganizationInvitation
138
+ # - - desc
139
+ # - - descData
140
+ # - - disablePowerUp
141
+ # - - displayName
142
+ # - - due
143
+ # - - edgeColor
144
+ # - - email
145
+ # - - emailCard
146
+ # - - enablePowerUp
147
+ # - - fullName
148
+ # - - idAttachmentCover
149
+ # - - idBoard
150
+ # - - idBoards
151
+ # - - idCard
152
+ # - - idChecklists
153
+ # - - idLabels
154
+ # - - idList
155
+ # - - idMember
156
+ # - - idMemberCreator
157
+ # - - idMembers
158
+ # - - idMembersVoted
159
+ # - - idOrganization
160
+ # - - idPremOrgsAdmin
161
+ # - - idShort
162
+ # - - initials
163
+ # - - invitations
164
+ # - - invited
165
+ # - - isUpload
166
+ # - - labelNames
167
+ # - - labels
168
+ # - - list
169
+ # - - logoHash
170
+ # - - makeAdminOfBoard
171
+ # - - makeNormalMemberOfBoard
172
+ # - - makeNormalMemberOfOrganization
173
+ # - - makeObserverOfBoard
174
+ # - - manualCoverAttachment
175
+ # - - me
176
+ # - - memberJoinedTrello
177
+ # - - memberType
178
+ # - - memberships
179
+ # - - mimeType
180
+ # - - mine
181
+ # - - minimal
182
+ # - - moveCardFromBoard
183
+ # - - moveCardToBoard
184
+ # - - moveListFromBoard
185
+ # - - moveListToBoard
186
+ # - - name
187
+ # - - normal
188
+ # - - open
189
+ # - - owners
190
+ # - - pinned
191
+ # - - pos
192
+ # - - powerUps
193
+ # - - prefs
194
+ # - - premiumFeatures
195
+ # - - previews
196
+ # - - products
197
+ # - - removeChecklistFromCard
198
+ # - - removeFromOrganizationBoard
199
+ # - - removeMemberFromCard
200
+ # - - shortLink
201
+ # - - shortUrl
202
+ # - - starred
203
+ # - - status
204
+ # - - subscribed
205
+ # - - type
206
+ # - - unconfirmedBoardInvitation
207
+ # - - unconfirmedOrganizationInvitation
208
+ # - - updateBoard
209
+ # - - updateCard
210
+ # - - updateCard:closed
211
+ # - - updateCard:desc
212
+ # - - updateCard:idList
213
+ # - - updateCard:name
214
+ # - - updateCheckItemStateOnCard
215
+ # - - updateChecklist
216
+ # - - updateList
217
+ # - - updateList:closed
218
+ # - - updateList:name
219
+ # - - updateMember
220
+ # - - updateOrganization
221
+ # - - url
222
+ # - - username
223
+ # - - uses
224
+ # - - visible
225
+ # - - website
226
+ #
227
+ # Default: ["default"]
228
+ config(:fields, :validate => :array, :default => ["default"])
229
+
230
+ # A hash of arrays which is used to cull entities.
231
+ # This is the master hash:
232
+ # {
233
+ # "cards" => ["open", "closed", "visible"],
234
+ # "lists" => ["open", "closed"],
235
+ # "members" => ["admins", "normal", "owners"],
236
+ # "membersInvited" => ["admins", "normal", "owners"],
237
+ # "boards" => ["closed", "members", "open", "organization",
238
+ # "pinned", "public", "starred", "unpinned"]
239
+ # }
240
+ #
241
+ # Ommiting a key from the hash will prevent that entity from being culled.
242
+ # Adding a filter to an entity's array will cause it to be culled by that.
243
+ # filter. For instance, if cards was set to ["open"], then trello would
244
+ # only return open cards.
245
+ #
246
+ # Default: {}, which means nothing will be culled.
247
+ config(:filters, :validate => :hash, :default => {})
248
+
249
+ # Do not change this.
250
+ # Defualt: "json_lines"
251
+ default(:codec, "json_lines")
252
+
253
+ # The port trello listens on for REST requests.
254
+ # Default: 443
255
+ config(:port, :validate => :number, :default => 443)
256
+
257
+ # An array of organizations from which to derive board ids.
258
+ # This is not used if board ids are provided.
259
+ config(:organizations, :validate => :array, :required => true)
260
+
261
+ # Coerce output field names into snake_case.
262
+ # Default: false
263
+ config(:snake_case, :validate => :boolean, :default => false)
264
+
265
+ # The frewuncy with wich to query Trello, in seconds.
266
+ # Default: 3600 (1 hour)
267
+ config(:interval, :validate => :number, :default => 3600)
268
+
269
+ # Trello oauth key
270
+ config(:key, :validate => :string, :required => true)
271
+
272
+ # Trello oauth secret
273
+ config(:token, :validate => :string, :required => true)
274
+
275
+ # An array of ids of boards to be parsed.
276
+ # A board id (shortLink actually) can be found in its URL.
277
+ # For instance, in this URL, https://trello.com/b/dFsjpzeN/logstash, the
278
+ # board id is dFsjpzeN.
279
+ #
280
+ # If no ids are given, this plugin will query Trello based upon all the
281
+ # boards assosciated with your organization.
282
+ #
283
+ # Default: []
284
+ config(:board_ids, :validate => :array, :default => [])
285
+
286
+ # An array of event types to be emitted.
287
+ # Output types include:
288
+ # - board
289
+ # - memberships
290
+ # - labels
291
+ # - cards
292
+ # - listscd
293
+ # - members
294
+ # - checklists
295
+ # - action
296
+ # If output_types is set to ["all"], then all types will be emitted.
297
+ #
298
+ # Default: ["all"]
299
+ config(:output_types, :validate => :array, :default => ["all"])
300
+
301
+ # An array of fields to be excluded from all events emitted.
302
+ # Nested fields must be written in fieldref format (ie "[foo][bar][etc]").
303
+ #
304
+ # Default: []
305
+ config(:exclude_fields, :validate => :array, :default => [])
306
+ # --------------------------------------------------------------------------
307
+
308
+ public
309
+ def register()
310
+ if @fields == ["all"]
311
+ @fields = TrelloUtils::PARAM_ALL_FIELDS
312
+ elsif @fields == ["default"]
313
+ @fields = TrelloUtils::PARAM_DEFAULT_FIELDS
314
+ end
315
+
316
+ if @filters == ["all"]
317
+ @filters = TrelloUtils::PARAM_ALL_FILTERS
318
+ elsif @filters == ["default"]
319
+ @filters = TrelloUtils::PARAM_DEFAULT_FILTERS
320
+ end
321
+
322
+ if @output_type == ["all"]
323
+ @output_types = [
324
+ "board",
325
+ "memberships",
326
+ "labels",
327
+ "cards",
328
+ "lists",
329
+ "members",
330
+ "checklists",
331
+ "actions"
332
+ ]
333
+ end
334
+ @host = Socket.gethostname
335
+ @client = TrelloUtils::TrelloClient.new({
336
+ organizations: @organizations,
337
+ key: @key,
338
+ token: @token,
339
+ board_ids: @board_ids,
340
+ fields: @fields,
341
+ filters: @filters,
342
+ port: @port
343
+ })
344
+ end
345
+ # --------------------------------------------------------------------------
346
+
347
+ private
348
+ def recursive_has_key?(data, keys)
349
+ if not data.is_a?(Hash)
350
+ return false
351
+ end
352
+ if data.has_key?(keys[0])
353
+ if keys.length > 1
354
+ recursive_has_key?(data[keys[0]], keys[1..-1])
355
+ else
356
+ return true
357
+ end
358
+ else
359
+ return false
360
+ end
361
+ end
362
+
363
+ private
364
+ def fieldref_to_array(fieldref)
365
+ output = fieldref.split("][")
366
+ output.map! { |item| item.gsub(/\[|\]/, "") }
367
+ return output
368
+ end
369
+
370
+ private
371
+ def exclude_fields!(event)
372
+ @exclude_fields.each_with_index do |field|
373
+ f = fieldref_to_array(field)
374
+ if recursive_has_key?(event.to_hash, f)
375
+ event.remove(field)
376
+ end
377
+ end
378
+ end
379
+
380
+ private
381
+ def create_lut(data)
382
+ # This cannot be done recursively because a recursive func will pick up
383
+ # stubs
384
+ lut = {}
385
+ @@plural_entities.map { |entity| lut[entity] = {} }
386
+
387
+ data.each do |key, entities|
388
+ if lut.has_key?(key)
389
+ entities.each do |entity|
390
+ lut[key][ entity["id"] ] = entity
391
+ end
392
+ end
393
+ end
394
+ return lut
395
+ end
396
+
397
+ private
398
+ def flatten(data)
399
+ func = lambda do |key, val|
400
+ output = val
401
+ ds_type = get_data_structure_type(val)
402
+ if ds_type == "array_of_hashes"
403
+ return group(output)
404
+ else
405
+ return output
406
+ end
407
+ end
408
+ return recurse(data.clone, func, func)
409
+ end
410
+
411
+ # private
412
+ # def clean_lut(lut)
413
+ # def _remove_entities(data)
414
+ # if not data.is_a?(Hash)
415
+ # return data # leaf (stop recursion here)
416
+ # end
417
+
418
+ # store = {}
419
+ # data.each do |key, val|
420
+ # if not @@all_entities_and_ids.include?(key)
421
+ # if val.is_a?(Hash)
422
+ # store[key] = _remove_entities(val)
423
+ # else
424
+ # store[key] = val
425
+ # end
426
+ # end
427
+ # end
428
+ # return store
429
+ # end
430
+
431
+ # lut = lut.clone
432
+ # lut.to_a.each do|ent_type, ids|
433
+ # ids.to_a.each do |item|
434
+ # temp = flatten(item.clone)
435
+ # lut[ent_type][item] = _remove_entities(temp)
436
+ # end
437
+ # end
438
+ # return lut
439
+ # end
440
+
441
+ private
442
+ def recurse(data, nonhash_func=nil, hash_func=nil, key_func=nil)
443
+ hash_func = lambda { |key, val| val } if hash_func.nil?
444
+ nonhash_func = lambda { |key, val| val } if nonhash_func.nil?
445
+ key_func = lambda { |key| key } if key_func.nil?
446
+
447
+ if not data.is_a?(Hash)
448
+ return data # leaf (stop recursion here)
449
+ end
450
+
451
+ store = {}
452
+ data.each do |key, val|
453
+ if val.is_a?(Hash)
454
+ store[key_func.call(key)] = recurse(hash_func.call(key, val),
455
+ nonhash_func, hash_func, key_func)
456
+ else
457
+ store[key_func.call(key)] = nonhash_func.call(key, val)
458
+ end
459
+ end
460
+ return store
461
+ end
462
+
463
+ private
464
+ def conform_field_names(data, form=nil)
465
+ if not data.is_a?(Hash)
466
+ return data
467
+ end
468
+ key_transformer = lambda do |key|
469
+ if @@all_ids.include?(key)
470
+ # clobber non-id fields with id fields
471
+ new_key = key.gsub(/^id/, '')
472
+ new_key = new_key[0].downcase + new_key[1..-1]
473
+ if not form.nil?
474
+ if form == 'plural'
475
+ if not /ed$/.match(new_key)
476
+ new_key = new_key.pluralize
477
+ end
478
+ elsif form == 'singular'
479
+ new_key = new_key.singularize
480
+ end
481
+ end
482
+ return new_key
483
+ else
484
+ return key
485
+ end
486
+ end
487
+ return recurse(data.clone, nil, nil, key_transformer)
488
+ end
489
+
490
+ private
491
+ def expand_entities(data, lut)
492
+ func = lambda do |key, val|
493
+ pkey = key.pluralize
494
+ l = lut[pkey]
495
+ if l.nil?
496
+ l = {}
497
+ end
498
+
499
+ output = val
500
+ ds_type = get_data_structure_type(val)
501
+ if /^array_of/.match(ds_type)
502
+ output = []
503
+ if ds_type == "array_of_hashes"
504
+ val.each do |item|
505
+ item_type = get_data_structure_type(item)
506
+ new_item = item
507
+ if item_type == "hash_with_id"
508
+ if l.has_key?(item["id"])
509
+ new_item = l[item["id"]]
510
+ end
511
+ end
512
+ new_item = flatten(new_item)
513
+ output.push(new_item)
514
+ end
515
+
516
+ elsif ds_type == "array_of_strings"
517
+ val.each do |id|
518
+ if l.has_key?(id)
519
+ output.push(l[id])
520
+ # output.push( flatten(l[id]) )
521
+ end
522
+ end
523
+ end
524
+ output = group(output)
525
+
526
+ elsif ds_type == "empty_array"
527
+ output = nil
528
+
529
+ elsif ds_type == "hash_with_id"
530
+ if l.has_key?(val["id"])
531
+ output = l[val["id"]]
532
+ # output = flatten(output)
533
+ end
534
+
535
+ elsif ds_type == "String"
536
+ if l.has_key?(val)
537
+ output = l[val]
538
+ # output = flatten(output)
539
+ end
540
+ end
541
+
542
+ return conform_field_names(output)
543
+ end
544
+ return recurse(data.clone, func, func)
545
+ end
546
+
547
+ private
548
+ def clean_data(data)
549
+ data = data.clone
550
+ # remove actions data field
551
+ if data.has_key?("data")
552
+ data["data"].each do |key, val|
553
+ if not key == "old"
554
+ data[key] = val
555
+ end
556
+ end
557
+ data.delete("data")
558
+ end
559
+ if data.has_key?("board")
560
+ data.delete("board")
561
+ end
562
+ return data
563
+ end
564
+
565
+ private
566
+ def get_data_structure_type(val)
567
+ if val.is_a?(Array)
568
+ if not val.empty?
569
+ if val[0].is_a?(Hash)
570
+ return "array_of_hashes"
571
+ elsif val[0].is_a?(String)
572
+ return "array_of_strings"
573
+ end
574
+ else
575
+ return "empty_array"
576
+ end
577
+ elsif val.is_a?(Hash)
578
+ if val.has_key?("id")
579
+ return "hash_with_id"
580
+ else
581
+ return "hash_without_id"
582
+ end
583
+ else
584
+ return val.class.to_s
585
+ end
586
+ end
587
+
588
+ private
589
+ def collapse(data, source, entities)
590
+ # entities might be drawn from lut.keys()
591
+ output = { source => {} }
592
+ data.each do |key, val|
593
+ if entities.include?(key)
594
+ output[key] = val
595
+ else
596
+ output[source][key] = val
597
+ end
598
+ end
599
+ return output
600
+ end
601
+
602
+ private
603
+ def coerce_nulls(data)
604
+ data.each do |index, item|
605
+ if item == ""
606
+ item == nil
607
+ end
608
+ end
609
+ return data
610
+ end
611
+
612
+ private
613
+ def group(data)
614
+ def _group(data)
615
+ prototype = {}
616
+ data.each do |entry|
617
+ entry.each do |key, val|
618
+ prototype[key] = []
619
+ end
620
+ end
621
+ data.each do |entry|
622
+ entry.each do |key, val|
623
+ if val != "" and !val.nil?
624
+ prototype[key].push(val)
625
+ end
626
+ end
627
+ end
628
+ return prototype
629
+ end
630
+
631
+ # ensure the proper data structure
632
+ if data.is_a?(Array)
633
+ if not data.empty?
634
+ if data[0].is_a?(Hash)
635
+ return _group(data)
636
+ end
637
+ end
638
+ else
639
+ # return data if data structure is wrong
640
+ return data
641
+ end
642
+ end
643
+
644
+ private
645
+ def to_snake_case(data)
646
+ data = data.clone
647
+ data.each { |index, item| index.map! { |item| item.underscore } }
648
+ return data
649
+ end
650
+
651
+ private
652
+ def nested_hash_to_matrix(data)
653
+ @sep = '.'
654
+ @output = []
655
+ def _nested_hash_to_matrix(data, name)
656
+ data.each do |key, val|
657
+ new_key = name + @sep + key.to_s
658
+ if val.is_a?(Hash) and val != {}
659
+ _nested_hash_to_matrix(val, new_key)
660
+ else
661
+ @output.push([new_key, val])
662
+ end
663
+ end
664
+ return @output
665
+ end
666
+
667
+ @output = _nested_hash_to_matrix(data, @sep)
668
+ @output = @output.map { |key, val| [key.split('.')[2..-1], val] }
669
+ return @output
670
+ end
671
+
672
+ private
673
+ def matrix_to_nested_hash(data)
674
+ output = {}
675
+ data.each do |keys, value|
676
+ cursor = output
677
+ for key in keys[0..-2]
678
+ if !cursor.include?(key)
679
+ cursor[key] = {}
680
+ cursor = cursor[key]
681
+ else
682
+ cursor = cursor[key]
683
+ end
684
+ end
685
+ cursor[keys[-1]] = value
686
+ end
687
+ return output
688
+ end
689
+ # --------------------------------------------------------------------------
690
+
691
+ private
692
+ def process_response(response, queue)
693
+ timestamp = Time.now
694
+
695
+ response = collapse(response, "board", @@plural_entities)
696
+ lut = create_lut(response)
697
+ # lut = clean_lut(lut)
698
+
699
+ @output_types.each do |out_type|
700
+ if response.has_key?(out_type)
701
+ response[out_type].each do |source|
702
+ out_type_ = out_type.singularize
703
+ data = clean_data(source)
704
+ data = conform_field_names(data, 'plural')
705
+ data = expand_entities(data, lut)
706
+ all_ent = @@all_entities.clone
707
+ all_ent.delete("entities")
708
+ data = collapse(data, out_type_, all_ent)
709
+ data = flatten(data)
710
+
711
+ # shuffle board info into data
712
+ board = response["board"]
713
+ board = conform_field_names(board)
714
+ data["board"] = board
715
+
716
+ data = nested_hash_to_matrix(data)
717
+ data = coerce_nulls(data)
718
+ if @snake_case
719
+ data = to_snake_case(data)
720
+ end
721
+ data = matrix_to_nested_hash(data)
722
+ event = nil
723
+ # set the timestamp of actions to their date field
724
+ _timestamp = timestamp
725
+ if out_type_ == "action"
726
+ _timestamp = data["action"]["date"]
727
+ data["action"].delete("date")
728
+ end
729
+ event = LogStash::Event.new(
730
+ "host" => @host,
731
+ "type" => @type + '_' + out_type_,
732
+ "@timestamp" => _timestamp,
733
+ "message" => JSON.dump(source) )
734
+ data.each do |key, val|
735
+ event[key] = val
736
+ end
737
+ exclude_fields!(event)
738
+ decorate(event)
739
+ queue << event
740
+ end
741
+ end
742
+ end
743
+ end
744
+
745
+ public
746
+ def run(queue)
747
+ init_time = Time.now - @interval
748
+ init_time = init_time.strftime('%Y-%m-%dT%H:%M:%S%z')
749
+ query_times = {}
750
+ board_ids.each { |board_id| query_times[board_id] = init_time}
751
+ Stud.interval(@interval) do
752
+ @client.board_ids.each do |board_id|
753
+ uri = @client.get_uri(board_id, query_times[board_id])
754
+ response = nil
755
+ begin
756
+ response = @client.issue_request(uri)
757
+ rescue RuntimeError
758
+ next
759
+ end
760
+ process_response(response, queue)
761
+ query_times[board_id] = Time.now.strftime('%Y-%m-%dT%H:%M:%S%z')
762
+ end
763
+ end
764
+ end
765
+ end