# frozen_string_literal: true require 'rack' require 'request_store' require 'securerandom' module Marlowe # Marlowe correlation id middleware. Including this into your middleware # stack will add a correlation id header as an incoming request, and save # that id in a request session variable. class Middleware # The name of the default header to look for and put the correlation id in. CORRELATION_HEADER = 'X-Request-Id' #:nodoc: # Configure the Marlowe middleware to call +app+ with options +opts+. # # === Options # # :header:: The name of the header to inspect. Defaults to # 'X-Request-Id'. Also available as # :correlation_header. # :handler:: The handler for request correlation IDs. Defaults to # sanitizing provided request IDs or generating a UUID. # If :simple is provided, provided request IDs # will not be sanitized. A callable (expecting a single # input of any possible existing request ID) may be # provided to introduce more complex request ID # handling. # :return:: If +true+ (the default), the request correlation ID # will be returned as part of the response headers. # :action_dispatch:: If +true+, Marlowe will add code to behave # like ActionDispatch::RequestId. # Depends on ActionDispatch::Request. def initialize(app, opts = {}) @app = app @header, @http_header = format_header_name( opts[:header] || opts[:correlation_header] || CORRELATION_HEADER ) @handler = opts.fetch(:handler, :clean) @return = opts.fetch(:return, true) @action_dispatch = opts.fetch(:action_dispatch, false) end # Stores the incoming correlation id from the +env+ hash. If the correlation # id has not been sent, a new UUID is generated and the +env+ is modified. def call(env) req_id = make_request_id(env[@http_header]) RequestStore.store[:correlation_id] = env[@http_header] = req_id if @action_dispatch req = ActionDispatch::Request.new(env) req.request_id = req_id end @app.call(env).tap { |_status, headers, _body| if @return headers[@header] = if @action_dispatch req.request_id else RequestStore.store[:correlation_id] end end } end private def format_header_name(header) [ header.to_s.tr('_', '-').freeze, "HTTP_#{header.to_s.tr('-', '_').upcase}" ] end def make_request_id(request_id) if @handler == :simple simple(request_id) elsif @handler.kind_of?(Proc) simple(@handler.call(request_id)) else clean(request_id) end end def clean(request_id) simple(request_id).gsub(/[^\w\-]/, '')[0, 255] end def simple(request_id) if request_id && !request_id.empty? && request_id !~ /\A[[:space]]*\z/ request_id else SecureRandom.uuid end end end end