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 +3 -0
- data/Rakefile +10 -0
- data/lib/vcap/logging/error.rb +5 -0
- data/lib/vcap/logging/formatter/base_formatter.rb +30 -0
- data/lib/vcap/logging/formatter/delimited_formatter.rb +96 -0
- data/lib/vcap/logging/formatter.rb +2 -0
- data/lib/vcap/logging/log_record.rb +71 -0
- data/lib/vcap/logging/logger.rb +116 -0
- data/lib/vcap/logging/sink/base_sink.rb +83 -0
- data/lib/vcap/logging/sink/file_sink.rb +120 -0
- data/lib/vcap/logging/sink/stdio_sink.rb +25 -0
- data/lib/vcap/logging/sink/string_sink.rb +18 -0
- data/lib/vcap/logging/sink/syslog_sink.rb +67 -0
- data/lib/vcap/logging/sink.rb +5 -0
- data/lib/vcap/logging/sink_map.rb +62 -0
- data/lib/vcap/logging/version.rb +5 -0
- data/lib/vcap/logging.rb +160 -0
- data/spec/Rakefile +15 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/unit/base_sink_spec.rb +38 -0
- data/spec/unit/delimited_formatter_spec.rb +44 -0
- data/spec/unit/file_sink_spec.rb +79 -0
- data/spec/unit/log_record_spec.rb +51 -0
- data/spec/unit/logger_spec.rb +148 -0
- data/spec/unit/logging_spec.rb +114 -0
- data/spec/unit/sink_map_spec.rb +64 -0
- data/spec/unit/stdio_sink_spec.rb +20 -0
- data/spec/unit/string_sink_spec.rb +13 -0
- data/spec/unit/syslog_sink_spec.rb +23 -0
- metadata +112 -0
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -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,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
|