logstash-output-application_insights 0.1.3

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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CONTRIBUTORS +9 -0
  4. data/DEVELOPER.md +0 -0
  5. data/Gemfile +26 -0
  6. data/LICENSE +17 -0
  7. data/README.md +495 -0
  8. data/Rakefile +22 -0
  9. data/lib/logstash/outputs/application_insights.rb +393 -0
  10. data/lib/logstash/outputs/application_insights/blob.rb +923 -0
  11. data/lib/logstash/outputs/application_insights/block.rb +118 -0
  12. data/lib/logstash/outputs/application_insights/channel.rb +259 -0
  13. data/lib/logstash/outputs/application_insights/channels.rb +142 -0
  14. data/lib/logstash/outputs/application_insights/client.rb +110 -0
  15. data/lib/logstash/outputs/application_insights/clients.rb +113 -0
  16. data/lib/logstash/outputs/application_insights/config.rb +341 -0
  17. data/lib/logstash/outputs/application_insights/constants.rb +208 -0
  18. data/lib/logstash/outputs/application_insights/exceptions.rb +55 -0
  19. data/lib/logstash/outputs/application_insights/flow_control.rb +80 -0
  20. data/lib/logstash/outputs/application_insights/multi_io_logger.rb +69 -0
  21. data/lib/logstash/outputs/application_insights/shutdown.rb +96 -0
  22. data/lib/logstash/outputs/application_insights/state.rb +89 -0
  23. data/lib/logstash/outputs/application_insights/storage_cleanup.rb +214 -0
  24. data/lib/logstash/outputs/application_insights/sub_channel.rb +75 -0
  25. data/lib/logstash/outputs/application_insights/telemetry.rb +99 -0
  26. data/lib/logstash/outputs/application_insights/timer.rb +90 -0
  27. data/lib/logstash/outputs/application_insights/utils.rb +139 -0
  28. data/lib/logstash/outputs/application_insights/version.rb +24 -0
  29. data/logstash-output-application-insights.gemspec +50 -0
  30. data/spec/outputs/application_insights_spec.rb +42 -0
  31. metadata +151 -0
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ # ----------------------------------------------------------------------------------
4
+ # Logstash Output Application Insights
5
+ #
6
+ # Copyright (c) Microsoft Corporation
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Licensed under the Apache License, Version 2.0 (the License);
11
+ # you may not use this file except in compliance with the License.
12
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ #
18
+ # See the Apache Version 2.0 License for specific language governing
19
+ # permissions and limitations under the License.
20
+ # ----------------------------------------------------------------------------------
21
+
22
+ require "logstash/devutils/rake"
@@ -0,0 +1,393 @@
1
+ # encoding: utf-8
2
+
3
+ # ----------------------------------------------------------------------------------
4
+ # Logstash Output Application Insights
5
+ #
6
+ # Copyright (c) Microsoft Corporation
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Licensed under the Apache License, Version 2.0 (the License);
11
+ # you may not use this file except in compliance with the License.
12
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ #
18
+ # See the Apache Version 2.0 License for specific language governing
19
+ # permissions and limitations under the License.
20
+ # ----------------------------------------------------------------------------------
21
+
22
+ require "logstash/outputs/base"
23
+ require "logstash/namespace"
24
+ require 'logstash-core/version'
25
+
26
+ require "stud/interval"
27
+
28
+ require "azure/storage"
29
+ require 'azure/storage/core/auth/shared_access_signature'
30
+
31
+ require 'azure/core/http/http_request'
32
+ require 'azure/core/http/http_response'
33
+
34
+ require 'rbconfig'
35
+ require 'faraday'
36
+ require 'openssl'
37
+ require "time"
38
+ require "json"
39
+ require "uri"
40
+ require "socket"
41
+
42
+ require "thread"
43
+ require "concurrent" # for atomic and thread safe operations
44
+ require "logger"
45
+ require "csv"
46
+
47
+ require "application_insights"
48
+
49
+ class LogStash::Outputs::Application_insights < LogStash::Outputs::Base
50
+ require "logstash/outputs/application_insights/version"
51
+ require "logstash/outputs/application_insights/utils"
52
+ require "logstash/outputs/application_insights/constants"
53
+ require "logstash/outputs/application_insights/config"
54
+ require "logstash/outputs/application_insights/blob"
55
+ autoload :Block, "logstash/outputs/application_insights/block"
56
+ autoload :Storage_cleanup, "logstash/outputs/application_insights/storage_cleanup"
57
+
58
+ autoload :Clients, "logstash/outputs/application_insights/clients"
59
+ autoload :Client, "logstash/outputs/application_insights/client"
60
+
61
+ autoload :Channels, "logstash/outputs/application_insights/channels"
62
+ autoload :Channel, "logstash/outputs/application_insights/channel"
63
+ autoload :Sub_channel, "logstash/outputs/application_insights/sub_channel"
64
+ autoload :State, "logstash/outputs/application_insights/state"
65
+ autoload :Flow_control, "logstash/outputs/application_insights/flow_control"
66
+ autoload :Shutdown, "logstash/outputs/application_insights/shutdown"
67
+ autoload :Telemetry, "logstash/outputs/application_insights/telemetry"
68
+
69
+ require "logstash/outputs/application_insights/exceptions"
70
+ require "logstash/outputs/application_insights/timer"
71
+ require "logstash/outputs/application_insights/multi_io_logger"
72
+
73
+ config_name "application_insights"
74
+
75
+ # default :codec, "json_lines"
76
+
77
+ # Array of pairs, storage_account_name and an array of acces_keys
78
+ # examples [ account1, key1 ]
79
+ # [ [account1, key1], [accoun2, key2], [account3, key3] ]
80
+ # [ [account1, [key11, key12]], [account2, key2], [account3, [key3] ]
81
+ config :storage_account_name_key, :validate => :array
82
+
83
+ # prefix for the azure storage tables used by this Logstash instance
84
+ # it is recommeded that each Logstash prefix have a unique prefix, to avoid
85
+ # confusion and loss of tracking, although sharing tables won't damage proper execution
86
+ # if not set, the machine name is used (not alphanumeric characters are removed, and converted downcase), if machine name available
87
+ # string may contain only alphanumeric character, case sensitive, and must start with a letter
88
+ config :azure_storage_table_prefix, :validate => :string
89
+
90
+ # prefix for the azure storage containers used by this Logstash instance
91
+ # it is recommeded that each Logstash prefix have a unique prefix, to avoid
92
+ # confusion and loss of tracking, although sharing containers won't damage proper execution
93
+ # if not set, the machine name is used (not alphanumeric characters are removed, and converted downcase), if machine name available
94
+ # string may contain only alphanumeric character and dash, double dash not allowed, case insesitive
95
+ config :azure_storage_container_prefix, :validate => :string
96
+
97
+ # prefix for the azure storage blobs used by this Logstash instance
98
+ # Each Logstash prefix MUST have a unique prefix, to avoid
99
+ # loss of data !!!
100
+ # if not set, the machine name is used (not alphanumeric characters are removed, and converted downcase), if machine name available
101
+ # string may include only characters that are allowed in any valid url
102
+ config :azure_storage_blob_prefix, :validate => :string
103
+
104
+ # Default Application Insights Analytics intrumentation_key
105
+ # will be used only in case it is not specified as a table_id property in tables
106
+ # or as part of the event's fields or event's metadata fields
107
+ config :intrumentation_key, :validate => :string
108
+
109
+ # Default Application Insights Analytics table_id
110
+ # will be used only in case it is not specified as part o
111
+ # of the event's fields or event's metadata fields
112
+ config :table_id, :validate => :string
113
+
114
+ # specifies the list of the fields that will be filtered from the events, fields not specified will be ignored.
115
+ # if not specified all fileds in events will be filtered, the order is kept, and is used for csv serialization
116
+ config :table_columns, :validate => :array
117
+
118
+ # if set to true events fields are refered as case insensitive, default case sensitive
119
+ config :case_insensitive_columns, :validate => :boolean
120
+
121
+ # A hash of table_ids, where each table_id points to a set of properties
122
+ # the properties are a hash, where the keys are are the properties
123
+ # current supported properties per table_id are:
124
+ # intrumentation_key, ext, table_columns, csv_default_value, csv_separator, blob_max_delay, event_separator, serialized_event_field
125
+ # intrumentation_key, Application Insights Analytics intrumentation_key, will be used in case not specified in any of the event's fields or events's metadata fileds
126
+ # serialized_event_field, specifies the field that may contain the full serialized event (either as json or csv),
127
+ # when specified, the ext property should be set either to csv or to json (json is the default)
128
+ # if event. does not conatin the field, value will be created based on the fileds in the evnt, according to table_columns if configured, or all fileds in event
129
+ # if event contains this filed, and ext is csv
130
+ # if value is a string, it will be used as is as the serialized event, without validating whether it is a csv string
131
+ # if value is an array, it will be serialized as an array of csv columns
132
+ # if value is a hash, it will be serialized based on table_columns to csv columns
133
+ # if event contains this filed, and ext is json
134
+ # if value is a string, it will be used as is as the serialized event, without validating whether it is a json string
135
+ # if value is a hash, it will be serialized to json, if fileds_map exit, it will be based on filds_map
136
+ # if value is an array, it will be zipped with table_columns (if exist) and serialized to json
137
+ # ext, blob extension, the only valid values are either csv or json,
138
+ # should be set whenever the default json is not appropriate (.e, csv)
139
+ # blob_max_delay, maximum latency time, in seconds, since the time the event arrived till it should be commited in azure storage, and Application Insights is notified
140
+ # event_separator, specifies the string that is used as a separator between events in the blob
141
+ # table_columns, specifies the event fields that should be serialized, and their order (order is required for csv)
142
+ # if csv serialization will be used for this table_id
143
+ # each table_columns field is a hash with 3 keys: name, type, and default. Only name is mandatory
144
+ # name - is the name of the event fleld that its value should be mapped to this columns
145
+ # type - is the type of this field: "string", "hash", "array", "number", "json", "boolean", "float", "integer", "dynamic", "datetime", "object"
146
+ # default - is the value to be used for this column, in case the field is missing in the event
147
+ # csv_separator, specifies the string that is used as a separator between columns,
148
+ # can be specified only together with table_columns
149
+ # csv_default_value, specifies the string that is used as the value in a csv record, in case the field does not exist in the event
150
+ # can be specified only together with table_columns
151
+ #
152
+ # Example json table_id
153
+ # tables => {"a679fbd2-702c-4c46-8548-80082c66ef28" => {"intrumentation_key" => "abee940b-e648-4242-b6b3-f2826667bf96", "blob_max_delay" => 60} }
154
+ # Example json table_id, input in serialized_event_field
155
+ # {"ab6a3584-aef0-4a82-8725-2f2336e59f3e" => {"serialized_event_field" => "message". "ext" => "json"} }
156
+ # Example csv table_id, input in serialized_event_field
157
+ # {"ab6a3584-aef0-4a82-8725-2f2336e59f3e" => {"serialized_event_field" => "csv_message". "ext" => "csv"} }
158
+ # Example csv table_id, input in event fields
159
+ # {"ab6a3584-aef0-4a82-8725-2f2336e59f3e" => { "ext" => "csv", "table_columns" => [ {name => "Timestamp" type => datetime }, "Value", "Custom" ] } }
160
+ # Example csv table_id, input in event fields
161
+ # {"ab6a3584-aef0-4a82-8725-2f2336e59f3e" => { "ext" => "json", "table_columns" => [ "Timestamp", "Value", "Custom" ] } }
162
+
163
+ config :tables, :validate => :hash, :default => {}
164
+
165
+ # Advanced, internal, should not be set, the default is 192 GB ( = 50,000 * 4 MB )
166
+ # azure storage maximum number of blocks per blob is 192 GB ( = 50,000 * 4 MB )
167
+ config :blob_max_bytesize, :validate => :number
168
+
169
+ # Specifies, maximum number of events in one blob,
170
+ # setting it low may reduce the latency, but reduce logstash performance
171
+ # setting it high may increase latency to maximum delay, but logstash will be more efficient, and load on network will be lower
172
+ config :blob_max_events, :validate => :number
173
+
174
+ # Specifies maximum latency time, in seconds, since the time the event arrived
175
+ # till it is commited to azure storage, and Application Insights is notified
176
+ # The total latency may be higher, as this is not the full ingestion flow
177
+ config :blob_max_delay, :validate => :number
178
+
179
+ # Specifies the blob serialziation to create. Default "json"
180
+ # currently 2 types are supported "csv" and "json""
181
+ config :blob_serialization, :validate => :string
182
+
183
+ # Interval of time between retries due to IO failures
184
+ config :io_retry_delay, :validate => :number
185
+
186
+ # Number of retries on IO failures, before giving up, and move to available options
187
+ config :io_max_retries, :validate => :number
188
+
189
+ # Specifies the retention time of the blob in the container after it is notified to Application Insighta Analytics
190
+ # Once the retention time expires, the blob is the deleted from container
191
+ config :blob_retention_time, :validate => :number
192
+
193
+ # Specifies the time Application Insights Analytics have access to the blob that are notifie
194
+ # Blob access is limited with SAS URL
195
+ config :blob_access_expiry_time, :validate => :number
196
+
197
+ # Advanced, internal, should not be set, the default is \r\n,
198
+ # specifies the string that is used as a separator between events in the blob
199
+ config :event_separator, :validate => :string
200
+
201
+ # Advanced, internal, should not be set, the default is comma,
202
+ # specifies the string that is used as a separator between columns in a csv record
203
+ config :csv_separator, :validate => :string
204
+
205
+ # specifies the string that is used as the value in a csv record, in case the field does not exist in the event, the default is ""
206
+ config :csv_default_value, :validate => :string
207
+
208
+ # specifies the log level. valid values are: DEBUG, INFO, WARN, ERROR, FATAL, UNKNOWN
209
+ config :logger_level, :validate => :string
210
+
211
+ # Specifies the list of targets for the log. may include files, devices, "stdout: and "stderr"
212
+ config :logger_files, :validate => :array
213
+
214
+ # Advanced, internal, should not be set, the default is AI,
215
+ # Specifies the program name that will displayed in each log record
216
+ config :logger_progname, :validate => :string
217
+
218
+ # Specifies when file logs are shifted. valid values are either an integer or "daily", "weekly" or "monthly"
219
+ config :logger_shift_size
220
+
221
+ # Specifies the shift age of a log.
222
+ # Number of old files to keep, or frequency of rotation (daily, weekly or monthly)
223
+ config :logger_shift_age, :validate => :number
224
+
225
+ # Specifies a serialized event field name, that if exist in current event, its value as is will be taken as the serialized event. No Default
226
+ config :serialized_event_field, :validate => :string
227
+
228
+ # Specifies the time interval, between tests that check whether a stoarge account came back to life,
229
+ # after it stoped responding
230
+ config :resurrect_delay, :validate => :number
231
+
232
+ # specifies the high water mark for the flow control, that used to avoid out of memory crash
233
+ # once memory consumption reach reach the high water mark, the plugin will stop accepting events, till memory
234
+ # is below the low water mark
235
+ config :flow_control_suspend_bytes, :validate => :number
236
+
237
+ # specifies the low water mark for the flow control, that used to avoid out of memory crash
238
+ # once memory consumption reach the high water mark, the plugin will stop accepting events, till memory
239
+ # is below the low water mark
240
+ config :flow_control_resume_bytes, :validate => :number
241
+
242
+ # specifies the amount of time the flow control suspend receiving event, to allow GC, and flush of event to Azure storage
243
+ # before checking whether memory is below low water mark
244
+ config :flow_control_delay, :validate => :number
245
+
246
+ # File path of the CA file if having issue with SSL
247
+ config :ca_file, :validate => :string
248
+
249
+
250
+ # When set to true, telemetry about the plugin, won't be sent to Application Insights
251
+ config :disable_telemetry, :validate => :boolean
252
+
253
+ # When set to true, storage cleanup won't be done by the plugin (should be done by some other means or by another Logstash process with this flag enabled)
254
+ config :disable_cleanup, :validate => :boolean
255
+
256
+ # When set to true, not notified blobs are deleted, if not set they are copied to the orphan-blobs container
257
+ config :delete_not_notified_blobs, :validate => :boolean
258
+
259
+ # When set to true, notified blobs records are save in table, as long as blobs are retained in their containers
260
+ config :save_notified_blobs_records, :validate => :boolean
261
+
262
+ # Advanced, internal, should not be set, the default is false
263
+ # When set to true, notification is not sent to application insights, but behaves as if notified
264
+ config :disable_notification, :validate => :boolean
265
+
266
+ # Advanced, internal, should not be set, the default is false
267
+ # When set to true, events are not uploaded, and blob not commited, but behaves as if uploaded and notified
268
+ config :disable_blob_upload, :validate => :boolean
269
+
270
+ # Advanced, internal, should not be set, the default is false
271
+ # When set to true, process will stop if an unknown IO error is found
272
+ config :stop_on_unknown_io_errors, :validate => :boolean
273
+
274
+ # Advanced, internal, should not be set, the default is Application Insights production endpoint
275
+ # when set notification are sent to an alternative endpoint, used for internal testing
276
+ config :notification_endpoint, :validate => :string
277
+
278
+ # Advanced, internal, should not be set, the only current valid value is 1
279
+ config :notification_version, :validate => :number
280
+
281
+ # When set to true, access to application insights will be validated at initialization
282
+ # and if validation fail, logstash process will abort
283
+ config :validate_endpoint, :validate => :boolean, :default => true
284
+
285
+ # When set to true, access to azure storage for each of the configured accounts will be validated at initialization
286
+ # and if validation fail, logstash process will abort
287
+ config :validate_storage, :validate => :boolean, :default => true
288
+
289
+ public
290
+
291
+ def register
292
+
293
+ # set configuration
294
+ Config.validate_and_adjust_configuration( default_configuration )
295
+ configuration = Config.current
296
+
297
+ Multi_io_logger.config( configuration )
298
+
299
+ # be careful don't use here @logger, as it will override Logstash @logger, and may create starnge behaviour
300
+ @private_logger = configuration[:logger]
301
+
302
+ @private_logger.info { "configuration: #{configuration}" }
303
+
304
+ @telemetry = Telemetry.instance
305
+ configuration[:telemetry_channel] = @telemetry.telemetry_channel
306
+
307
+ Timer.config( configuration )
308
+ Blob.config( configuration )
309
+ Blob.validate_endpoint if @validate_endpoint
310
+ Blob.validate_storage if @validate_storage
311
+
312
+ @shutdown = Shutdown.instance
313
+ @channels = Channels.instance
314
+
315
+ @storage_cleanup = Storage_cleanup.start
316
+
317
+ @private_logger.info { "plugin registered" }
318
+
319
+ # @codec.on_event do |event, encoded_event|
320
+ # @channels.receive( event, encoded_event )
321
+ # end
322
+
323
+ Telemetry.instance.track_event("register", {:properties => configuration})
324
+
325
+
326
+ return "ok\n"
327
+ end # def register
328
+
329
+
330
+ def receive ( event )
331
+ # @codec.encode( event )
332
+ @channels.receive( event, nil )
333
+ return "ok\n"
334
+ end
335
+
336
+ def close
337
+ Telemetry.instance.track_event( "close" )
338
+ Telemetry.instance.flush
339
+ @shutdown.submit
340
+ end
341
+
342
+ private
343
+
344
+ # -----------------------------------------------
345
+
346
+
347
+ def list_blob_names
348
+ blob_names = Set.new []
349
+ loop do
350
+ continuation_token = NIL
351
+ entries = @azure_blob.list_blobs(@container, { :timeout => 10, :marker => continuation_token})
352
+ @@logger.debug { 'blob entries: #{entries}' }
353
+ entries.each do |entry|
354
+ @@logger.debug { 'blob entry name: #{entry.name}' }
355
+ blob_names << entry.name
356
+ end
357
+ continuation_token = entries.continuation_token
358
+ break if continuation_token.empty?
359
+ end
360
+ return blob_names
361
+ end # def list_blobs
362
+
363
+
364
+ def list_container_names
365
+ container_names = Set.new []
366
+ loop do
367
+ continuation_token = NIL
368
+ containers = @azure_blob.list_containers()
369
+ @@logger.debug { 'containers: #{containers}' }
370
+ containers.each do |container|
371
+ @@logger.debug { 'container entry name:' + container.name }
372
+ container_names << container.name
373
+ upload(container.name, "blob-append-" + container.name, "test - " + container.name)
374
+ blobs = @azure_blob.list_blobs(container.name)
375
+ blobs.each do |blob|
376
+ @@logger.debug { 'blob name: ' + blob.name }
377
+ end
378
+ end
379
+ continuation_token = containers.continuation_token
380
+ break if continuation_token.empty?
381
+ end
382
+ return container_names
383
+ end # def list_blobs
384
+
385
+ def create_container (container_name)
386
+ begin
387
+ @azure_blob.create_container(container_name)
388
+ rescue
389
+ @@logger.debug { $! }
390
+ end
391
+ end
392
+ end
393
+
@@ -0,0 +1,923 @@
1
+ # encoding: utf-8
2
+
3
+ # ----------------------------------------------------------------------------------
4
+ # Logstash Output Application Insights
5
+ #
6
+ # Copyright (c) Microsoft Corporation
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Licensed under the Apache License, Version 2.0 (the License);
11
+ # you may not use this file except in compliance with the License.
12
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ #
18
+ # See the Apache Version 2.0 License for specific language governing
19
+ # permissions and limitations under the License.
20
+ # ----------------------------------------------------------------------------------
21
+ class LogStash::Outputs::Application_insights
22
+ class Blob
23
+
24
+ attr_reader :intrumentation_key
25
+ attr_reader :table_id
26
+ attr_reader :storage_account_name
27
+ attr_reader :container_name
28
+ attr_reader :blob_name
29
+ attr_reader :uploaded_events_count
30
+ attr_reader :uploaded_bytesize
31
+ attr_reader :oldest_event_time
32
+
33
+ attr_reader :io_queue
34
+
35
+ attr_reader :last_io_exception
36
+
37
+ public
38
+
39
+ def self.config ( configuration )
40
+ @@configuration = configuration
41
+
42
+ @@logger = configuration[:logger]
43
+ @@io_retry_delay = configuration[:io_retry_delay]
44
+ @@io_max_retries = configuration[:io_max_retries]
45
+ @@blob_max_bytesize = configuration[:blob_max_bytesize]
46
+ @@blob_max_events = configuration[:blob_max_events]
47
+ @@state_table_name = "#{configuration[:azure_storage_table_prefix]}#{STATE_TABLE_NAME}"
48
+ @@save_notified_blobs_records = configuration[:save_notified_blobs_records]
49
+
50
+ @@closing = false
51
+
52
+ # queues, per storage_account_name, for failed blob commit, will continue to try resending
53
+ @@failed_on_commit_retry_Qs = {}
54
+ launch_storage_recovery_threads( @@failed_on_commit_retry_Qs, :commit, :io_failure )
55
+ launch_storage_recovery_table_threads( :uploading )
56
+
57
+ # queues, per storage_account_name, for failed notify, will continue to try resending
58
+ @@failed_on_notify_retry_Qs = {}
59
+ launch_storage_recovery_threads( @@failed_on_notify_retry_Qs, :notify, :notify_failed_blob_not_accessible )
60
+ launch_storage_recovery_table_threads( :committed )
61
+
62
+ # for failed to notify due to endpoint, will continue to try resending
63
+ launch_endpoint_recovery_thread
64
+
65
+ # queues, per storage_account_name, for failed to log to table, will continue to try resending
66
+ @@failed_on_log_to_table_retry_Qs = {}
67
+ launch_storage_recovery_threads( @@failed_on_log_to_table_retry_Qs, :log_to_table_update, :io_failure )
68
+
69
+ end
70
+
71
+ def self.close
72
+ @@closing = true
73
+ end
74
+
75
+ def self.stopped?
76
+ @@closing
77
+ end
78
+
79
+ def self.launch_endpoint_recovery_thread
80
+ @@failed_on_notification_endpoint_retry_Q = Queue.new
81
+ storage_recovery_thread( nil, @@failed_on_notification_endpoint_retry_Q, :notify, :io_failure )
82
+ end
83
+
84
+ def self.launch_storage_recovery_threads ( queues, method, failure_reason )
85
+ @@configuration[:storage_account_name_key].each do |storage_account_name, storage_account_keys|
86
+ queues[storage_account_name] = Queue.new
87
+ # a threads, per storage account name
88
+ storage_recovery_thread( storage_account_name, queues[storage_account_name], method, failure_reason )
89
+ end
90
+ end
91
+
92
+ def self.launch_storage_recovery_table_threads ( state )
93
+ @@configuration[:storage_account_name_key].each do |storage_account_name, storage_account_keys|
94
+ recovery_table_thread( storage_account_name, state)
95
+ end
96
+ end
97
+
98
+ #return thread
99
+ def self.recovery_table_thread( storage_account_name, state )
100
+ Thread.new( storage_account_name, state ) do |storage_account_name, state|
101
+
102
+ blob = Blob.new
103
+
104
+ committed_tuples = [ ]
105
+ uncommitted_tuples = [ ]
106
+ upload_empty_tuples = [ ]
107
+ token = nil
108
+ finished = false
109
+ filter = "#{:PartitionKey} eq '#{@@configuration[:azure_storage_blob_prefix]}-#{state}'"
110
+
111
+ # should exit thread after fetching data from table, and submit recovery, the loop is only for case of failure
112
+ until finished || stopped? do
113
+ entities = blob.log_to_table_query( storage_account_name, filter, token )
114
+ if entities
115
+ token = entities.continuation_token
116
+
117
+ if :committed == state
118
+ entities.each do |entity|
119
+ State.instance.inc_pending_notifications
120
+ tuple = blob.table_entity_to_tuple( entity.properties )
121
+ @@failed_on_notification_endpoint_retry_Q << tuple
122
+ end
123
+
124
+ elsif :uploading == state
125
+ # first tuples are collected, before send to queues, to make sure blob states don't change in between
126
+ entities.each do |entity|
127
+ typed_tuple = nil
128
+ until typed_tuple || stopped?
129
+ typed_tuple = blob.update_commited_or_uncommited_list( entity.properties )
130
+ Stud.stoppable_sleep(60, 1) { stopped? } unless typed_tuple
131
+ end
132
+ next if stopped?
133
+
134
+ if typed_tuple[:committed]
135
+ committed_tuples << typed_tuple[:committed]
136
+ elsif typed_tuple[:uncommitted]
137
+ uncommitted_tuples << typed_tuple[:uncommitted]
138
+ else
139
+ upload_empty_tuples << typed_tuple[:upload_empty]
140
+ end
141
+ end
142
+ end
143
+
144
+ next if token
145
+ committed_tuples.each do |tuple|
146
+ State.instance.inc_pending_commits
147
+ @@failed_on_log_to_table_retry_Qs[storage_account_name] << tuple
148
+ end
149
+ uncommitted_tuples.each do |tuple|
150
+ State.instance.inc_pending_commits
151
+ @@failed_on_commit_retry_Qs[storage_account_name] << tuple
152
+ end
153
+ upload_empty_tuples.each do |tuple|
154
+ @@failed_on_log_to_table_retry_Qs[storage_account_name] << tuple
155
+ end
156
+ finished = true
157
+ else
158
+ Stud.stoppable_sleep(60, 1) { stopped? }
159
+ end
160
+ end
161
+ @@logger.info { "exit table recovery thread, storage: #{storage_account_name}, state: #{state}, entities: #{entities ? entities.length : nil}" }
162
+ end
163
+ end
164
+
165
+ def self.state_on? ( storage_account_name, blob, failure_reason )
166
+ if blob
167
+ if :io_failure == failure_reason
168
+ @@endpoint_state_on ||= blob.test_notification_endpoint( @@configuration[:storage_account_name_key][0][0] )
169
+ else
170
+ Clients.instance.storage_account_state_on?( storage_account_name )
171
+ end
172
+ elsif storage_account_name
173
+ Clients.instance.storage_account_state_on?( storage_account_name )
174
+ else
175
+ Clients.instance.storage_account_state_on?
176
+ end
177
+ end
178
+
179
+ def self.storage_recovery_thread( storage_account_name, queue, method, failure_reason )
180
+ # a threads, per storage account name, that retries failed blob commits / notification / table updates
181
+ Thread.new( storage_account_name, queue, method, failure_reason ) do |storage_account_name, queue, method, failure_reason|
182
+ blob = Blob.new if :notify == method
183
+ semaphore = Mutex.new
184
+ action = {:method => method, :semaphore => semaphore, :counter => 0 }
185
+ loop do
186
+ tuple ||= queue.pop
187
+ until state_on?( storage_account_name, blob, failure_reason ) do sleep( 1 ) end
188
+
189
+ not_busy = nil
190
+ semaphore.synchronize {
191
+ not_busy = action[:counter] += 1 if 10 > action[:counter]
192
+ }
193
+ if not_busy
194
+ Thread.new( action, tuple ) do |action, tuple|
195
+ Blob.new.send( action[:method], tuple )
196
+ action[:semaphore].synchronize {
197
+ action[:counter] -= 1
198
+ }
199
+ end
200
+ tuple = nil # release for GC
201
+ else
202
+ Stud.stoppable_sleep(60, 1) { 10 > action[:counter] }
203
+ next
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def self.validate_endpoint
210
+ io = Blob.new
211
+ raise ConfigurationError, "Failed to access application insights #{@@configuration[:notification_endpoint]}, due to error #{io.last_io_exception.inspect}" unless io.test_notification_endpoint( @@configuration[:storage_account_name_key][0][0] )
212
+ end
213
+
214
+ def self.validate_storage
215
+ io = Blob.new
216
+ @@configuration[:storage_account_name_key].each do |storage_account_name, storage_account_keys|
217
+ raise ConfigurationError, "Failed access azure storage account #{storage_account_name}, due to error #{io.last_io_exception.inspect}" unless io.test_storage( storage_account_name )
218
+ end
219
+ end
220
+
221
+
222
+ def initialize ( channel = nil, id = nil , no_queue = false )
223
+ @uploaded_block_ids = [ ]
224
+ @uploaded_block_numbers = [ ]
225
+ @uploaded_bytesize = 0
226
+ @uploaded_events_count = 0
227
+ @max_tries = @@io_max_retries + 1
228
+ @sub_state = :none
229
+
230
+ if channel
231
+ @id = id
232
+ @intrumentation_key = channel.intrumentation_key
233
+ @table_id = channel.table_id
234
+ @blob_max_delay = channel.blob_max_delay
235
+
236
+ @event_format_ext = channel.event_format_ext
237
+
238
+ unless no_queue
239
+
240
+ @io_queue = Queue.new
241
+ @timer = Timer.new
242
+
243
+ # create a thread that handles the IO of the blob
244
+ Thread.new do
245
+ next_block = nil
246
+ loop do
247
+ block_to_upload = nil # release reference to resource for GC
248
+ block_to_upload = next_block || @io_queue.pop
249
+ next_block = nil
250
+
251
+ if :trigger == @timer.state
252
+ next_block = block_to_upload unless :wakeup == block_to_upload
253
+ block_to_upload = :timeout
254
+ to_commit = :commit
255
+
256
+ elsif :close == block_to_upload
257
+ to_commit = :commit
258
+
259
+ # ignore :trigger as they are only to casue check timeout
260
+ elsif :wakeup == block_to_upload # ignore :wakeup
261
+ next
262
+
263
+ else
264
+ while @io_queue.length > 0
265
+ next_block = @io_queue.pop
266
+ next if :wakeup == next_block # ignore :wakeup
267
+ break if :close == next_block
268
+ break if blob_full?( next_block )
269
+ break unless block_to_upload.concat( next_block )
270
+ next_block = nil
271
+ end
272
+ end
273
+
274
+ unless to_commit
275
+ @timer.set( block_to_upload.oldest_event_time + @blob_max_delay, nil ) {|object| @io_queue << :wakeup if 0 == @io_queue.length } if blob_empty?
276
+ to_commit = :commit if blob_full?
277
+ upload( block_to_upload, to_commit)
278
+ block_to_upload = nil # release reference to resource for GC
279
+ else
280
+ commit unless @uploaded_block_ids.empty?
281
+ end
282
+
283
+ if to_commit
284
+ to_commit = nil
285
+ @uploaded_block_ids = [ ]
286
+ @timer.cancel
287
+ break if :close == block_to_upload
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ end
294
+
295
+ end
296
+
297
+ # close blob. It will finish whatever was already on the queue, and if necessary commit
298
+ # called on shutdown
299
+ def close
300
+ @io_queue << :close
301
+ end
302
+
303
+ def blob_full? ( next_block = nil )
304
+ if next_block
305
+ BLOB_MAX_BLOCKS < @uploaded_block_ids.length + 1 || @@blob_max_events < @uploaded_events_count + next_block.events_count || @@blob_max_bytesize < @uploaded_bytesize + next_block.bytesize
306
+ else
307
+ BLOB_MAX_BLOCKS <= @uploaded_block_ids.length || @@blob_max_events <= @uploaded_events_count || @@blob_max_bytesize <= @uploaded_bytesize
308
+ end
309
+ end
310
+
311
+ def blob_empty?
312
+ @uploaded_block_ids.empty?
313
+ end
314
+
315
+ def queue_empty?
316
+ @io_queue.length == 0 if @io_queue
317
+ end
318
+
319
+
320
+ def queue_size
321
+ @io_queue.length
322
+ end
323
+
324
+ def clear_state
325
+ @action = nil
326
+ @storage_account_name = nil
327
+ @container_name = nil
328
+ @blob_name = nil
329
+ @uploaded_block_ids = [ ]
330
+ @uploaded_block_numbers = [ ]
331
+ @uploaded_events_count = 0
332
+ @uploaded_bytesize = 0
333
+ @oldest_event_time = nil
334
+ end
335
+
336
+ def table_entity_to_tuple( options = {} )
337
+ [ options[:start_time.to_s] || Time.now.utc, options[:action.to_s], options[:intrumentation_key.to_s], options[:table_id.to_s],
338
+ options[:storage_account_name.to_s], options[:container_name.to_s], options[:blob_name.to_s],
339
+ eval( options[:uploaded_block_ids.to_s] ), eval( options[:uploaded_block_numbers.to_s] ),
340
+ options[:uploaded_events_count.to_s] || 0, options[:uploaded_bytesize.to_s] || 0, options[:oldest_event_time.to_s] || Time.now.utc,
341
+ options[:event_format_ext.to_s], options[:blob_max_delay.to_s] || 0,
342
+ options[:log_state.to_s].to_sym, (options[:sub_state.to_s] || :none).to_sym
343
+ ]
344
+ end
345
+
346
+ def state_to_tuple
347
+ [ @start_time || Time.now.utc, @action, @intrumentation_key, @table_id,
348
+ @storage_account_name, @container_name, @blob_name,
349
+ @uploaded_block_ids, @uploaded_block_numbers,
350
+ @uploaded_events_count, @uploaded_bytesize, @oldest_event_time,
351
+ @event_format_ext, @blob_max_delay,
352
+ @log_state, @sub_state
353
+ ]
354
+ end
355
+
356
+ def tuple_to_state ( tuple )
357
+ ( @start_time, @action, @intrumentation_key, @table_id,
358
+ @storage_account_name, @container_name, @blob_name,
359
+ @uploaded_block_ids, @uploaded_block_numbers,
360
+ @uploaded_events_count, @uploaded_bytesize, @oldest_event_time,
361
+ @event_format_ext, @blob_max_delay,
362
+ @log_state, @sub_state) = tuple
363
+ end
364
+
365
+ def state_to_table_entity
366
+ { :start_time => @start_time, :intrumentation_key => @intrumentation_key, :table_id => @table_id,
367
+ :storage_account_name => @storage_account_name, :container_name => @container_name, :blob_name => @blob_name,
368
+ :uploaded_block_ids => @uploaded_block_ids.to_s, :uploaded_block_numbers => @uploaded_block_numbers.to_s,
369
+ :uploaded_events_count => @uploaded_events_count, :uploaded_bytesize => @uploaded_bytesize, :oldest_event_time => @oldest_event_time,
370
+ :log_state => @log_state, :sub_state => @sub_state
371
+ }
372
+ end
373
+
374
+
375
+ def test_storage_recover
376
+ proc do |reason, e| @recovery = :ok if :container_exist == reason || :create_container == reason end
377
+ end
378
+
379
+
380
+ def test_storage ( storage_account_name )
381
+ @storage_account_name = storage_account_name
382
+ @action = :test_storage
383
+ @max_tries = 1
384
+ @force_client = true # to enable get a client even if all storage_accounts marked dead
385
+ @recoverable = [ :invalid_storage_key ]
386
+ storage_io_block( test_storage_recover ) {
387
+ if @recovery.nil? || :invalid_storage_key == @recovery
388
+ container_name = "logstash-test-container"
389
+ @info = "#{@action} #{@storage_account_name}"
390
+ @client.blobClient.create_container( container_name ) unless @@configuration[:disable_blob_upload]
391
+ end
392
+ }
393
+ end
394
+
395
+ def test_notification_endpoint_recover
396
+ proc do |reason, e| @recovery = :ok if :invalid_intrumentation_key == reason || :invalid_table_id == reason end
397
+ end
398
+
399
+ def test_notification_endpoint( storage_account_name )
400
+ @storage_account_name = storage_account_name
401
+ @action = :test_notification_endpoint
402
+ @max_tries = 1
403
+ @force_client = true # to enable get a client even if all storage_accounts marked dead
404
+ @recoverable = [ ]
405
+ success = storage_io_block( test_notification_endpoint_recover ) {
406
+ if @recovery.nil?
407
+ @container_name = "logstash-test-container"
408
+ @blob_name = "logstash-test-blob"
409
+ @table_id = GUID_NULL
410
+ @intrumentation_key = GUID_NULL
411
+ @info = "#{@action}"
412
+ set_blob_sas_url
413
+ payload = create_payload
414
+ post_notification( @client.notifyClient, payload )
415
+ end
416
+ }
417
+ sleep( 30 ) unless success
418
+ success
419
+ end
420
+
421
+
422
+ def notify_recover
423
+ proc do |reason, e|
424
+ if :notify_failed_blob_not_accessible == reason
425
+ @sub_state = reason
426
+ @@failed_on_notify_retry_Qs[@storage_account_name] << state_to_tuple
427
+ elsif :invalid_intrumentation_key == reason || :invalid_table_id == reason
428
+ @sub_state = reason
429
+ Channels.instance.channel( @intrumentation_key, @table_id ).failed_on_notify_retry_Q << state_to_tuple
430
+
431
+ else
432
+ @@endpoint_state_on = false
433
+ if :notify_failed_blob_not_accessible == @sub_state
434
+ @@failed_on_notify_retry_Qs[@storage_account_name] << state_to_tuple
435
+ elsif :invalid_intrumentation_key == @sub_state || :invalid_table_id == @sub_state
436
+ Channels.instance.channel( @intrumentation_key, @table_id ).failed_on_notify_retry_Q << state_to_tuple
437
+ else
438
+ @@failed_on_notification_endpoint_retry_Q << state_to_tuple
439
+ end
440
+ end
441
+ end
442
+ end
443
+
444
+ def notify ( tuple = nil )
445
+ tuple_to_state( tuple ) if tuple
446
+ @action = :notify
447
+ @force_client = true # to enable get a client even if all storage_accounts marked dead
448
+ @recoverable = [ :notify_failed_blob_not_accessible, :io_failure, :service_unavailable ]
449
+ success = storage_io_block( notify_recover ) {
450
+ set_blob_sas_url
451
+ payload = create_payload
452
+ @@logger.debug { "notification payload: #{payload}" }
453
+ @info = "#{@action.to_s} #{@storage_account_name}/#{@container_name}/#{@blob_name}, events: #{@uploaded_events_count}, size: #{@uploaded_bytesize}, blocks: #{@uploaded_block_numbers}, delay: #{Time.now.utc - @oldest_event_time}, blob_sas_url: #{@blob_sas_url}"
454
+
455
+ # assume that exceptions can be raised due to this method:
456
+ post_notification( @client.notifyClient, payload ) unless @@configuration[:disable_notification]
457
+ @log_state = :notified
458
+ }
459
+ log_to_table_update if success
460
+ end
461
+
462
+ CREATE_EXIST_ERRORS = { :container => [ :create_container, :container_exist ], :table => [ :create_table, :table_exist ] }
463
+ def create_exist_recovery( type, name = nil )
464
+ prev_info = @info
465
+ if CREATE_EXIST_ERRORS[type][0] == @recovery
466
+ name ||= ( :table == type ? @@state_table_name : @container_name )
467
+ @info = "create #{type} #{@storage_account_name}/#{name}"
468
+
469
+ # assume that exceptions can be raised due to this method:
470
+ yield name
471
+ @@logger.info { "Successed to #{@info}" }
472
+ @info = prev_info
473
+ elsif CREATE_EXIST_ERRORS[type][1] == @recovery
474
+ @@logger.info { "Successed (already exist) to #{@info}" }
475
+ @info = prev_info
476
+ end
477
+ end
478
+
479
+ def create_table_exist_recovery
480
+ create_exist_recovery( :table ) { |name| @client.tableClient.create_table( name ) }
481
+ end
482
+
483
+ def create_container_exist_recovery
484
+ create_exist_recovery( :container ) { |name| @client.blobClient.create_container( name ) }
485
+ end
486
+
487
+ # return true on success
488
+ def log_to_table_insert
489
+ @action = :log_to_table_insert
490
+ @recoverable = [ :invalid_storage_key, :io_failure, :service_unavailable, :table_exist, :create_table, :table_busy, :entity_exist ]
491
+ @info = "#{@action} #{@log_state} #{@storage_account_name}/#{@container_name}/#{@blob_name}"
492
+ success = storage_io_block( :uploading == @log_state ? proc do |reason, e| end : log_to_table_update_recover ) {
493
+ create_table_exist_recovery
494
+ if :entity_exist == @recovery
495
+ raise NotRecoverableError if :uploading == @log_state
496
+ else
497
+ entity_values = state_to_table_entity
498
+ entity_values[:PartitionKey] = "#{@@configuration[:azure_storage_blob_prefix]}-#{@log_state}"
499
+ entity_values[:RowKey] = @blob_name
500
+ @client.tableClient.insert_entity( @@state_table_name, entity_values )
501
+ end
502
+ }
503
+ end
504
+
505
+ def log_to_table_update_recover
506
+ proc do |reason, e| @@failed_on_log_to_table_retry_Qs[@storage_account_name] << state_to_tuple end
507
+ end
508
+
509
+ def log_to_table_update ( tuple = nil )
510
+ tuple_to_state( tuple ) if tuple
511
+ if :uploading == @log_state
512
+ log_to_table_delete
513
+ elsif :committed == @log_state
514
+ if log_to_table_insert && log_to_table_delete( nil, :uploading )
515
+ State.instance.dec_pending_commits
516
+ State.instance.inc_pending_notifications
517
+ @@failed_on_notification_endpoint_retry_Q << state_to_tuple
518
+ end
519
+ elsif :notified == @log_state
520
+ if (!@@save_notified_blobs_records || log_to_table_insert) && log_to_table_delete( nil, :committed )
521
+ State.instance.dec_pending_notifications
522
+ end
523
+ end
524
+ end
525
+
526
+
527
+ # retturn tru on success
528
+ def log_to_table_delete ( tuple = nil, state = nil )
529
+ tuple_to_state( tuple ) if tuple
530
+ state ||= @log_state
531
+ @action = :log_to_table_delete
532
+ @recoverable = [ :invalid_storage_key, :io_failure, :service_unavailable, :table_exist, :create_table, :table_busy, :create_resource ]
533
+ @info = "#{@action} #{state} #{@storage_account_name}/#{@container_name}/#{@blob_name}"
534
+
535
+ success = storage_io_block( log_to_table_update_recover ) {
536
+ create_table_exist_recovery
537
+ if :create_resource == @recovery
538
+ @@logger.info { "Note: delete entity failed, already deleted, #{@info}, state: #{state}, log_state: #{@log_state}" }
539
+ else
540
+ @client.tableClient.delete_entity( @@state_table_name, "#{@@configuration[:azure_storage_blob_prefix]}-#{state}", @blob_name )
541
+ end
542
+ }
543
+ end
544
+
545
+ # return entities
546
+ def log_to_table_query ( storage_account_name, filter , token )
547
+ @storage_account_name = storage_account_name
548
+
549
+ @action = :log_to_table_query
550
+ @recoverable = [ :invalid_storage_key, :io_failure, :service_unavailable, :table_exist, :create_table, :table_busy ]
551
+ @info = "#{@action} #{@storage_account_name}/#{@@state_table_name}"
552
+
553
+ entities = nil
554
+ success = storage_io_block( proc do |reason, e| end ) {
555
+ create_table_exist_recovery
556
+ options = { :filter => filter }
557
+ options[:continuation_token] = token if token
558
+ entities = @client.tableClient.query_entities( @@state_table_name, options )
559
+ }
560
+ entities
561
+ end
562
+
563
+ def commit_recover
564
+ proc do |reason, e| @@failed_on_commit_retry_Qs[@storage_account_name] << state_to_tuple end
565
+ end
566
+
567
+ def commit ( tuple = nil )
568
+ tuple_to_state( tuple ) if tuple
569
+
570
+ unless @uploaded_block_ids.empty?
571
+ @action = :commit
572
+ @recoverable = [ :invalid_storage_key, :io_failure, :service_unavailable ]
573
+ success = storage_io_block( commit_recover ) {
574
+ @info = "#{@action.to_s} #{@storage_account_name}/#{@container_name}/#{@blob_name}, events: #{@uploaded_events_count}, size: #{@uploaded_bytesize}, blocks: #{@uploaded_block_numbers}, delay: #{Time.now.utc - @oldest_event_time}"
575
+ # assume that exceptions can be raised due to this method:
576
+ @client.blobClient.commit_blob_blocks( @container_name, @blob_name, @uploaded_block_ids ) unless @@configuration[:disable_blob_upload]
577
+ @log_state = :committed
578
+ }
579
+ # next stage
580
+ log_to_table_update if success
581
+ end
582
+ end
583
+
584
+
585
+ def upload_recover
586
+ proc do |reason, e|
587
+ unless @uploaded_block_ids.empty?
588
+ info1 = "#{:commit} #{@storage_account_name}/#{@container_name}/#{@blob_name}, events: #{@uploaded_events_count}, size: #{@uploaded_bytesize}, blocks: #{@uploaded_block_numbers}, delay: #{Time.now.utc - @oldest_event_time}"
589
+ @@logger.error { "Pospone to #{info1} (; retry later, error: #{e.inspect}" }
590
+ @@failed_on_commit_retry_Qs[@storage_account_name] << state_to_tuple
591
+ @uploaded_block_ids = [ ]
592
+ end
593
+ unless :io_all_dead == reason
594
+ @recovery = :invalid_storage_account
595
+ else
596
+ Channels.instance.channel( @intrumentation_key, @table_id ).failed_on_upload_retry_Q << @block_to_upload
597
+ @block_to_upload = nil
598
+ end
599
+ end
600
+ end
601
+
602
+ def upload ( block, to_commit = nil )
603
+ @storage_account_name = nil if @uploaded_block_ids.empty?
604
+ @block_to_upload = block
605
+ block = nil # remove reference for GC
606
+ exclude_storage_account_names = [ ]
607
+ begin
608
+ if @uploaded_block_ids.empty?
609
+ @log_state = :uploading
610
+ @uploaded_block_numbers = [ ]
611
+ @uploaded_bytesize = 0
612
+ @uploaded_events_count = 0
613
+ @oldest_event_time = nil
614
+
615
+ # remove record of previous upload that failed
616
+ if @storage_account_name
617
+ exclude_storage_account_names << @storage_account_name
618
+ @@failed_on_log_to_table_retry_Qs[@storage_account_name] << state_to_tuple
619
+ end
620
+ set_conatainer_and_blob_names
621
+ @storage_account_name = Clients.instance.get_random_active_storage( exclude_storage_account_names )
622
+ unless @storage_account_name
623
+ upload_recover.call( :io_all_dead, nil )
624
+ return false
625
+ end
626
+ raise UploadRetryError unless log_to_table_insert
627
+ end
628
+
629
+ @action = :upload
630
+ @block_info = "blocks: #{@block_to_upload.block_numbers}, events: #{@block_to_upload.events_count}, size: #{@block_to_upload.bytes.length}"
631
+ @info = "#{@action} #{@storage_account_name}/#{@container_name}/#{@blob_name}, #{@block_info}, commitId: [\"#{100001 + @uploaded_block_ids.length}\"]"
632
+ @recoverable = [ :invalid_storage_key, :invalid_storage_account, :io_failure, :service_unavailable, :container_exist, :create_container ]
633
+
634
+ success = storage_io_block( upload_recover ) {
635
+ create_container_exist_recovery
636
+ block_id = "#{100001 + @uploaded_block_ids.length}"
637
+
638
+ # assume that exceptions can be raised due to this method:
639
+ @client.blobClient.put_blob_block( @container_name, @blob_name, block_id, @block_to_upload.bytes ) unless @@configuration[:disable_blob_upload]
640
+
641
+ # upload success
642
+ first_block_in_blob = @uploaded_block_ids.empty?
643
+ @uploaded_block_ids << [ block_id ]
644
+ @uploaded_block_numbers.concat( @block_to_upload.block_numbers )
645
+ @uploaded_bytesize += @block_to_upload.bytes.length
646
+ @uploaded_events_count += @block_to_upload.events_count
647
+ @oldest_event_time ||= @block_to_upload.oldest_event_time
648
+
649
+ # release memory
650
+ bytesize = @block_to_upload.bytesize
651
+ @block_to_upload.dispose
652
+ @block_to_upload = nil
653
+ State.instance.inc_pending_commits if first_block_in_blob
654
+ State.instance.dec_upload_bytesize( bytesize )
655
+ Telemetry.instance.track_event("uploading", {:properties => state_to_table_entity})
656
+ }
657
+
658
+ raise UploadRetryError if :invalid_storage_account == @recovery
659
+ commit if success && to_commit
660
+ rescue UploadRetryError
661
+ @recovery = nil
662
+ retry
663
+ end
664
+ end
665
+
666
+ def update_commited_or_uncommited_list( table_entity )
667
+ tuple = table_entity_to_tuple( table_entity )
668
+
669
+ tuple_to_state( tuple )
670
+ @action = :list_blob_blocks
671
+ @recoverable = [ :invalid_storage_key, :io_failure, :service_unavailable, :container_exist, :create_container, :create_blob ]
672
+ list_blob_blocks = nil
673
+ success = storage_io_block( proc do |reason, e| end ) {
674
+ @info = "#{@action} #{@storage_account_name}/#{@container_name}/#{@blob_name}"
675
+
676
+ create_container_exist_recovery
677
+ if :create_blob == @recovery
678
+ list_blob_blocks = { :uncommitted => [ ], :committed => [ ] }
679
+ else
680
+ list_blob_blocks = @client.blobClient.list_blob_blocks( @container_name, @blob_name, { :blocklist_type => :all } ) unless :create_blob == @recovery
681
+ end
682
+ }
683
+
684
+ if list_blob_blocks
685
+ blocks = ( list_blob_blocks[:uncommitted].empty? ? list_blob_blocks[:committed] : list_blob_blocks[:uncommitted] )
686
+ blocks.each do |block|
687
+ @uploaded_block_ids << [ block.name ]
688
+ @uploaded_bytesize += block.size
689
+ end
690
+ type = ( blocks.empty? || 0 == @uploaded_bytesize ? :upload_empty : blocks[0].type )
691
+
692
+ @log_state = :committed if :committed == type
693
+ { type => state_to_tuple }
694
+ else
695
+ nil
696
+ end
697
+ end
698
+
699
+
700
+ def << ( block )
701
+ @io_queue << block
702
+ end
703
+
704
+
705
+ private
706
+
707
+
708
+ def storage_io_block( recover_later_proc, valid_recovery = nil )
709
+ @recovery = nil
710
+ @try_count = 1
711
+
712
+ begin
713
+ @client ||= Client.new( @storage_account_name, @force_client )
714
+ yield
715
+ disabled = :notify == @action ? @@configuration[:disable_notification] : @@configuration[:disable_blob_upload]
716
+ @@logger.info { "Successed to #{disabled ? 'DISABLED ' : ''}#{@info}" }
717
+ true
718
+
719
+ rescue TypeError
720
+ raise
721
+
722
+ rescue StandardError => e
723
+ @last_io_exception = e
724
+ @recovery = nil
725
+ retry if recover_retry?( e, recover_later_proc )
726
+ false
727
+
728
+ ensure
729
+ @client = @client.dispose if @client
730
+ end
731
+ end
732
+
733
+
734
+ def recover_retry? ( e, recover_later_proc )
735
+ # http error, probably server error
736
+ if e.is_a?( Azure::Core::Http::HTTPError )
737
+
738
+ if 404 == e.status_code && "ContainerNotFound" == e.type
739
+ @recovery = :create_container
740
+
741
+ elsif 404 == e.status_code && "TableNotFound" == e.type
742
+ @recovery = :create_table
743
+
744
+ elsif 404 == e.status_code && "BlobNotFound" == e.type
745
+ @recovery = :create_blob
746
+
747
+ elsif 404 == e.status_code && "ResourceNotFound" == e.type
748
+ @recovery = :create_resource
749
+
750
+ elsif 409 == e.status_code && "ContainerAlreadyExists" == e.type
751
+ @recovery = :container_exist
752
+
753
+ elsif 409 == e.status_code && "BlobAlreadyExists" == e.type
754
+ @recovery = :blob_exist
755
+
756
+ elsif 409 == e.status_code && "TableAlreadyExists" == e.type
757
+ @recovery = :table_exist
758
+
759
+ elsif 409 == e.status_code && "TableBeingDeleted" == e.type
760
+ @recovery = :table_busy
761
+
762
+ elsif 409 == e.status_code && "EntityAlreadyExists" == e.type
763
+ @recovery = :entity_exist
764
+
765
+ elsif 403 == e.status_code && "AuthenticationFailed" == e.type
766
+ @recovery = :invalid_storage_key
767
+
768
+ elsif 403 == e.status_code && "Unknown" == e.type && e.description.include?("Blob does not exist or not accessible.")
769
+ @recovery = :notify_failed_blob_not_accessible
770
+
771
+ elsif 400 == e.status_code && "Unknown" == e.type && e.description.include?("Invalid instrumentation key")
772
+ @recovery = :invalid_intrumentation_key
773
+
774
+ elsif 500 == e.status_code && "Unknown" == e.type && e.description.include?("Processing error")
775
+ @recovery = :notification_process_down
776
+
777
+ elsif 503 == e.status_code
778
+ @recovery = :service_unavailable
779
+ elsif 404 == e.status_code
780
+ @recovery = :create_resource
781
+ elsif 403 == e.status_code
782
+ # todo, came from updating the log_table, how to hnadle this
783
+ @recovery = :access_denied
784
+ else
785
+ puts "\n>>>> HTTP error - #{e.inspect} <<<<\n"
786
+ @recovery = :http_unknown
787
+ raise e if @@configuration[:stop_on_unknown_io_errors]
788
+ end
789
+
790
+ # communication error
791
+ elsif e.is_a?( Faraday::ClientError )
792
+ @recovery = :io_failure
793
+
794
+ # communication error
795
+ elsif e.is_a?( IOError )
796
+ @recovery = :io_failure
797
+
798
+ # all storage accounts are dead, couldn't get client (internal exception)
799
+ elsif e.is_a?( StorageAccountsOffError )
800
+ @recovery = :io_all_dead
801
+
802
+ # all storage accounts are dead, couldn't get client (internal exception)
803
+ elsif e.is_a?( NotRecoverableError )
804
+ @recovery = :not_recoverable
805
+
806
+ elsif e.is_a?( NameError ) && e.message.include?( "uninitialized constant Azure::Core::Auth::Signer::OpenSSL" )
807
+ sleep( 1 )
808
+ @recovery = :io_failure
809
+
810
+ elsif e.is_a?( NameError ) && e.message.include?( "uninitialized constant Azure::Storage::Auth::SharedAccessSignature" )
811
+ sleep( 1 )
812
+ @recovery = :io_failure
813
+
814
+ else
815
+ # UNKNOWN error - #<NameError: uninitialized constant Azure::Core::Auth::Signer::OpenSSL>
816
+ puts "\n>>>> UNKNOWN error - #{e.inspect} <<<<\n"
817
+ raise e
818
+
819
+ end
820
+
821
+ reason = @recovery
822
+ if @recovery && @recoverable.include?( @recovery )
823
+ case @recovery
824
+ when :container_exist, :table_exist, :entity_exist, :create_container, :create_table
825
+ # ignore log error
826
+ # @@logger.error { "Failed to #{@info} ;( recovery: continue, error: #{e.inspect}" }
827
+
828
+ when :invalid_storage_key, :notify_failed_blob_not_accessible
829
+ if @client.switch_storage_account_key!
830
+ @@logger.error { "Failed to #{@info} ;( recovery: switched to secondary storage key, error: #{e.inspect}" }
831
+ else
832
+ @client = @client.dispose( :auth_to_storage_failed ) if @client && :invalid_storage_key == @recovery
833
+ @recovery = nil
834
+ end
835
+
836
+ when :table_busy
837
+ @client = @client.dispose if @client
838
+ sleep( @@io_retry_delay )
839
+ @@logger.error { "Failed to #{@info} ;( recovery: retry, error: #{e.inspect}" }
840
+
841
+ when :io_failure, :service_unavailable, :notification_process_down, :invalid_intrumentation_key, :invalid_table_id
842
+ if @try_count < @max_tries
843
+ @client = @client.dispose if @client
844
+ sleep( @@io_retry_delay )
845
+ @@logger.error { "Failed to #{@info} ;( recovery: retry, try #{@try_count} / #{@max_tries}, error: #{e.inspect}" }
846
+ @try_count += 1
847
+ else
848
+ if :invalid_intrumentation_key == @recovery
849
+ Channels.instance.mark_invalid_intrumentation_key( @intrumentation_key )
850
+ elsif :invalid_table_id == @recovery
851
+ Channels.instance.mark_invalid_table_id( @table_id )
852
+ elsif :io_failure == @recovery || ( :service_unavailable == @recovery && :notify != @action )
853
+ @client = @client.dispose( :io_to_storage_failed ) if @client
854
+ end
855
+ @recovery = nil
856
+ end
857
+ end
858
+ else
859
+ @recovery = nil
860
+ end
861
+
862
+ if @recovery
863
+ true
864
+ else
865
+ recover_later_proc.call( reason, e )
866
+ @@logger.error { "Failed to #{@info} ; retry later, error: #{e.inspect}" } unless :ok == @recovery
867
+ :ok == @recovery
868
+ end
869
+
870
+ # Blob service error codes - msdn.microsoft.com/en-us/library/azure/dd179439.aspx
871
+ # ConnectionFailed - problem with connection
872
+ # ParsingError - problem with request/response payload
873
+ # ResourceNotFound, SSLError, TimeoutError
874
+ end
875
+
876
+ def set_conatainer_and_blob_names
877
+ time_utc = Time.now.utc
878
+ id = @id.to_s.rjust(4, "0")
879
+ strtime = time_utc.strftime( "%F" )
880
+ @container_name = "#{@@configuration[:azure_storage_container_prefix]}-#{strtime}"
881
+
882
+ strtime = time_utc.strftime( "%F-%H-%M-%S-%L" )
883
+ @blob_name = "#{@@configuration[:azure_storage_blob_prefix]}_ikey-#{@intrumentation_key}_table-#{@table_id}_id-#{id}_#{strtime}.#{@event_format_ext}"
884
+ end
885
+
886
+
887
+ def create_payload
888
+ notification_hash = {
889
+ :data => {
890
+ :baseType => DATA_BASE_TYPE,
891
+ :baseData => {
892
+ :ver => BASE_DATA_REQUIRED_VERSION,
893
+ :blobSasUri => @blob_sas_url.to_s,
894
+ :sourceName => @table_id,
895
+ :sourceVersion => @@configuration[:notification_version].to_s
896
+ }
897
+ },
898
+ :ver => @@configuration[:notification_version],
899
+ :name => REQUEST_NAME,
900
+ :time => Time.now.utc.iso8601,
901
+ :iKey => @intrumentation_key
902
+ }
903
+ notification_hash.to_json
904
+ end
905
+
906
+
907
+ def post_notification ( http_client, body )
908
+ request = Azure::Core::Http::HttpRequest.new( :post, @@configuration[:notification_endpoint], { :body => body, :client => http_client } )
909
+ request.headers['Content-Type'] = 'application/json; charset=utf-8'
910
+ request.headers['Accept'] = 'application/json'
911
+ @@logger.debug { "send notification : \n endpoint: #{@@configuration[:notification_endpoint]}\n body : #{body}" }
912
+ response = request.call
913
+ end
914
+
915
+
916
+ def set_blob_sas_url
917
+ blob_url ="https://#{@storage_account_name}.blob.core.windows.net/#{@container_name}/#{@blob_name}"
918
+ options_and_constrains = {:permissions => "r", :resource => "b", :expiry => ( Time.now.utc + @@configuration[:blob_access_expiry_time] ).iso8601 }
919
+ @blob_sas_url = @client.storage_auth_sas.signed_uri( URI( blob_url ), options_and_constrains )
920
+ end
921
+
922
+ end
923
+ end