# frozen_string_literal: true RSpec.describe DiscourseLogstashLogger do let(:lograge_logstash_formatter_formatted_message) do "{\"method\":\"GET\",\"path\":\"/\",\"format\":\"html\",\"controller\":\"ListController\",\"action\":\"latest\",\"status\":200,\"allocations\":242307,\"duration\":178.2,\"view\":78.36,\"db\":0.0,\"params\":\"\",\"ip\":\"127.0.0.1\",\"username\":null,\"@timestamp\":\"2024-07-01T07:51:11.283Z\",\"@version\":\"1\",\"message\":\"[200] GET / (ListController#latest)\"}" end let(:output) { StringIO.new } let(:logger) { described_class.logger(logdev: output, type: "test") } describe "#add" do it "logs a JSON string with the right fields" do logger.add(Logger::INFO, lograge_logstash_formatter_formatted_message) output.rewind expect(output.read.chomp).to eq( { "message" => "[200] GET / (ListController#latest)", "severity" => 1, "severity_name" => "INFO", "pid" => described_class::PROCESS_PID, "type" => "test", "host" => described_class::HOST, "git_version" => described_class::GIT_VERSION, "method" => "GET", "path" => "/", "format" => "html", "controller" => "ListController", "action" => "latest", "status" => 200, "allocations" => 242_307, "duration" => 178.2, "view" => 78.36, "db" => 0.0, "params" => "", "ip" => "127.0.0.1", "username" => nil, "@timestamp" => "2024-07-01T07:51:11.283Z", "@version" => "1", }.to_json, ) end it "accepts an error object as the message" do logger = described_class.logger(logdev: output, type: "test") logger.add(Logger::ERROR, StandardError.new("error message")) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed["message"]).to eq("error message") end context "when `progname` is `sidekiq-exception`" do it "logs a JSON string with the `exception.class`, `exception.message`, `job.class`, `job.opts` and `job.problem_db` fields" do logger = described_class.logger(logdev: output, type: "test") logger.add_with_opts( Logger::ERROR, "Job exception: some job error message", "sidekiq-exception", exception_class: "Some::StandardError", exception_message: "some job error message", context: { opts: { user_id: 1, }, problem_db: "some_db", job: "SomeJob", }, ) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed["exception.class"]).to eq("Some::StandardError") expect(parsed["exception.message"]).to eq("some job error message") expect(parsed["job.class"]).to eq("SomeJob") expect(parsed["job.opts"]).to eq("{\"user_id\"=>1}") expect(parsed["job.problem_db"]).to eq("some_db") end end context "when `progname` is `web-exception`" do it "logs a JSON string with the `exception.class` and `exception.message` fields" do logger = described_class.logger(logdev: output, type: "test") logger.add( Logger::ERROR, "Some::StandardError (this is a normal message)\ntest", "web-exception", ) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed["exception.class"]).to eq("Some::StandardError") expect(parsed["exception.message"]).to eq("this is a normal message") end it "logs a JSON string with the `exception_class` and `exception_message` fields when the exception message contains newlines" do logger = described_class.logger(logdev: output, type: "test") logger.add( Logger::ERROR, "Some::StandardError (\n\nsome error message\n\nsomething else\n\n)\ntest", "web-exception", ) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed["exception.class"]).to eq("Some::StandardError") expect(parsed["exception.message"]).to eq("some error message\n\nsomething else") end described_class::ALLOWED_HEADERS_FROM_ENV.each do |header| it "includes `#{header}` from `env` keyword argument in the logged JSON string" do logger.add( Logger::ERROR, lograge_logstash_formatter_formatted_message, "web-exception", env: { header => "header", }, ) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed["request.headers.#{header.downcase}"]).to eq("header") end end it "does not include keys from `env` keyword argument in the logged JSOn string which are not in the allow list" do logger.add( Logger::ERROR, lograge_logstash_formatter_formatted_message, "web-exception", env: { "SOME_RANDOM_HEADER" => "header", }, ) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed).not_to have_key("request.headers.some_random_header") end end it "logs a JSON string with the right fields when `customize_event` attribute is set" do logger = described_class.logger( logdev: output, type: "test", customize_event: ->(event) { event["custom"] = "custom" }, ) logger.add(Logger::INFO, lograge_logstash_formatter_formatted_message) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed["custom"]).to eq("custom") end it "does not log a JSON string with the `backtrace` field when severity is less than `Logger::WARN`" do logger.add( Logger::INFO, lograge_logstash_formatter_formatted_message, nil, backtrace: "backtrace", ) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed).not_to have_key("backtrace") end it "logs a JSON string with the `backtrace` field when severity is at least `Logger::WARN`" do logger.add( Logger::ERROR, lograge_logstash_formatter_formatted_message, nil, backtrace: "backtrace", ) output.rewind parsed = JSON.parse(output.read.chomp) expect(parsed["backtrace"]).to eq("backtrace") end it "does not log the event if message matches a pattern configured by `Logster.store.ignore`" do original_logster_store_ignore = Logster.store.ignore Logster.store.ignore = [/^Some::StandardError/] logger.add( Logger::ERROR, "Some::StandardError (this is a normal message)\ntest", "web-exception", ) output.rewind expect(output.read.chomp).to be_empty ensure Logster.store.ignore = original_logster_store_ignore end end end