steno-capi 1.3.4

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