vcap_logging 0.1.4

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