Class: DiscourseLogstashLogger

Inherits:
Logger
  • Object
show all
Defined in:
lib/discourse_logstash_logger.rb

Constant Summary collapse

PROCESS_PID =
Process.pid
HOST =
Socket.gethostname
GIT_VERSION =
GitUtils.git_version
ALLOWED_HEADERS_FROM_ENV =
%w[
  REQUEST_URI
  REQUEST_METHOD
  HTTP_HOST
  HTTP_USER_AGENT
  HTTP_ACCEPT
  HTTP_REFERER
  HTTP_X_FORWARDED_FOR
  HTTP_X_REAL_IP
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#customize_eventObject

Returns the value of attribute customize_event.



12
13
14
# File 'lib/discourse_logstash_logger.rb', line 12

def customize_event
  @customize_event
end

#typeObject

Returns the value of attribute type.



12
13
14
# File 'lib/discourse_logstash_logger.rb', line 12

def type
  @type
end

Class Method Details

.logger(logdev:, type:, customize_event: nil) ⇒ Logger

Creates a new logger instance.

Parameters:

  • logdev (String, IO, nil)

    The log device. This can be one of:

    • A string filepath: entries are written to the file at that path. If the file exists, new entries are appended.

    • An IO stream (typically $stdout, $stderr, or an open file): entries are written to the given stream.

    • nil or File::NULL: no entries are written.

  • type (String)

    The type of log messages. This will add a ‘type` field to all log messages.

  • customize_event (Proc, nil) (defaults to: nil)

    A proc that customizes the log event before it is written to the log device. The proc is called with a hash of log event data and can be modified in place.

Returns:

  • (Logger)

    A new logger instance with the specified log device and type.



25
26
27
28
29
30
# File 'lib/discourse_logstash_logger.rb', line 25

def self.logger(logdev:, type:, customize_event: nil)
  logger = self.new(logdev)
  logger.type = type
  logger.customize_event = customize_event if customize_event
  logger
end

Instance Method Details

#add(*args, &block) ⇒ Object

:nodoc:



33
34
35
# File 'lib/discourse_logstash_logger.rb', line 33

def add(*args, &block)
  add_with_opts(*args, &block)
end

#add_with_opts(severity, message = nil, progname = nil, opts = {}, &block) ⇒ Object

:nodoc:



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
120
121
122
123
124
125
# File 'lib/discourse_logstash_logger.rb', line 49

def add_with_opts(severity, message = nil, progname = nil, opts = {}, &block)
  return true if @logdev.nil? || severity < @level

  progname = @progname if progname.nil?

  if message.nil?
    if block_given?
      message = yield
    else
      message = progname
      progname = @progname
    end
  end

  event = {
    "message" => message.to_s,
    "severity" => severity,
    "severity_name" => Logger::SEV_LABEL[severity],
    "pid" => PROCESS_PID,
    "type" => @type.to_s,
    "host" => HOST,
    "git_version" => GIT_VERSION,
  }

  # Only log backtrace and env for Logger::WARN and above.
  # Backtrace is just noise for anything below that.
  if severity >= Logger::WARN
    if (backtrace = opts&.dig(:backtrace)).present?
      event["backtrace"] = backtrace
    end

    # `web-exception` is a log message triggered by logster.
    # The exception class and message are extracted from the message based on the format logged by logster in
    # https://github.com/discourse/logster/blob/25375250fb8a5c312e9c55a75f6048637aad2c69/lib/logster/middleware/debug_exceptions.rb#L22.
    #
    # In theory we could get logster to include the exception class and message in opts but logster currently does not
    # need those options so we are parsing it from the message for now and not making a change in logster.
    if progname == "web-exception"
      # `Logster.store.ignore` is set in the logster initializer and is an array of regex patterns.
      return if Logster.store&.ignore&.any? { |pattern| pattern.match(message) }

      if message =~ /\A([^\(\)]+)\s{1}\(([\s\S]+)\)/
        event["exception.class"] = $1
        event["exception.message"] = $2.strip
      end

      ALLOWED_HEADERS_FROM_ENV.each do |header|
        event["request.headers.#{header.downcase}"] = opts.dig(:env, header)
      end
    end

    if progname == "sidekiq-exception"
      event["job.class"] = opts.dig(:context, :job)
      event["job.opts"] = opts.dig(:context, :opts)&.stringify_keys&.to_s
      event["job.problem_db"] = opts.dig(:context, :problem_db)
      event["exception.class"] = opts[:exception_class]
      event["exception.message"] = opts[:exception_message]
    end
  end

  if message.is_a?(String) && message.start_with?("{") && message.end_with?("}")
    begin
      parsed = JSON.parse(message)
      event["message"] = parsed.delete("message") if parsed["message"]
      event.merge!(parsed)
      event
    rescue JSON::ParserError
      # Do nothing
    end
  end

  @customize_event.call(event) if @customize_event

  @logdev.write("#{event.to_json}\n")
rescue Exception => e
  STDERR.puts "Error logging message `#{message}` in DiscourseLogstashLogger: #{e.class} (#{e.message})\n#{e.backtrace.join("\n")}"
end