mimi-logger 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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