logging 1.7.2 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -22,6 +22,7 @@ class Layout
22
22
  # * :string => to_s
23
23
  # * :inspect => inspect
24
24
  # * :yaml => to_yaml
25
+ # * :json => MultiJson.encode(obj)
25
26
  #
26
27
  # If the format is not specified then the global object format is used
27
28
  # (see Logging#format_as). If the global object format is not specified
@@ -37,7 +38,7 @@ class Layout
37
38
  f = f.intern if f.instance_of? String
38
39
 
39
40
  @obj_format = case f
40
- when :inspect, :yaml; f
41
+ when :inspect, :yaml, :json; f
41
42
  else :string end
42
43
 
43
44
  b = opts.getopt(:backtrace, ::Logging.backtrace)
@@ -94,23 +95,38 @@ class Layout
94
95
  str << case @obj_format
95
96
  when :inspect; obj.inspect
96
97
  when :yaml; try_yaml(obj)
98
+ when :json; try_json(obj)
97
99
  else obj.to_s end
98
100
  str
99
101
  end
100
102
  end
101
103
 
102
- # call-seq:
103
- # try_yaml( obj )
104
- #
105
104
  # Attempt to format the _obj_ using yaml, but fall back to inspect style
106
105
  # formatting if yaml fails.
107
106
  #
107
+ # obj - The Object to format.
108
+ #
109
+ # Returns a String representation of the object.
110
+ #
108
111
  def try_yaml( obj )
109
112
  "\n#{obj.to_yaml}"
110
113
  rescue TypeError
111
114
  obj.inspect
112
115
  end
113
116
 
117
+ # Attempt to format the given object as a JSON string, but fall back to
118
+ # inspect formatting if JSON encoding fails.
119
+ #
120
+ # obj - The Object to format.
121
+ #
122
+ # Returns a String representation of the object.
123
+ #
124
+ def try_json( obj )
125
+ MultiJson.encode(obj)
126
+ rescue StandardError
127
+ obj.inspect
128
+ end
129
+
114
130
  end # class Layout
115
131
  end # module Logging
116
132
 
@@ -64,12 +64,12 @@ module Logging::Layouts
64
64
  # follows:
65
65
  #
66
66
  # ---
67
- # timestamp: 2009-04-17 16:15:42
67
+ # timestamp: 2009-04-17T16:15:42
68
68
  # level: INFO
69
69
  # logger: Foo::Bar
70
70
  # message: this is a log message
71
71
  # ---
72
- # timestamp: 2009-04-17 16:15:43
72
+ # timestamp: 2009-04-17T16:15:43
73
73
  # level: ERROR
74
74
  # logger: Foo
75
75
  # message: <RuntimeError> Oooops!!
@@ -84,8 +84,8 @@ module Logging::Layouts
84
84
  # it line by line and parse the individual objects. Taking the same
85
85
  # example above the JSON output would be:
86
86
  #
87
- # {"timestamp":"2009-04-17 16:15:42","level":"INFO","logger":"Foo::Bar","message":"this is a log message"}
88
- # {"timestamp":"2009-04-17 16:15:43","level":"ERROR","logger":"Foo","message":"<RuntimeError> Oooops!!"}
87
+ # {"timestamp":"2009-04-17T16:15:42","level":"INFO","logger":"Foo::Bar","message":"this is a log message"}
88
+ # {"timestamp":"2009-04-17T16:15:43","level":"ERROR","logger":"Foo","message":"<RuntimeError> Oooops!!"}
89
89
  #
90
90
  # The output order of the fields is guaranteed to be the same as the order
91
91
  # specified in the _items_ list.
@@ -95,17 +95,19 @@ module Logging::Layouts
95
95
  # :stopdoc:
96
96
  # Arguments to sprintf keyed to directive letters
97
97
  DIRECTIVE_TABLE = {
98
- 'logger' => 'event.logger',
99
- 'timestamp' => 'event.time',
100
- 'level' => '::Logging::LNAMES[event.level]',
101
- 'message' => 'format_obj(event.data)',
102
- 'file' => 'event.file',
103
- 'line' => 'event.line',
104
- 'method' => 'event.method',
105
- 'pid' => 'Process.pid',
106
- 'millis' => 'Integer((event.time-@created_at)*1000)',
107
- 'thread_id' => 'Thread.current.object_id',
108
- 'thread' => 'Thread.current[:name]'
98
+ 'logger' => 'event.logger'.freeze,
99
+ 'timestamp' => 'iso8601_format(event.time)'.freeze,
100
+ 'level' => '::Logging::LNAMES[event.level]'.freeze,
101
+ 'message' => 'format_obj(event.data)'.freeze,
102
+ 'file' => 'event.file'.freeze,
103
+ 'line' => 'event.line'.freeze,
104
+ 'method' => 'event.method'.freeze,
105
+ 'pid' => 'Process.pid'.freeze,
106
+ 'millis' => 'Integer((event.time-@created_at)*1000)'.freeze,
107
+ 'thread_id' => 'Thread.current.object_id'.freeze,
108
+ 'thread' => 'Thread.current[:name]'.freeze,
109
+ 'mdc' => 'Logging::MappedDiagnosticContext.context'.freeze,
110
+ 'ndc' => 'Logging::NestedDiagnosticContext.context'.freeze
109
111
  }
110
112
 
111
113
  # call-seq:
@@ -134,14 +136,12 @@ module Logging::Layouts
134
136
  #
135
137
  def self.create_json_format_method( layout )
136
138
  code = "undef :format if method_defined? :format\n"
137
- code << "def format( event )\n\"{"
139
+ code << "def format( event )\nh = {\n"
138
140
 
139
- args = []
140
141
  code << layout.items.map {|name|
141
- args << "format_as_json(#{Parseable::DIRECTIVE_TABLE[name]})"
142
- "\\\"#{name}\\\":%s"
143
- }.join(',')
144
- code << "}\\n\" % [#{args.join(', ')}]\nend"
142
+ "'#{name}' => #{Parseable::DIRECTIVE_TABLE[name]}"
143
+ }.join(",\n")
144
+ code << "\n}\nMultiJson.encode(h) << \"\\n\"\nend\n"
145
145
 
146
146
  (class << layout; self end).class_eval(code, __FILE__, __LINE__)
147
147
  end
@@ -201,18 +201,33 @@ module Logging::Layouts
201
201
  create_format_method
202
202
  end
203
203
 
204
- private
205
-
206
- # Take the given _value_ and format it into a JSON compatible string.
204
+ # Public: Take a given object and convert it into a format suitable for
205
+ # inclusion as a log message. The conversion allows the object to be more
206
+ # easily expressed in YAML or JSON form.
207
+ #
208
+ # If the object is an Exception, then this method will return a Hash
209
+ # containing the exception class name, message, and backtrace (if any).
207
210
  #
208
- def format_as_json( value )
209
- case value
210
- when String, Integer, Float; value.inspect
211
- when nil; 'null'
212
- when Time; %Q{"#{iso8601_format(value)}"}
213
- else %Q{"#{value.inspect}"} end
211
+ # obj - The Object to format
212
+ #
213
+ # Returns the formatted Object.
214
+ #
215
+ def format_obj( obj )
216
+ case obj
217
+ when Exception
218
+ h = { :class => obj.class.name,
219
+ :message => obj.message }
220
+ h[:backtrace] = obj.backtrace if @backtrace && !obj.backtrace.nil?
221
+ h
222
+ when Time
223
+ iso8601_format(obj)
224
+ else
225
+ obj
226
+ end
214
227
  end
215
228
 
229
+ private
230
+
216
231
  # Call the appropriate class level create format method based on the
217
232
  # style of this parseable layout.
218
233
  #
@@ -65,7 +65,12 @@ module Logging::Layouts
65
65
  # [T] Used to output the name of the thread that generated the log event.
66
66
  # Name can be specified using Thread.current[:name] notation. Output
67
67
  # empty string if name not specified. This option helps to create
68
- # more human readable output for multithread application logs.
68
+ # more human readable output for multi-threaded application logs.
69
+ # [X] Used to output values from the Mapped Diagnostic Context. Requires
70
+ # a key name to lookup the value from the context. More details are
71
+ # listed below.
72
+ # [x] Used to output values from the Nested Diagnostic Context. Supports
73
+ # an optional context separator string. More details are listed below.
69
74
  # [%] The sequence '%%' outputs a single percent sign.
70
75
  #
71
76
  # The logger name directive 'c' accepts an optional precision that will
@@ -77,6 +82,23 @@ module Logging::Layouts
77
82
  # events is configured to generate tracing information. If this is not
78
83
  # the case these fields will always be empty.
79
84
  #
85
+ # The directives for include diagnostic context information in the log
86
+ # messages are X and x. For the Mapped Diagnostic Context the directive must
87
+ # be accompanied by the key identifying the value to insert into the log
88
+ # message. The X directive can appear multiple times to include multiple
89
+ # values from the mapped context.
90
+ #
91
+ # %X{Cookie} Insert the current session cookie
92
+ # %X{X-Session} Insert a session identifier
93
+ #
94
+ # For the Nested Diagnostic Context you need only include the directive
95
+ # once. All contexts currently in the stack will be added to the log message
96
+ # separated by spaces. If spaces are not your style, a separator string can
97
+ # be given, too.
98
+ #
99
+ # %x Insert all contexts separated by spaces
100
+ # %x{, } Insert all contexts separate by a comma and a space
101
+ #
80
102
  # By default the relevant information is output as is. However, with the
81
103
  # aid of format modifiers it is possible to change the minimum field width,
82
104
  # the maximum field width and justification.
@@ -103,15 +125,15 @@ module Logging::Layouts
103
125
  # Below are various format modifier examples for the category conversion
104
126
  # specifier.
105
127
  #
106
- # [%20c] Left pad with spaces if the logger name is less than 20
128
+ # %20c Left pad with spaces if the logger name is less than 20
107
129
  # characters long
108
- # [%-20c] Right pad with spaces if the logger name is less than 20
130
+ # %-20c Right pad with spaces if the logger name is less than 20
109
131
  # characters long
110
- # [%.30c] Truncates the logger name if it is longer than 30 characters
111
- # [%20.30c] Left pad with spaces if the logger name is shorter than
132
+ # %.30c Truncates the logger name if it is longer than 30 characters
133
+ # %20.30c Left pad with spaces if the logger name is shorter than
112
134
  # 20 characters. However, if the logger name is longer than
113
135
  # 30 characters, then truncate the name.
114
- # [%-20.30c] Right pad with spaces if the logger name is shorter than
136
+ # %-20.30c Right pad with spaces if the logger name is shorter than
115
137
  # 20 characters. However, if the logger name is longer than
116
138
  # 30 characters, then truncate the name.
117
139
  #
@@ -141,6 +163,8 @@ module Logging::Layouts
141
163
  'r' => 'Integer((event.time-@created_at)*1000).to_s'.freeze,
142
164
  't' => 'Thread.current.object_id.to_s'.freeze,
143
165
  'T' => 'Thread.current[:name]'.freeze,
166
+ 'X' => :placeholder,
167
+ 'x' => :placeholder,
144
168
  '%' => :placeholder
145
169
  }.freeze
146
170
 
@@ -151,7 +175,7 @@ module Logging::Layouts
151
175
  # * $3 is the directive letter
152
176
  # * $4 is the precision specifier for the logger name
153
177
  # * $5 is the stuff after the directive or "" if not applicable
154
- DIRECTIVE_RGXP = %r/([^%]*)(?:(%-?\d*(?:\.\d+)?)([a-zA-Z%])(?:\{(\d+)\})?)?(.*)/m
178
+ DIRECTIVE_RGXP = %r/([^%]*)(?:(%-?\d*(?:\.\d+)?)([a-zA-Z%])(?:\{([^\}]+)\})?)?(.*)/m
155
179
 
156
180
  # default date format
157
181
  ISO8601 = "%Y-%m-%d %H:%M:%S".freeze
@@ -167,7 +191,9 @@ module Logging::Layouts
167
191
  't' => :thread_id,
168
192
  'F' => :file,
169
193
  'L' => :line,
170
- 'M' => :method
194
+ 'M' => :method,
195
+ 'X' => :mdc,
196
+ 'x' => :ndc
171
197
  }.freeze
172
198
 
173
199
  # call-seq:
@@ -226,10 +252,15 @@ module Logging::Layouts
226
252
  format_string << fmt
227
253
  args << DIRECTIVE_TABLE[m[3]].dup
228
254
  if m[4]
229
- raise ArgumentError, "logger name precision must be an integer greater than zero: #{m[4]}" unless Integer(m[4]) > 0
230
- args.last <<
231
- ".split(::Logging::Repository::PATH_DELIMITER)" \
232
- ".last(#{m[4]}).join(::Logging::Repository::PATH_DELIMITER)"
255
+ precision = Integer(m[4]) rescue nil
256
+ if precision
257
+ raise ArgumentError, "logger name precision must be an integer greater than zero: #{precision}" unless precision > 0
258
+ args.last <<
259
+ ".split(::Logging::Repository::PATH_DELIMITER)" \
260
+ ".last(#{m[4]}).join(::Logging::Repository::PATH_DELIMITER)"
261
+ else
262
+ format_string << "{#{m[4]}}"
263
+ end
233
264
  end
234
265
  when 'l'
235
266
  if color_scheme and color_scheme.levels?
@@ -247,6 +278,23 @@ module Logging::Layouts
247
278
  args << DIRECTIVE_TABLE[m[3]]
248
279
  end
249
280
 
281
+ when 'X'
282
+ raise ArgumentError, "MDC must have a key reference" unless m[4]
283
+ fmt = m[2] + 's'
284
+ fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[m[3]]) if color_scheme and !color_scheme.lines?
285
+
286
+ format_string << fmt
287
+ args << "::Logging.mdc['#{m[4]}']"
288
+
289
+ when 'x'
290
+ fmt = m[2] + 's'
291
+ fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[m[3]]) if color_scheme and !color_scheme.lines?
292
+
293
+ format_string << fmt
294
+ separator = m[4].to_s
295
+ separator = ' ' if separator.empty?
296
+ args << "::Logging.ndc.context.join('#{separator}')"
297
+
250
298
  when *DIRECTIVE_TABLE.keys
251
299
  fmt = m[2] + 's'
252
300
  fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[m[3]]) if color_scheme and !color_scheme.lines?
@@ -254,6 +302,7 @@ module Logging::Layouts
254
302
  format_string << fmt
255
303
  format_string << "{#{m[4]}}" if m[4]
256
304
  args << DIRECTIVE_TABLE[m[3]]
305
+
257
306
  when nil; break
258
307
  else
259
308
  raise ArgumentError, "illegal format character - '#{m[3]}'"
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
 
2
3
  require File.expand_path('../setup', File.dirname(__FILE__))
3
4
 
@@ -161,6 +162,26 @@ module TestAppenders
161
162
  assert_nil(readline)
162
163
  end
163
164
 
165
+ if Object.const_defined?(:Encoding)
166
+ def test_force_encoding
167
+ a = 'ümlaut'
168
+ b = 'hello ümlaut'.force_encoding('BINARY')
169
+
170
+ event_a = Logging::LogEvent.new('TestLogger', @levels['info'], a, false)
171
+ event_b = Logging::LogEvent.new('TestLogger', @levels['info'], b, false)
172
+
173
+ @appender.append event_a
174
+ @appender.append event_b
175
+ assert_nil(readline)
176
+
177
+ @appender.append event_a
178
+ assert_equal " INFO TestLogger : #{a}\n", readline
179
+ assert_equal " INFO TestLogger : #{b.force_encoding('UTF-8')}\n", readline
180
+ assert_equal " INFO TestLogger : #{a}\n", readline
181
+ assert_nil(readline)
182
+ end
183
+ end
184
+
164
185
  private
165
186
  def readline
166
187
  @appender.readline
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
 
2
3
  require File.expand_path('../setup', File.dirname(__FILE__))
3
4
 
@@ -95,6 +96,25 @@ module TestAppenders
95
96
  end
96
97
  end
97
98
 
99
+ if Object.const_defined? :Encoding
100
+
101
+ def test_encoding
102
+ log = File.join(TMP, 'file-encoding.log')
103
+ #appender = Logging.appenders.file(NAME, :filename => log, :encoding => 'ISO-8859-16')
104
+ appender = Logging.appenders.file(NAME, :filename => log, :encoding => 'ASCII')
105
+
106
+ appender << "A normal line of text\n"
107
+ appender << "ümlaut\n"
108
+ appender.close
109
+
110
+ lines = File.readlines(log)
111
+ assert_equal "A normal line of text\n", lines[0]
112
+ assert_equal "ümlaut\n", lines[1]
113
+
114
+ cleanup
115
+ end
116
+ end
117
+
98
118
  private
99
119
  def cleanup
100
120
  unless Logging.appenders[NAME].nil?
@@ -52,12 +52,9 @@ module TestAppenders
52
52
  Process.waitpid(pid)
53
53
 
54
54
  if defined?(::Syslog::LOG_PERROR)
55
- assert_equal("syslog_test: INFO TestLogger : <Array> #{[1,2,3,4]}\n",
56
- stderr[0].gets)
57
- assert_equal("syslog_test: DEBUG TestLogger : the big log message\n",
58
- stderr[0].gets)
59
- assert_equal("syslog_test: WARN TestLogger : this is your last warning\n",
60
- stderr[0].gets)
55
+ assert_match(%r/INFO TestLogger : <Array> #{Regexp.escape [1,2,3,4].to_s}/, stderr[0].gets)
56
+ assert_match(%r/DEBUG TestLogger : the big log message/, stderr[0].gets)
57
+ assert_match(%r/WARN TestLogger : this is your last warning/, stderr[0].gets)
61
58
  end
62
59
  end
63
60
 
@@ -101,9 +98,9 @@ module TestAppenders
101
98
  Process.waitpid(pid)
102
99
 
103
100
  if defined?(::Syslog::LOG_PERROR)
104
- assert_equal("syslog_test: this is a test message\n", stderr[0].gets)
105
- assert_equal("syslog_test: this is another message\n", stderr[0].gets)
106
- assert_equal("syslog_test: some other line\n", stderr[0].gets)
101
+ assert_match(%r/this is a test message/, stderr[0].gets)
102
+ assert_match(%r/this is another message/, stderr[0].gets)
103
+ assert_match(%r/some other line/, stderr[0].gets)
107
104
  end
108
105
  end
109
106
 
@@ -22,29 +22,37 @@ module TestLayouts
22
22
  end
23
23
 
24
24
  def test_format
25
- fmt = %Q[\\{"timestamp":"#@date_fmt","level":"%s","logger":"%s","message":"%s"\\}\\n]
26
-
27
25
  event = Logging::LogEvent.new('ArrayLogger', @levels['info'],
28
26
  'log message', false)
29
- rgxp = Regexp.new(sprintf(fmt, 'INFO', 'ArrayLogger', 'log message'))
30
- assert_match rgxp, @layout.format(event)
27
+ format = @layout.format(event)
28
+ assert_match %r/"timestamp":"#@date_fmt"/, format
29
+ assert_match %r/"level":"INFO"/, format
30
+ assert_match %r/"logger":"ArrayLogger"/, format
31
+ assert_match %r/"message":"log message"/, format
31
32
 
32
33
  event.data = [1, 2, 3, 4]
33
- rgxp = Regexp.new(sprintf(fmt, 'INFO', 'ArrayLogger',
34
- Regexp.escape("<Array> #{[1,2,3,4]}")))
35
- assert_match rgxp, @layout.format(event)
34
+ format = @layout.format(event)
35
+ assert_match %r/"timestamp":"#@date_fmt"/, format
36
+ assert_match %r/"level":"INFO"/, format
37
+ assert_match %r/"logger":"ArrayLogger"/, format
38
+ assert_match %r/"message":\[1,2,3,4\]/, format
36
39
 
37
40
  event.level = @levels['debug']
38
41
  event.data = 'and another message'
39
- rgxp = Regexp.new(sprintf(fmt, 'DEBUG', 'ArrayLogger',
40
- 'and another message'))
41
- assert_match rgxp, @layout.format(event)
42
+ format = @layout.format(event)
43
+ assert_match %r/"timestamp":"#@date_fmt"/, format
44
+ assert_match %r/"level":"DEBUG"/, format
45
+ assert_match %r/"logger":"ArrayLogger"/, format
46
+ assert_match %r/"message":"and another message"/, format
42
47
 
43
48
  event.logger = 'Test'
44
49
  event.level = @levels['fatal']
45
50
  event.data = Exception.new
46
- rgxp = Regexp.new(sprintf(fmt, 'FATAL', 'Test', '<Exception> Exception'))
47
- assert_match rgxp, @layout.format(event)
51
+ format = @layout.format(event)
52
+ assert_match %r/"timestamp":"#@date_fmt"/, format
53
+ assert_match %r/"level":"FATAL"/, format
54
+ assert_match %r/"logger":"Test"/, format
55
+ assert_match %r/"message":\{(?:"(?:class|message)":"Exception",?){2}\}/, format
48
56
  end
49
57
 
50
58
  def test_items
@@ -103,6 +111,56 @@ module TestLayouts
103
111
  assert_equal %Q[{"thread":null}\n], @layout.format(event)
104
112
  Thread.current[:name] = "Main"
105
113
  assert_equal %Q[{"thread":"Main"}\n], @layout.format(event)
114
+
115
+ @layout.items = %w[mdc]
116
+ assert_match %r/\A\{"mdc":\{\}\}\n\z/, @layout.format(event)
117
+
118
+ @layout.items = %w[ndc]
119
+ assert_match %r/\A\{"ndc":\[\]\}\n\z/, @layout.format(event)
120
+ end
121
+
122
+ def test_mdc_output
123
+ event = Logging::LogEvent.new('TestLogger', @levels['info'],
124
+ 'log message', false)
125
+ Logging.mdc['X-Session'] = '123abc'
126
+ Logging.mdc['Cookie'] = 'monster'
127
+
128
+ @layout.items = %w[timestamp level logger message mdc]
129
+
130
+ format = @layout.format(event)
131
+ assert_match %r/"timestamp":"#@date_fmt"/, format
132
+ assert_match %r/"level":"INFO"/, format
133
+ assert_match %r/"logger":"TestLogger"/, format
134
+ assert_match %r/"message":"log message"/, format
135
+ assert_match %r/"mdc":\{(?:(?:"X-Session":"123abc"|"Cookie":"monster"),?){2}\}/, format
136
+
137
+ Logging.mdc.delete 'Cookie'
138
+ format = @layout.format(event)
139
+ assert_match %r/"mdc":\{"X-Session":"123abc"\}/, format
140
+ end
141
+
142
+ def test_ndc_output
143
+ event = Logging::LogEvent.new('TestLogger', @levels['info'],
144
+ 'log message', false)
145
+ Logging.ndc << 'context a'
146
+ Logging.ndc << 'context b'
147
+
148
+ @layout.items = %w[timestamp level logger message ndc]
149
+
150
+ format = @layout.format(event)
151
+ assert_match %r/"timestamp":"#@date_fmt"/, format
152
+ assert_match %r/"level":"INFO"/, format
153
+ assert_match %r/"logger":"TestLogger"/, format
154
+ assert_match %r/"message":"log message"/, format
155
+ assert_match %r/"ndc":\["context a","context b"\]/, format
156
+
157
+ Logging.ndc.pop
158
+ format = @layout.format(event)
159
+ assert_match %r/"ndc":\["context a"\]/, format
160
+
161
+ Logging.ndc.pop
162
+ format = @layout.format(event)
163
+ assert_match %r/"ndc":\[\]/, format
106
164
  end
107
165
 
108
166
  end # class TestJson