logstash-filter-aggregate 0.1.5 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,277 +1,277 @@
1
- # encoding: utf-8
2
-
3
- require "logstash/filters/base"
4
- require "logstash/namespace"
5
- require "thread"
6
-
7
- #
8
- # The aim of this filter is to aggregate information available among several events (typically log lines) belonging to a same task,
9
- # and finally push aggregated information into final task event.
10
- #
11
- # ==== Example #1
12
- #
13
- # * with these given logs :
14
- # [source,ruby]
15
- # ----------------------------------
16
- # INFO - 12345 - TASK_START - start
17
- # INFO - 12345 - SQL - sqlQuery1 - 12
18
- # INFO - 12345 - SQL - sqlQuery2 - 34
19
- # INFO - 12345 - TASK_END - end
20
- # ----------------------------------
21
- #
22
- # * you can aggregate "sql duration" for the whole task with this configuration :
23
- # [source,ruby]
24
- # ----------------------------------
25
- # filter {
26
- # grok {
27
- # match => [ "message", "%{LOGLEVEL:loglevel} - %{NOTSPACE:taskid} - %{NOTSPACE:logger} - %{WORD:label}( - %{INT:duration:int})?" ]
28
- # }
29
- #
30
- # if [logger] == "TASK_START" {
31
- # aggregate {
32
- # task_id => "%{taskid}"
33
- # code => "map['sql_duration'] = 0"
34
- # map_action => "create"
35
- # }
36
- # }
37
- #
38
- # if [logger] == "SQL" {
39
- # aggregate {
40
- # task_id => "%{taskid}"
41
- # code => "map['sql_duration'] += event['duration']"
42
- # map_action => "update"
43
- # }
44
- # }
45
- #
46
- # if [logger] == "TASK_END" {
47
- # aggregate {
48
- # task_id => "%{taskid}"
49
- # code => "event['sql_duration'] = map['sql_duration']"
50
- # map_action => "update"
51
- # end_of_task => true
52
- # timeout => 120
53
- # }
54
- # }
55
- # }
56
- # ----------------------------------
57
- #
58
- # * the final event then looks like :
59
- # [source,ruby]
60
- # ----------------------------------
61
- # {
62
- # "message" => "INFO - 12345 - TASK_END - end message",
63
- # "sql_duration" => 46
64
- # }
65
- # ----------------------------------
66
- #
67
- # the field `sql_duration` is added and contains the sum of all sql queries durations.
68
- #
69
- # ==== Example #2
70
- #
71
- # * If you have the same logs than example #1, but without a start log :
72
- # [source,ruby]
73
- # ----------------------------------
74
- # INFO - 12345 - SQL - sqlQuery1 - 12
75
- # INFO - 12345 - SQL - sqlQuery2 - 34
76
- # INFO - 12345 - TASK_END - end
77
- # ----------------------------------
78
- #
79
- # * you can also aggregate "sql duration" with a slightly different configuration :
80
- # [source,ruby]
81
- # ----------------------------------
82
- # filter {
83
- # grok {
84
- # match => [ "message", "%{LOGLEVEL:loglevel} - %{NOTSPACE:taskid} - %{NOTSPACE:logger} - %{WORD:label}( - %{INT:duration:int})?" ]
85
- # }
86
- #
87
- # if [logger] == "SQL" {
88
- # aggregate {
89
- # task_id => "%{taskid}"
90
- # code => "map['sql_duration'] ||= 0 ; map['sql_duration'] += event['duration']"
91
- # }
92
- # }
93
- #
94
- # if [logger] == "TASK_END" {
95
- # aggregate {
96
- # task_id => "%{taskid}"
97
- # code => "event['sql_duration'] = map['sql_duration']"
98
- # end_of_task => true
99
- # timeout => 120
100
- # }
101
- # }
102
- # }
103
- # ----------------------------------
104
- #
105
- # * the final event is exactly the same than example #1
106
- # * the key point is the "||=" ruby operator. It allows to initialize 'sql_duration' map entry to 0 only if this map entry is not already initialized
107
- #
108
- #
109
- # ==== How it works
110
- # * the filter needs a "task_id" to correlate events (log lines) of a same task
111
- # * at the task beggining, filter creates a map, attached to task_id
112
- # * for each event, you can execute code using 'event' and 'map' (for instance, copy an event field to map)
113
- # * in the final event, you can execute a last code (for instance, add map data to final event)
114
- # * after the final event, the map attached to task is deleted
115
- # * in one filter configuration, it is recommanded to define a timeout option to protect the feature against unterminated tasks. It tells the filter to delete expired maps
116
- # * if no timeout is defined, by default, all maps older than 1800 seconds are automatically deleted
117
- # * finally, if `code` execution raises an exception, the error is logged and event is tagged '_aggregateexception'
118
- #
119
- #
120
- class LogStash::Filters::Aggregate < LogStash::Filters::Base
121
-
122
- config_name "aggregate"
123
-
124
- # The expression defining task ID to correlate logs.
125
- #
126
- # This value must uniquely identify the task in the system.
127
- #
128
- # Example value : "%{application}%{my_task_id}"
129
- config :task_id, :validate => :string, :required => true
130
-
131
- # The code to execute to update map, using current event.
132
- #
133
- # Or on the contrary, the code to execute to update event, using current map.
134
- #
135
- # You will have a 'map' variable and an 'event' variable available (that is the event itself).
136
- #
137
- # Example value : "map['sql_duration'] += event['duration']"
138
- config :code, :validate => :string, :required => true
139
-
140
- # Tell the filter what to do with aggregate map.
141
- #
142
- # `create`: create the map, and execute the code only if map wasn't created before
143
- #
144
- # `update`: doesn't create the map, and execute the code only if map was created before
145
- #
146
- # `create_or_update`: create the map if it wasn't created before, execute the code in all cases
147
- config :map_action, :validate => :string, :default => "create_or_update"
148
-
149
- # Tell the filter that task is ended, and therefore, to delete map after code execution.
150
- config :end_of_task, :validate => :boolean, :default => false
151
-
152
- # The amount of seconds after a task "end event" can be considered lost.
153
- #
154
- # The task "map" is evicted.
155
- #
156
- # Default value (`0`) means no timeout so no auto eviction.
157
- config :timeout, :validate => :number, :required => false, :default => 0
158
-
159
-
160
- # Default timeout (in seconds) when not defined in plugin configuration
161
- DEFAULT_TIMEOUT = 1800
162
-
163
- # This is the state of the filter.
164
- # For each entry, key is "task_id" and value is a map freely updatable by 'code' config
165
- @@aggregate_maps = {}
166
-
167
- # Mutex used to synchronize access to 'aggregate_maps'
168
- @@mutex = Mutex.new
169
-
170
- # Aggregate instance which will evict all zombie Aggregate elements (older than timeout)
171
- @@eviction_instance = nil
172
-
173
- # last time where eviction was launched
174
- @@last_eviction_timestamp = nil
175
-
176
- # Initialize plugin
177
- public
178
- def register
179
- # process lambda expression to call in each filter call
180
- eval("@codeblock = lambda { |event, map| #{@code} }", binding, "(aggregate filter code)")
181
-
182
- # define eviction_instance
183
- @@mutex.synchronize do
184
- if (@timeout > 0 && (@@eviction_instance.nil? || @timeout < @@eviction_instance.timeout))
185
- @@eviction_instance = self
186
- @logger.info("Aggregate, timeout: #{@timeout} seconds")
187
- end
188
- end
189
- end
190
-
191
-
192
- # This method is invoked each time an event matches the filter
193
- public
194
- def filter(event)
195
- # return nothing unless there's an actual filter event
196
- return unless filter?(event)
197
-
198
- # define task id
199
- task_id = event.sprintf(@task_id)
200
- return if task_id.nil? || task_id == @task_id
201
-
202
- noError = false
203
-
204
- # protect aggregate_maps against concurrent access, using a mutex
205
- @@mutex.synchronize do
206
-
207
- # retrieve the current aggregate map
208
- aggregate_maps_element = @@aggregate_maps[task_id]
209
- if (aggregate_maps_element.nil?)
210
- return if @map_action == "update"
211
- aggregate_maps_element = LogStash::Filters::Aggregate::Element.new(Time.now);
212
- @@aggregate_maps[task_id] = aggregate_maps_element
213
- else
214
- return if @map_action == "create"
215
- end
216
- map = aggregate_maps_element.map
217
-
218
- # execute the code to read/update map and event
219
- begin
220
- @codeblock.call(event, map)
221
- noError = true
222
- rescue => exception
223
- @logger.error("Aggregate exception occurred. Error: #{exception} ; Code: #{@code} ; Map: #{map} ; EventData: #{event.instance_variable_get('@data')}")
224
- event.tag("_aggregateexception")
225
- end
226
-
227
- # delete the map if task is ended
228
- @@aggregate_maps.delete(task_id) if @end_of_task
229
- end
230
-
231
- # match the filter, only if no error occurred
232
- filter_matched(event) if noError
233
- end
234
-
235
- # Necessary to indicate logstash to periodically call 'flush' method
236
- def periodic_flush
237
- true
238
- end
239
-
240
- # This method is invoked by LogStash every 5 seconds.
241
- def flush(options = {})
242
- # Protection against no timeout defined by logstash conf : define a default eviction instance with timeout = DEFAULT_TIMEOUT seconds
243
- if (@@eviction_instance.nil?)
244
- @@eviction_instance = self
245
- @timeout = DEFAULT_TIMEOUT
246
- end
247
-
248
- # Launch eviction only every interval of (@timeout / 2) seconds
249
- if (@@eviction_instance == self && (@@last_eviction_timestamp.nil? || Time.now > @@last_eviction_timestamp + @timeout / 2))
250
- remove_expired_elements()
251
- @@last_eviction_timestamp = Time.now
252
- end
253
-
254
- return nil
255
- end
256
-
257
-
258
- # Remove the expired Aggregate elements from "aggregate_maps" if they are older than timeout
259
- def remove_expired_elements()
260
- min_timestamp = Time.now - @timeout
261
- @@mutex.synchronize do
262
- @@aggregate_maps.delete_if { |key, element| element.creation_timestamp < min_timestamp }
263
- end
264
- end
265
-
266
- end # class LogStash::Filters::Aggregate
267
-
268
- # Element of "aggregate_maps"
269
- class LogStash::Filters::Aggregate::Element
270
-
271
- attr_accessor :creation_timestamp, :map
272
-
273
- def initialize(creation_timestamp)
274
- @creation_timestamp = creation_timestamp
275
- @map = {}
276
- end
277
- end
1
+ # encoding: utf-8
2
+
3
+ require "logstash/filters/base"
4
+ require "logstash/namespace"
5
+ require "thread"
6
+
7
+ #
8
+ # The aim of this filter is to aggregate information available among several events (typically log lines) belonging to a same task,
9
+ # and finally push aggregated information into final task event.
10
+ #
11
+ # ==== Example #1
12
+ #
13
+ # * with these given logs :
14
+ # [source,ruby]
15
+ # ----------------------------------
16
+ # INFO - 12345 - TASK_START - start
17
+ # INFO - 12345 - SQL - sqlQuery1 - 12
18
+ # INFO - 12345 - SQL - sqlQuery2 - 34
19
+ # INFO - 12345 - TASK_END - end
20
+ # ----------------------------------
21
+ #
22
+ # * you can aggregate "sql duration" for the whole task with this configuration :
23
+ # [source,ruby]
24
+ # ----------------------------------
25
+ # filter {
26
+ # grok {
27
+ # match => [ "message", "%{LOGLEVEL:loglevel} - %{NOTSPACE:taskid} - %{NOTSPACE:logger} - %{WORD:label}( - %{INT:duration:int})?" ]
28
+ # }
29
+ #
30
+ # if [logger] == "TASK_START" {
31
+ # aggregate {
32
+ # task_id => "%{taskid}"
33
+ # code => "map['sql_duration'] = 0"
34
+ # map_action => "create"
35
+ # }
36
+ # }
37
+ #
38
+ # if [logger] == "SQL" {
39
+ # aggregate {
40
+ # task_id => "%{taskid}"
41
+ # code => "map['sql_duration'] += event['duration']"
42
+ # map_action => "update"
43
+ # }
44
+ # }
45
+ #
46
+ # if [logger] == "TASK_END" {
47
+ # aggregate {
48
+ # task_id => "%{taskid}"
49
+ # code => "event['sql_duration'] = map['sql_duration']"
50
+ # map_action => "update"
51
+ # end_of_task => true
52
+ # timeout => 120
53
+ # }
54
+ # }
55
+ # }
56
+ # ----------------------------------
57
+ #
58
+ # * the final event then looks like :
59
+ # [source,ruby]
60
+ # ----------------------------------
61
+ # {
62
+ # "message" => "INFO - 12345 - TASK_END - end message",
63
+ # "sql_duration" => 46
64
+ # }
65
+ # ----------------------------------
66
+ #
67
+ # the field `sql_duration` is added and contains the sum of all sql queries durations.
68
+ #
69
+ # ==== Example #2
70
+ #
71
+ # * If you have the same logs than example #1, but without a start log :
72
+ # [source,ruby]
73
+ # ----------------------------------
74
+ # INFO - 12345 - SQL - sqlQuery1 - 12
75
+ # INFO - 12345 - SQL - sqlQuery2 - 34
76
+ # INFO - 12345 - TASK_END - end
77
+ # ----------------------------------
78
+ #
79
+ # * you can also aggregate "sql duration" with a slightly different configuration :
80
+ # [source,ruby]
81
+ # ----------------------------------
82
+ # filter {
83
+ # grok {
84
+ # match => [ "message", "%{LOGLEVEL:loglevel} - %{NOTSPACE:taskid} - %{NOTSPACE:logger} - %{WORD:label}( - %{INT:duration:int})?" ]
85
+ # }
86
+ #
87
+ # if [logger] == "SQL" {
88
+ # aggregate {
89
+ # task_id => "%{taskid}"
90
+ # code => "map['sql_duration'] ||= 0 ; map['sql_duration'] += event['duration']"
91
+ # }
92
+ # }
93
+ #
94
+ # if [logger] == "TASK_END" {
95
+ # aggregate {
96
+ # task_id => "%{taskid}"
97
+ # code => "event['sql_duration'] = map['sql_duration']"
98
+ # end_of_task => true
99
+ # timeout => 120
100
+ # }
101
+ # }
102
+ # }
103
+ # ----------------------------------
104
+ #
105
+ # * the final event is exactly the same than example #1
106
+ # * the key point is the "||=" ruby operator. It allows to initialize 'sql_duration' map entry to 0 only if this map entry is not already initialized
107
+ #
108
+ #
109
+ # ==== How it works
110
+ # * the filter needs a "task_id" to correlate events (log lines) of a same task
111
+ # * at the task beggining, filter creates a map, attached to task_id
112
+ # * for each event, you can execute code using 'event' and 'map' (for instance, copy an event field to map)
113
+ # * in the final event, you can execute a last code (for instance, add map data to final event)
114
+ # * after the final event, the map attached to task is deleted
115
+ # * in one filter configuration, it is recommanded to define a timeout option to protect the feature against unterminated tasks. It tells the filter to delete expired maps
116
+ # * if no timeout is defined, by default, all maps older than 1800 seconds are automatically deleted
117
+ # * finally, if `code` execution raises an exception, the error is logged and event is tagged '_aggregateexception'
118
+ #
119
+ #
120
+ class LogStash::Filters::Aggregate < LogStash::Filters::Base
121
+
122
+ config_name "aggregate"
123
+
124
+ # The expression defining task ID to correlate logs.
125
+ #
126
+ # This value must uniquely identify the task in the system.
127
+ #
128
+ # Example value : "%{application}%{my_task_id}"
129
+ config :task_id, :validate => :string, :required => true
130
+
131
+ # The code to execute to update map, using current event.
132
+ #
133
+ # Or on the contrary, the code to execute to update event, using current map.
134
+ #
135
+ # You will have a 'map' variable and an 'event' variable available (that is the event itself).
136
+ #
137
+ # Example value : "map['sql_duration'] += event['duration']"
138
+ config :code, :validate => :string, :required => true
139
+
140
+ # Tell the filter what to do with aggregate map.
141
+ #
142
+ # `create`: create the map, and execute the code only if map wasn't created before
143
+ #
144
+ # `update`: doesn't create the map, and execute the code only if map was created before
145
+ #
146
+ # `create_or_update`: create the map if it wasn't created before, execute the code in all cases
147
+ config :map_action, :validate => :string, :default => "create_or_update"
148
+
149
+ # Tell the filter that task is ended, and therefore, to delete map after code execution.
150
+ config :end_of_task, :validate => :boolean, :default => false
151
+
152
+ # The amount of seconds after a task "end event" can be considered lost.
153
+ #
154
+ # The task "map" is evicted.
155
+ #
156
+ # Default value (`0`) means no timeout so no auto eviction.
157
+ config :timeout, :validate => :number, :required => false, :default => 0
158
+
159
+
160
+ # Default timeout (in seconds) when not defined in plugin configuration
161
+ DEFAULT_TIMEOUT = 1800
162
+
163
+ # This is the state of the filter.
164
+ # For each entry, key is "task_id" and value is a map freely updatable by 'code' config
165
+ @@aggregate_maps = {}
166
+
167
+ # Mutex used to synchronize access to 'aggregate_maps'
168
+ @@mutex = Mutex.new
169
+
170
+ # Aggregate instance which will evict all zombie Aggregate elements (older than timeout)
171
+ @@eviction_instance = nil
172
+
173
+ # last time where eviction was launched
174
+ @@last_eviction_timestamp = nil
175
+
176
+ # Initialize plugin
177
+ public
178
+ def register
179
+ # process lambda expression to call in each filter call
180
+ eval("@codeblock = lambda { |event, map| #{@code} }", binding, "(aggregate filter code)")
181
+
182
+ # define eviction_instance
183
+ @@mutex.synchronize do
184
+ if (@timeout > 0 && (@@eviction_instance.nil? || @timeout < @@eviction_instance.timeout))
185
+ @@eviction_instance = self
186
+ @logger.info("Aggregate, timeout: #{@timeout} seconds")
187
+ end
188
+ end
189
+ end
190
+
191
+
192
+ # This method is invoked each time an event matches the filter
193
+ public
194
+ def filter(event)
195
+ # return nothing unless there's an actual filter event
196
+
197
+
198
+ # define task id
199
+ task_id = event.sprintf(@task_id)
200
+ return if task_id.nil? || task_id.empty? || task_id == @task_id
201
+
202
+ noError = false
203
+
204
+ # protect aggregate_maps against concurrent access, using a mutex
205
+ @@mutex.synchronize do
206
+
207
+ # retrieve the current aggregate map
208
+ aggregate_maps_element = @@aggregate_maps[task_id]
209
+ if (aggregate_maps_element.nil?)
210
+ return if @map_action == "update"
211
+ aggregate_maps_element = LogStash::Filters::Aggregate::Element.new(Time.now);
212
+ @@aggregate_maps[task_id] = aggregate_maps_element
213
+ else
214
+ return if @map_action == "create"
215
+ end
216
+ map = aggregate_maps_element.map
217
+
218
+ # execute the code to read/update map and event
219
+ begin
220
+ @codeblock.call(event, map)
221
+ noError = true
222
+ rescue => exception
223
+ @logger.error("Aggregate exception occurred. Error: #{exception} ; Code: #{@code} ; Map: #{map} ; EventData: #{event.instance_variable_get('@data')}")
224
+ event.tag("_aggregateexception")
225
+ end
226
+
227
+ # delete the map if task is ended
228
+ @@aggregate_maps.delete(task_id) if @end_of_task
229
+ end
230
+
231
+ # match the filter, only if no error occurred
232
+ filter_matched(event) if noError
233
+ end
234
+
235
+ # Necessary to indicate logstash to periodically call 'flush' method
236
+ def periodic_flush
237
+ true
238
+ end
239
+
240
+ # This method is invoked by LogStash every 5 seconds.
241
+ def flush(options = {})
242
+ # Protection against no timeout defined by logstash conf : define a default eviction instance with timeout = DEFAULT_TIMEOUT seconds
243
+ if (@@eviction_instance.nil?)
244
+ @@eviction_instance = self
245
+ @timeout = DEFAULT_TIMEOUT
246
+ end
247
+
248
+ # Launch eviction only every interval of (@timeout / 2) seconds
249
+ if (@@eviction_instance == self && (@@last_eviction_timestamp.nil? || Time.now > @@last_eviction_timestamp + @timeout / 2))
250
+ remove_expired_elements()
251
+ @@last_eviction_timestamp = Time.now
252
+ end
253
+
254
+ return nil
255
+ end
256
+
257
+
258
+ # Remove the expired Aggregate elements from "aggregate_maps" if they are older than timeout
259
+ def remove_expired_elements()
260
+ min_timestamp = Time.now - @timeout
261
+ @@mutex.synchronize do
262
+ @@aggregate_maps.delete_if { |key, element| element.creation_timestamp < min_timestamp }
263
+ end
264
+ end
265
+
266
+ end # class LogStash::Filters::Aggregate
267
+
268
+ # Element of "aggregate_maps"
269
+ class LogStash::Filters::Aggregate::Element
270
+
271
+ attr_accessor :creation_timestamp, :map
272
+
273
+ def initialize(creation_timestamp)
274
+ @creation_timestamp = creation_timestamp
275
+ @map = {}
276
+ end
277
+ end