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
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'vcap/logging/sink/base_sink'
|
2
|
+
|
3
|
+
module VCAP::Logging::Sink
|
4
|
+
|
5
|
+
# A sink for writing to stderr/stdout
|
6
|
+
# Usage:
|
7
|
+
# stdout_sink = VCAP::Logging::Sink::StdioSink.new(STDOUT)
|
8
|
+
#
|
9
|
+
class StdioSink < BaseSink
|
10
|
+
def initialize(io, formatter=nil)
|
11
|
+
super(formatter)
|
12
|
+
@io = io
|
13
|
+
open
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def write(message)
|
19
|
+
@mutex.synchronize do
|
20
|
+
@io.write(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'vcap/logging/sink/base_sink'
|
2
|
+
|
3
|
+
module VCAP::Logging::Sink
|
4
|
+
|
5
|
+
# A sink for writing data to a string. Useful if you want to capture logs
|
6
|
+
# in memory along with writing to a file.
|
7
|
+
class StringSink < BaseSink
|
8
|
+
def initialize(str, formatter=nil)
|
9
|
+
super(formatter)
|
10
|
+
@str = str
|
11
|
+
open
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(message)
|
15
|
+
@mutex.synchronize { @str << message }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'syslog'
|
2
|
+
|
3
|
+
require 'vcap/logging/sink/base_sink'
|
4
|
+
|
5
|
+
module VCAP::Logging::Sink
|
6
|
+
|
7
|
+
# A sink for logging messages to the local syslog server.
|
8
|
+
# NB: The ruby syslog module is a thin wrapper around glibc syslog(). It will first
|
9
|
+
# attempt to open a unix stream socket to '/dev/log', and upon failure will attempt
|
10
|
+
# to open a unix datagram socket there. Make sure you configure your syslog server
|
11
|
+
# to use the appropriate type (probably dgram in our case).
|
12
|
+
#
|
13
|
+
# Beware that all messages will be silently lost if the syslog server goes away.
|
14
|
+
class SyslogSink < BaseSink
|
15
|
+
|
16
|
+
DEFAULT_LOG_LEVEL_MAP = {
|
17
|
+
:fatal => Syslog::LOG_CRIT,
|
18
|
+
:error => Syslog::LOG_ERR,
|
19
|
+
:warn => Syslog::LOG_WARNING,
|
20
|
+
:info => Syslog::LOG_INFO,
|
21
|
+
:debug => Syslog::LOG_DEBUG,
|
22
|
+
:debug1 => Syslog::LOG_DEBUG,
|
23
|
+
:debug2 => Syslog::LOG_DEBUG,
|
24
|
+
}
|
25
|
+
|
26
|
+
# @param prog_name String Program name to identify lines logged to syslog
|
27
|
+
# @param opts Hash :log_level_map Map of log level => syslog level
|
28
|
+
# :formatter LogFormatter
|
29
|
+
def initialize(prog_name, opts={})
|
30
|
+
super(opts[:formatter])
|
31
|
+
|
32
|
+
@prog_name = prog_name
|
33
|
+
@log_level_map = opts[:log_level_map] || DEFAULT_LOG_LEVEL_MAP
|
34
|
+
@syslog = nil
|
35
|
+
open
|
36
|
+
end
|
37
|
+
|
38
|
+
def open
|
39
|
+
@mutex.synchronize do
|
40
|
+
unless @opened
|
41
|
+
@syslog = Syslog.open(@prog_name, Syslog::LOG_PID, Syslog::LOG_USER)
|
42
|
+
@opened = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def close
|
48
|
+
@mutex.synchronize do
|
49
|
+
if @opened
|
50
|
+
@syslog.close
|
51
|
+
@syslog = nil
|
52
|
+
@opened = false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_record(log_record)
|
58
|
+
raise UsageError, "You cannot add a record until the sink has been opened" unless @opened
|
59
|
+
raise UsageError, "You must supply a formatter" unless @formatter
|
60
|
+
|
61
|
+
message = @formatter.format_record(log_record)
|
62
|
+
pri = @log_level_map[log_record.log_level]
|
63
|
+
@mutex.synchronize { @syslog.log(pri, '%s', message) }
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module VCAP
|
4
|
+
module Logging
|
5
|
+
class SinkMap
|
6
|
+
|
7
|
+
# @param log_levels Hash Map of level_name => value
|
8
|
+
def initialize(log_levels)
|
9
|
+
@log_levels = log_levels
|
10
|
+
@sinks = {}
|
11
|
+
for level in @log_levels.keys
|
12
|
+
@sinks[level] = []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Adds a sink for all the levels in the supplied range
|
17
|
+
#
|
18
|
+
# Usage:
|
19
|
+
# add_sink(nil, :debug, sink) # Add a sink for all levels up to, and including, the :debug level
|
20
|
+
# add_sink(:info, :info, sink) # Add a sink for only the info level
|
21
|
+
# add_sink(:warn, nil, sink) # Add a sink for all levels :warn and greater
|
22
|
+
# add_sink(nil, nil, sink) # Add a sink for all levels
|
23
|
+
#
|
24
|
+
# @param start_level Symbol The most noisy level you want this sink to apply to. Use nil to set no restriction.
|
25
|
+
# @param end_level Symbol The least noisy level you want this sink to apply to. Use nil to set no restriction.
|
26
|
+
# @param sink BaseSink The sink to add
|
27
|
+
def add_sink(start_level, end_level, sink)
|
28
|
+
raise ArgumentError, "Unknown level #{start_level}" if start_level && !@log_levels.has_key?(start_level)
|
29
|
+
raise ArgumentError, "Unknown level #{end_level}" if end_level && !@log_levels.has_key?(end_level)
|
30
|
+
|
31
|
+
start_value = @log_levels[start_level]
|
32
|
+
end_value = @log_levels[end_level]
|
33
|
+
|
34
|
+
for level, value in @log_levels
|
35
|
+
next if start_value && (value > start_value)
|
36
|
+
next if end_value && (value < end_value)
|
37
|
+
@sinks[level] << sink
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param level :Symbol Log level to retrieve sinks for
|
42
|
+
# @return Array
|
43
|
+
def get_sinks(level)
|
44
|
+
@sinks[level]
|
45
|
+
end
|
46
|
+
|
47
|
+
def each_sink
|
48
|
+
raise "You must supply a block" unless block_given?
|
49
|
+
|
50
|
+
seen = Set.new
|
51
|
+
for level, sinks in @sinks
|
52
|
+
for sink in sinks
|
53
|
+
next if seen.include?(sink)
|
54
|
+
yield sink
|
55
|
+
seen.add(sink)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
data/lib/vcap/logging.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'vcap/logging/error'
|
2
|
+
require 'vcap/logging/formatter'
|
3
|
+
require 'vcap/logging/log_record'
|
4
|
+
require 'vcap/logging/logger'
|
5
|
+
require 'vcap/logging/sink'
|
6
|
+
require 'vcap/logging/sink_map'
|
7
|
+
require 'vcap/logging/version'
|
8
|
+
|
9
|
+
module VCAP
|
10
|
+
module Logging
|
11
|
+
|
12
|
+
FORMATTER = VCAP::Logging::Formatter::DelimitedFormatter.new do
|
13
|
+
timestamp '[%F %T]'
|
14
|
+
logger_name
|
15
|
+
tags
|
16
|
+
process_id
|
17
|
+
thread_shortid
|
18
|
+
fiber_shortid
|
19
|
+
log_level
|
20
|
+
text '--'
|
21
|
+
data
|
22
|
+
end
|
23
|
+
|
24
|
+
LOG_LEVELS = {
|
25
|
+
:off => 0,
|
26
|
+
:fatal => 1,
|
27
|
+
:error => 5,
|
28
|
+
:warn => 10,
|
29
|
+
:info => 15,
|
30
|
+
:debug => 16,
|
31
|
+
:debug1 => 17,
|
32
|
+
:debug2 => 18,
|
33
|
+
}
|
34
|
+
|
35
|
+
class << self
|
36
|
+
|
37
|
+
attr_reader :default_log_level
|
38
|
+
|
39
|
+
def init
|
40
|
+
fail "init() can only be called once" if @initialized
|
41
|
+
reset
|
42
|
+
|
43
|
+
# Ideally we would call close() on each sink. Unfortunatley, we can't be sure
|
44
|
+
# that close runs last, and that other at_exit handlers aren't attempting to
|
45
|
+
# log to a sink. The best we can do is enable autoflushing.
|
46
|
+
at_exit do
|
47
|
+
@sink_map.each_sink {|s| s.autoflush = true }
|
48
|
+
end
|
49
|
+
|
50
|
+
@initialized = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Exists primarily for testing
|
54
|
+
def reset
|
55
|
+
VCAP::Logging::Logger.define_log_levels(LOG_LEVELS)
|
56
|
+
@default_log_level = pick_default_level(LOG_LEVELS)
|
57
|
+
@sink_map = VCAP::Logging::SinkMap.new(LOG_LEVELS)
|
58
|
+
@log_level_filters = {}
|
59
|
+
@sorted_log_level_filters = []
|
60
|
+
@loggers = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_log_level=(log_level_name)
|
64
|
+
log_level_name = log_level_name.to_sym if log_level_name.kind_of?(String)
|
65
|
+
raise ArgumentError, "Unknown level #{log_level_name}" unless LOG_LEVELS[log_level_name]
|
66
|
+
@default_log_level = log_level_name
|
67
|
+
end
|
68
|
+
|
69
|
+
# Configures the logging infrastructure using a hash parsed from a config file.
|
70
|
+
# The config file is expected to contain a section with the following format:
|
71
|
+
# logging:
|
72
|
+
# level: <default_log_level>
|
73
|
+
# file: <filename>
|
74
|
+
# syslog: <program name to use with the syslog sink>
|
75
|
+
#
|
76
|
+
# This interface is limiting, but it should satisfy the majority of our use cases.
|
77
|
+
# I'm imagining usage will be something like:
|
78
|
+
# config = YAML.load(<file>)
|
79
|
+
# ...
|
80
|
+
# VCAP::Logging.setup_from_config(config[:logging])
|
81
|
+
def setup_from_config(config={})
|
82
|
+
level = config[:level] || config['level']
|
83
|
+
if level
|
84
|
+
level_sym = level.to_sym
|
85
|
+
raise ArgumentError, "Unknown level: #{level}" unless LOG_LEVELS[level_sym]
|
86
|
+
@default_log_level = level_sym
|
87
|
+
end
|
88
|
+
|
89
|
+
logfile = config[:file] || config['file']
|
90
|
+
# Undecided as to whether or not we should enable buffering here. For now, don't buffer to stay consistent with the current logger.
|
91
|
+
add_sink(nil, nil, VCAP::Logging::Sink::FileSink.new(logfile, FORMATTER)) if logfile
|
92
|
+
|
93
|
+
syslog_name = config[:syslog] || config['syslog']
|
94
|
+
add_sink(nil, nil, VCAP::Logging::Sink::SyslogSink.new(syslog_name, :formatter => FORMATTER)) if syslog_name
|
95
|
+
|
96
|
+
# Log to stdout if no other sinks are supplied
|
97
|
+
add_sink(nil, nil, VCAP::Logging::Sink::StdioSink.new(STDOUT, FORMATTER)) unless (logfile || syslog_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the logger associated with _name_. Creates one if it doesn't exist. The log level will be inherited
|
101
|
+
# from the parent logger.
|
102
|
+
#
|
103
|
+
# @param name String Logger name
|
104
|
+
def logger(name)
|
105
|
+
if !@loggers.has_key?(name)
|
106
|
+
@loggers[name] = VCAP::Logging::Logger.new(name, @sink_map)
|
107
|
+
@loggers[name].log_level = @default_log_level
|
108
|
+
for level, regex in @sorted_log_level_filters
|
109
|
+
if regex.match(name)
|
110
|
+
@loggers[name].log_level = level
|
111
|
+
break
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
@loggers[name]
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_sink(*args)
|
120
|
+
@sink_map.add_sink(*args)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Sets the log level to _log_level_ for every logger whose name matches _path_regex_. Loggers who
|
124
|
+
# were previously set to this level and whose names no longer match _path_regex_ are reset to
|
125
|
+
# the default level.
|
126
|
+
#
|
127
|
+
# @param path_regex String Regular expression to use when matching against the logger name
|
128
|
+
# @param log_level_name Symbol Name of the log level to set on all matching loggers
|
129
|
+
def set_log_level(path_regex, log_level_name)
|
130
|
+
log_level_name = log_level_name.to_sym if log_level_name.kind_of?(String)
|
131
|
+
|
132
|
+
raise ArgumentError, "Unknown log level #{log_level_name}" unless LOG_LEVELS[log_level_name]
|
133
|
+
regex = Regexp.new("^#{path_regex}$")
|
134
|
+
|
135
|
+
@log_level_filters[log_level_name] = regex
|
136
|
+
@sorted_log_level_filters = @log_level_filters.keys.sort {|a, b| LOG_LEVELS[a] <=> LOG_LEVELS[b] }.map {|lvl| [lvl, @log_level_filters[lvl]] }
|
137
|
+
|
138
|
+
for logger_name, logger in @loggers
|
139
|
+
if regex.match(logger_name)
|
140
|
+
logger.log_level = log_level_name
|
141
|
+
elsif logger.log_level == log_level_name
|
142
|
+
# Reset any loggers at the supplied level that no longer match
|
143
|
+
logger.log_level = @default_log_level
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# The middle level seems like a reasonable default
|
151
|
+
def pick_default_level(level_map)
|
152
|
+
sorted_levels = level_map.keys.sort {|a, b| level_map[a] <=> level_map[b] }
|
153
|
+
sorted_levels[sorted_levels.length / 2]
|
154
|
+
end
|
155
|
+
|
156
|
+
end # << self
|
157
|
+
end # VCAP::Logging
|
158
|
+
end
|
159
|
+
|
160
|
+
VCAP::Logging.init
|
data/spec/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
BASE_DIR = File.expand_path(File.join('..', '..'), __FILE__)
|
2
|
+
ENV["BUNDLE_GEMFILE"] ||= File.join(BASE_DIR, 'Gemfile')
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.setup
|
6
|
+
|
7
|
+
require 'rake'
|
8
|
+
require 'rspec/core/rake_task'
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
11
|
+
t.pattern = '**/*_spec.rb'
|
12
|
+
t.rspec_opts = ['--color', '--format nested']
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => [:spec]
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe VCAP::Logging::Sink::BaseSink do
|
4
|
+
before :each do
|
5
|
+
@sink = VCAP::Logging::Sink::BaseSink.new
|
6
|
+
@rec = VCAP::Logging::LogRecord.new(:info, 'foo', VCAP::Logging::Logger.new('bar', nil), [])
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#add_record' do
|
10
|
+
it 'should raise an exception if called before the sink is open' do
|
11
|
+
lambda { @sink.add_record(@rec) }.should raise_error(VCAP::Logging::Sink::UsageError)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should raise an exception if called when a formatter has not been set for the sink' do
|
15
|
+
@sink.open
|
16
|
+
lambda { @sink.add_record(@rec) }.should raise_error(VCAP::Logging::Sink::UsageError)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#autoflush' do
|
21
|
+
it 'should immediately call flush when set to true' do
|
22
|
+
@sink.formatter = mock(:formatter)
|
23
|
+
@sink.should_receive(:flush)
|
24
|
+
@sink.open
|
25
|
+
@sink.autoflush = true
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should call flush after each add_record call if true' do
|
29
|
+
@sink.formatter = mock(:formatter)
|
30
|
+
@sink.formatter.should_receive(:format_record).with(@rec).and_return('foo')
|
31
|
+
@sink.should_receive(:write).with('foo')
|
32
|
+
@sink.should_receive(:flush).twice
|
33
|
+
@sink.open
|
34
|
+
@sink.autoflush = true
|
35
|
+
@sink.add_record(@rec)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe VCAP::Logging::Formatter::DelimitedFormatter do
|
4
|
+
describe '#initialize' do
|
5
|
+
|
6
|
+
it 'should define a format_record' do
|
7
|
+
fmt = VCAP::Logging::Formatter::DelimitedFormatter.new {}
|
8
|
+
fmt.respond_to?(:format_record).should be_true
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#format_record' do
|
14
|
+
it 'should return a correctly formatted message' do
|
15
|
+
rec = VCAP::Logging::LogRecord.new(:debug, 'foo', VCAP::Logging::Logger.new('foo', nil), ['bar', 'baz'])
|
16
|
+
fmt = VCAP::Logging::Formatter::DelimitedFormatter.new('.') do
|
17
|
+
timestamp '%s'
|
18
|
+
log_level
|
19
|
+
tags
|
20
|
+
process_id
|
21
|
+
thread_id
|
22
|
+
data
|
23
|
+
end
|
24
|
+
|
25
|
+
fmt.format_record(rec).should == [rec.timestamp.strftime('%s'), ' DEBUG', 'bar,baz', rec.process_id.to_s, rec.thread_id.to_s, 'foo'].join('.') + "\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should encode newlines' do
|
29
|
+
rec = VCAP::Logging::LogRecord.new(:debug, "test\ning123\n\n", VCAP::Logging::Logger.new('foo', nil), [])
|
30
|
+
fmt = VCAP::Logging::Formatter::DelimitedFormatter.new('.') { data }
|
31
|
+
fmt.format_record(rec).should == "test\\ning123\\n\\n\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should format exceptions' do
|
35
|
+
begin
|
36
|
+
raise StandardError, "Testing 123"
|
37
|
+
rescue => exc
|
38
|
+
end
|
39
|
+
rec = VCAP::Logging::LogRecord.new(:error, exc, VCAP::Logging::Logger.new('foo', nil), [])
|
40
|
+
fmt = VCAP::Logging::Formatter::DelimitedFormatter.new('.') { data }
|
41
|
+
fmt.format_record(rec).should == "StandardError(\"Testing 123\", [#{exc.backtrace.join(',')}])\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|