lib/aws/core/log_formatter.rb



# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

module AWS
  module Core

    # = Log Formatters
    #
    # Log formatters receive a {AWS::Core::Response} object and return
    # a log message.  When you construct a {LogFormatter}, you provide
    # a pattern string with substitutions.
    #
    #   pattern = '[REQUEST :http_status_code] :service :operation :duration'
    #   formatter = AWS::Core::LogFormatter.new(pattern)
    #   formatter.format(response)
    #   #=> '[AWS 200] EC2 get_bucket 0.0352'
    #
    # = AWS Configuration
    #
    # AWS.config provides a {LogFormatter.default} log formatter.  You can
    # repace this formatter by building your own and then passing it
    # to {AWS.config}.
    #
    #   pattern = '[REQUEST :http_status_code] :service :operation :duration'
    #   AWS.config(:log_formatter => AWS::Core::LogFormatter.new(pattern)
    #
    # == Canned Formatters
    #
    # Instead of providing your own pattern, you can choose a canned log
    # formatter.
    #
    #   AWS.config(:log_formatter => AWS::Core::LogFormatter.colored)
    #
    # Here is the list of canned formatters.
    #
    # * {LogFormatter.default}
    # * {LogFormatter.short}
    # * {LogFormatter.debug}
    # * {LogFormatter.colored}
    #
    # = Pattern Substitutions
    # 
    # You can put any of these placeholders into you pattern.  
    #
    # [+:service+]
    #   The AWS service name (e.g. 'S3', 'EC2', 'SimpleDB', etc)
    #
    # [+:region+]
    #   The AWS region name (e.g. 'us-east-1', 'us-west-1', etc)
    #
    # [+:operation+]
    #   The name of the client request method.  This maps to the name of 
    #   the serivce API operation (e.g. :describe_instances).
    #
    # [+:options+] 
    #   The hash of options passed to the client request method.
    #   Long strings are truncated/summarized if they excede the log
    #   formatters {#max_string_size}.  Other objects are inspected.
    # 
    # [+:retry_count+] 
    #   The number of times a client request was retried.
    #   Throttlings and service errors trigger the automatic retry logic.
    #   This value indicates how many extra attempts were made before
    #   getting a successful response or giving up.
    #
    # [+:duration+] 
    #   The time it took to generate a response, expressed
    #   in decimal seconds.  This time includes everything from
    #   calling the client request method, until that method returns
    #   a value (event retries and retry delays).
    #
    # [+:error_class+] 
    #   The class name of the error returned by the 
    #   service.  If no error was returned, this will be replcaed by
    #   an empty string.
    #
    # [+:error_message+]  
    #   The message of the error returned.  If
    #   no error was returned by the service, this will be an empty
    #   string.
    #
    # [+:http_request_method+]
    #   The HTTP request verb (e.g. 'POST', 'PUT', 'GET', etc).
    #
    # [+:http_request_protocol+] 
    #   This is replaced by 'http' or 'https'.
    #
    # [+:http_request_host+]  
    #   The host name of the http request endpoint (e.g. 's3.amazon.com').
    #
    # [+:http_request_port+] 
    #   The port number (e.g. '443' or '80').
    #
    # [+:http_request_uri+]
    #   The http request uri folling the host (e.g. 
    #   '/bucket_name/objects/key?versions').
    #
    # [+:http_request_body+]
    #   The http request payload.
    #
    # [+:http_request_headers+]
    #   The http request headers, inspected.
    #
    # [+:http_request_proxy_uri+]
    #   The proxy uri used, or an empty string.
    #
    # [+:http_response_status+]
    #   The http response status code (e.g. '200', '404', '500', etc).
    #
    # [+:http_response_headers+]
    #   The http response headers, inspected.
    #
    # [+:http_response_body+]
    #   The http response body contents.
    # 
    class LogFormatter

      # @param [String] pattern The log format pattern should be a string
      #   and may contain any of the following placeholders:
      #
      #   * +:service+
      #   * +:region+
      #   * +:operation+
      #   * +:options+
      #   * +:retry_count
      #   * +:duration+
      #   * +:error_class+
      #   * +:error_message+
      #   * +:http_request_method+
      #   * +:http_request_protocol+
      #   * +:http_request_host+
      #   * +:http_request_port+
      #   * +:http_request_uri+
      #   * +:http_request_body+
      #   * +:http_request_headers+
      #   * +:http_request_proxy_uri+
      #   * +:http_response_status+
      #   * +:http_response_headers+
      #   * +:http_response_body+
      #
      # @param [Hash] options
      #
      # @option options [Integer] :max_string_size (1000)
      #
      def initialize pattern, options = {}
        @pattern = pattern
        @max_string_size = options[:max_string_size] || 1000
      end

      # @return [String]
      attr_reader :pattern

      # @return [Integer]
      attr_reader :max_string_size

      # @param [Response] response
      # @return [String]
      def format response
        pattern.gsub(/:(\w+)/) {|sym| send("_#{sym[1..-1]}", response) }
      end

      protected

      def method_missing method_name, *args
        if method_name.to_s.chars.first == '_'
          ":#{method_name.to_s[1..-1]}"
        else
          super
        end
      end

      def _service response
        response.http_request.class.name.split('::')[1]
      end

      def _region response
        response.http_request.region
      end

      def _operation response
        response.request_type
      end

      def _options response
        summarize_hash(response.request_options) if response.request_options
      end

      def _retry_count response
        response.retry_count
      end

      def _duration response
        ("%.06f" % response.duration).sub(/0+$/, '')
      end

      def _error_class response
        response.error.class.name if response.error
      end

      def _error_message response
        response.error.message if response.error
      end

      def _http_request_method response
        response.http_request.http_method
      end

      def _http_request_protocol response
        response.http_request.use_ssl? ? 'https' : 'http'
      end

      def _http_request_host response
        response.http_request.host
      end

      def _http_request_port response
        response.http_request.port
      end

      def _http_request_uri response
        response.http_request.uri
      end

      def _http_request_headers response
        response.http_request.headers.inspect
      end

      def _http_request_body response
        response.http_request.body
      end

      def _http_request_proxy_uri response
        response.http_request.proxy_uri
      end

      def _http_response_status response
        response.http_response.status
      end

      def _http_response_headers response
        response.http_response.headers.inspect
      end

      def _http_response_body response
        response.http_response.body
      end

      # The following methods are for summarizing request options that have
      # symbolized keys and a mix of values.  The summarizing is important
      # because large request options (e.g. s3 data) can cause memory usage
      # to spike if it is inspected.

      # @param [Hash] hash
      # @return [String]
      def summarize_hash hash
        hash.map do |key,v|
          ":#{key}=>#{summarize_value(v)}"
        end.sort.join(",")
      end

      # @param [Object] value
      # @return [String]
      def summarize_value value
        case value
        when String   then summarize_string(value)
        when Hash     then '{' + summarize_hash(value) + '}'
        when Array    then summarize_array(value)
        when File     then summarize_file(value.path)
        when Pathname then summarize_file(value)
        else value.inspect
        end
      end
  
      # @param [String] str
      # @return [String]
      def summarize_string str
        max = max_string_size
        if str.size > max
          "#<String #{str[0...max].inspect} ... (#{str.size} bytes)>"
        else
          str.inspect
        end
      end

      # Given the path to a file on disk, this method returns a summarized
      # inspecton string that includes the file size.
      # @param [String] path
      # @return [String]
      def summarize_file path
        "#<File:#{path} (#{File.size(path)} bytes)>"
      end
  
      # @param [Array] array
      # @return [String]
      def summarize_array array
        "[" + array.map{|v| summarize_value(v) }.join(",") + "]"
      end

      class << self
      
        # The default log format.
        #
        # @example A sample of the default format.
        #
        #   [AWS SimpleEmailService 200 0.580066 0 retries] list_verified_email_addresses()
        # 
        # @return [LogFormatter]
        #
        def default

          pattern = []
          pattern << "[AWS"
          pattern << ":service"
          pattern << ":http_response_status"
          pattern << ":duration"
          pattern << ":retry_count retries]"
          pattern << ":operation(:options)"
          pattern << ":error_class"
          pattern << ":error_message"

          LogFormatter.new(pattern.join(' ') + "\n")

        end

        # The short log format.  Similar to default, but it does not
        # inspect the request params or report on retries.
        #
        # @example A sample of the short format
        #
        #   [AWS SimpleEmailService 200 0.494532] list_verified_email_addresses
        # 
        # @return [LogFormatter]
        #
        def short

          pattern = []
          pattern << "[AWS"
          pattern << ":service"
          pattern << ":http_response_status"
          pattern << ":duration]"
          pattern << ":operation"
          pattern << ":error_class"

          LogFormatter.new(pattern.join(' ') + "\n")

        end

        # A debug format that dumps most of the http request and response
        # data.
        #
        # @example A truncated sample of the debug format.
        #
        #   +-------------------------------------------------------------------------------
        #   | AWS us-east-1 SimpleEmailService list_verified_email_addresses 0.429189 0 retries
        #   +-------------------------------------------------------------------------------
        #   |   REQUEST
        #   +-------------------------------------------------------------------------------
        #   |    METHOD: POST
        #   |       URL: https://email.us-east-1.amazonaws.com::443:/
        #   |   HEADERS: {"content-type"=>"application/x-www-form-urlencoded" ... 
        #   |      BODY: Action=ListVerifiedEmailAddresses&Timestamp= ...
        #   +-------------------------------------------------------------------------------
        #   |  RESPONSE
        #   +-------------------------------------------------------------------------------
        #   |    STATUS: 200
        #   |   HEADERS: {"x-amzn-requestid"=>["..."], ...
        #   |      BODY: <ListVerifiedEmailAddressesResponse ...
        #   
        # @return [LogFormatter]
        #
        def debug

          sig_pattern = []
          sig_pattern << ':region'
          sig_pattern << ':service'
          sig_pattern << ':operation'
          sig_pattern << ':duration'
          sig_pattern << ':retry_count retries'

          uri_pattern = []
          uri_pattern << ':http_request_protocol'
          uri_pattern << '://'
          uri_pattern << ':http_request_host'
          uri_pattern << '::'
          uri_pattern << ':http_request_port'
          uri_pattern << ':'
          uri_pattern << ':http_request_uri'

          line = "+" + '-' * 79 

          pattern = []
          pattern << line
          pattern << "| AWS #{sig_pattern.join(' ')}"
          pattern << line
          pattern << "|   REQUEST"
          pattern << line
          pattern << "|    METHOD: :http_request_method"
          pattern << "|       URL: #{uri_pattern.join}"
          pattern << "|   HEADERS: :http_request_headers"
          pattern << "|      BODY: :http_request_body"
          pattern << line
          pattern << "|  RESPONSE"
          pattern << line
          pattern << "|    STATUS: :http_response_status"
          pattern << "|   HEADERS: :http_response_headers"
          pattern << "|      BODY: :http_response_body"

          LogFormatter.new(pattern.join("\n") + "\n")

        end

        # The default log format with ANSI colors.
        #
        # @example A sample of the colored format (sans the ansi colors).
        #
        #   [AWS SimpleEmailService 200 0.580066 0 retries] list_verified_email_addresses()
        # 
        # @return [LogFormatter]
        #
        def colored

          bold = "\x1b[1m"
          color = "\x1b[34m"
          reset = "\x1b[0m"

          pattern = []
          pattern << "#{bold}#{color}[AWS"
          pattern << ":service"
          pattern << ":http_response_status"
          pattern << ":duration"
          pattern << ":retry_count retries]#{reset}#{bold}"
          pattern << ":operation(:options)"
          pattern << ":error_class"
          pattern << ":error_message#{reset}"

          LogFormatter.new(pattern.join(' ') + "\n")

        end

      end

    end
  end
end