logging 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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