# # Copyright (c) 2011-2016 RightScale Inc # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. module RightSupport::Rack # middleware to detect a request ID header or generate a new ID for each # incoming request. the purpose is to track a request lineage throughout a # chain of internal API calls and, in some cases, out to external APIs that # also support the X-Request-ID header as a de-facto standard (google it). class RequestTracker # shorthand Generator = ::RightSupport::Data::Token # used by many public services to represent a client-generated request ID # that can be tracked and safely logged throughout the lineage of a request. # this is also supported by goa middleware. REQUEST_ID_HEADER = 'X-Request-Id'.freeze # incoming header as found in Rack env hash, if any. HTTP_REQUEST_ID_HEADER = 'HTTP_X_REQUEST_ID'.freeze # LEGACY: still supported but new code should use the standard (see above). REQUEST_UUID_HEADER = "X-Request-Uuid".freeze # LEGACY: incoming header as found in Rack env hash, if any. HTTP_REQUEST_UUID_HEADER = 'HTTP_X_REQUEST_UUID'.freeze # LEGACY: refers to the generated or passed-in request ID. the key has been # hardcoded in some places so is not easy to change and/or there is not much # value to finding and replacing with _id in all cases. REQUEST_UUID_ENV_NAME = 'rack.request_uuid'.freeze # @deprecated do not send the lineage header as support may go away. REQUEST_LINEAGE_UUID_HEADER = 'HTTP_X_REQUEST_LINEAGE_UUID'.freeze # @deprecated do not send the lineage header as support may go away. UUID_SEPARATOR = ' '.freeze # limit request [UU]ID to something reasonable to keep our logs from # overflowing on bad user-provided IDs. we do not want to be too restrictive # in case people are encoding some useful information, etc. the issue is # that the request ID will tend to show up a lot in logs. MAX_REQUEST_UUID_LENGTH = 128 # @param [Object] app as next middleware or the rack application def initialize(app) @app = app end # request tracking. # # @param [Hash] env from preceding middleware # # @return [Array] tuple of [status, headers, body] def call(env) request_uuid, response_header_name = self.class.detect_request_uuid(env) env[REQUEST_UUID_ENV_NAME] = request_uuid status, headers, body = @app.call(env) headers[response_header_name] = request_uuid [status, headers, body] end # detects whether the incoming env hash contains a request ID is some form # and generates a new ID when missing. # # @return [Array] tuple of detected/generated ID and the name of the header # to use to represent the ID in a response. def self.detect_request_uuid(env) request_uuid = '' response_header_name = nil { HTTP_REQUEST_ID_HEADER => REQUEST_ID_HEADER, HTTP_REQUEST_UUID_HEADER => REQUEST_UUID_HEADER, REQUEST_LINEAGE_UUID_HEADER => REQUEST_UUID_HEADER }.each do |in_key, out_key| if env.has_key?(in_key) request_uuid = env[in_key].to_s.strip response_header_name = out_key break end end # for legacy reasons we default to the -UUID header in response for newly- # generated IDs. we will use the -ID standard if that was passed-in. once # all apps are updated you will mostly see -ID in response except for API # calls initiated by browser code. the javascript can gradually be changed # to send -ID with requests as well. if request_uuid.empty? request_uuid = generate_request_uuid response_header_name = REQUEST_UUID_HEADER else # truncate, if necessary. request_uuid = request_uuid[0, MAX_REQUEST_UUID_LENGTH] end return request_uuid, response_header_name end # copies the request [UU]ID from the request environment to the given hash, # if present. does nothing if request [UU]ID is not set (because middleware # was not present, etc.) # # @param [Hash] from_env as source # @param [Hash] to_headers as target # # @return [Hash] updated headers def self.copy_request_uuid(from_env, to_headers) to_headers ||= {} if from_env if request_uuid = from_env[REQUEST_UUID_ENV_NAME] # note we always forward the _ID header as the standard. none of the # RS code ever actually accepted the _UUID header so that is a # non-issue. to_headers[REQUEST_ID_HEADER] = request_uuid end end to_headers end # generates a token (nicer than an actual UUID for logging) but we continue # to refer to it as the "request UUID" for legacy reasons. # # @return [String] a new token def self.generate_request_uuid Generator.generate end end # RequestTracker end # RightSupport::Rack