green_log 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +194 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/doc/adr/0001-record-architecture-decisions.md +19 -0
- data/doc/adr/0002-avoid-global-configuration.md +35 -0
- data/doc/adr/0003-decouple-generation-and-handling.md +30 -0
- data/doc/adr/0004-use-stacked-handlers-to-solve-many-problems.md +51 -0
- data/doc/adr/0005-restrict-data-types-allowable-in-log-entries.md +27 -0
- data/doc/adr/0006-use-lock-free-io.md +29 -0
- data/examples/multi-threaded +21 -0
- data/examples/try-it +21 -0
- data/green_log.gemspec +35 -0
- data/lib/green_log.rb +6 -0
- data/lib/green_log/classic_logger.rb +80 -0
- data/lib/green_log/contextualizer.rb +26 -0
- data/lib/green_log/core_refinements.rb +73 -0
- data/lib/green_log/entry.rb +102 -0
- data/lib/green_log/json_writer.rb +48 -0
- data/lib/green_log/logger.rb +91 -0
- data/lib/green_log/rack/request_logging.rb +86 -0
- data/lib/green_log/severity.rb +49 -0
- data/lib/green_log/severity_filter.rb +26 -0
- data/lib/green_log/severity_threshold_support.rb +18 -0
- data/lib/green_log/simple_writer.rb +85 -0
- data/lib/green_log/version.rb +7 -0
- metadata +166 -0
data/examples/try-it
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
|
6
|
+
require "green_log/logger"
|
7
|
+
require "green_log/json_writer"
|
8
|
+
require "green_log/simple_writer"
|
9
|
+
require "pry"
|
10
|
+
|
11
|
+
case ARGV.first
|
12
|
+
when "json"
|
13
|
+
writer_class = GreenLog::JsonWriter
|
14
|
+
when "simple", nil
|
15
|
+
writer_class = GreenLog::SimpleWriter
|
16
|
+
else
|
17
|
+
raise ArgumentError, "bad format: #{ARGV.first}"
|
18
|
+
end
|
19
|
+
|
20
|
+
logger = GreenLog::Logger.new(writer_class.new(STDOUT))
|
21
|
+
logger.pry
|
data/green_log.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "green_log/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "green_log"
|
9
|
+
spec.version = GreenLog::VERSION
|
10
|
+
spec.authors = ["Mike Williams"]
|
11
|
+
spec.email = ["mike.williams@greensync.com.au"]
|
12
|
+
|
13
|
+
spec.summary = "Structured logging for cloud-native systems."
|
14
|
+
spec.homepage = "https://github.com/greensync/green_log"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
18
|
+
Dir.glob("{bin,doc,examples,lib}/**/*") + %w[
|
19
|
+
README.md green_log.gemspec
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_runtime_dependency "values", "~> 1.8"
|
28
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
29
|
+
spec.add_development_dependency "pry", "~> 0.12.2"
|
30
|
+
spec.add_development_dependency "rack"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
spec.add_development_dependency "rubocop", "~> 0.77"
|
34
|
+
|
35
|
+
end
|
data/lib/green_log.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "green_log/contextualizer"
|
4
|
+
require "green_log/entry"
|
5
|
+
require "green_log/severity"
|
6
|
+
require "green_log/severity_threshold_support"
|
7
|
+
|
8
|
+
module GreenLog
|
9
|
+
|
10
|
+
# An alternative to `GreenLog::Logger` for older code, which implements the
|
11
|
+
# same interface as the built-in Ruby `::Logger`.
|
12
|
+
class ClassicLogger
|
13
|
+
|
14
|
+
def initialize(downstream)
|
15
|
+
@downstream = downstream
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :downstream
|
19
|
+
|
20
|
+
include SeverityThresholdSupport
|
21
|
+
|
22
|
+
def add(severity, message = :unspecified, &block)
|
23
|
+
severity = Integer(severity)
|
24
|
+
return if severity < severity_threshold
|
25
|
+
|
26
|
+
entry = Entry.build(severity, resolve_message(message, &block))
|
27
|
+
|
28
|
+
downstream << entry
|
29
|
+
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
Severity::NAMES.each_with_index do |name, severity|
|
34
|
+
|
35
|
+
define_method(name.downcase) do |message = :unspecified, &block|
|
36
|
+
add(severity, message, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
define_method("#{name.downcase}?") do
|
40
|
+
severity >= severity_threshold
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def resolve_message(message, &block)
|
48
|
+
normalise_message(extract_message(message, &block))
|
49
|
+
end
|
50
|
+
|
51
|
+
def extract_message(message, &block)
|
52
|
+
if block
|
53
|
+
raise ArgumentError, "both message and block provided" unless message == :unspecified
|
54
|
+
|
55
|
+
return block.call
|
56
|
+
end
|
57
|
+
raise ArgumentError, "no message provided" if message == :unspecified
|
58
|
+
|
59
|
+
message
|
60
|
+
end
|
61
|
+
|
62
|
+
def normalise_message(message)
|
63
|
+
return message if message.is_a?(Exception)
|
64
|
+
return message.to_str if message.respond_to?(:to_str)
|
65
|
+
|
66
|
+
message.inspect
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
# :rubocop:disable: Style/Documentation
|
72
|
+
class Logger
|
73
|
+
|
74
|
+
def to_classic_logger
|
75
|
+
GreenLog::ClassicLogger.new(downstream)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "green_log/severity_threshold_support"
|
4
|
+
|
5
|
+
module GreenLog
|
6
|
+
|
7
|
+
# Log middleware that adds context.
|
8
|
+
class Contextualizer
|
9
|
+
|
10
|
+
def initialize(downstream, &context_generator)
|
11
|
+
@downstream = downstream
|
12
|
+
@context_generator = context_generator
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :downstream
|
16
|
+
attr_reader :context_generator
|
17
|
+
|
18
|
+
def <<(entry)
|
19
|
+
downstream << entry.in_context(context_generator.call)
|
20
|
+
end
|
21
|
+
|
22
|
+
include SeverityThresholdSupport
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GreenLog
|
4
|
+
|
5
|
+
# Refine
|
6
|
+
module CoreRefinements
|
7
|
+
|
8
|
+
refine ::Hash do
|
9
|
+
|
10
|
+
def to_loggable_value
|
11
|
+
{}.tap do |result|
|
12
|
+
each do |k, v|
|
13
|
+
result[k.to_sym] = v.to_loggable_value
|
14
|
+
end
|
15
|
+
end.freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def integrate(other)
|
19
|
+
other = other.to_hash
|
20
|
+
merge(other) do |_key, old_value, new_value|
|
21
|
+
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
22
|
+
old_value.integrate(new_value)
|
23
|
+
else
|
24
|
+
new_value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
refine ::Numeric do
|
32
|
+
|
33
|
+
def to_loggable_value
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
refine ::String do
|
40
|
+
|
41
|
+
def to_loggable_value
|
42
|
+
frozen? ? self : dup.freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
refine ::NilClass do
|
48
|
+
|
49
|
+
def to_loggable_value
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
refine ::TrueClass do
|
56
|
+
|
57
|
+
def to_loggable_value
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
refine ::FalseClass do
|
64
|
+
|
65
|
+
def to_loggable_value
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "green_log/core_refinements"
|
4
|
+
require "green_log/severity"
|
5
|
+
require "values"
|
6
|
+
|
7
|
+
module GreenLog
|
8
|
+
|
9
|
+
# Represents a structured log entry.
|
10
|
+
class Entry < Value.new(:severity, :message, :context, :data, :exception)
|
11
|
+
|
12
|
+
using CoreRefinements
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
def with(**args)
|
17
|
+
args[:severity] = Severity.resolve(
|
18
|
+
args.fetch(:severity, Severity::INFO),
|
19
|
+
)
|
20
|
+
args[:message] ||= nil
|
21
|
+
args[:context] = args.fetch(:context, {}).to_loggable_value
|
22
|
+
args[:data] = args.fetch(:data, {}).to_loggable_value
|
23
|
+
args[:exception] ||= nil
|
24
|
+
super(**args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def build(severity, *args, &block)
|
28
|
+
Builder.new(severity).build(*args, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def in_context(extra_context)
|
34
|
+
with(context: extra_context.integrate(context).to_loggable_value)
|
35
|
+
end
|
36
|
+
|
37
|
+
# A builder for entries.
|
38
|
+
class Builder
|
39
|
+
|
40
|
+
def initialize(severity)
|
41
|
+
@severity = severity
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :severity
|
45
|
+
attr_reader :message
|
46
|
+
attr_reader :data
|
47
|
+
attr_reader :exception
|
48
|
+
|
49
|
+
def message=(arg)
|
50
|
+
raise ArgumentError, ":message already specified" if defined?(@message)
|
51
|
+
|
52
|
+
@message = arg
|
53
|
+
end
|
54
|
+
|
55
|
+
def data=(arg)
|
56
|
+
raise ArgumentError, ":data already specified" if defined?(@data)
|
57
|
+
|
58
|
+
@data = arg
|
59
|
+
end
|
60
|
+
|
61
|
+
def exception=(arg)
|
62
|
+
raise ArgumentError, ":exception already specified" if defined?(@exception)
|
63
|
+
|
64
|
+
@exception = arg
|
65
|
+
end
|
66
|
+
|
67
|
+
def build(*args, &block)
|
68
|
+
args.each(&method(:handle_arg))
|
69
|
+
if block
|
70
|
+
if block.arity.zero?
|
71
|
+
Array(block.call).each(&method(:handle_arg))
|
72
|
+
else
|
73
|
+
block.call(self)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
Entry.with(severity: severity, message: message, data: data, exception: exception)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def handle_arg(arg)
|
82
|
+
public_send("#{arg_type(arg)}=", arg)
|
83
|
+
end
|
84
|
+
|
85
|
+
def arg_type(arg)
|
86
|
+
case arg
|
87
|
+
when String
|
88
|
+
:message
|
89
|
+
when Hash
|
90
|
+
:data
|
91
|
+
when Exception
|
92
|
+
:exception
|
93
|
+
else
|
94
|
+
raise ArgumentError, "un-loggable argument: #{arg.inspect}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "green_log/entry"
|
4
|
+
require "green_log/severity"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module GreenLog
|
8
|
+
|
9
|
+
# A JSON-formated log.
|
10
|
+
class JsonWriter
|
11
|
+
|
12
|
+
def initialize(dest)
|
13
|
+
@dest = dest
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :dest
|
17
|
+
|
18
|
+
def <<(entry)
|
19
|
+
raise ArgumentError, "GreenLog::Entry expected" unless entry.is_a?(GreenLog::Entry)
|
20
|
+
|
21
|
+
dest << JSON.dump(entry_details(entry)) + "\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def entry_details(entry)
|
27
|
+
{
|
28
|
+
"severity" => Severity.name(entry.severity).upcase,
|
29
|
+
"message" => entry.message,
|
30
|
+
"data" => entry.data,
|
31
|
+
"context" => entry.context,
|
32
|
+
"exception" => exception_details(entry.exception),
|
33
|
+
}.compact
|
34
|
+
end
|
35
|
+
|
36
|
+
def exception_details(exception)
|
37
|
+
return nil if exception.nil?
|
38
|
+
|
39
|
+
{
|
40
|
+
"class" => exception.class.name,
|
41
|
+
"message" => exception.message,
|
42
|
+
"backtrace" => exception.backtrace,
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "green_log/contextualizer"
|
4
|
+
require "green_log/entry"
|
5
|
+
require "green_log/severity"
|
6
|
+
require "green_log/severity_filter"
|
7
|
+
require "green_log/severity_threshold_support"
|
8
|
+
require "green_log/simple_writer"
|
9
|
+
|
10
|
+
module GreenLog
|
11
|
+
|
12
|
+
# Log entry generator.
|
13
|
+
class Logger
|
14
|
+
|
15
|
+
def initialize(downstream)
|
16
|
+
@downstream = downstream
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :downstream
|
20
|
+
|
21
|
+
include SeverityThresholdSupport
|
22
|
+
|
23
|
+
# Generate a log entry.
|
24
|
+
# Arguments may include:
|
25
|
+
# - a message string
|
26
|
+
# - arbitrary data
|
27
|
+
# - an exception
|
28
|
+
def log(severity, *rest, &block)
|
29
|
+
severity = Severity.resolve(severity)
|
30
|
+
return false if severity < severity_threshold
|
31
|
+
|
32
|
+
entry = Entry.build(severity, *rest, &block)
|
33
|
+
downstream << entry
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
Severity::NAMES.each_with_index do |name, severity|
|
38
|
+
|
39
|
+
define_method(name.downcase) do |*args, &block|
|
40
|
+
log(severity, *args, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add a middleware in front of the downstream handler.
|
46
|
+
# Return a new Logger with the expanded handler-stack.
|
47
|
+
def with_middleware
|
48
|
+
self.class.new(yield(downstream))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add a middleware that adds context.
|
52
|
+
# Return a new Logger with the expanded handler-stack.
|
53
|
+
def with_context(static_context = nil, &context_generator)
|
54
|
+
with_middleware do |current_downstream|
|
55
|
+
downstream = current_downstream
|
56
|
+
downstream = Contextualizer.new(downstream) { static_context } unless static_context.nil?
|
57
|
+
downstream = Contextualizer.new(downstream, &context_generator) unless context_generator.nil?
|
58
|
+
downstream
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Add a middleware that filters by severity.
|
63
|
+
# Return a new Logger with the expanded handler-stack.
|
64
|
+
def with_severity_threshold(threshold)
|
65
|
+
with_middleware do |downstream|
|
66
|
+
SeverityFilter.new(downstream, threshold: threshold)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class << self
|
71
|
+
|
72
|
+
# Build a Logger.
|
73
|
+
def build(dest: $stdout, format: SimpleWriter, severity_threshold: nil)
|
74
|
+
format = resolve_format(format)
|
75
|
+
downstream = format.new(dest)
|
76
|
+
downstream = SeverityFilter.new(downstream, threshold: severity_threshold) if severity_threshold
|
77
|
+
new(downstream)
|
78
|
+
end
|
79
|
+
|
80
|
+
def resolve_format(format)
|
81
|
+
return format if format.is_a?(Class)
|
82
|
+
|
83
|
+
format = format.to_s if format.is_a?(Symbol)
|
84
|
+
GreenLog.const_get("#{format.capitalize}Writer")
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|