semantic_logger 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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