fluent-plugin-cmdaa-stat 0.1.14

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 601c84dcbb48331b0571ca1d25e915b51b1dd4a255b73367b0fdba2ce41e441d
4
+ data.tar.gz: 71943fc238a5d8bef0dc75cfa334ec34281710aa39569b50cdbc614e9c16a614
5
+ SHA512:
6
+ metadata.gz: 6be0631ff00754659053159cdb0b6794d391c6aa5c74f9399f3f62866fe5f5e78640fb99850f4d9f00e56f4a37b6e9cc71e18594c1776cdad27b2b958a6c11b8
7
+ data.tar.gz: f52fa8ca95de31cf0cf4a1e99b3b6b738ff740d7e3d3252cd8930695e5ea9820846861b1c5e1153efc1e53cee5ffab8708d4c4efbc60e914a9fafcd5827f2ca4
@@ -0,0 +1,614 @@
1
+ #
2
+ # Copyright 2018- Mark Pohl
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require "fluent/plugin/filter"
18
+ require "fluent/event"
19
+ require "statistics2"
20
+ require "digest"
21
+ require 'rest-client'
22
+
23
+
24
+ module Fluent::Plugin
25
+ class CmdaaStatFilter < Filter
26
+ Fluent::Plugin.register_filter("cmdaa_stat", self)
27
+
28
+ helpers :compat_parameters, :inject
29
+
30
+ desc "The amount of the total time for data collection in seconds."
31
+ config_param :total_time, :integer, default: 1800
32
+ desc "The length of the time slice in seconds"
33
+ config_param :time_slice, :integer, default: 30
34
+ desc "The url for connecting to the database. "
35
+ config_param :db_url, :string, default: "http://localhost:8080"
36
+ desc "The q-value for detecting anomalous log entries. "
37
+ config_param :q_value, :float, default: 0.95
38
+ desc "Where the process being monitored is running...docker or not."
39
+ config_param :docker_container, :bool, default: true
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+ #Need a hash for tags so that I only have to go to the database once
48
+ #Set this up here
49
+
50
+ $log.info("CmdaaStatFilter object version 0.1.12 is started!")
51
+
52
+
53
+
54
+ def configure(conf)
55
+ compat_parameters_convert(conf, :inject)
56
+ super
57
+ $log.info("CmdaaStatFilter has run configure. url value is #{db_url}}")
58
+ $log.debug("In configure: time slice is #{time_slice} total time is #{total_time}")
59
+ end
60
+
61
+ def start
62
+ super
63
+ $log.info("CmdaaStatFilter has run start")
64
+
65
+ #The cutoff for measuring uniqueness of a log. We might want this
66
+ #to be configured in a database.
67
+ @qValue = q_value
68
+
69
+ #Seed the random number generator which is used in calcuting the
70
+ #time offset to wait before updating the database with the
71
+ #current data.
72
+ srand
73
+ #Get the number of servers in this system
74
+ @serverCount = get_server_count
75
+
76
+ #Just above we got the server count. For our purposes we don't want
77
+ #this value to be lower than 10 since we are using the value just for
78
+ #determining timing related to database read/write and coordinating them
79
+ #so that they don't all request the read/write at the same time.
80
+ if (@serverCount < 10 )
81
+ @serverCount = 10
82
+ end
83
+
84
+ #Might want this in a database so that it can be configured for each
85
+ #installation
86
+ @updateTimeCost = 3 #seconds it takes to update the database
87
+
88
+ #Structure of the incrementing array and hashes
89
+ #
90
+ # Top Level hash
91
+ # tag => hash that contains "logId" and "newDataArr" and "oldDataArr"
92
+ # Next level
93
+ # "logId" => numeric identifier for this log File
94
+ # "newDataArr" => Array with (total_time/time_slice) elements...
95
+ # each element contains a hash with counts for template identifiers (md5Id)
96
+ # for the new data in this time span (total_time)
97
+ # "oldData" => Hash for old data. The hashes are the md5 hash from the database
98
+ # Next level --- for both the old and the new data hashes
99
+ # MD5 Hash
100
+ # md5 from log line contents => count for this md5Id for this time slice
101
+
102
+ @logInfoHash = Hash.new(0)
103
+ # @logInfoHash["0"] = 0
104
+ #The begin time for the current time slice
105
+ @startTime = Time.new
106
+ #The end time for the current time time slice
107
+ #when the time slice is shifted this will become the new startTime
108
+ #and we will create a new stopTime by adding the total_time value to it.
109
+ @stopTime = @startTime + total_time
110
+
111
+ @updateTime = @stopTime + get_update_time_offset()
112
+ @stopTimeSlice = @startTime + time_slice
113
+ @sliceIndex = 0
114
+ @totalIncrements = total_time / time_slice
115
+
116
+ #Need to know if we have just started since the arrays and hashes
117
+ #will not be fully populated until the second iteration. This affects
118
+ #the algorithm so we need keep track of it and set it to false after
119
+ #we have gone through the first iteration of all the time slices in
120
+ #the first total_time length
121
+ @firstIteration = true
122
+
123
+
124
+
125
+ end
126
+
127
+
128
+ def shutdown
129
+ super
130
+ $log.info("CmdaStatFilter has run shutdown.")
131
+ end
132
+
133
+
134
+
135
+ def getChiSqrScore (countNew,totalNew,countOld,totalOld)
136
+
137
+ countNew = countNew.to_f
138
+ countOld = countOld.to_f
139
+ totalNew = totalNew.to_f
140
+ totalOld = totalOld.to_f
141
+
142
+ p = countNew / totalNew
143
+ q = countOld / totalOld
144
+
145
+ if ( p < q )
146
+ return 0
147
+ end
148
+
149
+ t = (countNew + countOld) / (totalOld + totalNew)
150
+
151
+ if ( t == 0)
152
+ return nil
153
+ end
154
+
155
+ v = countNew * Math::log(p/t) + countOld * Math::log(q/t)
156
+
157
+ if ( t == 1 )
158
+ #set this to return absolute value since if countOld is zero
159
+ #seems like we should return 1 instead of -1. If countOld is zero
160
+ #that means we have never seen this value before!
161
+ return Statistics2.chi2X_(1,2*v).abs
162
+ end
163
+
164
+ if ( p < 1)
165
+ v = v + ((totalNew - countNew) * Math::log((1-p)/(1-t)))
166
+ end
167
+
168
+ if ( q < 1)
169
+ v = v + ((totalOld - countOld) * Math::log((1-q)/(1-t)))
170
+ end
171
+ #set this to return absolute value since if countOld is zero
172
+ #seems like we should return 1 instead of -1. If countOld is zero
173
+ #that means we have never seen this value before!
174
+ return Statistics2.chi2X_(1,2*v).abs
175
+ end
176
+
177
+ #Get the number of servers in the system.
178
+ def get_server_count
179
+ get_server_count_url = db_url + '/server/find-all-servers'
180
+ serverResponse = RestClient.get get_server_count_url
181
+ serverCount = 0
182
+ if serverResponse.code != 200
183
+ #just return a zero
184
+ serverCount = 0
185
+ else
186
+ serverList = serverResponse.body
187
+ jsonArray = JSON.parse(serverList)
188
+ serverCount = jsonArray.size
189
+ end
190
+
191
+ return serverCount
192
+ end
193
+
194
+
195
+ def resetTimeInterval(stopTime)
196
+ @startTime = stopTime
197
+ @stopTime = @startTime + total_time
198
+ end
199
+
200
+ # Need a method to create a "hash" for the regex. Since the grok_parser doesn't allow one to tag individual groks I think the best approach is to
201
+ # concatenate all of the field names and compute an md5 hash from that string.
202
+ # This should work no matter how the grok is configured and should be
203
+ # unique for each log source. We will need to insure that the groks have unique field names. Can we assign each grok a number and add that number
204
+ # to the field name to insure that the grok id will be unique?
205
+
206
+ # This method creates an array and populates the arroy with the tag (minus "cmdaa.") and keys from the json for the message.
207
+ # Next, since the keys are not guaranteed to be in order they are made lowercase and sorted to insure
208
+ # a consistent md5 will be computed each time. Then the md5 is calculated and returned
209
+ def create_line_hash(tag,message)
210
+ tag = tag.gsub('cmdaa.','')
211
+ all_keys = Array[]
212
+ all_values = Array[]
213
+ # all_keys.push(tag)
214
+ message.each do | key, value |
215
+ if key.start_with?("CMDA_XXXX")
216
+ all_keys.push(key)
217
+ end
218
+ end
219
+ #If the keys are defined by us, we don't really need the downcase operation but will keep it for now
220
+ all_keys.sort_by { |word| word.downcase }
221
+ # $log.debug("all_keys in order is #{all_keys}")
222
+ all_keys.each { |key| all_values.push(message[key]) }
223
+ # $log.debug("all_values in order is #{all_values.to_s}")
224
+ Digest::MD5.hexdigest(all_values.to_s())
225
+ end
226
+
227
+ #looks like the function above is not really needed now the the grok maker alllows us to label the grok.
228
+ #Here is a much simpler version that still makes an MD5 value since that is what the database expects but
229
+ #we create that with the grok_name value.
230
+ def create_md5_from_grok_name(message)
231
+
232
+ Digest::MD5.hexdigest(message["grok_name"])
233
+
234
+ end
235
+
236
+
237
+ #This is a method so that we can easily change this to get some constants
238
+ #from the database if we need to. The serverCount value here could be
239
+ #store in the database and populated when the system was setup. For now
240
+ #we set the serverCount = 10 and assume that any update to the database
241
+ #will take 3 seconds or less
242
+ def get_update_time_offset
243
+
244
+ timeOffset = @serverCount.to_f / @updateTimeCost.to_f
245
+ #return a random number between 0 and timeOffset.to_i. This random
246
+ #selection tries to reduce the number of systems trying to update the
247
+ #database at the same time.
248
+ return rand(timeOffset.to_i)
249
+
250
+ end
251
+
252
+ #get the logid using the logname so that I only need to call that once
253
+ #return the logid that will get passed to the other db methods
254
+ def get_logid(tag)
255
+
256
+ #Write code to create the two possible log names...we are parsing the log name from the tag
257
+ #but the tag has the file and path name after the text 'cmdaa' Since lots of filenames have a . in them (i.e. .log)
258
+ #we won't know if the last . is part of the file name or a replacement for the last /. This code will
259
+ #create a string with a _ in place of the last ".". This will allow the like operation to find the file/path
260
+ #for either case.
261
+
262
+ #The proper placement of the underscore requires some splitting and array operations
263
+ #the tag starts with cmdaa and then each element is separated by a "." All elements after
264
+ #cmdaa are directory levels except the last one which should be the filename
265
+ tmpStrArr = tag.gsub('cmdaa.','').split('.')
266
+
267
+ #Remove the last path segment and save it!
268
+ lastStr = tmpStrArr.pop
269
+
270
+ if docker_container
271
+ #Need to pop two more elements to get the one that is unique for docker logs
272
+ #the Kubernetes logs are usually stored in directories with this structure:
273
+ #/var/log/pods/containerid/pod-name/0.log I don't know if the zero is a constant or
274
+ #if it increments. Also of note in kubernetes there are multiple levels of
275
+ #file links before you get to the actual file. The "top level" link is
276
+ #/var/log/containers which points to /var/log/pods/ which points to
277
+ #/var/lib/docker/containers, which is where the actual file resides
278
+ lastStr = tmpStrArr.pop
279
+ lastStr = tmpStrArr.pop
280
+ fileName = "/VAR/LOG/PODS/%/" + lastStr.upcase + "/%LOG"
281
+
282
+ else
283
+ #If the environment is not Docker, or Kubernetes or Rancher do it this way
284
+ fileName = ''
285
+ tmpStrArr.each { |str| fileName = fileName + '/' + str }
286
+
287
+ #add the underscore with the last path segment after it!
288
+ fileName = fileName + '_' + lastStr
289
+
290
+ end
291
+ $log.info("Filename to search for in db is " + fileName)
292
+ # $log.debug("db_url is #{db_url} and fileName is #{fileName}")
293
+ #create the url to the REST call. /log-file/find-by-filename-like is the URL endpoint in the REST service.
294
+ get_log_url = db_url + '/log-file/find-by-filename-like/'
295
+ # $log.debug("input to rest call is " + get_log_url + URI.escape('{"fileName":"' + fileName + '"}',Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")))
296
+ logIdResponse = RestClient.get get_log_url + URI.escape('{"fileName":"' + fileName + '"}',Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
297
+ # logId = RestClient.get "http://192.168.88.137:8001/api/v1/namespaces/default/services/cmdaa:8080/proxy/log-file/find-by-filename-like/%7B%22fileName%22%3A%22%2Ffluentd%2Ftest%2Ftest_log%22%7D"
298
+ # $log.debug("logIdResponse code is #{logIdResponse.code}")
299
+ # $log.debug("logIdResponse body is #{logIdResponse.body}")
300
+ if logIdResponse.code != 200
301
+ #create something empty to return
302
+ logId = 0
303
+ else
304
+ logId = logIdResponse.body
305
+ end
306
+
307
+
308
+ return logId
309
+
310
+ end
311
+ #When this process is started, gets the data from the database service for this log source
312
+ def get_old_counts(logId)
313
+
314
+ #Use logId to get list of existing shift_counts with REST call
315
+ get_data_url = db_url + "/log-line-count/find-all-lines-by-log-file/#{logId}"
316
+ $log.debug("input to rest call for get_old_counts is #{get_data_url}")
317
+ old_counts_list = RestClient.get get_data_url
318
+ if old_counts_list.code != 200
319
+ #create something empty to return
320
+ oldHashData = Hash.new(0)
321
+ else
322
+ #process JSON and create new hash for oldHashData
323
+ oldHashData = Hash.new(0)
324
+ jsonArray = JSON.parse(old_counts_list.body)
325
+ #Make sure we found something...if this is empty we will still
326
+ #return a hash it will just be empty
327
+ if jsonArray.size > 0
328
+ jsonArray.each { |value| oldHashData[value["md5Id"]] = value["count"]
329
+ $log.debug("oldData key is #{value["md5Id"]} oldData count is #{oldHashData[value["md5Id"]]}")
330
+ }
331
+ end
332
+ end
333
+
334
+ return oldHashData
335
+ #if there are no counts available return an empty hash
336
+ #if there are counts avaialble return the list that was returned by the rest call but reformatted as
337
+ #a hash with the md5 value as the key and the count as the value.
338
+ end
339
+
340
+ #Calculate the total number of counts from the individual counts in the
341
+ #hash passed in.
342
+ def get_total(countHash)
343
+
344
+ totalCounts = 0
345
+ countHash.each_value { |value| totalCounts = totalCounts + value }
346
+
347
+ return totalCounts
348
+ end
349
+
350
+ #This should call a web service to add current data counts to the database
351
+ def update_counts_db(logId,countList)
352
+ currentTime = Time.now
353
+ $log.debug("in update_counts_db, time before db call is #{currentTime.utc.iso8601}")
354
+
355
+ insertHash = Hash.new(0)
356
+
357
+ jsonOut = ""
358
+
359
+ #$log.debug("countList as a string is #{countList.to_s}")
360
+
361
+ #Go through the countList hash and construct a string in JSON format
362
+ #for sending all records to the database.
363
+ countList.each { |key,value|
364
+ insertHash["logId"] = logId
365
+ insertHash["md5Id"] = key
366
+ insertHash["count"] = value
367
+
368
+ #If this is not the first time through this loop then add the new
369
+ #resord to the end of the string otherwise just assign the new JSON
370
+ #string to the jsonOUT string.
371
+ if jsonOut != ""
372
+ jsonOut = jsonOut + "," + insertHash.to_json
373
+ else
374
+ jsonOut = insertHash.to_json
375
+ end
376
+ }
377
+
378
+ #Add brackets to the beginning and the end of the JSON string since
379
+ #the service is expecting a JSON array.
380
+ jsonOut = "[" + jsonOut + "]"
381
+ #$log.debug("jsonOut as a string is #{jsonOut}")
382
+
383
+ #construct the url for sending the data to the log-line-counter service
384
+ #The data is sent as a datatype with 3 elements. Inside the service these
385
+ #elements are put into an insert/update statement. The SQL will
386
+ #first try an insert and then if the record already exists an update will
387
+ #be sent.
388
+ post_log_url = db_url + '/log-line-count/update-transaction/'
389
+ #$log.debug("url for post is #{post_log_url}")
390
+ #The RestClient.post interface expects at least two parameters to be passed but our service is really set with no named parameters so
391
+ #we pass in a second parameter that is empty...this seems to work. Also the URI.escape etc is a Ruby way to encode a URL
392
+ return_value = RestClient.post post_log_url + URI.escape(jsonOut,Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")), ""
393
+
394
+ currentTime = Time.now
395
+ $log.debug("in update_counts_db, time after db call is #{currentTime.utc.iso8601}")
396
+ end
397
+
398
+ def filter(tag, time, record)
399
+
400
+
401
+ #Checking for the tag allows us to handle multiple logs at once
402
+ #Some configurations in fluentd cmdaa_stat plugin might tail more than one log.
403
+ # By checking the tag here
404
+ #allows us to have different hashes for different tags and we can have
405
+ #completely separate data sets for each log file that is being tracked
406
+ #under the same cmdaa_stat <filter> configuration
407
+ $log.debug("In filter: time slice is #{time_slice} total time is #{total_time} tag is #{tag}")
408
+ if ( @logInfoHash[tag] == 0 )
409
+
410
+ @logInfoHash[tag] = Hash.new(0)
411
+ #Get the logId from the database. Use the tag value to create a
412
+ #file or path with a wildcard entry that should find the log file
413
+ #entry in the database and get the logId that identifies that entry
414
+ @logInfoHash[tag]["logId"] = get_logid(tag)
415
+
416
+ #Create a new array that will have an element for each time slice
417
+ #(i.e. increment) Each array element will point to a hash. The hash
418
+ #will contain MD5_ID keys with the count for each occurence of that
419
+ #MD5_ID in the log data coming through this filter
420
+ @logInfoHash[tag]["newDataArray"] = Array.new(@totalIncrements) { Hash.new(0) }
421
+
422
+ #Create an array of elments. Each element will count the total number
423
+ #of logs we see in the current time slice (i.e. increment)
424
+ @logInfoHash[tag]["newIncrementTotals"] = Array.new(@totalIncrements) { |value| 0 }
425
+
426
+ #Create a hash. This hash will contain the total number of logs that
427
+ #match a particular MD5_ID for the entire time span (total_time). The
428
+ #key for the hash is MD5_ID. The value for the hash is the count of
429
+ #how many times we see a log that computes to that MD5_ID
430
+ @logInfoHash[tag]["newTotal"] = Hash.new(0)
431
+
432
+ #Initialize a new total count for this time segment and this log file.
433
+ #Every time we see a log line for this file we increment this value
434
+ #by 1. When we are done with this time segment (total_time) we reset
435
+ #this value to 0
436
+ @logInfoHash[tag]["newTotalAll"] = 0
437
+
438
+ #Get data currently in the database for this logFileId
439
+ @logInfoHash[tag]["oldData"] = get_old_counts(@logInfoHash[tag]["logId"])
440
+
441
+ #Get the total for all the values in the oldData
442
+ @logInfoHash[tag]["oldDataTotal"] = get_total(@logInfoHash[tag]["oldData"])
443
+
444
+ #Create a new Array. Each element of the array will hold the updates
445
+ #that will be sent to the database. One index will be for holding the
446
+ #data that is currently being sent to the database (sendingIndex) and
447
+ #one index will be for the currently counting data (summingIndex)
448
+ @logInfoHash[tag]["updateGlobal"] = Array.new(2) { Hash.new(0) }
449
+
450
+ @summingIndex = 0
451
+ @sendingIndex = 1
452
+
453
+ end
454
+
455
+ if @logInfoHash[tag]["oldData"].size == 0
456
+ oldDataPresent = false
457
+ else
458
+ oldDataPresent = true
459
+ end
460
+
461
+ #get the current time
462
+ currentTime = Time.new
463
+
464
+ #If the current time slice has expired then move to the next slice and
465
+ #reset the time for the next time slice expiration
466
+ #Also need to check if we are past the last slice. If we are past
467
+ #the last slice we need to start again at zero and copy the current
468
+ #slice to the old_data.
469
+ if ( currentTime - @stopTimeSlice ) >= 0
470
+
471
+ #Increment the slice index since the time has expired for this
472
+ #time slice
473
+ @sliceIndex = @sliceIndex + 1
474
+
475
+ #if we have gone past the total number of increments we need to
476
+ #do some housekeeping like set the @sliceIndex to zero so that
477
+ #we start over again with our index
478
+ if @sliceIndex >= @totalIncrements
479
+ @sliceIndex = 0
480
+
481
+ #Need to know whether this is the first time through this
482
+ #time span since the first time through gets handled differently.
483
+ #The first time through we don't have any data in the "old"
484
+ #time slice elements so we can't add them to anything.
485
+ if @firstIteration == true
486
+ @firstIteration = false
487
+ end
488
+ end
489
+
490
+ #set the new time point for the end of the current time slice
491
+ @stopTimeSlice = @stopTimeSlice + time_slice
492
+
493
+ #NOTE: This seems strange but after we have iterated through all the
494
+ #time slices and we start again, the oldest time slice is the one that
495
+ #we are about to overwrite. It is also the one we want to use to add
496
+ #to the oldData and subtract from the new data. Therefore we use the
497
+ #@sliceIndex to set the @oldestSliceIndex and get those values
498
+ #before they are overwritten. This differs from how Jeff's original
499
+ #algorithm was written but I think this is correct.
500
+
501
+ #Also, in the first iteration, there is no data in the "oldData" arrays
502
+ #and hashes so we don't execute this code during the first iteration
503
+ #through the time slices.
504
+ if @firstIteration == false
505
+ @oldestSliceIndex = @sliceIndex
506
+ @logInfoHash[tag]["newTotalAll"] = @logInfoHash[tag]["newTotalAll"] - @logInfoHash[tag]["newIncrementTotals"][@oldestSliceIndex]
507
+ @logInfoHash[tag]["oldDataTotal"] = @logInfoHash[tag]["oldDataTotal"] + @logInfoHash[tag]["newIncrementTotals"][@oldestSliceIndex]
508
+ @logInfoHash[tag]["newDataArray"][@oldestSliceIndex].each { |key, value|
509
+ @logInfoHash[tag]["newTotal"][key] = @logInfoHash[tag]["newTotal"][key] - value
510
+ if oldDataPresent
511
+ @logInfoHash[tag]["oldData"][key] = @logInfoHash[tag]["oldData"][key] + value
512
+ end
513
+ @logInfoHash[tag]["newDataArray"][@oldestSliceIndex][key] = 0
514
+ }
515
+ @logInfoHash[tag]["newIncrementTotals"][@oldestSliceIndex] = 0
516
+ end
517
+ #Here we add the @sliceIndex -1 values to the updateGlobal Array/hash
518
+ #We always do this, we don't skip the 1st iteration. The -1 is because
519
+ #we added 1 to the sliceIndex above we need the values that were just
520
+ #counted
521
+ @logInfoHash[tag]["newDataArray"][@sliceIndex - 1].each { |key, value|
522
+ @logInfoHash[tag]["updateGlobal"][@summingIndex][key] = @logInfoHash[tag]["updateGlobal"][@summingIndex][key] + value
523
+ }
524
+
525
+
526
+ end
527
+
528
+ # $log.debug(" Outside Time if...logId from logInfoHash is #{@logInfoHash.to_s}")
529
+ #Not sure about this code and how it fits in with the algorithm
530
+ #Need to research a bit more.....
531
+ #As far as I can tell at this point there seems to be some overlap
532
+ #between the usefulness of this larger time interval (i.e. total_time)
533
+ #and the time interval that we need/want for the global_counts update
534
+ #Might need to check in with Jeff on this
535
+ if ( currentTime - @stopTime ) >= 0
536
+
537
+ resetTimeInterval(currentTime)
538
+ #Do all the other things that need to be done here:
539
+ #write records to database for all hash values for all logid's
540
+ #shift the current counts to be old counts for all hash values for all logid's
541
+ #get ready for the new counts
542
+
543
+ @logInfoHash.each_key { |key|
544
+ # Copy counts from the summingIndex hashes to the sendingIndex hashes and
545
+ # Reset the summingIndex hash values to 0
546
+ @logInfoHash[key]["updateGlobal"][@summingIndex].each { |key2,value2|
547
+ @logInfoHash[key]["updateGlobal"][@sendingIndex][key2] = value2
548
+ @logInfoHash[key]["updateGlobal"][@summingIndex][key2] = 0
549
+
550
+
551
+ }
552
+ #Refresh the old data here...get it from the database
553
+ #this can be done just about any time but this looks like the
554
+ #the most logical time
555
+ @logInfoHash[key]["oldData"] = get_old_counts(@logInfoHash[key]["logId"])
556
+
557
+ #Get the total for all the values in the oldData
558
+ @logInfoHash[key]["oldDataTotal"] = get_total(@logInfoHash[key]["oldData"])
559
+ }
560
+
561
+ end
562
+
563
+ if (currentTime - @updateTime) >= 0
564
+ #Send update to database here and then reset @updateTime
565
+ #This should be a System call- ( a fork here ) so that we do "multi-threading"
566
+ #We do not check for a return value since that would require a wait.
567
+ $log.debug("Before @updateTime has been reset it is #{@updateTime.utc.iso8601}")
568
+ @updateTime = @stopTime + get_update_time_offset()
569
+ $log.debug("After @updateTime has been reset it is #{@updateTime.utc.iso8601}")
570
+
571
+ #Split out to a subprocess to do this/these updates so there is no waiting
572
+ pid = fork do
573
+ @logInfoHash.each_key { |key|
574
+ update_counts_db(@logInfoHash[key]["logId"],@logInfoHash[key]["updateGlobal"][@sendingIndex])
575
+ }
576
+ end
577
+ #This tells Ruby that we don't want to hold on to this subprocess once it completes.
578
+ #When the subprocess completes just let it exit quietly.
579
+ #Supposedly one can use Thread#join to get the status of this subprocess
580
+ #after it has completed but I can't seem to get that to work. I'm
581
+ #not sure if the Thread methods really apply to Process.
582
+ Process.detach(pid)
583
+ end
584
+
585
+ #Switched to using the grok_name key so we don't need to create a string before we do the MD5 transformation.
586
+ #rec_hash = create_line_hash(tag,record)
587
+ rec_hash = create_md5_from_grok_name(record)
588
+ $log.debug("md5 hash is #{rec_hash}. tag is #{tag}")
589
+ $log.debug("logFileId is #{@logInfoHash[tag]["logId"]}")
590
+
591
+ @logInfoHash[tag]["newDataArray"][@sliceIndex][rec_hash] = @logInfoHash[tag]["newDataArray"][@sliceIndex][rec_hash] + 1
592
+ @logInfoHash[tag]["newTotal"][rec_hash] = @logInfoHash[tag]["newTotal"][rec_hash] + 1
593
+ @logInfoHash[tag]["newIncrementTotals"][@sliceIndex] = @logInfoHash[tag]["newIncrementTotals"][@sliceIndex] + 1
594
+ @logInfoHash[tag]["newTotalAll"] = @logInfoHash[tag]["newTotalAll"] + 1
595
+
596
+ #Can only calculate a score if we have old data!
597
+ if oldDataPresent
598
+ $log.debug("parameters passing into getChiSqrScore are #{@logInfoHash[tag]["newDataArray"][@sliceIndex][rec_hash]} and #{@logInfoHash[tag]["newTotal"][rec_hash]} and #{@logInfoHash[tag]["oldData"][rec_hash]} and #{@logInfoHash[tag]["oldDataTotal"]}")
599
+ #This score gives us something like ( or maybe just like) a q value.
600
+ #Typically anything >= 0.95 will be considered significant but we might want that paramertized
601
+ score = getChiSqrScore(@logInfoHash[tag]["newDataArray"][@sliceIndex][rec_hash],
602
+ @logInfoHash[tag]["newTotalAll"],
603
+ @logInfoHash[tag]["oldData"][rec_hash],
604
+ @logInfoHash[tag]["oldDataTotal"])
605
+ $log.debug("for message: #{record.to_s}")
606
+ $log.debug("the score is #{score}")
607
+ if score >= @qValue
608
+ $log.info("Found an anomaly!! score is #{score}")
609
+ $log.info("Anomaly is found for message: #{record.to_s}")
610
+ end
611
+ end
612
+ end
613
+ end
614
+ end
@@ -0,0 +1,45 @@
1
+
2
+ require "statistics2"
3
+
4
+ def getChiSqrScore (countNew,totalNew,countOld,totalOld)
5
+
6
+ countNew = countNew.to_f
7
+ countOld = countOld.to_f
8
+ totalNew = totalNew.to_f
9
+ totalOld = totalOld.to_f
10
+
11
+
12
+
13
+ p = countNew/ totalNew
14
+ q = countOld / totalOld
15
+
16
+ if ( p < q )
17
+ return 0
18
+ end
19
+
20
+ t = (countNew + countOld) / (totalOld + totalNew)
21
+
22
+ if ( t == 0)
23
+ return nil
24
+ end
25
+
26
+ v = countNew * Math::log(p/t) + countOld * Math::log(q/t)
27
+
28
+ if ( t == 1 )
29
+ return Statistics2..chi2X_(1,2*v)
30
+ end
31
+
32
+ if ( p < 1)
33
+ v = v + ((totalNew - countNew) * Math::log((1-p)/(1-t)))
34
+ end
35
+
36
+ if ( q < 1)
37
+ v = v + ((totalOld - countOld) * Math::log((1-q)/(1-t)))
38
+ end
39
+
40
+ return Statistics2.chi2X_(1,2*v)
41
+
42
+ end
43
+
44
+
45
+
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path("../../", __FILE__))
2
+ require "test-unit"
3
+ require "fluent/test"
4
+ require "fluent/test/driver/output"
5
+ require "fluent/test/helpers"
6
+
7
+ Test::Unit::TestCase.include(Fluent::Test::Helpers)
8
+ Test::Unit::TestCase.extend(Fluent::Test::Helpers)
@@ -0,0 +1,18 @@
1
+ require "helper"
2
+ require "fluent/plugin/out_cdma_stat.rb"
3
+
4
+ class CdmaaStatOutputTest < Test::Unit::TestCase
5
+ setup do
6
+ Fluent::Test.setup
7
+ end
8
+
9
+ test "failure" do
10
+ flunk
11
+ end
12
+
13
+ private
14
+
15
+ def create_driver(conf)
16
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::CdmaaStatOutput).configure(conf)
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-cmdaa-stat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.14
5
+ platform: ruby
6
+ authors:
7
+ - Mark Pohl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-03-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: test-unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fluentd
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.14.10
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '2'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 0.14.10
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '2'
75
+ description:
76
+ email:
77
+ - mark.g.pohl@gmail.com
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - lib/fluent/plugin/filter_cmda_stat.rb
83
+ - lib/fluent/plugin/log-likelihood.rb
84
+ - test/helper.rb
85
+ - test/plugin/test_out_cdma_stat.rb
86
+ homepage: http://rubygems.com/cdmaa
87
+ licenses:
88
+ - Apache-2.0
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.7.6
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: CMDA plugin to process logdata and save stats to a database
110
+ test_files:
111
+ - test/helper.rb
112
+ - test/plugin/test_out_cdma_stat.rb