Class: RightSupport::Rack::RequestTracker

Inherits:
Object
  • Object
show all
Defined in:
lib/right_support/rack/request_tracker.rb

Overview

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).

Constant Summary collapse

Generator =

shorthand

::RightSupport::Data::Token
REQUEST_ID_HEADER =

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.

'X-Request-Id'.freeze
HTTP_REQUEST_ID_HEADER =

incoming header as found in Rack env hash, if any.

'HTTP_X_REQUEST_ID'.freeze
REQUEST_UUID_HEADER =

LEGACY: still supported but new code should use the standard (see above).

"X-Request-Uuid".freeze
HTTP_REQUEST_UUID_HEADER =

LEGACY: incoming header as found in Rack env hash, if any.

'HTTP_X_REQUEST_UUID'.freeze
REQUEST_UUID_ENV_NAME =

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.

'rack.request_uuid'.freeze
API_CALL_COUNTER_ENV_KEY =

records count of additional API calls made by an application while handling a request, etc. this is useful for generating an incremental request [UU]ID to be forwarded to other services for additional API calls.

See Also:

'request_tracker.api_call_counter'
REQUEST_LINEAGE_UUID_HEADER =
Deprecated.

do not send the lineage header as support may go away.

'HTTP_X_REQUEST_LINEAGE_UUID'.freeze
UUID_SEPARATOR =
Deprecated.

do not send the lineage header as support may go away.

' '.freeze
MAX_REQUEST_UUID_LENGTH =

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.

128

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ RequestTracker

Returns a new instance of RequestTracker.

Parameters:

  • app (Object)

    as next middleware or the rack application



72
73
74
# File 'lib/right_support/rack/request_tracker.rb', line 72

def initialize(app)
  @app = app
end

Class Method Details

.copy_request_uuid(from_env, to_headers, options = {}) ⇒ Hash|Array

copies the request [UU]ID from the request environment to the given hash, if present.

Parameters:

  • from_env (Hash)

    as source hash

  • to_headers (Hash)

    as target

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :generator (Proc|Method)

    as callback to generate a new request uuid when not present or nil

  • :modifier (Proc|Method)

    as callback to modify request uuid before it is finally inserted in headers or nil. this can be useful for appending a counter for each new API call made by a request handler, etc. only called if identifier was retrieved from the environment hash because an explicity-set (or generated) ID value is used without modification.

  • :return_request_uuid (TrueClass|FalseClass)

    as true to return a tuple of [to_headers, request_uuid] instead. false to only return updated to_headers (default).

Returns:

  • (Hash|Array)

    updated to_headers (default) or tuple of [to_headers, request_uuid]. see :return_request_uuid option.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/right_support/rack/request_tracker.rb', line 146

def self.copy_request_uuid(from_env, to_headers, options = {})
  options = {
    generator: nil,
    modifier: nil,
    return_request_uuid: false
  }.merge(options)
  from_env ||= {}
  to_headers ||= {}

  # keep existing ID value, if any.
  request_uuid = ::RightSupport::Data::HashTools.header_value_get(
    to_headers,
    REQUEST_ID_HEADER)
  unless request_uuid
    # attempt to find ID key in environment hash.
    if request_uuid = from_env[REQUEST_UUID_ENV_NAME]
      # use modifier, if any.
      if modifier = options[:modifier]
        request_uuid = modifier.call(request_uuid)
      end
    else
      # use generator, if any.
      if generator = options[:generator]
        request_uuid = generator.call
      end
    end

    # note that we will 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 if request_uuid
  end
  return [to_headers, request_uuid] if options[:return_request_uuid]
  to_headers
end

.detect_request_uuid(env) ⇒ Array

detects whether the incoming env hash contains a request ID is some form and generates a new ID when missing.

Returns:

  • (Array)

    tuple of detected/generated ID and the name of the header to use to represent the ID in a response.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/right_support/rack/request_tracker.rb', line 96

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

.generate_request_uuidString

generates a token (nicer than an actual UUID for logging) but we continue to refer to it as the “request UUID” for legacy reasons.

Returns:



186
187
188
# File 'lib/right_support/rack/request_tracker.rb', line 186

def self.generate_request_uuid
  Generator.generate
end

.next_request_uuid(env, base_request_uuid = nil) ⇒ String

Returns next request [UU]ID.

Returns:

  • (String)

    next request [UU]ID



191
192
193
194
195
196
# File 'lib/right_support/rack/request_tracker.rb', line 191

def self.next_request_uuid(env, base_request_uuid = nil)
  base_request_uuid ||= env[REQUEST_UUID_ENV_NAME] || 'missing'
  env[API_CALL_COUNTER_ENV_KEY] ||= 0
  n = env[API_CALL_COUNTER_ENV_KEY] += 1
  "#{base_request_uuid}_#{n}"
end

Instance Method Details

#call(env) ⇒ Array

request tracking.

Parameters:

  • env (Hash)

    from preceding middleware

Returns:

  • (Array)

    tuple of [status, headers, body]



81
82
83
84
85
86
87
88
89
# File 'lib/right_support/rack/request_tracker.rb', line 81

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