green_log 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "green_log/json_writer"
4
+ require "green_log/logger"
5
+ require "green_log/simple_writer"
6
+ require "green_log/version"
@@ -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