logstash-input-azure_blob_storage 0.11.1 → 0.11.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fb68f13f46e7a0455fe4ffd3f6c9e04b136611e01504310bd739bbc6813c6f6
4
- data.tar.gz: 3f818813b0b45acac96edb34a4948d01c234946fb2580eefe5ece8e43240c0c1
3
+ metadata.gz: ececd96b04d2cab60eca54a0fe2a98c9ed093da2227e3568d4feea09264912fa
4
+ data.tar.gz: 7bcd39bc38d26a05da1275e5fb2317e41b5c2cddc6541535c7d166a69bb3cf62
5
5
  SHA512:
6
- metadata.gz: b596bbfc6a1e3400c33e54bbfa4adb753ea1c6593ae647da221368a089b25cd650856d4abb78c5f39ae39df67387b3de938962d63a90e06d2b54164599ced0a9
7
- data.tar.gz: 6c0eb3959fa0f393f63c0697f26d49b01280604e2443b8c0a17342d768f4c1a9402e4c8f658462cdd1d76cc11fa7a654bb36052ff05fdbec775050ad33539a1c
6
+ metadata.gz: 1bcbfab30de973e9eafee295221dc816411dca0e0f747a01c62bb48ec5c46eaf4db4162fdd5283611cd79da59910daab9e7c6e234df47f5ce7f320e65f7b8c69
7
+ data.tar.gz: 7bbdab8694d024b9c08cc89e13bc86aa8b90a536f5615565333593e0da7c3073d7c4cf3ad3f2b4005a90541de9693a93826158a18fbf9015234bee1812b3d46c
data/CHANGELOG.md CHANGED
@@ -1,29 +1,63 @@
1
+ ## 0.11.6
2
+ - fix in json head and tail learning the max_results
3
+ - broke out connection setup in order to call it again if connection exceptions come
4
+ - deal better with skipping of empty files.
5
+
6
+ ## 0.11.5
7
+ - added optional addfilename to add filename in message
8
+ - NSGFLOWLOG version 2 uses 0 as value instead of NULL in src and dst values
9
+ - added connection exception handling when full_read files
10
+ - rewritten json header footer learning to ignore learning from registry
11
+ - plumbing for emulator
12
+
13
+ ## 0.11.4
14
+ - fixed listing 3 times, rather than retrying to list max 3 times
15
+ - added option to migrate/save to using local registry
16
+ - rewrote interval timing
17
+ - reduced saving of registry to maximum once per interval, protect duplicate simultanious writes
18
+ - added debug_timer for better tracing how long operations take
19
+ - removing pipeline name from logfiles, logstash 7.6 and up have this in the log4j2 by default now
20
+ - moved initialization from register to run. should make logs more readable
21
+
22
+ ## 0.11.3
23
+ - don't crash on failed codec, e.g. gzip_lines could sometimes have a corrupted file?
24
+ - fix nextmarker loop so that more than 5000 files (or 15000 if faraday doesn't crash)
25
+
26
+ ## 0.11.2
27
+ - implemented path_filters to to use path filtering like this **/*.log
28
+ - implemented debug_until to debug only at the start of a pipeline until it processed enough messages
29
+
30
+ ## 0.11.1
31
+ - copied changes from irnc fork (danke!)
32
+ - fixed trying to load the registry, three time is the charm
33
+ - logs are less chatty, changed info to debug
34
+
1
35
  ## 0.11.0
2
- - Implemented start_fresh to skip all previous logs and start monitoring new entries
3
- - Fixed the timer, now properly sleep the interval and check again
4
- - Work around for a Faraday Middleware v.s. Azure Storage Account bug in follow_redirect
36
+ - implemented start_fresh to skip all previous logs and start monitoring new entries
37
+ - fixed the timer, now properly sleep the interval and check again
38
+ - work around for a Faraday Middleware v.s. Azure Storage Account bug in follow_redirect
5
39
 
6
40
  ## 0.10.6
7
- - Fixed the rootcause of the checking the codec. Now compare the classname.
41
+ - fixed the rootcause of the checking the codec. Now compare the classname.
8
42
 
9
43
  ## 0.10.5
10
- - Previous fix broke codec = "line"
44
+ - previous fix broke codec = "line"
11
45
 
12
46
  ## 0.10.4
13
- - Fixed JSON parsing error for partial files because somehow (logstash 7?) @codec.is_a? doesn't work anymore
47
+ - fixed JSON parsing error for partial files because somehow (logstash 7?) @codec.is_a? doesn't work anymore
14
48
 
15
49
  ## 0.10.3
16
- - Fixed issue-1 where iplookup confguration was removed, but still used
50
+ - fixed issue-1 where iplookup confguration was removed, but still used
17
51
  - iplookup is now done by a separate plugin named logstash-filter-weblookup
18
52
 
19
53
  ## 0.10.2
20
54
  - moved iplookup to own plugin logstash-filter-lookup
21
55
 
22
56
  ## 0.10.1
23
- - Implemented iplookup
24
- - Fixed sas tokens (maybe)
25
- - Introduced dns_suffix
57
+ - implemented iplookup
58
+ - fixed sas tokens (maybe)
59
+ - introduced dns_suffix
26
60
 
27
61
  ## 0.10.0
28
- - Plugin created with the logstash plugin generator
29
- - Reimplemented logstash-input-azureblob with incompatible config and data/registry
62
+ - plugin created with the logstash plugin generator
63
+ - reimplemented logstash-input-azureblob with incompatible config and data/registry
data/README.md CHANGED
@@ -1,29 +1,81 @@
1
- # Logstash Plugin
1
+ # Logstash
2
2
 
3
- This is a plugin for [Logstash](https://github.com/elastic/logstash).
3
+ This is a plugin for [Logstash](https://github.com/elastic/logstash). It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way. All logstash plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/). Need generic logstash help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
4
4
 
5
- It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
6
-
7
- ## Documentation
8
-
9
- All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
10
-
11
- ## Need Help?
12
-
13
- Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum. For real problems or feature requests, raise a github issue [GITHUB/janmg/logstash-input-azure_blob_storage/](https://github.com/janmg/logstash-input-azure_blob_storage). Pull requests will ionly be merged after discussion through an issue.
5
+ For problems or feature requests with this specific plugin, raise a github issue [GITHUB/janmg/logstash-input-azure_blob_storage/](https://github.com/janmg/logstash-input-azure_blob_storage). Pull requests will also be welcomed after discussion through an issue.
14
6
 
15
7
  ## Purpose
16
- This plugin can read from Azure Storage Blobs, for instance diagnostics logs for NSG flow logs or accesslogs from App Services.
8
+ This plugin can read from Azure Storage Blobs, for instance JSON diagnostics logs for NSG flow logs or LINE based accesslogs from App Services.
17
9
  [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/)
18
10
 
19
- After every interval it will write a registry to the storageaccount to save the information of how many bytes per blob (file) are read and processed. After all files are processed and at least one interval has passed a new file list is generated and a worklist is constructed that will be processed. When a file has already been processed before, partial files are read from the offset to the filesize at the time of the file listing. If the codec is JSON partial files will be have the header and tail will be added. They can be configured. If logtype is nsgflowlog, the plugin will process the splitting into individual tuple events. The logtype wadiis may in the future be used to process the grok formats to split into log lines. Any other format is fed into the queue as one event per file or partial file. It's then up to the filter to split and mutate the file format. use source => message in the filter {} block.
11
+ The plugin depends on the [Ruby library azure-storage-blon](https://rubygems.org/gems/azure-storage-blob/versions/1.1.0) from Microsoft, that depends on Faraday for the HTTPS connection to Azure.
12
+
13
+ The plugin executes the following steps
14
+ 1. Lists all the files in the azure storage account. where the path of the files are matching pathprefix
15
+ 2. Filters on path_filters to only include files that match the directory and file glob (e.g. **/*.json)
16
+ 3. Save the listed files in a registry of known files and filesizes. (data/registry.dat on azure, or in a file on the logstash instance)
17
+ 4. List all the files again and compare the registry with the new filelist and put the delta in a worklist
18
+ 5. Process the worklist and put all events in the logstash queue.
19
+ 6. if there is time left, sleep to complete the interval. If processing takes more than an inteval, save the registry and continue processing.
20
+ 7. If logstash is stopped, a stop signal will try to finish the current file, save the registry and than quit
20
21
 
21
22
  ## Installation
22
23
  This plugin can be installed through logstash-plugin
23
24
  ```
24
- logstash-plugin install logstash-input-azure_blob_storage
25
+ /usr/share/logstash/bin/logstash-plugin install logstash-input-azure_blob_storage
26
+ ```
27
+
28
+ ## Minimal Configuration
29
+ The minimum configuration required as input is storageaccount, access_key and container.
30
+
31
+ /etc/logstash/conf.d/test.conf
32
+ ```
33
+ input {
34
+ azure_blob_storage {
35
+ storageaccount => "yourstorageaccountname"
36
+ access_key => "Ba5e64c0d3=="
37
+ container => "insights-logs-networksecuritygroupflowevent"
38
+ }
39
+ }
25
40
  ```
26
41
 
42
+ ## Additional Configuration
43
+ The registry keeps track of files in the storage account, their size and how many bytes have been processed. Files can grow and the added part will be processed as a partial file. The registry is saved todisk every interval.
44
+
45
+ The registry_create_policy determines at the start of the pipeline if processing should resume from the last known unprocessed file, or to start_fresh ignoring old files and start only processing new events that came after the start of the pipeline. Or start_over to process all the files ignoring the registry.
46
+
47
+ interval defines the minimum time the registry should be saved to the registry file (by default to 'data/registry.dat'), this is only needed in case the pipeline dies unexpectedly. During a normal shutdown the registry is also saved.
48
+
49
+ When registry_local_path is set to a directory, the registry is saved on the logstash server in that directory. The filename is the pipe.id
50
+
51
+ with registry_create_policy set to resume and the registry_local_path set to a directory where the registry isn't yet created, should load the registry from the storage account and save the registry on the local server. This allows for a migration to localstorage
52
+
53
+ For pipelines that use the JSON codec or the JSON_LINE codec, the plugin uses one file to learn how the JSON header and tail look like, they can also be configured manually. Using skip_learning the learning can be disabled.
54
+
55
+ ## Running the pipeline
56
+ The pipeline can be started in several ways.
57
+ - On the commandline
58
+ ```
59
+ /usr/share/logstash/bin/logtash -f /etc/logstash/conf.d/test.conf
60
+ ```
61
+ - In the pipeline.yml
62
+ ```
63
+ /etc/logstash/pipeline.yml
64
+ pipe.id = test
65
+ pipe.path = /etc/logstash/conf.d/test.conf
66
+ ```
67
+ - As managed pipeline from Kibana
68
+
69
+ Logstash itself (so not specific to this plugin) has a feature where multiple instances can run on the same system. The default TCP port is 9600, but if it's already in use it will use 9601 (and up). To update a config file on a running instance on the commandline you can add the argument --config.reload.automatic and if you modify the files that are in the pipeline.yml you can send a SIGHUP channel to reload the pipelines where the config was changed.
70
+ [https://www.elastic.co/guide/en/logstash/current/reloading-config.html](https://www.elastic.co/guide/en/logstash/current/reloading-config.html)
71
+
72
+ ## Internal Working
73
+ When the plugin is started, it will read all the filenames and sizes in the blob store excluding the directies of files that are excluded by the "path_filters". After every interval it will write a registry to the storageaccount to save the information of how many bytes per blob (file) are read and processed. After all files are processed and at least one interval has passed a new file list is generated and a worklist is constructed that will be processed. When a file has already been processed before, partial files are read from the offset to the filesize at the time of the file listing. If the codec is JSON partial files will be have the header and tail will be added. They can be configured. If logtype is nsgflowlog, the plugin will process the splitting into individual tuple events. The logtype wadiis may in the future be used to process the grok formats to split into log lines. Any other format is fed into the queue as one event per file or partial file. It's then up to the filter to split and mutate the file format.
74
+
75
+ By default the root of the json message is named "message" so you can modify the content in the filter block
76
+
77
+ The configurations and the rest of the code are in [https://github.com/janmg/logstash-input-azure_blob_storage/tree/master/lib/logstash/inputs](lib/logstash/inputs) [https://github.com/janmg/logstash-input-azure_blob_storage/blob/master/lib/logstash/inputs/azure_blob_storage.rb#L10](azure_blob_storage.rb)
78
+
27
79
  ## Enabling NSG Flowlogs
28
80
  1. Enable Network Watcher in your regions
29
81
  2. Create Storage account per region
@@ -39,7 +91,6 @@ logstash-plugin install logstash-input-azure_blob_storage
39
91
  - Access key (key1 or key2)
40
92
 
41
93
  ## Troubleshooting
42
-
43
94
  The default loglevel can be changed in global logstash.yml. On the info level, the plugin save offsets to the registry every interval and will log statistics of processed events (one ) plugin will print for each pipeline the first 6 characters of the ID, in DEBUG the yml log level debug shows details of number of events per (partial) files that are read.
44
95
  ```
45
96
  log.level
@@ -50,10 +101,11 @@ The log level of the plugin can be put into DEBUG through
50
101
  curl -XPUT 'localhost:9600/_node/logging?pretty' -H 'Content-Type: application/json' -d'{"logger.logstash.inputs.azureblobstorage" : "DEBUG"}'
51
102
  ```
52
103
 
104
+ Because logstash debug makes logstash very chatty, the option debug_until will for a number of processed events and stops debuging. One file can easily contain thousands of events. The debug_until is useful to monitor the start of the plugin and the processing of the first files.
53
105
 
54
- ## Configuration Examples
55
- The minimum configuration required as input is storageaccount, access_key and container.
106
+ debug_timer will show detailed information on how much time listing of files took and how long the plugin will sleep to fill the interval and the listing and processing starts again.
56
107
 
108
+ ## Other Configuration Examples
57
109
  For nsgflowlogs, a simple configuration looks like this
58
110
  ```
59
111
  input {
@@ -77,6 +129,10 @@ filter {
77
129
  }
78
130
  }
79
131
 
132
+ output {
133
+ stdout { }
134
+ }
135
+
80
136
  output {
81
137
  elasticsearch {
82
138
  hosts => "elasticsearch"
@@ -84,22 +140,35 @@ output {
84
140
  }
85
141
  }
86
142
  ```
87
-
88
- It's possible to specify the optional parameters to overwrite the defaults. The iplookup, use_redis and iplist parameters are used for additional information about the source and destination ip address. Redis can be used for caching the results and iplist is to configure an array of ip addresses.
143
+ A more elaborate input configuration example
89
144
  ```
90
145
  input {
91
146
  azure_blob_storage {
147
+ codec => "json"
92
148
  storageaccount => "yourstorageaccountname"
93
149
  access_key => "Ba5e64c0d3=="
94
150
  container => "insights-logs-networksecuritygroupflowevent"
95
- codec => "json"
96
151
  logtype => "nsgflowlog"
97
152
  prefix => "resourceId=/"
153
+ path_filters => ['**/*.json']
154
+ addfilename => true
98
155
  registry_create_policy => "resume"
156
+ registry_local_path => "/usr/share/logstash/plugin"
99
157
  interval => 300
158
+ debug_timer => true
159
+ debug_until => 100
160
+ }
161
+ }
162
+
163
+ output {
164
+ elasticsearch {
165
+ hosts => "elasticsearch"
166
+ index => "nsg-flow-logs-%{+xxxx.ww}"
100
167
  }
101
168
  }
102
169
  ```
170
+ The configuration documentation is in the first 100 lines of the code
171
+ [GITHUB/janmg/logstash-input-azure_blob_storage/blob/master/lib/logstash/inputs/azure_blob_storage.rb](https://github.com/janmg/logstash-input-azure_blob_storage/blob/master/lib/logstash/inputs/azure_blob_storage.rb)
103
172
 
104
173
  For WAD IIS and App Services the HTTP AccessLogs can be retrieved from a storage account as line based events and parsed through GROK. The date stamp can also be parsed with %{TIMESTAMP_ISO8601:log_timestamp}. For WAD IIS logfiles the container is wad-iis-logfiles. In the future grokking may happen already by the plugin.
105
174
  ```
@@ -138,7 +207,7 @@ filter {
138
207
  remove_field => ["subresponse"]
139
208
  remove_field => ["username"]
140
209
  remove_field => ["clientPort"]
141
- remove_field => ["port"]
210
+ remove_field => ["port"]:0
142
211
  remove_field => ["timestamp"]
143
212
  }
144
213
  }
@@ -25,6 +25,9 @@ config :storageaccount, :validate => :string, :required => false
25
25
  # DNS Suffix other then blob.core.windows.net
26
26
  config :dns_suffix, :validate => :string, :required => false, :default => 'core.windows.net'
27
27
 
28
+ # For development this can be used to emulate an accountstorage when not available from azure
29
+ #config :use_development_storage, :validate => :boolean, :required => false
30
+
28
31
  # The (primary or secondary) Access Key for the the storage account. The key can be found in the portal.azure.com or through the azure api StorageAccounts/ListKeys. For example the PowerShell command Get-AzStorageAccountKey.
29
32
  config :access_key, :validate => :password, :required => false
30
33
 
@@ -39,6 +42,9 @@ config :container, :validate => :string, :default => 'insights-logs-networksecur
39
42
  # The default, `data/registry`, it contains a Ruby Marshal Serialized Hash of the filename the offset read sofar and the filelength the list time a filelisting was done.
40
43
  config :registry_path, :validate => :string, :required => false, :default => 'data/registry.dat'
41
44
 
45
+ # If registry_local_path is set to a directory on the local server, the registry is save there instead of the remote blob_storage
46
+ config :registry_local_path, :validate => :string, :required => false
47
+
42
48
  # The default, `resume`, will load the registry offsets and will start processing files from the offsets.
43
49
  # When set to `start_over`, all log files are processed from begining.
44
50
  # when set to `start_fresh`, it will read log files that are created or appended since this start of the pipeline.
@@ -55,9 +61,21 @@ config :registry_create_policy, :validate => ['resume','start_over','start_fresh
55
61
  # Z00000000000000000000000000000000 2 ]}
56
62
  config :interval, :validate => :number, :default => 60
57
63
 
64
+ # add the filename into the events
65
+ config :addfilename, :validate => :boolean, :default => false, :required => false
66
+
67
+ # debug_until will for a maximum amount of processed messages shows 3 types of log printouts including processed filenames. This is a lightweight alternative to switching the loglevel from info to debug or even trace
68
+ config :debug_until, :validate => :number, :default => 0, :required => false
69
+
70
+ # debug_timer show time spent on activities
71
+ config :debug_timer, :validate => :boolean, :default => false, :required => false
72
+
58
73
  # WAD IIS Grok Pattern
59
74
  #config :grokpattern, :validate => :string, :required => false, :default => '%{TIMESTAMP_ISO8601:log_timestamp} %{NOTSPACE:instanceId} %{NOTSPACE:instanceId2} %{IPORHOST:ServerIP} %{WORD:httpMethod} %{URIPATH:requestUri} %{NOTSPACE:requestQuery} %{NUMBER:port} %{NOTSPACE:username} %{IPORHOST:clientIP} %{NOTSPACE:httpVersion} %{NOTSPACE:userAgent} %{NOTSPACE:cookie} %{NOTSPACE:referer} %{NOTSPACE:host} %{NUMBER:httpStatus} %{NUMBER:subresponse} %{NUMBER:win32response} %{NUMBER:sentBytes:int} %{NUMBER:receivedBytes:int} %{NUMBER:timeTaken:int}'
60
75
 
76
+ # skip learning if you use json and don't want to learn the head and tail, but use either the defaults or configure them.
77
+ config :skip_learning, :validate => :boolean, :default => false, :required => false
78
+
61
79
  # The string that starts the JSON. Only needed when the codec is JSON. When partial file are read, the result will not be valid JSON unless the start and end are put back. the file_head and file_tail are learned at startup, by reading the first file in the blob_list and taking the first and last block, this would work for blobs that are appended like nsgflowlogs. The configuration can be set to override the learning. In case learning fails and the option is not set, the default is to use the 'records' as set by nsgflowlogs.
62
80
  config :file_head, :validate => :string, :required => false, :default => '{"records":['
63
81
  # The string that ends the JSON
@@ -76,64 +94,66 @@ config :file_tail, :validate => :string, :required => false, :default => ']}'
76
94
  # For NSGFLOWLOGS a path starts with "resourceId=/", but this would only be needed to exclude other files that may be written in the same container.
77
95
  config :prefix, :validate => :string, :required => false
78
96
 
97
+ config :path_filters, :validate => :array, :default => ['**/*'], :required => false
98
+
99
+ # TODO: Other feature requests
100
+ # show file path in logger
101
+ # add filepath as part of log message
102
+ # option to keep registry on local disk
79
103
 
80
104
 
81
105
  public
82
106
  def register
83
107
  @pipe_id = Thread.current[:name].split("[").last.split("]").first
84
- @logger.info("=== "+config_name+" / "+@pipe_id+" / "+@id[0,6]+" ===")
85
- #@logger.info("ruby #{ RUBY_VERSION }p#{ RUBY_PATCHLEVEL } / #{Gem.loaded_specs[config_name].version.to_s}")
108
+ @logger.info("=== #{config_name} #{Gem.loaded_specs["logstash-input-"+config_name].version.to_s} / #{@pipe_id} / #{@id[0,6]} / ruby #{ RUBY_VERSION }p#{ RUBY_PATCHLEVEL } ===")
86
109
  @logger.info("If this plugin doesn't work, please raise an issue in https://github.com/janmg/logstash-input-azure_blob_storage")
87
110
  # TODO: consider multiple readers, so add pipeline @id or use logstash-to-logstash communication?
88
111
  # TODO: Implement retry ... Error: Connection refused - Failed to open TCP connection to
112
+ end
113
+
89
114
 
115
+
116
+ def run(queue)
90
117
  # counter for all processed events since the start of this pipeline
91
118
  @processed = 0
92
119
  @regsaved = @processed
93
120
 
94
- # Try in this order to access the storageaccount
95
- # 1. storageaccount / sas_token
96
- # 2. connection_string
97
- # 3. storageaccount / access_key
98
-
99
- unless connection_string.nil?
100
- conn = connection_string.value
101
- end
102
- unless sas_token.nil?
103
- unless sas_token.value.start_with?('?')
104
- conn = "BlobEndpoint=https://#{storageaccount}.#{dns_suffix};SharedAccessSignature=#{sas_token.value}"
105
- else
106
- conn = sas_token.value
107
- end
108
- end
109
- unless conn.nil?
110
- @blob_client = Azure::Storage::Blob::BlobService.create_from_connection_string(conn)
111
- else
112
- @blob_client = Azure::Storage::Blob::BlobService.create(
113
- storage_account_name: storageaccount,
114
- storage_dns_suffix: dns_suffix,
115
- storage_access_key: access_key.value,
116
- )
117
- end
121
+ connect
118
122
 
119
123
  @registry = Hash.new
120
124
  if registry_create_policy == "resume"
121
- @logger.info(@pipe_id+" resuming from registry")
122
- for counter in 0..3
125
+ for counter in 1..3
123
126
  begin
124
- @registry = Marshal.load(@blob_client.get_blob(container, registry_path)[1])
125
- #[0] headers [1] responsebody
127
+ if (!@registry_local_path.nil?)
128
+ unless File.file?(@registry_local_path+"/"+@pipe_id)
129
+ @registry = Marshal.load(@blob_client.get_blob(container, registry_path)[1])
130
+ #[0] headers [1] responsebody
131
+ @logger.info("migrating from remote registry #{registry_path}")
132
+ else
133
+ if !Dir.exist?(@registry_local_path)
134
+ FileUtils.mkdir_p(@registry_local_path)
135
+ end
136
+ @registry = Marshal.load(File.read(@registry_local_path+"/"+@pipe_id))
137
+ @logger.info("resuming from local registry #{registry_local_path+"/"+@pipe_id}")
138
+ end
139
+ else
140
+ @registry = Marshal.load(@blob_client.get_blob(container, registry_path)[1])
141
+ #[0] headers [1] responsebody
142
+ @logger.info("resuming from remote registry #{registry_path}")
143
+ end
144
+ break
126
145
  rescue Exception => e
127
- @logger.error(@pipe_id+" caught: #{e.message}")
146
+ @logger.error("caught: #{e.message}")
128
147
  @registry.clear
129
- @logger.error(@pipe_id+" loading registry failed, starting over")
148
+ @logger.error("loading registry failed for attempt #{counter} of 3")
130
149
  end
131
150
  end
132
151
  end
133
152
  # read filelist and set offsets to file length to mark all the old files as done
134
153
  if registry_create_policy == "start_fresh"
135
- @logger.info(@pipe_id+" starting fresh")
136
154
  @registry = list_blobs(true)
155
+ save_registry(@registry)
156
+ @logger.info("starting fresh, writing a clean registry to contain #{@registry.size} blobs/files")
137
157
  end
138
158
 
139
159
  @is_json = false
@@ -146,34 +166,41 @@ def register
146
166
  @tail = ''
147
167
  # if codec=json sniff one files blocks A and Z to learn file_head and file_tail
148
168
  if @is_json
149
- learn_encapsulation
150
169
  if file_head
151
- @head = file_head
170
+ @head = file_head
152
171
  end
153
172
  if file_tail
154
- @tail = file_tail
173
+ @tail = file_tail
155
174
  end
156
- @logger.info(@pipe_id+" head will be: #{@head} and tail is set to #{@tail}")
175
+ if file_head and file_tail and !skip_learning
176
+ learn_encapsulation
177
+ end
178
+ @logger.info("head will be: #{@head} and tail is set to #{@tail}")
157
179
  end
158
- end # def register
159
-
160
-
161
180
 
162
- def run(queue)
163
181
  newreg = Hash.new
164
182
  filelist = Hash.new
165
183
  worklist = Hash.new
166
- # we can abort the loop if stop? becomes true
184
+ @last = start = Time.now.to_i
185
+
186
+ # This is the main loop, it
187
+ # 1. Lists all the files in the remote storage account that match the path prefix
188
+ # 2. Filters on path_filters to only include files that match the directory and file glob (**/*.json)
189
+ # 3. Save the listed files in a registry of known files and filesizes.
190
+ # 4. List all the files again and compare the registry with the new filelist and put the delta in a worklist
191
+ # 5. Process the worklist and put all events in the logstash queue.
192
+ # 6. if there is time left, sleep to complete the interval. If processing takes more than an inteval, save the registry and continue.
193
+ # 7. If stop signal comes, finish the current file, save the registry and quit
167
194
  while !stop?
168
- chrono = Time.now.to_i
169
195
  # load the registry, compare it's offsets to file list, set offset to 0 for new files, process the whole list and if finished within the interval wait for next loop,
170
196
  # TODO: sort by timestamp ?
171
197
  #filelist.sort_by(|k,v|resource(k)[:date])
172
198
  worklist.clear
173
199
  filelist.clear
174
200
  newreg.clear
201
+
202
+ # Listing all the files
175
203
  filelist = list_blobs(false)
176
- # registry.merge(filelist) {|key, :offset, :length| :offset.merge :length }
177
204
  filelist.each do |name, file|
178
205
  off = 0
179
206
  begin
@@ -182,63 +209,98 @@ def run(queue)
182
209
  off = 0
183
210
  end
184
211
  newreg.store(name, { :offset => off, :length => file[:length] })
212
+ if (@debug_until > @processed) then @logger.info("2: adding offsets: #{name} #{off} #{file[:length]}") end
185
213
  end
186
-
214
+ # size nilClass when the list doesn't grow?!
187
215
  # Worklist is the subset of files where the already read offset is smaller than the file size
188
216
  worklist.clear
217
+ chunk = nil
218
+
189
219
  worklist = newreg.select {|name,file| file[:offset] < file[:length]}
190
- # This would be ideal for threading since it's IO intensive, would be nice with a ruby native ThreadPool
191
- worklist.each do |name, file|
192
- #res = resource(name)
193
- @logger.debug(@pipe_id+" processing #{name} from #{file[:offset]} to #{file[:length]}")
220
+ if (worklist.size > 4) then @logger.info("worklist contains #{worklist.size} blobs") end
221
+
222
+ # Start of processing
223
+ # This would be ideal for threading since it's IO intensive, would be nice with a ruby native ThreadPool
224
+ if (worklist.size > 0) then
225
+ worklist.each do |name, file|
226
+ start = Time.now.to_i
227
+ if (@debug_until > @processed) then @logger.info("3: processing #{name} from #{file[:offset]} to #{file[:length]}") end
194
228
  size = 0
195
229
  if file[:offset] == 0
196
- chunk = full_read(name)
197
- size=chunk.size
230
+ # This is where Sera4000 issue starts
231
+ # For an append blob, reading full and crashing, retry, last_modified? ... lenght? ... committed? ...
232
+ # length and skip reg value
233
+ if (file[:length] > 0)
234
+ begin
235
+ chunk = full_read(name)
236
+ size=chunk.size
237
+ rescue Exception => e
238
+ @logger.error("Failed to read #{name} because of: #{e.message} .. will continue and pretend this never happened")
239
+ end
240
+ else
241
+ @logger.info("found a zero size file #{name}")
242
+ chunk = nil
243
+ end
198
244
  else
199
245
  chunk = partial_read_json(name, file[:offset], file[:length])
200
- @logger.debug(@pipe_id+" partial file #{name} from #{file[:offset]} to #{file[:length]}")
246
+ @logger.debug("partial file #{name} from #{file[:offset]} to #{file[:length]}")
201
247
  end
202
248
  if logtype == "nsgflowlog" && @is_json
249
+ # skip empty chunks
250
+ unless chunk.nil?
203
251
  res = resource(name)
204
252
  begin
205
253
  fingjson = JSON.parse(chunk)
206
- @processed += nsgflowlog(queue, fingjson)
207
- @logger.debug(@pipe_id+" Processed #{res[:nsg]} [#{res[:date]}] #{@processed} events")
254
+ @processed += nsgflowlog(queue, fingjson, name)
255
+ @logger.debug("Processed #{res[:nsg]} [#{res[:date]}] #{@processed} events")
208
256
  rescue JSON::ParserError
209
- @logger.error(@pipe_id+" parse error on #{res[:nsg]} [#{res[:date]}] offset: #{file[:offset]} length: #{file[:length]}")
257
+ @logger.error("parse error on #{res[:nsg]} [#{res[:date]}] offset: #{file[:offset]} length: #{file[:length]}")
210
258
  end
259
+ end
211
260
  # TODO: Convert this to line based grokking.
212
261
  # TODO: ECS Compliance?
213
262
  elsif logtype == "wadiis" && !@is_json
214
263
  @processed += wadiislog(queue, name)
215
264
  else
216
265
  counter = 0
217
- @codec.decode(chunk) do |event|
266
+ begin
267
+ @codec.decode(chunk) do |event|
218
268
  counter += 1
269
+ if @addfilename
270
+ event.set('filename', name)
271
+ end
219
272
  decorate(event)
220
273
  queue << event
274
+ end
275
+ rescue Exception => e
276
+ @logger.error("codec exception: #{e.message} .. will continue and pretend this never happened")
277
+ @registry.store(name, { :offset => file[:length], :length => file[:length] })
278
+ @logger.debug("#{chunk}")
221
279
  end
222
280
  @processed += counter
223
281
  end
224
282
  @registry.store(name, { :offset => size, :length => file[:length] })
225
283
  # TODO add input plugin option to prevent connection cache
226
284
  @blob_client.client.reset_agents!
227
- #@logger.info(@pipe_id+" name #{name} size #{size} len #{file[:length]}")
285
+ #@logger.info("name #{name} size #{size} len #{file[:length]}")
228
286
  # if stop? good moment to stop what we're doing
229
287
  if stop?
230
288
  return
231
289
  end
232
- # save the registry past the regular intervals
233
- now = Time.now.to_i
234
- if ((now - chrono) > interval)
290
+ if ((Time.now.to_i - @last) > @interval)
235
291
  save_registry(@registry)
236
- chrono += interval
237
292
  end
293
+ end
294
+ end
295
+ # The files that got processed after the last registry save need to be saved too, in case the worklist is empty for some intervals.
296
+ now = Time.now.to_i
297
+ if ((now - @last) > @interval)
298
+ save_registry(@registry)
299
+ end
300
+ sleeptime = interval - ((now - start) % interval)
301
+ if @debug_timer
302
+ @logger.info("going to sleep for #{sleeptime} seconds")
238
303
  end
239
- # Save the registry and sleep until the remaining polling interval is over
240
- save_registry(@registry)
241
- sleeptime = interval - (Time.now.to_i - chrono)
242
304
  Stud.stoppable_sleep(sleeptime) { stop? }
243
305
  end
244
306
  end
@@ -252,8 +314,54 @@ end
252
314
 
253
315
 
254
316
  private
317
+ def connect
318
+ # Try in this order to access the storageaccount
319
+ # 1. storageaccount / sas_token
320
+ # 2. connection_string
321
+ # 3. storageaccount / access_key
322
+
323
+ unless connection_string.nil?
324
+ conn = connection_string.value
325
+ end
326
+ unless sas_token.nil?
327
+ unless sas_token.value.start_with?('?')
328
+ conn = "BlobEndpoint=https://#{storageaccount}.#{dns_suffix};SharedAccessSignature=#{sas_token.value}"
329
+ else
330
+ conn = sas_token.value
331
+ end
332
+ end
333
+ unless conn.nil?
334
+ @blob_client = Azure::Storage::Blob::BlobService.create_from_connection_string(conn)
335
+ else
336
+ # unless use_development_storage?
337
+ @blob_client = Azure::Storage::Blob::BlobService.create(
338
+ storage_account_name: storageaccount,
339
+ storage_dns_suffix: dns_suffix,
340
+ storage_access_key: access_key.value,
341
+ )
342
+ # else
343
+ # @logger.info("not yet implemented")
344
+ # end
345
+ end
346
+ end
347
+
255
348
  def full_read(filename)
256
- return @blob_client.get_blob(container, filename)[1]
349
+ tries ||= 2
350
+ begin
351
+ return @blob_client.get_blob(container, filename)[1]
352
+ rescue Exception => e
353
+ @logger.error("caught: #{e.message} for full_read")
354
+ if (tries -= 1) > 0
355
+ if e.message = "Connection reset by peer"
356
+ connect
357
+ end
358
+ retry
359
+ end
360
+ end
361
+ begin
362
+ chuck = @blob_client.get_blob(container, filename)[1]
363
+ end
364
+ return chuck
257
365
  end
258
366
 
259
367
  def partial_read_json(filename, offset, length)
@@ -276,8 +384,7 @@ def strip_comma(str)
276
384
  end
277
385
 
278
386
 
279
-
280
- def nsgflowlog(queue, json)
387
+ def nsgflowlog(queue, json, name)
281
388
  count=0
282
389
  json["records"].each do |record|
283
390
  res = resource(record["resourceId"])
@@ -290,9 +397,16 @@ def nsgflowlog(queue, json)
290
397
  tups = tup.split(',')
291
398
  ev = rule.merge({:unixtimestamp => tups[0], :src_ip => tups[1], :dst_ip => tups[2], :src_port => tups[3], :dst_port => tups[4], :protocol => tups[5], :direction => tups[6], :decision => tups[7]})
292
399
  if (record["properties"]["Version"]==2)
400
+ tups[9] = 0 if tups[9].nil?
401
+ tups[10] = 0 if tups[10].nil?
402
+ tups[11] = 0 if tups[11].nil?
403
+ tups[12] = 0 if tups[12].nil?
293
404
  ev.merge!( {:flowstate => tups[8], :src_pack => tups[9], :src_bytes => tups[10], :dst_pack => tups[11], :dst_bytes => tups[12]} )
294
405
  end
295
406
  @logger.trace(ev.to_s)
407
+ if @addfilename
408
+ ev.merge!( {:filename => name } )
409
+ end
296
410
  event = LogStash::Event.new('message' => ev.to_json)
297
411
  decorate(event)
298
412
  queue << event
@@ -323,66 +437,108 @@ end
323
437
  # list all blobs in the blobstore, set the offsets from the registry and return the filelist
324
438
  # inspired by: https://github.com/Azure-Samples/storage-blobs-ruby-quickstart/blob/master/example.rb
325
439
  def list_blobs(fill)
326
- files = Hash.new
327
- nextMarker = nil
328
- counter = 0
329
- loop do
330
- begin
331
- if (counter > 10)
332
- @logger.error(@pipe_id+" lets try again for the 10th time, why don't faraday and azure storage accounts not play nice together? it has something to do with follow_redirect and a missing authorization header?")
333
- end
440
+ tries ||= 3
441
+ begin
442
+ return try_list_blobs(fill)
443
+ rescue Exception => e
444
+ @logger.error("caught: #{e.message} for list_blobs retries left #{tries}")
445
+ if (tries -= 1) > 0
446
+ retry
447
+ end
448
+ end
449
+ end
450
+
451
+ def try_list_blobs(fill)
452
+ # inspired by: http://blog.mirthlab.com/2012/05/25/cleanly-retrying-blocks-of-code-after-an-exception-in-ruby/
453
+ chrono = Time.now.to_i
454
+ files = Hash.new
455
+ nextMarker = nil
456
+ counter = 1
457
+ loop do
334
458
  blobs = @blob_client.list_blobs(container, { marker: nextMarker, prefix: @prefix})
335
459
  blobs.each do |blob|
336
- # exclude the registry itself
337
- unless blob.name == registry_path
460
+ # FNM_PATHNAME is required so that "**/test" can match "test" at the root folder
461
+ # FNM_EXTGLOB allows you to use "test{a,b,c}" to match either "testa", "testb" or "testc" (closer to shell behavior)
462
+ unless blob.name == registry_path
463
+ if @path_filters.any? {|path| File.fnmatch?(path, blob.name, File::FNM_PATHNAME | File::FNM_EXTGLOB)}
338
464
  length = blob.properties[:content_length].to_i
339
- offset = 0
465
+ offset = 0
340
466
  if fill
341
467
  offset = length
342
- end
468
+ end
343
469
  files.store(blob.name, { :offset => offset, :length => length })
470
+ if (@debug_until > @processed) then @logger.info("1: list_blobs #{blob.name} #{offset} #{length}") end
344
471
  end
472
+ end
345
473
  end
346
474
  nextMarker = blobs.continuation_token
347
475
  break unless nextMarker && !nextMarker.empty?
348
- rescue Exception => e
349
- @logger.error(@pipe_id+" caught: #{e.message}")
350
- counter += 1
351
- end
352
- end
476
+ if (counter % 10 == 0) then @logger.info(" listing #{counter * 50000} files") end
477
+ counter+=1
478
+ end
479
+ if @debug_timer
480
+ @logger.info("list_blobs took #{Time.now.to_i - chrono} sec")
481
+ end
353
482
  return files
354
483
  end
355
484
 
356
485
  # When events were processed after the last registry save, start a thread to update the registry file.
357
486
  def save_registry(filelist)
358
- # TODO because of threading, processed values and regsaved are not thread safe, they can change as instance variable @!
487
+ # Because of threading, processed values and regsaved are not thread safe, they can change as instance variable @! Most of the time this is fine because the registry is the last resort, but be careful about corner cases!
359
488
  unless @processed == @regsaved
360
489
  @regsaved = @processed
361
- @logger.info(@pipe_id+" processed #{@processed} events, saving #{filelist.size} blobs and offsets to registry #{registry_path}")
362
- Thread.new {
490
+ unless (@busy_writing_registry)
491
+ Thread.new {
363
492
  begin
364
- @blob_client.create_block_blob(container, registry_path, Marshal.dump(filelist))
493
+ @busy_writing_registry = true
494
+ unless (@registry_local_path)
495
+ @blob_client.create_block_blob(container, registry_path, Marshal.dump(filelist))
496
+ @logger.info("processed #{@processed} events, saving #{filelist.size} blobs and offsets to remote registry #{registry_path}")
497
+ else
498
+ File.open(@registry_local_path+"/"+@pipe_id, 'w') { |file| file.write(Marshal.dump(filelist)) }
499
+ @logger.info("processed #{@processed} events, saving #{filelist.size} blobs and offsets to local registry #{registry_local_path+"/"+@pipe_id}")
500
+ end
501
+ @busy_writing_registry = false
502
+ @last = Time.now.to_i
365
503
  rescue
366
- @logger.error(@pipe_id+" Oh my, registry write failed, do you have write access?")
504
+ @logger.error("Oh my, registry write failed, do you have write access?")
367
505
  end
368
506
  }
507
+ else
508
+ @logger.info("Skipped writing the registry because previous write still in progress, it just takes long or may be hanging!")
509
+ end
369
510
  end
370
511
  end
371
512
 
513
+
372
514
  def learn_encapsulation
515
+ @logger.info("learn_encapsulation, this can be skipped by setting skip_learning => true. Or set both head_file and tail_file")
373
516
  # From one file, read first block and last block to learn head and tail
374
- # If the blobstorage can't be found, an error from farraday middleware will come with the text
375
- # org.jruby.ext.set.RubySet cannot be cast to class org.jruby.RubyFixnum
376
- blob = @blob_client.list_blobs(container, { maxresults: 1, prefix: @prefix }).first
377
- return if blob.nil?
378
- blocks = @blob_client.list_blob_blocks(container, blob.name)[:committed]
379
- @logger.debug(@pipe_id+" using #{blob.name} to learn the json header and tail")
380
- @head = @blob_client.get_blob(container, blob.name, start_range: 0, end_range: blocks.first.size-1)[1]
381
- @logger.debug(@pipe_id+" learned header: #{@head}")
382
- length = blob.properties[:content_length].to_i
383
- offset = length - blocks.last.size
384
- @tail = @blob_client.get_blob(container, blob.name, start_range: offset, end_range: length-1)[1]
385
- @logger.debug(@pipe_id+" learned tail: #{@tail}")
517
+ begin
518
+ blobs = @blob_client.list_blobs(container, { max_results: 3, prefix: @prefix})
519
+ blobs.each do |blob|
520
+ unless blob.name == registry_path
521
+ begin
522
+ blocks = @blob_client.list_blob_blocks(container, blob.name)[:committed]
523
+ if blocks.first.name.start_with?('A00')
524
+ @logger.debug("using #{blob.name}/#{blocks.first.name} to learn the json header")
525
+ @head = @blob_client.get_blob(container, blob.name, start_range: 0, end_range: blocks.first.size-1)[1]
526
+ end
527
+ if blocks.last.name.start_with?('Z00')
528
+ @logger.debug("using #{blob.name}/#{blocks.last.name} to learn the json footer")
529
+ length = blob.properties[:content_length].to_i
530
+ offset = length - blocks.last.size
531
+ @tail = @blob_client.get_blob(container, blob.name, start_range: offset, end_range: length-1)[1]
532
+ @logger.debug("learned tail: #{@tail}")
533
+ end
534
+ rescue Exception => e
535
+ @logger.info("learn json one of the attempts failed #{e.message}")
536
+ end
537
+ end
538
+ end
539
+ rescue Exception => e
540
+ @logger.info("learn json header and footer failed because #{e.message}")
541
+ end
386
542
  end
387
543
 
388
544
  def resource(str)
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-input-azure_blob_storage'
3
- s.version = '0.11.1'
3
+ s.version = '0.11.6'
4
4
  s.licenses = ['Apache-2.0']
5
5
  s.summary = 'This logstash plugin reads and parses data from Azure Storage Blobs.'
6
6
  s.description = <<-EOF
@@ -22,6 +22,6 @@ EOF
22
22
  # Gem dependencies
23
23
  s.add_runtime_dependency 'logstash-core-plugin-api', '~> 2.1'
24
24
  s.add_runtime_dependency 'stud', '~> 0.0.23'
25
- s.add_runtime_dependency 'azure-storage-blob', '~> 1.0'
26
- s.add_development_dependency 'logstash-devutils', '~> 1.0', '>= 1.0.0'
25
+ s.add_runtime_dependency 'azure-storage-blob', '~> 1.1'
26
+ #s.add_development_dependency 'logstash-devutils', '~> 2'
27
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-azure_blob_storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.11.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Geertsma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-18 00:00:00.000000000 Z
11
+ date: 2021-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -17,8 +17,8 @@ dependencies:
17
17
  - !ruby/object:Gem::Version
18
18
  version: '2.1'
19
19
  name: logstash-core-plugin-api
20
- prerelease: false
21
20
  type: :runtime
21
+ prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
@@ -31,8 +31,8 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.0.23
33
33
  name: stud
34
- prerelease: false
35
34
  type: :runtime
35
+ prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
@@ -43,35 +43,15 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.0'
46
+ version: '1.1'
47
47
  name: azure-storage-blob
48
- prerelease: false
49
48
  type: :runtime
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '1.0'
55
- - !ruby/object:Gem::Dependency
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: 1.0.0
61
- - - "~>"
62
- - !ruby/object:Gem::Version
63
- version: '1.0'
64
- name: logstash-devutils
65
49
  prerelease: false
66
- type: :development
67
50
  version_requirements: !ruby/object:Gem::Requirement
68
51
  requirements:
69
- - - ">="
70
- - !ruby/object:Gem::Version
71
- version: 1.0.0
72
52
  - - "~>"
73
53
  - !ruby/object:Gem::Version
74
- version: '1.0'
54
+ version: '1.1'
75
55
  description: " This gem is a Logstash plugin. It reads and parses data from Azure\
76
56
  \ Storage Blobs. The azure_blob_storage is a reimplementation to replace azureblob\
77
57
  \ from azure-diagnostics-tools/Logstash. It can deal with larger volumes and partial\