semantic_logger 0.0.2

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.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'rubygems'
5
+ require 'rake/clean'
6
+ require 'rake/testtask'
7
+ require 'date'
8
+ require 'semantic_logger/version'
9
+
10
+ desc "Build gem"
11
+ task :gem do |t|
12
+ gemspec = Gem::Specification.new do |spec|
13
+ spec.name = 'semantic_logger'
14
+ spec.version = SemanticLogger::VERSION
15
+ spec.platform = Gem::Platform::RUBY
16
+ spec.authors = ['Reid Morrison']
17
+ spec.email = ['reidmo@gmail.com']
18
+ spec.homepage = 'https://github.com/ClarityServices/semantic_logger'
19
+ spec.date = Date.today.to_s
20
+ spec.summary = "Semantic Logger for Ruby, and Ruby on Rails"
21
+ spec.description = "Logging with additional machine readable and parseable log data"
22
+ spec.files = FileList["./**/*"].exclude('*.gem', 'nbproject').map{|f| f.sub(/^\.\//, '')}
23
+ spec.has_rdoc = true
24
+ spec.add_development_dependency 'shoulda'
25
+ end
26
+ Gem::Builder.new(gemspec).build
27
+ end
28
+
29
+ desc "Run Test Suite"
30
+ task :test do
31
+ Rake::TestTask.new(:functional) do |t|
32
+ t.test_files = FileList['test/*_test.rb']
33
+ t.verbose = true
34
+ end
35
+
36
+ Rake::Task['functional'].invoke
37
+ end
data/init.txt ADDED
@@ -0,0 +1,59 @@
1
+ -- if (($0 == 'irb' || $0 == 'jirb') && ENV['RAILS_CONSOLE_STDOUT']) || ENV['RAILS_LOGGER_STDOUT']
2
+ -- config.logger = Logger.new(STDOUT)
3
+ -- end
4
+ ++ # Setup Semantic Logging
5
+ ++# require 'lib/semantic_logger'
6
+ ++# if (($0 == 'irb' || $0 == 'jirb') && ENV['RAILS_CONSOLE_STDOUT']) || ENV['RAILS_LOGGER_STDOUT']
7
+ ++# config.logger = SemanticLogger::LoggerAppender.new(Logger.new(STDOUT))
8
+ ++# else
9
+ ++# exc = nil
10
+ ++# begin
11
+ ++# # Don't use Mongo Log Appender in dev and test
12
+ ++# unless false #Rails.env.test? #or Rails.env.development?
13
+ ++# require 'mongo'
14
+ ++# require 'lib/auto_failover_repl_set_connection'
15
+ ++# # Log only to Mongo
16
+ ++# cfg = YAML.load_file(File.join(Rails.root, "config", "mongo.yml"))[Rails.env]
17
+ ++# conn = Mongo::AutoFailoverReplSetConnection.from_uri(cfg['uri'],cfg['options'] || {})
18
+ ++# SemanticLogger::MongoAppender.db = conn.db("logger_#{Rails.env}")
19
+ ++# SemanticLogger::MongoAppender.create_indexes
20
+ ++# config.logger = SemanticLogger::MongoAppender.new('Rails', config.log_level)
21
+ ++# end
22
+ ++# rescue StandardError => exc
23
+ ++# end
24
+ ++#
25
+ ++# begin
26
+ ++# unless config.logger
27
+ ++# # Wrap the BufferedLogger with the Semantic Logger
28
+ ++# logger = ActiveSupport::BufferedLogger.new(config.log_path)
29
+ ++# logger.level = ActiveSupport::BufferedLogger.const_get(config.log_level.to_s.upcase)
30
+ ++# logger.auto_flushing = false if config.environment == "production"
31
+ ++# config.logger = SemanticLogger::LoggerAppender.new(logger)
32
+ ++# config.logger.error("Failed to start Mongo Logger, falling back to File Logging", exc) if exc
33
+ ++# end
34
+ ++# rescue StandardError => exc
35
+ ++# end
36
+ ++#
37
+ ++# unless config.logger
38
+ ++# config.logger = ActiveSupport::BufferedLogger.new(STDERR)
39
+ ++# config.logger.level = ActiveSupport::BufferedLogger::WARN
40
+ ++# config.logger.error("Failed to start SemanticLogger::LoggerAppender")
41
+ ++# config.logger.error(exc) if exc
42
+ ++# config.logger.warn(
43
+ ++# "Rails Error: Unable to access log file. Please ensure that #{config.log_path} exists and is chmod 0666. " +
44
+ ++# "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
45
+ ++# )
46
+ ++# end
47
+ ++
48
+ ++ # Rails.logger =
49
+ ++# def initialize_framework_logging
50
+ ++# for framework in ([ :active_record, :action_controller, :action_mailer ] & configuration.frameworks)
51
+ ++# framework.to_s.camelize.constantize.const_get("Base").logger ||= Rails.logger
52
+ ++# end
53
+ ++#
54
+ ++# ActiveSupport::Dependencies.logger ||= Rails.logger
55
+ ++# Rails.cache.logger ||= Rails.logger
56
+ ++# end
57
+ ++
58
+ ++
59
+ ++# end
@@ -0,0 +1,102 @@
1
+ # Logger appender
2
+ #
3
+ # Maps the SemanticLogger API's to the Rails log, log4j, or Ruby Logger
4
+ #
5
+ # Installation:
6
+ # Rails.logger = SemanticLogger::Appender::Logger.new(Rails.logger)
7
+ #
8
+ # Also works with the Ruby Logger
9
+ # require 'logger'
10
+ # require 'semantic_logger'
11
+ # logger = Logger.new(STDOUT)
12
+ # Rails.log = SemanticLogger::Appender::Logger.new(logger)
13
+ #
14
+ # ActiveResource::BufferedLogger
15
+ # ...
16
+ #
17
+ # The log level is controlled by the Logging implementation passed into
18
+ # this appender
19
+ module SemanticLogger
20
+ module Appender
21
+ class Logger
22
+ attr_reader :logger
23
+
24
+ def initialize(logger)
25
+ raise "logger cannot be null when initiailizing the SemanticLogging::Appender::Logger" unless logger
26
+ @logger = logger
27
+ end
28
+
29
+ # The default log formatter
30
+ # Generates logs of the form:
31
+ # 2011-07-19 14:36:15.660 D [1149:ScriptThreadProcess] Rails -- Hello World
32
+ @@formatter = Proc.new do |level, name, message, time, duration|
33
+ str = "#{time.strftime("%Y-%m-%d %H:%M:%S")}.#{"%03d" % (time.usec/1000)} #{level.to_s[0..0].upcase} [#{$$}:#{thread_name}] #{name} -- #{message}\n"
34
+ str << " (#{duration}ms)" if duration
35
+ str
36
+ end
37
+
38
+ # For JRuby include the Thread name rather than its id
39
+ if defined? Java
40
+ def self.thread_name
41
+ Java::java.lang::Thread.current_thread.name
42
+ end
43
+ else
44
+ def self.thread_name
45
+ Thread.object_id
46
+ end
47
+ end
48
+
49
+ # Allow the global formatter to be replaced
50
+ def self.formatter=(formatter)
51
+ @@formatter = formatter
52
+ end
53
+
54
+ # Pass log calls to the underlying Rails, log4j or Ruby logger
55
+ # trace entries are mapped to debug since :trace is not supported by the
56
+ # Ruby or Rails Loggers
57
+ def log(level, name, message, hash, &block)
58
+ @logger.send(level == :trace ? :debug : level) { self.class.format_message(level, name, message, hash, &block) }
59
+ end
60
+
61
+ # Convert a semantic log entry into plain text
62
+ def self.format_message(level, name, message, hash=nil, &block)
63
+ # TODO need to define :try if not already defined. E.g. Outside Rails
64
+ msg = time = duration = nil
65
+ if hash
66
+ msg = hash.delete(:message)
67
+ time = hash.delete(:time)
68
+ duration = hash.delete(:duration)
69
+ end
70
+ msg ||= message.to_s
71
+ time ||= Time.now
72
+
73
+ msg << " -- " << self.msg2str(hash) if hash
74
+ msg << " -- " << self.msg2str(block.call) if block
75
+ @@formatter.call(level, name, msg, time, duration)
76
+ end
77
+
78
+ # Convert Objects to Strings for text based logging
79
+ def self.msg2str(message)
80
+ case message
81
+ when ::String
82
+ message
83
+ when ::Exception
84
+ "#{message.class}: #{message.message}\n#{(message.backtrace || []).join("\n")}"
85
+ when ::Hash
86
+ # With a hash, the message can be an element of the hash itself
87
+ if msg = message[:message]
88
+ # Cannot change supplied hash
89
+ hash = message.clone
90
+ hash.delete(:message)
91
+ "#{msg} #{hash.inspect}"
92
+ else
93
+ message.inspect
94
+ end
95
+ else
96
+ message.inspect
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,219 @@
1
+ module SemanticLogger
2
+ module Appender
3
+ # The Mongo Appender for the SemanticLogger
4
+ # clarity_log Schema:
5
+ # _id : ObjectId("4d9cbcbf7abb3abdaf9679cd"),
6
+ # time : ISODate("2011-04-06T19:19:27.006Z"),
7
+ # host_name : "Name of the host on which this log entry originated",
8
+ # application: 'Name of application or service logging the data - clarity_base, nginx, tomcat',
9
+ # pid : process id
10
+ # thread : "name of thread",
11
+ # name : "ch.qos.logback.classic.db.mongo.MongoDBAppenderTest",
12
+ # level : 'trace|debug|warn|info|error',
13
+ # message : "blah blah",
14
+ # duration : ms, # Set by Logger#benchmark
15
+ # tracking_number : "Some tracking id"
16
+ # metadata : {
17
+ # Optional. Any user supplied data, including any thread specific context variables
18
+ # values supplied on a per log entry will override any thread context values
19
+ # }
20
+ # # When an exception is supplied as the first or second parameter
21
+ # # If supplied as the first parameter, message='exception name'
22
+ # exception: {
23
+ # name: 'MyException',
24
+ # description: 'blah',
25
+ # stack_trace: []
26
+ # }
27
+ #
28
+ # # For trace and debug level logging, the following can also be logged
29
+ # # for all levels. Not on for higher levels due to performance impact
30
+ # source: {
31
+ # filename:
32
+ # method:
33
+ # line_number:
34
+ # }
35
+ #
36
+ # # Future, the Rails around filter can log the following additional data
37
+ # controller:
38
+ # action:
39
+ # duration: 'ms'
40
+ # http_verb: 'get|post|..'
41
+ # params: Hash
42
+ #
43
+ # tracking_number: 'user defined tracking number'
44
+ class MongoDB
45
+ attr_reader :db, :collection_name
46
+ attr_accessor :formatter, :host_name, :safe, :application
47
+
48
+ # Create a MongoDB Appender instance
49
+ # SemanticLogger::Appender::MongoDB.new(
50
+ # :db => Cache::Work.db
51
+ # )
52
+ #
53
+ # Parameters:
54
+ # :db [Mongo::Database]
55
+ # The MongoDB database connection to use, not the database name
56
+ #
57
+ # :collection_name [String]
58
+ # Name of the collection to store log data in
59
+ # Default: semantic_logger
60
+ #
61
+ # :host_name [String]
62
+ # host_name to include in the document logged to Mongo
63
+ # Default: first part of host name extracted from Socket
64
+ #
65
+ # :safe [Boolean]
66
+ # Whether to use safe write for logging
67
+ # Not recommended to change this value except to diagnose connection
68
+ # issues or when log entries are not being written to Mongo
69
+ # Default: false
70
+ #
71
+ # :application [String]
72
+ # Name of the application to include in the document written to mongo
73
+ # Default: nil (None)
74
+ #
75
+ # :collection_size [Integer]
76
+ # The size of the MongoDB capped collection to create in bytes
77
+ # Default: 1 GB
78
+ #
79
+ # Some examples
80
+ # Prod: 25GB (.5GB per day across 4 servers over 10 days)
81
+ # Dev: .5GB
82
+ # Test: File
83
+ # Release: 4GB
84
+ #
85
+ # :collection_max [Integer]
86
+ # Maximum number of log entries that the capped collection will hold
87
+ #
88
+ def initialize(params={}, &block)
89
+ @db = params[:db] || raise('Missing mandatory parameter :db')
90
+ @collection_name = params[:collection_name] || 'semantic_logger'
91
+ @host_name = params[:host_name] || Socket.gethostname.split('.').first
92
+ @safe = params[:safe] || false
93
+ @application = params[:application]
94
+
95
+ # Create a collection that will hold the lesser of 1GB space or 10K documents
96
+ @collection_size = params[:collection_size] || 1024**3
97
+ @collection_max = params[:collection_max]
98
+
99
+ # Set the formatter to the supplied block
100
+ @formatter = block || self.default_formatter
101
+
102
+ # Create the collection and necessary indexes
103
+ create_indexes
104
+ end
105
+
106
+ # Create the required capped collection
107
+ # Features of capped collection:
108
+ # * No indexes by default (not even on _id)
109
+ # * Documents cannot be deleted,
110
+ # * Document updates cannot make them any larger
111
+ # * Documents are always stored in insertion order
112
+ # * A find will always return the documents in their insertion order
113
+ def create_indexes
114
+ db.create_collection(collection_name, {:capped => true, :size => @collection_size, :max => @collection_max})
115
+ end
116
+
117
+ # Purge all data from the capped collection by dropping the collection
118
+ # and recreating it.
119
+ # Also useful when the size of the capped collection needs to be changed
120
+ def purge_all
121
+ collection.drop
122
+ @collection = nil
123
+ create_indexes
124
+ end
125
+
126
+ # Return the collection being used to write the log document to
127
+ def collection
128
+ @collection ||= db[collection_name]
129
+ end
130
+
131
+ # For JRuby include the Thread name rather than its id
132
+ if defined? Java
133
+ def self.thread_name
134
+ Java::java.lang::Thread.current_thread.name
135
+ end
136
+ else
137
+ def self.thread_name
138
+ Thread.object_id
139
+ end
140
+ end
141
+
142
+ # Default log formatter
143
+ # Replace this formatter by supplying a Block to the initializer
144
+ def default_formatter
145
+ Proc.new do |level, name, message, hash, block|
146
+ document = {
147
+ :time => Time.now,
148
+ :host_name => host_name,
149
+ :pid => $PID,
150
+ :thread => SemanticLogger::Appender::MongoDB.thread_name,
151
+ :name => name,
152
+ :level => level,
153
+ }
154
+ document[:application] = application if application
155
+ document[:message] = self.class.strip_colorizing(message) if message
156
+
157
+ SemanticLogger::Appender::MongoDB.populate(document, hash) if hash
158
+ SemanticLogger::Appender::MongoDB.populate(document, block.call) if block
159
+ document
160
+ end
161
+ end
162
+
163
+ # Strip the standard Rails colorizing from the logged message
164
+ def self.strip_colorizing(message)
165
+ message.to_s.gsub(/(\e(\[([\d;]*[mz]?))?)?/, '').strip
166
+ end
167
+
168
+ # Log the message
169
+ def log(level, name, message, hash, &block)
170
+ # Insert log entry into Mongo
171
+ # Use safe=>false so that we do not wait for it to be written to disk, or
172
+ # for the response from the MongoDB server
173
+ document = formatter.call(level, name, message, hash, &block)
174
+ collection.insert(document, :safe=>safe)
175
+ end
176
+
177
+ # Populate Log Hash
178
+ def self.populate(document, message)
179
+ case message
180
+ when ::String
181
+ if document[:message]
182
+ document[:message] << " " << strip_colorizing(message)
183
+ else
184
+ document[:message] = strip_colorizing(message)
185
+ end
186
+ when ::Exception
187
+ document[:exception] = {
188
+ :class => message.class.name,
189
+ :message => message.message,
190
+ :backtrace => message.backtrace
191
+ }
192
+ when ::Hash
193
+ # With a hash, the message can be an element of the hash itself
194
+ if msg = message[:message]
195
+ # Cannot change supplied hash
196
+ hash = message.clone
197
+ hash.delete(:message)
198
+ if document[:message]
199
+ document[:message] << " " << strip_colorizing(msg)
200
+ else
201
+ document[:message] = strip_colorizing(msg)
202
+ end
203
+ document[:metadata] = hash
204
+ else
205
+ document[:metadata] = message
206
+ end
207
+ else
208
+ if document[:message]
209
+ document[:message] << " " << msg.inspect
210
+ else
211
+ document[:message] = msg.inspect
212
+ end
213
+ end
214
+ document
215
+ end
216
+
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,136 @@
1
+ # Logger is the interface used by
2
+ #
3
+ # Logger maintains the logging name to be used for all log entries generated
4
+ # by the invoking classes or modules
5
+ #
6
+ # It is recommended to create an instance of the class for every class or
7
+ # module so that it can be uniquely identified and searched on
8
+ #
9
+ # Example, log to Logger:
10
+ # require 'logger'
11
+ # require 'semantic_logger'
12
+ # log = Logger.new(STDOUT)
13
+ # log.level = Logger::DEBUG
14
+ #
15
+ # SemanticLogger::Manager.register_appender(SemanticLogger::Appender::Logger.new(log))
16
+ #
17
+ # logger = SemanticLogger::Logger.new("my.app.class")
18
+ # logger.debug("Login time", :user => 'Joe', :duration => 100, :ip_address=>'127.0.0.1')
19
+ #
20
+ # # Now log to the Logger above as well as Mongo at the same time
21
+ #
22
+ # SemanticLogger::Manager.register_appender(SemanticLogger::Appender::Mongo.new(cfg))
23
+ # ...
24
+ # logger.debug("Login time", :user => 'Mary', :duration => 230, :ip_address=>'192.168.0.1')
25
+ module SemanticLogger
26
+ class Logger
27
+ include SyncAttr
28
+
29
+ # Logging levels in order of precendence
30
+ LEVELS = [:trace, :debug, :info, :warn, :error]
31
+
32
+ # Mapping of Rails and Ruby Logger levels to SemanticLogger levels
33
+ MAP_LEVELS = []
34
+ ::Logger::Severity.constants.each do |constant|
35
+ MAP_LEVELS[::Logger::Severity.const_get(constant)] = LEVELS.find_index(constant.downcase.to_sym) || LEVELS.find_index(:error)
36
+ end
37
+
38
+ # Thread safe Class Attribute accessor for appenders array
39
+ sync_cattr_accessor :appenders do
40
+ []
41
+ end
42
+
43
+ # Allow for setting the default log level
44
+ def self.default_level=(default_level)
45
+ @@default_level = default_level
46
+ end
47
+
48
+ def self.default_level
49
+ @@default_level
50
+ end
51
+
52
+ attr_reader :application, :level
53
+
54
+ @@default_level = :info
55
+
56
+ # Create a Logger instance
57
+ # Parameters:
58
+ # application: A class, module or a string with the application/class name
59
+ # to be used in the logger
60
+ # options:
61
+ # :level The initial log level to start with for this logger instance
62
+ def initialize(application, options={})
63
+ @application = application.is_a?(String) ? application : application.name
64
+ set_level(options[:level] || self.class.default_level)
65
+ end
66
+
67
+ # Set the logging level
68
+ # Must be one of the values in #LEVELS
69
+ def level=(level)
70
+ set_level(level)
71
+ end
72
+
73
+ # Implement the log level calls
74
+ # logger.debug(message|hash|exception, hash|exception=nil, &block)
75
+ #
76
+ # Implement the log level query
77
+ # logger.debug?
78
+ #
79
+ # Example:
80
+ # logger = SemanticLogging::Logger.new('MyApplication')
81
+ # logger.debug("Only display this if log level is set to Debug or lower")
82
+ #
83
+ # # Log semantic information along with a text message
84
+ # logger.info("Request received", :user => "joe", :duration => 100)
85
+ #
86
+ # # Log an exception in a semantic way
87
+ # logger.info("Parsing received XML", exc)
88
+ #
89
+ LEVELS.each_with_index do |level, index|
90
+ class_eval <<-EOT, __FILE__, __LINE__
91
+ def #{level}(message = nil, data = nil, &block) # def trace(message = nil, data = nil, &block)
92
+ if @level_index <= #{index} # if @level_index <= 0
93
+ self.class.appenders.each {|appender| appender.log(:#{level}, application, message, data, &block) } # self.class.appenders.each {|appender| appender.log(:trace, application, message, data, &block) }
94
+ true # true
95
+ else # else
96
+ false # false
97
+ end # end
98
+ end # end
99
+
100
+ def #{level}? # def trace?
101
+ @level_index <= #{index} # @level_index <= 0
102
+ end # end
103
+ EOT
104
+ end
105
+
106
+ # Semantic Logging does not support :fatal or :unknown levels since these
107
+ # are not understood by the majority of the logging providers
108
+ # Map them to :error
109
+ alias :fatal :error
110
+ alias :fatal? :error?
111
+ alias :unknown :error
112
+ alias :unknown? :error?
113
+
114
+ # forward other calls to ActiveResource::BufferedLogger
115
+ # #silence is not implemented since it is not thread safe prior to Rails 3.2
116
+ # #TODO implement a thread safe silence method
117
+
118
+ protected
119
+
120
+ # Verify and set the level
121
+ def set_level(level)
122
+ index = if level.is_a?(Integer)
123
+ MAP_LEVELS[level]
124
+ elsif level.is_a?(String)
125
+ level = level.downcase.to_sym
126
+ LEVELS.index(level)
127
+ else
128
+ LEVELS.index(level)
129
+ end
130
+
131
+ raise "Invalid level:#{level.inspect} being requested. Must be one of #{LEVELS.inspect}" unless index
132
+ @level_index = index
133
+ @level = level
134
+ end
135
+ end
136
+ end