class MashapeAnalytics::Frameworks::Rack

Public Class Methods

new(app, options = {}) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 17
def initialize(app, options = {})
  @app = app
  @service_token = options[:service_token]
  @environment = options[:environment] || ''
  @send_body = options[:send_body] || false
  host = options[:host] || 'tcp://socket.analytics.mashape.com:5500'

  MashapeAnalytics::Capture.setOptions(host: host)
end

Public Instance Methods

call(env) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 27
def call(env)
  startedDateTime = Time.now
  status, headers, body = @app.call(env)

  if body.respond_to? :to_str
    response_body = [body.to_str]
  elsif body.respond_to?(:body)
    response_body = [body.body]
  elsif body.respond_to?(:each)
    response_body = body
  else
    raise TypeError, "stringable or iterable required"
  end

  record_alf startedDateTime, env, {
    :status => status,
    :headers => header_hash(headers),
    :body => response_body
  }

  [status, headers, body]
end

Protected Instance Methods

absolute_url(request) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 70
def absolute_url(request)
  "#{request['rack.url_scheme']}://#{host(request)}#{url(request)}"
end
host(request) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 51
def host(request)
  if forwarded = request['HTTP_X_FORWARDED_HOST']
    forwarded.split(/,\s?/).last
  elsif (request['rack.url_scheme'] == 'http' and request['SERVER_PORT'] == '80') or (request['rack.url_scheme'] == 'https' and request['SERVER_PORT'] == '443')
    request['HTTP_HOST'] || "#{request['SERVER_NAME'] || request['SERVER_ADDR']}"
  else
    request['HTTP_HOST'] || "#{request['SERVER_NAME'] || request['SERVER_ADDR']}:#{request['SERVER_PORT']}"
  end
end
record_alf(startedDateTime, request, response) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 158
def record_alf(startedDateTime, request, response)
  time = Time.now - startedDateTime
  alf = MashapeAnalytics::Message::Alf.new @service_token, @environment

  req_headers_size = request_header_size(request)
  req_content_size = request_content_size(request)

  res_headers_size = response_headers_size(response)
  res_content_size = response_content_size(response)

  entry = {
    startedDateTime: startedDateTime.iso8601,
    serverIPAddress: Socket.ip_address_list.detect{|intf| intf.ipv4_private?}.ip_address,
    time: (time * 1000).to_i,
    request: {
      method: request['REQUEST_METHOD'],
      url: absolute_url(request),
      httpVersion: 'HTTP/1.1', # not available, default http/1.1
      cookies: [],
      queryString: request_query_string(request),
      headers: request_headers(request),
      headersSize: req_headers_size,
      content: {
        size: req_content_size,
        mimeType: request['HTTP_CONTENT_TYPE'] || 'application/octet-stream'
      },
      bodySize: req_headers_size + req_content_size
    },
    response: {
      status: response[:status],
      statusText: status_code(response[:status]),
      httpVersion: 'HTTP/1.1', # not available, default http/1.1
      cookies: [],
      headers: response_headers(response),
      headersSize: res_headers_size,
      content: {
        size: res_content_size,
        mimeType: response[:headers]['Content-Type'] || 'application/octet-stream'
      },
      bodySize: res_headers_size + res_content_size,
      redirectURL: response[:headers]['Location'] || ''
    },
    cache: {},
    timings: {
      blocked: -1,
      dns: -1,
      connect: -1,
      send: 0,
      wait: (time * 1000).to_i,
      receive: 0,
      ssl: -1
    }
  }

  if @send_body
    require 'base64'
    entry[:request][:content][:encoding] = 'base64'
    request['rack.input'].rewind
    entry[:request][:content][:text] = Base64.strict_encode64(request['rack.input'].read)


    # TODO Handle streams as well
    if response[:body].respond_to? :join
      entry[:response][:content][:encoding] = 'base64'
      entry[:response][:content][:text] = Base64.strict_encode64(response[:body].join())
    end
  end

  alf.add_entry entry
  MashapeAnalytics::Capture.record! alf
end
request_content_size(request) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 114
def request_content_size(request)
  if request['HTTP_CONTENT_LENGTH']
    request['HTTP_CONTENT_LENGTH'].to_i
  else
    if request['rack.input'].respond_to? :size
      request['rack.input'].size
    elsif request['rack.input'].respond_to? :write
      stream_size(request['rack.input'])
    else
      -1 # Not available
    end
  end
end
request_header_size(request) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 79
def request_header_size(request)
  # {METHOD} {URL} HTTP/1.1\r\n = 12 extra characters for space between method and url, and ` HTTP/1.1\r\n`
  first_line = request['REQUEST_METHOD'].length + url(request).length + 12

  # {KEY}: {VALUE}\n\r = 4 extra characters for `: ` and `\n\r` minus `HTTP_` in the KEY is -1
  header_fields = request.select { |k,v| k.start_with? 'HTTP_' }
    .map { |k,v| k.length + (v.respond_to?(:bytesize) ? v.bytesize : 0) - 1 }
    .inject(0) { |sum,v| sum + v }

  last_line = 2 # /r/n

  first_line + header_fields + last_line
end
request_headers(request) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 74
def request_headers(request)
  request.select {|k,v| k.start_with? 'HTTP_'}
    .map { |k,v| {name: k.sub(/^HTTP_/, '').sub(/_/, '-'), value: v} }
end
request_query_string(request) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 93
def request_query_string(request)
  request['QUERY_STRING'].split('&')
    .map do |q|
      parts = q.split('=')
      {name: parts.first, value: parts.length > 1 ? parts.last : nil }
    end
end
response_content_size(response) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 143
def response_content_size(response)
  # puts 'BODY: ' << response[:body]
  if response[:headers]['Content-Length']
    response[:headers]['Content-Length'].to_i
  else
    if response[:body].respond_to? :inject
      response[:body].inject(0) { |sum, b| sum + b.bytesize }
    elsif response[:body].respond_to? :write
      stream_size(response[:body])
    else
      -1 # Not available
    end
  end
end
response_headers(response) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 128
def response_headers(response)
  response[:headers].map { |k,v| {name: k, value: v} }
end
response_headers_size(response) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 132
def response_headers_size(response)
  # HTTP/1.1 {STATUS} {STATUS_TEXT} = 10 extra characters
  first_line = response[:status].to_s.length + status_code(response[:status]).length + 10

  # {KEY}: {VALUE}\n\r
  header_fields = response[:headers].map { |k,v| k.length + v.bytesize + 4 }
    .inject(0) { |sum,v| sum + v }

  return first_line + header_fields
end
stream_size(stream) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 101
def stream_size(stream)
  size = nil
  io = StringIO.new
  begin
    stream.write io
    io.flush
    size = io.size
  ensure
    io.close
  end
  size
end
url(request) click to toggle source
# File lib/mashape-analytics/frameworks/rack.rb, line 61
def url(request)
  query_string = ''
  if request['QUERY_STRING'] != '' and request['QUERY_STRING'] != nil
    query_string = '?' + request['QUERY_STRING']
  end

  "#{request['PATH_INFO']}#{query_string}"
end