summaryrefslogtreecommitdiff
path: root/lib/gitlab_logger.rb
blob: ed169f7d35a9a2f0171446971ff0712900c993bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
require 'json'
require 'logger'
require 'time'

require_relative 'gitlab_config'

def convert_log_level(log_level)
  Logger.const_get(log_level.upcase)
rescue NameError
  $stderr.puts "WARNING: Unrecognized log level #{log_level.inspect}."
  $stderr.puts "WARNING: Falling back to INFO."
  Logger::INFO
end

class GitlabLogger
  # Emulate the quoting logic of logrus
  # https://github.com/sirupsen/logrus/blob/v1.0.5/text_formatter.go#L143-L156
  SHOULD_QUOTE = /[^a-zA-Z0-9\-._\/@^+]/

  LEVELS = {
    Logger::INFO => 'info'.freeze,
    Logger::DEBUG => 'debug'.freeze,
    Logger::WARN => 'warn'.freeze,
    Logger::ERROR => 'error'.freeze
  }.freeze

  def initialize(level, path, log_format)
    @level = level

    @log_file = File.open(path, 'ab')
    # By default Ruby will buffer writes. This is a problem when we exec
    # into a new command before Ruby flushed its buffers. Setting 'sync' to
    # true disables Ruby's buffering.
    @log_file.sync = true

    @log_format = log_format
  end

  def info(message, data = {})
    log_at(Logger::INFO, message, data)
  end

  def debug(message, data = {})
    log_at(Logger::DEBUG, message, data)
  end

  def warn(message, data = {})
    log_at(Logger::WARN, message, data)
  end

  def error(message, data = {})
    log_at(Logger::ERROR, message, data)
  end

  private

  attr_reader :log_file, :log_format

  def log_at(level, message, data)
    return unless @level <= level

    data[:pid] = pid
    data[:level] = LEVELS[level]
    data[:msg] = message

    # Use RFC3339 to match logrus in the Go parts of gitlab-shell
    data[:time] = time_now.to_datetime.rfc3339

    case log_format
    when 'json'
      log_file.puts format_json(data)
    else
      log_file.puts format_text(data)
    end
  end

  def pid
    Process.pid
  end

  def time_now
    Time.now
  end

  def format_text(data)
    # We start the line with these fields to match the behavior of logrus
    result = [
      format_key_value(:time, data.delete(:time)),
      format_key_value(:level, data.delete(:level)),
      format_key_value(:msg, data.delete(:msg))
    ]

    data.sort.each { |k, v| result << format_key_value(k, v) }
    result.join(' ')
  end

  def format_key_value(key, value)
    value_string = value.to_s
    value_string = value_string.inspect if SHOULD_QUOTE =~ value_string

    "#{key}=#{value_string}"
  end

  def format_json(data)
    data.each do |key, value|
      next unless value.is_a?(String)

      value = value.dup.force_encoding('utf-8')
      value = value.inspect unless value.valid_encoding?
      data[key] = value.freeze
    end

    data.to_json
  end
end

config = GitlabConfig.new

$logger = GitlabLogger.new(convert_log_level(config.log_level), config.log_file, config.log_format)