Skip to content

Allow Logstash to write its logs in JSON format #4820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/bootstrap/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

require "rspec/core"
require "rspec"
require 'ci/reporter/rake/rspec_loader'

status = RSpec::Core::Runner.run(ARGV.empty? ? ["spec"] : ARGV).to_i
exit status if status != 0
2 changes: 1 addition & 1 deletion logstash-core/lib/logstash/api/lib/app/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def started?
end

def update(snapshot)
logger.debug("[api-service] snapshot received", :snapshot => snapshot) if logger.debug?
logger.debug("[api-service] snapshot received", :snapshot_time => snapshot.created_at) if logger.debug?
if @snapshot_rotation_mutex.try_lock
@snapshot = snapshot
@snapshot_rotation_mutex.unlock
Expand Down
2 changes: 1 addition & 1 deletion logstash-core/lib/logstash/inputs/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def stop

public
def do_stop
@logger.debug("stopping", :plugin => self)
@logger.debug("stopping", :plugin => self.class.name)
@stop_called.make_true
stop
end
Expand Down
2 changes: 1 addition & 1 deletion logstash-core/lib/logstash/instrument/collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def update(time_of_execution, result, exception)
logger.error("Collector: Something went wrong went sending data to the observers",
:execution_time => time_of_execution,
:result => result,
:exception => exception)
:exception => exception.class.name)
end

# Snapshot the current Metric Store and return it immediately,
Expand Down
21 changes: 21 additions & 0 deletions logstash-core/lib/logstash/logging/json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# encoding: utf-8
require "logstash/namespace"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to add the # encoding: utf-8 header to appease the utf gods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

require "logstash/logging"
require "logstash/json"

module LogStash; class Logging; class JSON
def initialize(io)
raise ArgumentError, "Expected IO, got #{io.class.name}" unless io.is_a?(IO)

@io = io
@lock = Mutex.new
end

def <<(obj)
serialized = LogStash::Json.dump(obj)
@lock.synchronize do
@io.puts(serialized)
@io.flush
end
end
end; end; end
4 changes: 2 additions & 2 deletions logstash-core/lib/logstash/output_delegator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def register
@workers << @klass.new(@config)
@workers.first.register # Needed in case register calls `workers_not_supported`

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

# Threadsafe versions don't need additional workers
setup_additional_workers!(target_worker_count) unless @threadsafe
Expand Down Expand Up @@ -147,7 +147,7 @@ def worker_multi_receive(events)
end

def do_close
@logger.debug("closing output delegator", :klass => @klass)
@logger.debug("closing output delegator", :klass => @klass.name)

if @threadsafe
@workers.each(&:do_close)
Expand Down
2 changes: 1 addition & 1 deletion logstash-core/lib/logstash/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def shutdown(&before_stop)
def shutdown_workers
# Each worker thread will receive this exactly once!
@worker_threads.each do |t|
@logger.debug("Pushing shutdown", :thread => t)
@logger.debug("Pushing shutdown", :thread => t.inspect)
@input_queue.push(LogStash::SHUTDOWN)
end

Expand Down
2 changes: 1 addition & 1 deletion logstash-core/lib/logstash/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def plugin_unique_name
# close is called during shutdown, after the plugin worker
# main task terminates
def do_close
@logger.debug("closing", :plugin => self)
@logger.debug("closing", :plugin => self.class.name)
close
end

Expand Down
21 changes: 17 additions & 4 deletions logstash-core/lib/logstash/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ class LogStash::Runner < Clamp::Command
I18n.t("logstash.runner.flag.allow-env"),
:attribute_name => :allow_env, :default => false

option ["--[no-]log-in-json"], :flag,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts about renaming this flag to --logger.format to make it future proof for setting.yml? This way it translates easily to yml.. See #4499 /cc @jsvd

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be consistent, this pr should add the flag according to the current state of master. right now it should use "-", but if it happens to be merged after logstash.yml, then the pr needs to be changed

I18n.t("logstash.runner.flag.log-in-json"),
:default => false

def pipeline_workers=(pipeline_workers_value)
@pipeline_settings[:pipeline_workers] = validate_positive_integer(pipeline_workers_value)
end
Expand Down Expand Up @@ -136,7 +140,7 @@ def execute
require "logstash/util/java_version"
require "stud/task"
require "cabin" # gem 'cabin'

require "logstash/logging/json"

# Configure Logstash logging facility, this need to be done before everything else to
# make sure the logger has the correct settings and the log level is correctly defined.
Expand Down Expand Up @@ -326,11 +330,20 @@ def configure_logging(path)
:path => path, :error => e))
end

@logger.subscribe(STDOUT, :level => :fatal)
@logger.subscribe(@log_fd)
if log_in_json?
@logger.subscribe(LogStash::Logging::JSON.new(STDOUT), :level => :fatal)
@logger.subscribe(LogStash::Logging::JSON.new(@log_fd))
else
@logger.subscribe(STDOUT, :level => :fatal)
@logger.subscribe(@log_fd)
end
@logger.terminal "Sending logstash logs to #{path}."
else
@logger.subscribe(STDOUT)
if log_in_json?
@logger.subscribe(LogStash::Logging::JSON.new(STDOUT))
else
@logger.subscribe(STDOUT)
end
end

if debug_config? && @logger.level != :debug
Expand Down
6 changes: 5 additions & 1 deletion logstash-core/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,8 @@ en:
debug_config: |+
Print the compiled config ruby code out as a debug log (you must also have --debug enabled).
WARNING: This will include any 'password' options passed to plugin configs as plaintext, and may result
in plaintext passwords appearing in your logs!
in plaintext passwords appearing in your logs!
log-in-json: |+
Specify that Logstash should write its own logs in JSON form - one
event per line. If false, Logstash will log using Ruby's
Object#inspect (not easy to machine-parse)
2 changes: 2 additions & 0 deletions logstash-core/spec/logstash/output_delegator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
let(:out_klass) { double("output klass") }
let(:out_inst) { double("output instance") }


before(:each) do
allow(out_klass).to receive(:new).with(any_args).and_return(out_inst)
allow(out_klass).to receive(:threadsafe?).and_return(false)
allow(out_klass).to receive(:workers_not_supported?).and_return(false)
allow(out_klass).to receive(:name).and_return("example")
allow(out_inst).to receive(:register)
allow(out_inst).to receive(:multi_receive)
allow(out_inst).to receive(:metric=).with(any_args)
Expand Down
4 changes: 2 additions & 2 deletions logstash-core/spec/logstash/plugin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

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

it "should fail lookup on inexisting name" do
expect_any_instance_of(Cabin::Channel).to receive(:debug).once
#expect_any_instance_of(Cabin::Channel).to receive(:debug).once
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for removing them.
I think checking for the raise_error is actually the behavior we want to verify.

expect { LogStash::Plugin.lookup("filter", "badname") }.to raise_error(LogStash::PluginLoadingError)
end

Expand Down
35 changes: 32 additions & 3 deletions logstash-core/spec/logstash/runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
require "logstash/runner"
require "stud/task"
require "stud/trap"
require "stud/temporary"
require "logstash/util/java_version"
require "logstash/logging/json"
require "json"

class NullRunner
def run(args); end
Expand All @@ -16,7 +19,7 @@ def run(args); end

before :each do
allow(Cabin::Channel).to receive(:get).with(LogStash).and_return(channel)
allow(channel).to receive(:subscribe).with(any_args)
allow(channel).to receive(:subscribe).with(any_args).and_call_original
end

describe "argument parsing" do
Expand Down Expand Up @@ -95,6 +98,32 @@ def run(args); end
end
end

context "--log-in-json" do
subject { LogStash::Runner.new("") }
let(:logfile) { Stud::Temporary.file }
let(:args) { [ "--log-in-json", "-l", logfile.path, "-e", "input {} output{}" ] }

after do
logfile.close
File.unlink(logfile.path)
end

before do
expect(channel).to receive(:subscribe).with(kind_of(LogStash::Logging::JSON)).and_call_original
subject.run(args)

# Log file should have stuff in it.
expect(logfile.stat.size).to be > 0
end

it "should log in valid json. One object per line." do
logfile.each_line do |line|
expect(line).not_to be_empty
expect { JSON.parse(line) }.not_to raise_error
end
end
end

Copy link
Contributor

@ph ph Apr 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to add test for the --debug log level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding --debug feels more like an integration test, not a unit test. My feeling is that this test intends to make sure that the --log-in-json flag will cause logs to come out in JSON.

Can you help me understand what testing --debug would help with?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jordansissel Correct, you are right it feel more like an integration test.

but this error was found when I ran logstash with --debug and --log-in-json, we have a serialization issue on some type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated (see spec_helper) the tests to always force debug level + json logging (at least, as far as I can tell) so that we can catch some problems if they come up.

describe "--config-test" do
subject { LogStash::Runner.new("") }
let(:args) { ["-t", "-e", pipeline_string] }
Expand Down Expand Up @@ -150,14 +179,14 @@ def run(args); end
it "should set 'debug_config' to false by default" do
expect(LogStash::Config::Loader).to receive(:new).with(anything, false).and_call_original
expect(LogStash::Pipeline).to receive(:new).with(pipeline_string, hash_including(:debug_config => false)).and_return(pipeline)
args = ["--debug", "-e", pipeline_string]
args = ["--debug", "-e", pipeline_string, "-l", "/dev/null", "--log-in-json"]
subject.run("bin/logstash", args)
end

it "should allow overriding debug_config" do
expect(LogStash::Config::Loader).to receive(:new).with(anything, true).and_call_original
expect(LogStash::Pipeline).to receive(:new).with(pipeline_string, hash_including(:debug_config => true)).and_return(pipeline)
args = ["--debug", "--debug-config", "-e", pipeline_string]
args = ["--debug", "--debug-config", "-e", pipeline_string, "-l", "/dev/null", "--log-in-json"]
subject.run("bin/logstash", args)
end
end
Expand Down
28 changes: 28 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@

require "logstash/devutils/rspec/spec_helper"

class JSONIOThingy < IO
def initialize; end
def flush; end

def puts(payload)
# Ensure that all log payloads are valid json.
LogStash::Json.load(payload)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh neat idea.


RSpec.configure do |c|
c.before do
# Force Cabin to always have a JSON subscriber. The main purpose of this
# is to catch crashes in json serialization for our logs. JSONIOThingy
# exists to validate taht what LogStash::Logging::JSON emits is always
# valid JSON.
jsonvalidator = JSONIOThingy.new
allow(Cabin::Channel).to receive(:new).and_wrap_original do |m, *args|
logger = m.call(*args)
logger.level = :debug
logger.subscribe(LogStash::Logging::JSON.new(jsonvalidator))

logger
end
end

end

def installed_plugins
Gem::Specification.find_all.select { |spec| spec.metadata["logstash_plugin"] }.map { |plugin| plugin.name }
end