teelogger 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a05f90f38dfc709f361c48700fd75abc7b4cae97
4
- data.tar.gz: 21adf664c10e60ff5788c4b1b6b782cda65158ca
3
+ metadata.gz: 374fbb37a9d2c70a80c83f6c52ec70501fef456a
4
+ data.tar.gz: 43ff68bc53641a1805f21e76916ec5241b0e5549
5
5
  SHA512:
6
- metadata.gz: 4088feb8ab68f3635e0ae30d88bbefc2ec498c0f6fb3c05b20158b73eeb47ca1253c5c1ba2fe69668f3fcce4dcb103c4b0e96036c0d77e63b91060dccc693605
7
- data.tar.gz: edcccbf758d9b8cc6b95ae5694ab60869c565062447ccef2fa1b78916a1d67e16e411082f4639135f355c22276fd15e82e4d2563d3c0433b8614c0ba81088a79
6
+ metadata.gz: 704c17de207b1a44bb1886ea9291c27c77c67b8e38140dcca144f21009705dae5e58c50a8ce056e877195320be3413985915d2d96651a7e691042846126ad0b8
7
+ data.tar.gz: 0797bc600ca9c252c61ec27289cc61103143ab7df846760b025f0039f5222d98aa0fd13db267fd6042053fc614bcad71e7e2bb005e97ea50f8a198d5f663bd2e
data/.travis.yml CHANGED
@@ -9,6 +9,12 @@ language: ruby
9
9
  rvm:
10
10
  - 1.9.3
11
11
  - 2.0.0
12
+ - 2.1.0
13
+ - 2.2.2
14
+ - jruby
15
+ matrix:
16
+ allow_failures:
17
+ rvm: 2.2.2
12
18
  env:
13
19
  CODECLIMATE_REPO_TOKEN=e37a93224a40c36fd6a24465c063e7627661921a868a2d1321ad2937d7889de6
14
20
  notifications:
@@ -0,0 +1,80 @@
1
+ @filter
2
+ Feature: Filter
3
+ As a user of the teelogger gem
4
+ When I use the Filter class
5
+ I expect it to work as documented.
6
+
7
+ @filter_01
8
+ Scenario Outline: Replace patterns in Strings
9
+ Given I create a TeeLogger for testing filters
10
+ And I write a log message containing the word "<word>"
11
+ Then I expect the log message to <condition> the word "<word>"
12
+
13
+ Examples:
14
+ | word | condition |
15
+ | hello | contain |
16
+ | hello=123 | contain |
17
+ | --hello=123 | contain |
18
+ | hello: 123 | contain |
19
+ | password=123 | not contain |
20
+ | password: 123 | not contain |
21
+ | --password=123 | not contain |
22
+
23
+ @filter_02
24
+ Scenario Outline: Replace patterns in Strings in Arrays
25
+ Given I create a TeeLogger for testing filters
26
+ And I write a log message containing the word "<word>" in an Array
27
+ Then I expect the log message to <condition> the word "<word>"
28
+
29
+ Examples:
30
+ | word | condition |
31
+ | hello | contain |
32
+ | hello=123 | contain |
33
+ | --hello=123 | contain |
34
+ | hello: 123 | contain |
35
+ | password=123 | not contain |
36
+ | password: 123 | not contain |
37
+ | --password=123 | not contain |
38
+
39
+ @filter_03
40
+ Scenario Outline: Replace patterns in Strings in Hashes
41
+ Given I create a TeeLogger for testing filters
42
+ And I write a log message containing the value "<word>" for the key "<key>" in a Hash
43
+ Then I expect the log message to <condition> the word "<word>"
44
+
45
+ Examples:
46
+ | key | word | condition |
47
+ | hello | MUST REMAIN | contain |
48
+ | password | TO BE HIDDEN | not contain |
49
+
50
+ @filter_04
51
+ Scenario Outline: Replace patterns in CLI-like arrays
52
+ Given I create a TeeLogger for testing filters
53
+ And I write a log message containing the word sequence "<word1>", "<word2>"
54
+ Then I expect the log message to <condition> the word "<word2>"
55
+
56
+ Examples:
57
+ | word1 | word2 | condition |
58
+ | hello | MUST REMAIN | contain |
59
+ | password | TO BE HIDDEN | not contain |
60
+
61
+ @filter_05
62
+ Scenario Outline: Ensure custom filter words work
63
+ Given I create a TeeLogger for testing filters
64
+ And I set filter words to include "<filter>"
65
+ And I write a log message containing the word "<word>"
66
+ Then I expect the log message to <condition> the word "<word>"
67
+
68
+ Examples:
69
+ | filter | word | condition |
70
+ | foo | foo=123 | not contain |
71
+ | bar | foo=123 | contain |
72
+ | foo | bar=123 | contain |
73
+ | bar | bar=123 | not contain |
74
+
75
+ @filter_06
76
+ Scenario: Custom filter
77
+ Given I create a TeeLogger for testing filters
78
+ And I register a custom filter
79
+ And I write a log message containing the word "foo"
80
+ Then I expect the log message to not contain the word "foo"
@@ -0,0 +1,49 @@
1
+ @formatter
2
+ Feature: Formatter
3
+ As a user of the teelogger gem
4
+ When I use the Formatter class
5
+ I expect it to work as documented.
6
+
7
+ @formatter_01
8
+ Scenario Outline: Placeholders
9
+ Given I create a Formatter with "{<placeholder>}" in the format string
10
+ And I call it with parameters "<severity>", "<time>", "<progname>" and "<message>"
11
+ Then I expect the result to match "<result>"
12
+
13
+ Examples:
14
+ | placeholder | severity | time | progname | message | result |
15
+ | severity | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | INFO |
16
+ | severity | InfO | 2015-07-03T12:10:57Z | STDOUT | test message | INFO |
17
+ | short_severity | iNFO | 2015-07-03T12:10:57Z | STDOUT | test message | I |
18
+ | short_severity | infO | 2015-07-03T12:10:57Z | STDOUT | test message | I |
19
+ | logger_timestamp | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | 2015-07-03T12:10:57.000000 |
20
+ | logger_timestamp | InfO | 2015-07-03T12:10:57Z | STDOUT | test message | 2015-07-03T12:10:57.000000 |
21
+ | logger_timestamp | InfO | 2015-07-03T12:10:57+0100 | STDOUT | test message | 2015-07-03T11:10:57.000000 |
22
+ | iso8601_timestamp | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | 2015-07-03T12:10:57\\\\+0000 |
23
+ | iso8601_timestamp | InfO | 2015-07-03T12:10:57Z | STDOUT | test message | 2015-07-03T12:10:57\\\\+0000 |
24
+ | iso8601_timestamp | InfO | 2015-07-03T12:10:57+0100 | STDOUT | test message | 2015-07-03T11:10:57\\\\+0000 |
25
+ | iso8601_timestamp_utc | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | 2015-07-03T12:10:57Z |
26
+ | iso8601_timestamp_utc | InfO | 2015-07-03T12:10:57Z | STDOUT | test message | 2015-07-03T12:10:57Z |
27
+ | iso8601_timestamp_utc | InfO | 2015-07-03T12:10:57+0100 | STDOUT | test message | 2015-07-03T11:10:57Z |
28
+ | tai64n_timestamp | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | @4000000055967bdb000001f4 |
29
+ | tai64n_timestamp | InfO | 2015-07-03T12:10:57Z | STDOUT | test message | @4000000055967bdb000001f4 |
30
+ | tai64n_timestamp | InfO | 2015-07-03T12:10:57+0100 | STDOUT | test message | @4000000055966dcb000001f4 |
31
+ | logger | iNFO | 2015-07-03T12:10:57Z | sTDouT | test message | sTDouT |
32
+ | message | iNFO | 2015-07-03T12:10:57Z | STDOUT | teSt mESsage | teSt mESsage |
33
+ | pid | iNFO | 2015-07-03T12:10:57Z | STDOUT | teSt mESsage | \\\\d+ |
34
+
35
+
36
+ # Note 1: need four \ to escape a special character in the regex field
37
+
38
+ @formatter_02
39
+ Scenario Outline: Format strings
40
+ Given I create a Formatter with the "<format>" format string
41
+ And I call it with parameters "<severity>", "<time>", "<progname>" and "<message>"
42
+ Then I expect the result to match "<result>"
43
+
44
+ Examples:
45
+ | format | severity | time | progname | message | result |
46
+ | FORMAT_LOGGER | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | I, \\\\[2015-07-03T12:10:57.000000 #\\\\d+\\\\] INFO -- STDOUT: test message |
47
+ | FORMAT_DEFAULT | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | I, \\\\[2015-07-03T12:10:57\\\\+0000 #\\\\d+\\\\] STDOUT: test message |
48
+ | FORMAT_SHORT | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | I, \\\\[2015-07-03T12:10:57\\\\+0000\\\\] test message |
49
+ | FORMAT_DJB | INFO | 2015-07-03T12:10:57Z | STDOUT | test message | @4000000055967bdb000001f4 INFO: test message |
@@ -89,4 +89,24 @@ Feature: Logger
89
89
  | ERROR | appear |
90
90
  | FATAL | not appear |
91
91
 
92
+ @logger_07
93
+ Scenario Outline: Bad log levels
94
+ Given I create a TeeLogger with default parameters
95
+ And I set the log level to "<initial>"
96
+ And I set the log level to "<level>"
97
+ Then I expect this to <raise> an exception
98
+ And I expect the log level to be "<result>"
99
+
100
+ Examples:
101
+ | initial | level | raise | result |
102
+ | fatal | DeBuG | not raise | debug |
103
+ | fatal | debugFOO | raise | fatal |
104
+ | fatal | InfO | not raise | info |
105
+ | fatal | infoFOO | raise | fatal |
106
+ | fatal | wARn | not raise | warn |
107
+ | fatal | warnFOO | raise | fatal |
108
+ | fatal | eRrOR | not raise | error |
109
+ | fatal | errorFOO | raise | fatal |
110
+ | debug | faTAl | not raise | fatal |
111
+ | debug | fatalFOO | raise | debug |
92
112
 
@@ -0,0 +1,70 @@
1
+ io = nil
2
+ logger = nil
3
+
4
+ Given(/^I create a TeeLogger for testing filters$/) do
5
+ io = StringIO.new
6
+ logger = TeeLogger::TeeLogger.new io
7
+ assert [TeeLogger::DEFAULT_FLUSH_INTERVAL] == logger.flush_interval, "Flush interval is not default: #{logger.flush_interval}"
8
+ end
9
+
10
+ Given(/^I write a log message containing the word "([^"]*)"$/) do |word|
11
+ # Log a string
12
+ logger.error(word)
13
+ end
14
+
15
+ Given(/^I write a log message containing the word "([^"]*)" in an Array$/) do |word|
16
+ # Log an Array
17
+ logger.error([1, word, 3])
18
+ end
19
+
20
+ Given(/^I write a log message containing the value "([^"]*)" for the key "([^"]*)" in a Hash$/) do |value, key|
21
+ # Log a Hash
22
+ val = {
23
+ 1 => 2,
24
+ 2 => {
25
+ key => value
26
+ },
27
+ 3 => [
28
+ 'a', "#{key}=#{value}", 'b',
29
+ ],
30
+ }
31
+ logger.error(val)
32
+ end
33
+
34
+ Given(/^I set filter words to include "([^"]*)"$/) do |filter_word|
35
+ logger.filter_words = [filter_word]
36
+ end
37
+
38
+ Given(/^I register a custom filter$/) do
39
+ class MyFilter < TeeLogger::Filter::FilterBase
40
+ FILTER_TYPES = [String]
41
+ WINDOW_SIZE = 1
42
+ def process(*args)
43
+ # Nil everything
44
+ args.each_with_index do |dummy, idx|
45
+ args[idx] = nil
46
+ end
47
+ return args
48
+ end
49
+ end
50
+
51
+ logger.register_filter(MyFilter)
52
+ end
53
+
54
+ Given(/^I write a log message containing the word sequence "([^"]*)", "([^"]*)"$/) do |word1, word2|
55
+ logger.error(word1, word2)
56
+ end
57
+
58
+ Then(/^I expect the log message to ([^ ]* ?)contain the word "([^"]*)"$/) do |mod, word|
59
+ mod = mod.strip
60
+ message = io.string
61
+
62
+ case mod
63
+ when "not"
64
+ assert !message.include?(word), "Log message contains '#{word}' when it must not!:\n#{message}"
65
+ else
66
+ assert message.include?(word), "Log message does not contain '#{word}' when it should!:\n#{message}"
67
+ end
68
+ end
69
+
70
+
@@ -0,0 +1,30 @@
1
+ formatter = nil
2
+ result = nil
3
+
4
+ Given(/^I create a Formatter with "([^"]*)" in the format string$/) do |format|
5
+ formatter = ::TeeLogger::Formatter.new(format)
6
+ end
7
+
8
+ Given(/^I create a Formatter with the "([^"]*)" format string$/) do |constant|
9
+ format = ::TeeLogger::Formatter.const_get(constant)
10
+ formatter = ::TeeLogger::Formatter.new(format)
11
+ end
12
+
13
+ Given(/^I call it with parameters "([^"]*)", "([^"]*)", "([^"]*)" and "([^"]*)"$/) do |severity, time, progname, message|
14
+ # The time needs to be parsed to make some kind of sense; anything else
15
+ # can just be passed through. For time parsing, we need to temporarily force
16
+ # the timezone to UTC, otherwise the tests will run differently on different
17
+ # machines.
18
+ zone = ENV["TZ"]
19
+ ENV["TZ"] = "UTC"
20
+ t = Time.parse(time)
21
+ ENV["TZ"] = zone
22
+
23
+ result = formatter.call(severity, t, progname, message)
24
+ end
25
+
26
+ Then(/^I expect the result to match "([^"]*)"$/) do |expected|
27
+ regex = Regexp.new("^#{expected}$")
28
+ assert regex.match(result), "Expected to match '#{regex}' (#{expected}), but got '#{result}'"
29
+ end
30
+
@@ -1,9 +1,3 @@
1
- begin
2
- require 'test/unit/assertions'
3
- rescue LoadError
4
- require 'minitest/assertions'
5
- end
6
-
7
1
  message = "test message"
8
2
  io = nil
9
3
  logger = nil
@@ -19,9 +13,14 @@ Given(/^I set the flush_interval to "([^"]*)"$/) do |interval|
19
13
  assert [i] == logger.flush_interval, "Setting flush interval did not take: #{logger.flush_interval}"
20
14
  end
21
15
 
22
-
16
+ level_set_exception = nil
23
17
  Given(/^I set the log level to "(.*?)"$/) do |level|
24
- logger.level = level
18
+ begin
19
+ logger.level = level
20
+ level_set_exception = nil
21
+ rescue StandardError => err
22
+ level_set_exception = err
23
+ end
25
24
  end
26
25
 
27
26
  Given(/^I write a log message at log level "(.*?)"$/) do |level|
@@ -33,6 +32,17 @@ Then(/^I expect the log message to appear on the screen$/) do
33
32
  puts "Can't test this; please check manually"
34
33
  end
35
34
 
35
+ Then(/^I expect this to ([^ ]*?) ?raise an exception$/) do |mod|
36
+ if mod.strip != "not"
37
+ assert !level_set_exception.nil?, "Expected an exception, but none was raised."
38
+ end
39
+ end
40
+
41
+ Then(/^I expect the log level to be "([^"]*)"$/) do |result|
42
+ expected = TeeLogger::TeeLogger.convert_level(result.strip)
43
+ assert logger.level == [expected], "Expected numeric level #{expected} but got #{logger.level[0]}."
44
+ end
45
+
36
46
  Then(/^I expect the log level "(.*?)" to (.*?) taken hold$/) do |level, condition|
37
47
  meth = "#{level.downcase}?".to_sym
38
48
  res = logger.send(meth)
@@ -1,4 +1,10 @@
1
1
  require "codeclimate-test-reporter"
2
2
  CodeClimate::TestReporter.start
3
3
 
4
+ begin
5
+ require 'test/unit/assertions'
6
+ rescue LoadError
7
+ require 'minitest/assertions'
8
+ end
9
+
4
10
  require "teelogger"
data/lib/teelogger.rb CHANGED
@@ -6,53 +6,16 @@
6
6
  # All rights reserved.
7
7
  #
8
8
  require "teelogger/version"
9
+ require "teelogger/extensions"
10
+ require "teelogger/levels"
11
+ require "teelogger/formatter"
12
+ require "teelogger/filter"
9
13
 
10
14
  require "logger"
11
15
 
12
16
  module TeeLogger
13
17
  DEFAULT_FLUSH_INTERVAL = 2000
14
18
 
15
- ##
16
- # Extensions for the ruby logger
17
- module LoggerExtensions
18
- attr_accessor :teelogger_io
19
- attr_accessor :flush_interval
20
-
21
- ##
22
- # Flush ruby and OS buffers for this logger
23
- def flush
24
- if @teelogger_io.nil?
25
- raise "TeeLogger logger without IO object, can't do anything"
26
- end
27
-
28
- @teelogger_io.flush
29
- begin
30
- @teelogger_io.fsync
31
- rescue NotImplementedError, Errno::EINVAL
32
- # pass
33
- end
34
- end
35
-
36
-
37
- ##
38
- # This function invokes flush if it's been invoked more often than
39
- # flush_interval.
40
- def auto_flush
41
- if @written.nil?
42
- @written = 0
43
- end
44
-
45
- @written += 1
46
-
47
- if @written >= self.flush_interval
48
- self.flush
49
- @written = 0
50
- end
51
- end
52
- end # module LoggerExtensions
53
-
54
-
55
-
56
19
  ##
57
20
  # Logger that writes to multiple outputs. Behaves just like Ruby's Logger,
58
21
  # and like a hash of String => Logger.
@@ -73,44 +36,22 @@ module TeeLogger
73
36
  # end
74
37
  # end
75
38
  class TeeLogger
39
+ # Extends and includes
40
+ extend ::TeeLogger::Levels
41
+ include ::TeeLogger::Levels
42
+ include ::TeeLogger::Filter
43
+
44
+ # Properties
76
45
  @default_level
46
+ @formatter
77
47
  @loggers
78
48
  @ios
79
49
 
80
- ##
81
- # Convert a log level to its string name
82
- def self.string_level(level)
83
- if level.is_a? String
84
- return level
85
- end
86
-
87
- Logger::Severity.constants.each do |const|
88
- if level == Logger.const_get(const)
89
- return const
90
- end
91
- end
92
-
93
- return nil
94
- end
95
-
96
- ##
97
- # Convert a string log level to its constant value
98
- def self.convert_level(val)
99
- if val.is_a? String
100
- begin
101
- val = Logger.const_get(val.upcase)
102
- rescue NameError
103
- val = Logger::Severity::WARN
104
- end
105
- end
106
-
107
- return val
108
- end
109
50
 
110
51
  private
111
52
  ##
112
53
  # Define log functions as strings, for internal re-use
113
- LOG_FUNCTIONS = Logger::Severity.constants.map { |level| TeeLogger.string_level(level.to_s).downcase }
54
+ LOG_FUNCTIONS = Logger::Severity.constants.map { |level| string_level(level.to_s).downcase }
114
55
 
115
56
  public
116
57
 
@@ -129,26 +70,40 @@ public
129
70
  logger = Logger.new(io)
130
71
 
131
72
  # Initialize logger
132
- io.write "Logging to '#{arg}' initialized with level #{TeeLogger.string_level(@default_level)}.\n"
133
- logger.level = TeeLogger.convert_level(@default_level)
73
+ io.write "Logging to '#{arg}' initialized with level #{string_level(@default_level)}.\n"
74
+ logger.level = convert_level(@default_level)
134
75
  else
135
76
  # We have some other object - let's hope it's an IO object
136
- key = arg.to_s
77
+ key = nil
78
+ case arg
79
+ when STDOUT
80
+ key = 'STDOUT'
81
+ when STDERR
82
+ key = 'STDERR'
83
+ else
84
+ key = arg.to_s
85
+ end
137
86
 
138
87
  # Try to create the logger.
139
88
  io = arg
140
89
  logger = Logger.new(io)
141
90
 
142
91
  # Initialize logger
143
- io.write "Logging to #{key} initialized with level #{TeeLogger.string_level(@default_level)}.\n"
144
- logger.level = TeeLogger.convert_level(@default_level)
92
+ io.write "Logging to #{key} initialized with level #{string_level(@default_level)}.\n"
93
+ logger.level = convert_level(@default_level)
145
94
  end
146
95
 
96
+ # Set the logger formatter
97
+ logger.formatter = @formatter
98
+
147
99
  # Extend logger instances with extra functionality
148
- logger.extend(LoggerExtensions)
100
+ logger.extend(::TeeLogger::LoggerExtensions)
149
101
  logger.teelogger_io = io
150
102
  logger.flush_interval = DEFAULT_FLUSH_INTERVAL
151
103
 
104
+ # Flush the "Logging to..." line
105
+ logger.flush
106
+
152
107
  if not key.nil? and not logger.nil? and not io.nil?
153
108
  @loggers[key] = logger
154
109
  @ios[key] = io
@@ -166,9 +121,13 @@ public
166
121
 
167
122
  # Initialization
168
123
  @default_level = Logger::Severity::INFO
124
+ @formatter = ::TeeLogger::Formatter.new
169
125
  @loggers = {}
170
126
  @ios = {}
171
127
 
128
+ # Load built-in filters
129
+ load_filters(*args)
130
+
172
131
  # Create logs for all arguments
173
132
  args.each do |arg|
174
133
  add_logger(arg)
@@ -180,7 +139,7 @@ public
180
139
  # Set log level; override this to also accept strings
181
140
  def level=(val)
182
141
  # Convert strings to the constant value
183
- val = TeeLogger.convert_level(val)
142
+ val = convert_level(val)
184
143
 
185
144
  # Update the default log level
186
145
  @default_level = val
@@ -192,6 +151,19 @@ public
192
151
  end
193
152
 
194
153
 
154
+ ##
155
+ # Set the formatter
156
+ def formatter=(formatter)
157
+ # Update the default formatter
158
+ @formatter = formatter
159
+
160
+ # Set all loggers' formatters
161
+ @loggers.each do |key, logger|
162
+ logger.formatter = formatter
163
+ end
164
+ end
165
+
166
+
195
167
  ##
196
168
  # Log an exception
197
169
  def exception(message, ex)
@@ -254,6 +226,7 @@ public
254
226
  end
255
227
 
256
228
  def method_missing(meth, *args, &block)
229
+ puts "MISSING #{meth}"
257
230
  dispatch(meth, *args, &block)
258
231
  end
259
232
 
@@ -261,12 +234,36 @@ public
261
234
 
262
235
 
263
236
  def dispatch(meth, *args, &block)
264
- meth_name = meth.to_s
265
-
266
237
  if @loggers.nil? or @loggers.empty?
267
238
  raise "No loggers created, can't do anything."
268
239
  end
269
240
 
241
+ # Try dispatching the call, with preprocessing based on whether it
242
+ # is a log function or not.
243
+ meth_name = meth.to_s
244
+
245
+ ret = []
246
+ if LOG_FUNCTIONS.include? meth_name
247
+ ret = dispatch_log(meth_name, *args)
248
+ else
249
+ ret = dispatch_other(meth_name, *args, &block)
250
+ end
251
+
252
+ # Some double checking on the return value(s).
253
+ if not ret.empty?
254
+ return ret
255
+ end
256
+
257
+ # If the method wasn't from the loggers, we'll try to send it to the
258
+ # hash.
259
+ return @loggers.send(meth_name, *args, &block)
260
+ end
261
+
262
+
263
+ def dispatch_log(meth_name, *args)
264
+ # Filter all arguments
265
+ args = apply_filters(*args)
266
+
270
267
  # Compose message
271
268
  msg = args.map do |arg|
272
269
  if arg.is_a? String
@@ -281,26 +278,23 @@ public
281
278
  ret = []
282
279
  @loggers.each do |key, logger|
283
280
  if logger.respond_to? meth_name
284
- if LOG_FUNCTIONS.include? meth_name
285
- ret << logger.send(meth_name, key) do
286
- message
287
- end
288
- else
289
- ret << logger.send(meth_name, *args, &block)
281
+ ret << logger.send(meth_name, key) do
282
+ message
290
283
  end
291
284
  end
292
285
  end
286
+ return ret
287
+ end
293
288
 
294
- # Some double checking on the return value(s).
295
- if not ret.empty?
296
- return ret
297
- end
298
289
 
299
- # If the method wasn't from the loggers, we'll try to send it to the
300
- # hash.
301
- return @loggers.send(meth_name, *args, &block)
290
+ def dispatch_other(meth_name, *args, &block)
291
+ ret = []
292
+ @loggers.each do |key, logger|
293
+ if logger.respond_to? meth_name
294
+ ret << logger.send(meth_name, *args, &block)
295
+ end
296
+ end
297
+ return ret
302
298
  end
303
-
304
299
  end
305
-
306
300
  end
@@ -0,0 +1,47 @@
1
+ #
2
+ # TeeLogger
3
+ # https://github.com/spriteCloud/teelogger
4
+ #
5
+ # Copyright (c) 2014-2015 spriteCloud B.V. and other TeeLogger contributors.
6
+ # All rights reserved.
7
+ #
8
+ module TeeLogger
9
+ ##
10
+ # Extensions for the ruby logger
11
+ module LoggerExtensions
12
+ attr_accessor :teelogger_io
13
+ attr_accessor :flush_interval
14
+
15
+ ##
16
+ # Flush ruby and OS buffers for this logger
17
+ def flush
18
+ if @teelogger_io.nil?
19
+ raise "TeeLogger logger without IO object, can't do anything"
20
+ end
21
+
22
+ @teelogger_io.flush
23
+ begin
24
+ @teelogger_io.fsync
25
+ rescue NotImplementedError, Errno::EINVAL
26
+ # pass
27
+ end
28
+ end
29
+
30
+
31
+ ##
32
+ # This function invokes flush if it's been invoked more often than
33
+ # flush_interval.
34
+ def auto_flush
35
+ if @written.nil?
36
+ @written = 0
37
+ end
38
+
39
+ @written += 1
40
+
41
+ if @written >= self.flush_interval
42
+ self.flush
43
+ @written = 0
44
+ end
45
+ end
46
+ end # module LoggerExtensions
47
+ end # module TeeLogger
@@ -0,0 +1,215 @@
1
+ #
2
+ # TeeLogger
3
+ # https://github.com/spriteCloud/teelogger
4
+ #
5
+ # Copyright (c) 2014,2015 spriteCloud B.V. and other TeeLogger contributors.
6
+ # All rights reserved.
7
+ #
8
+ require 'require_all'
9
+
10
+ module TeeLogger
11
+ module Filter
12
+ ##
13
+ # The default words to filter. It's up to each individual filter to decide
14
+ # what to do when they encounter a word, but these are the words the filters
15
+ # should process.
16
+ # Note that they can be strings or regular expressions. Regular expressions
17
+ # should by and large not be anchored to the beginning or end of strings.
18
+ DEFAULT_FILTER_WORDS = [
19
+ /password[a-z\-_]*/,
20
+ /salt[a-z\-_]*/,
21
+ ]
22
+
23
+ ##
24
+ # Filter words
25
+ def filter_words
26
+ @filter_words ||= DEFAULT_FILTER_WORDS
27
+ return @filter_words
28
+ end
29
+
30
+ def filter_words=(arg)
31
+ # Coerce into array
32
+ begin
33
+ arr = []
34
+ arg.each do |item|
35
+ arr << item
36
+ end
37
+ @filter_words = arr
38
+ rescue NameError, NoMethodError
39
+ raise "Can't set filter words, not iterable: #{arg}"
40
+ end
41
+ end
42
+
43
+
44
+ ##
45
+ # Load all built-in filters.
46
+ def load_filters(*args)
47
+ require_rel 'filters'
48
+ ::TeeLogger::Filter.constants.collect {|const_sym|
49
+ ::TeeLogger::Filter.const_get(const_sym)
50
+ }.each do |filter|
51
+ begin
52
+ register_filter(filter)
53
+ if not ENV['TEELOGGER_VERBOSE'].nil? and ENV['TEELOGGER_VERBOSE'].to_i > 0
54
+ puts "Registered filter #{filter}."
55
+ end
56
+ rescue StandardError => err
57
+ if not ENV['TEELOGGER_VERBOSE'].nil? and ENV['TEELOGGER_VERBOSE'].to_i > 0
58
+ puts "Not registering filter: #{err}"
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ ##
65
+ # Returns all registered filters.
66
+ def registered_filters
67
+ # Initialize if it doesn't exist
68
+ @filters ||= {}
69
+ return @filters
70
+ end
71
+
72
+ ##
73
+ # Expects a class, registers the class for use by the filter function
74
+ def register_filter(filter)
75
+ # Sanity checks/register filter
76
+ if filter.class != Class
77
+ raise "Ignoring '#{filter}', not a class."
78
+ end
79
+
80
+ if not filter < FilterBase
81
+ raise "Class '#{filter}' is not derived from FilterBase."
82
+ end
83
+
84
+ begin
85
+ window = filter::WINDOW_SIZE.to_i
86
+ window_filters = registered_filters.fetch(window, {})
87
+
88
+ filter::FILTER_TYPES.each do |type|
89
+ type_filters = window_filters.fetch(type, [])
90
+ type_filters.push(filter) unless type_filters.include?(filter)
91
+ window_filters[type] = type_filters
92
+ end
93
+
94
+ registered_filters[window] = window_filters
95
+ rescue NameError, NoMethodError
96
+ raise "Class '#{filter}' is missing a FILTER_TYPES Array or a WINDOW_SIZE Integer."
97
+ end
98
+ end
99
+
100
+ ##
101
+ # Applies all registered filters.
102
+ def apply_filters(*args)
103
+ # Pre-process filter words: we need to have regular expressions everywhere
104
+ words = []
105
+ filter_words.each do |word|
106
+ if word.is_a? Regexp
107
+ words << word
108
+ else
109
+ words << Regexp.new(word.to_s)
110
+ end
111
+ end
112
+
113
+ # We instanciate each filter once per application, and store the instnaces
114
+ # in a cache for that duration.
115
+ filter_cache = {}
116
+
117
+ # Pass state on to apply_filters_internal
118
+ state = {
119
+ :words => words,
120
+ :filter_cache => filter_cache,
121
+ :filters => self,
122
+ }
123
+ return apply_filters_internal(state, *args)
124
+ end
125
+
126
+
127
+ ##
128
+ # Implementation of apply_filters that doesn't initialize state, but carries
129
+ # it over. Used internally only.
130
+ def apply_filters_internal(state, *args)
131
+ filtered_args = args
132
+
133
+ # Iterate through filters
134
+ registered_filters.each do |window, window_filters|
135
+ # Determine actual window size
136
+ window_size = [window, filtered_args.size].min
137
+
138
+ # Process each window so that elements are updated in-place. This
139
+ # means we'll start at index 0 and process up to window_size elements.
140
+ idx = 0
141
+ while (idx + window_size - 1) < filtered_args.size
142
+ # We need to use *one* argument to determine whether the filter
143
+ # type applies. The current strategy is to match the first argument
144
+ # only, and let the filter cast to other types if necessary.
145
+ first_arg = filtered_args[idx]
146
+
147
+ window_filters.each do |class_match, type_filters|
148
+ # We process with these type filters if first_arg matches the
149
+ # class_match.
150
+ if not first_arg.is_a? class_match
151
+ next
152
+ end
153
+
154
+ # Now process with the given filters.
155
+ type_filters.each do |filter|
156
+ # XXX Do not turn this into a one-liner, or we'll instanciate
157
+ # filters without using them.
158
+ filter_instance = state[:filter_cache].fetch(filter, nil)
159
+ if filter_instance.nil?
160
+ filter_instance = filter.new(state)
161
+ state[:filter_cache][filter] = filter_instance
162
+ end
163
+
164
+ # Single item windows need to be processed a bit differently from
165
+ # multi-item windows.
166
+ tuple = filtered_args[idx..idx + window_size - 1]
167
+ filtered = filter_instance.process(*tuple)
168
+
169
+ # Sanity check result
170
+ if filtered.size != tuple.size
171
+ raise "Filter #{filter} added or removed items to the log; don't know how to process!"
172
+ end
173
+
174
+ filtered.each_with_index do |item, offset|
175
+ filtered_args[idx + offset] = item
176
+ end
177
+ end # type_filters.each
178
+ end # window_filters.each
179
+
180
+ # Advance to the next window
181
+ idx += 1
182
+ end # each window
183
+ end # all registered filters
184
+
185
+ return filtered_args
186
+ end
187
+
188
+
189
+ ##
190
+ # Any filter implementations must derive from this
191
+ class FilterBase
192
+ # Define FILTER_TYPES = [class, class] to declare what types this filter
193
+ # applies to.
194
+ # Define WINDOW_SIZE = int to declare how many parameters the filter
195
+ # processes at a time. It will be a sliding window of arguments.
196
+ # Note that filters may receive fewer arguments if there are less than
197
+ # WINDOW_SIZE in total.
198
+
199
+ ##
200
+ # Initialize with filter words
201
+ attr_accessor :run_data
202
+
203
+ def initialize(run_data)
204
+ @run_data = run_data
205
+ end
206
+
207
+ ##
208
+ # Base filter leaves the argument untouched.
209
+ def process(*args)
210
+ args
211
+ end
212
+
213
+ end # class FilterBase
214
+ end # end module Filter
215
+ end # end module TeeLogger
@@ -0,0 +1,42 @@
1
+ #
2
+ # TeeLogger
3
+ # https://github.com/spriteCloud/teelogger
4
+ #
5
+ # Copyright (c) 2014,2015 spriteCloud B.V. and other TeeLogger contributors.
6
+ # All rights reserved.
7
+ #
8
+ require 'teelogger/filter'
9
+
10
+ module TeeLogger
11
+ module Filter
12
+ ##
13
+ # The Assignment filter takes strings of the form <prefix><word>=<value> and
14
+ # obfuscates the value.
15
+ class Assignment < FilterBase
16
+ FILTER_TYPES = [String]
17
+ WINDOW_SIZE = 1
18
+
19
+ def initialize(*args)
20
+ super(*args)
21
+
22
+ # We create more complex matches out of the filter words passed.
23
+ @matches = []
24
+ run_data[:words].each do |word|
25
+ @matches << /(-{0,2}#{word} *[=:] *)(.*)/i
26
+ end
27
+ end
28
+
29
+ def process(*args)
30
+ # Note that due to the window size of one, args is only an element long.
31
+ args.each do |arg|
32
+ @matches.each do |match|
33
+ # Modify the matching arguments in place
34
+ arg.gsub!(match, '\1[REDACTED]')
35
+ end
36
+ end
37
+
38
+ return args
39
+ end
40
+ end # class Assignment
41
+ end # module Filter
42
+ end # module TeeLogger
@@ -0,0 +1,45 @@
1
+ #
2
+ # TeeLogger
3
+ # https://github.com/spriteCloud/teelogger
4
+ #
5
+ # Copyright (c) 2014,2015 spriteCloud B.V. and other TeeLogger contributors.
6
+ # All rights reserved.
7
+ #
8
+ require 'teelogger/filter'
9
+
10
+ module TeeLogger
11
+ module Filter
12
+ ##
13
+ # The CLI filter takes sequences of strings of the form ["word", "value"]
14
+ # and obfuscates the value if the word matches.
15
+ class CLI < FilterBase
16
+ FILTER_TYPES = [String]
17
+ WINDOW_SIZE = 2
18
+
19
+ def initialize(*args)
20
+ super(*args)
21
+
22
+ # We create more complex matches out of the filter words passed.
23
+ @matches = []
24
+ run_data[:words].each do |word|
25
+ @matches << /(-{0,2}#{word})(.*)/i
26
+ end
27
+ end
28
+
29
+ def process(*args)
30
+ # In case the window is too small, ignore it
31
+ if args.size < 2
32
+ return args
33
+ end
34
+
35
+ # Otherwise, if the first argument matches, we'll redact the second.
36
+ @matches.each do |word|
37
+ if word.match(args[0])
38
+ args[1] = '[REDACTED]'
39
+ end
40
+ end
41
+ return args
42
+ end
43
+ end # class CLI
44
+ end # module Filter
45
+ end # module TeeLogger
@@ -0,0 +1,55 @@
1
+ #
2
+ # TeeLogger
3
+ # https://github.com/spriteCloud/teelogger
4
+ #
5
+ # Copyright (c) 2014,2015 spriteCloud B.V. and other TeeLogger contributors.
6
+ # All rights reserved.
7
+ #
8
+ require 'teelogger/filter'
9
+
10
+ module TeeLogger
11
+ module Filter
12
+ ##
13
+ # The Recursive filter takes Hashes or Arrays, and recursively applies the
14
+ # other filters to their values.
15
+ class Recursive < FilterBase
16
+ FILTER_TYPES = [Enumerable]
17
+ WINDOW_SIZE = 1
18
+
19
+ def process(*args)
20
+ # For each argument, recurse processing. Note that due to the window
21
+ # size of one, args is only an element long - but let's write this out
22
+ # properly.
23
+ args.each do |arg|
24
+ # Since we're matching enumerabls, the argument must respond to .each
25
+ arg.each do |expanded|
26
+ # The expanded variable can be a single item or a list of items.
27
+ # If expanded is itself an Enumarable, the first item is a key, the remainder
28
+ # values. We need to recursively process the values.
29
+ if expanded.is_a? Enumerable
30
+ # If the key matches any of the filter words, we'll just skip
31
+ # the value entirely.
32
+ key = expanded[0]
33
+ redacted = false
34
+ run_data[:words].each do |word|
35
+ if word.match(key.to_s)
36
+ arg[key] = '[REDACTED]'
37
+ redacted = true
38
+ break
39
+ end
40
+ end
41
+
42
+ if not redacted
43
+ arg[key] = run_data[:filters].apply_filters_internal(run_data, *expanded[1..-1])
44
+ end
45
+ else
46
+ arg = run_data[:filters].apply_filters_internal(run_data, expanded)
47
+ end
48
+ end
49
+ end
50
+
51
+ return args
52
+ end
53
+ end # class Recursive
54
+ end # module Filter
55
+ end # module TeeLogger
@@ -0,0 +1,108 @@
1
+ #
2
+ # TeeLogger
3
+ # https://github.com/spriteCloud/teelogger
4
+ #
5
+ # Copyright (c) 2014-2015 spriteCloud B.V. and other TeeLogger contributors.
6
+ # All rights reserved.
7
+ #
8
+ require 'tai64'
9
+
10
+ module TeeLogger
11
+ ##
12
+ # Placeholders for the formatter take a single argument, and convert it to
13
+ # a string argument using placeholder specific rules.
14
+ module FormatterPlaceholders
15
+ def severity(severity, time, progname, message)
16
+ severity.to_s.upcase
17
+ end
18
+
19
+ def short_severity(severity, time, progname, message)
20
+ severity.to_s.upcase[0..0]
21
+ end
22
+
23
+ def logger_timestamp(severity, time, progname, message)
24
+ time.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d" % time.usec
25
+ end
26
+
27
+ def iso8601_timestamp(severity, time, progname, message)
28
+ time.strftime("%Y-%m-%dT%H:%M:%S%z")
29
+ end
30
+
31
+ def iso8601_timestamp_utc(severity, time, progname, message)
32
+ time.dup.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
33
+ end
34
+
35
+ def tai64n_timestamp(severity, time, progname, message)
36
+ Tai64::Time.new(time).to_label.to_s
37
+ end
38
+
39
+ def logger(severity, time, progname, message)
40
+ progname.to_s
41
+ end
42
+
43
+ def message(severity, time, progname, message)
44
+ message.to_s
45
+ end
46
+
47
+ def pid(severity, time, progname, message)
48
+ $$.to_s
49
+ end
50
+
51
+ extend self
52
+ end # module FormatterPlaceholders
53
+
54
+ ##
55
+ # The formatter class accepts a format string, but in a different format from
56
+ # Kernel#sprintf. Instead, placeholders enclosed in {} (but without the Ruby-
57
+ # typical #, so not #{}) will get replaced with the output of the functions
58
+ # defined in FormatterPlaceholders.
59
+ #
60
+ # The class also defines a few example format strings as constants.
61
+ class Formatter
62
+ # Valid placeholder to use in the format string
63
+ PLACEHOLDERS = ::TeeLogger::FormatterPlaceholders.instance_methods
64
+
65
+ ##
66
+ # Some format strings defined
67
+
68
+ # Format string most similar to the Ruby logger
69
+ FORMAT_LOGGER = "{short_severity}, [{logger_timestamp} \#{pid}] {severity} -- {logger}: {message}\n"
70
+
71
+ # Default format string
72
+ FORMAT_DEFAULT = "{short_severity}, [{iso8601_timestamp} \#{pid}] {logger}: {message}\n"
73
+
74
+ # Shorter format string
75
+ FORMAT_SHORT = "{short_severity}, [{iso8601_timestamp}] {message}\n"
76
+
77
+ # DJB format using Tai64N labels
78
+ FORMAT_DJB = "{tai64n_timestamp} {severity}: {message}\n"
79
+
80
+ ##
81
+ # Implementation
82
+ def initialize(format = FORMAT_DEFAULT)
83
+ @format = format
84
+ end
85
+
86
+ def call(*args) # shortern *args; the same pattern as placeholders is used
87
+ # Formatting the message means replacing each placeholder with results
88
+ # from the placeholder function. We're caching results to save some time.
89
+ cache = {}
90
+ message = @format.dup
91
+
92
+ PLACEHOLDERS.each do |placeholder|
93
+ value = nil
94
+ begin
95
+ value = cache.fetch(placeholder,
96
+ ::TeeLogger::FormatterPlaceholders.send(placeholder.to_sym, *args))
97
+ cache[placeholder] = value
98
+ rescue NoMethodError
99
+ raise "Invalid formatter placeholder used in format string: #{placeholder}"
100
+ end
101
+
102
+ message.gsub!(/{#{placeholder}}/, value)
103
+ end
104
+
105
+ return message
106
+ end
107
+ end # class Formatter
108
+ end # module TeeLogger
@@ -0,0 +1,41 @@
1
+ #
2
+ # TeeLogger
3
+ # https://github.com/spriteCloud/teelogger
4
+ #
5
+ # Copyright (c) 2014-2015 spriteCloud B.V. and other TeeLogger contributors.
6
+ # All rights reserved.
7
+ #
8
+ module TeeLogger
9
+ module Levels
10
+ ##
11
+ # Convert a log level to its string name
12
+ def string_level(level)
13
+ if level.is_a? String
14
+ return level
15
+ end
16
+
17
+ Logger::Severity.constants.each do |const|
18
+ if level == Logger.const_get(const)
19
+ return const
20
+ end
21
+ end
22
+
23
+ return nil
24
+ end
25
+
26
+ ##
27
+ # Convert a string log level to its constant value
28
+ def convert_level(val)
29
+ if val.is_a? String
30
+ begin
31
+ val = Logger.const_get(val.upcase)
32
+ rescue NameError
33
+ raise "Invalid log level '#{val}' specified."
34
+ end
35
+ end
36
+
37
+ return val
38
+ end
39
+
40
+ end # module Levels
41
+ end # module TeeLogger
@@ -6,5 +6,5 @@
6
6
  # All rights reserved.
7
7
  #
8
8
  module TeeLogger
9
- VERSION = "0.4.1"
9
+ VERSION = "0.5.0"
10
10
  end
data/teelogger.gemspec CHANGED
@@ -22,4 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "cucumber"
24
24
  spec.add_development_dependency "minitest"
25
+
26
+ spec.add_dependency "tai64", "~> 0.0"
27
+ spec.add_dependency "require_all", "~> 1.3"
25
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teelogger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jens Finkhaeuser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-23 00:00:00.000000000 Z
11
+ date: 2015-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - '>='
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tai64
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '0.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: require_all
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
69
97
  description: Mini wrapper around Ruby Logger for logging to multiple destinations.
70
98
  email:
71
99
  - foss@spritecloud.com
@@ -79,10 +107,21 @@ files:
79
107
  - LICENSE
80
108
  - README.md
81
109
  - Rakefile
110
+ - features/filter.feature
111
+ - features/formatter.feature
82
112
  - features/logger.feature
83
- - features/step_definitions/steps.rb
113
+ - features/step_definitions/filter_steps.rb
114
+ - features/step_definitions/formatter_steps.rb
115
+ - features/step_definitions/logger_steps.rb
84
116
  - features/support/env.rb
85
117
  - lib/teelogger.rb
118
+ - lib/teelogger/extensions.rb
119
+ - lib/teelogger/filter.rb
120
+ - lib/teelogger/filters/assignment.rb
121
+ - lib/teelogger/filters/cli.rb
122
+ - lib/teelogger/filters/recursive.rb
123
+ - lib/teelogger/formatter.rb
124
+ - lib/teelogger/levels.rb
86
125
  - lib/teelogger/version.rb
87
126
  - teelogger.gemspec
88
127
  homepage: https://github.com/spriteCloud/teelogger
@@ -110,6 +149,10 @@ signing_key:
110
149
  specification_version: 4
111
150
  summary: Mini wrapper around Ruby Logger for logging to multiple destinations.
112
151
  test_files:
152
+ - features/filter.feature
153
+ - features/formatter.feature
113
154
  - features/logger.feature
114
- - features/step_definitions/steps.rb
155
+ - features/step_definitions/filter_steps.rb
156
+ - features/step_definitions/formatter_steps.rb
157
+ - features/step_definitions/logger_steps.rb
115
158
  - features/support/env.rb