Skip to content

Commit e24da9d

Browse files
committed
Allow Logstash to write its logs in JSON format
This is made available by a --log-in-json flag. Default is false. When false, the old behavior [1] is used. When true, JSON logs are emitted. [1] The old behavior is realy two things. First, using Object#inspect to serialize. Second, to color the output if the IO is a tty. For #1569 This is a manual backport of PR #4820 into the 2.x branch.
1 parent 9b712e5 commit e24da9d

File tree

13 files changed

+112
-10
lines changed

13 files changed

+112
-10
lines changed

lib/bootstrap/rspec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
require "rspec/core"
99
require "rspec"
10+
require 'ci/reporter/rake/rspec_loader'
1011

1112
status = RSpec::Core::Runner.run(ARGV.empty? ? ["spec"] : ARGV).to_i
1213
exit status if status != 0

logstash-core/lib/logstash/agent.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ class LogStash::Agent < Clamp::Command
9191
I18n.t("logstash.agent.flag.allow-env"),
9292
:attribute_name => :allow_env, :default => false
9393

94+
option ["--[no-]log-in-json"], :flag,
95+
I18n.t("logstash.agent.flag.log-in-json"),
96+
:default => false
97+
9498
def initialize(*params)
9599
super(*params)
96100
@logger = Cabin::Channel.get(LogStash)
@@ -142,6 +146,7 @@ def execute
142146
require "logstash/pipeline"
143147
require "cabin" # gem 'cabin'
144148
require "logstash/plugin"
149+
require "logstash/logging/json"
145150

146151
LogStash::ShutdownWatcher.unsafe_shutdown = unsafe_shutdown?
147152
LogStash::ShutdownWatcher.logger = @logger
@@ -294,11 +299,22 @@ def configure_logging(path)
294299

295300
puts "Sending logstash logs to #{path}."
296301
@logger.unsubscribe(@logger_subscription) if @logger_subscription
297-
@logger_subscription = @logger.subscribe(@log_fd)
302+
if log_in_json?
303+
@logger_subscription = @logger.subscribe(LogStash::Logging::JSON.new(@log_fd))
304+
@logger.subscribe(LogStash::Logging::JSON.new(STDOUT), :level => :fatal)
305+
else
306+
@logger_subscription = @logger.subscribe(@log_fd)
307+
@logger.subscribe(STDOUT, :level => :fatal)
308+
end
298309
else
299-
@logger.subscribe(STDOUT)
310+
if log_in_json?
311+
@logger.subscribe(LogStash::Logging::JSON.new(STDOUT))
312+
else
313+
@logger.subscribe(STDOUT)
314+
end
300315
end
301316

317+
302318
# TODO(sissel): redirect stdout/stderr to the log as well
303319
# http://jira.codehaus.org/browse/JRUBY-7003
304320
end # def configure_logging

logstash-core/lib/logstash/inputs/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def stop
7878

7979
public
8080
def do_stop
81-
@logger.debug("stopping", :plugin => self)
81+
@logger.debug("stopping", :plugin => self.class.name)
8282
@stop_called.make_true
8383
stop
8484
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# encoding: utf-8
2+
require "logstash/namespace"
3+
require "logstash/logging"
4+
require "logstash/json"
5+
6+
module LogStash; class Logging; class JSON
7+
def initialize(io)
8+
raise ArgumentError, "Expected IO, got #{io.class.name}" unless io.is_a?(IO)
9+
10+
@io = io
11+
@lock = Mutex.new
12+
end
13+
14+
def <<(obj)
15+
serialized = LogStash::Json.dump(obj)
16+
@lock.synchronize do
17+
@io.puts(serialized)
18+
@io.flush
19+
end
20+
end
21+
end; end; end

logstash-core/lib/logstash/output_delegator.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def register
7474
@workers << @klass.new(@config)
7575
@workers.first.register # Needed in case register calls `workers_not_supported`
7676

77-
@logger.debug("Will start workers for output", :worker_count => target_worker_count, :class => @klass)
77+
@logger.debug("Will start workers for output", :worker_count => target_worker_count, :class => @klass.name)
7878

7979
# Threadsafe versions don't need additional workers
8080
setup_additional_workers!(target_worker_count) unless @threadsafe
@@ -134,7 +134,7 @@ def worker_multi_receive(events)
134134
end
135135

136136
def do_close
137-
@logger.debug("closing output delegator", :klass => @klass)
137+
@logger.debug("closing output delegator", :klass => @klass.name)
138138

139139
if @threadsafe
140140
@workers.each(&:do_close)
@@ -169,4 +169,4 @@ def worker_count
169169
private
170170
# Needed for testing, so private
171171
attr_reader :threadsafe_worker, :worker_queue
172-
end end
172+
end end

logstash-core/lib/logstash/pipeline.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def shutdown(&before_stop)
392392
def shutdown_workers
393393
# Each worker thread will receive this exactly once!
394394
@worker_threads.each do |t|
395-
@logger.debug("Pushing shutdown", :thread => t)
395+
@logger.debug("Pushing shutdown", :thread => t.inspect)
396396
@input_queue.push(LogStash::SHUTDOWN)
397397
end
398398

logstash-core/lib/logstash/plugin.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def initialize(params=nil)
3232
# main task terminates
3333
public
3434
def do_close
35-
@logger.debug("closing", :plugin => self)
35+
@logger.debug("closing", :plugin => self.class.name)
3636
close
3737
end
3838

logstash-core/locales/en.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,7 @@ en:
226226
Print the compiled config ruby code out as a debug log (you must also have --debug enabled).
227227
WARNING: This will include any 'password' options passed to plugin configs as plaintext, and may result
228228
in plaintext passwords appearing in your logs!
229+
log-in-json: |+
230+
Specify that Logstash should write its own logs in JSON form - one
231+
event per line. If false, Logstash will log using Ruby's
232+
Object#inspect (not easy to machine-parse)

logstash-core/spec/logstash/output_delegator_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
allow(out_klass).to receive(:new).with(any_args).and_return(out_inst)
1717
allow(out_klass).to receive(:threadsafe?).and_return(false)
1818
allow(out_klass).to receive(:workers_not_supported?).and_return(false)
19+
allow(out_klass).to receive(:name).and_return("example")
1920
allow(out_inst).to receive(:register)
2021
allow(out_inst).to receive(:multi_receive)
2122
allow(logger).to receive(:debug).with(any_args)

logstash-core/spec/logstash/plugin_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
describe LogStash::Plugin do
66
it "should fail lookup on inexisting type" do
7-
expect_any_instance_of(Cabin::Channel).to receive(:debug).once
7+
#expect_any_instance_of(Cabin::Channel).to receive(:debug).once
88
expect { LogStash::Plugin.lookup("badbadtype", "badname") }.to raise_error(LogStash::PluginLoadingError)
99
end
1010

1111
it "should fail lookup on inexisting name" do
12-
expect_any_instance_of(Cabin::Channel).to receive(:debug).once
12+
#expect_any_instance_of(Cabin::Channel).to receive(:debug).once
1313
expect { LogStash::Plugin.lookup("filter", "badname") }.to raise_error(LogStash::PluginLoadingError)
1414
end
1515

logstash-core/spec/logstash/runner_spec.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "logstash/runner"
44
require "stud/task"
55
require "stud/trap"
6+
require "stud/temporary"
67

78
class NullRunner
89
def run(args); end
@@ -14,6 +15,7 @@ def run(args); end
1415

1516
before :each do
1617
allow(Cabin::Channel).to receive(:get).with(LogStash).and_return(channel)
18+
allow(channel).to receive(:subscribe).with(any_args).and_call_original
1719
end
1820

1921
context "argument parsing" do
@@ -54,4 +56,29 @@ def run(args); end
5456
end
5557
end
5658
end
59+
60+
context "--log-in-json" do
61+
let(:logfile) { Stud::Temporary.file }
62+
let(:args) { [ "agent", "--log-in-json", "-l", logfile.path, "-e", "some-invalid-config" ] }
63+
64+
after do
65+
logfile.close
66+
File.unlink(logfile.path)
67+
end
68+
69+
before do
70+
expect(channel).to receive(:subscribe).with(kind_of(LogStash::Logging::JSON)).and_call_original
71+
subject.run(args).wait
72+
73+
# Log file should have stuff in it.
74+
expect(logfile.stat.size).to be > 0
75+
end
76+
77+
it "should log in valid json. One object per line." do
78+
logfile.each_line do |line|
79+
expect(line).not_to be_empty
80+
expect { LogStash::Json.load(line) }.not_to raise_error
81+
end
82+
end
83+
end
5784
end

logstash-core/spec/logstash/shutdown_watcher_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
allow(pipeline).to receive(:thread).and_return(Thread.current)
2121
allow(reporter).to receive(:snapshot).and_return(reporter_snapshot)
2222
allow(reporter_snapshot).to receive(:o_simple_hash).and_return({})
23+
allow(reporter_snapshot).to receive(:to_json_data).and_return("reporter-double")
2324

2425
allow(subject).to receive(:pipeline_report_snapshot).and_wrap_original do |m, *args|
2526
report_count += 1

spec/spec_helper.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,38 @@
55
CoverageHelper.eager_load if ENV['COVERAGE']
66

77
require "logstash/devutils/rspec/spec_helper"
8+
require "logstash/logging/json"
9+
10+
class JSONIOThingy < IO
11+
def initialize; end
12+
def flush; end
13+
14+
def puts(payload)
15+
# Ensure that all log payloads are valid json.
16+
LogStash::Json.load(payload)
17+
end
18+
end
19+
20+
RSpec.configure do |c|
21+
c.before do
22+
# Force Cabin to always have a JSON subscriber. The main purpose of this
23+
# is to catch crashes in json serialization for our logs. JSONIOThingy
24+
# exists to validate taht what LogStash::Logging::JSON emits is always
25+
# valid JSON.
26+
jsonvalidator = JSONIOThingy.new
27+
allow(Cabin::Channel).to receive(:new).and_wrap_original do |m, *args|
28+
logger = m.call(*args)
29+
logger.level = :debug
30+
logger.subscribe(LogStash::Logging::JSON.new(jsonvalidator))
31+
32+
logger
33+
end
34+
end
35+
36+
end
837

938
def installed_plugins
1039
Gem::Specification.find_all.select { |spec| spec.metadata["logstash_plugin"] }.map { |plugin| plugin.name }
1140
end
41+
42+

0 commit comments

Comments
 (0)