mimi-logger 0.1.2 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fbe29541826be5c0039eaa0e048fb358b16fbdce
4
- data.tar.gz: 3df2c56beae9eebb87a7651f4a38d14e871d6395
2
+ SHA256:
3
+ metadata.gz: 1dac6f3cbaeef0582579cb7f695e92896676d9267c286e7d82d95d7d72312219
4
+ data.tar.gz: 3e9866931dff24f400dca04da3e1e41d59139f33860c621d2e7136656f80f87d
5
5
  SHA512:
6
- metadata.gz: 18b631e1f90f9406c9556710b721e3761a01e9fff760881330f9b996f758ef2aa41e8eb9ead36432f7b06ab15d02b6f2753c83d9780a9050ec9dc4ae6b1e0432
7
- data.tar.gz: 644d8221e16b761a5a7fcb63484566e88659421e71677eb728b3684f1938cec4453ff1fd50e3ee6a4a59804f3c57c999479654a15081567cb093a537ae744034
6
+ metadata.gz: 0c02fa6f4e89c807b5b6284aec1e304939cf3aec08820c73db08380b81a0b0f071e85e99c55810bf8e0b584a5497f227748311dd44b7d4e9b087b649970a2b2a
7
+ data.tar.gz: 62cdc14d5fcbfe555e2fe45af17db5c004d051767445802dbb35fa3525825f4de514afad51da9fd700f110ae4c1ad70680344da47641b3bba50250a7c2dfdf9e
data/README.md CHANGED
@@ -25,11 +25,93 @@ Or install it yourself as:
25
25
  ```ruby
26
26
  require 'mimi/logger'
27
27
 
28
- logger = Mimi::Logger.new
28
+ logger = Mimi::Logger.new format: :string
29
29
 
30
30
  logger.info 'I am a banana!' # outputs "I, I am a banana!" to STDOUT
31
31
  ```
32
32
 
33
+ ### Logging format
34
+
35
+ Since v0.2.0 the default format for logging is JSON:
36
+
37
+ ```ruby
38
+ require 'mimi/logger'
39
+
40
+ logger = Mimi::Logger.new
41
+
42
+ logger.info 'I am a banana' # => '{"s":"I","m":"I am a banana","c":"60eecc2e764fe2f6"}'
43
+ ```
44
+
45
+ The following properties of a serialized JSON object are reserved:
46
+
47
+ name | description
48
+ -----|------------
49
+ s | severity, one of D,I,W,E,F
50
+ m | message
51
+ c | context ID, 8 bytes hex encoded
52
+
53
+ Additional properties may be provided by the caller and they will be included in the logged JSON:
54
+
55
+ ```ruby
56
+ require 'mimi/logger'
57
+
58
+ logger = Mimi::Logger.new
59
+
60
+ t_start = Time.now
61
+ ... # work, work
62
+ logger.info 'Jobs done', t: Time.now - t_start
63
+ # => '{"s":"I","m":"Jobs done","c":"60eecc2e764fe2f6", "t": 13.794034499}'
64
+ ```
65
+
66
+ ### Logging context
67
+
68
+ Logging context refers to a set of instructions that are somehow logically grouped. For example,
69
+ the whole logic processing an incoming web request can be seen as operating within a single context
70
+ of this particular web request. In a distributed or a multithreaded application it would be beneficial
71
+ to identify logged messages produced within same context, otherwise the messages related to different
72
+ web requests will be interleaved and understanding the flow, the cause and effect would be very difficult.
73
+
74
+ To solve this problem, Mimi::Logger allows for setting an arbitrary (or random) context ID, which is local
75
+ to the current thread, and which is included in every logged message.
76
+
77
+ A new context may be initiated by `.new_context_id!`:
78
+
79
+ ```ruby
80
+ require 'mimi/logger'
81
+
82
+ logger = Mimi::Logger.new
83
+
84
+ logger.info 'I am a banana!'
85
+ logger.new_context_id!
86
+ logger.info 'I am a banana!' # this is not the same banana, it's from a different context
87
+ ```
88
+
89
+ Or it can be set to an explicit value:
90
+
91
+ ```ruby
92
+ require 'mimi/logger'
93
+
94
+ logger = Mimi::Logger.new
95
+
96
+ logger.info 'I am a banana!'
97
+ logger.context_id = 'different-context!'
98
+ logger.info 'I am a banana!' # this is not the same banana, it's from a 'different-context!'
99
+
100
+ ```
101
+
102
+ #### Context ID in a multithreaded application
103
+
104
+ The context ID is local to a current thread, so it's safe to start or assign a different context ID
105
+ in other threads of the application.
106
+
107
+ #### Context ID in a distributed application
108
+
109
+ If you have a distributed multicomponent application (e.g. microservice architecture), context ID
110
+ may help to track requests between multiple parties. In order to achieve it, you need to generate a
111
+ new context ID in the beginning of your processing and pass it along with all the requests/messages to
112
+ other components of the system. Upon receiving such a request, another application sets its local context ID
113
+ to the received value and continues.
114
+
33
115
  ## Contributing
34
116
 
35
117
  Bug reports and pull requests are welcome on GitHub at https://github.com/kukushkin/mimi-logger. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -1,5 +1,5 @@
1
1
  module Mimi
2
2
  class Logger
3
- VERSION = '0.1.2'.freeze
3
+ VERSION = '0.2.0'.freeze
4
4
  end
5
5
  end
data/lib/mimi/logger.rb CHANGED
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'logger'
5
+ require 'json'
3
6
  require 'mimi/core'
4
7
 
5
8
  module Mimi
@@ -7,17 +10,21 @@ module Mimi
7
10
  # Mimi::Logger is a preconfigured logger which outputs log messages to STDOUT.
8
11
  #
9
12
  class Logger
13
+ CONTEXT_ID_SIZE = 8 # bytes
14
+ CONTEXT_ID_THREAD_VARIABLE = 'mimi_logger_context_id'
15
+
10
16
  extend Forwardable
11
17
  include Mimi::Core::Module
12
18
 
13
19
  attr_reader :logger_instance
14
20
  delegate [
15
- :debug, :info, :warn, :error, :fatal, :unknown,
16
21
  :debug?, :info?, :warn?, :error?, :fatal?,
17
22
  :<<, :add, :log
18
23
  ] => :logger_instance
19
24
 
20
25
  default_options(
26
+ format: 'json', # 'string' or 'json'
27
+ log_context: true,
21
28
  level: 'info',
22
29
  cr_character: '↲' # alternative CR: ↵ ↲ ⏎
23
30
  )
@@ -36,7 +43,7 @@ module Mimi
36
43
  # logger.debug 'blabla' # => "D, blabla"
37
44
  #
38
45
  def initialize(*args)
39
- io = args.shift if args.first.is_a?(IO)
46
+ io = args.shift if args.first.is_a?(IO) || args.first.is_a?(StringIO)
40
47
  io ||= STDOUT
41
48
  opts = args.shift if args.first.is_a?(Hash)
42
49
  opts ||= {}
@@ -51,7 +58,7 @@ module Mimi
51
58
 
52
59
  # Returns the current log level
53
60
  #
54
- # @return [Fixnum]
61
+ # @return [Integer]
55
62
  #
56
63
  def level
57
64
  logger_instance.level
@@ -60,18 +67,41 @@ module Mimi
60
67
  # Sets the log level.
61
68
  # Allows setting the log level from a String or a Symbol, in addition to the standard ::Logger::INFO etc.
62
69
  #
63
- # @param [String,Symbol,Fixnum] value
70
+ # @param [String,Symbol,Integer] value
64
71
  #
65
72
  def level=(value)
66
73
  logger_instance.level = self.class.level_from_any(value)
67
74
  end
68
75
 
76
+ # Logs a new message at the corresponding logging level
77
+ #
78
+ #
79
+ #
80
+ def debug(*args, &block)
81
+ logger_instance.debug(args, &block)
82
+ end
83
+ def info(*args, &block)
84
+ logger_instance.info(args, &block)
85
+ end
86
+ def warn(*args, &block)
87
+ logger_instance.warn(args, &block)
88
+ end
89
+ def error(*args, &block)
90
+ logger_instance.error(args, &block)
91
+ end
92
+ def fatal(*args, &block)
93
+ logger_instance.fatal(args, &block)
94
+ end
95
+ def unknown(*args, &block)
96
+ logger_instance.unknown(args, &block)
97
+ end
98
+
69
99
  # Returns the log level inferred from value
70
100
  #
71
- # @param value [String,Symbol,Fixnum]
101
+ # @param value [String,Symbol,Integer]
72
102
  #
73
103
  def self.level_from_any(value)
74
- return value if value.is_a?(Fixnum)
104
+ return value if value.is_a?(Integer)
75
105
  case value.to_s.downcase.to_sym
76
106
  when :debug
77
107
  ::Logger::DEBUG
@@ -90,12 +120,138 @@ module Mimi
90
120
  end
91
121
  end
92
122
 
123
+ # Returns formatter Proc object depending on configured format
124
+ #
125
+ # @return [Proc]
126
+ #
93
127
  def self.formatter
128
+ case module_options[:format].to_s
129
+ when 'json'
130
+ formatter_json
131
+ when 'string'
132
+ formatter_string
133
+ else
134
+ raise "Invalid format specified for Mimi::Logger: '#{module_options[:format]}'"
135
+ end
136
+ end
137
+
138
+ # Returns formatter for 'json' format
139
+ #
140
+ # @return [Proc]
141
+ #
142
+ def self.formatter_json
94
143
  proc do |severity, _datetime, _progname, message|
95
- "#{severity.to_s[0]}, #{message.to_s.tr("\n", module_options[:cr_character])}\n"
144
+ h = formatter_message_args_to_hash(message)
145
+ h[:c] = context_id if module_options[:log_context]
146
+ h[:s] = severity.to_s[0]
147
+ h.to_json
148
+ end
149
+ end
150
+
151
+ # Returns formatter for 'string' format
152
+ #
153
+ # @return [Proc]
154
+ #
155
+ def self.formatter_string
156
+ proc do |severity, _datetime, _progname, message|
157
+ h = formatter_message_args_to_hash(message)
158
+ parts = []
159
+ parts << severity.to_s[0]
160
+ parts << context_id if module_options[:log_context]
161
+ parts << h[:m].to_s.tr("\n", module_options[:cr_character])
162
+ parts << '...' unless h.except(:m).empty?
163
+ parts.join(', ')
164
+ end
165
+ end
166
+
167
+ # Converts logger methods arguments passed in various forms to a message hash.
168
+ #
169
+ # Arguments to a logger may be passed in 6 different ways:
170
+ # @example
171
+ # logger.info('String')
172
+ # logger.info('String', param1: '..and a Hash')
173
+ # logger.info(m: 'Just a Hash', param1: 'with optional data')
174
+ #
175
+ # # and the same 3 ways in a block form
176
+ # logger.info { ... }
177
+ #
178
+ # This helper method converts all possible variations into one Hash, where key m: refers to the
179
+ # message and the rest are optional parameters passed in a Hash argument.
180
+ #
181
+ # @return [Hash]
182
+ #
183
+ def self.formatter_message_args_to_hash(message_args)
184
+ message_args = message_args.is_a?(String) || message_args.is_a?(Hash) ? [message_args] : message_args
185
+ if !message_args.is_a?(Array) || message_args.size > 2
186
+ raise ArgumentError, "Mimi::Logger arguments expected to be Array of up to 2 elements: #{message_args.inspect}"
96
187
  end
188
+
189
+ h = {}
190
+ arg1 = message_args.shift
191
+ arg2 = message_args.shift
192
+
193
+ if arg1.is_a?(String)
194
+ if arg2 && !arg2.is_a?(Hash)
195
+ raise ArgumentError, 'Mimi::Logger arguments are expected to be one of (<String>, <Hash>, [<String>, <Hash>])'
196
+ end
197
+ h = arg2.dup || {}
198
+ h[:m] = arg1
199
+ elsif arg1.is_a?(Hash)
200
+ if arg2
201
+ raise ArgumentError, 'Mimi::Logger arguments are expected to be one of (<String>, <Hash>, [<String>, <Hash>])'
202
+ end
203
+ h = arg1.dup
204
+ else
205
+ raise ArgumentError, 'Mimi::Logger arguments are expected to be one of (<String>, <Hash>, [<String>, <Hash>])'
206
+ end
207
+ h
208
+ end
209
+
210
+ # Returns current context ID if set, otherwise generates and sets a new context ID
211
+ # and returns it.
212
+ #
213
+ # Context ID is local to the current thread. It identifies a group of instructions
214
+ # happening within same logical context, as a single operation. For example, processing
215
+ # an incoming request may be seen as a single context.
216
+ #
217
+ # Context ID is logged with every message
218
+ #
219
+ # @return [String] a hex encoded context ID
220
+ #
221
+ def self.context_id
222
+ Thread.current[CONTEXT_ID_THREAD_VARIABLE] || new_context_id!
97
223
  end
98
224
 
225
+ def context_id
226
+ self.class.context_id
227
+ end
228
+
229
+ # Sets the new context ID to the given value
230
+ #
231
+ # @param id [String]
232
+ #
233
+ def self.context_id=(id)
234
+ Thread.current[CONTEXT_ID_THREAD_VARIABLE] = id
235
+ end
236
+
237
+ def context_id=(id)
238
+ self.class.context_id = id
239
+ end
240
+
241
+ # Generates a new random context ID and sets it as current
242
+ #
243
+ # @return [String] a new context ID
244
+ #
245
+ def self.new_context_id!
246
+ self.context_id = SecureRandom.hex(CONTEXT_ID_SIZE)
247
+ end
248
+
249
+ def new_context_id!
250
+ self.class.new_context_id!
251
+ end
252
+
253
+ # private-ish
254
+
99
255
  def self.module_manifest
100
256
  {
101
257
  log_level: {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mimi-logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Kukushkin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-05-17 00:00:00.000000000 Z
11
+ date: 2018-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mimi-core
@@ -123,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
123
  version: '0'
124
124
  requirements: []
125
125
  rubyforge_project:
126
- rubygems_version: 2.4.5.1
126
+ rubygems_version: 2.7.6
127
127
  signing_key:
128
128
  specification_version: 4
129
129
  summary: A pre-configured logger for microservice applications