marlowe 1.0.3 → 3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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