vcap_logging 0.1.4

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/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