semantic_logger 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,11 +3,9 @@ semantic-logger-ruby
3
3
 
4
4
  * http://github.com/ClarityServices/semantic-logger-ruby
5
5
 
6
- Feedback is welcome and appreciated :)
7
-
8
6
  ### Introduction
9
7
 
10
- SemanticLogger is a Logger that supports logging of meta-data, not only text messages
8
+ SemanticLogger is a Logger that supports logging of meta-data, along with text messages
11
9
 
12
10
  Machines can understand the logged data without having to use
13
11
  complex Regular Expressions or other text parsing techniques
@@ -36,30 +34,348 @@ This can be changed over time to:
36
34
  :table => "users",
37
35
  :action => "query")
38
36
 
39
- Using this semantic example, a machine can easily find all queries for table
40
- 'users' that took longer than 100 ms.
37
+ Using the MongoDB appender, we can easily find all queries for table 'users'
38
+ that took longer than 100 ms:
41
39
 
42
- db.logs.find({"metadata.table":"users", "metadata.action":"query"})
40
+ db.logs.find({"payload.table":"users", "payload.action":"query", "payload.duration":{$gt:100} })
43
41
 
44
42
  Since SemanticLogger can call existing Loggers, it does not force end-users
45
43
  to have to adopt a Semantic aware adapter. Although, such adapters create
46
44
  tremendous value in the problem monitoring and determination processes.
47
45
 
48
- For ease of use each Appender can be used directly if required. It is preferred
49
- however to use the SemanticLogger interface and supply the configuration to it.
50
- In this way the adapter can be changed without modifying the application
51
- source code.
46
+ ### Logging API
47
+
48
+ #### Standard Logging methods
49
+
50
+ The Semantic Logger logging API supports the existing logging interface for
51
+ the Rails and Ruby Loggers. For example:
52
+
53
+ logger.info("Hello World")
54
+
55
+ Or to query whether a specific log level is set
56
+
57
+ logger.info?
58
+
59
+ The following logging methods are all available
60
+
61
+ logger.trace(message, payload) { # optional block }
62
+ logger.debug(message, payload) { # optional block }
63
+ logger.info(message, payload) { # optional block }
64
+ logger.warn(message, payload) { # optional block }
65
+ logger.error(message, payload) { # optional block }
66
+ logger.fatal(message, payload) { # optional block }
67
+
68
+ Parameters
69
+
70
+ - message: The text message to log.
71
+ Mandatory only if no block is supplied
72
+ - payload: Optional, either a Ruby Exception object or a Hash
73
+ - block: The optional block is executed only if the corresponding log level
74
+ is active. Can be used to prevent unnecessary calculations of debug data in
75
+ production.
76
+
77
+ Examples:
78
+
79
+ logger.debug("Calling Supplier")
80
+
81
+ logger.debug("Calling Supplier", :request => 'update', :user => 'Jack')
82
+
83
+ logger.debug { "A total of #{result.inject(0) {|sum, i| i+sum }} were processed" }
84
+
85
+ ### Exceptions
86
+
87
+ The Semantic Logger adds an extra parameter to the existing log methods so that
88
+ a corresponding Exception can be logged in a standard way
89
+
90
+ begin
91
+ # ... Code that can raise an exception
92
+ rescue Exception => exception
93
+ logger.error("Oops external call failed", exception)
94
+ # Re-raise or handle the exception
95
+ raise exception
96
+ end
97
+
98
+
99
+ #### Payload
100
+
101
+ The Semantic Logger adds an extra parameter to the existing log methods so that
102
+ additional payload can be logged, such as a Hash or a Ruby Exception object.
103
+
104
+ logger.info("Oops external call failed", :result => :failed, :reason_code => -10)
105
+
106
+ The additional payload is machine readable so that we don't have to write complex
107
+ regular expressions so that a program can analyze log output. With the MongoDB
108
+ appender the payload is written directly to MongoDB as part of the document and
109
+ is therefore fully searchable
110
+
111
+ #### Benchmarking
112
+
113
+ Another common logging requirement is to measure the time it takes to execute a block
114
+ of code based on the log level. For example:
115
+
116
+ Rails.logger.benchmark_info "Calling external interface" do
117
+ # Code to call external service ...
118
+ end
119
+
120
+ The following output will be written to file:
121
+
122
+ 2012-08-30 15:37:29.474 I [48308:ScriptThreadProcess: script/rails] Rails -- Calling external interface (5.2ms)
123
+
124
+ If an exception is raised during the block the exception is logged
125
+ at the same log level as the benchmark along with the duration and message
52
126
 
53
- # Future
54
- - Web Interface to view and search log information
55
- - Override the log level at the appender level
127
+ #### Logging levels
56
128
 
57
- # Configuration
129
+ The following logging levels are available through Semantic Logger
130
+
131
+ :trace, :debug, :info, :warn, :error, :fatal
132
+
133
+ The log levels are listed above in the order of precedence with the most detail to the least.
134
+ For example :debug would include :info, :warn, :error, :fatal levels but not :trace
135
+ And :fatal would only log :fatal error messages and nothing else
136
+
137
+ :unknown has been mapped to :fatal for Rails and Ruby Logger
138
+
139
+ :trace is a new level that is often used for tracing low level calls such
140
+ as the data sent or received to external web services. It is also commonly used
141
+ in the development environment for low level trace logging of methods calls etc.
142
+
143
+ If only the rails logger is being used, then :trace level calls will be logged
144
+ as debug calls only if the log level is set to trace
145
+
146
+ #### Changing the Class name for Log Entries
147
+
148
+ When Semantic Logger is included on a Rails project it automatically replaces the
149
+ loggers for Rails, ActiveRecord::Base, ActionController::Base, and ActiveResource::Base
150
+ with wrappers that set their Class name. For example in railtie.rb:
151
+
152
+ ActiveRecord::Base.logger = SemanticLogger::Logger.new(ActiveRecord)
153
+
154
+ By replacing their loggers we now get the class name in the text logging output:
155
+
156
+ 2012-08-30 15:24:13.439 D [47900:main] ActiveRecord -- SQL (12.0ms) SELECT `schema_migrations`.`version` FROM `schema_migrations`
157
+
158
+ It is recommended to include a class specific logger for all major classes that will
159
+ be logging. For Example:
160
+
161
+ class ExternalSupplier
162
+ # Gem sync_attr is a dependency of semantic_logger so is already installed
163
+ include SyncAttr
164
+
165
+ # Lazy initializes the class logger on it's first call in a thread-safe way
166
+ sync_cattr_reader :logger do
167
+ SemanticLogger::Logger.new(self)
168
+ end
169
+
170
+ def call(params)
171
+ self.class.logger.benchmark_info "Calling external interface" do
172
+ # Code to call external service ...
173
+ end
174
+ end
175
+ end
176
+
177
+ This will result in the log output identitying the log entry as from the ExternalSupplier class
178
+
179
+ 2012-08-30 15:37:29.474 I [48308:ScriptThreadProcess: script/rails] ExternalSupplier -- Calling external interface (5.2ms)
180
+
181
+ #### Tagged Logging
182
+
183
+ logger.with_tags(tracking_number) do
184
+ logger.debug("Hello World")
185
+ # ...
186
+ end
187
+
188
+ #### Payload injected logging
189
+
190
+ logger.with_payload(:user => 'Jack') do
191
+ logger.debug("Hello World")
192
+ # ...
193
+ end
194
+
195
+ ### Configuration
58
196
 
59
197
  The Semantic Logger follows the principle where multiple appenders can be active
60
198
  at the same time. This allows one to log to MongoDB and the Rails
61
199
  ActiveResource::BufferedLogger at the same time.
62
200
 
201
+ #### Rails Configuration
202
+
203
+ Add the following line to Gemfile
204
+
205
+ gem 'semantic_logger'
206
+
207
+ Also add the following line to Gemfile if you want to log to MongoDB
208
+
209
+ gem 'mongo'
210
+
211
+ Install required gems with bundler
212
+
213
+ bundle install
214
+
215
+ This will automatically replace the standard Rails logger with Semantic Logger
216
+ which will write all log data to the configured Rails logger.
217
+
218
+ By default Semantic Logger will detect the log level from Rails. To set the
219
+ log level explicitly, add the following line to
220
+ config/environments/production.rb inside the Application.configure block
221
+
222
+ config.log_level = :trace
223
+
224
+ To log to both the Rails logger and MongoDB add the following lines to
225
+ config/environments/production.rb inside the Application.configure block
226
+
227
+ config.after_initialize do
228
+ # Re-use the existing MongoDB connection, or create a new one here
229
+ db = Mongo::Connection.new['production_logging']
230
+
231
+ # Besides logging to the standard Rails logger, also log to MongoDB
232
+ config.semantic_logger.appenders << SemanticLogger::Appender::MongoDB.new(
233
+ :db => db,
234
+ :collection_size => 25.gigabytes
235
+ )
236
+ end
237
+
238
+ #### Custom Formatters
239
+
240
+ The formatting for each appender can be replaced with custom code. To replace the
241
+ existing formatter supply a block of code when creating the appender.
242
+
243
+ For example to replace the Rails or Ruby text log formatter, in the environment configuration file:
244
+
245
+ config.after_initialize do
246
+ # Since the Rails logger is already initialized, replace its default formatter
247
+ config.semantic_logger.appenders.first.formatter = Proc.new do |log|
248
+ # log is a struct with the following fields:
249
+ #
250
+ # level
251
+ # Log level of the supplied log call
252
+ # :trace, :debug, :info, :warn, :error, :fatal
253
+ #
254
+ # thread_name
255
+ # Name of the thread in which the logging call was called
256
+ #
257
+ # name
258
+ # Class name supplied to the logging instance
259
+ #
260
+ # message
261
+ # Text message to be logged
262
+ #
263
+ # payload
264
+ # Optional Hash or Ruby Exception object to be logged
265
+ #
266
+ # time
267
+ # The time at which the log entry was created
268
+ #
269
+ # duration
270
+ # The time taken to complete a benchmark call
271
+ #
272
+ # tags
273
+ # Any tags active on the thread when the log call was made
274
+ #
275
+
276
+ message = log.message.to_s
277
+ tags = log.tags.collect { |tag| "[#{tag}]" }.join(" ") + " " if log.tags && (log.tags.size > 0)
278
+
279
+ if log.payload
280
+ if log.payload.is_a?(Exception)
281
+ exception = log.payload
282
+ message << " -- " << "#{exception.class}: #{exception.message}\n#{(exception.backtrace || []).join("\n")}"
283
+ else
284
+ message << " -- " << log.payload.inspect
285
+ end
286
+ end
287
+
288
+ 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}"
289
+ str << " (#{'%.1f' % log.duration}ms)" if log.duration
290
+ str
291
+ end
292
+ end
293
+
294
+ For example to replace the MongoDB formatter, in the environment configuration file:
295
+
296
+ config.after_initialize do
297
+ # Log to MongoDB and supply a custom document formatter
298
+ mongodb_appender = SemanticLogger::Appender::MongoDB.new(
299
+ :db => Cache::Work.db,
300
+ :collection_size => 25.gigabytes
301
+ ) do |log|
302
+ # log is a struct with the following fields:
303
+ # level
304
+ # Log level of the supplied log call
305
+ # :trace, :debug, :info, :warn, :error, :fatal
306
+ #
307
+ # thread_name
308
+ # Name of the thread in which the logging call was called
309
+ #
310
+ # name
311
+ # Class name supplied to the logging instance
312
+ #
313
+ # message
314
+ # Text message to be logged
315
+ #
316
+ # payload
317
+ # Optional Hash or Ruby Exception object to be logged
318
+ #
319
+ # time
320
+ # The time at which the log entry was created
321
+ #
322
+ # duration
323
+ # The time taken to complete a benchmark call
324
+ #
325
+ # tags
326
+ # Any tags active on the thread when the log call was made
327
+ #
328
+
329
+ # Return a document (Hash) of the data to be saved to MongoDB
330
+ document = {
331
+ :time => log.time,
332
+ :host_name => SemanticLogger::Appender::MongoDB.host_name,
333
+ :pid => $PID,
334
+ :thread_name => log.thread_name,
335
+ :name => log.name,
336
+ :level => log.level,
337
+ }
338
+ document[:application] = 'MyApplication'
339
+ document[:message] = SemanticLogger::Appender::MongoDB.strip_colorizing(log.message) if log.message
340
+ document[:duration] = log.duration if log.duration
341
+ document[:tags] = log.tags if log.tags && (log.tags.size > 0)
342
+
343
+ if log.payload
344
+ if log.payload.is_a?(Exception)
345
+ exception = log.payload
346
+ document[:payload] = {
347
+ :exception => exception.class.name,
348
+ :message => exception.message,
349
+ :backtrace => exception.backtrace
350
+ }
351
+ else
352
+ document[:payload] = log.payload
353
+ end
354
+ end
355
+ document
356
+ end
357
+ config.semantic_logger.appenders << mongodb_appender
358
+ end
359
+
360
+ ### Architecture & Performance
361
+
362
+ In order to ensure that logging does not hinder the performance of the application
363
+ all log entries are written to thread-safe Queue. A separate thread is responsible
364
+ for writing the log entries to each of the appenders.
365
+
366
+ In this way formatting and disk or network write delays will not affect the
367
+ performance of the application. Also adding more than one appender does not affect
368
+ the runtime performance of the application
369
+
370
+ The additional thread is automatically started on initialization. When the program
371
+ terminates it will complete writing out all log data and flush the appenders before
372
+ the program exits.
373
+
374
+ Calling SemanticLogger::Logger#flush will wait until all outstanding log messages
375
+ have been written and flushed to their respective appenders before returning.
376
+ Since all logging is now from this thread calling flush is no longer thread
377
+ specific.
378
+
63
379
  ### Dependencies
64
380
 
65
381
  - Ruby MRI 1.8.7 (or above) Or, JRuby 1.6.3 (or above)
@@ -68,7 +384,15 @@ ActiveResource::BufferedLogger at the same time.
68
384
 
69
385
  ### Install
70
386
 
71
- gem install semantic-logger
387
+ gem install semantic-logger
388
+
389
+ To log to MongoDB
390
+
391
+ gem install mongo
392
+
393
+ ### Future
394
+
395
+ - Web Interface to view and search log information stored in MongoDB
72
396
 
73
397
  Development
74
398
  -----------
@@ -94,9 +418,6 @@ Once you've made your great commits:
94
418
  4. Create an [Issue](http://github.com/ClarityServices/semantic-logger/issues) with a link to your branch
95
419
  5. That's it!
96
420
 
97
- We are looking for volunteers to create implementations in other languages such as:
98
- * Java, C# and Go
99
-
100
421
  Meta
101
422
  ----
102
423
 
data/Rakefile CHANGED
@@ -21,6 +21,7 @@ task :gem do |t|
21
21
  spec.description = "Machine readable document oriented logging with support for MongoDB and text files"
22
22
  spec.files = FileList["./**/*"].exclude('*.gem', 'nbproject').map{|f| f.sub(/^\.\//, '')}
23
23
  spec.has_rdoc = true
24
+ spec.add_dependency 'sync_attr'
24
25
  spec.add_development_dependency 'shoulda'
25
26
  end
26
27
  Gem::Builder.new(gemspec).build
@@ -1,4 +1,4 @@
1
- # Temp until in Gemfile
1
+ # Include sync_attr dependency
2
2
  require 'sync_attr'
3
3
 
4
4
  module SemanticLogger
@@ -8,6 +8,7 @@ module SemanticLogger
8
8
  module Appender
9
9
  class Logger
10
10
  attr_reader :logger
11
+ attr_accessor :formatter
11
12
 
12
13
  # Create a Logger or Rails Logger appender instance
13
14
  #
@@ -109,8 +109,11 @@ module SemanticLogger
109
109
  # * Document updates cannot make them any larger
110
110
  # * Documents are always stored in insertion order
111
111
  # * A find will always return the documents in their insertion order
112
+ #
113
+ # Creates an index based on tags to support faster lookups
112
114
  def create_indexes
113
115
  db.create_collection(collection_name, {:capped => true, :size => @collection_size, :max => @collection_max})
116
+ collection.ensure_index('tags')
114
117
  end
115
118
 
116
119
  # Purge all data from the capped collection by dropping the collection
@@ -171,6 +174,16 @@ module SemanticLogger
171
174
  message.to_s.gsub(/(\e(\[([\d;]*[mz]?))?)?/, '').strip
172
175
  end
173
176
 
177
+ # Default host_name to use if none is supplied to the appenders initializer
178
+ def self.host_name
179
+ @@host_name ||= Socket.gethostname.split('.').first
180
+ end
181
+
182
+ # Override the default host_name
183
+ def self.host_name=(host_name)
184
+ @@host_name = host_name
185
+ end
186
+
174
187
  # Log the message to MongoDB
175
188
  def log(log)
176
189
  # Insert log entry into Mongo
@@ -12,16 +12,23 @@
12
12
  # log = Logger.new(STDOUT)
13
13
  # log.level = Logger::DEBUG
14
14
  #
15
- # SemanticLogger::Manager.register_appender(SemanticLogger::Appender::Logger.new(log))
15
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::Logger.new(log)
16
16
  #
17
17
  # logger = SemanticLogger::Logger.new("my.app.class")
18
18
  # logger.debug("Login time", :user => 'Joe', :duration => 100, :ip_address=>'127.0.0.1')
19
19
  #
20
- # # Now log to the Logger above as well as Mongo at the same time
20
+ # # Now log to the Logger above as well as MongoDB at the same time
21
21
  #
22
- # SemanticLogger::Manager.register_appender(SemanticLogger::Appender::Mongo.new(cfg))
22
+ # db = Mongo::Connection.new['production_logging']
23
+ #
24
+ # SemanticLogger::Logger.appenders << SemanticLogger::Appender::MongoDB.new(
25
+ # :db => db,
26
+ # :collection_size => 25.gigabytes
27
+ # )
23
28
  # ...
29
+ # # This will be logged to both the Ruby Logger and MongoDB
24
30
  # logger.debug("Login time", :user => 'Mary', :duration => 230, :ip_address=>'192.168.0.1')
31
+ #
25
32
  module SemanticLogger
26
33
  class Logger
27
34
  include SyncAttr
@@ -213,12 +220,6 @@ module SemanticLogger
213
220
  queue.size
214
221
  end
215
222
 
216
- # Flush all pending log entry disk, database, etc.
217
- # All pending log writes are completed and each appender is flushed in turn
218
- def flush
219
- self.class.flush
220
- end
221
-
222
223
  # Flush all pending log entry disk, database, etc.
223
224
  # All pending log writes are completed and each appender is flushed in turn
224
225
  def self.flush
@@ -251,6 +252,33 @@ module SemanticLogger
251
252
  Queue.new
252
253
  end
253
254
 
255
+ # Struct Log
256
+ #
257
+ # level
258
+ # Log level of the supplied log call
259
+ # :trace, :debug, :info, :warn, :error, :fatal
260
+ #
261
+ # thread_name
262
+ # Name of the thread in which the logging call was called
263
+ #
264
+ # name
265
+ # Class name supplied to the logging instance
266
+ #
267
+ # message
268
+ # Text message to be logged
269
+ #
270
+ # payload
271
+ # Optional Hash or Ruby Exception object to be logged
272
+ #
273
+ # time
274
+ # The time at which the log entry was created
275
+ #
276
+ # duration
277
+ # The time taken to complete a benchmark call
278
+ #
279
+ # tags
280
+ # Any tags active on the thread when the log call was made
281
+ #
254
282
  Log = Struct.new(:level, :thread_name, :name, :message, :payload, :time, :duration, :tags)
255
283
 
256
284
  # For JRuby include the Thread name rather than its id
@@ -1,3 +1,3 @@
1
1
  module SemanticLogger #:nodoc
2
- VERSION = "0.3.3"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -33,7 +33,7 @@ class LoggerTest < Test::Unit::TestCase
33
33
  SemanticLogger::Logger::LEVELS.each do |level|
34
34
  should "log #{level} info" do
35
35
  @logger.send(level, 'hello world', @hash) { "Calculations" }
36
- @logger.flush
36
+ SemanticLogger::Logger.flush
37
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
38
  end
39
39
  end
@@ -42,7 +42,7 @@ class LoggerTest < Test::Unit::TestCase
42
42
  should "add tags to log entries" do
43
43
  @logger.with_tags('12345', 'DJHSFK') do
44
44
  @logger.info('Hello world')
45
- @logger.flush
45
+ SemanticLogger::Logger.flush
46
46
  assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:.+\] \[12345\] \[DJHSFK\] LoggerTest -- Hello world/, @mock_logger.message
47
47
  end
48
48
  end
@@ -51,7 +51,7 @@ class LoggerTest < Test::Unit::TestCase
51
51
  @logger.with_tags('First Level', 'tags') do
52
52
  @logger.with_tags('Second Level') do
53
53
  @logger.info('Hello world')
54
- @logger.flush
54
+ SemanticLogger::Logger.flush
55
55
  assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:.+\] \[First Level\] \[tags\] \[Second Level\] LoggerTest -- Hello world/, @mock_logger.message
56
56
  end
57
57
  end
@@ -61,7 +61,7 @@ class LoggerTest < Test::Unit::TestCase
61
61
  @logger.with_payload(:tracking_number => '123456') do
62
62
  @logger.with_payload(:more => 'data', :even => 2) do
63
63
  @logger.info('Hello world')
64
- @logger.flush
64
+ SemanticLogger::Logger.flush
65
65
  assert_match /\d+-\d+-\d+ \d+:\d+:\d+.\d+ \w \[\d+:.+\] LoggerTest -- Hello world -- \{:more=>\"data\", :even=>2, :tracking_number=>\"123456\"\}/, @mock_logger.message
66
66
  end
67
67
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 3
8
- - 3
9
- version: 0.3.3
7
+ - 4
8
+ - 0
9
+ version: 0.4.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Reid Morrison
@@ -14,11 +14,11 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2012-08-30 00:00:00 -04:00
17
+ date: 2012-09-05 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: shoulda
21
+ name: sync_attr
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  requirements:
@@ -27,8 +27,20 @@ dependencies:
27
27
  segments:
28
28
  - 0
29
29
  version: "0"
30
- type: :development
30
+ type: :runtime
31
31
  version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: shoulda
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :development
43
+ version_requirements: *id002
32
44
  description: Machine readable document oriented logging with support for MongoDB and text files
33
45
  email:
34
46
  - reidmo@gmail.com