mirror of
https://github.com/meineerde/rackstash.git
synced 2025-10-17 14:01:01 +00:00
Add lograge encoder
This encoder formats logs similar to the key-value formatter of the lograge gem.
This commit is contained in:
parent
40aa74c5c0
commit
b101d7356a
@ -5,6 +5,7 @@
|
||||
# of the MIT license. See the LICENSE.txt file for details.
|
||||
|
||||
require 'rackstash/encoders/json'
|
||||
require 'rackstash/encoders/lograge'
|
||||
require 'rackstash/encoders/logstash'
|
||||
require 'rackstash/encoders/message'
|
||||
require 'rackstash/encoders/raw'
|
||||
|
||||
139
lib/rackstash/encoders/lograge.rb
Normal file
139
lib/rackstash/encoders/lograge.rb
Normal file
@ -0,0 +1,139 @@
|
||||
# frozen_string_literal: true
|
||||
# Copyright 2017 Holger Just
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the MIT license. See the LICENSE.txt file for details.
|
||||
|
||||
require 'rackstash/encoders/helpers/timestamp'
|
||||
|
||||
module Rackstash
|
||||
module Encoders
|
||||
# The Lograge encoder formats the log event in the original key-value format
|
||||
# of the [lograge gem](https://github.com/roidrage/lograge).
|
||||
#
|
||||
# The fields of the passed event are serielized to simple `key=value` pairs,
|
||||
# separated by a single space each. The following formatting rules apply:
|
||||
#
|
||||
# * Field names for nested Hashes and Arrays are separated by a dot to form
|
||||
# a unique key name
|
||||
# * Arrays are formatted the same as nested hashes, using the value's index
|
||||
# in the array in the field name for each value.
|
||||
# * Floats are formatted with 2 decimal digits
|
||||
# * Newlines, dots, equals signs or spaces in keys and values are preserved
|
||||
# and not escaped in any way (apart from the `"error"` field, see below).
|
||||
# You might thus want to avoid any whitespace charactes in general and
|
||||
# dots or equals signs in hash keys. You can use filters in your {Flow} to
|
||||
# ensure suitable field names.
|
||||
# * The `"message"` and `"error_trace"` fields are never included in the
|
||||
# output.
|
||||
# * If there is an error in the event hash, we will generate a quoted string
|
||||
# from the `"error"` and `"error_message"` fields.
|
||||
#
|
||||
# Given the following event
|
||||
#
|
||||
# {
|
||||
# "@timestamp" => Time.utc(2017, 4, 18, 23, 21, 58),
|
||||
# "message" => ["This is ignored"],
|
||||
# "foo" => ["bar", "baz"],
|
||||
# "beep" => {
|
||||
# "pling" => "plong",
|
||||
# "toot" => "chirp"
|
||||
# },
|
||||
# "runtime" => 3.14159
|
||||
# }
|
||||
#
|
||||
# the encoder will output the following log line:
|
||||
#
|
||||
# timestamp=2017-04-18T23:21:58.000000Z foo.0=bar foo.1=baz beep.pling=plong beep.toot=chirp runtime=3.14
|
||||
#
|
||||
# With an error in the event hash, e.g. like this
|
||||
#
|
||||
# {
|
||||
# "@timestamp" => Time.utc(2017, 4, 18, 23, 21, 58),
|
||||
# "message" => ["This is ignored"],
|
||||
# "error" => "RuntimeError",
|
||||
# "error_message" => "Something bad happened",
|
||||
# "error_trace" => "my_lib.rb:5:in `broken_method'\nmy_lib.rb:10:in `<main>'"
|
||||
# }
|
||||
#
|
||||
# the encoder will output the following log line:
|
||||
#
|
||||
# timestamp=2017-04-18T23:21:58.000000Z error='RuntimeError: Something bad happened'
|
||||
#
|
||||
class Lograge
|
||||
include Rackstash::Encoders::Helpers::Timestamp
|
||||
|
||||
SKIP = [
|
||||
FIELD_MESSAGE,
|
||||
FIELD_ERROR_TRACE
|
||||
].freeze
|
||||
|
||||
# @param event [Hash] a log event as produced by the {Flow}
|
||||
# @return [String] a log line with formatted key-value pairs
|
||||
def encode(event)
|
||||
normalize_timestamp(event)
|
||||
|
||||
format_error(event)
|
||||
skip_fields(event)
|
||||
|
||||
serialize_hash(event)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def format_error(event)
|
||||
error = event[FIELD_ERROR]
|
||||
error_message = event.delete(FIELD_ERROR_MESSAGE)
|
||||
|
||||
event[FIELD_ERROR] =
|
||||
if error.nil?
|
||||
error_message.nil? ? nil : "'#{error_message}'"
|
||||
else
|
||||
error_message.nil? ? "'#{error}'" : "'#{error}: #{error_message}'"
|
||||
end
|
||||
end
|
||||
|
||||
def skip_fields(event)
|
||||
SKIP.each do |field|
|
||||
event.delete(field)
|
||||
end
|
||||
end
|
||||
|
||||
def serialize_hash(hash, prefix: nil)
|
||||
hash.map { |key, value|
|
||||
serialize_pair(key, value, prefix)
|
||||
}.compact.join(' '.freeze)
|
||||
end
|
||||
|
||||
def serialize_array(array, prefix: nil)
|
||||
array.each_with_index.map { |value, index|
|
||||
serialize_pair(index.to_s, value, prefix)
|
||||
}.compact.join(' '.freeze)
|
||||
end
|
||||
|
||||
def serialize_pair(key, value, prefix)
|
||||
if prefix
|
||||
key = "#{prefix}.#{key}"
|
||||
elsif key == FIELD_TIMESTAMP
|
||||
# Use 'timestamp' instead of '@timestamp' on the top-level
|
||||
key = 'timestamp'
|
||||
end
|
||||
|
||||
case value
|
||||
when nil
|
||||
return
|
||||
when Hash
|
||||
return if value.empty?
|
||||
return serialize_hash(value, prefix: key)
|
||||
when Array
|
||||
return if value.empty?
|
||||
return serialize_array(value, prefix: key)
|
||||
when Float
|
||||
value = Kernel.format('%.2f'.freeze, value)
|
||||
end
|
||||
|
||||
"#{key}=#{value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
70
spec/rackstash/encoders/lograge_spec.rb
Normal file
70
spec/rackstash/encoders/lograge_spec.rb
Normal file
@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
# Copyright 2017 Holger Just
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the MIT license. See the LICENSE.txt file for details.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rackstash/encoders/lograge'
|
||||
|
||||
describe Rackstash::Encoders::Lograge do
|
||||
let(:encoder) { described_class.new }
|
||||
|
||||
describe '#encode' do
|
||||
it 'formats the timestamp if present' do
|
||||
event = { '@timestamp' => Time.new(2016, 10, 17, 16, 37, 0, '+03:00') }
|
||||
expect(encoder.encode(event))
|
||||
.to eql 'timestamp=2016-10-17T13:37:00.000000Z'
|
||||
end
|
||||
|
||||
it 'formats multiple values' do
|
||||
event = { 'pling' => 'plong', 'toot' => 'chirp' }
|
||||
expect(encoder.encode(event))
|
||||
.to eql 'pling=plong toot=chirp'
|
||||
end
|
||||
|
||||
it 'formats nested objects' do
|
||||
event = { 'pling' => ['plong', nil, { 'toot' => { 'bird' => ['chirp', 'tweet'] } }] }
|
||||
expect(encoder.encode(event))
|
||||
.to eql 'pling.0=plong pling.2.toot.bird.0=chirp pling.2.toot.bird.1=tweet'
|
||||
end
|
||||
|
||||
it 'formats float values' do
|
||||
event = { 'key' => 3.14159, 'rounded' => 4.947 }
|
||||
expect(encoder.encode(event)).to eql 'key=3.14 rounded=4.95'
|
||||
end
|
||||
|
||||
it 'formats complex errors' do
|
||||
event = {
|
||||
'error' => 'RuntimeError',
|
||||
'error_message' => 'Something',
|
||||
'error_trace' => "Foo\nBar\nBaz",
|
||||
|
||||
'nested' => {
|
||||
'error' => 'NestedError',
|
||||
'error_message' => 'a message'
|
||||
}
|
||||
}
|
||||
|
||||
expect(encoder.encode(event))
|
||||
.to eql "error='RuntimeError: Something' nested.error=NestedError nested.error_message=a message"
|
||||
end
|
||||
|
||||
it 'formats an error' do
|
||||
event = { 'error' => 'StandardError' }
|
||||
expect(encoder.encode(event)).to eql "error='StandardError'"
|
||||
end
|
||||
|
||||
it 'formats an error_message' do
|
||||
event = { 'error_message' => 'Something happened' }
|
||||
expect(encoder.encode(event)).to eql "error='Something happened'"
|
||||
end
|
||||
|
||||
it 'ignores dots, spaces and equal signs' do
|
||||
event = { 'some.key' => 'a.value', 'a=b' => 'c=d', 'a key' => 'some value' }
|
||||
expect(encoder.encode(event))
|
||||
.to eql 'some.key=a.value a=b=c=d a key=some value'
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
x
Reference in New Issue
Block a user