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.
@@ -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,5 @@
1
+ require 'vcap/logging/sink/base_sink'
2
+ require 'vcap/logging/sink/file_sink'
3
+ require 'vcap/logging/sink/stdio_sink'
4
+ require 'vcap/logging/sink/string_sink'
5
+ require 'vcap/logging/sink/syslog_sink'
@@ -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
+
@@ -0,0 +1,5 @@
1
+ module VCAP
2
+ module Logging
3
+ VERSION = '0.1.4'
4
+ end
5
+ end
@@ -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]
@@ -0,0 +1,4 @@
1
+ require 'rspec/core'
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
4
+ require 'vcap/logging'
@@ -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