logstash-output-unomaly 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4a098c44ba976c41ad152a148d265a6a7ece4216307104156ae1ff9f578589cd
4
+ data.tar.gz: b1dc31ddc497b25ae5895aa5407117047f695cc03d1ccc99e4f69f605e3cc9e0
5
+ SHA512:
6
+ metadata.gz: 1b9e317eab598594b691766d46199d12dfe3291baa7e49863762cae50e7a9679693f1d35ed80ce69c4d033908171b9026a19e910c78c818b979ca0d5a7b23cf0
7
+ data.tar.gz: cecec9cc21f9dc4c23663010be493597baeb082c73eecec5e3fcd122bdfac1f679b303e275e0a6659d6c669b3fe5a25a829c7be9517a45e4046b04e32e2b9fbb
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## 0.1.3
2
+ - Add option `keep_timestamp` defaulted to true.
3
+ ## 0.1.0
4
+ - First version of the plugin
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ logstash_path = ENV["LOGSTASH_PATH"] || "../../logstash"
6
+ use_logstash_source = ENV["LOGSTASH_SOURCE"] && ENV["LOGSTASH_SOURCE"].to_s == "1"
7
+
8
+ if Dir.exist?(logstash_path) && use_logstash_source
9
+ gem 'logstash-core', :path => "#{logstash_path}/logstash-core"
10
+ gem 'logstash-core-plugin-api', :path => "#{logstash_path}/logstash-core-plugin-api"
11
+ end
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Logstash output plugin for Unomaly
2
+
3
+ [![Build Status](https://travis-ci.org/unomaly/logstash-output-unomaly.svg?branch=master)](https://travis-ci.org/unomaly/logstash-output-unomaly)
4
+
5
+ This plugin sends Logstash events to the [Unomaly](https://www.unomaly.com) ingestion API (min version Unomaly 2.27). The minimal configuration looks like this:
6
+
7
+
8
+ ```
9
+ output {
10
+ unomaly {
11
+ host => "https://your-unomaly-instance:443"
12
+ }
13
+ }
14
+ ```
15
+
16
+ # Important options
17
+
18
+
19
+ | Option | Description | Default |
20
+ |----------------------------|----------------------------------------------------------------------------------|------------|
21
+ | host | Unomaly instance address. Must define full path such as "https://my-instance:443"| No default |
22
+ | message_key | The key in the Logstash event that Unomaly should use for anomaly detection. | "message" |
23
+ | source_key | The event key defining the Unomaly system. | "host" |
24
+ | ssl_certificate_validation | Enable or disable SSL certificate validation | "strict" |
25
+
26
+ See the [source code](lib/logstash/outputs/unomaly.rb) for the full list of options
27
+
28
+
29
+ ## Known issues
30
+ - Installation of the plugin fails on Logstash 6.2.1.
31
+
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome. This project is intended to
36
+ be a safe, welcoming space for collaboration.
37
+
38
+ ## Development
39
+
40
+ We use docker to build the plugin. You can build it by running `docker-compose run jruby gem build logstash-output-unomaly.gemspec `
@@ -0,0 +1,333 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/base"
3
+ require "logstash/namespace"
4
+ require "logstash/json"
5
+ require 'uri'
6
+ require 'json'
7
+ require 'manticore'
8
+
9
+ # An example output that does nothing.
10
+ class LogStash::Outputs::Unomaly < LogStash::Outputs::Base
11
+ class InvalidHTTPConfigError < StandardError; end
12
+
13
+ concurrency :shared
14
+ config_name "unomaly"
15
+
16
+ # Event batch size to send to Unomaly. Increasing the batch size can increase throughput by reducing HTTP overhead
17
+ config :batch_size, :validate => :number, :default => 50
18
+
19
+ # Unomaly host to send the logs to
20
+ config :host, :validate => :string, :required => true
21
+
22
+ # Key that will be used by Unomaly as the log message
23
+ config :message_key, :validate => :string, :default => "message"
24
+
25
+ # Key that will be used by Unomaly as the system key
26
+ config :source_key, :validate => :string, :default => "host"
27
+
28
+ # Unomaly api path to push events
29
+ config :api_path, :validate => :string, :default => "/v1/batch"
30
+
31
+ # Keep logstash timestamp
32
+ config :keep_timestamp, :validate => :boolean, :default => true
33
+
34
+ # Display debug logs
35
+ config :debug, :validate => :boolean, :default => false
36
+
37
+ # Timeout (in seconds) for the entire request
38
+ config :request_timeout, :validate => :number, :default => 60
39
+
40
+ # Timeout (in seconds) to wait for data on the socket. Default is `10s`
41
+ config :socket_timeout, :validate => :number, :default => 10
42
+
43
+ # Timeout (in seconds) to wait for a connection to be established. Default is `10s`
44
+ config :connect_timeout, :validate => :number, :default => 10
45
+
46
+ # Should redirects be followed? Defaults to `true`
47
+ config :follow_redirects, :validate => :boolean, :default => true
48
+
49
+ # Max number of concurrent connections. Defaults to `50`
50
+ config :pool_max, :validate => :number, :default => 50
51
+
52
+ # Max number of concurrent connections to a single host. Defaults to `25`
53
+ config :pool_max_per_route, :validate => :number, :default => 25
54
+
55
+ # Turn this on to enable HTTP keepalive support. We highly recommend setting `automatic_retries` to at least
56
+ # one with this to fix interactions with broken keepalive implementations.
57
+ config :keepalive, :validate => :boolean, :default => true
58
+
59
+ # How many times should the client retry a failing URL. We highly recommend NOT setting this value
60
+ # to zero if keepalive is enabled. Some servers incorrectly end keepalives early requiring a retry!
61
+ # Note: if `retry_non_idempotent` is set only GET, HEAD, PUT, DELETE, OPTIONS, and TRACE requests will be retried.
62
+ config :automatic_retries, :validate => :number, :default => 1
63
+
64
+ # If `automatic_retries` is enabled this will cause non-idempotent HTTP verbs (such as POST) to be retried.
65
+ config :retry_non_idempotent, :validate => :boolean, :default => false
66
+
67
+ # How long to wait before checking if the connection is stale before executing a request on a connection using keepalive.
68
+ # # You may want to set this lower, possibly to 0 if you get connection errors regularly
69
+ # Quoting the Apache commons docs (this client is based Apache Commmons):
70
+ # 'Defines period of inactivity in milliseconds after which persistent connections must be re-validated prior to being leased to the consumer. Non-positive value passed to this method disables connection validation. This check helps detect connections that have become stale (half-closed) while kept inactive in the pool.'
71
+ # See https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.html#setValidateAfterInactivity(int)[these docs for more info]
72
+ config :validate_after_inactivity, :validate => :number, :default => 200
73
+
74
+ # refer to https://github.com/cheald/manticore/blob/6764e2d3fb67a1ef244cb4610d5b74ba1dd6694c/lib/manticore/client.rb#L161
75
+ # instead of using symbol, use string "disable" or "false", "browser", "strict"
76
+ config :ssl_certificate_validation, :validate => :string, :default => "strict"
77
+
78
+ # If you need to use a custom X.509 CA (.pem certs) specify the path to that here
79
+ config :cacert, :validate => :path
80
+
81
+ # If you'd like to use a client certificate (note, most people don't want this) set the path to the x509 cert here
82
+ config :client_cert, :validate => :path
83
+ # If you're using a client certificate specify the path to the encryption key here
84
+ config :client_key, :validate => :path
85
+
86
+ # If you need to use a custom keystore (`.jks`) specify that here. This does not work with .pem keys!
87
+ config :keystore, :validate => :path
88
+
89
+ # Specify the keystore password here.
90
+ # Note, most .jks files created with keytool require a password!
91
+ config :keystore_password, :validate => :password
92
+
93
+ # Specify the keystore type here. One of `JKS` or `PKCS12`. Default is `JKS`
94
+ config :keystore_type, :validate => :string, :default => "JKS"
95
+
96
+ # If you need to use a custom truststore (`.jks`) specify that here. This does not work with .pem certs!
97
+ config :truststore, :validate => :path
98
+
99
+ # Specify the truststore password here.
100
+ # Note, most .jks files created with keytool require a password!
101
+ config :truststore_password, :validate => :password
102
+
103
+ # Specify the truststore type here. One of `JKS` or `PKCS12`. Default is `JKS`
104
+ config :truststore_type, :validate => :string, :default => "JKS"
105
+
106
+ # Enable cookie support. With this enabled the client will persist cookies
107
+ # across requests as a normal web browser would. Enabled by default
108
+ config :cookies, :validate => :boolean, :default => true
109
+
110
+ # If you'd like to use an HTTP proxy . This supports multiple configuration syntaxes:
111
+ #
112
+ # 1. Proxy host in form: `http://proxy.org:1234`
113
+ # 2. Proxy host in form: `{host => "proxy.org", port => 80, scheme => 'http', user => 'username@host', password => 'password'}`
114
+ # 3. Proxy host in form: `{url => 'http://proxy.org:1234', user => 'username@host', password => 'password'}`
115
+ config :proxy
116
+
117
+ # Username to use for HTTP auth.
118
+ config :user, :validate => :string
119
+
120
+ # Password to use for HTTP auth
121
+ config :password, :validate => :password
122
+
123
+ public
124
+ def register
125
+ @total = 0
126
+ @total_failed = 0
127
+ logger.info("Initialized Unomaly output plugin with configuration",
128
+ :host => @host,
129
+ :accept_self_signed_cert => @accept_self_signed_cert)
130
+
131
+ end # def register
132
+
133
+ def client_config
134
+ c = {
135
+ connect_timeout: @connect_timeout,
136
+ socket_timeout: @socket_timeout,
137
+ request_timeout: @request_timeout,
138
+ follow_redirects: @follow_redirects,
139
+ automatic_retries: @automatic_retries,
140
+ retry_non_idempotent: @retry_non_idempotent,
141
+ check_connection_timeout: @validate_after_inactivity,
142
+ pool_max: @pool_max,
143
+ pool_max_per_route: @pool_max_per_route,
144
+ cookies: @cookies,
145
+ keepalive: @keepalive
146
+ }
147
+
148
+ if @proxy
149
+ # Symbolize keys if necessary
150
+ c[:proxy] = @proxy.is_a?(Hash) ?
151
+ @proxy.reduce({}) {|memo,(k,v)| memo[k.to_sym] = v; memo} :
152
+ @proxy
153
+ end
154
+
155
+ if @user
156
+ if !@password || !@password.value
157
+ raise ::LogStash::ConfigurationError, "User '#{@user}' specified without password!"
158
+ end
159
+
160
+ # Symbolize keys if necessary
161
+ c[:auth] = {
162
+ :user => @user,
163
+ :password => @password.value,
164
+ :eager => true
165
+ }
166
+ end
167
+
168
+ c[:ssl] = {}
169
+ if @ssl_certificate_validation == "disable" || @ssl_certificate_validation == "false"
170
+ c[:ssl][:verify] = :disable
171
+ elsif @ssl_certificate_validation == "browser"
172
+ c[:ssl][:verify] = :browser
173
+ end
174
+
175
+ if @cacert
176
+ c[:ssl][:ca_file] = @cacert
177
+ end
178
+
179
+ if @truststore
180
+ c[:ssl].merge!(
181
+ :truststore => @truststore,
182
+ :truststore_type => @truststore_type,
183
+ :truststore_password => @truststore_password.value
184
+ )
185
+
186
+ if c[:ssl][:truststore_password].nil?
187
+ raise LogStash::ConfigurationError, "Truststore declared without a password! This is not valid, please set the 'truststore_password' option"
188
+ end
189
+ end
190
+
191
+ if @keystore
192
+ c[:ssl].merge!(
193
+ :keystore => @keystore,
194
+ :keystore_type => @keystore_type,
195
+ :keystore_password => @keystore_password.value
196
+ )
197
+
198
+ if c[:ssl][:keystore_password].nil?
199
+ raise LogStash::ConfigurationError, "Keystore declared without a password! This is not valid, please set the 'keystore_password' option"
200
+ end
201
+ end
202
+
203
+ if @client_cert && @client_key
204
+ c[:ssl][:client_cert] = @client_cert
205
+ c[:ssl][:client_key] = @client_key
206
+ elsif !!@client_cert ^ !!@client_key
207
+ raise InvalidHTTPConfigError, "You must specify both client_cert and client_key for an HTTP client, or neither!"
208
+ end
209
+
210
+ c
211
+ end
212
+
213
+ private
214
+ def make_client
215
+ puts client_config
216
+ Manticore::Client.new(client_config)
217
+ end
218
+
219
+ public
220
+ def client
221
+ @client ||= make_client
222
+ end
223
+
224
+ public
225
+ def close
226
+ @client.close
227
+ end
228
+
229
+ def flatten(data, prefix)
230
+ ret = {}
231
+ if data.is_a? Hash
232
+ data.each { |key, value|
233
+ if prefix.to_s.empty?
234
+ ret.merge! flatten(value, "#{key.to_s}")
235
+ else
236
+ ret.merge! flatten(value, "#{prefix}__#{key.to_s}")
237
+ end
238
+ }
239
+ elsif data.is_a? Array
240
+ data.each_with_index {|val,index | ret.merge! flatten(val, "#{prefix}__#{index}")}
241
+ else
242
+ return {prefix => data.to_s}
243
+ end
244
+
245
+ ret
246
+ end
247
+
248
+ def send_batch(events)
249
+ url = @host + @api_path
250
+ body = events.to_json
251
+
252
+ request = client.post(url, {
253
+ :body => body,
254
+ :headers => {"Content-Type"=>"application/json"}
255
+ })
256
+
257
+ request.on_success do |response|
258
+ if response.code == 200
259
+ @logger.debug("Successfully sent ",
260
+ :response_code => response.code,
261
+ :total => @total,
262
+ :time => Time::now.utc)
263
+ else
264
+ @total_failed += 1
265
+ log_failure(
266
+ "Encountered non-200 HTTP code #{response.code}",
267
+ :response_code => response.code,
268
+ :url => url,
269
+ :response_body => response.body,
270
+ :total_failed => @total_failed)
271
+ end
272
+ end
273
+
274
+ request.on_failure do |exception|
275
+ @total_failed += 1
276
+ log_failure("Could not access URL",
277
+ :url => url,
278
+ :method => @http_method,
279
+ :body => body,
280
+ :message => exception.message,
281
+ :class => exception.class.name,
282
+ :backtrace => exception.backtrace,
283
+ :total_failed => @total_failed
284
+ )
285
+ end
286
+
287
+ @logger.debug("Sending Unomaly Event",
288
+ :total => @total,
289
+ :time => Time::now.utc)
290
+ request.call
291
+
292
+ rescue Exception => e
293
+ @logger.error("[Exception=] #{e.message} #{e.backtrace}")
294
+ end
295
+
296
+
297
+ public
298
+ def multi_receive(events)
299
+ if debug
300
+ puts events.to_json
301
+ end
302
+
303
+ events.each_slice(@batch_size) do |chunk|
304
+ documents = []
305
+ chunk.each do |event|
306
+ unomaly_event = {
307
+ message: event.get(@message_key),
308
+ source: event.get(@source_key),
309
+ }
310
+
311
+ metadata = event.to_hash
312
+
313
+ if @keep_timestamp
314
+ unomaly_event["timestamp"] = event.get("@timestamp")
315
+ metadata.delete("@timestamp")
316
+ end
317
+
318
+ metadata.delete(@source_key)
319
+ metadata.delete(@message_key)
320
+
321
+ unomaly_event["metadata"]=flatten(metadata,"")
322
+
323
+
324
+ documents.push(unomaly_event)
325
+ end
326
+ send_batch(documents)
327
+ end
328
+ end
329
+
330
+ def log_failure(message, opts)
331
+ @logger.error("[HTTP Output Failure] #{message}", opts)
332
+ end
333
+ end # class LogStash::Outputs::Unomaly
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-output-unomaly'
3
+ s.version = '0.1.3'
4
+ s.licenses = ['Apache License (2.0)']
5
+ s.summary = "Logstash output plugin for Unomaly"
6
+ s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
7
+ s.authors = ["Unomaly"]
8
+ s.email = "support@unomaly.com"
9
+ s.homepage = "https://unomaly.com"
10
+ s.require_paths = ["lib"]
11
+
12
+ # Files
13
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','Gemfile']
14
+ # Tests
15
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
+
17
+ # Special flag to let us know this is actually a logstash plugin
18
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "output" }
19
+
20
+ # Gem dependencies
21
+ #
22
+
23
+ s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
24
+ s.add_runtime_dependency "logstash-codec-plain"
25
+ s.add_runtime_dependency 'manticore', '>= 0.5.2', '< 1.0.0'
26
+
27
+ s.add_development_dependency 'logstash-devutils'
28
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/outputs/unomaly"
4
+ require "logstash/event"
5
+
6
+ describe LogStash::Outputs::Unomaly do
7
+ let(:sample_event) { LogStash::Event.new("message" => "hello this is log") }
8
+ let(:client) { @unomaly.client }
9
+
10
+ before do
11
+ @unomaly = LogStash::Outputs::Unomaly.new("host" => "localhost", "batch_size" => 3)
12
+ @unomaly.register
13
+ allow(@unomaly).to receive(:client).and_return(client)
14
+ allow(client).to receive(:post).and_call_original
15
+ end
16
+
17
+ before do
18
+ allow(@unomaly).to receive(:client).and_return(client)
19
+ end
20
+
21
+
22
+ it "Forwards an event" do
23
+ expect(client).to receive(:post).once.and_call_original
24
+ @unomaly.multi_receive([sample_event])
25
+ end
26
+
27
+ it "Batches multiple events and extracts metadata" do
28
+ event1 = LogStash::Event.new("message" => "hello this is log 1", "host" => "host1")
29
+ event2 = LogStash::Event.new("message" => "hello this is log 2", "host" => "host2")
30
+ event3 = LogStash::Event.new("message" => "hello this is log 3", "host" => "host3")
31
+ expect(client).to receive(:post).once.with("localhost/v1/batch",hash_including(:body => LogStash::Json.dump(
32
+ [{"message" => "hello this is log 1", "source" => "host1", "timestamp" => event1.timestamp.to_s, "metadata" => {"@version" => "1"}},
33
+ {"message" => "hello this is log 2", "source" => "host2", "timestamp" => event2.timestamp.to_s, "metadata" => {"@version" => "1"}},
34
+ {"message" => "hello this is log 3", "source" => "host3", "timestamp" => event3.timestamp.to_s, "metadata" => {"@version" => "1"}}
35
+ ]
36
+ ))).and_call_original
37
+ @unomaly.multi_receive([event1, event2, event3])
38
+ end
39
+
40
+ it "Batches data of size batch_size" do
41
+ expect(client).to receive(:post).exactly(2).times.and_call_original
42
+ @unomaly.multi_receive([sample_event, sample_event, sample_event, sample_event])
43
+ end
44
+
45
+
46
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-output-unomaly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Unomaly
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash-core-plugin-api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.60'
20
+ - - "<="
21
+ - !ruby/object:Gem::Version
22
+ version: '2.99'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.60'
30
+ - - "<="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.99'
33
+ - !ruby/object:Gem::Dependency
34
+ name: logstash-codec-plain
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: manticore
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.5.2
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: 1.0.0
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 0.5.2
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: 1.0.0
67
+ - !ruby/object:Gem::Dependency
68
+ name: logstash-devutils
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ description: This gem is a Logstash plugin required to be installed on top of the
82
+ Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This
83
+ gem is not a stand-alone program
84
+ email: support@unomaly.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - CHANGELOG.md
90
+ - Gemfile
91
+ - README.md
92
+ - lib/logstash/outputs/unomaly.rb
93
+ - logstash-output-unomaly.gemspec
94
+ - spec/outputs/unomaly_spec.rb
95
+ homepage: https://unomaly.com
96
+ licenses:
97
+ - Apache License (2.0)
98
+ metadata:
99
+ logstash_plugin: 'true'
100
+ logstash_group: output
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.7.7
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Logstash output plugin for Unomaly
121
+ test_files:
122
+ - spec/outputs/unomaly_spec.rb