Skip to content

Commit 14d908a

Browse files
committed
Merge pull request #4 from firespring/story/TP-17814
Add a normalizer to deal with certain nested context objects dumping with different formats that cause Logstash and Loggly to complain.
2 parents e5861d8 + fefc782 commit 14d908a

File tree

8 files changed

+217
-44
lines changed

8 files changed

+217
-44
lines changed

lib/hedgelog.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'hedgelog/version'
33
require 'hedgelog/context'
44
require 'hedgelog/scrubber'
5+
require 'hedgelog/normalizer'
56
require 'logger'
67
require 'yajl'
78

@@ -26,7 +27,8 @@ def initialize(logdev = STDOUT, shift_age = nil, shift_size = nil)
2627
@logdev = nil
2728
@app = nil
2829
@scrubber = Hedgelog::Scrubber.new
29-
@channel_context = Hedgelog::Context.new(@scrubber)
30+
@normalizer = Hedgelog::Normalizer.new
31+
@channel_context = Hedgelog::Context.new(@scrubber, @normalizer)
3032

3133
if logdev.is_a?(self.class)
3234
@channel = logdev
@@ -51,7 +53,7 @@ def add(severity = LEVELS[:unknown], message = nil, progname = nil, context = {}
5153
message, context = *yield if block
5254
context ||= {}
5355

54-
context = Hedgelog::Context.new(@scrubber, context) unless context.is_a? Hedgelog::Context
56+
context = Hedgelog::Context.new(@scrubber, @normalizer, context) unless context.is_a? Hedgelog::Context
5557
context.merge!(@channel_context)
5658
context[:message] ||= message
5759

@@ -119,6 +121,7 @@ def level_from_int(level)
119121
def write(severity, context)
120122
return true if @logdev.nil?
121123

124+
context.normalize!
122125
context.scrub!
123126

124127
data = context.merge(default_data(severity))

lib/hedgelog/context.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
require 'hedgelog/scrubber'
2+
require 'hedgelog/normalizer'
23

34
class Hedgelog
45
class Context
5-
def initialize(scrubber, data = {})
6+
def initialize(scrubber, normalizer, data = {})
67
raise ::ArgumentError, "#{self.class}: argument must be Hash got #{data.class}." unless data.is_a? Hash
78
check_reserved_keys(data)
89
@data = data
910
@scrubber = scrubber
11+
@normalizer = normalizer
1012
end
1113

1214
def []=(key, val)
@@ -50,6 +52,11 @@ def scrub!
5052
self
5153
end
5254

55+
def normalize!
56+
@data = @normalizer.normalize(@data)
57+
self
58+
end
59+
5360
def to_h
5461
@data
5562
end

lib/hedgelog/normalizer.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
class Hedgelog
2+
class Normalizer
3+
def normalize(data)
4+
# Need to Marshal.dump/Marshal.load to deep copy the input so that scrubbing doesn't change global state
5+
d = Marshal.load(Marshal.dump(data))
6+
normalize_hash(d)
7+
end
8+
9+
def normalize_struct(struct)
10+
normalize_hash(Hash[struct.each_pair.to_a])
11+
end
12+
13+
def normalize_hash(hash)
14+
Hash[hash.map do |key, val|
15+
[key, normalize_thing(val)]
16+
end]
17+
end
18+
19+
def normalize_array(array)
20+
array.to_json
21+
end
22+
23+
private
24+
25+
def normalize_thing(thing)
26+
return '' if thing.nil?
27+
return normalize_struct(thing) if thing.is_a?(Struct)
28+
return normalize_array(thing) if thing.is_a?(Array)
29+
return normalize_hash(thing) if thing.is_a?(Hash)
30+
thing
31+
end
32+
end
33+
end

spec/hedgelog/context_spec.rb

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
require 'hedgelog/context'
33

44
dummy_scrubber = Hedgelog::ScrubReplacement.new(:dummy, 'DUMMY')
5+
dummy_normalizer = Hedgelog::Normalizer.new
56

67
describe Hedgelog::Context do
78
describe '#[]=' do
8-
subject(:instance) { described_class.new(dummy_scrubber, data) }
9+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, data) }
910
subject(:instance_with_value) do
1011
instance[key] = val
1112
instance
@@ -29,7 +30,7 @@
2930
end
3031

3132
describe '#[]' do
32-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
33+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
3334

3435
context 'when key is a valid key' do
3536
it 'sets the value on the context' do
@@ -44,7 +45,7 @@
4445
end
4546

4647
describe '#delete' do
47-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
48+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
4849

4950
context 'when key is a valid key' do
5051
it 'deletes the key' do
@@ -62,7 +63,7 @@
6263
end
6364

6465
describe '#clear' do
65-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
66+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
6667
it 'clears the context' do
6768
expect(instance[:foo]).to eq 'bar'
6869
instance.clear
@@ -71,15 +72,15 @@
7172
end
7273

7374
describe '#merge' do
74-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
75+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
7576
it 'returns a merged hash of the context and the data' do
7677
expect(instance[:foo]).to eq 'bar'
7778
expect(instance.merge(baz: 'qux')).to include(foo: 'bar', baz: 'qux')
7879
end
7980
end
8081

8182
describe '#merge' do
82-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
83+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
8384

8485
context 'with valid keys in the hash' do
8586
it 'updates the context with the merged data' do
@@ -110,7 +111,7 @@
110111
end
111112

112113
describe '#overwrite!' do
113-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
114+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
114115

115116
context 'with valid keys in the hash' do
116117
it 'replaces the context with new data, preserving keys that already exist' do
@@ -129,16 +130,24 @@
129130
end
130131

131132
describe '#scrub!' do
132-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
133+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
133134
it 'scrubs the data' do
134135
expect(dummy_scrubber).to receive(:scrub).with(foo: 'bar')
135136
subject.scrub!
136137
end
137138
end
138139

140+
describe '#normalize!' do
141+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
142+
it 'normalizes the data' do
143+
expect(dummy_normalizer).to receive(:normalize).with(foo: 'bar')
144+
subject.normalize!
145+
end
146+
end
147+
139148
describe 'to_h' do
140-
subject(:instance) { described_class.new(dummy_scrubber, foo: 'bar') }
141-
it 'returnst he data as a hash' do
149+
subject(:instance) { described_class.new(dummy_scrubber, dummy_normalizer, foo: 'bar') }
150+
it 'returns the data as a hash' do
142151
expect(instance.to_h).to include(foo: 'bar')
143152
end
144153
end

spec/hedgelog/normalizer_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
require 'spec_helper'
2+
require 'hedgelog/normalizer'
3+
4+
describe Hedgelog::Normalizer do
5+
subject(:instance) { described_class.new }
6+
let(:struct_class) { Struct.new(:foo, :bar) }
7+
let(:struct) { struct_class.new(1234, 'dummy') }
8+
let(:hash) { {message: 'dummy=1234'} }
9+
let(:array) { ['dummy string', {message: 'dummy=1234'}] }
10+
describe '#normalize' do
11+
it 'returns the normalized data' do
12+
expect(instance.normalize(hash)).to include(message: 'dummy=1234')
13+
end
14+
15+
it 'does not modify external state' do
16+
myvar = 'dummy=1234'
17+
orig_myvar = myvar.clone
18+
19+
data = {foo: myvar}
20+
instance.normalize(data)
21+
expect(myvar).to eq orig_myvar
22+
end
23+
end
24+
describe '#normalize_hash' do
25+
it 'returns a normalized hash' do
26+
expect(instance.normalize_hash(hash)).to include(message: 'dummy=1234')
27+
end
28+
end
29+
describe '#normalize_struct' do
30+
it 'returns struct as a normalized hash' do
31+
expect(instance.normalize_struct(struct)).to include(foo: 1234, bar: 'dummy')
32+
end
33+
end
34+
describe '#normalize_array' do
35+
it 'returns array as a json string' do
36+
normalized_array = instance.normalize_array(array)
37+
expect(normalized_array).to be_a String
38+
expect(normalized_array).to eq '["dummy string",{"message":"dummy=1234"}]'
39+
end
40+
end
41+
context 'When a hash contains different types of data' do
42+
let(:data) { hash }
43+
before :each do
44+
# add other types to the hash
45+
data[:hash] = hash.clone
46+
data[:struct] = struct
47+
data[:array] = array
48+
data[:string] = 'dummy'
49+
data[:number] = 1234
50+
end
51+
it 'normalizes recursively' do
52+
result = instance.normalize_hash(data)
53+
expect(result[:hash]).to include(message: 'dummy=1234')
54+
expect(result[:message]).to include('dummy=1234')
55+
expect(result[:struct]).to be_a Hash
56+
expect(result[:struct]).to include(foo: 1234, bar: 'dummy')
57+
expect(result[:array]).to eq '["dummy string",{"message":"dummy=1234"}]'
58+
expect(result[:string]).to eq 'dummy'
59+
expect(result[:number]).to eq 1234
60+
end
61+
end
62+
end

spec/hedgelog_spec.rb

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -278,46 +278,55 @@
278278
let(:log_dev) { '/dev/null' }
279279
let(:standard_logger) { Logger.new(log_dev) }
280280
let(:hedgelog_logger) { Hedgelog.new(log_dev) }
281+
let(:message) { 'log message' }
282+
let(:hedgelog_params) { [message] }
281283

282-
context 'when logging a string' do
283-
let(:message) { 'log message' }
284-
285-
context 'when in debug mode' do
286-
it 'is not be more than 12x slower than standard ruby logger' do
287-
report = Benchmark.ips(quiet: true) do |bm|
288-
bm.config(time: 5, warmup: 2)
289-
bm.report('standard_logger') { standard_logger.debug(message) }
290-
bm.report('hedgelog_logger') { hedgelog_logger.debug(message) }
291-
end
284+
before :each do
285+
standard_logger.level = level
286+
hedgelog_logger.level = level
287+
end
292288

293-
standard_benchmark, hedgelog_benchmark = *report.entries
289+
shared_context 'logging with context' do
290+
let(:hash) { {message: 'dummy=1234'} }
291+
let(:array) { ['dummy string', {message: 'dummy=1234'}] }
292+
let(:context) { hash }
293+
let(:hedgelog_params) { [message, context] }
294+
before :each do
295+
# add other types to the hash
296+
context[:hash] = hash.clone
297+
context[:array] = array
298+
context[:string] = 'dummy'
299+
context[:number] = 1234
300+
end
301+
end
294302

295-
expect(hedgelog_benchmark.ips).to be > (standard_benchmark.ips / 12)
303+
context 'when in debug mode' do
304+
let(:level) { Logger::DEBUG }
305+
context 'when logging a string' do
306+
it 'is no more than 12x slower than the stdlib logger' do
307+
expect { standard_logger.debug(message) }.to perform.times_slower(12).than { hedgelog_logger.debug(*hedgelog_params) }
296308
end
297309
end
310+
context 'when logging with context' do
311+
include_context 'logging with context'
298312

299-
context 'when not in debug mode' do
300-
let(:standard_logger) do
301-
logger = Logger.new(log_dev)
302-
logger.level = Logger::INFO
303-
logger
313+
it 'is no more than 16x slower than the stdlib logger' do
314+
expect { standard_logger.debug(message) }.to perform.times_slower(16).than { hedgelog_logger.debug(*hedgelog_params) }
304315
end
305-
let(:hedgelog_logger) do
306-
logger = Hedgelog.new(log_dev)
307-
logger.level = Logger::INFO
308-
logger
316+
end
317+
end
318+
context 'when not in debug mode' do
319+
let(:level) { Logger::INFO }
320+
context 'when logging a string' do
321+
it 'is no more than 5x slower than the stdlib logger' do
322+
expect { standard_logger.info(message) }.to perform.times_slower(5).than { hedgelog_logger.info(*hedgelog_params) }
309323
end
324+
end
325+
context 'when logging with context' do
326+
include_context 'logging with context'
310327

311-
it 'is not be more than 5x slower than standard ruby logger' do
312-
report = Benchmark.ips(quiet: true) do |bm|
313-
bm.config(time: 5, warmup: 2)
314-
bm.report('standard_logger') { standard_logger.info(message) }
315-
bm.report('hedgelog_logger') { hedgelog_logger.info(message) }
316-
end
317-
318-
standard_benchmark, hedgelog_benchmark = *report.entries
319-
320-
expect(hedgelog_benchmark.ips).to be > (standard_benchmark.ips / 5)
328+
it 'is no more than 12x slower than the stdlib logger' do
329+
expect { standard_logger.info(message) }.to perform.times_slower(12).than { hedgelog_logger.info(*hedgelog_params) }
321330
end
322331
end
323332
end

spec/spec_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,10 @@
33
require 'simplecov'
44
require 'hedgelog'
55

6+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each { |f| require f }
7+
68
SimpleCov.start
9+
10+
RSpec.configure do |config|
11+
config.include Matchers::Benchmark
12+
end

spec/support/matchers/benchmark.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'rspec/expectations'
2+
require 'benchmark/ips'
3+
4+
module Matchers
5+
module Benchmark
6+
extend RSpec::Matchers::DSL
7+
8+
matcher :perform do
9+
match do |block_arg|
10+
report = ::Benchmark.ips(quiet: true) do |bm|
11+
bm.config(time: 5, warmup: 2)
12+
bm.report('first') { @second_block.call }
13+
bm.report('second') { block_arg.call }
14+
end
15+
16+
@first_bm, @second_bm = *report.entries
17+
18+
@first_bm.ips >= target_ips(@second_bm)
19+
end
20+
21+
chain :times_slower do |slower|
22+
@slower = slower
23+
end
24+
25+
chain :than do |&blk|
26+
@second_block = blk
27+
end
28+
29+
failure_message do
30+
"expected function to perform #{target_ips(@second_bm)} IPS, but it only performed #{@first_bm.ips} IPS"
31+
end
32+
33+
def target_ips(result)
34+
result.ips / (@slower || 1)
35+
end
36+
37+
def supports_block_expectations?
38+
true
39+
end
40+
41+
diffable
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)