steno-capi 1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +7136 -0
  3. data/README.md +78 -0
  4. data/Rakefile +15 -0
  5. data/bin/steno-prettify +99 -0
  6. data/lib/steno.rb +133 -0
  7. data/lib/steno/codec.rb +2 -0
  8. data/lib/steno/codec/base.rb +34 -0
  9. data/lib/steno/codec/json.rb +49 -0
  10. data/lib/steno/config.rb +101 -0
  11. data/lib/steno/context.rb +59 -0
  12. data/lib/steno/core_ext.rb +11 -0
  13. data/lib/steno/errors.rb +3 -0
  14. data/lib/steno/json_prettifier.rb +131 -0
  15. data/lib/steno/log_level.rb +24 -0
  16. data/lib/steno/logger.rb +174 -0
  17. data/lib/steno/record.rb +41 -0
  18. data/lib/steno/sink.rb +6 -0
  19. data/lib/steno/sink/base.rb +38 -0
  20. data/lib/steno/sink/counter.rb +44 -0
  21. data/lib/steno/sink/eventlog.rb +46 -0
  22. data/lib/steno/sink/fluentd.rb +31 -0
  23. data/lib/steno/sink/io.rb +72 -0
  24. data/lib/steno/sink/syslog.rb +62 -0
  25. data/lib/steno/tagged_logger.rb +59 -0
  26. data/lib/steno/version.rb +3 -0
  27. data/spec/spec_helper.rb +6 -0
  28. data/spec/support/barrier.rb +22 -0
  29. data/spec/support/null_sink.rb +17 -0
  30. data/spec/support/shared_context_specs.rb +7 -0
  31. data/spec/unit/config_spec.rb +229 -0
  32. data/spec/unit/context_spec.rb +62 -0
  33. data/spec/unit/core_ext_spec.rb +38 -0
  34. data/spec/unit/json_codec_spec.rb +68 -0
  35. data/spec/unit/json_prettifier_spec.rb +84 -0
  36. data/spec/unit/log_level_spec.rb +19 -0
  37. data/spec/unit/logger_spec.rb +101 -0
  38. data/spec/unit/record_spec.rb +30 -0
  39. data/spec/unit/sink/counter_spec.rb +27 -0
  40. data/spec/unit/sink/eventlog_spec.rb +41 -0
  41. data/spec/unit/sink/fluentd_spec.rb +46 -0
  42. data/spec/unit/sink/io_spec.rb +111 -0
  43. data/spec/unit/sink/syslog_spec.rb +75 -0
  44. data/spec/unit/steno_spec.rb +86 -0
  45. data/spec/unit/tagged_logger_spec.rb +35 -0
  46. data/steno-capi.gemspec +39 -0
  47. metadata +179 -0
@@ -0,0 +1,6 @@
1
+ require "steno/sink/base"
2
+ require "steno/sink/io"
3
+ require "steno/sink/syslog"
4
+ require "steno/sink/fluentd"
5
+ require "steno/sink/counter"
6
+ require "steno/sink/eventlog"
@@ -0,0 +1,38 @@
1
+ require "rbconfig"
2
+ require "thread"
3
+
4
+ module Steno
5
+ module Sink
6
+ WINDOWS = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
7
+ end
8
+ end
9
+
10
+ # Sinks represent the final destination for log records. They abstract storage
11
+ # mediums (like files) and transport layers (like sockets).
12
+ class Steno::Sink::Base
13
+
14
+ attr_accessor :codec
15
+
16
+ # @param [Steno::Codec::Base] formatter Transforms log records to their
17
+ # raw, string-based representation that will be written to the underlying
18
+ # sink.
19
+ def initialize(codec = nil)
20
+ @codec = codec
21
+ end
22
+
23
+ # Adds a record to be flushed at a later time.
24
+ #
25
+ # @param [Hash] record
26
+ #
27
+ # @return [nil]
28
+ def add_record(record)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ # Flushes any buffered records.
33
+ #
34
+ # @return [nil]
35
+ def flush
36
+ raise NotImplementedError
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ require "steno/sink/base"
2
+
3
+ module Steno
4
+ module Sink
5
+ end
6
+ end
7
+
8
+ class Steno::Sink::Counter < Steno::Sink::Base
9
+
10
+ def initialize
11
+ # Map of String -> numeric count
12
+ @counts = {}
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def add_record(record)
17
+ level = record.log_level.to_s
18
+
19
+ @mutex.synchronize do
20
+ unless @counts[level]
21
+ @counts[level] = 0
22
+ end
23
+ @counts[level] += 1
24
+ end
25
+ end
26
+
27
+ def flush
28
+ end
29
+
30
+ def to_json
31
+ hash = {}
32
+ @mutex.synchronize do
33
+ Steno::Logger::LEVELS.keys.each do |level_name|
34
+ hash[level_name] = @counts.fetch(level_name.to_s, 0)
35
+ end
36
+ end
37
+ Yajl::Encoder.encode(hash)
38
+ end
39
+
40
+ # Provide a map of string level -> count. This is thread-safe, the return value is a copy.
41
+ def counts
42
+ @mutex.synchronize { @counts.dup }
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ if Steno::Sink::WINDOWS
2
+ require "steno/sink/base"
3
+
4
+ require "singleton"
5
+ require "thread"
6
+ require 'win32/eventlog'
7
+
8
+ class Steno::Sink::Eventlog < Steno::Sink::Base
9
+ include Singleton
10
+
11
+ LOG_LEVEL_MAP = {
12
+ :fatal => Win32::EventLog::ERROR_TYPE,
13
+ :error => Win32::EventLog::ERROR_TYPE,
14
+ :warn => Win32::EventLog::WARN_TYPE,
15
+ :info => Win32::EventLog::INFO_TYPE,
16
+ :debug => Win32::EventLog::INFO_TYPE,
17
+ :debug1 => Win32::EventLog::INFO_TYPE,
18
+ :debug2 => Win32::EventLog::INFO_TYPE,
19
+ }
20
+
21
+ def initialize
22
+ super
23
+ @eventlog = nil
24
+ end
25
+
26
+ def open()
27
+ @eventlog = Win32::EventLog::open('Application')
28
+ end
29
+
30
+ def add_record(record)
31
+ msg = @codec.encode_record(record)
32
+ pri = LOG_LEVEL_MAP[record.log_level]
33
+
34
+ @eventlog.report_event(
35
+ :source => 'CloudFoundry',
36
+ :event_type => pri,
37
+ :data => msg
38
+ )
39
+ end
40
+
41
+ def flush
42
+ nil
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ require 'fluent-logger'
2
+ #
3
+ # Steno sink implementation for Fluentd
4
+ #
5
+ # See fluentd at http://fluentd.org/
6
+ # and fluent-logger at https://github.com/fluent/fluent-logger-ruby
7
+ #
8
+ class Steno::Sink::Fluentd < Steno::Sink::Base
9
+
10
+ # @param [Hash] opts Key :tag_prefix tag prefix of fluent logs (default: steno)
11
+ # Key :host fluentd host (default: 127.0.0.1)
12
+ # Key :port fluentd port (deafult: 24224)
13
+ # Key :buffer_limit buffer limit of fluent-logger
14
+ def initialize(opts = {})
15
+ super
16
+
17
+ @fluentd = Fluent::Logger::FluentLogger.new(opts[:tag_prefix] || "steno",
18
+ :host => opts[:host] || "127.0.0.1",
19
+ :port => opts[:port] || 24224,
20
+ :buffer_limit => opts[:buffer_limit] || Fluent::Logger::FluentLogger::BUFFER_LIMIT)
21
+ @io_lock = Mutex.new
22
+ end
23
+
24
+ def add_record(record)
25
+ @fluentd.post(record.source, record)
26
+ end
27
+
28
+ def flush
29
+ nil
30
+ end
31
+ end
@@ -0,0 +1,72 @@
1
+ require "steno/sink/base"
2
+
3
+ module Steno
4
+ module Sink
5
+ end
6
+ end
7
+
8
+ class Steno::Sink::IO < Steno::Sink::Base
9
+ class << self
10
+ # Returns a new sink configured to append to the file at path.
11
+ #
12
+ # @param [String] path
13
+ # @param [Hash] If the key :autoflush is set to true, encoded records
14
+ # will not be buffered by Ruby. The key :max_retries
15
+ # is forwarded to Steno::Sink::IO object during creation.
16
+ # @return [Steno::Sink::IO]
17
+ def for_file(path, opts = {})
18
+ autoflush = true
19
+ if opts.include?(:autoflush)
20
+ autoflush = opts[:autoflush]
21
+ end
22
+
23
+ io = File.open(path, "a+")
24
+
25
+ io.sync = autoflush
26
+
27
+ new(io, :max_retries => opts[:max_retries])
28
+ end
29
+ end
30
+
31
+ attr_reader :max_retries
32
+
33
+ # @param [IO] io The IO object that will be written to
34
+ # @param [Hash] opts Key :codec is used to specify a codec inheriting from
35
+ # Steno::Codec::Base.
36
+ # Key :max_retries takes an integer value which specifies
37
+ # the number of times the write operation can be retried
38
+ # when IOError is raised while writing a record.
39
+ def initialize(io, opts = {})
40
+ super(opts[:codec])
41
+
42
+ @max_retries = opts[:max_retries] || -1
43
+ @io_lock = Mutex.new
44
+ @io = io
45
+ end
46
+
47
+ def add_record(record)
48
+ bytes = @codec.encode_record(record)
49
+
50
+ @io_lock.synchronize do
51
+ retries = 0
52
+ begin
53
+ @io.write(bytes)
54
+ rescue IOError => e
55
+ if retries < @max_retries
56
+ retries += 1
57
+ retry
58
+ else
59
+ raise e
60
+ end
61
+ end
62
+ end
63
+
64
+ nil
65
+ end
66
+
67
+ def flush
68
+ @io_lock.synchronize { @io.flush }
69
+
70
+ nil
71
+ end
72
+ end
@@ -0,0 +1,62 @@
1
+ unless Steno::Sink::WINDOWS
2
+ require "steno/sink/base"
3
+
4
+ require "singleton"
5
+ require "thread"
6
+ require "syslog/logger"
7
+
8
+ class Steno::Sink::Syslog < Steno::Sink::Base
9
+ include Singleton
10
+
11
+ MAX_MESSAGE_SIZE = 1024 * 3
12
+ TRUNCATE_POSTFIX = "..."
13
+
14
+ LOG_LEVEL_MAP = {
15
+ :fatal => Syslog::LOG_CRIT,
16
+ :error => Syslog::LOG_ERR,
17
+ :warn => Syslog::LOG_WARNING,
18
+ :info => Syslog::LOG_INFO,
19
+ :debug => Syslog::LOG_DEBUG,
20
+ :debug1 => Syslog::LOG_DEBUG,
21
+ :debug2 => Syslog::LOG_DEBUG,
22
+ }
23
+
24
+ def initialize
25
+ super
26
+
27
+ @syslog = nil
28
+ @syslog_lock = Mutex.new
29
+ end
30
+
31
+ def open(identity)
32
+ @identity = identity
33
+
34
+ Syslog::Logger.new(@identity)
35
+ @syslog = Syslog::Logger.syslog
36
+ end
37
+
38
+ def add_record(record)
39
+ record = truncate_record(record)
40
+ msg = @codec.encode_record(record)
41
+ pri = LOG_LEVEL_MAP[record.log_level]
42
+ @syslog_lock.synchronize { @syslog.log(pri, "%s", msg) }
43
+ end
44
+
45
+ def flush
46
+ nil
47
+ end
48
+
49
+ private
50
+
51
+ def truncate_record(record)
52
+ return record if record.message.size <= MAX_MESSAGE_SIZE
53
+
54
+ truncated = record.message.slice(0...(MAX_MESSAGE_SIZE - TRUNCATE_POSTFIX.size))
55
+ truncated << TRUNCATE_POSTFIX
56
+ Steno::Record.new(record.source, record.log_level,
57
+ truncated,
58
+ [record.file, record.lineno, record.method],
59
+ record.data)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,59 @@
1
+ require "steno/logger"
2
+
3
+ module Steno
4
+ end
5
+
6
+ # Provides a proxy that allows persistent user data
7
+ class Steno::TaggedLogger
8
+
9
+ attr_reader :proxied_logger
10
+ attr_accessor :user_data
11
+
12
+ class << self
13
+ # The following helpers are used to create a new scope for binding the log
14
+ # level.
15
+
16
+ def define_log_method(name)
17
+ define_method(name) { |*args, &blk| log(name, *args, &blk) }
18
+ end
19
+
20
+ def define_logf_method(name)
21
+ define_method(name.to_s + "f") { |fmt, *args| log(name, fmt % args) }
22
+ end
23
+ end
24
+
25
+ Steno::Logger::LEVELS.each do |name, _|
26
+ # Define #debug, for example
27
+ define_log_method(name)
28
+
29
+ # Define #debugf, for example
30
+ define_logf_method(name)
31
+ end
32
+
33
+ def initialize(proxied_logger, user_data = {})
34
+ @proxied_logger = proxied_logger
35
+ @user_data = user_data
36
+ end
37
+
38
+ def method_missing(method, *args, &blk)
39
+ @proxied_logger.send(method, *args, &blk)
40
+ end
41
+
42
+ # @see Steno::Logger#log
43
+ def log(level_name, message = nil, user_data = nil, &blk)
44
+ ud = @user_data.merge(user_data || {})
45
+
46
+ @proxied_logger.log(level_name, message, ud, &blk)
47
+ end
48
+
49
+ # @see Steno::Logger#log_exception
50
+ def log_exception(ex, user_data = {})
51
+ ud = @user_data.merge(user_data || {})
52
+
53
+ @proxied_logger.log_exception(ex, ud)
54
+ end
55
+
56
+ def tag(new_user_data = {})
57
+ Steno::TaggedLogger.new(proxied_logger, user_data.merge(new_user_data))
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module Steno
2
+ VERSION = "1.3.4"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "rack/test"
2
+ require "rspec"
3
+
4
+ require "steno"
5
+
6
+ Dir["./spec/support/**/*.rb"].each { |file| require file }
@@ -0,0 +1,22 @@
1
+ require "thread"
2
+
3
+ class Barrier
4
+ def initialize
5
+ @lock = Mutex.new
6
+ @cvar = ConditionVariable.new
7
+ @done = false
8
+ end
9
+
10
+ def release
11
+ @lock.synchronize do
12
+ @done = true
13
+ @cvar.broadcast
14
+ end
15
+ end
16
+
17
+ def wait
18
+ @lock.synchronize do
19
+ @cvar.wait(@lock) if !@done
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ class NullSink
2
+ attr_accessor :records
3
+
4
+ def initialize
5
+ @records = []
6
+ end
7
+
8
+ def add_record(record)
9
+ @records << record
10
+
11
+ nil
12
+ end
13
+
14
+ def flush
15
+ nil
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ shared_context :steno_context do
2
+ it "should support clearing context local data" do
3
+ context.data["test"] = "value"
4
+ context.clear
5
+ expect(context.data["test"]).to be_nil
6
+ end
7
+ end
@@ -0,0 +1,229 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+
4
+ require "spec_helper"
5
+
6
+ describe Steno::Config do
7
+
8
+ if Steno::Sink::WINDOWS
9
+ describe ".from_hash" do
10
+ before :each do
11
+ @log_path = "some_file"
12
+
13
+ @mock_sink_file = double("sink")
14
+ expect(@mock_sink_file).to receive(:codec=)
15
+ expect(Steno::Sink::IO).to receive(:for_file).with(@log_path,
16
+ :max_retries => 5)
17
+ .and_return(@mock_sink_file)
18
+
19
+ @mock_sink_eventlog = double("sink")
20
+ expect(@mock_sink_eventlog).to receive(:codec=)
21
+ expect(@mock_sink_eventlog).to receive(:open).with("test")
22
+ expect(Steno::Sink::Eventlog).to receive(:instance).twice()
23
+ .and_return(@mock_sink_eventlog)
24
+ end
25
+
26
+ after :each do
27
+ @config = Steno::Config.from_hash(@hash)
28
+
29
+ expect(@config.default_log_level).to eq(:debug2)
30
+ expect(@config.context.class).to eq(Steno::Context::Null)
31
+ expect(@config.codec.class).to eq(Steno::Codec::Json)
32
+
33
+ expect(@config.sinks.size).to eq(2)
34
+ expect(@config.sinks).to match_array([@mock_sink_file, @mock_sink_eventlog])
35
+ end
36
+
37
+ it "should work for symbolized keys" do
38
+ @hash = {
39
+ :file => @log_path,
40
+ :level => "debug2",
41
+ :default_log_level => "warn",
42
+ :eventlog => "test",
43
+ :max_retries => 5,
44
+ }
45
+ end
46
+
47
+ it "should work for non-symbolized keys" do
48
+ @hash = {
49
+ "file" => @log_path,
50
+ "level" => "debug2",
51
+ "default_log_level" => "warn",
52
+ "eventlog" => "test",
53
+ "max_retries" => 5,
54
+ }
55
+ end
56
+
57
+ end
58
+ else
59
+ describe ".from_hash" do
60
+ before :each do
61
+ @log_path = "some_file"
62
+
63
+ @mock_sink_file = double("sink")
64
+ allow(@mock_sink_file).to receive(:codec=)
65
+ expect(Steno::Sink::IO).to receive(:for_file).with(@log_path,
66
+ :max_retries => 5)
67
+ .and_return(@mock_sink_file)
68
+
69
+ @mock_sink_syslog = double("sink")
70
+ expect(@mock_sink_syslog).to receive(:codec=)
71
+ expect(@mock_sink_syslog).to receive(:open).with("test")
72
+ expect(Steno::Sink::Syslog).to receive(:instance).twice()
73
+ .and_return(@mock_sink_syslog)
74
+ end
75
+
76
+ after :each do
77
+ @config = Steno::Config.from_hash(@hash)
78
+
79
+ expect(@config.default_log_level).to eq(:debug2)
80
+ expect(@config.context.class).to eq(Steno::Context::Null)
81
+ expect(@config.codec.class).to eq(Steno::Codec::Json)
82
+
83
+ expect(@config.sinks.size).to eq(2)
84
+ expect(@config.sinks).to match_array([@mock_sink_file, @mock_sink_syslog])
85
+ end
86
+
87
+ it "should work for symbolized keys" do
88
+ @hash = {
89
+ :file => @log_path,
90
+ :level => "debug2",
91
+ :default_log_level => "warn",
92
+ :syslog => "test",
93
+ :max_retries => 5,
94
+ }
95
+ end
96
+
97
+ it "should work for non-symbolized keys" do
98
+ @hash = {
99
+ "file" => @log_path,
100
+ "level" => "debug2",
101
+ "default_log_level" => "warn",
102
+ "syslog" => "test",
103
+ "max_retries" => 5,
104
+ }
105
+ end
106
+
107
+ end
108
+ end
109
+
110
+ describe ".from_file" do
111
+ before :each do
112
+ @tmpdir = Dir.mktmpdir
113
+ @config_path = File.join(@tmpdir, "config.yml")
114
+ @log_path = File.join(@tmpdir, "test.log")
115
+ end
116
+
117
+ after :each do
118
+ FileUtils.rm_rf(@tmpdir)
119
+ end
120
+
121
+ it "should return Steno::Config instance with sane defaults" do
122
+ write_config(@config_path, {})
123
+
124
+ config = Steno::Config.from_file(@config_path)
125
+
126
+ expect(config.sinks.size).to eq(1)
127
+ expect(config.sinks[0].class).to eq(Steno::Sink::IO)
128
+
129
+ expect(config.default_log_level).to eq(:info)
130
+
131
+ expect(config.context.class).to eq(Steno::Context::Null)
132
+
133
+ expect(config.codec.class).to eq(Steno::Codec::Json)
134
+ expect(config.codec.iso8601_timestamps?).to eq(false)
135
+ end
136
+
137
+ it "should configure json codec with readable dates if iso8601_timestamps is true" do
138
+ write_config(@config_path, {"iso8601_timestamps" => "true"})
139
+ config = Steno::Config.from_file(@config_path)
140
+ expect(config.codec.class).to eq(Steno::Codec::Json)
141
+ expect(config.codec.iso8601_timestamps?).to eq(true)
142
+ end
143
+
144
+ it "should set the default_log_level if a key with the same name is supplied" do
145
+ write_config(@config_path, {"level" => "debug2"})
146
+ expect(Steno::Config.from_file(@config_path).default_log_level).to eq(:debug2)
147
+
148
+ write_config(@config_path, {"default_log_level" => "debug2"})
149
+ expect(Steno::Config.from_file(@config_path).default_log_level).to eq(:debug2)
150
+ end
151
+
152
+ it "should read the 'level' key if both default_log_level and level are spscified" do
153
+ write_config(@config_path, {"level" => "debug2",
154
+ "default_log_level" => "warn"})
155
+ expect(Steno::Config.from_file(@config_path).default_log_level).to eq(:debug2)
156
+ end
157
+
158
+ it "should add a file sink if the 'file' key is specified" do
159
+ write_config(@config_path, {"file" => @log_path, "max_retries" => 2})
160
+ mock_sink = double("sink")
161
+ expect(mock_sink).to receive(:codec=)
162
+
163
+ expect(Steno::Sink::IO).to receive(:for_file).
164
+ with(@log_path, :max_retries => 2).and_return(mock_sink)
165
+ config = Steno::Config.from_file(@config_path)
166
+ expect(config.sinks.size).to eq(1)
167
+ expect(config.sinks[0]).to eq(mock_sink)
168
+ end
169
+
170
+ if Steno::Sink::WINDOWS
171
+ it "should add a event sink if the 'eventlog' key is specified" do
172
+ write_config(@config_path, {"eventlog" => "test"})
173
+ mock_sink = double("sink")
174
+ expect(mock_sink).to receive(:open).with("test")
175
+ expect(mock_sink).to receive(:codec=)
176
+
177
+ expect(Steno::Sink::Eventlog).to receive(:instance).twice().and_return(mock_sink)
178
+
179
+ config = Steno::Config.from_file(@config_path)
180
+ expect(config.sinks.size).to eq(1)
181
+ expect(config.sinks[0]).to eq(mock_sink)
182
+ end
183
+ else
184
+ it "should add a syslog sink if the 'syslog' key is specified" do
185
+ write_config(@config_path, {"syslog" => "test"})
186
+ mock_sink = double("sink")
187
+ expect(mock_sink).to receive(:open).with("test")
188
+ expect(mock_sink).to receive(:codec=)
189
+
190
+ expect(Steno::Sink::Syslog).to receive(:instance).twice().and_return(mock_sink)
191
+
192
+ config = Steno::Config.from_file(@config_path)
193
+ expect(config.sinks.size).to eq(1)
194
+ expect(config.sinks[0]).to eq(mock_sink)
195
+ end
196
+ end
197
+
198
+
199
+
200
+ it "should add an io sink to stdout if no sinks are explicitly specified in the config file" do
201
+ write_config(@config_path, {})
202
+ mock_sink = double("sink")
203
+ expect(mock_sink).to receive(:codec=)
204
+
205
+ expect(Steno::Sink::IO).to receive(:new).with(STDOUT).and_return(mock_sink)
206
+
207
+ config = Steno::Config.from_file(@config_path)
208
+ expect(config.sinks.size).to eq(1)
209
+ expect(config.sinks[0]).to eq(mock_sink)
210
+ end
211
+
212
+ it "should merge supplied overrides with the file based config" do
213
+ write_config(@config_path, {"default_log_level" => "debug"})
214
+
215
+ context = Steno::Context::ThreadLocal.new
216
+ config = Steno::Config.from_file(@config_path,
217
+ :default_log_level => "warn",
218
+ :context => context)
219
+ expect(config.context).to eq(context)
220
+ expect(config.default_log_level).to eq(:warn)
221
+ end
222
+ end
223
+
224
+ def write_config(path, config)
225
+ File.open(path, "w+") do |f|
226
+ f.write(YAML.dump({"logging" => config}))
227
+ end
228
+ end
229
+ end