Commit 17e642c7 authored by Bob Van Landuyt's avatar Bob Van Landuyt 💬
Browse files

Merge branch 'mk-logging-api' into 'master'

Extract GitLab JsonLogger into labkit

See merge request !96
parents 0c32f4d0 d53e26e0
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -6,5 +6,6 @@ module Labkit
  module Logging
    autoload :GRPC, "labkit/logging/grpc"
    autoload :Sanitizer, "labkit/logging/sanitizer"
    autoload :JsonLogger, "labkit/logging/json_logger"
  end
end
+44 −0
Original line number Diff line number Diff line
# frozen_string_literal: true
require "time"
require "logger"
require "json"

module Labkit
  module Logging
    class JsonLogger < ::Logger
      def self.log_level(fallback: ::Logger::DEBUG)
        ENV.fetch("GITLAB_LOG_LEVEL", fallback)
      end

      def initialize(path, level: JsonLogger.log_level)
        super
      end

      def format_message(severity, timestamp, progname, message)
        data = default_attributes
        data[:severity] = severity
        data[:time] = timestamp.utc.iso8601(3)
        data[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id

        case message
        when String
          data[:message] = message
        when Hash
          data.merge!(message)
        end

        dump_json(data) << "\n"
      end

      private

      def default_attributes
        {}
      end

      def dump_json(data)
        JSON.generate(data)
      end
    end
  end
end
+123 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

RSpec.describe Labkit::Logging::JsonLogger do
  include StubENV

  subject { described_class.new("/dev/null") }

  let(:now) { Time.now }

  before do
    allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return("new-correlation-id")
  end

  describe ".initialize" do
    it "builds logger using log_level" do
      expect(described_class).to receive(:log_level).and_return(:warn)

      expect(subject.level).to eq(described_class::WARN)
    end

    it "raises ArgumentError if invalid log level" do
      allow(described_class).to receive(:log_level).and_return(:invalid)

      expect { subject.level }.to raise_error(ArgumentError, "invalid log level: invalid")
    end

    using RSpec::Parameterized::TableSyntax

    where(:env_value, :resulting_level) do
      0 | described_class::DEBUG
      :debug | described_class::DEBUG
      "debug" | described_class::DEBUG
      "DEBUG" | described_class::DEBUG
      "DeBuG" | described_class::DEBUG
      1 | described_class::INFO
      :info | described_class::INFO
      "info" | described_class::INFO
      "INFO" | described_class::INFO
      "InFo" | described_class::INFO
      2 | described_class::WARN
      :warn | described_class::WARN
      "warn" | described_class::WARN
      "WARN" | described_class::WARN
      "WaRn" | described_class::WARN
      3 | described_class::ERROR
      :error | described_class::ERROR
      "error" | described_class::ERROR
      "ERROR" | described_class::ERROR
      "ErRoR" | described_class::ERROR
      4 | described_class::FATAL
      :fatal | described_class::FATAL
      "fatal" | described_class::FATAL
      "FATAL" | described_class::FATAL
      "FaTaL" | described_class::FATAL
      5 | described_class::UNKNOWN
      :unknown | described_class::UNKNOWN
      "unknown" | described_class::UNKNOWN
      "UNKNOWN" | described_class::UNKNOWN
      "UnKnOwN" | described_class::UNKNOWN
    end

    with_them do
      it "builds logger if valid log level" do
        stub_env("GITLAB_LOG_LEVEL", env_value)

        expect(subject.level).to eq(resulting_level)
      end
    end
  end

  describe ".log_level" do
    context "if GITLAB_LOG_LEVEL is set" do
      before do
        stub_env("GITLAB_LOG_LEVEL", described_class::ERROR)
      end

      it "returns value of GITLAB_LOG_LEVEL" do
        expect(described_class.log_level).to eq(described_class::ERROR)
      end

      it "ignores fallback" do
        expect(described_class.log_level(fallback: described_class::FATAL)).to eq(described_class::ERROR)
      end
    end

    context "if GITLAB_LOG_LEVEL is not set" do
      it "returns default fallback DEBUG" do
        expect(described_class.log_level).to eq(described_class::DEBUG)
      end

      it "returns passed fallback" do
        expect(described_class.log_level(fallback: described_class::FATAL)).to eq(described_class::FATAL)
      end
    end
  end

  it "appends newline" do
    output = subject.format_message("INFO", now, "test", "Hello world")

    expect(output).to end_with("\n")
  end

  it "formats strings" do
    output = subject.format_message("INFO", now, "test", "Hello world")
    data = JSON.parse(output)

    expect(data["severity"]).to eq("INFO")
    expect(data["time"]).to eq(now.utc.iso8601(3))
    expect(data["message"]).to eq("Hello world")
    expect(data["correlation_id"]).to eq("new-correlation-id")
  end

  it "formats hashes" do
    output = subject.format_message("INFO", now, "test", { hello: 1 })
    data = JSON.parse(output)

    expect(data["severity"]).to eq("INFO")
    expect(data["time"]).to eq(now.utc.iso8601(3))
    expect(data["hello"]).to eq(1)
    expect(data["message"]).to be_nil
    expect(data["correlation_id"]).to eq("new-correlation-id")
  end
end