teelogger 0.4.1 → 0.5.0

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.
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