marlowe 1.0.3 → 3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configuration object for Marlowe.
4
+ class Marlowe::Config
5
+ # The name of the default header to look for and put the correlation id in.
6
+ CORRELATION_HEADER = "X-Request-Id" # :nodoc:
7
+
8
+ class << self
9
+ # The global Marlowe configuration.
10
+ def global
11
+ @global ||= new
12
+ end
13
+
14
+ # Override the global Marlowe configuration.
15
+ def override(opts)
16
+ new(global, opts)
17
+ end
18
+
19
+ def configure(&block) # :nodoc:
20
+ @global = new(global, &block)
21
+ end
22
+
23
+ private
24
+
25
+ def clear_global!
26
+ @global = nil
27
+ end
28
+ end
29
+
30
+ # The name of the header to inspect. Defaults to 'X-Request-Id'.
31
+ attr_accessor :header
32
+ # The HTTP formatted version of the header name to inspect. Defaults to
33
+ # 'HTTP_X_REQUEST_ID'.
34
+ attr_reader :http_header
35
+ # The handler for request correlation IDs. Defaults to sanitizing provided
36
+ # request IDs or generating a UUID. If <tt>:simple</tt> is provided, provided
37
+ # request IDs will not be sanitized. A callable (expecting a single input of
38
+ # any possible existing request ID) may be provided to introduce more complex
39
+ # request ID handling.
40
+ attr_accessor :handler
41
+ # If +true+ (the default), the request correlation ID will be returned as
42
+ # part of the response headers. Only affects Marlowe::Middleware.
43
+ attr_accessor :return
44
+ # If +true+, Marlowe will add code to behave like
45
+ # <tt>ActionDispatch::RequestId</tt>. Depends on
46
+ # <tt>ActionDispatch::Request</tt>. Only affects Marlowe::Middleware.
47
+ attr_accessor :action_dispatch
48
+
49
+ # === Option Values
50
+ #
51
+ # <tt>:header</tt>:: The name of the header to inspect. Defaults to
52
+ # 'X-Request-Id'. Also available as
53
+ # <tt>:correlation_header</tt>.
54
+ # <tt>:handler</tt>:: The handler for request correlation IDs. Defaults to
55
+ # sanitizing provided request IDs or generating a UUID.
56
+ # If <tt>:simple</tt> is provided, provided request IDs
57
+ # will not be sanitized. A callable (expecting a single
58
+ # input of any possible existing request ID) may be
59
+ # provided to introduce more complex request ID
60
+ # handling.
61
+ # <tt>:return</tt>:: If +true+ (the default), the request correlation ID
62
+ # will be returned as part of the response headers.
63
+ # <tt>:action_dispatch</tt>:: If +true+, Marlowe will add code to behave
64
+ # like <tt>ActionDispatch::RequestId</tt>.
65
+ # Depends on <tt>ActionDispatch::Request</tt>.
66
+ def initialize(base = nil, opts = nil) # :yields: self
67
+ opts =
68
+ if base.nil? && opts.nil?
69
+ {}
70
+ elsif base.nil? && opts.is_a?(Hash)
71
+ opts
72
+ elsif base.is_a?(Hash) && opts.nil?
73
+ base
74
+ elsif base.is_a?(self.class) && opts.nil?
75
+ base.to_hash
76
+ elsif (base.is_a?(Hash) || base.is_a?(self.class)) && opts.is_a?(Hash)
77
+ hash =
78
+ if base.is_a?(self.class)
79
+ base.to_hash
80
+ else
81
+ base
82
+ end
83
+ hash.update(opts)
84
+ end
85
+
86
+ @header, @http_header = format_header_name(
87
+ opts[:header] || opts[:correlation_header] || CORRELATION_HEADER
88
+ )
89
+
90
+ @handler = opts.fetch(:handler, :clean)
91
+ @return = opts.fetch(:return, true)
92
+ @action_dispatch = opts.fetch(:action_dispatch, false)
93
+
94
+ yield self if block_given?
95
+
96
+ freeze
97
+ end
98
+
99
+ def to_hash
100
+ {
101
+ header: header,
102
+ handler: handler,
103
+ return: self.return,
104
+ action_dispatch: action_dispatch
105
+ }
106
+ end
107
+
108
+ private
109
+
110
+ def format_header_name(header)
111
+ [
112
+ header.to_s.tr("_", "-").freeze,
113
+ "HTTP_#{header.to_s.tr("-", "_").upcase}"
114
+ ]
115
+ end
116
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "marlowe"
4
+
5
+ # Marlowe correlation ID middleware for Faraday. Including this into your
6
+ # request middleware stack will use the captured correlation ID.
7
+ class Marlowe::Faraday < Faraday::Middleware
8
+ def initialize(app, opts = {})
9
+ super(app)
10
+ @config = Marlowe::Config.override(opts)
11
+ end
12
+
13
+ def call(env)
14
+ env[:request_headers][@config.header] =
15
+ Marlowe.make_request_id(RequestStore[:correlation_id], @config)
16
+ @app.call(env)
17
+ end
18
+ end
19
+
20
+ Faraday::Request.register_middleware marlowe: -> { Marlowe::Faraday }
@@ -1,13 +1,13 @@
1
- require 'request_store'
1
+ # frozen_string_literal: true
2
2
 
3
- module Marlowe
3
+ require "request_store"
4
4
 
5
+ module Marlowe
5
6
  # Marlowe::Formatter is a subclass of +ActiveSupport::Logger::Formatter+
6
7
  # that adds a correlation id string to a rails log.
7
8
  class Formatter < ActiveSupport::Logger::Formatter
8
-
9
9
  # Overrides the formatter return to add the correlation id.
10
- def call(severity, timestamp, progname, msg)
10
+ def call(_severity, _timestamp, _progname, _msg)
11
11
  "[#{RequestStore.store[:correlation_id]}] #{super}"
12
12
  end
13
13
  end
@@ -1,36 +1,66 @@
1
- require 'rack'
2
- require 'request_store'
3
- require 'securerandom'
1
+ # frozen_string_literal: true
4
2
 
5
- module Marlowe
6
- # Marlowe correlation id middleware. Including this into your
7
- # middleware stack will add a correlation id header as an incoming
8
- # request, and save that id in a request session variable.
9
-
10
- # Name of the default header to look for and put the correlation id in.
11
- CORRELATION_HEADER = 'Correlation-Id'.freeze
3
+ require "rack"
4
+ require "request_store"
5
+ require "securerandom"
12
6
 
7
+ module Marlowe
8
+ # Marlowe correlation id middleware. Including this into your middleware
9
+ # stack will capture or add a correlation id header on an incoming request,
10
+ # and save that id in a request session variable.
13
11
  class Middleware
14
- # Sets the the rack application to +app+
15
- def initialize(app, opts={})
12
+ # The name of the default header to look for and put the correlation id in.
13
+ CORRELATION_HEADER = Marlowe::Config::CORRELATION_HEADER # :nodoc:
14
+
15
+ # Configure the Marlowe middleware to call +app+ with options +opts+.
16
+ #
17
+ # === Options
18
+ #
19
+ # <tt>:header</tt>:: The name of the header to inspect. Defaults to
20
+ # 'X-Request-Id'. Also available as
21
+ # <tt>:correlation_header</tt>.
22
+ # <tt>:handler</tt>:: The handler for request correlation IDs. Defaults to
23
+ # sanitizing provided request IDs or generating a UUID.
24
+ # If <tt>:simple</tt> is provided, provided request IDs
25
+ # will not be sanitized. A callable (expecting a single
26
+ # input of any possible existing request ID) may be
27
+ # provided to introduce more complex request ID
28
+ # handling.
29
+ # <tt>:return</tt>:: If +true+ (the default), the request correlation ID
30
+ # will be returned as part of the response headers.
31
+ # <tt>:action_dispatch</tt>:: If +true+, Marlowe will add code to behave
32
+ # like <tt>ActionDispatch::RequestId</tt>.
33
+ # Depends on <tt>ActionDispatch::Request</tt>.
34
+ def initialize(app, opts = nil)
16
35
  @app = app
17
- @correlation_header = format_http_header(opts[:correlation_header] || Marlowe::CORRELATION_HEADER)
36
+ @config = Marlowe::Config.override(opts)
18
37
  end
19
38
 
20
39
  # Stores the incoming correlation id from the +env+ hash. If the correlation
21
40
  # id has not been sent, a new UUID is generated and the +env+ is modified.
22
41
  def call(env)
23
- env[@correlation_header] ||= SecureRandom.uuid
24
- RequestStore.store[:correlation_id] = env[@correlation_header]
42
+ req_id = Marlowe.make_request_id(env[config.http_header], config)
43
+ RequestStore.store[:correlation_id] = env[config.http_header] = req_id
44
+
45
+ if config.action_dispatch
46
+ req = ActionDispatch::Request.new(env)
47
+ req.request_id = req_id
48
+ end
25
49
 
26
- @status, @headers, @response = @app.call(env)
27
- [@status, @headers, @response]
50
+ app.call(env).tap { |_status, headers, _body|
51
+ if config.return
52
+ headers[config.header] =
53
+ if config.action_dispatch
54
+ req.request_id
55
+ else
56
+ RequestStore.store[:correlation_id]
57
+ end
58
+ end
59
+ }
28
60
  end
29
61
 
30
62
  private
31
63
 
32
- def format_http_header(header)
33
- ("HTTP_" + header.gsub(/-/, '_').upcase).freeze
34
- end
64
+ attr_reader :app, :config
35
65
  end
36
66
  end
data/lib/marlowe/rails.rb CHANGED
@@ -1,11 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Marlowe
2
- class Railtie < Rails::Railtie
3
- initializer 'marlowe.configure_rails_initialization' do
4
- app.middleware.insert_before Rails::Rack::Logger, Marlowe::Middleware, correlation_header: Rails.application.config.try(:marlowe_correlation_header)
4
+ class Railtie < Rails::Railtie # :nodoc:
5
+ initializer "marlowe.configure_rails_initialization" do
6
+ config = app.config
7
+
8
+ opts = {
9
+ header: config&.marlowe_header || config&.marlowe_correlation_header,
10
+ handler: config&.marlowe_request_id_handler,
11
+ return: config&.marlowe_return_request_id,
12
+ action_dispatch: config&.marlowe_replace_action_dispatch_request_id
13
+ }.compact
14
+
15
+ if opts[:action_dispatch]
16
+ app.middleware.insert_before ActionDispatch::RequestId, Marlowe::Middleware, opts
17
+ app.middleware.delete ActionDispatch::RequestId
18
+ else
19
+ app.middleware.insert_before Rails::Rack::Logger, Marlowe::Middleware, opts
20
+ end
5
21
  end
6
22
 
7
- #:nodoc:
8
- def app
23
+ def app # :nodoc:
9
24
  Rails.application
10
25
  end
11
26
  end
@@ -1,16 +1,15 @@
1
- require 'request_store'
1
+ # frozen_string_literal: true
2
+
3
+ require "request_store"
2
4
 
3
5
  module Marlowe
4
6
  # Marlowe::SimpleFormatter is a subclass of
5
7
  # +ActiveSupport::Logger::SimpleFormatter+ that adds a correlation id
6
8
  # string to a rails log.
7
9
  class SimpleFormatter < ActiveSupport::Logger::SimpleFormatter
8
-
9
10
  # Overrides the formatter return to add the correlation id.
10
11
  def call(severity, timestamp, progname, msg)
11
12
  "[#{RequestStore.store[:correlation_id]}] #{super}"
12
13
  end
13
14
  end
14
15
  end
15
-
16
-
data/lib/marlowe.rb CHANGED
@@ -1,10 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Marlowe, a correlation id injector.
2
4
  module Marlowe
3
- VERSION = '1.0.3' #:nodoc:
5
+ VERSION = "3.0" # :nodoc:
6
+
7
+ require "marlowe/config"
8
+ require "marlowe/middleware"
9
+ require "marlowe/rails" if defined? Rails::Railtie
10
+
11
+ autoload :Formatter, "marlowe/formatter"
12
+ autoload :SimpleFormatter, "marlowe/simple_formatter"
13
+
14
+ class << self
15
+ # Configure Marlowe
16
+ def configure(&block)
17
+ Marlowe::Config.configure(&block)
18
+ end
19
+
20
+ # Make a Marlowe request ID
21
+ def make_request_id(request_id, config = Marlowe::Config.global)
22
+ if config.handler == :simple
23
+ simple(request_id)
24
+ elsif config.handler.is_a?(Proc)
25
+ simple(config.handler.call(request_id))
26
+ else
27
+ clean(request_id)
28
+ end
29
+ end
30
+
31
+ private
4
32
 
5
- require 'marlowe/middleware'
6
- require 'marlowe/rails' if defined? Rails::Railtie
33
+ def clean(request_id)
34
+ simple(request_id).gsub(/[^\w\-]/, "")[0, 255]
35
+ end
7
36
 
8
- autoload :Formatter, 'marlowe/formatter'
9
- autoload :SimpleFormatter, 'marlowe/simple_formatter'
37
+ def simple(request_id)
38
+ if request_id && !request_id.empty? && request_id !~ /\A[[:space]]*\z/
39
+ request_id
40
+ else
41
+ SecureRandom.uuid
42
+ end
43
+ end
44
+ end
10
45
  end
@@ -1,22 +1,11 @@
1
- # -*- ruby encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
- gem 'minitest'
3
+ gem "minitest"
4
4
 
5
- require 'rack/test'
6
- require 'rack/mock'
7
- require 'minitest/autorun'
8
- require 'minitest/focus'
9
- require 'minitest/moar'
10
- require 'minitest/bisect'
5
+ require "rack/test"
6
+ require "rack/mock"
7
+ require "minitest/autorun"
8
+ require "minitest/focus"
9
+ require "minitest/moar"
11
10
 
12
- require 'request_store'
13
- require 'marlowe'
14
-
15
- class RackApp
16
- def call(env)
17
- end
18
-
19
- def coordination_id
20
- RequestStore[:correlation_id]
21
- end
22
- end
11
+ require "marlowe"
data/test/test_marlowe.rb CHANGED
@@ -1,32 +1,103 @@
1
- require 'minitest_config'
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_config"
2
4
 
3
5
  class TestMarlowe < Minitest::Test
6
+ include Rack::Test::Methods
7
+
8
+ attr_reader :marlowe_options
9
+
4
10
  def setup
5
- @app = RackApp.new
6
- @middleware = Marlowe::Middleware.new(@app)
11
+ @marlowe_options = {}
12
+ Marlowe::Config.send(:clear_global!)
13
+ end
14
+
15
+ def app
16
+ options = marlowe_options
17
+ Rack::Builder.new do
18
+ use Marlowe::Middleware, options
19
+
20
+ run lambda { |_env|
21
+ [
22
+ 200,
23
+ {"Content-Type" => "text/plain"},
24
+ [RequestStore[:correlation_id]]
25
+ ]
26
+ }
27
+ end
28
+ end
29
+
30
+ def test_default_config_no_header_value
31
+ get "/"
32
+ assert last_response.header.key?("X-Request-Id")
33
+ refute_empty last_response.header["X-Request-Id"]
34
+ assert_equal last_response.header["X-Request-Id"], last_response.body
35
+ end
36
+
37
+ def test_default_config_with_header_value
38
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "testvalue"}
39
+ assert last_response.header.key?("X-Request-Id")
40
+ refute_empty last_response.header["X-Request-Id"]
41
+ assert_equal last_response.header["X-Request-Id"], last_response.body
42
+ assert_equal "testvalue", last_response.header["X-Request-Id"]
43
+ end
44
+
45
+ def test_header_config_no_header_value
46
+ marlowe_options[:header] = "Correlation-Id"
47
+ get "/"
48
+ assert last_response.header.key?("Correlation-Id")
49
+ refute_empty last_response.header["Correlation-Id"]
50
+ assert_equal last_response.header["Correlation-Id"], last_response.body
51
+ end
52
+
53
+ def test_header_config_no_header_with_header_value
54
+ marlowe_options[:header] = "Correlation-Id"
55
+ get "/", {}, {"HTTP_CORRELATION_ID" => "testvalue"}
56
+ assert last_response.header.key?("Correlation-Id")
57
+ refute_empty last_response.header["Correlation-Id"]
58
+ assert_equal last_response.header["Correlation-Id"], last_response.body
59
+ assert_equal "testvalue", last_response.header["Correlation-Id"]
60
+ end
61
+
62
+ def test_handler_config_default_handler
63
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
64
+ assert last_response.header.key?("X-Request-Id")
65
+ refute_empty last_response.header["X-Request-Id"]
66
+ assert_equal last_response.header["X-Request-Id"], last_response.body
67
+ assert_equal "testvalue", last_response.header["X-Request-Id"]
7
68
  end
8
69
 
9
- def test_no_header
10
- @middleware.call({})
11
- refute_empty @app.coordination_id
70
+ def test_handler_config_with_simple_handler
71
+ marlowe_options[:handler] = :simple
72
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
73
+ assert last_response.header.key?("X-Request-Id")
74
+ refute_empty last_response.header["X-Request-Id"]
75
+ assert_equal last_response.header["X-Request-Id"], last_response.body
76
+ assert_equal "test+value", last_response.header["X-Request-Id"]
12
77
  end
13
78
 
14
- def test_with_header
15
- @middleware.call({'HTTP_CORRELATION_ID' => 'testvalue'})
16
- refute_empty @app.coordination_id
17
- assert_equal 'testvalue', @app.coordination_id
79
+ def test_handler_config_with_proc_handler
80
+ marlowe_options[:handler] = ->(item) { item && item.reverse || SecureRandom.uuid }
81
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
82
+ assert last_response.header.key?("X-Request-Id")
83
+ refute_empty last_response.header["X-Request-Id"]
84
+ assert_equal last_response.header["X-Request-Id"], last_response.body
85
+ assert_equal "eulav+tset", last_response.header["X-Request-Id"]
18
86
  end
19
87
 
20
- def test_with_custom_no_header
21
- @customized_middleware = Marlowe::Middleware.new(@app, correlation_header: "Custom-Header")
22
- @customized_middleware.call({})
23
- refute_empty @app.coordination_id
88
+ def test_handler_config_with_proc_handler_returning_nil
89
+ marlowe_options[:handler] = ->(_item) {}
90
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
91
+ assert last_response.header.key?("X-Request-Id")
92
+ refute_empty last_response.header["X-Request-Id"]
93
+ assert_equal last_response.header["X-Request-Id"], last_response.body
94
+ assert_match(/\A[-\w]+\z/, last_response.header["X-Request-Id"])
24
95
  end
25
96
 
26
- def test_with_custom_header
27
- @customized_middleware = Marlowe::Middleware.new(@app, correlation_header: "Custom-Header")
28
- @customized_middleware.call({'HTTP_CUSTOM_HEADER' => 'testvalue'})
29
- refute_empty @app.coordination_id
30
- assert_equal 'testvalue', @app.coordination_id
97
+ def test_return_config_false
98
+ marlowe_options[:return] = false
99
+ get "/"
100
+ refute last_response.header.key?("X-Request-Id")
101
+ assert_equal RequestStore[:correlation_id], last_response.body
31
102
  end
32
103
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_config"
4
+
5
+ class TestMarloweConfig < Minitest::Test
6
+ include Rack::Test::Methods
7
+
8
+ attr_reader :marlowe_options
9
+
10
+ def setup
11
+ @marlowe_options = {}
12
+ Marlowe::Config.send(:clear_global!)
13
+ end
14
+
15
+ def app
16
+ Marlowe.configure do |config|
17
+ marlowe_options.each do |k, v|
18
+ config.send(:"#{k}=", v) if config.respond_to?(:"#{k}=")
19
+ end
20
+ end
21
+
22
+ options = marlowe_options
23
+ Rack::Builder.new do
24
+ use Marlowe::Middleware, options
25
+
26
+ run lambda { |_env|
27
+ [
28
+ 200,
29
+ {"Content-Type" => "text/plain"},
30
+ [RequestStore[:correlation_id]]
31
+ ]
32
+ }
33
+ end
34
+ end
35
+
36
+ def test_default_config_no_header_value
37
+ get "/"
38
+ assert last_response.header.key?("X-Request-Id")
39
+ refute_empty last_response.header["X-Request-Id"]
40
+ assert_equal last_response.header["X-Request-Id"], last_response.body
41
+ end
42
+
43
+ def test_default_config_with_header_value
44
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "testvalue"}
45
+ assert last_response.header.key?("X-Request-Id")
46
+ refute_empty last_response.header["X-Request-Id"]
47
+ assert_equal last_response.header["X-Request-Id"], last_response.body
48
+ assert_equal "testvalue", last_response.header["X-Request-Id"]
49
+ end
50
+
51
+ def test_header_config_no_header_value
52
+ marlowe_options[:header] = "Correlation-Id"
53
+ get "/"
54
+ assert last_response.header.key?("Correlation-Id")
55
+ refute_empty last_response.header["Correlation-Id"]
56
+ assert_equal last_response.header["Correlation-Id"], last_response.body
57
+ end
58
+
59
+ def test_header_config_no_header_with_header_value
60
+ marlowe_options[:header] = "Correlation-Id"
61
+ get "/", {}, {"HTTP_CORRELATION_ID" => "testvalue"}
62
+ assert last_response.header.key?("Correlation-Id")
63
+ refute_empty last_response.header["Correlation-Id"]
64
+ assert_equal last_response.header["Correlation-Id"], last_response.body
65
+ assert_equal "testvalue", last_response.header["Correlation-Id"]
66
+ end
67
+
68
+ def test_handler_config_default_handler
69
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
70
+ assert last_response.header.key?("X-Request-Id")
71
+ refute_empty last_response.header["X-Request-Id"]
72
+ assert_equal last_response.header["X-Request-Id"], last_response.body
73
+ assert_equal "testvalue", last_response.header["X-Request-Id"]
74
+ end
75
+
76
+ def test_handler_config_with_simple_handler
77
+ marlowe_options[:handler] = :simple
78
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
79
+ assert last_response.header.key?("X-Request-Id")
80
+ refute_empty last_response.header["X-Request-Id"]
81
+ assert_equal last_response.header["X-Request-Id"], last_response.body
82
+ assert_equal "test+value", last_response.header["X-Request-Id"]
83
+ end
84
+
85
+ def test_handler_config_with_proc_handler
86
+ marlowe_options[:handler] = ->(item) { item && item.reverse || SecureRandom.uuid }
87
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
88
+ assert last_response.header.key?("X-Request-Id")
89
+ refute_empty last_response.header["X-Request-Id"]
90
+ assert_equal last_response.header["X-Request-Id"], last_response.body
91
+ assert_equal "eulav+tset", last_response.header["X-Request-Id"]
92
+ end
93
+
94
+ def test_handler_config_with_proc_handler_returning_nil
95
+ marlowe_options[:handler] = ->(_item) {}
96
+ get "/", {}, {"HTTP_X_REQUEST_ID" => "test+value"}
97
+ assert last_response.header.key?("X-Request-Id")
98
+ refute_empty last_response.header["X-Request-Id"]
99
+ assert_equal last_response.header["X-Request-Id"], last_response.body
100
+ assert_match(/\A[-\w]+\z/, last_response.header["X-Request-Id"])
101
+ end
102
+
103
+ def test_return_config_false
104
+ marlowe_options[:return] = false
105
+ get "/"
106
+ refute last_response.header.key?("X-Request-Id")
107
+ assert_equal RequestStore[:correlation_id], last_response.body
108
+ end
109
+ end