mr_loga_loga 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ <div align="center">
2
+
3
+ # Mr. Loga Loga
4
+
5
+ <img alt="logo" src="logo.png" width="300px" height="auto">
6
+
7
+ ### The simply bombastic, fantastic logger for Ruby 💎
8
+
9
+ [![Gem Version](https://badge.fury.io/rb/mr_loga_loga.svg)](https://badge.fury.io/rb/mr_loga_loga)
10
+ [![Main](https://github.com/hschne/mr-loga-loga/actions/workflows/main.yml/badge.svg)](https://github.com/hschne/mr-loga-loga/actions/workflows/main.yml)
11
+ ![License](https://img.shields.io/github/license/hschne/mr-loga-loga)
12
+
13
+ ## What's this?
14
+
15
+ MrLogaLoga is a logger for Ruby that allows you to easily attach contextual information to your log messages. When writing logs, messages only tell half the story. MrLogaLoga allows you to make the most of your logs:
16
+
17
+ ```ruby
18
+ logger.info('message', user: 'name', data: 1)
19
+ # I, [2022-01-01T12:00:00.000000 #19074] INFO -- Main: message user=user data=1
20
+ ```
21
+
22
+ You can find out more about the motivation behind the project [here](#why-mrlogaloga). For usage read [Usage](#usage) or [Advanced Usage](#advanced-usage)
23
+
24
+ **Note**: This gem is in early development. Try it out and leave some feedback, it really goes a long way in helping me out with development. Any [feature request](https://github.com/hschne/mr-loga-loga/issues/new?assignees=&labels=type%3ABug&template=FEATURE_REQUEST.md&title=) or [bug report](https://github.com/hschne/mr-loga-loga/issues/new?assignees=&labels=type%3AEnhancement&template=BUG_REPORT.md&title=) is welcome. If you like this project, leave a star to show your support! ⭐
25
+
26
+ ## Getting Started
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem 'mr_loga_loga'
32
+ ```
33
+
34
+ And then execute:
35
+
36
+ ```
37
+ bundle install
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ MrLogaLoga provides the same interface as the Ruby default logger you are used to. In addition, however, you can attach contextual information to your log messages:
43
+
44
+ ```ruby
45
+ require 'mr_loga_loga'
46
+
47
+ logger = MrLogaLoga::Logger.new
48
+ logger.info('message', user: 'name', data: 1)
49
+ # I, [2022-01-01T12:00:00.000000 #19074] INFO -- Main: message user=user data=1
50
+ logger.context(class: 'classname').warn('message')
51
+ # W, [2022-01-01T12:00:00.000000 #19074] WARN -- Main: message class=classname
52
+ ```
53
+
54
+ To customize how log messages are formatted see [Formatters][#formatters].
55
+
56
+ ## Advanced Usage
57
+
58
+ MrLogaLoga provides a fluent interface to build log messages. For example, to attach an additional `user` field to a log message you can use any of the following:
59
+
60
+ ```ruby
61
+ logger.info('message', user: 'name')
62
+ logger.context(user: 'name').info('message') # Explicit context
63
+ logger.context { { user: 'name' } }.info('message') # Block context
64
+ logger.user('name').info('message') # Dynamic context method
65
+ logger.user { 'name' }.info('message') # Dynamic context block
66
+ ```
67
+
68
+ The block syntax [ is recommended when logging calculated properties ](https://ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger.html#class-Logger-label-How+to+log+a+message).
69
+
70
+ #### Shared Context
71
+
72
+ If multiple log messages within the same class should share a context include the `MrLogaLoga` module. Using `logger` will result in the defined context being included per default:
73
+
74
+ ```ruby
75
+ class MyClass
76
+ include MrLogaLoga
77
+
78
+ # This is the default. You may overwrite this in your own classes
79
+ def loga_context
80
+ { class_name: self.class.name }
81
+ end
82
+
83
+ def log
84
+ # This includes the class name in the log message now
85
+ logger.debug('debug') # debug class_name=MyClass
86
+ # Additional context will be merged
87
+ logger.debug('debug', user: 'user') # debug class_name=MyClass user=user
88
+ end
89
+ end
90
+ ```
91
+
92
+ When used with [Rails](#rails) logger will default to `Rails.logger`. If you use MrLogaLoga outside of Rails, you can either configure the logger instance to be used globally, or by overwriting the `loga_loga` method:
93
+
94
+ ```ruby
95
+ # In some configuration class
96
+ MrLogaLoga.configure do |configuration|
97
+ logger = MrLogaLoga::Logger.new($stdout)
98
+ end
99
+
100
+ # In the class where you do the logging itself
101
+ class MyClass
102
+ include MrLogaLoga
103
+
104
+ def loga_loga
105
+ MrLogaLoga::Logger.new($stdout)
106
+ end
107
+
108
+ def log
109
+ # ...
110
+ end
111
+ end
112
+ ```
113
+
114
+
115
+ ### Formatters
116
+
117
+ MrLogaLoga uses the [KeyValue](https://github.com/hschne/mr-loga-loga/blob/main/lib/mr_loga_loga/formatters/key_value.rb) formatter per default. The [Json](https://github.com/hschne/mr-loga-loga/blob/main/lib/mr_loga_loga/formatters/json.rb) formatter is also included. To use a specific formatter pass it to the logger constructor:
118
+
119
+ ```Ruby
120
+
121
+ MrLogaLoga::Logger.new(STDOUT, formatter: MrLogaLoga::Formatters::KeyValue.new)
122
+ ```
123
+
124
+ You can implement and add your own formatters like so:
125
+
126
+ ```ruby
127
+ class MyFormatter
128
+ def call(severity, datetime, progname, message, context)
129
+ context = context.map { |key, value| "#{key}=#{value}" }.compact.join(' ')
130
+ "#{severity} #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%6N')} #{progname} #{message} #{context}"
131
+ end
132
+ end
133
+
134
+ MrLogaLoga::Logger.new(STDOUT, formatter: MyFormatter.new)
135
+ ```
136
+
137
+ ### Rails
138
+
139
+ Using MrLogaLoga in Ruby on Rails is straightforward. Set up MrLogaLoga as logger in your `application.rb` or environment files and you are off to the races:
140
+
141
+ ```ruby
142
+ # application.rb
143
+ config.logger = MrLogaLoga::Logger.new(STDOUT)
144
+ config.log_level = :info
145
+ ```
146
+
147
+ Note that setting `config.log_formatter` does not work. You must set the formatter in the logger constructor as described in [Formatters](#formatters).
148
+
149
+ ### Lograge
150
+
151
+ [LogRage](https://github.com/roidrage/lograge) and MrLogaLoga work well together. When using both gems Lograge will be patched so that data will be available as `context` in MrLogaLoga. Make sure that MrLogaLoga is required **after** Lograge:
152
+
153
+ ```ruby
154
+ gem 'lograge'
155
+ gem 'mr_loga_loga'
156
+ ```
157
+
158
+ Note that Lograge's formatters won't be used. Use MrLogaLoga's own [formatters](#formatters) instead.
159
+
160
+ ## Why MrLogaLoga?
161
+
162
+ The more context your logs provide, the more use you will get out of them. The standard Ruby logger only takes a string as an argument, so you have to resort to something like this:
163
+
164
+ ```ruby
165
+ logger.debug("my message user=#{user} more_data=#{data}")
166
+ ```
167
+
168
+ This is fine, as long as you do not need to change your log format. Changing your log formatter will not change the format of your message, nor the formatting of the contextual information you provided.
169
+
170
+ MrLogaLoga addresses this by allowing you to attach contextual information to your logs and giving you full control over how both message and context are formatted.
171
+
172
+ ## Credit
173
+
174
+ This little library was inspired by [Lograge](https://github.com/roidrage/lograge) first and foremost. I would like to thank the amazing [@LenaSchnedlitz](https://twitter.com/LenaSchnedlitz) for the incredible logo! 🤩
175
+
176
+ ## Development
177
+
178
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests and linter. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
179
+
180
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
181
+
182
+ ## Contributing
183
+
184
+ Thank you for contributing! :heart:
185
+
186
+ We welcome all support, whether on bug reports, code, design, reviews, tests, documentation, translations, or just feature requests.
187
+
188
+ Please use [GitHub issues](https://github.com/hschne/rails-mini-profiler/issues) to submit bugs or feature requests.
189
+
190
+ ## License
191
+
192
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
193
+
194
+ ## Code of Conduct
195
+
196
+ Everyone interacting in the MrLogaLoga project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/hschne/mr_loga_loga/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'mr_loga_loga'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MrLogaLoga
4
+ # == Description
5
+ #
6
+ # The configuration class for MrLogaLoga
7
+ #
8
+ # == Usage
9
+ #
10
+ # MrLogaLoga.configure do |configuration|
11
+ # configuration.logger = ...
12
+ # end
13
+ class Configuration
14
+ attr_accessor :logger
15
+
16
+ # Initialize the configuration by setting configuration default values
17
+ def initialize(**kwargs)
18
+ reset
19
+ kwargs.each { |key, value| instance_variable_set("@#{key}", value) }
20
+ end
21
+
22
+ # Reset the configuration to default values
23
+ def reset
24
+ @logger = MrLogaLoga::Logger.new($stdout)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module MrLogaLoga
6
+ # == Description
7
+ #
8
+ # This class provides a fluent interface to attach contextual information to log messages.
9
+ class Context
10
+ def initialize(logger, context = {})
11
+ @logger = logger
12
+ @context = context
13
+ end
14
+
15
+ def context(context = {}, &block)
16
+ @context = merge_context(@context, context)
17
+ @context = merge_context(@context, block)
18
+ self
19
+ end
20
+
21
+ def add(severity, message = nil, **context, &block)
22
+ severity ||= UNKNOWN
23
+ return true unless @logger.log?(severity)
24
+
25
+ context = merge_context(@context, context)
26
+ context = context.call if context.is_a?(Proc)
27
+
28
+ @logger.add(severity, message, **context, &block)
29
+ end
30
+
31
+ alias log add
32
+
33
+ %i[debug info warn error fatal unknown].each do |symbol|
34
+ define_method(symbol) do |message = nil, **context, &block|
35
+ severity = Object.const_get("Logger::Severity::#{symbol.to_s.upcase}")
36
+ return true unless @logger.log?(severity)
37
+
38
+ context = merge_context(@context, context)
39
+ context = context.call if context.is_a?(Proc)
40
+ @logger.public_send(symbol, message, **context, &block)
41
+ end
42
+ end
43
+
44
+ def method_missing(symbol, *args, &block)
45
+ context = block ? -> { { symbol => block.call } } : { symbol => unwrap(args) }
46
+ @context = merge_context(@context, context)
47
+ self
48
+ end
49
+
50
+ def respond_to_missing?(name, include_private = false)
51
+ super(name, include_private)
52
+ end
53
+
54
+ private
55
+
56
+ def unwrap(args)
57
+ if args.size == 1
58
+ args[0]
59
+ else
60
+ args
61
+ end
62
+ end
63
+
64
+ def merge_context(original, new)
65
+ return original unless new
66
+
67
+ return original.merge(new) if original.is_a?(Hash) && new.is_a?(Hash)
68
+
69
+ merge_blocks(original, new)
70
+ end
71
+
72
+ def merge_blocks(original, new)
73
+ return -> { original.merge(new.call) } if original.is_a?(Hash) && new.is_a?(Proc)
74
+
75
+ return -> { original.call.merge(new) } if original.is_a?(Proc) && new.is_a?(Hash)
76
+
77
+ -> { original.call.merge(new.call) }
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'byebug'
4
+
5
+ module MrLogaLoga
6
+ module Adapters
7
+ # This patches Lograge to forward data as context to MrLogaLoga
8
+ module LogragePatch
9
+ class << self
10
+ def apply
11
+ return unless defined?(Lograge)
12
+
13
+ patch_applies = defined?(Lograge::LogSubscribers::Base) &&
14
+ Lograge::LogSubscribers::Base.private_method_defined?(:process_main_event)
15
+ unless patch_applies
16
+ puts 'WARNING: Failed to patch Lograge. It looks like '\
17
+ "MrLogaLoga's patch no longer applies in "\
18
+ "#{__FILE__}. Please contact MrLogaLoga maintainers."
19
+ return
20
+ end
21
+
22
+ Lograge::LogSubscribers::Base.prepend(self)
23
+ end
24
+ end
25
+
26
+ def process_main_event(event)
27
+ return if Lograge.ignore?(event)
28
+
29
+ payload = event.payload
30
+ data = extract_request(event, payload)
31
+ data = before_format(data, payload)
32
+ if logger.is_a?(MrLogaLoga::Logger)
33
+ logger.send(Lograge.log_level, '', **data)
34
+ else
35
+ formatted_message = Lograge.formatter.call(data)
36
+ logger.send(Lograge.log_level, formatted_message)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ MrLogaLoga::Adapters::LogragePatch.apply
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MrLogaLoga
4
+ module Formatters
5
+ # == Description
6
+ #
7
+ # A simple Json formatter for MrLogaLoga.
8
+ #
9
+ # == Format
10
+ #
11
+ # The json formatter renders messages into a single-line json. Context keys are embedded on the top level.
12
+ #
13
+ # Log Format:
14
+ #
15
+ # { "severity": "Severity", .. "message": "Message", "key1": "Key1" }
16
+ #
17
+ class Json < Logger::Formatter
18
+ # Render a log message in JSON
19
+ #
20
+ # @param severity [String] The message severity
21
+ # @param datetime [DateTime] The message date time
22
+ # @param progname [DateTime] The program name
23
+ # @param message [String] The log message
24
+ # @param context [Hash] The log message context
25
+ #
26
+ # @return [String] the formatted log message
27
+ def call(severity, datetime, progname, message, context)
28
+ message = message.nil? || message.empty? ? nil : msg2str(message)
29
+
30
+ message_hash = {
31
+ severity: severity,
32
+ datetime: datetime.strftime('%Y-%m-%dT%H:%M:%S.%6N'),
33
+ pid: Process.pid,
34
+ progname: progname,
35
+ message: message,
36
+ **context
37
+ }.compact
38
+ "#{message_hash.to_json}\n"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MrLogaLoga
4
+ module Formatters
5
+ # == Description
6
+ #
7
+ # A simple key value formatter that extends the standard formatter by rendering additional contextual information.
8
+ #
9
+ # == Format
10
+ #
11
+ # The key-value formatter renders messages into the following format:
12
+ #
13
+ # Log format:
14
+ #
15
+ # SeverityID, [DateTime #pid] SeverityLabel -- ProgName: message key1=value1 key2=value2
16
+ #
17
+ class KeyValue < Logger::Formatter
18
+ # Render a log message
19
+ #
20
+ # @param severity [String] The message severity
21
+ # @param datetime [DateTime] The message date time
22
+ # @param progname [DateTime] The program name
23
+ # @param message [String] The log message
24
+ # @param context [Hash] The log message context
25
+ #
26
+ # @return [String] the formatted log message
27
+ def call(severity, datetime, progname, message, context)
28
+ message = context.map { |key, value| "#{key}=#{value}" }
29
+ .prepend(message)
30
+ .compact
31
+ .join(' ')
32
+ super(severity, datetime, progname, message)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MrLogaLoga
4
+ # == Description
5
+ #
6
+ # Instance methods to be attached when including the main module.
7
+ #
8
+ # @api private
9
+ module InstanceMethods
10
+ def loga_context
11
+ { class_name: self.class.name }
12
+ end
13
+
14
+ def logger
15
+ MrLogaLoga::LoggerProxy.new(loga_loga, -> { loga_context })
16
+ end
17
+
18
+ def loga_loga
19
+ @loga_loga ||= if defined?(Rails.application.logger)
20
+ Rails.application.logger
21
+ else
22
+ MrLogaLoga.configuration.logger
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module MrLogaLoga
6
+ # == Description
7
+ #
8
+ # This class extends the default Ruby Logger to allow users to attach contextual information to log messages.
9
+ #
10
+ # === Example
11
+ #
12
+ # This creates a Logger that outputs to the standard output stream, with a
13
+ # level of +WARN+:
14
+ #
15
+ # require 'mr_loga_loga'
16
+ #
17
+ # logger = MrLogaLoga::Logger.new(STDOUT)
18
+ # logger.level = Logger::WARN
19
+ #
20
+ # logger.debug("Default")
21
+ # logger.context(user: 1).debug('with context')
22
+ class Logger < ::Logger
23
+ def context(**kwargs, &block)
24
+ context = block ? -> { kwargs.merge(block.call) } : kwargs
25
+ Context.new(self, context)
26
+ end
27
+
28
+ def message(message, &block)
29
+ message ||= block
30
+ Message.new(self, message)
31
+ end
32
+
33
+ def add(severity, message = nil, progname = nil, **context, &block)
34
+ severity ||= UNKNOWN
35
+ return true unless log?(severity)
36
+
37
+ message = block.call if block
38
+ @logdev.write(format(format_severity(severity), Time.now, progname, message, context))
39
+ true
40
+ end
41
+
42
+ alias log add
43
+
44
+ %i[debug info warn error fatal unknown].each do |symbol|
45
+ define_method(symbol) do |message = nil, **context, &block|
46
+ # Map the symbol (e.g. :debug) to the severity constant (e.g. DEBUG)
47
+ severity = Object.const_get("Logger::Severity::#{symbol.to_s.upcase}")
48
+ add(severity, message, **context, &block)
49
+ end
50
+ end
51
+
52
+ def method_missing(symbol, *args, &block)
53
+ context = block ? -> { { symbol => block.call } } : { symbol => unwrap(args) }
54
+ Context.new(self, context)
55
+ end
56
+
57
+ def respond_to_missing?(name, include_private = false)
58
+ super(name, include_private)
59
+ end
60
+
61
+ def log?(severity)
62
+ !@logdev.nil? && severity >= level
63
+ end
64
+
65
+ private
66
+
67
+ def unwrap(args)
68
+ if args.size == 1
69
+ args[0]
70
+ else
71
+ args
72
+ end
73
+ end
74
+
75
+ def format(severity, datetime, progname, message, context)
76
+ formatter.call(severity, datetime, progname, message, context)
77
+ end
78
+
79
+ def formatter
80
+ @formatter ||= MrLogaLoga::Formatters::KeyValue.new
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module MrLogaLoga
6
+ # == Description
7
+ #
8
+ # A proxy that attaches contextual information to the underlying logger when called.
9
+ #
10
+ # @api private
11
+ class LoggerProxy
12
+ def initialize(logger, context_proc)
13
+ @logger = logger
14
+ @context_proc = context_proc
15
+ end
16
+
17
+ def add(severity, message = nil, **context, &block)
18
+ severity ||= UNKNOWN
19
+ return true unless @logger.log?(severity)
20
+
21
+ context = @context_proc.call.merge(context)
22
+
23
+ @logger.add(severity, message, **context, &block)
24
+ end
25
+
26
+ alias log add
27
+
28
+ %i[debug info warn error fatal unknown].each do |symbol|
29
+ define_method(symbol) do |message = nil, **context, &block|
30
+ severity = Object.const_get("Logger::Severity::#{symbol.to_s.upcase}")
31
+ return true unless @logger.log?(severity)
32
+
33
+ context = @context_proc.call.merge(context)
34
+
35
+ @logger.public_send(symbol, message, **context, &block)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MrLogaLoga
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'byebug'
4
+
5
+ require_relative 'mr_loga_loga/version'
6
+ require_relative 'mr_loga_loga/configuration'
7
+ require_relative 'mr_loga_loga/logger_proxy'
8
+ require_relative 'mr_loga_loga/instance_methods'
9
+ require_relative 'mr_loga_loga/context'
10
+ require_relative 'mr_loga_loga/logger'
11
+ require_relative 'mr_loga_loga/formatters/key_value'
12
+ require_relative 'mr_loga_loga/formatters/json'
13
+
14
+ require_relative 'mr_loga_loga/extensions/lograge_patch'
15
+
16
+ # == Description
17
+ #
18
+ # The MrLogaLoga module provides additional logging functionality when included in your classes.
19
+ #
20
+ module MrLogaLoga
21
+ class Error < StandardError; end
22
+
23
+ def self.included(base)
24
+ base.send :include, InstanceMethods
25
+ end
26
+
27
+ class << self
28
+ # Create a new configuration object
29
+ #
30
+ # @return [Configuration] a new configuration
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def configure
36
+ yield(configuration)
37
+ end
38
+ end
39
+ end
data/logo.png ADDED
Binary file