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