semantic_logger 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,14 +2,17 @@
2
2
  require 'sync_attr'
3
3
 
4
4
  module SemanticLogger
5
+ autoload :Base, 'semantic_logger/base'
5
6
  autoload :Logger, 'semantic_logger/logger'
6
7
 
7
8
  module Appender
8
- autoload :File, 'semantic_logger/appender/file'
9
- autoload :Logger, 'semantic_logger/appender/logger'
10
- # Only load the MongoDB appender if the Mongo Ruby Driver is loaded
9
+ autoload :File, 'semantic_logger/appender/file'
10
+ autoload :Wrapper, 'semantic_logger/appender/wrapper'
11
11
  autoload :MongoDB, 'semantic_logger/appender/mongodb'
12
12
  end
13
+
14
+ # Logging levels in order with most detailed logging first
15
+ LEVELS = [:trace, :debug, :info, :warn, :error, :fatal]
13
16
  end
14
17
 
15
18
  if defined?(Rails)
@@ -4,14 +4,16 @@
4
4
  #
5
5
  module SemanticLogger
6
6
  module Appender
7
- class File
8
- attr_accessor :formatter
7
+ class File < SemanticLogger::Base
9
8
 
10
9
  # Create a File Logger appender instance
11
10
  #
12
11
  # Example
13
12
  # require 'semantic_logger'
14
13
  #
14
+ # # Enable trace level logging
15
+ # SemanticLogger::Logger.level = :info
16
+ #
15
17
  # # Log to screen
16
18
  # SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new(STDOUT)
17
19
  #
@@ -21,7 +23,23 @@ module SemanticLogger
21
23
  # logger = SemanticLogger::Logger.new('test')
22
24
  # logger.info 'Hello World'
23
25
  #
24
- def initialize(filename, &block)
26
+ # Example 2. To log all levels to file and only :info and above to screen:
27
+ #
28
+ # require 'semantic_logger'
29
+ #
30
+ # # Enable trace level logging
31
+ # SemanticLogger::Logger.level = :trace
32
+ #
33
+ # # Log to screen but only display :info and above
34
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new(STDOUT, :info)
35
+ #
36
+ # # And log to a file at the same time, including all :trace level data
37
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new('application.log')
38
+ #
39
+ # logger = SemanticLogger::Logger.new('test')
40
+ # logger.info 'Hello World'
41
+ #
42
+ def initialize(filename, level=nil, &block)
25
43
  raise "logger cannot be null when initializing the SemanticLogging::Appender::Logger" unless filename
26
44
  @filename = filename
27
45
  @log = if filename.respond_to?(:write) and filename.respond_to?(:close)
@@ -29,38 +47,14 @@ module SemanticLogger
29
47
  else
30
48
  @log = open(filename, (::File::WRONLY | ::File::APPEND | ::File::CREAT))
31
49
  # Force all log entries to write immediately without buffering
50
+ # Allows multiple processes to write to the same log file simultaneously
32
51
  @log.sync = true
33
52
  @log.set_encoding(Encoding::BINARY) if @log.respond_to?(:set_encoding)
34
53
  @log
35
54
  end
36
55
 
37
- # Set the formatter to the supplied block
38
- @formatter = block || self.default_formatter
39
- end
40
-
41
- # Default log formatter
42
- # Replace this formatter by supplying a Block to the initializer
43
- # Generates logs of the form:
44
- # 2011-07-19 14:36:15.660 D [1149:ScriptThreadProcess] Rails -- Hello World\n
45
- def default_formatter
46
- Proc.new do |log|
47
- message = log.message.to_s
48
- tags = log.tags.collect { |tag| "[#{tag}]" }.join(" ") + " " if log.tags && (log.tags.size > 0)
49
-
50
- if log.payload
51
- if log.payload.is_a?(Exception)
52
- exception = log.payload
53
- message << " -- " << "#{exception.class}: #{exception.message}\n#{(exception.backtrace || []).join("\n")}"
54
- else
55
- message << " -- " << log.payload.inspect
56
- end
57
- end
58
-
59
- str = "#{log.time.strftime("%Y-%m-%d %H:%M:%S")}.#{"%03d" % (log.time.usec/1000)} #{log.level.to_s[0..0].upcase} [#{$$}:#{log.thread_name}] #{tags}#{log.name} -- #{message}"
60
- str << " (#{'%.1f' % log.duration}ms)" if log.duration
61
- str << "\n"
62
- str
63
- end
56
+ # Set the log level and formatter if supplied
57
+ super(level, &block)
64
58
  end
65
59
 
66
60
  # Pass log calls to the underlying Rails, log4j or Ruby logger
@@ -69,7 +63,8 @@ module SemanticLogger
69
63
  def log(log)
70
64
  # Since only one appender thread will be writing to the file at a time
71
65
  # it is not necessary to protect access to the file with a semaphore
72
- @log.write(@formatter.call(log))
66
+ # Allow this logger to filter out log levels lower than it's own
67
+ @log.write(@formatter.call(log) << "\n") if level_index <= (log.level_index || 0)
73
68
  end
74
69
 
75
70
  # Flush all pending logs to disk.
@@ -1,3 +1,4 @@
1
+ require 'socket'
1
2
  module SemanticLogger
2
3
  module Appender
3
4
  # The Mongo Appender for the SemanticLogger
@@ -41,9 +42,9 @@ module SemanticLogger
41
42
  # params: Hash
42
43
  #
43
44
  # tracking_number: 'user defined tracking number'
44
- class MongoDB
45
+ class MongoDB < SemanticLogger::Base
45
46
  attr_reader :db, :collection_name
46
- attr_accessor :formatter, :host_name, :safe, :application
47
+ attr_accessor :host_name, :safe, :application
47
48
 
48
49
  # Create a MongoDB Appender instance
49
50
  #
@@ -84,6 +85,9 @@ module SemanticLogger
84
85
  # :collection_max [Integer]
85
86
  # Maximum number of log entries that the capped collection will hold
86
87
  #
88
+ # :level [Symbol]
89
+ # Only allow log entries of this level or higher to be written to MongoDB
90
+ #
87
91
  def initialize(params={}, &block)
88
92
  @db = params[:db] || raise('Missing mandatory parameter :db')
89
93
  @collection_name = params[:collection_name] || 'semantic_logger'
@@ -95,11 +99,11 @@ module SemanticLogger
95
99
  @collection_size = params[:collection_size] || 1024**3
96
100
  @collection_max = params[:collection_max]
97
101
 
98
- # Set the formatter to the supplied block
99
- @formatter = block || self.default_formatter
100
-
101
102
  # Create the collection and necessary indexes
102
103
  create_indexes
104
+
105
+ # Set the log level and formatter
106
+ super(params[:level], &block)
103
107
  end
104
108
 
105
109
  # Create the required capped collection
@@ -189,8 +193,7 @@ module SemanticLogger
189
193
  # Insert log entry into Mongo
190
194
  # Use safe=>false so that we do not wait for it to be written to disk, or
191
195
  # for the response from the MongoDB server
192
- document = formatter.call(log)
193
- collection.insert(document, :safe=>safe)
196
+ collection.insert(formatter.call(log), :safe=>safe) if level_index <= (log.level_index || 0)
194
197
  end
195
198
 
196
199
  end
@@ -1,14 +1,11 @@
1
- # Logger appender
1
+ # Wrapper appender
2
2
  #
3
- # Maps the SemanticLogger API's to the Rails log, log4j, or Ruby Logger
3
+ # Wraps the Rails log, log4r, or Ruby Logger with the SemanticLogger API's
4
4
  #
5
- # The log level is controlled by the Logging implementation passed into
6
- # this appender
7
5
  module SemanticLogger
8
6
  module Appender
9
- class Logger
7
+ class Wrapper < SemanticLogger::Base
10
8
  attr_reader :logger
11
- attr_accessor :formatter
12
9
 
13
10
  # Create a Logger or Rails Logger appender instance
14
11
  #
@@ -16,48 +13,30 @@ module SemanticLogger
16
13
  # require 'logger'
17
14
  # require 'semantic_logger'
18
15
  # ruby_logger = Logger.new(STDOUT)
19
- # SemanticLogger::Logger.appenders << SemanticLogger::Appender::Logger.new(ruby_logger)
16
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::Wrapper.new(ruby_logger)
20
17
  # logger = SemanticLogger::Logger.new('test')
21
18
  # logger.info('Hello World', :some => :payload)
22
19
  #
23
20
  # Enhance the Rails Logger
24
21
  # # Add the Rails logger to the list of appenders
25
- # SemanticLogger::Logger.appenders << SemanticLogger::Appender::Logger.new(Rails.logger)
22
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::Wrapper.new(Rails.logger)
26
23
  # Rails.logger = SemanticLogger::Logger.new('Rails')
27
24
  #
28
25
  # # Make ActiveRecord logging include its class name in every log entry
29
26
  # ActiveRecord::Base.logger = SemanticLogger::Logger.new('ActiveRecord')
27
+ #
28
+ # Note: Since the log level is controlled by setting the Ruby or Rails logger directly
29
+ # the level is ignored for this appender
30
30
  def initialize(logger, &block)
31
31
  raise "logger cannot be null when initiailizing the SemanticLogging::Appender::Logger" unless logger
32
32
  @logger = logger
33
33
 
34
34
  # Set the formatter to the supplied block
35
35
  @formatter = block || self.default_formatter
36
+ super(:trace, &block)
36
37
  end
37
38
 
38
- # Default log formatter
39
- # Replace this formatter by supplying a Block to the initializer
40
- # Generates logs of the form:
41
- # 2011-07-19 14:36:15.660 D [1149:ScriptThreadProcess] Rails -- Hello World
42
- def default_formatter
43
- Proc.new do |log|
44
- message = log.message.to_s
45
- tags = log.tags.collect { |tag| "[#{tag}]" }.join(" ") + " " if log.tags && (log.tags.size > 0)
46
-
47
- if log.payload
48
- if log.payload.is_a?(Exception)
49
- exception = log.payload
50
- message << " -- " << "#{exception.class}: #{exception.message}\n#{(exception.backtrace || []).join("\n")}"
51
- else
52
- message << " -- " << log.payload.inspect
53
- end
54
- end
55
-
56
- str = "#{log.time.strftime("%Y-%m-%d %H:%M:%S")}.#{"%03d" % (log.time.usec/1000)} #{log.level.to_s[0..0].upcase} [#{$$}:#{log.thread_name}] #{tags}#{log.name} -- #{message}"
57
- str << " (#{'%.1f' % log.duration}ms)" if log.duration
58
- str
59
- end
60
- end
39
+ # TODO Compatible calls to #level and #level= against the underlying Rails/Ruby logger
61
40
 
62
41
  # Pass log calls to the underlying Rails, log4j or Ruby logger
63
42
  # trace entries are mapped to debug since :trace is not supported by the
@@ -0,0 +1,280 @@
1
+ # Base appender
2
+ #
3
+ # Abstract base class for appenders
4
+ #
5
+ # Implements common behavior such as log level, default text formatter etc
6
+ #
7
+ # Note: Do not create instances of this class directly
8
+ #
9
+ module SemanticLogger
10
+ class Base
11
+ attr_accessor :formatter
12
+ attr_reader :level
13
+
14
+ def initialize(level, &block)
15
+ # Set the formatter to the supplied block
16
+ @formatter = block || default_formatter
17
+
18
+ # Log everything that comes to the appender by default
19
+ self.level = level || :trace
20
+ end
21
+
22
+ # Default log formatter
23
+ # Replace this formatter by supplying a Block to the initializer
24
+ # Generates logs of the form:
25
+ # 2011-07-19 14:36:15.660 D [1149:ScriptThreadProcess] Rails -- Hello World
26
+ def default_formatter
27
+ Proc.new do |log|
28
+ message = log.message.to_s
29
+ tags = log.tags.collect { |tag| "[#{tag}]" }.join(" ") + " " if log.tags && (log.tags.size > 0)
30
+
31
+ if log.payload
32
+ if log.payload.is_a?(Exception)
33
+ exception = log.payload
34
+ message << " -- " << "#{exception.class}: #{exception.message}\n#{(exception.backtrace || []).join("\n")}"
35
+ else
36
+ message << " -- " << log.payload.inspect
37
+ end
38
+ end
39
+
40
+ str = "#{log.time.strftime("%Y-%m-%d %H:%M:%S")}.#{"%03d" % (log.time.usec/1000)} #{log.level.to_s[0..0].upcase} [#{$$}:#{log.thread_name}] #{tags}#{log.name} -- #{message}"
41
+ str << " (#{'%.1f' % log.duration}ms)" if log.duration
42
+ str
43
+ end
44
+ end
45
+
46
+ # Write log data to underlying data storage
47
+ def log(log_)
48
+ raise "Logging Appender must implement #log(log)"
49
+ end
50
+
51
+ # Set the logging level for this appender
52
+ #
53
+ # Note: This level is only for this particular appender. It does not override
54
+ # the log level in any logging instance or the default log level
55
+ # SemanticLogger::Logger.level
56
+ #
57
+ # Must be one of the values in SemanticLogger::Logger::LEVELS
58
+ def level=(level)
59
+ @level_index = self.class.map_level_to_index(level)
60
+ @level = level
61
+ end
62
+
63
+ # Implement the log level calls
64
+ # logger.debug(message, hash|exception=nil, &block)
65
+ #
66
+ # Implement the log level query
67
+ # logger.debug?
68
+ #
69
+ # Example:
70
+ # require 'semantic_logger'
71
+ #
72
+ # # Enable trace level logging
73
+ # SemanticLogger::Logger.level = :info
74
+ #
75
+ # # Log to screen
76
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new(STDOUT)
77
+ #
78
+ # # And log to a file at the same time
79
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new('application.log')
80
+ #
81
+ # logger = SemanticLogger::Logger.new('MyApplication')
82
+ # logger.debug("Only display this if log level is set to Debug or lower")
83
+ #
84
+ # # Log semantic information along with a text message
85
+ # logger.info("Request received", :user => "joe", :duration => 100)
86
+ #
87
+ # # Log an exception in a semantic way
88
+ # logger.info("Parsing received XML", exc)
89
+ #
90
+ SemanticLogger::LEVELS.each_with_index do |level, index|
91
+ class_eval <<-EOT, __FILE__, __LINE__
92
+ def #{level}(message = nil, payload = nil)
93
+ if @level_index <= #{index}
94
+ if block_given? && (result = yield)
95
+ if result.is_a?(String)
96
+ message = message.nil? ? result : "\#{message} -- \#{result.to_s}"
97
+ else
98
+ payload = payload.nil? ? result : payload.merge(result)
99
+ end
100
+ end
101
+ # Add scoped payload
102
+ if self.payload
103
+ payload = payload.nil? ? self.payload : self.payload.merge(payload)
104
+ end
105
+ log Log.new(:#{level}, self.class.thread_name, name, message, payload, Time.now, nil, tags, #{index})
106
+ true
107
+ else
108
+ false
109
+ end
110
+ end
111
+
112
+ def #{level}?
113
+ @level_index <= #{index}
114
+ end
115
+
116
+ # Log the duration of the supplied block
117
+ # If an exception occurs in the block the exception is logged using the
118
+ # same log level. The exception will flow through to the caller unchanged
119
+ def benchmark_#{level}(message, payload = nil)
120
+ raise "Mandatory block missing" unless block_given?
121
+ if @level_index <= #{index}
122
+ start = Time.now
123
+ begin
124
+ result = yield
125
+ end_time = Time.now
126
+ # Add scoped payload
127
+ if self.payload
128
+ payload = payload.nil? ? self.payload : self.payload.merge(payload)
129
+ end
130
+ log Log.new(:#{level}, self.class.thread_name, name, message, payload, end_time, 1000.0 * (end_time - start), tags, #{index})
131
+ result
132
+ rescue Exception => exc
133
+ # TODO Need to be able to have both an exception and a Payload
134
+ log Log.new(:#{level}, self.class.thread_name, name, message, exc, Time.now, 1000.0 * (Time.now - start), tags, #{index})
135
+ raise exc
136
+ end
137
+ else
138
+ yield
139
+ end
140
+ end
141
+ EOT
142
+ end
143
+
144
+ # Add the supplied tags to the list of tags to log for this thread whilst
145
+ # the supplied block is active
146
+ # Returns nil if no tags are currently set
147
+ def with_tags(*tags)
148
+ current_tags = self.tags
149
+ # Check for nil tags
150
+ if tags
151
+ Thread.current[:semantic_logger_tags] = current_tags ? current_tags + tags : tags
152
+ end
153
+ yield
154
+ ensure
155
+ Thread.current[:semantic_logger_tags] = current_tags
156
+ end
157
+
158
+ # Add support for the ActiveSupport::TaggedLogging
159
+ alias_method :tagged, :with_tags
160
+
161
+ # Returns [Array] of [String] tags currently active for this thread
162
+ # Returns nil if no tags are set
163
+ def tags
164
+ Thread.current[:semantic_logger_tags]
165
+ end
166
+
167
+ # Thread specific context information to be logged with every log entry
168
+ #
169
+ # Add a payload to all log calls on This Thread within the supplied block
170
+ #
171
+ # logger.with_payload(:tracking_number=>12345) do
172
+ # logger.debug('Hello World')
173
+ # end
174
+ #
175
+ # If a log call already includes a pyload, this payload will be merged with
176
+ # the supplied payload, with the supplied payload taking precedence
177
+ #
178
+ # logger.with_payload(:tracking_number=>12345) do
179
+ # logger.debug('Hello World', :result => 'blah')
180
+ # end
181
+ def with_payload(payload)
182
+ current_payload = self.payload
183
+ Thread.current[:semantic_logger_payload] = current_payload ? current_payload.merge(payload) : payload
184
+ yield
185
+ ensure
186
+ Thread.current[:semantic_logger_payload] = current_payload
187
+ end
188
+
189
+ # Returns [Hash] payload to be added to every log entry in the current scope
190
+ # on this thread.
191
+ # Returns nil if no payload is currently set
192
+ def payload
193
+ Thread.current[:semantic_logger_payload]
194
+ end
195
+
196
+ # Semantic Logging does not support :unknown level since these
197
+ # are not understood by the majority of the logging providers
198
+ # Map it to :error
199
+ alias :unknown :error
200
+ alias :unknown? :error?
201
+
202
+ # #TODO implement a thread safe #silence method
203
+
204
+ ############################################################################
205
+ protected
206
+
207
+ # Return the level index for fast comparisons
208
+ attr_reader :level_index
209
+
210
+ # Struct Log
211
+ #
212
+ # level
213
+ # Log level of the supplied log call
214
+ # :trace, :debug, :info, :warn, :error, :fatal
215
+ #
216
+ # thread_name
217
+ # Name of the thread in which the logging call was called
218
+ #
219
+ # name
220
+ # Class name supplied to the logging instance
221
+ #
222
+ # message
223
+ # Text message to be logged
224
+ #
225
+ # payload
226
+ # Optional Hash or Ruby Exception object to be logged
227
+ #
228
+ # time
229
+ # The time at which the log entry was created
230
+ #
231
+ # duration
232
+ # The time taken to complete a benchmark call
233
+ #
234
+ # tags
235
+ # Any tags active on the thread when the log call was made
236
+ #
237
+ # level_index
238
+ # Internal index of the log level
239
+ Log = Struct.new(:level, :thread_name, :name, :message, :payload, :time, :duration, :tags, :level_index)
240
+
241
+ # For JRuby include the Thread name rather than its id
242
+ if defined? Java
243
+ def self.thread_name
244
+ Java::java.lang::Thread.current_thread.name
245
+ end
246
+ else
247
+ def self.thread_name
248
+ Thread.current.object_id
249
+ end
250
+ end
251
+
252
+ # Internal method to return the log level as an internal index
253
+ # Also supports mapping the ::Logger levels to SemanticLogger levels
254
+ def self.map_level_to_index(level)
255
+ index = if level.is_a?(Integer)
256
+ # Mapping of Rails and Ruby Logger levels to SemanticLogger levels
257
+ @@map_levels ||= begin
258
+ levels = []
259
+ ::Logger::Severity.constants.each do |constant|
260
+ levels[::Logger::Severity.const_get(constant)] = LEVELS.find_index(constant.downcase.to_sym) || LEVELS.find_index(:error)
261
+ end
262
+ end [level] if defined?(::Logger::Severity)
263
+ elsif level.is_a?(String)
264
+ level = level.downcase.to_sym
265
+ LEVELS.index(level)
266
+ else
267
+ LEVELS.index(level)
268
+ end
269
+ raise "Invalid level:#{level.inspect} being requested. Must be one of #{LEVELS.inspect}" unless index
270
+ index
271
+ end
272
+
273
+ # Appenders don't take a class name, so use this class name if an appender
274
+ # is logged to directly
275
+ def name
276
+ self.class.name
277
+ end
278
+
279
+ end
280
+ end
@@ -19,7 +19,7 @@
19
19
  #
20
20
  # # Now log to the Logger above as well as MongoDB at the same time
21
21
  #
22
- # db = Mongo::Connection.new['production_logging']
22
+ # db = Mongodb::Connection.new['production_logging']
23
23
  #
24
24
  # SemanticLogger::Logger.appenders << SemanticLogger::Appender::MongoDB.new(
25
25
  # :db => db,
@@ -29,40 +29,31 @@
29
29
  # # This will be logged to both the Ruby Logger and MongoDB
30
30
  # logger.debug("Login time", :user => 'Mary', :duration => 230, :ip_address=>'192.168.0.1')
31
31
  #
32
- require 'logger'
33
32
  module SemanticLogger
34
- class Logger
33
+ class Logger < Base
35
34
  include SyncAttr
36
35
 
37
- # Logging levels in order of precedence
38
- LEVELS = [:trace, :debug, :info, :warn, :error, :fatal]
39
-
40
- # Mapping of Rails and Ruby Logger levels to SemanticLogger levels
41
- MAP_LEVELS = []
42
- ::Logger::Severity.constants.each do |constant|
43
- MAP_LEVELS[::Logger::Severity.const_get(constant)] = LEVELS.find_index(constant.downcase.to_sym) || LEVELS.find_index(:error)
44
- end
45
-
46
36
  # Thread safe Class Attribute accessor for appenders array
47
37
  sync_cattr_accessor :appenders do
48
38
  []
49
39
  end
50
40
 
41
+ # Initial default Level for all new instances of SemanticLogger::Logger
42
+ @@default_level = :info
43
+
51
44
  # Allow for setting the global default log level
52
45
  # This change only applies to _new_ loggers, existing logger levels
53
46
  # will not be changed in any way
54
- def self.level=(level)
55
- @@level = level
47
+ def self.default_level=(level)
48
+ @@default_level = level
56
49
  end
57
50
 
58
51
  # Returns the global default log level for new Logger instances
59
- def self.level
60
- @@level
52
+ def self.default_level
53
+ @@default_level
61
54
  end
62
55
 
63
- attr_reader :name, :level
64
-
65
- @@level = :info
56
+ attr_reader :name
66
57
 
67
58
  # Returns a Logger instance
68
59
  #
@@ -76,144 +67,11 @@ module SemanticLogger
76
67
  # to be used in the logger
77
68
  # options:
78
69
  # :level The initial log level to start with for this logger instance
79
- def initialize(klass, options={})
70
+ def initialize(klass, level=self.class.default_level)
80
71
  @name = klass.is_a?(String) ? klass : klass.name
81
- set_level(options[:level] || self.class.level)
72
+ self.level = level
82
73
  end
83
74
 
84
- # Set the logging level
85
- # Must be one of the values in #LEVELS
86
- def level=(level)
87
- set_level(level)
88
- end
89
-
90
- # Implement the log level calls
91
- # logger.debug(message|hash|exception, hash|exception=nil, &block)
92
- #
93
- # Implement the log level query
94
- # logger.debug?
95
- #
96
- # Example:
97
- # logger = SemanticLogging::Logger.new(self)
98
- # logger.debug("Only display this if log level is set to Debug or lower")
99
- #
100
- # # Log semantic information along with a text message
101
- # logger.info("Request received", :user => "joe", :duration => 100)
102
- #
103
- # # Log an exception in a semantic way
104
- # logger.info("Parsing received XML", exc)
105
- #
106
- LEVELS.each_with_index do |level, index|
107
- class_eval <<-EOT, __FILE__, __LINE__
108
- def #{level}(message = nil, payload = nil)
109
- if @level_index <= #{index}
110
- if block_given? && (result = yield)
111
- if result.is_a?(String)
112
- message = message.nil? ? result : "\#{message} -- \#{result.to_s}"
113
- else
114
- payload = payload.nil? ? result : payload.merge(result)
115
- end
116
- end
117
- # Add scoped payload
118
- if self.payload
119
- payload = payload.nil? ? self.payload : self.payload.merge(payload)
120
- end
121
- self.class.queue << Log.new(:#{level}, self.class.thread_name, name, message, payload, Time.now, nil, tags)
122
- true
123
- else
124
- false
125
- end
126
- end
127
-
128
- def #{level}?
129
- @level_index <= #{index}
130
- end
131
-
132
- # Log the duration of the supplied block
133
- # If an exception occurs in the block the exception is logged using the
134
- # same log level. The exception will flow through to the caller unchanged
135
- def benchmark_#{level}(message, payload = nil)
136
- raise "Mandatory block missing" unless block_given?
137
- if @level_index <= #{index}
138
- start = Time.now
139
- begin
140
- result = yield
141
- end_time = Time.now
142
- # Add scoped payload
143
- if self.payload
144
- payload = payload.nil? ? self.payload : self.payload.merge(payload)
145
- end
146
- self.class.queue << Log.new(:#{level}, self.class.thread_name, name, message, payload, end_time, 1000.0 * (end_time - start), tags)
147
- result
148
- rescue Exception => exc
149
- # TODO Need to be able to have both an exception and a Payload
150
- self.class.queue << Log.new(:#{level}, self.class.thread_name, name, message, exc, Time.now, 1000.0 * (Time.now - start), tags)
151
- raise exc
152
- end
153
- else
154
- yield
155
- end
156
- end
157
- EOT
158
- end
159
-
160
- # Add the supplied tags to the list of tags to log for this thread whilst
161
- # the supplied block is active
162
- # Returns nil if no tags are currently set
163
- def with_tags(*tags)
164
- current_tags = self.tags
165
- # Check for nil tags
166
- if tags
167
- Thread.current[:semantic_logger_tags] = current_tags ? current_tags + tags : tags
168
- end
169
- yield
170
- ensure
171
- Thread.current[:semantic_logger_tags] = current_tags
172
- end
173
-
174
- # Returns [Array] of [String] tags currently active for this thread
175
- # Returns nil if no tags are set
176
- def tags
177
- Thread.current[:semantic_logger_tags]
178
- end
179
-
180
- # Thread specific context information to be logged with every log entry
181
- #
182
- # Add a payload to all log calls on This Thread within the supplied block
183
- #
184
- # logger.with_payload(:tracking_number=>12345) do
185
- # logger.debug('Hello World')
186
- # end
187
- #
188
- # If a log call already includes a pyload, this payload will be merged with
189
- # the supplied payload, with the supplied payload taking precedence
190
- #
191
- # logger.with_payload(:tracking_number=>12345) do
192
- # logger.debug('Hello World', :result => 'blah')
193
- # end
194
- def with_payload(payload)
195
- current_payload = self.payload
196
- Thread.current[:semantic_logger_payload] = current_payload ? current_payload.merge(payload) : payload
197
- yield
198
- ensure
199
- Thread.current[:semantic_logger_payload] = current_payload
200
- end
201
-
202
- # Returns [Hash] payload to be added to every log entry in the current scope
203
- # on this thread.
204
- # Returns nil if no payload is currently set
205
- def payload
206
- Thread.current[:semantic_logger_payload]
207
- end
208
-
209
- # Semantic Logging does not support :unknown level since these
210
- # are not understood by the majority of the logging providers
211
- # Map it to :error
212
- alias :unknown :error
213
- alias :unknown? :error?
214
-
215
- # #TODO implement a thread safe #silence method
216
-
217
75
  # Returns [Integer] the number of log entries that have not been written
218
76
  # to the appenders
219
77
  #
@@ -235,22 +93,27 @@ module SemanticLogger
235
93
  reply_queue.pop
236
94
  end
237
95
 
96
+ ############################################################################
97
+ protected
98
+
99
+ # Place log request on the queue for the Appender thread to write to each
100
+ # appender in the order that they were registered
101
+ def log(log)
102
+ self.class.queue << log
103
+ end
104
+
238
105
  # Internal logger for SemanticLogger
239
106
  # For example when an appender is not working etc..
240
- # By default logs to STDERR, replace with another Ruby logger or Rails
241
- # logger, but never to SemanticLogger itself
107
+ # By default logs to STDERR
108
+ # Can be replaced with another Ruby logger or Rails logger, but never to
109
+ # SemanticLogger::Logger itself
242
110
  #
243
111
  # Warning: Do not use this logger directly it is intended for internal logging
244
112
  # within Semantic Logger itself
245
113
  sync_cattr_accessor :logger do
246
- l = ::Logger.new(STDERR)
247
- l.level = ::Logger::INFO
248
- l
114
+ SemanticLogger::Appender::File.new(STDERR, :warn)
249
115
  end
250
116
 
251
- ############################################################################
252
- protected
253
-
254
117
  # Log to queue
255
118
  # Starts the appender thread the first time a logging call is made
256
119
  sync_cattr_reader :queue do
@@ -259,70 +122,11 @@ module SemanticLogger
259
122
  Queue.new
260
123
  end
261
124
 
262
- # Struct Log
263
- #
264
- # level
265
- # Log level of the supplied log call
266
- # :trace, :debug, :info, :warn, :error, :fatal
267
- #
268
- # thread_name
269
- # Name of the thread in which the logging call was called
270
- #
271
- # name
272
- # Class name supplied to the logging instance
273
- #
274
- # message
275
- # Text message to be logged
276
- #
277
- # payload
278
- # Optional Hash or Ruby Exception object to be logged
279
- #
280
- # time
281
- # The time at which the log entry was created
282
- #
283
- # duration
284
- # The time taken to complete a benchmark call
285
- #
286
- # tags
287
- # Any tags active on the thread when the log call was made
288
- #
289
- Log = Struct.new(:level, :thread_name, :name, :message, :payload, :time, :duration, :tags)
290
-
291
- # For JRuby include the Thread name rather than its id
292
- if defined? Java
293
- def self.thread_name
294
- Java::java.lang::Thread.current_thread.name
295
- end
296
- else
297
- def self.thread_name
298
- Thread.current.object_id
299
- end
300
- end
301
-
302
- # Verify and set the level
303
- def set_level(level)
304
- index = if level.is_a?(Integer)
305
- MAP_LEVELS[level]
306
- elsif level.is_a?(String)
307
- level = level.downcase.to_sym
308
- LEVELS.index(level)
309
- else
310
- LEVELS.index(level)
311
- end
312
-
313
- raise "Invalid level:#{level.inspect} being requested. Must be one of #{LEVELS.inspect}" unless index
314
- @level_index = index
315
- @level = level
316
- end
317
-
318
125
  # Start a separate appender thread responsible for reading log messages and
319
126
  # calling the appenders in it's thread
320
127
  def self.startup
321
128
  @@appender_thread = Thread.new do
322
129
  begin
323
- # #TODO Logger needs it's own "reliable" appender ;)
324
- # For example if an appender constantly fails
325
- # ( bad filename or path, invalid server )
326
130
  logger.debug "SemanticLogger::Logger Appender thread started"
327
131
  while message=queue.pop
328
132
  if message.is_a? Log
@@ -365,5 +169,11 @@ module SemanticLogger
365
169
  result
366
170
  end
367
171
 
172
+ # Formatting does not occur within this thread, it is done by each appender
173
+ # in the appender thread
174
+ def default_formatter
175
+ nil
176
+ end
177
+
368
178
  end
369
179
  end
@@ -27,7 +27,7 @@ module SemanticLogger #:nodoc:
27
27
  config = Rails.application.config
28
28
 
29
29
  # Set the default log level based on the Rails config
30
- SemanticLogger::Logger.level = config.log_level
30
+ SemanticLogger::Logger.default_level = config.log_level
31
31
 
32
32
  # Existing loggers are ignored because servers like trinidad supply their
33
33
  # own file loggers which would result in duplicate logging to the same log file
@@ -38,26 +38,24 @@ module SemanticLogger #:nodoc:
38
38
  FileUtils.mkdir_p File.dirname path
39
39
  end
40
40
 
41
- # First set the internal logger in case something goes wrong
42
- # with an appender
43
- SemanticLogger::Logger.logger = begin
44
- l = ::Logger.new(path)
45
- l.level = ::Logger.const_get(config.log_level.to_s.upcase)
46
- l
47
- end
48
-
49
41
  # Add the log file to the list of appenders
50
- SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new(path)
51
-
52
- #logger = ActiveSupport::TaggedLogging.new(logger) if defined?(ActiveSupport::TaggedLogging)
42
+ file_appender = SemanticLogger::Appender::File.new(path)
53
43
 
44
+ # Set internal logger to log to file only, in case another appender
45
+ # experiences logging problems
46
+ SemanticLogger::Logger.logger = file_appender
47
+ SemanticLogger::Logger.appenders << file_appender
48
+
54
49
  SemanticLogger::Logger.new(Rails)
55
50
  rescue StandardError
56
- SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new(STDERR)
51
+ # If not able to log to file, log to standard error with warning level only
52
+ SemanticLogger::Logger.default_level = :warn
53
+
54
+ file_appender = SemanticLogger::Appender::File.new(STDERR)
55
+ SemanticLogger::Logger.logger = file_appender
56
+ SemanticLogger::Logger.appenders << file_appender
57
57
 
58
58
  logger = SemanticLogger::Logger.new(Rails)
59
- logger.level = :warn
60
- #logger = ActiveSupport::TaggedLogging.new(logger) if defined?(ActiveSupport::TaggedLogging)
61
59
  logger.warn(
62
60
  "Rails Error: Unable to access log file. Please ensure that #{path} exists and is chmod 0666. " +
63
61
  "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
@@ -1,3 +1,3 @@
1
1
  module SemanticLogger #:nodoc
2
- VERSION = "0.5.3"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -0,0 +1,52 @@
1
+ # Allow test to be run in-place without requiring a gem install
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+
4
+ require 'rubygems'
5
+ require 'test/unit'
6
+ require 'shoulda'
7
+ require 'logger'
8
+ require 'semantic_logger'
9
+ require 'test/mock_logger'
10
+
11
+ # Unit Test for SemanticLogger::Appender::File
12
+ #
13
+ class AppenderFileTest < Test::Unit::TestCase
14
+ context SemanticLogger::Appender::File do
15
+ setup do
16
+ @time = Time.parse("2012-08-02 09:48:32.482")
17
+ @io = StringIO.new
18
+ @appender = SemanticLogger::Appender::File.new(@io)
19
+ @hash = { :session_id => 'HSSKLEU@JDK767', :tracking_number => 12345 }
20
+ @hash_str = @hash.inspect.sub("{", "\\{").sub("}", "\\}")
21
+ @thread_name = SemanticLogger::Base.thread_name
22
+ end
23
+
24
+ context "format logs into text form" do
25
+ should "handle no message or payload" do
26
+ @appender.debug
27
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ D \[\d+:#{@thread_name}\] SemanticLogger::Appender::File -- \n/, @io.string
28
+ end
29
+
30
+ should "handle message" do
31
+ @appender.debug 'hello world'
32
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ D \[\d+:#{@thread_name}\] SemanticLogger::Appender::File -- hello world\n/, @io.string
33
+ end
34
+
35
+ should "handle message and payload" do
36
+ @appender.debug 'hello world', @hash
37
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ D \[\d+:#{@thread_name}\] SemanticLogger::Appender::File -- hello world -- #{@hash_str}\n/, @io.string
38
+ end
39
+ end
40
+
41
+ context "for each log level" do
42
+ # Ensure that any log level can be logged
43
+ SemanticLogger::LEVELS.each do |level|
44
+ should "log #{level} information" do
45
+ @appender.send(level, 'hello world', @hash)
46
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:#{@thread_name}\] SemanticLogger::Appender::File -- hello world -- #{@hash_str}\n/, @io.string
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -98,7 +98,7 @@ class AppenderMongoDBTest < Test::Unit::TestCase
98
98
 
99
99
  context "for each log level" do
100
100
  # Ensure that any log level can be logged
101
- SemanticLogger::Logger::LEVELS.each do |level|
101
+ SemanticLogger::LEVELS.each do |level|
102
102
  should "log #{level} information" do
103
103
  @appender.log SemanticLogger::Logger::Log.new(level, 'thread', 'my_class', 'hello world -- Calculations', @hash, @time)
104
104
  document = @appender.collection.find_one
@@ -8,15 +8,16 @@ require 'logger'
8
8
  require 'semantic_logger'
9
9
  require 'test/mock_logger'
10
10
 
11
- # Unit Test for SemanticLogger::Appender::Logger
11
+ # Unit Test for SemanticLogger::Appender::Wrapper
12
12
  #
13
- class AppenderLoggerTest < Test::Unit::TestCase
14
- context SemanticLogger::Appender::Logger do
13
+ class AppenderWrapperTest < Test::Unit::TestCase
14
+ context SemanticLogger::Appender::Wrapper do
15
15
  setup do
16
16
  @time = Time.parse("2012-08-02 09:48:32.482")
17
17
  @mock_logger = MockLogger.new
18
- @appender = SemanticLogger::Appender::Logger.new(@mock_logger)
18
+ @appender = SemanticLogger::Appender::Wrapper.new(@mock_logger)
19
19
  @hash = { :session_id => 'HSSKLEU@JDK767', :tracking_number => 12345 }
20
+ @hash_str = @hash.inspect.sub("{", "\\{").sub("}", "\\}")
20
21
  end
21
22
 
22
23
  context "format logs into text form" do
@@ -55,7 +56,7 @@ class AppenderLoggerTest < Test::Unit::TestCase
55
56
  log.message = 'hello world'
56
57
  log.payload = @hash
57
58
  @appender.log(log)
58
- assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ D \[\d+:\] class -- hello world -- \{:session_id=>\"HSSKLEU@JDK767\", :tracking_number=>12345\}/, @mock_logger.message
59
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ D \[\d+:\] class -- hello world -- #{@hash_str}/, @mock_logger.message
59
60
  end
60
61
  end
61
62
 
@@ -63,8 +64,8 @@ class AppenderLoggerTest < Test::Unit::TestCase
63
64
  # Ensure that any log level can be logged
64
65
  Logger::Severity.constants.each do |level|
65
66
  should "log #{level.downcase.to_sym} info" do
66
- @appender.log SemanticLogger::Logger::Log.new(level.downcase.to_sym, 'thread', 'class', 'hello world -- Calculations', @hash, Time.now)
67
- assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:thread\] class -- hello world -- Calculations -- \{:session_id=>\"HSSKLEU@JDK767\", :tracking_number=>12345\}/, @mock_logger.message
67
+ @appender.log SemanticLogger::Logger::Log.new(level.downcase.to_sym, 'thread', 'class', 'hello world', @hash, Time.now)
68
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:thread\] class -- hello world -- #{@hash_str}/, @mock_logger.message
68
69
  end
69
70
  end
70
71
  end
data/test/logger_test.rb CHANGED
@@ -16,13 +16,14 @@ class LoggerTest < Test::Unit::TestCase
16
16
  setup do
17
17
  # Use a mock logger that just keeps the last logged entry in an instance variable
18
18
  @mock_logger = MockLogger.new
19
- @appender = SemanticLogger::Appender::Logger.new(@mock_logger)
19
+ @appender = SemanticLogger::Appender::Wrapper.new(@mock_logger)
20
20
  SemanticLogger::Logger.appenders << @appender
21
21
 
22
22
  # Use this test's class name as the application name in the log output
23
- @logger = SemanticLogger::Logger.new('LoggerTest', :level => :trace)
23
+ @logger = SemanticLogger::Logger.new('LoggerTest', :trace)
24
24
 
25
25
  @hash = { :session_id => 'HSSKLEU@JDK767', :tracking_number => 12345 }
26
+ @hash_str = @hash.inspect.sub("{", "\\{").sub("}", "\\}")
26
27
  end
27
28
 
28
29
  teardown do
@@ -30,11 +31,11 @@ class LoggerTest < Test::Unit::TestCase
30
31
  end
31
32
 
32
33
  # Ensure that any log level can be logged
33
- SemanticLogger::Logger::LEVELS.each do |level|
34
+ SemanticLogger::LEVELS.each do |level|
34
35
  should "log #{level} info" do
35
36
  @logger.send(level, 'hello world', @hash) { "Calculations" }
36
37
  SemanticLogger::Logger.flush
37
- assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:.+\] LoggerTest -- hello world -- Calculations -- \{:session_id=>\"HSSKLEU@JDK767\", :tracking_number=>12345\}/, @mock_logger.message
38
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:.+\] LoggerTest -- hello world -- Calculations -- #{@hash_str}/, @mock_logger.message
38
39
  end
39
40
  end
40
41
 
@@ -58,12 +59,13 @@ class LoggerTest < Test::Unit::TestCase
58
59
  end
59
60
 
60
61
  should "add payload to log entries" do
62
+ hash = {:tracking_number=>"123456", :even=>2, :more=>"data"}
63
+ hash_str = hash.inspect.sub("{", "\\{").sub("}", "\\}")
61
64
  @logger.with_payload(:tracking_number => '123456') do
62
65
  @logger.with_payload(:even => 2, :more => 'data') do
63
66
  @logger.info('Hello world')
64
67
  SemanticLogger::Logger.flush
65
- # TODO make test ignore order of Hash elements
66
- assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:.+\] LoggerTest -- Hello world -- \{:even=>2, :more=>\"data\", :tracking_number=>\"123456\"\}/, @mock_logger.message
68
+ assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:.+\] LoggerTest -- Hello world -- #{hash_str}/, @mock_logger.message
67
69
  end
68
70
  end
69
71
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 5
8
- - 3
9
- version: 0.5.3
7
+ - 6
8
+ - 0
9
+ version: 0.6.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Reid Morrison
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2012-09-14 00:00:00 -04:00
17
+ date: 2012-09-18 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -54,8 +54,9 @@ files:
54
54
  - FUTURE.rb
55
55
  - init.txt
56
56
  - lib/semantic_logger/appender/file.rb
57
- - lib/semantic_logger/appender/logger.rb
58
57
  - lib/semantic_logger/appender/mongodb.rb
58
+ - lib/semantic_logger/appender/wrapper.rb
59
+ - lib/semantic_logger/base.rb
59
60
  - lib/semantic_logger/logger.rb
60
61
  - lib/semantic_logger/railtie.rb
61
62
  - lib/semantic_logger/version.rb
@@ -63,8 +64,9 @@ files:
63
64
  - LICENSE.txt
64
65
  - Rakefile
65
66
  - README.md
66
- - test/appender_logger_test.rb
67
+ - test/appender_file_test.rb
67
68
  - test/appender_mongodb_test.rb
69
+ - test/appender_wrapper_test.rb
68
70
  - test/logger_test.rb
69
71
  - test/mock_logger.rb
70
72
  has_rdoc: true