File: parseable.rb

package info (click to toggle)
ruby-logging 2.2.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 660 kB
  • sloc: ruby: 6,139; sh: 11; makefile: 2
file content (297 lines) | stat: -rw-r--r-- 10,305 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
require 'socket'

module Logging::Layouts

  # Accessor for the Parseable layout.
  #
  def self.parseable
    ::Logging::Layouts::Parseable
  end

  # Factory for the Parseable layout using JSON formatting.
  #
  def self.json( *args )
    ::Logging::Layouts::Parseable.json(*args)
  end

  # Factory for the Parseable layout using YAML formatting.
  #
  def self.yaml( *args )
    ::Logging::Layouts::Parseable.yaml(*args)
  end

  # This layout will produce parseable log output in either JSON or YAML
  # format. This makes it much easier for machines to parse log files and
  # perform analysis on those logs.
  #
  # The information about the log event can be configured when the layout is
  # created. Any or all of the following labels can be set as the _items_ to
  # log:
  #
  #   'logger'     Used to output the name of the logger that generated the
  #                log event.
  #   'timestamp'  Used to output the timestamp of the log event.
  #   'level'      Used to output the level of the log event.
  #   'message'    Used to output the application supplied message
  #                associated with the log event.
  #   'file'       Used to output the file name where the logging request
  #                was issued.
  #   'line'       Used to output the line number where the logging request
  #                was issued.
  #   'method'     Used to output the method name where the logging request
  #                was issued.
  #   'hostname'   Used to output the hostname
  #   'pid'        Used to output the process ID of the currently running
  #                program.
  #   'millis'     Used to output the number of milliseconds elapsed from
  #                the construction of the Layout until creation of the log
  #                event.
  #   'thread_id'  Used to output the object ID of the thread that generated
  #                the log event.
  #   'thread'     Used to output the name of the thread that generated the
  #                log event. Name can be specified using Thread.current[:name]
  #                notation. Output empty string if name not specified. This
  #                option helps to create more human readable output for
  #                multithread application logs.
  #
  # These items are supplied to the layout as an array of strings. The items
  # 'file', 'line', and 'method' will only work if the Logger generating the
  # events is configured to generate tracing information. If this is not the
  # case these fields will always be empty.
  #
  # When configured to output log events in YAML format, each log message
  # will be formatted as a hash in it's own YAML document. The hash keys are
  # the name of the item, and the value is what you would expect it to be.
  # Therefore, for the default set of times log message would appear as
  # follows:
  #
  #   ---
  #   timestamp: 2009-04-17T16:15:42
  #   level: INFO
  #   logger: Foo::Bar
  #   message: this is a log message
  #   ---
  #   timestamp: 2009-04-17T16:15:43
  #   level: ERROR
  #   logger: Foo
  #   message: <RuntimeError> Oooops!!
  #
  # The output order of the fields is not guaranteed to be the same as the
  # order specified in the _items_ list. This is because Ruby hashes are not
  # ordered by default (unless you're running this in Ruby 1.9).
  #
  # When configured to output log events in JSON format, each log message
  # will be formatted as an object (in the JSON sense of the word) on it's
  # own line in the log output. Therefore, to parse the output you must read
  # it line by line and parse the individual objects. Taking the same
  # example above the JSON output would be:
  #
  #   {"timestamp":"2009-04-17T16:15:42","level":"INFO","logger":"Foo::Bar","message":"this is a log message"}
  #   {"timestamp":"2009-04-17T16:15:43","level":"ERROR","logger":"Foo","message":"<RuntimeError> Oooops!!"}
  #
  # The output order of the fields is guaranteed to be the same as the order
  # specified in the _items_ list.
  #
  class Parseable < ::Logging::Layout

    # :stopdoc:
    # Arguments to sprintf keyed to directive letters
    DIRECTIVE_TABLE = {
      'logger'    => 'event.logger'.freeze,
      'timestamp' => 'iso8601_format(event.time)'.freeze,
      'level'     => '::Logging::LNAMES[event.level]'.freeze,
      'message'   => 'format_obj(event.data)'.freeze,
      'file'      => 'event.file'.freeze,
      'line'      => 'event.line'.freeze,
      'method'    => 'event.method'.freeze,
      'hostname'  => "'#{Socket.gethostname}'".freeze,
      'pid'       => 'Process.pid'.freeze,
      'millis'    => 'Integer((event.time-@created_at)*1000)'.freeze,
      'thread_id' => 'Thread.current.object_id'.freeze,
      'thread'    => 'Thread.current[:name]'.freeze,
      'mdc'       => 'Logging::MappedDiagnosticContext.context'.freeze,
      'ndc'       => 'Logging::NestedDiagnosticContext.context'.freeze
    }

    # call-seq:
    #    Pattern.create_yaml_format_methods( layout )
    #
    # This method will create the +format+ method in the given Parseable
    # _layout_ based on the configured items for the layout instance.
    #
    def self.create_yaml_format_method( layout )
      code = "undef :format if method_defined? :format\n"
      code << "def format( event )\nstr = {\n"

      code << layout.items.map {|name|
        "'#{name}' => #{Parseable::DIRECTIVE_TABLE[name]}"
      }.join(",\n")
      code << "\n}.to_yaml\nreturn str\nend\n"

      (class << layout; self end).class_eval(code, __FILE__, __LINE__)
    end

    # call-seq:
    #    Pattern.create_json_format_methods( layout )
    #
    # This method will create the +format+ method in the given Parseable
    # _layout_ based on the configured items for the layout instance.
    #
    def self.create_json_format_method( layout )
      code = "undef :format if method_defined? :format\n"
      code << "def format( event )\nh = {\n"

      code << layout.items.map {|name|
        "'#{name}' => #{Parseable::DIRECTIVE_TABLE[name]}"
      }.join(",\n")
      code << "\n}\nMultiJson.encode(h) << \"\\n\"\nend\n"

      (class << layout; self end).class_eval(code, __FILE__, __LINE__)
    end
    # :startdoc:

    # call-seq:
    #    Parseable.json( opts )
    #
    # Create a new Parseable layout that outputs log events using JSON style
    # formatting. See the initializer documentation for available options.
    #
    def self.json( opts = {} )
      opts[:style] = 'json'
      new(opts)
    end

    # call-seq:
    #    Parseable.yaml( opts )
    #
    # Create a new Parseable layout that outputs log events using YAML style
    # formatting. See the initializer documentation for available options.
    #
    def self.yaml( opts = {} )
      opts[:style] = 'yaml'
      new(opts)
    end

    # call-seq:
    #    Parseable.new( opts )
    #
    # Creates a new Parseable layout using the following options:
    #
    #    :style      => :json or :yaml
    #    :items      => %w[timestamp level logger message]
    #    :utc_offset =>  "-06:00" or -21600 or "UTC"
    #
    def initialize( opts = {} )
      super
      @created_at = Time.now
      @style = opts.fetch(:style, 'json').to_s.intern
      self.items = opts.fetch(:items, %w[timestamp level logger message])
    end

    attr_reader :items

    # call-seq:
    #    layout.items = %w[timestamp level logger message]
    #
    # Set the log event items that will be formatted by this layout. These
    # items, and only these items, will appear in the log output.
    #
    def items=( ary )
      @items = Array(ary).map {|name| name.to_s.downcase}
      valid = DIRECTIVE_TABLE.keys
      @items.each do |name|
        raise ArgumentError, "unknown item - #{name.inspect}" unless valid.include? name
      end
      create_format_method
    end

    # Public: Take a given object and convert it into a format suitable for
    # inclusion as a log message. The conversion allows the object to be more
    # easily expressed in YAML or JSON form.
    #
    # If the object is an Exception, then this method will return a Hash
    # containing the exception class name, message, and backtrace (if any).
    #
    # obj - The Object to format
    #
    # Returns the formatted Object.
    #
    def format_obj( obj )
      case obj
      when Exception
        hash = {
          :class   => obj.class.name,
          :message => obj.message
        }
        hash[:backtrace] = obj.backtrace if backtrace? && obj.backtrace

        cause = format_cause(obj)
        hash[:cause] = cause unless cause.empty?
        hash
      when Time
        iso8601_format(obj)
      else
        obj
      end
    end

    # Internal: Format any nested exceptions found in the given exception `e`
    # while respecting the maximum `cause_depth`.
    #
    # e - Exception to format
    #
    # Returns the cause formatted as a Hash
    def format_cause(e)
      rv = curr = {}
      prev = nil

      cause_depth.times do
        break unless e.respond_to?(:cause) && e.cause

        cause = e.cause
        curr[:class]     = cause.class.name
        curr[:message]   = cause.message
        curr[:backtrace] = format_cause_backtrace(e, cause) if backtrace? && cause.backtrace

        prev[:cause] = curr unless prev.nil?
        prev, curr = curr, {}

        e = cause
      end

      if e.respond_to?(:cause) && e.cause
        prev[:cause] = {message: "Further #cause backtraces were omitted"}
      end

      rv
    end

  private

    # Call the appropriate class level create format method based on the
    # style of this parseable layout.
    #
    def create_format_method
      case @style
      when :json; Parseable.create_json_format_method(self)
      when :yaml; Parseable.create_yaml_format_method(self)
      else raise ArgumentError, "unknown format style '#@style'" end
    end

    # Convert the given `time` into an ISO8601 formatted time string.
    #
    def iso8601_format( time )
      value = apply_utc_offset(time)

      str = value.strftime('%Y-%m-%dT%H:%M:%S')
      str << ('.%06d' % value.usec)

      offset = value.gmt_offset.abs
      return str << 'Z' if offset == 0

      offset = sprintf('%02d:%02d', offset / 3600, offset % 3600 / 60)
      return str << (value.gmt_offset < 0 ? '-' : '+') << offset
    end

  end
end