vcap_logging 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ task :spec do
7
+ desc 'Run tests'
8
+ sh('bundle install')
9
+ sh('cd spec && rake spec')
10
+ end
@@ -0,0 +1,5 @@
1
+ module VCAP
2
+ module Logging
3
+ class LoggingError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ require 'vcap/logging/log_record'
2
+
3
+ module VCAP
4
+ module Logging
5
+ module Formatter
6
+
7
+ # Formatters are responsible for taking a log record and
8
+ # producing a string representation suitable for writing to a sink.
9
+ #
10
+ class BaseFormatter
11
+ # Produces a string suitable for writing to a sink
12
+ #
13
+ # @param log_record VCAP::Logging::LogRecord Log record to be formatted
14
+ # @return String
15
+ def format_record(log_record)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ # The inverse of format_record()
20
+ #
21
+ # @param message String A string formatted using format_record()
22
+ # @return VCAP::Logging::LogRecord
23
+ def parse_message(message)
24
+ raise NotImplementedError
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,96 @@
1
+ require 'vcap/logging/formatter/base_formatter'
2
+ require 'vcap/logging/log_record'
3
+
4
+ module VCAP::Logging::Formatter
5
+
6
+ # A formatter for creating messages delimited by a given value (e.g. space separated logs)
7
+ class DelimitedFormatter < BaseFormatter
8
+
9
+ DEFAULT_DELIMITER = ' '
10
+ DEFAULT_TIMESTAMP_FORMAT = '%F %T %z' # YYYY-MM-DD HH:MM:SS TZ
11
+
12
+ # This provides a tiny DSL for constructing the formatting function. We opt
13
+ # to define the method inline in order to avoid incurring multiple method calls
14
+ # per call to format_record().
15
+ #
16
+ # Usage:
17
+ #
18
+ # formatter = VCAP::Logging::Formatter::DelimitedFormatter.new do
19
+ # timestamp '%s'
20
+ # log_level
21
+ # data
22
+ # end
23
+ #
24
+ # @param delim String Delimiter that will separate fields in the message
25
+ # @param Block Block that defines the log message format
26
+ def initialize(delim=DEFAULT_DELIMITER, &blk)
27
+ @exprs = []
28
+
29
+ # Collect the expressions we want to use when constructing messages in the
30
+ # order that they should appear.
31
+ instance_eval(&blk)
32
+
33
+ # Build the format string to that will generate the message along with
34
+ # the arguments
35
+ fmt_chars = @exprs.map {|e| e[0] }
36
+ fmt = fmt_chars.join(delim) + "\n"
37
+ fmt_args = @exprs.map {|e| e[1] }.join(', ')
38
+
39
+ instance_eval("def format_record(log_record); '#{fmt}' % [#{fmt_args}]; end")
40
+ end
41
+
42
+ private
43
+
44
+ def log_level
45
+ @exprs << ['%6s', "log_record.log_level.to_s.upcase"]
46
+ end
47
+
48
+ def data
49
+ # Not sure of a better way to do this...
50
+ # If we are given an exception, include the class name, string representation, and stacktrace
51
+ snippet = "(log_record.data.kind_of?(Exception) ? " \
52
+ + "log_record.data.class.to_s + '(\"' + log_record.data.to_s + '\", [' + (log_record.data.backtrace ? log_record.data.backtrace.join(',') : '') + '])'" \
53
+ + ": log_record.data.to_s" \
54
+ + ").gsub(/\n/, '\\n')"
55
+ @exprs << ['%s', snippet]
56
+ end
57
+
58
+ def tags
59
+ @exprs << ['%s', "log_record.tags.empty? ? '-': log_record.tags.join(',')"]
60
+ end
61
+
62
+ def fiber_id
63
+ @exprs << ['%s', "log_record.fiber_id"]
64
+ end
65
+
66
+ def fiber_shortid
67
+ @exprs << ['%s', "log_record.fiber_shortid"]
68
+ end
69
+
70
+ def process_id
71
+ @exprs << ['%s', "log_record.process_id"]
72
+ end
73
+
74
+ def thread_id
75
+ @exprs << ['%s', "log_record.thread_id"]
76
+ end
77
+
78
+ def thread_shortid
79
+ @exprs << ['%s', "log_record.thread_shortid"]
80
+ end
81
+
82
+ def timestamp(fmt=DEFAULT_TIMESTAMP_FORMAT)
83
+ @exprs << ['%s', "log_record.timestamp.strftime('#{fmt}')"]
84
+ end
85
+
86
+ def logger_name
87
+ @exprs << ['%s', "log_record.logger_name"]
88
+ end
89
+
90
+ def text(str)
91
+ @exprs << ['%s', "'#{str}'"]
92
+ end
93
+
94
+ end
95
+
96
+ end
@@ -0,0 +1,2 @@
1
+ require 'vcap/logging/formatter/base_formatter'
2
+ require 'vcap/logging/formatter/delimited_formatter'
@@ -0,0 +1,71 @@
1
+ require 'digest/md5'
2
+ require 'thread'
3
+
4
+ module VCAP
5
+ module Logging
6
+
7
+ class LogRecord
8
+
9
+ @@have_fibers = nil
10
+
11
+ class << self
12
+
13
+ def have_fibers?
14
+ if @@have_fibers == nil
15
+ begin
16
+ require 'fiber'
17
+ @@have_fibers = true
18
+ rescue LoadError
19
+ @@have_fibers = false
20
+ end
21
+ else
22
+ @@have_fibers
23
+ end
24
+ end
25
+
26
+ def current_fiber_id
27
+ if have_fibers?
28
+ Fiber.current.object_id
29
+ else
30
+ nil
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ attr_reader :timestamp
37
+ attr_reader :data
38
+ attr_reader :log_level
39
+ attr_reader :logger_name
40
+ attr_reader :tags
41
+ attr_reader :thread_id
42
+ attr_reader :thread_shortid
43
+ attr_reader :fiber_id
44
+ attr_reader :fiber_shortid
45
+ attr_reader :process_id
46
+
47
+ def initialize(log_level, data, logger, tags=[])
48
+ @timestamp = Time.now
49
+ @data = data
50
+ @logger_name = logger.name
51
+ @log_level = log_level
52
+ @tags = tags
53
+
54
+ @thread_id = Thread.current.object_id
55
+ @thread_shortid = shortid(@thread_id)
56
+ @fiber_id = LogRecord.current_fiber_id
57
+ @fiber_shortid = @fiber_id ? shortid(@fiber_id) : nil
58
+ @process_id = Process.pid
59
+ end
60
+
61
+ private
62
+
63
+ def shortid(data, len=4)
64
+ digest = Digest::MD5.hexdigest(data.to_s)
65
+ len = len > digest.length ? digest.length : len
66
+ digest[0, len]
67
+ end
68
+
69
+ end # VCAP::Logging::LogRecord
70
+ end
71
+ end
@@ -0,0 +1,116 @@
1
+ require 'vcap/logging/log_record'
2
+
3
+ module VCAP
4
+ module Logging
5
+ class Logger
6
+
7
+ # Loggers are responsible for dispatching log messages to an appropriate
8
+ # sink.
9
+
10
+ LogLevel = Struct.new(:name, :value)
11
+
12
+ class << self
13
+ attr_reader :log_levels
14
+
15
+ # Defines convenience methods for each log level. For example, if 'debug' is the name of a level
16
+ # corresponding 'debug' and 'debugf' instance methods will be defined for all loggers.
17
+ #
18
+ # @param levels Array[VCAP::Logging::LogLevel] Log levels to use
19
+ def define_log_levels(levels)
20
+
21
+ @prev_log_methods ||= []
22
+ # Clean up previously defined methods
23
+ for meth_name in @prev_log_methods
24
+ undef_method(meth_name)
25
+ end
26
+
27
+ @prev_log_methods = []
28
+ @log_levels = {}
29
+
30
+ # Partially evaluate log/logf for the level specified by each name
31
+ for name, level in levels
32
+ @log_levels[name] = LogLevel.new(name, level)
33
+
34
+ define_log_helper(name)
35
+ @prev_log_methods << name
36
+
37
+ name_f = "#{name}f".to_sym
38
+ define_logf_helper(name_f, name)
39
+ @prev_log_methods << name_f
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Helper methods for defining helper methods (look out, you might be getting incepted...)
46
+ # Needed in order to create a new scope when binding level to the blocks
47
+
48
+ def define_log_helper(level)
49
+ define_method(level) {|*args| log(level, *args) }
50
+ end
51
+
52
+ def define_logf_helper(name, level)
53
+ define_method(name) {|*args| logf(level, *args) }
54
+ end
55
+
56
+ end
57
+
58
+ attr_reader :name
59
+ attr_accessor :sink_map
60
+
61
+ def initialize(name, sink_map)
62
+ @name = name
63
+ @sink_map = sink_map
64
+ end
65
+
66
+ def log_level
67
+ @log_level.name
68
+ end
69
+
70
+ def log_level=(lvl_name)
71
+ level = self.class.log_levels[lvl_name]
72
+ raise ArgumentError, "Unknown level #{lvl_name}" unless level
73
+ @log_level = level
74
+
75
+ self
76
+ end
77
+
78
+ # Logs a message to the configured sinks. You may optionally supply a block to be called; its return value
79
+ # will be used as data for the log record.
80
+ #
81
+ # @param lvl_name Symbol Log level for the associated message
82
+ # @param data Object Optional data to log. How this is converted to a string is determined by the formatters.
83
+ # @param opts Hash :tags => Array[String] Tags to associated with this log message
84
+ def log(lvl_name, data=nil, opts={})
85
+ level = self.class.log_levels[lvl_name]
86
+ raise ArgumentError, "Unknown level #{lvl_name}" unless level
87
+
88
+ return unless level.value <= @log_level.value
89
+ data = yield if block_given?
90
+ tags = opts[:tags] || []
91
+ tags << :exception if data.kind_of?(Exception)
92
+
93
+ rec = VCAP::Logging::LogRecord.new(lvl_name, data, self, tags)
94
+ @sink_map.get_sinks(lvl_name).each {|s| s.add_record(rec) }
95
+ end
96
+
97
+ # Logs a message to the configured sinks. This is analogous to the printf() family
98
+ #
99
+ # @param lvl_name Symbol Log level for the associated message
100
+ # @param fmt String Format string to use when formatting the message
101
+ # @param fmt_args Array Arguments to format string
102
+ # @param opts Hash See log()
103
+ def logf(lvl_name, fmt, fmt_args, opts={})
104
+ level = self.class.log_levels[lvl_name]
105
+ raise ArgumentError, "Unknown level #{lvl_name}" unless level
106
+
107
+ return unless level.value <= @log_level.value
108
+ data = fmt % fmt_args
109
+
110
+ rec = VCAP::Logging::LogRecord.new(lvl_name, data, self, opts[:tags] || [])
111
+ @sink_map.get_sinks(lvl_name).each {|s| s.add_record(rec) }
112
+ end
113
+
114
+ end # VCAP::Logging::Logger
115
+ end
116
+ end
@@ -0,0 +1,83 @@
1
+ require 'thread'
2
+
3
+ require 'vcap/logging/error'
4
+ require 'vcap/logging/log_record'
5
+
6
+ module VCAP
7
+ module Logging
8
+ module Sink
9
+
10
+ class SinkError < VCAP::Logging::LoggingError; end
11
+ class UsageError < SinkError; end
12
+
13
+ # Sinks serve as the final destination for log records.
14
+ # Usually they are lightweight wrappers around other objects that perform IO (files and sockets come to mind).
15
+
16
+ class BaseSink
17
+ attr_reader :opened
18
+ attr_accessor :formatter
19
+ attr_accessor :autoflush
20
+
21
+ def initialize(formatter=nil)
22
+ @formatter = formatter
23
+ @opened = false
24
+ @mutex = Mutex.new
25
+ @autoflush = false
26
+ end
27
+
28
+ # Opens any underlying file descriptors, etc. and ensures that the sink
29
+ # is capable of receiving records.
30
+ #
31
+ # This MUST be called before any calls to add_record().
32
+ def open
33
+ @mutex.synchronize { @opened = true }
34
+ end
35
+
36
+ # Closes any underlying file descriptors and ensures that any log records
37
+ # buffered in memory are flushed.
38
+ def close
39
+ @mutex.synchronize { @opened = false }
40
+ end
41
+
42
+ def autoflush=(should_autoflush)
43
+ @autoflush = should_autoflush
44
+ flush if @autoflush
45
+ end
46
+
47
+ # Formats the log record using the configured formatter and
48
+ # NB: Depending on the implementation of write(), this may buffer the record in memory.
49
+ #
50
+ # @param log_record VCAP::Logging::LogRecord Record to add
51
+ def add_record(log_record)
52
+ raise UsageError, "You cannot add a record until the sink has been opened" unless @opened
53
+ raise UsageError, "You must supply a formatter" unless @formatter
54
+
55
+ message = @formatter.format_record(log_record)
56
+ write(message)
57
+ flush if @autoflush
58
+ end
59
+
60
+ # Flushes any log records that may have been buffered in memory
61
+ def flush
62
+ nil
63
+ end
64
+
65
+ private
66
+
67
+ # Writes the formatted log message to the underlying device
68
+ #
69
+ # NB: Implementations MUST:
70
+ # - Be thread-safe.
71
+ # - Handle all exceptions that may occur when writing a message.
72
+ # An appropriate strategy could be as simple as logging the exception to standard error.
73
+ #
74
+ # @param log_message String Message to write
75
+ def write(log_message)
76
+ raise NotImplementedError
77
+ end
78
+
79
+ end # VCAP::Logging::Sink::BaseSink
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,120 @@
1
+ module VCAP::Logging::Sink
2
+
3
+ # A sink for writing to a file. Buffering is supported, but disabled by default.
4
+ class FileSink < BaseSink
5
+
6
+ attr_reader :filename
7
+
8
+ class MessageBuffer
9
+ attr_reader :size
10
+ attr_accessor :max_buffer_size
11
+
12
+ def initialize(buffer_size)
13
+ @max_buffer_size = buffer_size
14
+ @buffer = []
15
+ @size = 0
16
+ end
17
+
18
+ def append(msg)
19
+ @buffer << msg
20
+ @size += msg.length
21
+ end
22
+
23
+ def compact
24
+ return nil unless @size > 0
25
+ ret = @buffer.join
26
+ @buffer = []
27
+ @size = 0
28
+ ret
29
+ end
30
+
31
+ def full?
32
+ @size >= @max_buffer_size
33
+ end
34
+
35
+ def empty?
36
+ @size == 0
37
+ end
38
+ end
39
+
40
+ # @param filename String Pretty obvious...
41
+ # @param formatter BaseFormatter Formatter to use when generating log messages
42
+ # @param opts Hash :buffer_size => Size (in bytes) to buffer in memory before flushing to disk
43
+ #
44
+ def initialize(filename, formatter=nil, opts={})
45
+ super(formatter)
46
+
47
+ @filename = filename
48
+ @file = nil
49
+ if opts[:buffer_size] && (Integer(opts[:buffer_size]) > 0)
50
+ @buffer = MessageBuffer.new(opts[:buffer_size])
51
+ else
52
+ @buffer = nil
53
+ end
54
+ open()
55
+ end
56
+
57
+ # Missing Python's decorators pretty badly here. Even guards would be better than the existing solution.
58
+ # Alas, ruby has no real destructors.
59
+
60
+ def open
61
+ @mutex.synchronize do
62
+ if !@opened
63
+ @file = File.new(@filename, 'a+')
64
+ @file.sync = true
65
+ @opened = true
66
+ end
67
+ end
68
+ end
69
+
70
+ def close
71
+ @mutex.synchronize do
72
+ if @opened
73
+ perform_write(@buffer.compact) if @buffer && !@buffer.empty?
74
+ @file.close
75
+ @file = nil
76
+ @opened = false
77
+ end
78
+ end
79
+ end
80
+
81
+ def flush
82
+ @mutex.synchronize do
83
+ perform_write(@buffer.compact) if @buffer && !@buffer.empty?
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def write(message)
90
+ @mutex.synchronize do
91
+ if @buffer
92
+ @buffer.append(message)
93
+ perform_write(@buffer.compact) if @buffer.full?
94
+ else
95
+ perform_write(message)
96
+ end
97
+ end
98
+ end
99
+
100
+ def perform_write(message)
101
+ bytes_left = message.length
102
+ begin
103
+ while bytes_left > 0
104
+ written = @file.syswrite(message)
105
+ bytes_left -= written
106
+ message = message[written, message.length - written] if bytes_left
107
+ end
108
+ rescue Errno::EINTR
109
+ # This can only happen if the write is interrupted before any data is written.
110
+ # If a partial write occurs due to an interrupt write(2) will return the number of bytes written
111
+ # instead of -1.
112
+ #
113
+ # The rest of the exceptions that syswrite() can throw cannot be recovered from,
114
+ # and should be propagated up the stack. (See `man 2 write`.)
115
+ retry
116
+ end
117
+ end
118
+ end
119
+
120
+ end