betterlog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,163 @@
1
+ class Log
2
+ class Event
3
+ require 'socket'
4
+
5
+ def self.ify(arg, severity: :debug, notify: nil, rest: {})
6
+ notify ||= rest.delete(:notify)
7
+ if e = arg.ask_and_send(:exception)
8
+ ify(
9
+ {
10
+ error_class: e.class.name,
11
+ message: "#{e.class.name}: #{e.message}",
12
+ backtrace: e.backtrace,
13
+ },
14
+ severity: severity,
15
+ rest: rest,
16
+ notify: notify,
17
+ )
18
+ elsif s = arg.ask_and_send(:to_str)
19
+ new(
20
+ ({ notify: s } if notify).to_h |
21
+ {
22
+ message: s,
23
+ severity: severity,
24
+ } | rest
25
+ )
26
+ elsif h = arg.ask_and_send(:to_hash)
27
+ arg = h | { severity: severity } | rest
28
+ new(
29
+ ({ notify: h[:message] || arg.to_s } if notify).to_h |
30
+ arg
31
+ )
32
+ else
33
+ message = "Logging #{arg.inspect}"
34
+ new(
35
+ ({ notify: message } if notify).to_h |
36
+ {
37
+ message: message,
38
+ severity: severity,
39
+ } | rest
40
+ )
41
+ end
42
+ end
43
+
44
+ def self.parse(json)
45
+ new(JSON.parse(json))
46
+ rescue JSON::ParserError
47
+ end
48
+
49
+ def self.is?(json)
50
+ if json = json.ask_and_send(:to_str)
51
+ data = JSON.parse(json).ask_and_send(:to_hash)
52
+ data&.key?('emitter')
53
+ end
54
+ rescue JSON::ParserError
55
+ false
56
+ end
57
+
58
+ def initialize(data = {})
59
+ data = data.symbolize_keys_recursive | meta
60
+ unless data.key?(:message)
61
+ data[:message] = "a #{data[:type]} type log message of severity #{data[:severity]}"
62
+ end
63
+ data[:severity] =
64
+ begin
65
+ Severity.new((data[:severity] || :debug))
66
+ rescue
67
+ Severity.new(:debug)
68
+ end
69
+ @data = Hash[data.sort_by(&:first)]
70
+ end
71
+
72
+ class BreakCircles
73
+ def initialize
74
+ @seen = {}
75
+ end
76
+
77
+ IN_JSON = Tins::ModuleGroup[
78
+ Float, Integer, NilClass, FalseClass, TrueClass
79
+ ]
80
+
81
+ def perform(object)
82
+ if @seen.key?(object.__id__)
83
+ :circular
84
+ else
85
+ @seen[object.__id__] = true
86
+ case
87
+ when h = object.ask_and_send(:to_hash)
88
+ h.each_with_object({}) { |(k, v), h| h[k.to_s.to_sym] = perform(v) }
89
+ when a = object.ask_and_send(:to_ary)
90
+ a.map { |o| perform(o) }
91
+ when IN_JSON === object
92
+ object
93
+ else
94
+ object.to_s
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def as_hash(*a)
101
+ BreakCircles.new.perform(@data)
102
+ end
103
+
104
+ def to_json(*a)
105
+ as_hash.to_json(*a)
106
+ end
107
+
108
+ def format(*args)
109
+ Log::EventFormatter.new(self).format(*args)
110
+ end
111
+
112
+ alias to_s format
113
+
114
+ def []=(name, value)
115
+ @data[name.to_sym] = value
116
+ end
117
+
118
+ def [](name)
119
+ @data[name.to_sym]
120
+ end
121
+
122
+ def severity
123
+ @data[:severity]
124
+ end
125
+
126
+ def emitter
127
+ @data[:emitter]
128
+ end
129
+
130
+ def notify?
131
+ @data[:notify]
132
+ end
133
+
134
+ def eql?(other)
135
+ @data.eql? other.instance_variable_get(:@data)
136
+ end
137
+
138
+ alias == eql?
139
+
140
+ def hash
141
+ @data.hash
142
+ end
143
+
144
+ private
145
+
146
+ def meta
147
+ m = {
148
+ timestamp: Time.now.utc.iso8601(3),
149
+ pid: $$,
150
+ program: File.basename($0),
151
+ severity: :debug,
152
+ type: 'rails',
153
+ facility: 'local0',
154
+ host: (Socket.gethostname rescue nil),
155
+ thread_id: Thread.current.object_id
156
+ }
157
+ if defined? GlobalMetadata
158
+ m |= GlobalMetadata.data
159
+ end
160
+ m
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,117 @@
1
+ class Log
2
+ class EventFormatter
3
+ include Term::ANSIColor
4
+
5
+ def initialize(event)
6
+ @event = event
7
+ end
8
+
9
+ def format(pretty: false, color: false, format: :default)
10
+ old_coloring, Term::ANSIColor.coloring = Term::ANSIColor.coloring?, color
11
+ f = cc.log.formats[format] and format = f
12
+ case pretty
13
+ when :format
14
+ format_pattern(format: format)
15
+ else
16
+ @event.to_json
17
+ end
18
+ ensure
19
+ Term::ANSIColor.coloring = old_coloring
20
+ end
21
+
22
+ private
23
+
24
+ def colorize(key, value, string = key)
25
+ case style = cc.log.styles[key]
26
+ when nil, String, Array
27
+ apply_style(style, string)
28
+ when ComplexConfig::Settings
29
+ apply_style(style[value], string)
30
+ end
31
+ end
32
+
33
+ def apply_style(style, string)
34
+ style.nil? and return string + Term::ANSIColor.reset
35
+ string = Term::ANSIColor.uncolor(string)
36
+ if style.respond_to?(:each)
37
+ style.
38
+ each.
39
+ map { |s| -> v { __send__(:color, s, v) } }.
40
+ inject(string) { |v, s| s.(v) }
41
+ else
42
+ __send__(:color, style, string)
43
+ end
44
+ end
45
+
46
+ def format_pattern(format:)
47
+ format.
48
+ gsub('\n', "\n").
49
+ gsub('\t', "\t").
50
+ gsub(/\{(-)?(%[^%]+%)?([^}]+)\}/) {
51
+ invisible = $1.full?
52
+ directive = $2
53
+ key = $3
54
+ if value = @event[key]
55
+ formatted_value =
56
+ if directive
57
+ case directive
58
+ when /\A%O%/
59
+ format_object(value)
60
+ when /\A%([ulif])?t%/
61
+ flag = $1
62
+ t = case
63
+ when v = value.ask_and_send(:to_str)
64
+ Time.parse(v)
65
+ when v = value.ask_and_send(:to_time)
66
+ v
67
+ else
68
+ Time.at(0)
69
+ end
70
+ case flag
71
+ when ?u then t.utc.iso8601(3)
72
+ when ?l then t.localtime.iso8601(3)
73
+ when ?i then t.to_i.to_s
74
+ when ?f then t.to_f.to_s
75
+ else t.utc.iso8601(3)
76
+ end
77
+ else
78
+ begin
79
+ directive[0..-2] % value
80
+ rescue ArgumentError
81
+ value.to_s
82
+ end
83
+ end
84
+ else
85
+ value.to_s
86
+ end
87
+ colorize(key, value, formatted_value)
88
+ else
89
+ unless invisible
90
+ "{#{key}}"
91
+ end
92
+ end
93
+ }
94
+ end
95
+
96
+ def format_object(object, depth: 0, nl: ?\n)
97
+ case
98
+ when a = object.ask_and_send(:to_ary)
99
+ result = ''
100
+ depth += 2
101
+ for v in a
102
+ result << "\n#{' ' * depth}- #{format_object(v, depth: depth)}"
103
+ end
104
+ result
105
+ when h = object.ask_and_send(:to_hash)
106
+ result = ''
107
+ depth += 2
108
+ for k, v in h
109
+ result << "\n#{' ' * depth}#{k}: #{format_object(v, depth: depth)}"
110
+ end
111
+ result
112
+ else
113
+ object.to_s
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,49 @@
1
+ class Log
2
+ class Severity
3
+ include Comparable
4
+
5
+ def initialize(name)
6
+ @name = name.to_s.downcase.to_sym
7
+ begin
8
+ @level = ActiveSupport::Logger::Severity.const_get(@name.upcase)
9
+ rescue NameError
10
+ @name = :UNKNOWN
11
+ @level = ActiveSupport::Logger::Severity::UNKNOWN
12
+ end
13
+ end
14
+
15
+ def self.all
16
+ ActiveSupport::Logger::Severity.constants.map { |c| new(c) }
17
+ end
18
+
19
+ def to_i
20
+ @level
21
+ end
22
+
23
+ def to_s
24
+ @name.to_s.upcase
25
+ end
26
+
27
+ def to_sym
28
+ @name
29
+ end
30
+
31
+ def as_json(*)
32
+ to_sym
33
+ end
34
+
35
+ def <=>(other)
36
+ to_i <=> other.to_i
37
+ end
38
+
39
+ def eql?(other)
40
+ to_sym == other.to_sym
41
+ end
42
+
43
+ alias == eql?
44
+
45
+ def hash
46
+ @name.hash
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,181 @@
1
+ require 'tins'
2
+ require 'tins/xt'
3
+ require 'betterlog/log/event'
4
+ require 'betterlog/log/event_formatter'
5
+ require 'betterlog/log/severity'
6
+
7
+ class Log
8
+ include Tins::SexySingleton
9
+
10
+ def logger
11
+ Rails.logger
12
+ end
13
+
14
+ # Logs a message on severity info.
15
+ #
16
+ # @param object this object is logged
17
+ # @param **rest additional data is logged as well.
18
+ # @return [ Log ] this object itself.
19
+ def info(object, **rest)
20
+ protect do
21
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
22
+ end
23
+ end
24
+
25
+ # Logs a message on severity warn.
26
+ #
27
+ # @param object this object is logged
28
+ # @param **rest additional data is logged as well.
29
+ # @return [ Log ] this object itself.
30
+ def warn(object, **rest)
31
+ protect do
32
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
33
+ end
34
+ end
35
+
36
+ # Logs a message on severity debug.
37
+ #
38
+ # @param object this object is logged
39
+ # @param **rest additional data is logged as well.
40
+ # @return [ Log ] this object itself.
41
+ def debug(object, **rest)
42
+ protect do
43
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
44
+ end
45
+ end
46
+
47
+ # Logs a message on severity error.
48
+ #
49
+ # @param object this object is logged
50
+ # @param **rest additional data is logged as well.
51
+ # @return [ Log ] this object itself.
52
+ def error(object, **rest)
53
+ protect do
54
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
55
+ end
56
+ end
57
+
58
+ # Logs a message on severity fatal.
59
+ #
60
+ # @param object this object is logged
61
+ # @param **rest additional data is logged as well.
62
+ # @return [ Log ] this object itself.
63
+ def fatal(object, **rest)
64
+ protect do
65
+ emit Log::Event.ify(object, severity: __method__, rest: rest)
66
+ end
67
+ end
68
+
69
+ # Logs a message on severity debug, by default, this can be changed by
70
+ # passing the severity: keyword.
71
+ #
72
+ # @param object this object is logged
73
+ # @param **rest additional data is logged as well.
74
+ # @return [ Log ] this object itself.
75
+ def output(object, **rest)
76
+ protect do
77
+ emit Log::Event.ify(object, severity: rest[:severity], rest: rest)
78
+ end
79
+ end
80
+
81
+ # Logs a metric on severity debug, by default, this can be changed by passing
82
+ # the severity: keyword.
83
+ #
84
+ # @param metric the name of the recorded metric.
85
+ # @param type of the recorded metric.
86
+ # @param value of the recorded metric.
87
+ # @param **rest additional rest is logged as well.
88
+ # @return [ Log ] this object itself.
89
+ def metric(metric:, type:, value:, **rest)
90
+ protect do
91
+ event = build_metric(metric: metric, type: type, value: value, **rest)
92
+ emit event
93
+ end
94
+ end
95
+
96
+ # Logs a time measure on severity debug, by default, this can be changed by
97
+ # passing the severity: keyword.
98
+ #
99
+ # If an error occurs during measurement details about it are added to the
100
+ # metric event.
101
+ #
102
+ # @param metric the name of the recorded metric.
103
+ # @param **rest additional rest is logged as well.
104
+ # @param block the block around which the measure is teaken.
105
+ # @return [ Log ] this object itself.
106
+ def measure(metric:, **rest, &block)
107
+ raise ArgumentError, 'must be called with a block' unless block_given?
108
+ time_block { yield }
109
+ rescue => error
110
+ e = Log::Event.ify(error)
111
+ rest |= e.as_hash.subhash(:error_class, :backtrace, :message)
112
+ rest[:message] = "#{rest[:message]} while measuring metric #{metric}"
113
+ raise error
114
+ ensure
115
+ protect do
116
+ event = build_metric(metric: metric, type: 'seconds', value: timed_duration, **rest)
117
+ emit event
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def protect
124
+ yield
125
+ rescue => e
126
+ begin
127
+ # Try logging e once by ourselves
128
+ emit Log::Event.ify(e, severity: :fatal)
129
+ rescue
130
+ # Ok, I give up let's use logger directly instead
131
+ logger.fatal(
132
+ "Crashed during logging with #{e.class}: #{e.message}):\n"\
133
+ "#{e.backtrace * ?\n}"
134
+ )
135
+ end
136
+ self
137
+ end
138
+
139
+ def build_metric(metric:, type:, value:, **rest)
140
+ severity = rest.fetch(:severity, :debug)
141
+ rest |= {
142
+ message: "a metric #{metric} of type #{type}",
143
+ }
144
+ Log::Event.ify(
145
+ {
146
+ metric: metric,
147
+ type: type,
148
+ value: value,
149
+ } | rest,
150
+ severity: severity
151
+ )
152
+ end
153
+
154
+ def emit(event)
155
+ if l = caller_locations.reverse_each.each_cons(3).find { |c, n1, n2|
156
+ n2.absolute_path =~ /app\/lib\/log\.rb/ and break c
157
+ }
158
+ then
159
+ event[:location] = [ l.absolute_path, l.lineno ] * ?:
160
+ end
161
+ event[:emitter] = self.class.name.downcase
162
+ if event.notify?
163
+ notify(event)
164
+ end
165
+ logger.send(event.severity.to_sym, event.to_json)
166
+ self
167
+ end
168
+
169
+ def notify(event)
170
+ Honeybadger.notify(event.notify?, event.as_hash)
171
+ end
172
+
173
+ thread_local :timed_duration
174
+
175
+ def time_block
176
+ s = Time.now
177
+ yield
178
+ ensure
179
+ self.timed_duration = Time.now - s
180
+ end
181
+ end
@@ -0,0 +1,47 @@
1
+ class LogEventFormatter < ActiveSupport::Logger::Formatter
2
+ include ActiveSupport::TaggedLogging::Formatter
3
+
4
+ def emitter
5
+ 'legacy'
6
+ end
7
+
8
+ def call(severity, timestamp, program, message)
9
+ super
10
+ message = message.to_s
11
+ if cc.log.legacy_supported
12
+ if message.blank?
13
+ return ''
14
+ elsif !Log::Event.is?(message)
15
+ m = message.sub(/\s+$/, '')
16
+ timestamp = timestamp.utc.iso8601(3)
17
+ event = Log::Event.new(
18
+ emitter: emitter,
19
+ timestamp: timestamp,
20
+ message: m,
21
+ severity: severity.to_s.downcase,
22
+ # tags: current_tags,
23
+ )
24
+ if backtrace = m.grep(/^\s*([^:]+):(\d+)/)
25
+ if backtrace.size > 1
26
+ event[:backtrace] = backtrace.map(&:chomp)
27
+ event[:message] = 'a logged backtrace'
28
+ end
29
+ end
30
+ if l = caller_locations.reverse_each.each_cons(2).find { |c, n|
31
+ n.absolute_path =~ /\/lib\/ruby\/.*?\/logger\.rb/ and break c
32
+ }
33
+ then
34
+ event[:location] = [ l.absolute_path, l.lineno ] * ?:
35
+ end
36
+ program and event[:program] = program
37
+ message = event.to_json
38
+ end
39
+ end
40
+ rescue => e
41
+ Rails.logger.error(e)
42
+ ensure
43
+ # Do not "message << ?\n" - A frozn string may be passed in
44
+ message.end_with?(?\n) or message = "#{message}\n"
45
+ return message
46
+ end
47
+ end
@@ -0,0 +1,8 @@
1
+ module Betterlog
2
+ # Betterlog version
3
+ VERSION = '0.1.0'
4
+ VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
+ VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
+ VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
7
+ VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
8
+ end
data/lib/betterlog.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'tins/xt'
2
+ require 'json'
3
+ require 'logger'
4
+ require 'time'
5
+ require 'tins'
6
+ require 'rails'
7
+
8
+ require 'active_support'
9
+ require 'term/ansicolor'
10
+
11
+ require 'betterlog/global_metadata'
12
+ require 'betterlog/betterlog_railtie'
13
+ require 'betterlog/log'
14
+ require 'betterlog/log_event_formatter'
data/log.yml ADDED
@@ -0,0 +1,77 @@
1
+ default: &default
2
+ styles:
3
+ 'timestamp': [ yellow, bold ]
4
+ 'file': [ blue, bold ]
5
+ severity:
6
+ debug: green
7
+ info: green
8
+ warn: yellow
9
+ error: red
10
+ fatal: [ red, blink ]
11
+ unknown: red
12
+ formats:
13
+ default: &default_format >
14
+ {%lt%timestamp} {%5s%severity}
15
+ "{%0.<%= ENV.fetch('COLUMNS', 80).to_i / 2 %>%message}"
16
+ {location}
17
+ {file}{-%O%backtrace}{-%O%meta}
18
+ d: *default_format
19
+ long: &long_format |
20
+ timestamp: {%lt%timestamp}
21
+ severity: {severity}
22
+ message: "{message}"
23
+ metric: {metric} {value} {type}
24
+ error_class: {error_class}
25
+ backtrace: {%O%backtrace}
26
+ location: {location}
27
+ file: {file}
28
+ meta: {%O%meta}\n
29
+ l: *long_format
30
+ legacy: >
31
+ {%0.1s%severity} [{%lt%timestamp} #{%d%pid}] {%5s%severity} --
32
+ {program}: {message}
33
+ metric: >
34
+ {%ft%timestamp} {metric} {value} {type}
35
+
36
+ development:
37
+ <<: *default
38
+ config_files:
39
+ rails:
40
+ - log/development.log
41
+ test:
42
+ - log/test.log
43
+ redis:
44
+ - /usr/local/var/log/redis.log
45
+ elasticsearch:
46
+ - /usr/local/var/log/elasticsearch@1.7.log
47
+ nginx:
48
+ - /usr/local/var/log/nginx/access.log
49
+ - /usr/local/var/log/nginx/error.log
50
+ legacy_supported: <%= ENV['LOG_LEGACY_SUPPORTED'].to_i == 1 %>
51
+
52
+ test:
53
+ <<: *default
54
+ config_files:
55
+ test:
56
+ - log/test.log
57
+ legacy_supported: <%= ENV['LOG_LEGACY_SUPPORTED'].to_i == 1 %>
58
+
59
+ production:
60
+ <<: *default
61
+ config_files:
62
+ rails:
63
+ - /var/apps/betterplace/current/log/production.log
64
+ cron:
65
+ - /var/apps/betterplace/current/log/cron.log
66
+ nginx:
67
+ - /var/apps/betterplace/current/log/nginx.assets.access.log
68
+ - /var/apps/betterplace/current/log/nginx.assets.error.log
69
+ - /var/apps/betterplace/current/log/nginx.betterplace.access.log
70
+ - /var/apps/betterplace/current/log/nginx.betterplace.error.log
71
+ - /var/apps/betterplace/current/log/nginx.default.access.log
72
+ - /var/apps/betterplace/current/log/nginx.default.error.log
73
+ memosig:
74
+ - /var/log/memosig/current
75
+ unicorn:
76
+ - /var/log/unicorn/current
77
+ legacy_supported: yes